【ITニュース解説】Stop Routing Reads to Primary: Building Causal Consistency in Java with Postgres LSN Tokens
2026年10月19日に「Dev.to」が公開したITニュース「Stop Routing Reads to Primary: Building Causal Consistency in Java with Postgres LSN Tokens」について初心者にもわかりやすく解説しています。
ITニュース概要
大規模システムで読み込みをプライマリDBに集中させると非効率。DBのLSNトークンでレプリカDBの更新追いつきを確認。追いついていればレプリカから、遅れていればプライマリから読み込む。負荷を分散し、システム性能とコスト効率を高める。
ITニュース解説
Webシステムを構築する際、データベースは非常に重要な役割を担う。多くのユーザーが同時にサービスを利用すると、データベースへのアクセスが集中し、システム全体の処理が遅くなることがある。特に、データベースへのデータの書き込み(更新や追加など)とデータの読み込み(表示など)は異なる性質を持つため、これらを効率的に処理し、システムをスムーズに動かし続けるための工夫が求められる。
一般的なデータベースの構成では、データの書き込みと全ての読み込みを「プライマリ」(主データベース)と呼ばれる一台のサーバーが担当することが多い。しかし、サービスが大規模になりユーザーが増加すると、プライマリへの負荷が極端に高まり、システムのボトルネックとなる。この問題を解決するためによく用いられるのが、「リードレプリカ」と呼ばれるプライマリのコピーを作成し、読み込み処理をリードレプリカに分散させる方法である。リードレプリカはプライマリと常にデータを同期しており、読み込み専用として利用することで、プライマリの負荷を大きく軽減し、システムの処理能力(スケーラビリティ)を向上させることができる。
しかし、リードレプリカを利用する際には一つの課題がある。それは、プライマリでデータが更新されても、その変更がリードレプリカに反映されるまでにわずかな時間差(「レプリケーション遅延」と呼ぶ)が生じる可能性があることだ。もしユーザーがデータを書き込んだ直後にリードレプリカからそのデータを読み込もうとすると、まだ変更が反映されていない「古いデータ」が表示されてしまうかもしれない。例えば、ECサイトで商品をカートに入れたのに、ページを更新してもカートに商品が表示されない、といった状況だ。これはユーザー体験を損なうだけでなく、システムの信頼性にも関わる問題となる。
このような状況を避けるために重要なのが「因果一貫性」という考え方である。因果一貫性とは、「ある操作(原因)が完了した後に発生する別の操作(結果)は、原因となった操作の結果を反映した状態を見るべきだ」という一貫性のレベルを指す。特に、システムエンジニアがよく耳にする「Read-Your-Own-Writes」(自分の書き込みは自分自身がすぐに読める)という特性は、因果一貫性の一種である。ユーザーが自分で更新したデータをすぐに確認できることは、多くのWebサービスにおいて必須の要件となる。
多くの開発者がこの問題に対処する際に誤ったアプローチを取ってしまうことがある。一つは「TTL pinning」と呼ばれる方法で、ユーザーがデータを書き込んだ後、例えば「5秒間だけ」は必ずプライマリからデータを読み込むようにするというものだ。これは一時的には有効だが、トラフィックが急増した際にプライマリへの負荷を集中させ、プライマリが停止するなどの問題を引き起こす可能性がある。また、レプリカの読み込み処理能力を十分に活用できないため、コスト効率も悪くなる。別の誤解として、データベースの「ウォーターマーク」というレプリケーションの進捗を示す情報を無視し、レプリケーション遅延を単なる不確定なものとして扱う点が挙げられる。さらに、分散システムで使われるような複雑な「分散ロック」などの仕組みを用いて因果一貫性を実現しようとすると、過剰な設計となり、システムが不要に複雑化し、開発や運用コストが増大する。
そこで、記事で提案されている「正しい方法」は、Postgresのようなデータベースが内部で持っている「ログシーケンス番号(LSN)」という情報を活用するものである。LSNは、データベースの更新履歴(WAL: Write-Ahead Log)における各変更の位置を示すユニークな識別子のようなものだ。
この方法の具体的な手順は次のようになる。
- 書き込み時にLSNを取得する:ユーザーがデータをデータベースに書き込む(更新、追加など)際に、その書き込み処理が完了した時点のLSN(Postgresであれば
pg_current_wal_lsn()で取得できる)をデータベースから取得する。 - クライアントにLSNを返す:アプリケーションは、書き込み処理が成功したことをユーザーに通知するHTTPレスポンスのヘッダーなどに、このLSNを含めてクライアント(ユーザーのウェブブラウザやモバイルアプリなど)に返す。
- 後続の読み込みリクエストにLSNを含める:クライアントは、次に同じユーザーがデータを読み込もうとする際、以前受け取ったLSNをHTTPリクエストのヘッダーなどに含めてサーバーに送る。
- リードレプリカのLSNを確認する:サーバーは、このLSNを含んだ読み込みリクエストを受け取ると、データを提供するリードレプリカが、リクエストに含まれるLSNが示す時点の書き込みまで追いついているかを確認する。これは、リードレプリカが現在どこまでプライマリの変更を反映したかを示す「ウォーターマーク」(Postgresであれば
pg_last_wal_replay_lsn()で取得できる)と比較することで行う。 - 適切なデータベースにルーティングする:
- もしリードレプリカがリクエストのLSNまで追いついていれば、リードレプリカからデータを読み込む。
- もしリードレプリカが追いついていない、つまりまだデータが反映されていない可能性があれば、一時的にプライマリからデータを読み込む。
この仕組みは、JavaのSpringフレームワークにおけるAbstractRoutingDataSourceのような機能を使って実装できる。これは、実行時にどのデータベース(プライマリかリードレプリカか)にアクセスするかを動的に切り替えるための仕組みである。CausalReadDataSourceRouterというカスタムクラスを作成し、リクエストにLSNが含まれているかどうか、そしてリードレプリカがそのLSNまで追いついているかどうかを確認し、適切なデータベースを選択してルーティングする。
このアプローチの最大の利点は、個々のユーザーのセッション内で「自分の書き込みを自分がすぐに読める」という因果一貫性を、システム全体に複雑な一貫性制御を課すことなく実現できる点にある。各クライアントが自分の操作のタイムライン(LSNトークン)を追跡することで、グローバルな同期のボトルネックを回避しつつ、正しいデータを提供できる。
また、レプリケーションの遅延によってリードレプリカが追いついていない場合でも、すぐにプライマリにフォールバックする(「ヘッジング」と呼ぶ)仕組みがあるため、ユーザーはデータの読み込みが遅延することなく、常に最新の自分の操作結果を確認できる。これにより、パフォーマンスの劣化を最小限に抑えつつ、高い一貫性を保証できる。
結論として、LSNトークンを活用したこの手法は、プライマリへの不要なリードトラフィックを減らし、リードレプリカを最大限に活用することで、システムの高いスケーラビリティとコスト効率を実現する上で非常に効果的である。同時に、ユーザーにとっては自分の操作が即座に反映されるという、自然で信頼性の高いユーザー体験を提供することが可能となる。システムエンジニアにとって、大規模なWebサービスを設計・構築する上で理解しておくべき重要なアーキテクチャパターンの一つである。