3 ポイント 投稿者 GN⁺ 2023-12-20 | 1件のコメント | WhatsAppで共有
  • MySQL 8.0.34 のデフォルト分離レベルである Repeatable Read は、単一の正常ノード上であっても ANSI SQL および Adya の PL-2.99 の期待に一致しないトランザクション整合性違反を示した
  • Elle の list-append チェッカー、targeted workload、LazyFS を組み合わせて、MySQL 8.0.34、MariaDB 10.11.3、binlog レプリケーションクラスタ、AWS RDS MySQL Multi-AZ DB Cluster をあわせて検証した
  • Kleppmann の 2014 年の Hermitage の結果と同様に、G2-itemG-single、lost update が再現され、内部整合性違反、non-repeatable read、Monotonic Atomic View 違反も観測された
  • 単一 MySQL の Read Uncommitted、Read Committed、Serializable はそれぞれ PL-1、PL-2、PL-3 に適合しているように見えたが、AWS RDS MySQL クラスタは Serializable でも G2-item と G-single を示した
  • ANSI または PL-2.99 レベルの Repeatable Read が必要なら、MySQL Repeatable Read だけを信頼するのは難しく、SerializableSELECT... FOR UPDATE のような明示的ロックが必要になる

評価対象と範囲

  • MySQL は広く使われているリレーショナルデータベースであり、この分析で「MySQL」とは、デフォルトのストレージエンジンである InnoDB を使用する MySQL を指す
  • 焦点は単一サーバー MySQL だが、binlog レプリケーションを使う単一書き込み primary と読み取り専用 secondary のクラスタもあわせて扱う
  • テスト対象は次のとおり
    • MySQL 8.0.34
    • MariaDB 10.11.3
    • Debian Bookworm
    • AWS RDS Cluster の「Multi-AZ DB Cluster」プロファイル
  • 作業は無償で独立して実施され、Jepsen ethics policy に従って進められた

SQL 分離レベルと Repeatable Read の基準

  • ANSI SQL は Read Uncommitted、Read Committed、Repeatable Read、Serializable を、P1 dirty read、P2 non-repeatable read、P3 phantom の可否で定義している
  • 1995 年に Berenson らは A Critique of ANSI SQL Isolation Levels で ANSI 定義の曖昧さと不完全さを批判した
    • P1、P2、P3 には解釈の余地がある
    • P0 dirty write のような重要な現象が抜け落ちている
    • P3 は predicate に影響する insert だけを禁止し、update や delete は扱っていない
  • Atul Adya の 1999 年の論文は、トランザクション間の 依存グラフ に基づく実装非依存の分離レベルを定義した
    • PL-1 は G0 write cycle を禁止する
    • PL-2 は G0 と G1 を禁止する
    • PL-2.99 は G0、G1、G2-item を禁止し、Repeatable Read に対応する
    • PL-3 は G0、G1、G2 を禁止し、Serializable に対応する
  • Jepsen は一般に Adya の形式を使ってトランザクション履歴と異常を判定する

MySQL ドキュメントと Repeatable Read の衝突

  • MySQL のドキュメントでは、InnoDB が SQL:1992 標準の 4 つの分離レベルをすべて提供すると説明している
  • デフォルト分離レベルである Repeatable Read については、同じトランザクション内の consistent read は最初の読み取りで設定されたスナップショットを読むと説明している
  • consistent read のドキュメント でも、最初の読み取り時点の timepoint に従ってデータベースを見ると説明している
  • しかし同じ文書の注記には、スナップショットは SELECT には適用されるが DML 文には必ずしも適用されず、他のトランザクションが commit した row に DELETEUPDATE が触れうると書かれている
  • この注記は、ANSI SQL と MySQL リファレンスマニュアルが SELECT も DML と見なしている点と衝突し、Repeatable Read で書き込みが読み取れなかった row に影響しうるという混乱を生んでいる

