MySQL5.7 GA の Multi-threaded slave
-
Upload
takanori-sejima -
Category
Technology
-
view
5.557 -
download
5
Transcript of MySQL5.7 GA の Multi-threaded slave
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 有効にするのが無難
おわり