チョットワカル Row-Based Replication・その1

こんにちわ。せじまです。珍しく replication の話をします。しかも、複数回に渡って続きます。連載です。

はじめに

先日、 こちらのスライド

詳しくは後日、ソースコード交えつつ別のかたちでご紹介したいと思います。

「詳しくは後日、ソースコード交えつつ別のかたちでご紹介したいと思います。」と言っていた件です。

Row-Based Replication の話をします。

昨年の8月頃、「そろそろ、SBRからRBRに移行して良い時期かなぁ、しないと将来めんどくさいかなぁ」「でも、 SET GLOBAL で binlog_format 変更できないと、手間かかってしょうがないなぁ」「じゃソースコード読むかぁ」ということで、MySQL の Replication はそんなに得意分野でもないので、他の仕事の合間にちょくちょく調べて、社内文書にまとめてました。

公開しても良いのでは、と思った内容なので、一部修正しつつ、お届けします。

はじめに断っておきますと、この話は長いです。2-3回では終わらないと思いますので、じっくり腰を据えて読んで頂くのがよろしいかと思います。

解説

前提: そもそも binary log による replication とは

まずは前提として、このあたりのスライドを読んでもらえると、MYSQL_BIN_LOG::ordered_commit() あたりの実装がわかります。

slave の SQL_Thread(最近は Applyer thread ともいいます。 binlog_format=row が導入されたことによって、 SQL だけではなく SQLCOM_BINLOG_BASE64_EVENT なども実行するようになったってことでしょう)は歴史的にシングルスレッドです。
よって、SQL_Thread がシングルスレッドだった時代は、 master は複数のスレッドで同時にトランザクションを実行できましたが、 slave は一つのSQL_Threadだけで一個ずつトランザクションを実行するしかなかったのです。
そうであるならば、master が複数のスレッドで実行していた複数のトランザクションを、シリアライズして binary log に書いて、トランザクション一個一個を SQL_Thread で replay するしかないわけです。

というわけで、MYSQL_BIN_LOG::ordered_commit() は、トランザクションが commit された順番で、各トランザクションを BEGIN ~ COMMIT で挟んで、シリアライズされた状態で binary log に書き出しています。
InnoDB の Isolation Level 的に考えると、 READ UNCOMMITTED でない限り、他のスレッドはCOMMIT されてないトランザクションの結果を参照できません。
よって、 master の InnoDB で同時に複数のトランザクション実行されていたとしても、COMMIT されない限り、そのトランザクションによる更新結果を他のスレッドは参照できないので、 COMMIT の順が保たれていれば、その binary log でレプリケーションしている slave で見えている結果は、(非同期なので遅延しているが)整合性が担保できるはずである、と。そういうわけでしょう。

(master と slave の状態が異なっている場合、 unsafe statement とかで整合性が崩れてしまうケースもありえるのですが、それについては今回言及しないことにします)

binlog_format=ROW になったとき、binlog event はどの時点で transaction cache に書き出されるのか

MySQL Musings:Binary Log Group Commit in MySQL 5.6 から抜粋すると

As the server executes transactions the server will collect the changes done by the transaction in a per-connection transaction cache. If statement-based replication is used, the statements will be written to the transaction cache, and if row-based replication is used, the actual rows changed will be written to the transaction cache. Once the transaction commits, the transaction cache is written to the binary log as one single block.

ソースコードを読んだほうが早いでしょうから、 MySQL 8.0.12 のソースコードをもとに解説します。

※当時、「ぼちぼちMySQL 8.0のソースコード読んどくかなぁ」と思っていたので、以降の説明は MySQL 8.0.12 が前提となっています。

とりあえず、(UPDATEは読みやすいかなと思ったので) SQLCOM_UPDATE 受けてから binlog_format=row で UPDATE_ROWS_EVENT 書くところまで

https://github.com/mysql/mysql-server/blob/mysql-8.0.12/sql/sql_parse.cc#L3218

https://github.com/mysql/mysql-server/blob/mysql-8.0.12/sql/sql_parse.cc#L3228

https://github.com/mysql/mysql-server/blob/mysql-8.0.12/sql/sql_select.cc#L469

https://github.com/mysql/mysql-server/blob/mysql-8.0.12/sql/sql_select.cc#L554

https://github.com/mysql/mysql-server/blob/mysql-8.0.12/sql/sql_update.cc#L1514

https://github.com/mysql/mysql-server/blob/mysql-8.0.12/sql/sql_update.cc#L265

https://github.com/mysql/mysql-server/blob/mysql-8.0.12/sql/sql_update.cc#L809

https://github.com/mysql/mysql-server/blob/mysql-8.0.12/sql/handler.cc#L7490

handler の中で、 binlog_log_row() で UPDATE_ROWS_EVENT を transaction cache に書いていきます。

https://github.com/mysql/mysql-server/blob/mysql-8.0.12/sql/handler.cc#L7513

https://github.com/mysql/mysql-server/blob/mysql-8.0.12/sql/sql_update.cc#L953