テスト設計

  • MySQL 向けテストスイートJepsen testing library 0.3.4 をベースに書かれている
  • クライアントは mysql-connector-j JDBC アダプタを使用する
  • テストには process pause、crash、network partition、fsync されていないディスク書き込み損失のような fault injection が含まれる
  • ただし今回の分析における発見のほとんどは、正常状態の単一 MySQL ノード で発生している
  • Elle list-append workload

    • 中核となる workload は Elle の list-append checker を使用する
    • Elle はトランザクション間の write-write、write-read、read-write 依存を推論し、依存グラフの cycle によって特定の分離レベル違反を立証する
    • list-append workload は、primary key で識別される複数の list に対して read と append からなるランダムなトランザクションを実行する
    • list はカンマ区切りの値を保持する text フィールドとしてエンコードし、append は SQL CONCAT で処理する
    • 最近の改善により、Elle は次をよりよく検出できるようになった
      • 読み取られなかった append element に対する ww/rw 依存の推論
      • P4 lost update の明示的検出
      • real-time edge と process edge を含む複雑な cycle 探索
  • Targeted workload

    • Non-repeatable read workload は people テーブルの 1 つの row を対象にする
    • ある系列のトランザクションは name だけを update し、別の系列は name を読み、gender を update した後、再び name を読む
    • 2 回の読み取りの間で name が変わっていれば Repeatable Read 違反である
    • Monotonic Atomic View workload は 2 つの row の value を使う
    • writer は row 0 の value を増やした後で row 1 を増やす
    • reader は row 0 を読み、row 1 の noop を update した後で row 1 と row 0 を読む
    • あるトランザクションの効果の一部を見たなら、そのすべての効果を見なければならない
  • LazyFS

    • LazyFS は fsync されていない書き込み損失をシミュレートする FUSE ファイルシステムである
    • MySQL process を kill し、LazyFS キャッシュを破棄した後で MySQL を再起動する方法でテストした
    • このレポートは LazyFS を含む最初の公開 Jepsen レポートである

MySQL Repeatable Read で見つかった異常

  • G2-item

    • Adya の PL-2.99 Repeatable Read は、predicate を含まない write-write、write-read、read-write 依存 cycle である G2-item を禁止する
    • MySQL Repeatable Read は、単一の正常ノード上でも G2-item を繰り返し許容する
    • Kleppmann が 2014 年に Hermitage で報告した挙動は、MySQL 8.0.34 でも引き続き発生する
    • ある例のテストでは 40 秒間で 214 個の cycle が見られた
    • この挙動は PL-2.99 Repeatable Read では禁止されるが、ANSI SQL の P2 定義は同じ row を 2 回読むケースしか扱わないため、ANSI 定義上は解釈の余地が残る
  • G-single と read skew

    • MySQL Repeatable Read は G-single も示す
    • G-single は write-write、write-read、read-write edge から構成されるが、read-write edge が互いに隣接しない cycle である
    • Kleppmann が 2014 年に報告した read skew は、MySQL 8.0.34 でも確認された
    • 60 秒の append テストでは、毎秒およそ 140 トランザクションで G-single 244 件と G2-item 305 件が現れた
    • append テストは predicate 演算を使わないため、いずれも Repeatable Read 違反として分類される
  • Lost update

    • P4 lost update は、2 つのトランザクションが同じ key の同じ version を読み、両方が update する G-single の特殊ケースである
    • Snapshot Isolation と PL-2.99 Repeatable Read は lost update を禁止する
    • MySQL Repeatable Read は、単一の正常ノード上でも lost update を繰り返し許容する
    • あるテストでは 9,048 件の成功トランザクションのうち、新しい checker が 198 件の lost update に関与した 446 件のトランザクションを見つけた
    • このうち cycle として表れた例は 47 件だけだった
    • 値を読んでから書くパターンは、MySQL Repeatable Read では安全ではない
    • オブジェクトを読み、メモリ上で修正してから保存し直す標準的な ORM パターンでは、commit 済みの変更が静かに失われうる
    • ユーザーは明示的ロックを自ら使う必要がある
  • Non-repeatable read と内部整合性違反

    • MySQL Repeatable Read は、単一の正常ノード上でも 内部整合性違反 を示す
    • 同じテスト run で、9,048 件の commit トランザクションのうち 126 件が内部整合性エラーを示した
    • ある例では、1 つのトランザクションが key を nil として読み、1 つの値を append した後で同じ key を再び読むと、他の 3 つの値が追加された状態を観測した
    • 別の例では、key 1096 を [1 2 3] として読み、7 を append してから再度読むと [1 2 3 4 5 6 7] を観測した
    • targeted workload では、1 つの Repeatable Read トランザクション内で name"pebble" と読み、gender"femme" に update した後、同じ name をもう一度読むと "moss" が返ってきた
    • このような挙動は、ANSI SQL の non-repeatable read の定義と、MySQL ドキュメントの「最初の読み取りで設定されたスナップショット」という説明に反する
  • Monotonic Atomic View 違反

    • Monotonic Atomic View は、あるトランザクションの何らかの効果を見たトランザクションは、そのトランザクションのすべての効果を見なければならないという性質である
    • MySQL Repeatable Read は、正常な単一ノード上でもこれを繰り返し破る
    • workload では writer が row 0 を増やした後で row 1 を増やす
    • reader は row 0 で以前の値 0 を見た後、row 1 で writer の増加分 1 を見て、再び row 0 で依然として 0 を見る
    • これは row 1 の効果は見たが row 0 の効果は見ていない 非単調読み取り であり、一般的なスナップショットの挙動と一致しない

