2 ポイント 投稿者 GN⁺ 2024-11-04 | 1件のコメント | WhatsAppで共有
  • Webサービスで一般的な PostgreSQL+Redis の組み合わせは便利だが、バックグラウンドジョブキュー・分散ロック・Pub/Sub の一部は PostgreSQL だけでも処理可能
  • PostgreSQL 9.5 の FOR UPDATE SKIP LOCKED は、ロックされた行を待たずに飛ばすことで、複数ワーカーが同じジョブを取得する状況を防ぐキュー実装に活用できる
  • アプリケーションレベルの 分散ロック は PostgreSQL の advisory locks で実装でき、内部ロックエンジンをアプリケーション定義の目的に再利用する
  • PostgreSQL 9 の LISTEN/NOTIFY は任意の文字列チャネルの購読と通知送信をサポートし、Pub/Sub レイヤーとして使える。Rails ActionCable も PostgreSQL の利用を標準でサポートしている
  • Redis は TTL キャッシュと一時データ操作で依然として強みを持つが、一部のシステムでは Redis 依存を減らして 運用コストと開発の複雑さ を下げられる

PostgreSQLに吸収できるRedisの役割

  • 一般的なWebサービスは PostgreSQL をデータストアとして使い、Redis をバックグラウンドジョブキューの調整や限定的な原子的操作に利用する
  • Redis 自体は有用だが、この組み合わせで Redis が担う一部の役割は PostgreSQL の機能だけでも置き換え可能

バックグラウンドジョブキュー

  • Redis は Web サービスでバックグラウンドワーカープールへジョブを渡す ジョブキューの調整 によく使われる
    • 実行すべきバックグラウンドジョブと入力データを記録する
    • 複数ワーカーのうち1つだけがそのジョブを取得することを保証する必要がある
    • Redis はデータ構造に対する原子的操作が豊富で、この用途に適している
  • PostgreSQL 9.5 以降では SELECT ... FOR ... 文の SKIP LOCKED オプションを使える
    • このオプションを指定すると、PostgreSQL はロック解除待ちになる行を無視する
    • FOR UPDATE SKIP LOCKED を指定すると、返された行に対して 行レベルロック が暗黙的に取得される
    • SKIP LOCKED により、他トランザクションのロックでブロックされる可能性がない
    • 処理すべき別のジョブがあれば、そのジョブが返される
    • 複数ワーカーが同じコマンドを実行しても、行レベルロックのため同じ行を受け取らない
  • 基本的な流れは、トランザクション内で pending 状態のジョブを1つ選び、そのジョブを running に変更して返す方式
    • BEGIN
    • SELECT ... FOR UPDATE SKIP LOCKED
    • UPDATE jobs SET status = 'running' ... RETURNING jobs.*
    • COMMIT
  • 注意点として、ワーカー数もジョブ数も多い場合、キューを走査しながらロック取得を試みるコストが大きくなる可能性がある
    • 実際に扱ってきた大半のアプリではバックグラウンドワーカーが12未満だったため、このコストは大きくない可能性が高かった

アプリケーションロック

  • サードパーティサービスと同期するルーチンのように、全サーバープロセスで特定ユーザーについて1インスタンスだけ実行されるべきケースがある
  • こうした 分散ロック は Redis のもう1つの一般的な利用例
  • PostgreSQL は advisory locks によって同じ目的を達成できる
    • advisory locks は PostgreSQL が内部的に使用しているロックエンジンを、アプリケーション定義の目的に活用できるようにする

Pub/Sub

  • アクティブなクライアントにイベントをプッシュする際にも Redis がよく使われる
    • ユーザーが新しいメッセージを読めることを通知する
    • データの準備ができ次第、クライアントへストリーミングする
    • 一般的には WebSocket がイベント配信レイヤーで、Redis が Pub/Sub エンジン の役割を担う
  • PostgreSQL 9 以降は LISTENNOTIFY 文で Pub/Sub 機能を提供する
    • PostgreSQL クライアントは、任意の文字列である特定のメッセージチャネルを LISTEN で購読できる
    • 他のクライアントがそのチャネルに NOTIFY を送ると、購読中のすべてのクライアントが通知を受け取る
    • 必要に応じて小さなメッセージを添付することもできる
  • Rails と ActionCable を使っているなら、PostgreSQL の利用が標準でサポートされている

