Redisは必要か? PostgreSQLがキューイング、ロック、Pub/Subを提供する(2021年)
(spin.atomicobject.com)- 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に変更して返す方式BEGINSELECT ... FOR UPDATE SKIP LOCKEDUPDATE 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 以降は
LISTENとNOTIFY文で Pub/Sub 機能を提供する- PostgreSQL クライアントは、任意の文字列である特定のメッセージチャネルを
LISTENで購読できる - 他のクライアントがそのチャネルに
NOTIFYを送ると、購読中のすべてのクライアントが通知を受け取る - 必要に応じて小さなメッセージを添付することもできる
- PostgreSQL クライアントは、任意の文字列である特定のメッセージチャネルを
- Rails と ActionCable を使っているなら、PostgreSQL の利用が標準でサポートされている
Redisを残しておく価値のある領域
- Redis は PostgreSQL とは異なる領域を担い、PostgreSQL が目指していない作業で強みを持つ
- TTL のある データキャッシュ
- 一時データの保存と操作
- PostgreSQL は、単純な SQL データベースや ORM の背後にある不透明な構成要素として見るには、機能の幅が広い
- Redis に任せている作業の一部は PostgreSQL にも適している可能性がある
- Redis を省略すれば、複数のデータサービスに依存するときに生じる 運用コスト と開発の複雑さを減らす選択肢になりうる
1件のコメント
Hacker News の意見
みんなが過度に分散したアーキテクチャにこだわるため、Redis の本当の利点を見落とすことが多い。アプリケーションと同じマシンで動かせば 1 ミリ秒よりはるかに短い時間で応答できるので、Postgres では難しいことをアプリケーションでできるようになる
Postgres が優れているのは確かだが、アプリケーションと同じマシンのメモリ上で動くわけではない。キューのようなものだけが必要なら、インメモリのキー・バリューストアは不要かもしれない。インメモリのキー・バリューストアの核心は、RAM の性能特性が必要な処理を行うことにあり、その特性はネットワーク接続越しには得られない
Postgres に特別な魔法があるわけではなく、Redis と同じく別プロセスで動くプログラムにすぎない。ローカル接続では遅延を減らすために高速なパイプを使い、さらに高速なバルクデータ転送方式も使える。こうした形で何度も使ってきた
しかし社内業務ツールのような場合は、単一インスタンスを長く大きくして使うことができ、このインメモリキャッシュのおかげで非常に高速になる。django-cachalot は、テーブルへの書き込みが発生するたびにキャッシュ無効化を自動処理するライブラリだ。やや粗い方式だが、ほとんど手間なく性能向上を得られ、更新が少ない社内業務アプリは実質的に RAM 上で動き、キャッシュにないときだけ通常のデータベースクエリに戻る
https://github.com/noripyt/django-cachalot
ここでの議論は Redis の観点からの防御的な話が多いが、もちろん Redis のほうが優れている特定の領域はある
ただし、記事の核心はそこではないと思う。要点は「PostgreSQL は、単なる SQL データベースや ORM の背後に隠れた神秘的な存在として扱うときに想像するより、はるかに多くの機能を持っている」という一文に要約できる。データベースをORM の背後でだけ使っているなら、どのデータベースであっても機能を見逃している可能性が高い。Redis のような別サービスを追加する必要があるなら、新しい依存関係を増やすより、すでに構成済みのデータベースを使うほうがよい場合もある
Postgres に何ができるかを理解するのはよいことだ。強力なデータベースである
反論としては、Redis を使い始めるハードルは非常に低く、その見返りとして高い性能、豊富なライブラリサポート、メインデータベースの負荷軽減が得られるという点がある。例えば API レスポンスキャッシュを Postgres で作ることはできる。TTL は cron ジョブで古いキャッシュ値を掃除すればよい。あるいは単に Redis を使えばよい
アドバイザリロックは優れていて有用だが、PgBouncer のようなものを使う場合、セッションアドバイザリロックとトランザクションのインターリービングの間で問題が起きる可能性がある。別システムにはネットワーク呼び出し、可用性、ドメイン知識といった欠点があるが、Redis 程度のトレードオフはかなり小さい
かなり古い記事だが、今では非常に一般的なパターンになっている。メール送信やレポート生成用のジョブキューだけが必要なプロジェクトの 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)
PGQueuer は PostgreSQL の FOR UPDATE SKIP LOCKED と LISTEN/NOTIFY を使い、ジョブキュー、ロック、リアルタイム通知を提供する
すでに PostgreSQL を使っている場合、Redis が不要なミニマルな代替手段だ
https://github.com/janbjorge/PGQueuer
ちなみに私が作ったものだ
Postgres は好きだが、いくつか限界がある
キー・バリュー・ストアが必要なら、autovacuum を理解しているか、コネクションプールの限界を理解しているか、スループットと安全性のどちらを求めるのかを考える必要がある。キューが必要なら、順次処理なのか、レート制限があるのか、ファンアウトなのか、トピック別に分離するのかを見る必要がある。Publish/Subscribe が必要なら、重複受信を気にするのか、メッセージロスを気にするのか、リプレイが必要なのかを検討する必要がある。ロックが必要なら、コネクションプールの限界と statement_timeout を理解しておく必要がある。上記の問題のほとんどは解決可能だが、それほど単純ではない
Postgres の Publish/Subscribe における大きな障害は、メッセージの最大サイズが 8000バイトであること
推奨される回避策は、データをテーブルに入れて ID だけを送る方式だが、永遠に残したくないならそのデータをガベージコレクションする必要があり、メッセージごとに作業が増える。もちろん問題ない場合もあるが、多くの Redis のユースケースでは、この制限のために同等とは見なしにくい
pgsql が クライアント接続 15000個を処理できるか見てみよう
Planetscale の人がポッドキャストで、GitHub の MySQL インスタンスはそれぞれ 5万以上の接続を処理すると言っていた。一方、Postgres で 100 を超える接続が必要なら、すでに PgBouncer が必要になる
キュー、ロック、Publish/Subscribe は可能だ。しかし Redis の最も重要な用途である キャッシュが抜けている
Postgres の更新は挿入より高コストなことで悪名高く、ゴミを生み、vacuum が必要になる。キャッシュには重要ではない耐久性保証のために、書き込みもかなり遅くなる。自動期限切れは非常に便利で、ミスを減らしてくれる
同期コミットや autovacuum などもオフにできる。もちろん Redis のほうが依然として高速だろうが、一般的な会社が気にするほどの差ではないかもしれない
記事の要旨をより明確に言うと、Postgres で始め、必要が出てきたら Redis に移せばよい
動く部品の数はできるだけ少なく保つのがよい