MySQL5.7 GA の Multi-threaded slave

7,452 views

Published on

MySQL5.7 GA の Multi-threaded slave のお話です

Published in: Technology
  • Be the first to comment

MySQL5.7 GA の Multi-threaded slave

  1. 1. MySQL5.7 GA の Multi-Threaded Slave 瀬島 貴則瀬島 貴則
  2. 2. 免責事項 - 本資料は個人の見解であり、私が所属する組 織の見解とは必ずしも一致しません。 - 内容の一部に偏ったものがあるかもしれません が、各自オトナの判断でよろしくお願いします。 - MySQL 5.7.12 や 5.7.13を読みつつ書いてま す。最近はGAリリース以降も機能追加されたり するので、そのへんはご了承下さい。
  3. 3. 自己紹介 - わりとMySQLでごはんたべてます - 一時期は Resource Monitoring もよくやってま した - Twitter: @ts4th
  4. 4. ちょっと宣伝 - 最近はわりとスライドを公開してますので - よろしかったら参考までに - http://www.slideshare.net/takanorisejima
  5. 5. 今日のお題 - MySQL5.7 GA で Multi-Threaded Slave (MTS) が改善されました - --slave-parallel-type=LOGICAL_CLOCK が 追加されて、一見、良さそうなんですが - その実装についてまとめられた記事をみかけな いので、ざっくりまとめてみました - 有識者からのマサカリ歓迎します
  6. 6. というかぶっちゃけ - 自分でもコード読んでて難しいなと思ったので - 「ここって正確にはこうじゃない?」と思った有識 者の方は - 積極的にマサカリ投げてください - むしろ投げて
  7. 7. では、 はじめます
  8. 8. 5.7で導入された LOGICAL_CLOCK - MySQL 5.6 の MTS の実装である slave-parallel-type=DATABASE と異なり、 同じ DATABASE でも、 slave が複数 thread で更新可能 - MySQL の伝統的な replication は SQL_Thread がシング ルスレッドであるがゆえに、 master の更新頻度が高いと slave の SQL_Thread がボトルネックになって、 replication の遅延が発生することがあった - master は複数の Thread で更新処理を実行できたが、 slave は SQL_Thread のみで更新する実装だった
  9. 9. 夢のような機能ではあるけれど - どのようにして、同時実行可能だと判断するの か? - slave が複数の Thread で更新する場合、 slave の整合性はどうやって保たれるのか? - master の binlog とどうやって見比べれば良いのか? - stop slave したときの振る舞いは? - master <-> slave 間の connection が切れたと き、再接続や retry は?
  10. 10. 込み入った実装について書い てあるドキュメントや blog 等が 見当たらないので、どうやって 実現しているのかがわからない
  11. 11. よろしいならば
  12. 12. コードを読もう
  13. 13. だがしかし
  14. 14. なにこれ むずかしい
  15. 15. コードだけでは難しいので、 先ずは設計思想を理解しよう
  16. 16. 次にわたしがとった行動 - sql/rpl_rli_pdb.cc の commit log を漁る - 関連しそうな WorkLog を読む - 知らない用語が出てきたら調べる - ソースコードと MySQL5.7 が出力するバイナリ ログを眺めてみる
  17. 17. おおむね わかった
  18. 18. 一通り見てわかったのは - これ初見でソースコードだけ読んで理解するの ハードル高いわ - なんで理解できなかったかわかったわ - というわけで、一つ一つ解説します
  19. 19. 一つ一つ 見ていきましょう
  20. 20. はじめに - そもそも、 slave の SQL_Thread がシングルス レッドのとき、どのようにして replication で master と同じ状態が復元されるのか? - いたってシンプル - master が注意深く binlog 吐いてる
  21. 21. 例えば InnoDB の場合 1. master で更新処理実行中の各スレッドが、そ れぞれ transaction cache に更新内容をため ていく 2. InnoDB で PREPARE する(5.7.10 以降、 innodb_support_xa は常に true) 3. 1. の transaction cache から一連の更新処理 を BEGIN&COMMIT で挟んで binlogに書く 4. InnoDBで COMMIT する
  22. 22. Two-Phase Commit & Group Commit - MySQL の Replication 開発者であらせられる Dr. Mats Kindahl の blog この記事がわかりや すいですが - Binary Log Group Commit in MySQL 5.6 - (この後の話に関連して)大事なところを二つだ けかいつまんで解説すると
  23. 23. Two-Phase Commit(2PC) - 参考になるのは ha_commit_trans() や MYSQL_BIN_LOG::ordered_commit() あたり - Binary Log Group Commit in MySQL 5.6 の Figure.1 のとおり - storage engine(InnoDBなど)に prepare して - binlog に 書いて - binlog に COMMIT(fsync) してから - storage engineに COMMIT する
  24. 24. Transaction Coordinator Log - ソースコード中に tc_log ってのが出てきますが - Transaction の順序を管理するための Log の 抽象クラスが TC_LOG であって、その実装の ひとつが MYSQL_BIN_LOG - MYSQL_BIN_LOG::prepare() や MYSQL_BIN_LOG::commit() が、 Two-Phase COMMIT を実現するために必要 な関数を呼んでる
  25. 25. innodb_support_xa=true と 2PC - innodb_support_xa=true だと、 prepare のと き undo log に xid が書き込まれる(5.7.10以降 は常にそうなる) - undo log に xid 書き込まれた PREPARED な transaction は、 クラッシュ後の再起動時、 binlog から xid 読み込んだ後、その xid 使って innobase_commit_by_xid() で最終的に COMMIT される
  26. 26. なんかややこしいですが - クラッシュリカバリ時、xid のない PREPARED は rollback の 対象になるんですが、 xid つき の PREPARED は binlog からその xid が取得 できれば COMMIT にできるようです。詳しくは - innobase_xa_prepare() - MYSQL_BIN_LOG::recover() - innobase_xa_recover() - innobase_commit_by_xid()
  27. 27. というわけで、 MySQL の 2PC は - InnoDB のクラッシュリカバリ機能単体では実現 できず、 InnoDB のクラッシュリカバリ機能と binlog のクラッシュリカバリ機能とが組み合わ さって、実現されてるようです - binlog のヘッダには open するときに立てて close する ときにリセットするフラグがあるので、正常に close した か(クラッシュしてないか)は、フラグをみて判断してます
  28. 28. Group Commit - Binary Log Group Commit in MySQL 5.6 の Figure.5 を参照 - flush/sync/commit という stage がある - binlog へ書き出す のが flush stage - binlog に fsync() する のが sync stage - storage engine に commit するのが commit stage - flush stage に書きだした順序で、 commit stage で commit することが保証されている
  29. 29. ソースコード的にいうと - Group Commit はまさに MYSQL_BIN_LOG::ordered_commit() - flush/sync/commit の stage を queue で管理 することによって、 fsync() の回数を減らして、 binlog に event 書き出す順番と storage engine に commit する順番を担保している - そして、 binlog に書くとき、各 Transaction を BEGIN - COMMIT でシリアライズしてる
  30. 30. だから binary log は読みやすいし - そして slave の SQL_Thread は性能がでない - master は Transaction を並列実行しながらも、 それらをひとかたまりの BEGIN - COMMIT に まとめシリアライズして binlog に吐いている - master では並列実行してる Transaction が、 slave だと BEGIN - COMMIT のひとかたまり が、ひとつずつしか実行できない - まぁ SQL_Thread はシングルスレッドだしね
  31. 31. 次に Anonymous_gtid_log_event - なぜここでGTIDの話がはじまるのか? - WL#7592: GTIDs: generate Gtid_log_event and Previous_gtids_log_event always - MySQL5.7.6 以降は、 GTID_MODE=OFF の ときでも、 Anonymous_gtid_log_event を出力 します - それはなぜか - 理由は二つ
  32. 32. WL#7083 はわかる - WL#7592いわく - Therefore, we need to generate a per-transaction event also when GTID_MODE = OFF; this is needed e.g. for WL#7083 and WL#7165. - WL#7083は、GTIDをオンラインで有効化する ための修正らしいです。 - なるほどGTIDのためならしょうがない
  33. 33. しかし WL#7165 は - WL#7165: MTS: Optimizing MTS scheduling by increasing the parallelization window on master - Anonymous_gtid_log_event や Gtid_log_event には、 MTS を最適化するため の、 logical timestamp が埋め込まれているそ うです
  34. 34. それGTID関係ないよ! 全然関係ないよ!!
  35. 35. 気を取り直して logical timestamp とは - WL#6314: MTS: Prepared transactions slave parallel applier で解説されてます - Lamport clock を使っているようです。 - (すごい雑にいうと)、 slave で並列実行可能で あること示すヒントを、 master は binlog に埋め 込み、 slave は binlog からヒントを読んで、複 数のTransactionを並列実行するようです。
  36. 36. Lamport Clock とは - Lamport Timestamps とも呼ばれるようです - 分散処理システムで使われているアルゴリズム のようですが、よくできていてわりとシンプルな 考え方です - 詳しくは後ほど
  37. 37. Anonymous_gtid_log_event には - last_committed と sequence_number が 8byte ずつ埋め込まれている。これが MySQL での logical timestamp。 - sequence_number は、master で binlog に Transaction をflushする度に increment される - last_committed は、master で commit 済みの Transaction のうち、最も値の大きい sequence_number
  38. 38. そしてAnonymous_gtid_log_eventは - binlog に BEGIN 書きだす前に、出力されてい ます。 - ANONYMOUS_GTID(or GTID) -> BEGIN -> (statement or row) -> COMMIT という順で書 かれるわけです。 - ゆえに、後続する Transaction(BEGIN - COMMIT)に sequence_number を付与できる わけです。
  39. 39. つまるところ - --slave-parallel-type=LOGICAL_CLOCK のと き、 GTID の log_event に埋め込まれた logical timestamp を利用している。 - slave で複数の Transaction が同時に実行さ れたら、それらの Transaction に紐付いた sequence_number が、(最終的に) slave の last_lwm_timestamp を更新している - lwm == low-water-mark
  40. 40. 図に描くとこう
  41. 41. 一つ一つ 見ていきましょう
  42. 42. last_committed - binlog 読んだとき、 その Transaction に紐付 いてる last_committed が last_lwm_timestamp より小さければ、実行可 能と slave は判断する - last_committed は、その Transaction が lock を取得するまでに、 COMMIT が完了している べき Transaction を示す値
  43. 43. Group Assigned Queue(GAQ) - last_lwm_timestamp は GAQ と関係している - GAQ は、 5.6 のMTSで(WL#5569)できた概 念で、雑にいうと - GAQ は、Coordinator Thread が Relay Log から binlog event 読みだして Worker Thread に 渡すとき、 管理に使う、 固定長の queue - Transaction という job の Group をどのWorker Thread に Assign して、実行完了したかどうかを管理。
  44. 44. GAQ の checkpoint - 具体的には mts_checkpoint_routine() - GAQ使い切るか、次のいずれかのタイミングで - slave_checkpoint_group 回 Transaction を実行 - slave_checkpoint_period msec 経過したとき - 次のような処理をする - 実行完了した Transaction のエントリを GAQ から削除 - SHOW SLAVE STATUS で表示される情報を更新 - GAQ.lwm を更新。これ重要大変重要 - これが最終的に last_lwm_timestamp を更新する
  45. 45. last_lwm_timestamp の必要性 - Coordinator Thread が checkpoint で GAQ.lwm を更新する理由(推測) - Transaction 完了した worker thread が直接 last_lwm_timstamp を更新 するとマズイ - 複数の Worker が Transaction を実行する場合、古くて時間のかかる Transaction が残ってるかもしれない - Transaction を assign した Coordinator がときどき Worker の状態を見 て、 どこまで Transaction 捌けてるか確認して lwm 更新する方が良い
  46. 46. というのが、 LOGICAL_CLOCK に 基づいた MTS
  47. 47. さしあたって - slave-parallel-type=LOGICAL_CLOCK で性 能上がるかどうかは - master の binlog で Gtid_log_event や  Anonymous_gtid_log_event の last_committed をみて、同じ last_committed がいくつあるか見ると、参考になる - 同じ last_committed の イベントが多いということは、そ れだけ slave で同時に実行できる Transaction が多い
  48. 48. 例えば - 同じ last_committed の値を数えられるから - $ mysqlbinlog ${BINLOG_FILE} | egrep ‘GTID.*last_committed’ | awk '{print $11}' | sort | uniq -c - 同時実行可能な Transaction の数の上限を、 ざっくり数えられる - $ mysqlbinlog ${BINLOG_FILE} | egrep ‘GTID.*last_committed’| awk '{print $11}' | sort | uniq -c | sort -n | tail
  49. 49. 次に
  50. 50. - last_committed を基準に複数の worker thread が Transaction を実行していいというこ とになると、 last_committed 的にOKなら、 InnoDB の COMMIT の順序はどうなってもい いということになる - ということは、 slave が複数存在した場合、 slave ごとに COMMIT の順序が異なるというこ とになる consistency の問題
  51. 51. slave ごとに COMMIT の順が異なると - replication が遅延してるしてないの問題ではな く - slave ごとに異なる状態が見えてしまう - 例えば、 master で Table A, Table B という二 つのTable にそれぞれ Record X, Record Yが 別々の Transaction から INSERT されたとき - X しか見えない slave と、 Y しか見えない slave が存在しうることになる
  52. 52. slave-parallel-type=DATABASE では - この consistency の問題を回避するすべがな い - DATABASE が複数あった場合、DATABASE 間で更新順序が保証されない - 順序が保証されなくても、最終的に整合性は保 たれるだろうけど - 例えば、master で更新処理が終わったとき、すべての slave の table は同じ状態にあるはず
  53. 53. slave-paralell-type= DATABASE のことは
  54. 54. 存在自体 忘れようと わたしは決めた
  55. 55. ※感想には個人差があります
  56. 56. consistency 関連の WorkLog - WL#6813: MTS: ordered commits (sequential consistency) - すべての slave は master の binlog と同じ順 番で COMMIT するべきだという WorkLog です - そのためにでてくるオプションが slave_preserve_commit_order
  57. 57. slave_preserve_commit_order - blog にもありますが制限がいくつかあります - On master - binlog_order_commits should be enabled - On slave - binlog_order_commits should be enabled - binary log should be enabled - log_slave_update should be enabled - slave_preserve_commit_order should be enabled - そして LOGICAL_CLOCK 必須
  58. 58. なぜ log_slave_updates ? - Commit_order_manager のインスタンスが生 成される条件 になってるんですが - slave で binlog を吐くときに、 Two-Phase Commit や Group Commit を使うことで、 InnoDB の COMMIT 順を制御できるから - slave で binlog の COMMIT 順を担保することで、 InnoDB の COMMIT 順を担保している - Master の Group Commit をリプレイしてる
  59. 59. ざっくり流れとしては 1. slave の cordinator thread が relay log 読む 2. cordinator thread が Commit_order_manager::register_trx() で Commit_order_manager の FIFO な queue に worker thread の id つむ 3. worker thread が並行して Transaction 実行 4. process_flush_stage_queue() で、 2. の queue の順に binlog を書き出す(FLUSH)
  60. 60. そして 5. sync_binlog_file() で binlog を fsync() する (sync_binlog の値次第で、 fsync しないことも あるけれど) 6. process_commit_stage_queue() で InnoDB に COMMIT する
  61. 61. 図に描くとこう
  62. 62. かくして slave の COMMIT の順序は master の binlog の順序に 従って COMMIT されるようになり
  63. 63. slave の世界に 整合性がもたらされ るのだが
  64. 64. なんという パワープレイ!
  65. 65. ※感想には個人差があります
  66. 66. それから、 Slave での retry - 5.6 のとき、松信さんが MTS でも slave_transaction_retries 有効にして欲しい と バグレポートあげておられたのですが - WL#6964 で対応されました - slave で一時的なエラーが発生したとしても、こ れで自動で対応可能に - 例えば、 MTS で worker thread 起動しすぎるなどして transaction が timeout しちゃったとしても、 retry できる
  67. 67. あと、補足すると - MTS & Statement-Based Replication & 非決 定性クエリの組み合わせはダメゼッタイ - INSERT … SELECT など、 MTS だと slave ごとに結 果が変わってもおかしくない - MTS & READ UNCOMMITTED の組み合わ せもヤバイと思う - slave_preserve_commit_order で保証されるのは、 binlog の COMMIT の順番だけなので
  68. 68. もうちょっと補足すると - slave_pending_jobs_size_max を master の max_allowed_packet より大きくするべき - Coordinator Thread が Worker Thread に job (binlog event)を積むとき、slave_pending_jobs_size_max よ り大きな binlog event を積めないので - どっちも default のままなら、充分な余裕があると思う
  69. 69. もっというと - LOGICAL_CLOCK ベースの MTS は、ある程 度 master が忙しくないと、効果が薄い - SQL_Thread でやってた仕事が、 cordinator thread と worker thread で分担されるので、オーバヘッドが大きく なる。 WL#6314 の Highe Level Architecture の 3.2 Problems にもそう書いてある
  70. 70. なにはともあれ - おおむね実装わかったし - slave_preserve_commit_order や log_slave_updates などを指定すれば、 master の binlog と同じ順序で MTS の slave も SQL 実行されるとわかった
  71. 71. とりあえず - かつて slave-parallel-type=DATABASE 相当 の実装しかなかったときは、 consistency の問 題が厳しかった(と思う) - それが今ではだいぶ楽になったんじゃないか なぁ。 - LOGICAL_CLOCK 使う分には
  72. 72. だがしかし
  73. 73. ここで 残念なお知らせが あります
  74. 74. MTS使うときは 5.7でも GTID推奨です!
  75. 75. かつて Percona の人は言いました - MySQL5.6 で MTS 使うなら GTID 有効にしま しょうと - sql_slave_skip_counter するときなどがつらいんで - 5.7 は slave_preserve_commit_order のおか げでだいぶ良くなったんだけど - すべてはただ一つの問題
  76. 76. MTS有効にしたときは Exec_Master_Log_Pos の意味が変わってしまう
  77. 77. Exec_Master_Log_Pos の意味 - MTS 使うとき、 relay_log_info_repository = ‘TABLE’ にして select * from mysql.slave_worker_info; するとわかる - (worker thread が更新処理を実行していると き、)worker thread ごとの Master_log_pos と Exec_Master_Log_Pos は一致しない。
  78. 78. SHOW SLAVE STATUS の更新頻度 - MTS のとき、 Exec_Master_Log_Pos などは リアルタイムで更新されない - 先ほど出てきた、 GAQ の checkpoint のタイミ ングで、 Exec_Master_Log_Pos などが更新さ れる - MTS を使う場合、 SHOW SLAVE STATUS だ けに頼るわけには行かなくなってくる
  79. 79. いちおう、ドキュメントにも書いてある - 18.4.1.34 Replication and Transaction Inconsistencies - 課題は3つ - Half-applied transaction - Gap - Gap-free low-watermark position
  80. 80. Half-applied transacitons - SQL_Thread を KILL などしたとき、 rollback できないと、 transaction の Atomicity が保た れない - まぁこれは InnoDB 使えばいいでしょ - MTS 有効なときでも slave_transaction_retries 効くようになったし - MySQL 5.7 すばらしい
  81. 81. Gaps - ざっくりいうと、 5.6 以前の MTS だと binlog の 順に Transaction 実行される保証がないので、 「一時的なエラーなどで、 relay log の途中に実 行されてない event が残ったらどうなるの?」と いう話 - これは slave_preserve_commit_order で commit 順保証できるようになって、改善した - MySQL 5.7 すばらしい
  82. 82. Gap-free low-watermark position - これが Exec_Master_Log_Pos の話 - 5.7 で LOGICAL_CLOCK 使っても、コレが避 けられない - MTS 有効なとき、 mysql.slave_worker_info の Checkpoint_master_log_pos が、 Exec_Master_Log_Pos になる - worker_thread がどこまで relay log 上の event を実行 したかは、 Exec_Master_Log_Pos では分からない
  83. 83. そして、sql_slave_skip_counter 問題 - 特定の event だけ狙って skip するの、 MTS だとめんどくさい - ここはやっぱ GTID 使えるほうが便利
  84. 84. かつて Yahoo! Inc. の人は言いました - 昨年の Oracle Open World で - 「Multi-Threaded Replication 導入しつつ GTID 入れて、GTID入れるために Percona Server 5.6 導入した」と言ってたんですが - MySQL 5.7 になっても、 やっぱり GTID 使える ほうが、 MTS は導入しやすいママなんだなぁ - 遺憾でござる。
  85. 85. 今回、改めて思ったのは - あるていど難しい実装になると、コード読んだだ けでは新機能を理解できないこともある - その点、 MySQL は commit log から WorkLog 漁っていけば、設計思想を踏まえて理解してい くことができる - 非常にとっつきやすいOSSで素晴らしいなと思 いました
  86. 86. そして、いまやGTIDは - GTID は master の failover をシンプルにする ためだけではなく - GTID を踏まえつつ MTS が設計されているな ど、 replication の性能向上目的でも、(間接的 に)使われるようになってきてる - MySQL5.6 のとき、 Facebook さんらが GTID のバグレポートたくさんしてくれたし、そろそろ導 入していったほうが良い時期かなぁ
  87. 87. ただ、 GTID まだ入れられない場合でも - 「やっべ slave めっちゃ遅延してるどうしよう」と いう状況になったとき、非常手段として、一時的 に LOGICAL_CLOCK & slave_preserve_commit_order & log_slave_updates という手段が取れるように なったのはけっこう便利なんじゃないでしょうか - SET GLOBAL relay_log_info_repository = ‘TABLE’ できるから、意外と敷居高くないし
  88. 88. MTS使うときのまとめ - LOGICAL_CLOCK がよさそう - slave-parallel-type=LOGICAL_CLOCK - slave-parallel-workers > 1 - log_slave_updates=ON - slave_preserve_commit_order=ON - mysql.slave_worker_info 見えると良い - relay_log_info_repository = ‘TABLE’
  89. 89. - max_allowed_packet いじってるなら、 slave_pending_jobs_size_max も見なおそう - SET GLOBAL sql_slave_skip_counter =N; を 運用上使いたいなら、 GTID 有効にするのが無 難
  90. 90. おわり

×