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