世界を動かす技術を、日本語で。

Postgresの書き込みを高速化しましたが、レプリケーションが壊れました

概要

Postgresの拡張機能pg_searchで書き込み性能向上のためLSMツリーを導入したが、物理レプリケーションが破損する問題が発生。 LSMツリーの特性とPostgresのWALによる一貫性確保の仕組みを解説。 物理・論理一貫性の違いと、VACUUMや圧縮処理での課題を説明。 hot_standby_feedback設定により論理一貫性を守る方法を提案。 最終的に、高速な書き込みと一貫性を両立する設計に到達。

Postgres書き込み高速化とレプリケーション破損

  • pg_search はElasticsearch代替を目指すPostgres拡張機能
  • リアルタイム分析や検索 用途で高い書き込み性能が必須
  • 標準のB-treeやGINインデックスは 書き込みが遅い 欠点
  • Log-Structured Merge(LSM)ツリー を採用し書き込み最適化
  • LSMツリー導入で 物理レプリケーションが破損 する問題発生

LSMツリーとは

  • LSMツリー はRocksDBやCassandraで利用される 書き込み最適化データ構造
  • 書き込みはまず メモリ上のmemtable に格納
  • memtableが満杯になると SSTable としてディスクにフラッシュ
  • 複数のSSTableをレイヤー化し、 コンパクション で統合・重複排除
  • 高頻度な書き込み・圧縮処理が特徴

レプリケーション・セーフティとは

  • 分散データストアには 物理一貫性論理一貫性 の両方が必要
    • 物理一貫性:レプリカのデータ構造が正しい状態
    • 論理一貫性:トランザクションとして一貫したデータビュー
  • 物理一貫性のみでは 途中状態のデータ が生じるリスク
  • 例:本の複写で途中の章をコピーするイメージ

WALによる物理一貫性保証

  • Write-Ahead Log(WAL) で全てのバイナリ変更を記録
  • WALを スタンバイサーバー にストリーミングし順次適用
  • hot standby 構成でリアルタイム同期を実現

原子性と物理一貫性

  • Postgresのロック はレプリカで再現されない
  • WALはバッファ単位でロック・変更・解放を行う
  • 複数バッファにまたがるデータ構造は 原子操作が必須
  • pg_search はリストの複数ノード編集時に Copy-on-Writeヘッドの原子スワップ で対処

VACUUMと論理一貫性の破壊

  • VACUUMは 不要なタプル(行)を削除 するメンテナンス処理
  • MVCC により複数バージョンのタプルが共存
  • スタンバイでクエリ実行中にVACUUMが適用されると 途中でデータ消失 のリスク
  • 長時間クエリがVACUUM済みタプルへアクセスすると クエリエラー

LSMツリーにおける脆弱性

  • LSMツリーでは コンパクション頻度が高く、VACUUM同様の問題が頻発
  • 高書き込み負荷 環境で衝突リスクが増大
  • コンパクションやVACUUMが 安全に古いデータを削除 できるタイミングの把握が難しい

hot_standby_feedbackによる論理一貫性確保

  • hot_standby_feedback 設定でスタンバイからプライマリへ 安全な削除範囲 を通知
  • 各タプルの xmin(生成XID)xmax(削除XID) が管理される
  • スタンバイで最も古いxminをプライマリに伝え、 まだ必要なタプルの削除を防止
  • VACUUMやコンパクションの安全な実行タイミング 判定が可能に

まとめと今後

  • hot_standby_feedback で論理一貫性を確保しつつ、高速なLSMツリーを実現
  • 物理・論理一貫性の両立 は分散Postgresでの大きな課題
  • pg_search は原子ログ+hot_standby_feedbackでこの課題を克服
  • 高速検索と一貫性維持 を両立した設計が可能に
  • 詳細は 公式ドキュメントやオープンソースプロジェクト を参照

Hackerたちの意見

このノーフラフの技術的な深掘りスタイル、めっちゃ好き!HNにはこういうコンテンツがもっと必要だよね。

Elasticsearchの効果的な代替手段になるためには、リアルタイムで高い取り込み負荷をサポートする必要があった。なんでOpenSearchやElasticSearchを使わないの?ツールはもう在庫にあるのに、必要なものがあるのにドライバーを使う理由は何?これは「ハンマーを持っていると、すべてが親指に見える」っていう話の一つだね。

同期する必要がないし、結合もACIDがあるから。

理由はいくつかあるけど、すぐに思いつくのは、できるだけシンプルなスタックを維持することだね。現実的に言うと、ほとんどの企業は特別なツールが必要な規模で運営していないから。

取り込みが必要なら、CassandraベースのElasticを使えばいいじゃん?

それは、すでにあるデータのための全く別のアーキテクチャコンポーネントになるからだよ。あのツールはそのためだけに作られていて、SELECT full_text_search('kitty pictures');が足りないだけなんだ。

なんでOpenSearchやElasticSearchを使わないの? ElasticSearchみたいな別のツールを導入して統合するにはコストがかかるからね。いくつかの組織にとっては、ROIが見合わないかもしれないし、既存のデータベースがこの分野で追加の機能を提供しているなら、それを使った方がいいかも。 これは「ハンマーを持っていると、すべてが親指に見える」ってやつだね。専用のOLAPデータベースで解決しなきゃいけないと思ってる人のことを言ってるの?

Elasticsearchを運用するのは本当に大変だよ。既に持ってるツールを少し頑張って使えるなら、それは素晴らしいことだね。インデックスの再構築やガーベジコレクタの調整、ESのメジャーバージョン移行を考える必要もないし。

図はどうやって作ったの?

Figmaかな?

こういう内容がもっと必要だって言ってる人たちに同意!読んでみたけど、メモリに保持されているロックがWALの出荷にどう影響するのかがよくわからない。WALリーダーはシングルスレッドでそれを読み取り、定期的にメモリ内のデータ構造を更新してディスクにダンプするんだよね。もしかして、WALから一つの大きな命令を読み取って、複数のスレッドを使って多くのバッファに適用したいのかな?

アルゴリズムをブロックレベルで原子的に動作させることは、物理的レプリケーションの基本だよ。なんで?私にとって原子的にやらなきゃいけないのはWALの書き込みだけだと思う。WALリーダーは部分的な書き込みを検出してWALを再生できる限り、好きに読み書きできるし。 プライマリでVACUUMが実行中の時にクエリがリードレプリカにヒットすると、Postgresがリードを中止する可能性があるよ。君が言ってる状況はこうだね:1. レコードが挿入された 2. スタンバイで長いクエリが始まった 3. レコードが削除された 4. プライマリでVACUUMが始まった 5. VACUUMがレプリケートされた 6. スタンバイのVACUUMは長いクエリによって読み取られているからレコードを削除できない 7. PGがVACUUMを進めるためにクエリをキャンセルする。君の実装はコンパクション中にたくさんのデッドタプルを生成してるんじゃないかな。明らかにPGと戦ってるね。カスタムストレージエンジンの方がいい選択肢かも?