AWS RDS MySQL Serializable の異常

  • AWS RDS MySQL クラスタは、「Serializable」分離レベルでも Serializability を繰り返し破る
  • デフォルトの推奨 production プロファイルの RDS MySQL クラスタでは、append テストで G2-item と G-single anomaly が見られた
  • 観測された anomaly は、あるトランザクションの効果を見たトランザクションの先行依存を別のトランザクションが取りこぼす形だった
  • この anomaly は G-single かつ G2-item に分類され、Snapshot Isolation、Repeatable Read、Serializability のすべてに違反する
  • replica_preserve_commit_order 関連の設定が疑わしい要因として残っている
    • MySQL 8.0.27 以降では replica_preserve_commit_order=ON がデフォルト値である
    • RDS のデフォルトパラメータは、依然として replica_preserve_commit_order=OFF に相当する設定を選んでいる
    • RDS parameter group では、この設定の旧名称である slave_preserve_commit_order を使う
    • ローカルのテストクラスタにこの設定を適用すると、似た G-single と G2-item が観測される

正常に見えた点と LazyFS の結果

  • MySQL 8.0.34 の Read Uncommitted、Read Committed、Serializable は、それぞれ PL-1、PL-2、PL-3 を満たしているように見える
  • この結果は、単一ノードと binlog レプリケーションを使用する小規模な読み取り専用レプリカクラスタの両方で観測された
  • process pause、crash、network partition においてもこの結果は維持された
  • LazyFS fault injection では、MySQL のデフォルト設定に問題は見つからなかった
  • innodb_flush_log_at_trx_commit=1 のデフォルト値では、process crash と fsync されていないデータ損失の後でも、commit 済みトランザクションの損失は現れなかった
  • innodb_flush_log_at_trx_commit=0 に変更すると、MySQL は数秒に 1 回しか fsync せず、データ損失が観測された

MySQL Repeatable Read の実際の性質

  • MySQL Repeatable Read は PL-2.99 Repeatable Read を満たさない
    • G2-item と write skew を示す
  • Snapshot Isolation も満たさない
    • G-single、read skew、lost update を示す
  • cursor stability も満たさない
    • lost update が発生する
  • Read Atomic、Causal Consistency、Consistent View、Prefix Consistency、Parallel Snapshot Isolation も除外される
    • 内部整合性違反が観測される
  • MySQL Repeatable Read は Read Committed よりはいくらか強いように見える
    • G0 dirty write、G1a aborted read、G1b intermediate read、G1c cyclic information flow は観測されなかった
    • 一部の読み取りの repeatability は Read Committed より強い性質を提供する
  • ただし、MySQL Repeatable Read が正確にどの consistency model なのかは明確でなく、公式な性質定義もない

ドキュメントとコミュニティ理解の不一致

  • MySQL コミュニティでは、Repeatable Read の挙動が十分に理解されていない状態である
  • 多くの記事は MySQL Repeatable Read が lost update を防ぐと信じているが、別の記事では防げないと報告し、明示的ロックの使用を勧めている
  • 多くのインターネット上の資料は MySQL Repeatable Read は実際に repeatable だと述べているが、Jepsen のテストはそうではない例を示している
  • MySQL と MariaDB のドキュメントも、Repeatable Read は同じトランザクション内で同じスナップショットを読むと説明している
  • MySQL の consistent read ドキュメントの一文は、この説明と矛盾する挙動を示唆しているが、その内容は文書の中に埋もれている

推奨事項

  • MySQL が現在の挙動を維持するなら、「Repeatable Read」が実際にはどのような consistency model を提供するのかを明確に文書化すべきである
  • 別の選択肢は、現在の挙動をバグとして扱い修正することである
  • MySQL と他のベンダーが PL-2.99 Repeatable Read の提供を約束するなら、Jepsen はこれを歓迎すると述べている
  • PL-2.99 または ANSI Repeatable Read が必要なユーザーは、MySQL Repeatable Read に注意すべきである
  • 実務上の代替策は次のとおり
    • MySQL の Serializable 分離レベルを使う
    • READ COMMITTEDSELECT ... FOR UPDATE のようなロック手法により読み取りを強化する

