CockroachDB から覗く形式手法の世界 #CNDT2020 / CloudNative Days Tokyo 2020

332f89cc697355902a817506b6995f2b?s=47 y_taka_23
September 09, 2020

CockroachDB から覗く形式手法の世界 #CNDT2020 / CloudNative Days Tokyo 2020

CloudNative Days Tokyo 2020 で使用したスライドです。

バグのない分散システムの設計は果たして可能でしょうか? この問いに対する一つの答えとして、CockroachDB では形式手法ツール TLA+ を用いて分散トランザクションの正しさを担保しています。 形式手法はシステムの挙動を数学的に解析する技法で、「ノードが特定のタイミングで故障した場合にのみ発生するバグ」といった再現困難な問題を確実に検出することができます。 本講演では、CockroachDB の事例を通して、形式手法が実世界で活用されている様子をお伝えします。

イベント概要:https://event.cloudnativedays.jp/cndt2020
ブログ記事:https://ccvanishing.hateblo.jp/entry/2020/09/10/044848

332f89cc697355902a817506b6995f2b?s=128

y_taka_23

September 09, 2020
Tweet

Transcript

  1. CockroachDB から覗く 形式手法の世界 #CNDT2020 #CNDT2020_E チェシャ猫 (@y_taka_23) CloudNative Days Tokyo

    2020 (9th Sep. 2020)
  2. https://github.com/cockroachdb/cockroach

  3. QCon 2019 での発表 形式手法ツール TLA+ により「証明」済 https://qconnewyork.com/ny2019/presentation/cockroachdb-architecture-geo-distributed-sql-database

  4. 本日お話しすること • CockroachDB の概要 ◦ スケーラビリティを実現するアーキテクチャ ◦ 生じた課題と Parallel Commit

    による解決 • 形式手法ツール TLA+ の概要 ◦ 理論はともかく、ツールとしての雰囲気 ◦ CockroachDB の挙動をどう表現・検証するか
  5. CockroachDB を知る 1. How Does CockroachDB Construct Transactions?

  6. CockroachDB • Spanner 系の分散データベース ◦ インタフェースとして SQL をサポート ◦ 強い一貫性を持ったトランザクションをサポート

    ◦ 旧来の RDB と異なりスケールアウト可能 • データは Node 間で冗長化される ◦ 一定規模以下の Node 障害時でもデータを守れる
  7. 中身はどうなっているのか

  8. SQL Transaction Distribution Replication Storage クライアントから見た 処理の流れ

  9. SQL Transaction Distribution Replication Storage SQL を Key-Value 命令に変換 トランザクション管理(本日メイン)

    適切な Node に命令をルーティング Node 間でデータを冗長化 データをディスクに永続化(省略)
  10. SQL Transaction Distribution Replication Storage SQL を Key-Value 命令に変換

  11. id (PK) author like 1 alice 4 2 alice 8

    3 bob 5 table: posts
  12. id (PK) author like 1 alice 4 2 alice 8

    3 bob 5 table: posts /posts/2/author /posts/2/like /posts/3/id /posts/3/author /posts/3/like /posts/1/id /posts/1/author /posts/1/like /posts/2/id key alice 8 _ bob 5 _ alice 4 _ value
  13. SQL Transaction Distribution Replication Storage 適切な Node に命令をルーティング

  14. key value すべての Key-Value

  15. key value Range 1 (512 MiB) Range 2 (512 MiB)

    Range 3 (512 MiB)
  16. Node 2 Distribution Layer Node 3 Node 1 Range 1

    Range 2 Range 3
  17. Node 2 Distribution Layer Node 3 Node 1 Range 1

    Range 2 Range 3 PUT k1 = v1 (k1 ∈ Range 1)
  18. Node 2 Distribution Layer Node 3 Node 1 Range 1

    Range 2 Range 3
  19. Node 障害で Range 内のデータが喪失

  20. SQL Transaction Distribution Replication Storage Node 間でデータを冗長化

  21. Raft • 合意 (Consensus) プロトコル ◦ 一度、合意が得られればその値は覆らない • Leader Election

    ◦ 見本となる Node (Leader, LeaseHolder) を選択 • Log Replication ◦ 操作列について合意し leaseholder の状態を複製
  22. Node 2 Distribution Layer Node 3 Node 1 Range 1

    Range 2 Range 3 Range 1 Range 2 Range 3 Range 1 Range 2 Range 3
  23. Raft Consensus Groups 1, 2, 3 Range 1 (LH) Range

    2 Range 3 Range 1 Range 2 (LH) Range 3 Range 1 Range 2 Range 3 (LH) (LH = LeaseHolder)
  24. Node 2 Distribution Layer Node 3 Node 1 k1 =

    v1 PUT k1 = v1 (k1 ∈ Range 1)
  25. Node 2 Range 1: Raft Consensus Node 3 Node 1

    k1 = v1 k1 = v1 k1 = v1
  26. Node 2 Distribution Layer Node 3 Node 1 k1 =

    v1 k1 = v1 k1 = v1 Ack Success
  27. Node よりむしろ Range が基本単位

  28. Range 2 Distribution Layer Range 3 Range 1 k1 =

    v1 PUT k1 = v1 (k1 ∈ Range 1)
  29. Range 2 Distribution Layer Range 3 Range 1 k1 =

    v1 PUT k1 = v1 (k1 ∈ Range 1)
  30. Distribution Layer Range 1 k1 = v1 PUT k1 =

    v1 (k1 ∈ Range 1) • 各 Range は Raft で可用性と一貫性が 保たれた一塊の DB とみなせる • Range 間にはコミュニケーションの 仕組みがない
  31. INSERT INTO table VALUES (k1, v1), (k2, v2); SQL Layer

  32. INSERT INTO table VALUES (k1, v1), (k2, v2); PUT k1

    = v1 + PUT k2 = v2 SQL Layer
  33. Range 2 Range 3 Range 1 Client 1 Client 2

    k1 = v1 k2 = v2
  34. Range 2 Range 3 Range 1 k1 = v1’ k2

    = v2’ (合意待ち) Client 1 Client 2 k2 = v2
  35. Range 2 Range 3 Range 1 k1 = v1’ k2

    = v2 Client 1 Client 2 v1’ + v2 (?)
  36. 分散トランザクションが必要

  37. SQL Transaction Distribution Replication Storage トランザクション管理(本日メイン)

  38. Transaction Record 1. トランザクション開始時、Key-Value データと同様に Pending 状態の Transaction Record を書き込む

    2. データを書き込む際は上書きする代わりに、 Intent と呼ばれるマーカをつけ、Record を参照 3. 全てのデータ書き込みが成功したら Record の状態を Committed に変更することでコミット
  39. Range 2 Range 3 Range 1 k1 = v1 k2

    = v2 Client 1 Client 2
  40. Range 2 Range 3 Range 1 k1 = v1 k2

    = v2 Client 1 Client 2 k1 = v1’ : t1 k2 = v2’ : t1 t1 = pending Round-1 Consensus
  41. Range 2 Range 3 Range 1 k1 = v1 k2

    = v2 Client 1 Client 2 k1 = v1’ : t1 k2 = v2’ : t1 t1 = pending Round-1 Consensus
  42. Range 2 Range 3 Range 1 k1 = v1 k2

    = v2 Client 1 Client 2 k1 = v1’ : t1 k2 = v2’ : t1 t1 = committed Round-2 Consensus
  43. Range 2 Range 3 Range 1 k1 = v1 k2

    = v2 Client 1 Client 2 k1 = v1’ : t1 k2 = v2’ : t1 t1 = committed Round-2 Consensus
  44. Transaction Record 1. データを読み込もうとした際、Intent を見つけたら まず対応する Transaction Record の状態を確認 2.

    Record が Pending 状態であれば元の値を採用 3. Record が Committed 状態であれば Intent として 保持されている値を採用
  45. Range 2 Range 3 Range 1 k1 = v1 k2

    = v2 Client 1 Client 2 k1 = v1’ (t1) k2 = v2’ (t1) t1 = pending
  46. Range 2 Range 3 Range 1 k1 = v1 k2

    = v2 Client 1 Client 2 k1 = v1’ (t1) k2 = v2’ (t1) t1 = pending
  47. Range 2 Range 3 Range 1 k1 = v1 k2

    = v2 Client 1 Client 2 k1 = v1’ (t1) k2 = v2’ (t1) t1 = pending
  48. Range 2 Range 3 Range 1 k1 = v1 k2

    = v2 Client 1 Client 2 k1 = v1’ (t1) k2 = v2’ (t1) t1 = pending v1 + v2
  49. 第 1 章のまとめ • CockroachDB のアーキテクチャ ◦ Range ごとに Raft

    Group を構成し合意 • 分散トランザクションの必要性 ◦ SQL に伴う Raft Group をまたいだ操作 • Transaction Record による実装 ◦ データ + Transaction Record で 2 回の合意
  50. As a database that prides itself on geo-distributed use cases,

    we must strive to reduce the latency incurred by common OLTP transactions to near the theoretical minimum: the sum of all read latencies plus one consensus latency. CockroachDB RFC: Parallel Commits https://github.com/cockroachdb/cockroach/blob/master/docs/RFCS/20180324_parallel_commit.md
  51. 合意を 1 ラウンド分にできないか?

  52. 形式手法に触れる 2. How Do Formal Methods Handle the Complexity?

  53. 例:投稿に「いいね!」する機能

  54. 各ユーザの動作 1. DB から現在の Like の個数を取得 2. ローカルでその値を + 1

    する 3. DB に新しい値を書き戻す
  55. DB Alice Bob 0 0

  56. DB Alice Bob 0 0 0 0

  57. DB Alice Bob 0 1 1 0 0 0

  58. DB Alice Bob 0 1 1 0 0 0 1

    1
  59. DB Alice Bob 0 1 1 0 0 0 1

    1
  60. Lost Update Anomaly

  61. 分散システムのテスト困難性 • マルチプロセスの問題 ◦ 動作する順番の全組み合わせを考える必要がある • 故障の問題 ◦ 個々の Node

    の動作にもランダム性がある • ネットワークの問題 ◦ 通信は遅延・消失し、順番も保存されない
  62. 理論的・体系的にバグを発見したい

  63. 形式手法 Formal Methods 形式手法

  64. 形式手法とは • システムを数学的対象により表現 ◦ その対象が満たす理論に基づいて検証 • エンドユーザ視点のメリット ◦ システムの挙動や仕様を曖昧さなく表現できる ◦

    ケースの抜けや漏れが原理的に生じない ◦ 実装ではなく仕様・設計に対して検査できる
  65. https://lamport.azurewebsites.net/tla/tla.html

  66. TLA+ の特徴 • モデル検査ツールとして使われることが多い ◦ IDE (TLA Toolbox) とセットで配布 ◦

    Lamport (Paxos の人) が中心となって開発 ◦ Temporal Logic of Actions と呼ばれる論理が基盤 • システムを状態遷移系として表現 ◦ 擬似プログラミング言語 PlusCal から生成可能
  67. TLA+ は実証的な事例が豊富

  68. CloudNative 界隈の TLA+ 人気 • AWS DynamoDB / S3 •

    Elasticsearch • Cosmos DB • TiDB • FINAL FANTASY XV POCKET EDITION ◦ DynamoDB の結果整合性が問題にならないか検証
  69. そして CockroachDB でも採用 形式手法ツール TLA+ により「証明」済 https://qconnewyork.com/ny2019/presentation/cockroachdb-architecture-geo-distributed-sql-database

  70. TLA+ による「いいね!」の検証

  71. process user \in { "alice", "bob" } variables x =

    0; begin Get: x := like; Incr: x := x + 1; Put: like := x; Ack: acked_users := acked_users + 1 end process; CountOK == acked_users = 2 => like = 2
  72. process user \in { "alice", "bob" } variables x =

    0; begin Get: x := like; Incr: x := x + 1; Put: like := x; Ack: acked_users := acked_users + 1 end process; CountOK == acked_users = 2 => like = 2 プロセス alice と bob が並行に動く
  73. process user \in { "alice", "bob" } variables x =

    0; begin Get: x := like; Incr: x := x + 1; Put: like := x; Ack: acked_users := acked_users + 1 end process; CountOK == acked_users = 2 => like = 2 ラベルが一つの実行ステップを表し その間では他プロセスの割り込みが入る
  74. process user \in { "alice", "bob" } variables x =

    0; begin Get: x := like; Incr: x := x + 1; Put: like := x; Ack: acked_users := acked_users + 1 end process; CountOK == acked_users = 2 => like = 2 検査したい条件
  75. None
  76. None
  77. None
  78. 第 2 章のまとめ • 分散システムをテストするのは難しい ◦ タイミングに依存したバグの再現性 • TLA+ による問題の発見

    ◦ 状態遷移系を網羅的に探索して条件を確認 • TLA+ が使用された事例 ◦ CockroachDB、Cosmos DB、TiDB など
  79. Parallel Commit を学ぶ 3. How Does the Protocol Reduce the

    Latencies?
  80. 合意のレイテンシを 1 回分に抑えたい

  81. Parallel Commit 1. データと Transaction Record を並列で書き込み 2. ただしこの時点での Record

    の状態は Staging とし、 トランザクションに属するキーのリストも付ける 3. データと Record が合意して戻ってきたら クライアントにトランザクション完了通知を出す 4. 最後に Record の状態を Committed に変更
  82. Range 2 Range 3 Range 1 k1 = v1 k2

    = v2 Client 1 Client 2
  83. Range 2 Range 3 Range 1 k1 = v1 k2

    = v2 Client 1 Client 2 k1 = v1’ (t1) k2 = v2’ (t1) t1 = staging : k1, k2 Round-1 Consensus
  84. Range 2 Range 3 Range 1 k1 = v1 k2

    = v2 Client 1 Client 2 k1 = v1’ (t1) k2 = v2’ (t1) t1 = staging : k1, k2 Round-1 Consensus
  85. Range 2 Range 3 Range 1 k1 = v1 k2

    = v2 Client 1 Client 2 k1 = v1’ (t1) k2 = v2’ (t1) t1 = committed : k1, k2 Round-2 Consensus
  86. Range 2 Range 3 Range 1 k1 = v1 k2

    = v2 Client 1 Client 2 k1 = v1’ (t1) k2 = v2’ (t1) t1 = committed : k1, k2 Round-2 Consensus
  87. 結局、合意 2 回分が必要なのでは

  88. Transaction Recovery 1. Intent を発見したら Transaction Record を確認 2. その

    Record が Staging だった場合、リストされている キーについて Intent が書き込み成功かどうか確認 3. 全キーが OK なら Record を Committed に変更 4. まだ書き込まれていないキーを見つけた場合は Conflict と判断して Record を Aborted に変更
  89. Range 2 Range 3 Range 1 k1 = v1 k2

    = v2 Client 1 Client 2 k1 = v1’ (t1) k2 = v2’ (t1) t1 = staging : k1, k2
  90. Range 2 Range 3 Range 1 k1 = v1 k2

    = v2 Client 1 Client 2 k1 = v1’ (t1) k2 = v2’ (t1) t1 = staging : k1, k2
  91. Range 2 Range 3 Range 1 k1 = v1 k2

    = v2 Client 1 Client 2 k1 = v1’ (t1) k2 = v2’ (t1) t1 = staging : k1, k2
  92. Range 2 Range 3 Range 1 k1 = v1 k2

    = v2 Client 1 Client 2 k1 = v1’ (t1) k2 = v2’ (t1) t1 = staging : k1, k2 v1’ + v2’
  93. Range 2 Range 3 Range 1 k1 = v1 k2

    = v2 Client 1 Client 2 k1 = v1’ (t1) k2 = v2’ (t1) t1 = committed : k1, k2
  94. トランザクション途中だった場合

  95. Range 2 Range 3 Range 1 k1 = v1 k2

    = v2 Client 1 Client 2 k1 = v1’ (t1) t1 = staging : k1, k2
  96. Range 2 Range 3 Range 1 k1 = v1 k2

    = v2 Client 1 Client 2 k1 = v1’ (t1) t1 = staging : k1, k2
  97. Range 2 Range 3 Range 1 k1 = v1 k2

    = v2 Client 1 Client 2 k1 = v1’ (t1) t1 = staging : k1, k2
  98. Range 2 Range 3 Range 1 k1 = v1 k2

    = v2 Client 1 Client 2 k1 = v1’ (t1) t1 = staging : k1, k2 (No k2 intent)
  99. Range 2 Range 3 Range 1 k1 = v1 k2

    = v2 Client 1 Client 2 k1 = v1’ (t1) t1 = aborted : k1, k2 v1 + v2
  100. TLA+ ではどう表現できるのか?

  101. コミット状態の再定式化 • 暗黙的コミット済み状態(読み取り可能状態) ◦ Transaction Record が Staging 状態 ◦

    かつ全てのキーについて Intent が書き込み成功 • 明示的コミット済み状態 ◦ Transaction Record が Committed 状態 ◦ Parallel Commit 導入前と同じ、安全側の条件
  102. ImplicitlyCommitted == /\ record.status = "staging" /\ \A k \in

    KEYS: /\ intent_writes[k].epoch = record.epoch /\ intent_writes[k].ts <= record.ts ExplicitlyCommitted == Record.status = "committed"
  103. ImplicitlyCommitted == /\ record.status = "staging" /\ \A k \in

    KEYS: /\ intent_writes[k].epoch = record.epoch /\ intent_writes[k].ts <= record.ts ExplicitlyCommitted == Record.status = "committed" 任意の key ∈ KEYS に対し
  104. 保証したい性質 • トランザクション完了通知の正しさ ◦ 完了通知の際は必ずコミット済みである ◦ ただし暗黙的コミット済みの段階で構わない • 暗黙的コミット済み状態の妥当性 ◦

    暗黙的にコミットされればいずれ明示的にもされる ◦ 仮に Node が故障した場合であっても保証したい
  105. 安全性と活性 • 安全性 (Safety) ◦ 何か悪いことが「起こらない」ことを要求 ◦ 実行中、常に有効なアサーションのようなもの • 活性

    (Liveness) ◦ 何か良いことがいつかは「起こる」ことを要求 ◦ 何もしないシステムは無意味なので検証には必須
  106. A A A A が成り立つ: いつか A が成り立つ:

  107. A A A A が成り立つ:個々の状態に対する条件 いつか A が成り立つ: True True

    False False
  108. A A A A が成り立つ:個々の状態に対する条件 いつか A が成り立つ:状態の「列」に対する条件 True False

    True True False False
  109. 普通の条件式では表現できない

  110. 時相論理 (Temporal Logic) • 通常の論理式に以下の記号を追加した体系 ◦ □A:現在の状態から先では常に A が真 ◦

    ◇A:現在の状態から A が真になるルートが存在 • 状態の「列」に対して真偽が決まる ◦ 一連の実行の様子に対して条件を定義できる ◦ 真偽の与え方は CTL、LTL などいくつか存在
  111. A □A:現時点以降、常に A が成り立つ ◇A:現時点以降、いつかは A が成り立つ A A A

    A A
  112. A □A:現時点以降、常に A が成り立つ ◇A:現時点以降、いつかは A が成り立つ A A A

    A A False True
  113. A □A:現時点以降、常に A が成り立つ ◇A:現時点以降、いつかは A が成り立つ True False A

    A A A A False True
  114. AckImpliesCommit == commit_ack => ImplicitlyCommitted \/ ExplicitlyCommitted ImplicitCommitLeadsToExplicitCommit == ImplicitlyCommitted

    ~> ExplicitlyCommitted AckLeadsToExplicitCommit == commit_ack ~> ExplicitlyCommitted
  115. AckImpliesCommit == commit_ack => ImplicitlyCommitted \/ ExplicitlyCommitted ImplicitCommitLeadsToExplicitCommit == ImplicitlyCommitted

    ~> ExplicitlyCommitted AckLeadsToExplicitCommit == commit_ack ~> ExplicitlyCommitted 安全性
  116. AckImpliesCommit == commit_ack => ImplicitlyCommitted \/ ExplicitlyCommitted ImplicitCommitLeadsToExplicitCommit == ImplicitlyCommitted

    ~> ExplicitlyCommitted AckLeadsToExplicitCommit == commit_ack ~> ExplicitlyCommitted 活性
  117. AckImpliesCommit == commit_ack => ImplicitlyCommitted \/ ExplicitlyCommitted ImplicitCommitLeadsToExplicitCommit == ImplicitlyCommitted

    ~> ExplicitlyCommitted AckLeadsToExplicitCommit == commit_ack ~> ExplicitlyCommitted A ~> B (lead to) 一度 A が成立すると その後いつか B も成立
  118. AckImpliesCommit == commit_ack => ImplicitlyCommitted \/ ExplicitlyCommitted ImplicitCommitLeadsToExplicitCommit == ImplicitlyCommitted

    ~> ExplicitlyCommitted AckLeadsToExplicitCommit == commit_ack ~> ExplicitlyCommitted A ~> B == A => <>B
  119. Node の故障をどう表現するか?

  120. プロセスの公平性 • 公平性なし(TLA+ のデフォルト) ◦ まったく動かない可能性がある • 弱い公平性の仮定(fair をつけた場合) ◦

    常に動ける状態ならいつか必ず動く • 強い公平性の仮定 ◦ 動ける瞬間が無限回来るならいつか必ず動く
  121. process committer = "committer" begin ... end process; fair process

    preventer \in PREVENTER begin ... end process;
  122. process committer = "committer" begin ... end process; fair process

    preventer \in PREVENTER begin ... end process; トランザクションの持ち主のプロセスが 任意のタイミングで Stall する分岐を生成
  123. process committer = "committer" begin ... end process; fair process

    preventer \in PREVENTER begin ... end process; それ以外のプロセスは、動ける状況が続けば 必ずどこかのタイミングでは動く
  124. 通信の遅延・消失をどう表現するか?

  125. PipelineWrites: while to_write /= {} do with key \in to_write

    do to_write := to_write \ {key}; ... end with; end while;
  126. PipelineWrites: while to_write /= {} do with key \in to_write

    do to_write := to_write \ {key}; ... end with; end while; Key を一つ取り出す際、 すべての「取り出し方」を 網羅して分岐を生成
  127. ... either Intent_writes[k] := [ epoch |-> txn_epoch, ts |->

    txn_ts, resolved |-> FALSE ]; or skip; end either; ...
  128. ... either Intent_writes[k] := [ epoch |-> txn_epoch, ts |->

    txn_ts, resolved |-> FALSE ]; or skip; end either; ... 二つのパターンのうち どちらが選ばれるかを両方考えて 分岐を生成
  129. ... either Intent_writes[k] := [ epoch |-> txn_epoch, ts |->

    txn_ts, resolved |-> FALSE ]; or skip; end either; ... 書き込みが成功したパターン
  130. ... either Intent_writes[k] := [ epoch |-> txn_epoch, ts |->

    txn_ts, resolved |-> FALSE ]; or skip; end either; ... 書き込みが失敗したパターン
  131. ランダム性を網羅性に • 分散システムに特有な非決定的な動作の扱い ◦ 通信の失敗:成功・失敗でランダムに分岐 ◦ 通信の非順序性:値をランダムに取り出す • E2E テストと違い、実際に実行するわけではない

    ◦ 全ての可能性を考慮して状態遷移を分岐させる ◦ 起こりうる全てのランダム性が考慮できる
  132. 第 3 章のまとめ • Parallel Commits のプロトコル ◦ Staging 状態の導入と

    Transaction Recovery • 時相論理式の導入 ◦ 時間的な幅を持った実行列に関する仕様を検証 • ランダム実行ではなく分岐の網羅 ◦ 障害の際にもプロトコルが正しく動くかを検証
  133. 本日のまとめ 4. Wrap Up the Session!

  134. 本日のまとめ • CockroachDB と Parallel Commit ◦ 一貫性を保ったままパフォーマンスを改善したい • 分散システムのテスト困難性

    ◦ 動作順・故障・通信のランダムネス • 形式手法を利用したシステムの記述や検証 ◦ より厳密な仕様の理解と膨大なパターン網羅
  135. We found that the process of writing this specification gave

    us more confidence in the Parallel Commit protocol itself and in its integration into CockroachDB. Parallel Commits: An Atomic Commit Protocol For Globally Distributed Transactions https://www.cockroachlabs.com/blog/parallel-commits/
  136. Have a Nice Formalism! Presented by チェシャ猫 (@y_taka_23)