Redisを残しておく価値のある領域

  • Redis は PostgreSQL とは異なる領域を担い、PostgreSQL が目指していない作業で強みを持つ
    • TTL のある データキャッシュ
    • 一時データの保存と操作
  • PostgreSQL は、単純な SQL データベースや ORM の背後にある不透明な構成要素として見るには、機能の幅が広い
  • Redis に任せている作業の一部は PostgreSQL にも適している可能性がある
  • Redis を省略すれば、複数のデータサービスに依存するときに生じる 運用コスト と開発の複雑さを減らす選択肢になりうる

1件のコメント

 
GN⁺ 2024-11-04
Hacker News の意見
  • みんなが過度に分散したアーキテクチャにこだわるため、Redis の本当の利点を見落とすことが多い。アプリケーションと同じマシンで動かせば 1 ミリ秒よりはるかに短い時間で応答できるので、Postgres では難しいことをアプリケーションでできるようになる
    Postgres が優れているのは確かだが、アプリケーションと同じマシンのメモリ上で動くわけではない。キューのようなものだけが必要なら、インメモリのキー・バリューストアは不要かもしれない。インメモリのキー・バリューストアの核心は、RAM の性能特性が必要な処理を行うことにあり、その特性はネットワーク接続越しには得られない

    • ローカルプロセス 1 つだけがマシン内の Redis をインメモリキャッシュとして使うなら、単に使用しているプログラミング言語のデータ構造を使うほうがよい
    • ローカルで実行したときに Postgres と Redis のオーバーヘッドがどれほど違うのか気になる。なぜ Postgres はローカルで動かないと見なされるのかも分からない
      Postgres に特別な魔法があるわけではなく、Redis と同じく別プロセスで動くプログラムにすぎない。ローカル接続では遅延を減らすために高速なパイプを使い、さらに高速なバルクデータ転送方式も使える。こうした形で何度も使ってきた
    • Django には Redis をサポートするキャッシュが組み込まれており、インメモリキャッシュのオプションもあるが、「本番向けではない」と表示されている。Django インスタンスが複数あると、それぞれのインメモリキャッシュが互いに異なるためだ
      しかし社内業務ツールのような場合は、単一インスタンスを長く大きくして使うことができ、このインメモリキャッシュのおかげで非常に高速になる。django-cachalot は、テーブルへの書き込みが発生するたびにキャッシュ無効化を自動処理するライブラリだ。やや粗い方式だが、ほとんど手間なく性能向上を得られ、更新が少ない社内業務アプリは実質的に RAM 上で動き、キャッシュにないときだけ通常のデータベースクエリに戻る
      https://github.com/noripyt/django-cachalot
    • この構成で Redis より速いのはハッシュマップだ。Postgres がアプリと同じサーバーで動けない理由もなく、実際かなり一般的な構成だ
    • 過剰設計と拙速な分散化は実際の問題だが、Redis は「Remote Dictionary Server」の略だ。本来の目的はローカルで動かすことではまったくない。もちろん、言語標準の辞書が範囲クエリをサポートしない場合のように、ローカル実行も正当な設計上の選択になり得る
  • ここでの議論は Redis の観点からの防御的な話が多いが、もちろん Redis のほうが優れている特定の領域はある
    ただし、記事の核心はそこではないと思う。要点は「PostgreSQL は、単なる SQL データベースや ORM の背後に隠れた神秘的な存在として扱うときに想像するより、はるかに多くの機能を持っている」という一文に要約できる。データベースをORM の背後でだけ使っているなら、どのデータベースであっても機能を見逃している可能性が高い。Redis のような別サービスを追加する必要があるなら、新しい依存関係を増やすより、すでに構成済みのデータベースを使うほうがよい場合もある

  • Postgres に何ができるかを理解するのはよいことだ。強力なデータベースである
    反論としては、Redis を使い始めるハードルは非常に低く、その見返りとして高い性能、豊富なライブラリサポート、メインデータベースの負荷軽減が得られるという点がある。例えば API レスポンスキャッシュを Postgres で作ることはできる。TTL は cron ジョブで古いキャッシュ値を掃除すればよい。あるいは単に Redis を使えばよい
    アドバイザリロックは優れていて有用だが、PgBouncer のようなものを使う場合、セッションアドバイザリロックとトランザクションのインターリービングの間で問題が起きる可能性がある。別システムにはネットワーク呼び出し、可用性、ドメイン知識といった欠点があるが、Redis 程度のトレードオフはかなり小さい

    • 本番環境で管理するものが 1 つ減るのは利点だ。PostgreSQL から始め、性能・スケール・コストのために必要になったときに特化したシステムを追加すればよい
  • かなり古い記事だが、今では非常に一般的なパターンになっている。メール送信やレポート生成用のジョブキューだけが必要なプロジェクトの 90% は毎秒数百万メッセージを処理するわけではないので、スタックを単純化する方法は検討する価値がある
    Celery で遭遇した問題を回避するためにこのパターンをよく使っていて、別フレームワークとして切り出した: https://github.com/TkTech/chancy フィードバック歓迎
    この種のツールは多く、そのうちいくつかは商用サービスなので、確かな需要がある
    https://worker.graphile.org/ (Node.js)
    https://riverqueue.com/ (Go)
    https://github.com/acaloiaro/neoq (Go)
    https://github.com/contribsys/faktory (Go)
    https://github.com/sorentwo/oban (Elixir)
    https://github.com/procrastinate-org/procrastinate (Python)

    • シンプルなジョブキューを探していた。Huey も悪くないが、すでに Postgres を使っており、キューに入れるジョブは 1 時間に 1 回程度なので、Redis ベースはいつも過剰に感じていた
    • Node 側には PG-Boss もある: https://github.com/timgit/pg-boss
  • PGQueuer は PostgreSQL の FOR UPDATE SKIP LOCKEDLISTEN/NOTIFY を使い、ジョブキュー、ロック、リアルタイム通知を提供する
    すでに PostgreSQL を使っている場合、Redis が不要なミニマルな代替手段だ
    https://github.com/janbjorge/PGQueuer
    ちなみに私が作ったものだ

  • Postgres は好きだが、いくつか限界がある
    キー・バリュー・ストアが必要なら、autovacuum を理解しているか、コネクションプールの限界を理解しているか、スループットと安全性のどちらを求めるのかを考える必要がある。キューが必要なら、順次処理なのか、レート制限があるのか、ファンアウトなのか、トピック別に分離するのかを見る必要がある。Publish/Subscribe が必要なら、重複受信を気にするのか、メッセージロスを気にするのか、リプレイが必要なのかを検討する必要がある。ロックが必要なら、コネクションプールの限界と statement_timeout を理解しておく必要がある。上記の問題のほとんどは解決可能だが、それほど単純ではない

    • 長時間実行されるトランザクションが vacuum の タプル削除を妨げるときに、実装が破綻しないかも見る必要がある
  • Postgres の Publish/Subscribe における大きな障害は、メッセージの最大サイズが 8000バイトであること
    推奨される回避策は、データをテーブルに入れて ID だけを送る方式だが、永遠に残したくないならそのデータをガベージコレクションする必要があり、メッセージごとに作業が増える。もちろん問題ない場合もあるが、多くの Redis のユースケースでは、この制限のために同等とは見なしにくい

  • pgsql が クライアント接続 15000個を処理できるか見てみよう

    • Postgres と MySQL の差は、ここでは本当に驚くほど大きい
      Planetscale の人がポッドキャストで、GitHub の MySQL インスタンスはそれぞれ 5万以上の接続を処理すると言っていた。一方、Postgres で 100 を超える接続が必要なら、すでに PgBouncer が必要になる
    • コネクションプーリングを使えばよい。AWS RDS Proxy のように数クリックで使える
  • キュー、ロック、Publish/Subscribe は可能だ。しかし Redis の最も重要な用途である キャッシュが抜けている
    Postgres の更新は挿入より高コストなことで悪名高く、ゴミを生み、vacuum が必要になる。キャッシュには重要ではない耐久性保証のために、書き込みもかなり遅くなる。自動期限切れは非常に便利で、ミスを減らしてくれる

    • 不要な機能はほとんどオフにできる。たとえばテーブルを unlogged にできる: https://www.postgresql.org/docs/current/sql-createtable.html...
      同期コミットや autovacuum などもオフにできる。もちろん Redis のほうが依然として高速だろうが、一般的な会社が気にするほどの差ではないかもしれない
  • 記事の要旨をより明確に言うと、Postgres で始め、必要が出てきたら Redis に移せばよい
    動く部品の数はできるだけ少なく保つのがよい

    • Redis はデプロイが特に難しいわけではない。後でどんな予期せぬ副作用が出るかわからない移行を計画するくらいなら、最初から Redis を使うのも悪くない