RDS 利用者への推奨

  • AWS RDS MySQL クラスタは「Serializable」で read skew と G2-item を示す
  • Serializability に依存するユーザーは、RDS parameter group で slave_preserve_commit_orderON に設定すべきである
  • AWS はデフォルト値を変更するか、RDS MySQL の known limitations 文書で許容される Serializability 違反を明確に説明すべきだという提案が出ている

今後の作業と標準化への要請

  • MySQL binlog replication は脆弱に見えた
    • ローカルの Jepsen テストでは、replication が停止する複数の状況が観測された
    • AWS RDS MySQL replication は数分のテストだけで完全に壊れうることがあり、primary で成功した CREATE DATABASE が secondary に現れない状態が 1 時間回復しなかった
  • secondary を primary に昇格するケースや、ring、star のようなレプリケーショントポロジーは探索していない
  • predicate safety を評価するための、より一般的な predicate test の研究が進行中である
  • ANSI SQL 分離レベルの定義は、Berenson らが曖昧さと不完全さを指摘してから 28 年、さらに 7 回の ANSI・ISO 改訂を経ても変わっていない
  • ISO/IEC 9075-2 が、内部 anomaly、lost update、dirty write のような現象を明確に扱えるよう、より形式的で移植可能な分離レベル定義が必要である

