- Jepsen テストで、Amazon RDS for PostgreSQL Multi-AZ クラスターが、全ノードを通じて利用できる最強の分離レベルである Snapshot Isolation を満たさない事例が確認された
- 主因は、primary ではトランザクションの可視化順序が メモリ内ロック によって決まる一方、secondary では WAL 順序 に従うため、この 2 つの順序がずれる可能性がある点にある
- 障害注入や failover なしで、
gp3ストレージとdb.m6id.largeインスタンスを使った条件でも、約 150 write TPS / 1600 read-only TPS で数分ごとに G-nonadjacent cycle が現れた - この異常は Long Fork に該当し、AWS がサポートする PostgreSQL 13.15 から 17.4 までテストしたすべてのバージョンで確認され、Short Fork/Write Skew は観測されなかった
- 安全性が重要なトランザクションでは、read-only secondary の利用時に実行順序の見え方が変わる可能性があるため、writer endpoint のみを使う、または少なくとも 1 件の write を含める方式の検討が必要となる
Long Fork の原因アップデート
- AWS の Sergey Melnik と、HN コメント参加者の matashii、Ants Aasma が PostgreSQL クラスターにおける Long Fork の原因 を特定した
- PostgreSQL primary は、トランザクションを可視化する順序を メモリ内ロック で決定する
- secondary は、トランザクションを Write-Ahead Log(WAL) 内の順序に従って可視化する
- ロック順序と WAL 順序が異なると、primary と secondary でトランザクションの見かけ上の順序が食い違う可能性がある
- この挙動は 2013 年の PostgreSQL メーリングリスト投稿でも扱われており、Melnik は AWS ブログに PostgreSQL クラスターと read replica における transaction visibility を説明する記事を書いている
- Jepsen は、AWS と PostgreSQL が修正作業と合わせてこの問題を文書化することを推奨している
RDS for PostgreSQL の分離レベルと構成
- PostgreSQL は汎用のオープンソース SQL データベースであり、MVCC により 3 つのトランザクション分離レベルを提供する
Read UncommittedとRead Committedは、どちらも Read Committed として動作するRepeatable Readは実際には Repeatable Read ではなく、Snapshot Isolation を提供するSerializableは Serializability を提供する
- Amazon RDS for PostgreSQL は、マネージド PostgreSQL クラスターを提供する AWS のサービスである
- プロビジョニング、ストレージ管理、レプリケーション、バックアップ、アップグレードなどを自動化する
- Multi-AZ deployments は、データベースノードを複数のアベイラビリティゾーンに分散し、相関障害の可能性を下げる
- RDS は、primary と少なくとも 1 台の secondary インスタンスの両方でトランザクション耐久性が確保された後に応答する 同期レプリケーション を使用する
- ユーザーには、PostgreSQL wire protocol を話す 2 つの URL が提供される
- primary endpoint: read-write トランザクション用
- reader endpoint: read-only トランザクション用
- primary endpoint は PostgreSQL の全分離レベルをサポートするが、secondary は Serializable をサポートしない
- 全ノードで使える最強の分離レベルは、PostgreSQL が
Repeatable Readと呼ぶ Snapshot Isolation である
テスト設計
- Jepsen は、PostgreSQL 用の テストライブラリ を Amazon RDS for PostgreSQL 向けに調整し、小さな wrapper program を使用した
- 各テストラウンドごとに、AWS の CreateDBCluster API で RDS クラスターをプロビジョニングした
- ストレージは
gp3 - インスタンスは
db.m6id.large
- ストレージは
- テスト実行用に EC2 ノードを 1 台起動し、RDS クラスターの main endpoint と read-only endpoint を利用した
- 障害注入は行わず、failover も発生させていない
- 主な workload は、一意整数のリストを扱うトランザクションで構成される
- 各リストは単一の row に保存され、カンマ区切りの値を持つ
TEXTフィールドとしてエンコードされる - トランザクションは、primary key でリストを読み取るか、
CONCATで一意整数をリストに append する
- 各リストは単一の row に保存され、カンマ区切りの値を持つ
- この workload により、Elle checker はトランザクション間のデータフロー依存関係を推論し、グラフの cycle を探して複数の分離レベルを検証できる
G-nonadjacent cycle の観測
- 通常条件と中程度の並行性でも、Amazon RDS for PostgreSQL 17.4 は数分ごとに G-nonadjacent cycle を示した
- ある 2 分間のテスト実行 では、約 150 write TPS と 1600 read-only TPS を処理し、4 トランザクションの cycle を含んでいた
- 例の cycle は 4 つのトランザクション
T1、T2、T3、T4で構成されるT1は row 89 に9を append してリスト[4 9]を作り、T2がこれを観測したT3は row 90 に11を append してリスト[11]を作ったT4は row 90 に3を append し、結果のリスト[11, 3]を読み取ってT3の version を上書きしたT2は row 89 でT1の append を観測したが、row 90 ではT3の append を見ていない- 逆に
T4は row 90 でT3の append を観測したが、row 89 でT1の append は見逃した
- この cycle は、互いに隣接しない read-write dependency を含むため、Snapshot Isolation 違反である G-nonadjacent cycle に当たる
- 標準 PostgreSQL の
Repeatable Readではこのような挙動は起きるべきではなく、Jepsen は標準 PostgreSQL ではこれを観測していない
Snapshot Isolation と矛盾する理由
- Snapshot Isolation では、すべてのトランザクションが開始 timestamp
s時点のデータベース snapshot 上で動作するように見えなければならない - トランザクションの効果は、その後の commit timestamp
cに他のトランザクションから見えるようになる - 例の cycle の観測結果を timestamp 関係で表すと、互いに矛盾する
T2はT1の append を読んだので、T2の開始はT1の commit 後でなければならない:c1 < s2T2はT3の append を観測していないので、s2 < c3T4はT3を上書きし観測しているので、c3 < s4T4はT1の append を観測していないので、s4 < c1
- これらの関係は同時には成り立たず、Snapshot Isolation の timestamp モデルと矛盾する
Long Fork とバージョン別結果
- この cycle は Long Fork の例でもある
- 1 つ目と 2 つ目のトランザクションは、1 つの論理状態 fork を構成する
- 3 つ目と 4 つ目のトランザクションは、2 つ目の fork を構成する
- 2 つの fork は異なる row を更新するが、互いの効果を観測できない
- Short Fork、つまり Write Skew は観測されなかった
- この結果は、Amazon RDS for PostgreSQL が Snapshot Isolation よりわずかに弱い Parallel Snapshot Isolation を提供している可能性を示唆する
- G-nonadjacent 異常は、write-read edge のみでつながる場合や、4 個を超えるトランザクションを含む場合まで、さまざまな形で現れた
- AWS がサポートする最も古いバージョンである PostgreSQL 13.15 から、最新バージョン 17.4 まで、テストしたすべてのバージョンで同種の異常が発生した
ユーザーが確認すべき点
- Long Fork とその他の G-nonadjacent cycle が存在するため、Amazon RDS for PostgreSQL Multi-AZ クラスターは Snapshot Isolation を保証しない
- この点で、RDS for PostgreSQL Multi-AZ クラスターは、以前の Jepsen テストで Strong Snapshot Isolation を提供しているように見えた単一ノード PostgreSQL より弱い安全性セマンティクスを提供する
- ユーザーは、自分のトランザクション構造が Long Fork に脆弱かどうかを調べるか、意図した不変条件が維持されるかを実験で検証できる
- read トランザクションは、トランザクション実行順序について他のトランザクションと異なる結果を見る可能性がある
- この異常は read-only secondary へのクエリに関連しているように見えるため、次の方法で Snapshot Isolation を回復できる可能性がある
-
writer endpoint のみを使用
- 安全性が重要なすべてのトランザクションに少なくとも 1 件の write を含める
- Jepsen の検証は実験的アプローチであり、バグの存在は証明できても、不在は証明できない
- このレポートは RDS for PostgreSQL の動作を詳細に調査した結果ではなく、予備的な探索 の産物である
-
1件のコメント
Hacker News のコメント
ソフトウェア界隈の記事も、もっと頻繁にこういう書き方だといいのにと思う。「Amazon RDS for PostgreSQL は、PostgreSQL データベースのマネージドインスタンスを提供する Amazon Web Services(AWS)のサービスだ。Amazon RDS for PostgreSQL multi-AZ クラスターが、すべてのエンドポイントでサポートされる最も強い一貫性モデルであるスナップショット分離に違反することを示す……」
率直で、要点だけで、飾り気がなく、ほかの STEM 分野での研究成果の共有方法に近い。以前はミームで説明する気の利いたブログ記事が好きだったが、今はプレーンでシンプルな文章が恋しい
かなり深い技術記事を書くと、いいねやコメントはほとんどなく、Staff Engineer から「対象をもっと絞ったほうがいい」と言われるほどだった。一方で、初期の Kubecost を試しながら、推奨事項によるコスト削減は小さく、コンテナの性能問題を引き起こし得るという記事を書いたところ、CPU throttling や cgroups を扱うかなり技術的な内容だったにもかかわらず、ミームを入れたら人々にものすごく好評だった
その後、C で小さな Python 外部ライブラリを作り、ctypes でアクセスしつつスタック/ヒープ割り当てを比較する、より硬い記事にもミームを入れたところ、似たような結果になった。こういう流れは気に入らないが、幅広い読者に届かせるには、ほかに避ける方法もあまり分からない。Jensen はそういう読者を狙っていたわけではなく、厳密で純粋な文章は称賛に値する
タイトルにもなく、本文でも非常に明確というわけではないが、この問題は RDS の比較的新しい機能である multi-AZ クラスターに限定される。多くの人が慣れ親しんでいる multi-AZ インスタンスとは異なる
multi-AZ インスタンスは、プライマリ DB が別のアベイラビリティゾーンのセカンダリ DB に同期レプリケーションされ、プライマリが失敗すると RDS がセカンダリへフェイルオーバーする古くからある機能だ
multi-AZ クラスターはセカンダリが 2 つあり、トランザクションはそのうち少なくとも 1 つに同期レプリケーションされる。セカンダリの 1 つが失敗したり性能が低下したりしたとき、multi-AZ インスタンスより堅牢で、セカンダリへの読み取り専用アクセスも可能だ
ただし multi-AZ クラスターには、内部に PostgreSQL の標準機能ではない 追加の魔法がさらにあるはずで、おそらくそのために Jepsen テストで失敗したのだと思う
ただし PostgreSQL には、このパターンに似た問題を可能にする欠陥がまだある。クライアントがコミット中に消える非レプリケーションのトランザクションは、すぐに見えるようになる。例で T1 が分離されたリーダー上で起き、コミット中に接続が切れ、T2 も分離されたノード上で起き、T3/T4 が後から新しいリーダー上で起きると、同じ結果が見える可能性がある。しかし、このテストでは fault injection をしていないという説明とはあまり合わない
追記: レプリカとプライマリノードの コミット順序の不一致でこのパターンが説明されるという記事を見落としていた。この問題を直す方法を発表したことがあるので、少し恥ずかしい
よい調査だと思う。最近のソフトウェア開発者はトランザクションそのものもよく知らないことが多く、さまざまなトランザクションモデルとなるとなおさら知らない傾向がある。いわゆる「シニア開発者」と呼ばれる CRUD 開発者の中にも、データベーストランザクションをまったく知らない人を見たことがある。
実際、トラフィック規模があり、ソフトウェアが些細ではない問題を解く場合、トランザクションとトランザクションモデルは性能とバグのないコードにとって非常に重要だ。
たとえば大規模プロジェクトで多くの分析を経て、SQL Server のデフォルトの Read Committed から Read Committed Snapshot Isolation に変更したところ、ロック競合が大きく減り、ユーザーは非常に満足していた。そのプロジェクトのソフトウェアエンジニアたちはトランザクションを多用していたが、基礎を教えるまでトランザクションモデルやロックについてはまったく知らなかった。
主に小売分野で働いているため、競合状態に似たエラーだらけのシステムをよく見るが、こうした分離レベルが大いに役立ち得る部分なので、なおさら残念に思う。
ただ、こうしたケースは主にスタートアップのエンジニアに見られたもので、大企業の典型的な Oracle/MSSQL 開発者は少なくとも基礎は押さえているので、かなり高く評価している。
キャリアの中で何度か、このやり方が本当に悪い結果を生むのを見た。
ただし、この移行時に注意すべき点は、ブロッキング読み取りに依存するすべてのコードが壊れることだ。たとえば
select with existsのようなコードは、明示的ロックや別の方法で書き直す必要がある。以前の会社でバックアップスクリプトの
pg_dumpコマンドを変更し、並列ワーカー(-jフラグ)を使い始めたとき、復元中に重複キーエラーや外部キー制約エラーのような、不整合を示唆するエラーがまれに見られた。当時 AWS と PostgreSQL のメーリングリストに報告しようとしたが、簡単に再現できず進展がなく、結局あきらめてシングルスレッドのダンプに戻した。そのとき見た現象がこの問題と関係しているのか気になる。
この記事を読むと、実際の影響は同じ行への書き込み直後に素早く読み取りが起きたときに、古いデータが返される可能性がある、ということのように見える。書き込みトランザクションは完了と表示されるが、multi-AZ RDS インスタンスの分散レイヤー全体が完全に更新される前なので、同じ行を即座に読むと、その行がまだ存在しなかったり、列が完全に更新されておらず以前の値が出たりする可能性がある。
PostgreSQL のスナップショット方式からすると、マルチバイト列型の一部のバイトだけが更新され、意味不明な値を読むということではないように見える。
結局、時間が経てば収束する競合状態のように見える。あるいは、「long fork」の後続トランザクションが通常の状況でも永遠に完了しない可能性がある、という意味に読んだ人がいるのか気になる。
「この作業は報酬なしで Jepsen が独立して実施した」という文言は、RDBMS の利害関係者が、たとえ好調な日であっても見たくない内容だ。内部では懸念を含むメールが何通か飛び交ったのではないかと思う。いつものように aphyr に敬意を表する。
これが multi-instance upstream PostgreSQL クラスターでは問題にならないのか、完全には明確ではない。AWS がクラスター設定で何かをしているのか、あるいはこの挙動を引き起こすパッチを追加したと理解すればよいのか気になる
PostgreSQL のレプリケーションには一般にさまざまな方式があり、結果も異なる。たとえば Bin Wang の Patroni レポートがある: https://www.binwang.me/2024-12-02-PostgreSQL-High-Availabili...
ここでも分かったのは、PostgreSQL が現在、プライマリノードとレプリカの間で一貫したスナップショット挙動を提供していないという点。おそらく読み取り専用トランザクション T2 はセカンダリノードで実行され、変更トランザクション T1/T3/T4 はプライマリノードで実行されたのだろう
背景として、セカンダリ PostgreSQL ノードのスナップショットは、どのトランザクションが見えるかを決める際にトランザクションの永続化順序、つまり WAL のコミットレコード位置に依存する。一方、プライマリノードの可視性の順序は、そのトランザクションを承認したバックエンドがトランザクションが完全にコミットされたという通知を最初に受け取った時点と、その後にコミット表示を行う時点によって決まる
プライマリノードとセカンダリノードのそれぞれでは、接続されたバックエンド間のコミット順序は一貫しているが、プライマリとセカンダリの間ではコミット順序が多少異なる可能性がある。これを改善する作業は進行中だが、まだかなり進行中の段階である
AWS は PostgreSQL にパッチを当てて 2 つのインスタンスにレプリケートし、そのどちらか一方が変更を確認すれば十分だと扱っているように見える。この確認がいつ行われるのかは公開情報ではない
個人的には、PostgreSQL には drbd のようなファイルシステムレベルのレプリケーションの方がよいと思う。昔ながらの AWS Multi-AZ インスタンスはおそらくこの方式だろう。ただしスループットは下がり、セカンダリインスタンスからは読み取れない
特にこの箇所: https://youtu.be/fLqJXTOhUg4?t=434
投稿されたタイトルは核心を問うている。RDS for PostgreSQL 17.4 はスナップショット分離を正しく実装していない
タイトルがシステムに厳しすぎるのか、好意的すぎるのか、見つかった十数個の問題のうち最も意味のあるものを含んでいるのか、Jepsen がデータベース安全性の結果について誠実な仲介者であろうとする基準から見て公正なのか、10 年後に人々がリンクし続けるが最近のバージョンにはもはや当てはまらないときにどう解釈されるのか、といった議論はかなり白熱することがある
何度かもどかしい試みをした後、すべてのレポートタイトルを “Jepsen: ” という形にする方針でこの問題を避けている。HN がより説明的、あるいはより彩りのあるリンクテキストを望むなら、もちろん自分たちで選べばよい
それでもこれはトランザクション保証の Chuck Norris のような Kyle Kingsbury が書いたものなので、AWS は回答または説明すべきだ。PostgreSQL 用 RDS の 2 つの選択肢のうち 1 つである multi-AZ クラスターにだけ適用されるように見えるとしても同じ。Multi-AZ 配置にはスタンバイ DB インスタンスが 1 つの場合と 2 つの場合があり、ここでは 2 つのスタンバイ DB インスタンス構成についての話
AWS ドキュメントにはそのような約束はない。RDS の 5494 ページのマニュアルも、各エンジンのパラメータ文書で isolation や serializable にほとんど触れているだけ
Multi-AZ クラスターのグローバルな読み取り一貫性についても記載はない。半同期レプリケーションなので writer が 1 台の standby のログレコード確認を待つとはいうものの、2 台の reader が互いに異なるスナップショット上にいる可能性がある
[1] - "New Amazon RDS for MySQL & PostgreSQL Multi-AZ Deployment Option: Improved Write Performance & Faster Failover" - https://aws.amazon.com/blogs/aws/amazon-rds-multi-az-db-clus...
[2] - "Amazon RDS Multi-AZ with two readable standbys: Under the hood" - https://aws.amazon.com/blogs/database/amazon-rds-multi-az-wi...
開発者がスナップショット分離を前提にしているのに、Amazon RDS for PostgreSQL が実際には 並列スナップショット分離しか提供していないとしたら、特に読み取りレプリカのエンドポイントを使う multi-AZ 構成で、どんな安全性バグやアプリケーションレベルのバグが起こり得るのか気になる
git pushのような流れを考えるとよい。トランザクションを開始し、現在の状態を読んで期待した状態と一致するか確認し、新しい状態を書き込み、新しい状態のハッシュとともにコミットする。不運な状況では、どの有効な状態にも対応しない コミットハッシュができる可能性があるこうしたことを推論するのが難しいという事実自体が、問題を避けにくくしている。なので最も簡単な解決策は、読み取りを条件とする書き込みなら「writer エンドポイントだけを使えばスナップショット分離を回復できるかもしれない」に近いものになるだろう
ただし「writer エンドポイントだけを使う」方法が、特に可用性が失われる状況でテストされていない点は意外だ
User1 がコメントし、User2 がコメントした後、User1 が別トランザクションでコメントが1件だけだと確認してバッジを受け取る。User2 も別トランザクションで同じ確認を行い、自分のコメント1件だけが見えてバッジを受け取ることがあり得る
スナップショット分離ではこれは不可能だ。別トランザクションの少なくとも一方はコメント2件を見るはずだ
並列スナップショットに関する元論文も読む価値がある: https://scispace.com/pdf/transactional-storage-for-geo-repli...
「この現象はテストしたすべてのバージョン、13.15から17.4までで発生した」という文を見て、メジャーバージョンを上げたのが間違った選択だったのかと心配したが、そうではなさそうだ。これは リグレッションではなく、機能リクエストか古くからあるバグに近い