https://github.com/mysql/mysql-server/blob/mysql-8.0.12/sql/binlog.cc#L11251

https://github.com/mysql/mysql-server/blob/mysql-8.0.12/sql/binlog.cc#L11277

その query おわったら、 binlog_flush_pending_rows_event() で STMT_END_F 書いて UPDATE_ROWS_EVENT の終わりであることを示します。

ちなみに、 binlog_format=statement の場合は

https://github.com/mysql/mysql-server/blob/mysql-8.0.12/sql/sql_update.cc#L953

ここで渡してる thd->query() から

https://github.com/mysql/mysql-server/blob/mysql-8.0.12/sql/binlog.cc#L11344

Query_log_event のinstance生成して mysql_bin_log.write_event() するだけですね。

具体的に確認すると

例えば、MySQL8.0.18 で INSERT で確認してみますと、

handler 経由で InnoDB を更新してて、 handler API 経由で record 追加したときに WRITE_ROWS_EVENT を transaction cache に書いているわけなので

WRITE_ROWS_EVENT で行が追加されているということは、それだけ Innodb_rows_inserted も増えています。
余談ですが、WRITE_ROWS_EVENT で record 一件あたりどれくらいのデータが追加されそうか は、 Avg_row_length を参考にできる可能性があります。(Avg_row_length はちょうざっくりした概算なので、あくまで参考にしかならないですが) 。

ちなみに、binlog_rows_query_log_events を有効にすると、 binlog_format=ROW のときでも statement を binary log にコメントとして記録することができます。具体的には、 上記の例だ と Rows_query がそれに当たります。

例えば、 show binlog events で見ると次のようになります。 Query は binlog_format=STATEMENT で実行 した query で、 Rows_query や Writes_rows が、 binlog_format=ROW で実行した結果です。

Rows_query がいつ transaction cache に書かれるかというと、もういちど、handler::ha_update_row() あたりから読むとわかりやすいです。

https://github.com/mysql/mysql-server/blob//mysql-8.0.12/sql/handler.cc#L7490

https://github.com/mysql/mysql-server/blob//mysql-8.0.12/sql/handler.cc#L7513

https://github.com/mysql/mysql-server/blob//mysql-8.0.12/sql/handler.cc#L7350

https://github.com/mysql/mysql-server/blob//mysql-8.0.12/sql/handler.cc#L7298

https://github.com/mysql/mysql-server/blob//mysql-8.0.12/sql/handler.cc#L7311

binlog_rows_query_log_events が true な場合、

https://github.com/mysql/mysql-server/blob//mysql-8.0.12/sql/handler.cc#L7333

https://github.com/mysql/mysql-server/blob//mysql-8.0.12/sql/binlog.cc#L9376

https://github.com/mysql/mysql-server/blob//mysql-8.0.12/sql/binlog.cc#L9397

上記の show binlog events; のように、 Table_map(TABLE_MAP_EVENT) を書く前にRows_query(ROWS_QUERY_LOG_EVENT) を書いています。

TABLE_MAP_EVENT については、このあたりが詳しいです。

Used for row-based binary logging. This event precedes each row operation event. It maps a table definition to a number, where the table definition consists of database and table names and column definitions. The purpose of this event is to enable replication when a table has different definitions on the master and slave. Row operation events that belong to the same transaction may be grouped into sequences, in which case each such sequence of events begins with a sequence ofTABLE_MAP_EVENT events: one per table used by events in the sequence.

雑に考えると、 InnoDB は内部的に *.ibd ごとに table の id を割り当てているのですが、 table の作成順序や table の個数が master と slave で完全に一致するとは限りません。なので、 master と slave で (内部的な) table の id が一致するとも限らないので、 database の名前と table 名、そしてその table に対する一意な id を TABLE_MAP_EVENT で割り当てることにより、 TABLE_MAP_EVENT の後に続く WRITE_ROWS_EVENT で対象のテーブルを更新できる、ということではないでしょうか。

例えば上記の show binlog events でいうと、 Anonymous_Gtid から Xid(COMMIT)
までが一連のトランザクションとなるのではないかと。

ROWS_QUERY_LOG_EVENT に関する WorkLog など

Bug #50935 Record original SQL statement in RBR binlog という Feature request が事の発端であるようです

To aid in debugging replication issues, it would help to have the full
original SQL statement (including any embedded comments) recorded in the
binlog when in RBR mode.
The reconstructed pseudo-SQL is not always sufficient to uniquely
identify the source of the statement.

binary log 使って Point In Time Recovery したり duplicate した event を skip したりすると
き、元の query が log にないと困るだろう、ということでしょう。

そして MySQL5.6 で binlog_rows_query_log_events が追加され、

WL#4033: Support for informational log events

mysqlbinlog -vv で、 ROWS_QUERY_LOG_EVENT はコメントとして表示されるようになった模様です。

WL#5404: Propagation of Rows_query log event

今日はこれまで

もともとが社内文書なので、こういったノリで淡々と続きます。一年ぶりに読み返して見ると、我ながら随分そっけないなと思いました。

次回以降も、 RBR の話は続きます。