1件のコメント

 
GN⁺ 2023-12-20
Hacker News の意見
  • repeatable read は、実装が完璧であっても悪いアイデアだと昔から思っている。
    データベース内部で正しく動作したとしても、複雑なクエリでは推論があまりにも難しい。
    意味のある分離レベルは read committedserializable の2つだけだと思う。
    最後まで一貫して驚きのない serializable を選ぶか、あるいはトランザクション内で一貫したビューが必要なら、読み取り前に行をロックしなければならないことが明確な read committed を選ぶべきだ。
    read committed は一般的なマルチスレッドコードやメモリ管理に近く、エンジニアが直感を持ちやすいし、serializable は非常に厳格なので思いがけないミスを生みにくい。
    その中間は無人地帯であり、read committed より整合性が低いものは、もはやまともなデータベースとは言い難い。

    • read committed を人々がうまく推論できるとは思わない。
      アプリケーションが大きくなるほど、どこでロックが取られ、どこでデータにアクセスされるのかを、あらゆるケースで理解するのは非常に難しくなる。
      読み書きトランザクションには serializable だけが正気の分離モデルであり、読み取り専用トランザクションには、ある時点のデータベーススナップショットを扱う snapshot isolation が良いモデルだと思う。
      Spanner が提供するモードも、実質的にはこの2つだけだ: https://cloud.google.com/spanner/docs/transactions
    • read uncommitted は集計統計には使えるが、その程度ならデータを ClickHouse に流したほうがよい。
    • 読み取り専用の スナップショットクエリ は実システムで非常に有用だ。
    • repeatable read が本当に正しく機能するなら、わざわざ行をロックする必要はないはずだ。
  • FOSSDEM 2024 で、SQL データベースの 分離レベルと MVCC を比較した発表がある。
    Oracle、MySQL、SQL Server、PostgreSQL、YugabyteDB を扱っている。
    https://fosdem.org/2024/schedule/event/fosdem-2024-3600-isol...

    • 発表者は YugabyteDB でデベロッパーアドボケイトをしているが、これが Kyle の仕事とどう関係しているのか気になる。
  • append(a) が与えられたテーブルの実際の SQL 演算 にどうマッピングされるのか気になる。
    TEXT フィールドをリストのように使っているのだろうか。
    MySQL の repeatable read モードでは、単一行を対象とする単一の SELECT がありえない結果を返したこともある。
    SELECT min(value), max(value) FROM table WHERE id = 1; のような形で、id は主キーだったのに、minmax が異なる値になった。

  • 記事と AWS RDS を取り上げてくれたのはよかったが、AWS Aurora MySQL にも焦点が当たっていたのか気になる。
    知らない人のために言うと、AWS は MySQL や PostgreSQL を装うプロトコル互換のデータベースプラットフォームを作った。
    Aurora MySQL が RDS や MariaDB と同じような「特徴」を持つのかを見るのは面白そうだ。

    • Aurora はまったく別の DB エンジン なので、並行性の問題も異なり、ここでは扱われなかったのだろう。
      それでも非常に興味深い対象であり、Aurora ははるかに新しいデータベースなので、古い MySQL よりもまだ見つかっていない微妙な問題がありそうな気がする。
    • MySQL Aurora はかなり使っていて、私たちの用途では利用量は非常に多いがクエリパターンが単純なので、大きな違いはあまり見えない。
      ただし大きな厄介ごとが1つある。
      Plaid のエンジニアが違いをうまく整理した記事を書いている: https://plaid.com/blog/exploring-performance-differences-bet...
      私にとって最大の違いは、Aurora クラスタが 共有ストレージ を使うため、分離モデルが少し異なることだ。
      read committed はクラスタ全体のパラメータを設定した場合にのみ可能で、read uncommitted は私の見る限り不可能だ。
  • 非常に興味深い記事だ。
    これほど多くの 整合性異常 を示す基盤の上でも、どれほど多くの「実際に動くシステム」が作れてしまうのかをよく示している。

    • ほとんどのシステムは実質的に壊れており、人間的な補正で回避しながら動かしている
  • 5分ほど触っただけでRDSレプリケーションが停止し、失敗したヘルスチェックのアラートもなかったという点は少し心配

    • 詳細が重要で、スクリーンキャストだけではほとんど問題解決は不可能だが、私の経験では AWS は概して CloudWatch Metrics をかなり豊富に提供している
      ただし、ユーザーに 150 個を超えるメトリクスをあさり、ドキュメントを読んで重要なものを見つける負担を押し付けがちでもある
      また、<https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_...> ではレプリケーション状態を示すコンソールの表セルがあるとしているが、コンソールではしばしばその列表示をユーザーが自分で有効にしなければならず、よくない
      AWS が言う「共同責任モデル」にかなり大きく依存している
    • AWS のヘルスチェックをダウン状況の一次通知として信用してはいけないと断言できる
      ホストやコンテナの内部ですべて自前でやる必要がある
      AWS/Rackspace のサポートは「AWS サービス内で動作しているものは当社では管理していないので顧客の問題です」と言うだけだ
  • 2022 年に Jepsen が Porto 大学の INESC TEC に LazyFS の開発を依頼したという点がよい
    fsync されていない書き込みの損失をシミュレーションする FUSE ファイルシステムとは、技術水準を前進させる素晴らしい例だ

  • SELECT ... FOR UPDATE がこうした問題への答えのように見える
    更新する行をロックすれば、突然すべてが宣伝どおりに動くのではないか?

    • 一般に、行をロックする操作は repeatable read とは無関係に、値を存在するように「固定」する傾向がある
      あるレコードを別のレコードのデータに基づいて更新したいなら、その別のレコードと、おそらく更新するレコードにも ロック読み取り を行う必要がある
      単一の SQL クエリで別のレコードに基づいてレコードを更新すると、MySQL はどのみち両方をロックしてくれる
      複数の対象に基づいて何かを更新しなければならないなら、私の経験では デッドロック が非常に起きやすい
      その代わり、ロック用レコードのようなものをロックしてから、目的のデータに対して repeatable read を行い、更新するほうがよい
      repeatable read の時点は、一貫読み取りを行うまで決まらない
      SELECT ... FOR UPDATE は一貫読み取りではないので、通常の SQL 更新で数十・数百行をロックすることなく、同時実行の状況でもうまく動作する
    • パフォーマンスが完全に壊れても構わないなら、その通り
  • 私の経験では、ほとんどの開発者はそもそも 分離レベル を考慮せず、デフォルト値をそのまま使う
    競合状態が起きても「え、変だな」で済ませてしまう

    • 反論したいが、MongoDB の成功した初期の時代がその言葉をよく証明している
    • だからこそ、デフォルトの分離レベルは serializable であるべきだと言ったのだ
      [1] https://news.ycombinator.com/item?id=38696421
    • 分離の問題は推論するには難しすぎるので、serializable 整合性より低いほとんどのものは、結局さまざまな形で足を引っ張ることになる
      だから、ほとんどの開発者は分離レベルを自分で悩まないほうがよく、MySQL や一部のデータベースは平均的な開発者に対して保証レベルをあまりにも少なくしか提供していないと思う
    • 私の経験では、ほとんどどんな開発者も 一貫性 自体を考慮していない