MySQL5.7 GA の Multi-Threaded Slave 瀬島 貴則瀬島 貴則
免責事項 - 本資料は個人の見解であり、私が所属する組 織の見解とは必ずしも一致しません。 - 内容の一部に偏ったものがあるかもしれません が、各自オトナの判断でよろしくお願いします。 - MySQL 5.7.12 や 5.7.13を読みつつ書いてま す。最近はGAリリース以降も機能追加されたり するので、そのへんはご了承下さい。
自己紹介 - わりとMySQLでごはんたべてます - 一時期は Resource Monitoring もよくやってま した - Twitter: @ts4th
ちょっと宣伝 - 最近はわりとスライドを公開してますので - よろしかったら参考までに - http://www.slideshare.net/takanorisejima
今日のお題 - MySQL5.7 GA で Multi-Threaded Slave (MTS) が改善されました - --slave-parallel-type=LOGICAL_CLOCK が 追加されて、一見、良さそうなんですが - その実装についてまとめられた記事をみかけな いので、ざっくりまとめてみました - 有識者からのマサカリ歓迎します
というかぶっちゃけ - 自分でもコード読んでて難しいなと思ったので - 「ここって正確にはこうじゃない?」と思った有識 者の方は - 積極的にマサカリ投げてください - むしろ投げて
では、 はじめます
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 のみで更新する実装だった
夢のような機能ではあるけれど - どのようにして、同時実行可能だと判断するの か? - slave が複数の Thread で更新する場合、 slave の整合性はどうやって保たれるのか? - master の binlog とどうやって見比べれば良いのか? - stop slave したときの振る舞いは? - master <-> slave 間の connection が切れたと き、再接続や retry は?
込み入った実装について書い てあるドキュメントや blog 等が 見当たらないので、どうやって 実現しているのかがわからない
よろしいならば
コードを読もう
だがしかし
なにこれ むずかしい
コードだけでは難しいので、 先ずは設計思想を理解しよう
次にわたしがとった行動 - sql/rpl_rli_pdb.cc の commit log を漁る - 関連しそうな WorkLog を読む - 知らない用語が出てきたら調べる - ソースコードと MySQL5.7 が出力するバイナリ ログを眺めてみる
おおむね わかった
一通り見てわかったのは - これ初見でソースコードだけ読んで理解するの ハードル高いわ - なんで理解できなかったかわかったわ - というわけで、一つ一つ解説します
一つ一つ 見ていきましょう
はじめに - そもそも、 slave の SQL_Thread がシングルス レッドのとき、どのようにして replication で master と同じ状態が復元されるのか? - いたってシンプル - master が注意深く binlog 吐いてる
例えば 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 する
Two-Phase Commit & Group Commit - MySQL の Replication 開発者であらせられる Dr. Mats Kindahl の blog この記事がわかりや すいですが - Binary Log Group Commit in MySQL 5.6 - (この後の話に関連して)大事なところを二つだ けかいつまんで解説すると
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 する
Transaction Coordinator Log - ソースコード中に tc_log ってのが出てきますが - Transaction の順序を管理するための Log の 抽象クラスが TC_LOG であって、その実装の ひとつが MYSQL_BIN_LOG - MYSQL_BIN_LOG::prepare() や MYSQL_BIN_LOG::commit() が、 Two-Phase COMMIT を実現するために必要 な関数を呼んでる
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 される
なんかややこしいですが - クラッシュリカバリ時、xid のない PREPARED は rollback の 対象になるんですが、 xid つき の PREPARED は binlog からその xid が取得 できれば COMMIT にできるようです。詳しくは - innobase_xa_prepare() - MYSQL_BIN_LOG::recover() - innobase_xa_recover() - innobase_commit_by_xid()
というわけで、 MySQL の 2PC は - InnoDB のクラッシュリカバリ機能単体では実現 できず、 InnoDB のクラッシュリカバリ機能と binlog のクラッシュリカバリ機能とが組み合わ さって、実現されてるようです - binlog のヘッダには open するときに立てて close する ときにリセットするフラグがあるので、正常に close した か(クラッシュしてないか)は、フラグをみて判断してます
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 することが保証されている
ソースコード的にいうと - Group Commit はまさに MYSQL_BIN_LOG::ordered_commit() - flush/sync/commit の stage を queue で管理 することによって、 fsync() の回数を減らして、 binlog に event 書き出す順番と storage engine に commit する順番を担保している - そして、 binlog に書くとき、各 Transaction を BEGIN - COMMIT でシリアライズしてる
だから binary log は読みやすいし - そして slave の SQL_Thread は性能がでない - master は Transaction を並列実行しながらも、 それらをひとかたまりの BEGIN - COMMIT に まとめシリアライズして binlog に吐いている - master では並列実行してる Transaction が、 slave だと BEGIN - COMMIT のひとかたまり が、ひとつずつしか実行できない - まぁ SQL_Thread はシングルスレッドだしね
次に 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 を出力 します - それはなぜか - 理由は二つ
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のためならしょうがない
しかし 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 が埋め込まれているそ うです
それGTID関係ないよ! 全然関係ないよ!!
気を取り直して logical timestamp とは - WL#6314: MTS: Prepared transactions slave parallel applier で解説されてます - Lamport clock を使っているようです。 - (すごい雑にいうと)、 slave で並列実行可能で あること示すヒントを、 master は binlog に埋め 込み、 slave は binlog からヒントを読んで、複 数のTransactionを並列実行するようです。
Lamport Clock とは - Lamport Timestamps とも呼ばれるようです - 分散処理システムで使われているアルゴリズム のようですが、よくできていてわりとシンプルな 考え方です - 詳しくは後ほど
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
そしてAnonymous_gtid_log_eventは - binlog に BEGIN 書きだす前に、出力されてい ます。 - ANONYMOUS_GTID(or GTID) -> BEGIN -> (statement or row) -> COMMIT という順で書 かれるわけです。 - ゆえに、後続する Transaction(BEGIN - COMMIT)に sequence_number を付与できる わけです。
つまるところ - --slave-parallel-type=LOGICAL_CLOCK のと き、 GTID の log_event に埋め込まれた logical timestamp を利用している。 - slave で複数の Transaction が同時に実行さ れたら、それらの Transaction に紐付いた sequence_number が、(最終的に) slave の last_lwm_timestamp を更新している - lwm == low-water-mark
図に描くとこう
一つ一つ 見ていきましょう
last_committed - binlog 読んだとき、 その Transaction に紐付 いてる last_committed が last_lwm_timestamp より小さければ、実行可 能と slave は判断する - last_committed は、その Transaction が lock を取得するまでに、 COMMIT が完了している べき Transaction を示す値
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 して、実行完了したかどうかを管理。
GAQ の checkpoint - 具体的には mts_checkpoint_routine() - GAQ使い切るか、次のいずれかのタイミングで - slave_checkpoint_group 回 Transaction を実行 - slave_checkpoint_period msec 経過したとき - 次のような処理をする - 実行完了した Transaction のエントリを GAQ から削除 - SHOW SLAVE STATUS で表示される情報を更新 - GAQ.lwm を更新。これ重要大変重要 - これが最終的に last_lwm_timestamp を更新する
last_lwm_timestamp の必要性 - Coordinator Thread が checkpoint で GAQ.lwm を更新する理由(推測) - Transaction 完了した worker thread が直接 last_lwm_timstamp を更新 するとマズイ - 複数の Worker が Transaction を実行する場合、古くて時間のかかる Transaction が残ってるかもしれない - Transaction を assign した Coordinator がときどき Worker の状態を見 て、 どこまで Transaction 捌けてるか確認して lwm 更新する方が良い
というのが、 LOGICAL_CLOCK に 基づいた MTS
さしあたって - slave-parallel-type=LOGICAL_CLOCK で性 能上がるかどうかは - master の binlog で Gtid_log_event や  Anonymous_gtid_log_event の last_committed をみて、同じ last_committed がいくつあるか見ると、参考になる - 同じ last_committed の イベントが多いということは、そ れだけ slave で同時に実行できる Transaction が多い
例えば - 同じ 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
次に
- last_committed を基準に複数の worker thread が Transaction を実行していいというこ とになると、 last_committed 的にOKなら、 InnoDB の COMMIT の順序はどうなってもい いということになる - ということは、 slave が複数存在した場合、 slave ごとに COMMIT の順序が異なるというこ とになる consistency の問題
slave ごとに COMMIT の順が異なると - replication が遅延してるしてないの問題ではな く - slave ごとに異なる状態が見えてしまう - 例えば、 master で Table A, Table B という二 つのTable にそれぞれ Record X, Record Yが 別々の Transaction から INSERT されたとき - X しか見えない slave と、 Y しか見えない slave が存在しうることになる
slave-parallel-type=DATABASE では - この consistency の問題を回避するすべがな い - DATABASE が複数あった場合、DATABASE 間で更新順序が保証されない - 順序が保証されなくても、最終的に整合性は保 たれるだろうけど - 例えば、master で更新処理が終わったとき、すべての slave の table は同じ状態にあるはず
slave-paralell-type= DATABASE のことは
存在自体 忘れようと わたしは決めた
※感想には個人差があります
consistency 関連の WorkLog - WL#6813: MTS: ordered commits (sequential consistency) - すべての slave は master の binlog と同じ順 番で COMMIT するべきだという WorkLog です - そのためにでてくるオプションが slave_preserve_commit_order
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 必須
なぜ log_slave_updates ? - Commit_order_manager のインスタンスが生 成される条件 になってるんですが - slave で binlog を吐くときに、 Two-Phase Commit や Group Commit を使うことで、 InnoDB の COMMIT 順を制御できるから - slave で binlog の COMMIT 順を担保することで、 InnoDB の COMMIT 順を担保している - Master の Group Commit をリプレイしてる
ざっくり流れとしては 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)
そして 5. sync_binlog_file() で binlog を fsync() する (sync_binlog の値次第で、 fsync しないことも あるけれど) 6. process_commit_stage_queue() で InnoDB に COMMIT する
図に描くとこう
かくして slave の COMMIT の順序は master の binlog の順序に 従って COMMIT されるようになり
slave の世界に 整合性がもたらされ るのだが
なんという パワープレイ!
※感想には個人差があります
それから、 Slave での retry - 5.6 のとき、松信さんが MTS でも slave_transaction_retries 有効にして欲しい と バグレポートあげておられたのですが - WL#6964 で対応されました - slave で一時的なエラーが発生したとしても、こ れで自動で対応可能に - 例えば、 MTS で worker thread 起動しすぎるなどして transaction が timeout しちゃったとしても、 retry できる
あと、補足すると - MTS & Statement-Based Replication & 非決 定性クエリの組み合わせはダメゼッタイ - INSERT … SELECT など、 MTS だと slave ごとに結 果が変わってもおかしくない - MTS & READ UNCOMMITTED の組み合わ せもヤバイと思う - slave_preserve_commit_order で保証されるのは、 binlog の COMMIT の順番だけなので
もうちょっと補足すると - slave_pending_jobs_size_max を master の max_allowed_packet より大きくするべき - Coordinator Thread が Worker Thread に job (binlog event)を積むとき、slave_pending_jobs_size_max よ り大きな binlog event を積めないので - どっちも default のままなら、充分な余裕があると思う
もっというと - LOGICAL_CLOCK ベースの MTS は、ある程 度 master が忙しくないと、効果が薄い - SQL_Thread でやってた仕事が、 cordinator thread と worker thread で分担されるので、オーバヘッドが大きく なる。 WL#6314 の Highe Level Architecture の 3.2 Problems にもそう書いてある
なにはともあれ - おおむね実装わかったし - slave_preserve_commit_order や log_slave_updates などを指定すれば、 master の binlog と同じ順序で MTS の slave も SQL 実行されるとわかった
とりあえず - かつて slave-parallel-type=DATABASE 相当 の実装しかなかったときは、 consistency の問 題が厳しかった(と思う) - それが今ではだいぶ楽になったんじゃないか なぁ。 - LOGICAL_CLOCK 使う分には
だがしかし
ここで 残念なお知らせが あります
MTS使うときは 5.7でも GTID推奨です!
かつて Percona の人は言いました - MySQL5.6 で MTS 使うなら GTID 有効にしま しょうと - sql_slave_skip_counter するときなどがつらいんで - 5.7 は slave_preserve_commit_order のおか げでだいぶ良くなったんだけど - すべてはただ一つの問題
MTS有効にしたときは Exec_Master_Log_Pos の意味が変わってしまう
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 は一致しない。
SHOW SLAVE STATUS の更新頻度 - MTS のとき、 Exec_Master_Log_Pos などは リアルタイムで更新されない - 先ほど出てきた、 GAQ の checkpoint のタイミ ングで、 Exec_Master_Log_Pos などが更新さ れる - MTS を使う場合、 SHOW SLAVE STATUS だ けに頼るわけには行かなくなってくる
いちおう、ドキュメントにも書いてある - 18.4.1.34 Replication and Transaction Inconsistencies - 課題は3つ - Half-applied transaction - Gap - Gap-free low-watermark position
Half-applied transacitons - SQL_Thread を KILL などしたとき、 rollback できないと、 transaction の Atomicity が保た れない - まぁこれは InnoDB 使えばいいでしょ - MTS 有効なときでも slave_transaction_retries 効くようになったし - MySQL 5.7 すばらしい
Gaps - ざっくりいうと、 5.6 以前の MTS だと binlog の 順に Transaction 実行される保証がないので、 「一時的なエラーなどで、 relay log の途中に実 行されてない event が残ったらどうなるの?」と いう話 - これは slave_preserve_commit_order で commit 順保証できるようになって、改善した - MySQL 5.7 すばらしい
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 では分からない
そして、sql_slave_skip_counter 問題 - 特定の event だけ狙って skip するの、 MTS だとめんどくさい - ここはやっぱ GTID 使えるほうが便利
かつて Yahoo! Inc. の人は言いました - 昨年の Oracle Open World で - 「Multi-Threaded Replication 導入しつつ GTID 入れて、GTID入れるために Percona Server 5.6 導入した」と言ってたんですが - MySQL 5.7 になっても、 やっぱり GTID 使える ほうが、 MTS は導入しやすいママなんだなぁ - 遺憾でござる。
今回、改めて思ったのは - あるていど難しい実装になると、コード読んだだ けでは新機能を理解できないこともある - その点、 MySQL は commit log から WorkLog 漁っていけば、設計思想を踏まえて理解してい くことができる - 非常にとっつきやすいOSSで素晴らしいなと思 いました
そして、いまやGTIDは - GTID は master の failover をシンプルにする ためだけではなく - GTID を踏まえつつ MTS が設計されているな ど、 replication の性能向上目的でも、(間接的 に)使われるようになってきてる - MySQL5.6 のとき、 Facebook さんらが GTID のバグレポートたくさんしてくれたし、そろそろ導 入していったほうが良い時期かなぁ
ただ、 GTID まだ入れられない場合でも - 「やっべ slave めっちゃ遅延してるどうしよう」と いう状況になったとき、非常手段として、一時的 に LOGICAL_CLOCK & slave_preserve_commit_order & log_slave_updates という手段が取れるように なったのはけっこう便利なんじゃないでしょうか - SET GLOBAL relay_log_info_repository = ‘TABLE’ できるから、意外と敷居高くないし
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’
- max_allowed_packet いじってるなら、 slave_pending_jobs_size_max も見なおそう - SET GLOBAL sql_slave_skip_counter =N; を 運用上使いたいなら、 GTID 有効にするのが無 難
おわり

MySQL5.7 GA の Multi-threaded slave