チョットワカル Row-Based Replication・その5
こんにちわ。せじまです。
今回も replication の話をします。
はじめに
第5回です。
今回は TABLE_MAP_EVENT に関する話をします。 MySQL Internal Manual ではこちらになります。
解説
TABLE_MAP_EVENT には、更新対象の database_name.table_name と、更新対象の columnの型 の情報が保存されています。また、型の compatibility check や replicate-do-db などのチェックをするのに使われています。
では早速ソースコードを読んでいきましょう。
SQL Thread で handle_slave_sql() から降りてきます。
- https://github.com/mysql/mysql-server/blob/mysql-8.0.12/sql/rpl_slave.cc#L6576
- https://github.com/mysql/mysql-server/blob/mysql-8.0.12/sql/rpl_slave.cc#L6842
- https://github.com/mysql/mysql-server/blob/mysql-8.0.12/sql/rpl_slave.cc#L4680
- https://github.com/mysql/mysql-server/blob/mysql-8.0.12/sql/rpl_slave.cc#L4782
- https://github.com/mysql/mysql-server/blob/mysql-8.0.12/sql/rpl_slave.cc#L4246
- https://github.com/mysql/mysql-server/blob/mysql-8.0.12/sql/rpl_slave.cc#L4323
- https://github.com/mysql/mysql-server/blob/mysql-8.0.12/sql/log_event.cc#L3627
- https://github.com/mysql/mysql-server/blob/mysql-8.0.12/sql/log_event.cc#L3644
まずは Table_map_log_event::do_apply_event()
- https://github.com/mysql/mysql-server/blob/mysql-8.0.12/sql/log_event.cc#L10688
- https://github.com/mysql/mysql-server/blob/mysql-8.0.12/sql/log_event.cc#L10731
10731 |
enum_tbl_map_status tblmap_status = check_table_map(rli, table_list); |
TABLE_MAP_EVENT に含まれる database_name.table_name が replication filter にヒット するか確認しています。
10651 10652 10653 10654 10655 10656 10657 10658 10659 10660 10661 10662 10663 10664 10665 10666 10667 10668 10669 10670 10671 10672 10673 10674 10675 10676 10677 10678 10679 10680 10681 |
static enum_tbl_map_status check_table_map(Relay_log_info const *rli, RPL_TABLE_LIST *table_list) { DBUG_ENTER("check_table_map"); enum_tbl_map_status res = OK_TO_PROCESS; if (rli->info_thd->slave_thread /* filtering is for slave only */ && (!rli->rpl_filter->db_ok(table_list->db) || (rli->rpl_filter->is_on() && !rli->rpl_filter->tables_ok("", table_list)))) if (rli->info_thd->get_transaction()->xid_state()->has_state( XID_STATE::XA_ACTIVE)) res = FILTERED_WITH_XA_ACTIVE; else res = FILTERED_OUT; else { RPL_TABLE_LIST *ptr = static_cast<RPL_TABLE_LIST *>(rli->tables_to_lock); for (uint i = 0; ptr && (i < rli->tables_to_lock_count); ptr = static_cast<RPL_TABLE_LIST *>(ptr->next_local), i++) { if (ptr->table_id == table_list->table_id) { if (strcmp(ptr->db, table_list->db) || strcmp(ptr->alias, table_list->table_name) || ptr->lock_descriptor().type != TL_WRITE) // the ::do_apply_event always sets TL_WRITE res = SAME_ID_MAPPING_DIFFERENT_TABLE; else res = SAME_ID_MAPPING_SAME_TABLE; break; } } } |
更新対象であると判断されれば、その table は rli->tables_to_lock に追加されます。
10731 10732 10733 10734 10735 10736 10737 10738 10739 10740 10741 10742 10743 10744 10745 10746 10747 10748 10749 10750 10751 10752 10753 10754 10755 10756 10757 10758 |
enum_tbl_map_status tblmap_status = check_table_map(rli, table_list); if (tblmap_status == OK_TO_PROCESS) { DBUG_ASSERT(thd->lex->query_tables != table_list); /* Use placement new to construct the table_def instance in the memory allocated for it inside table_list. The memory allocated by the table_def structure (i.e., not the memory allocated *for* the table_def structure) is released inside Relay_log_info::clear_tables_to_lock() by calling the table_def destructor explicitly. */ new (&table_list->m_tabledef) table_def(m_coltype, m_colcnt, m_field_metadata, m_field_metadata_size, m_null_bits, m_flags); table_list->m_tabledef_valid = true; table_list->m_conv_table = NULL; table_list->open_type = OT_BASE_ONLY; /* We record in the slave's information that the table should be locked by linking the table into the list of tables to lock. */ table_list->next_global = table_list->next_local = rli->tables_to_lock; const_cast<Relay_log_info *>(rli)->tables_to_lock = table_list; const_cast<Relay_log_info *>(rli)->tables_to_lock_count++; |
Rows_log_event::do_apply_event() も読んでみましょう
- https://github.com/mysql/mysql-server/blob/mysql-8.0.12/sql/log_event.cc#L9549
- https://github.com/mysql/mysql-server/blob/mysql-8.0.12/sql/log_event.cc#L9567-L9783
9567 行目から始まる if のブロックは 9783 行目まで続きます。たいへん長いです。
で、この中で Table_map_log_event::do_apply_event() で登録された、更新対象である table に関する処理が実行されます。
9709 9710 9711 |
if (!ptr->m_tabledef.compatible_with(thd, const_cast<Relay_log_info *>(rli), ptr->table, &conv_table)) { |
table の compatibility check ということで、型チェックが入ります。
- https://github.com/mysql/mysql-server/blob/mysql-8.0.12/sql/rpl_utility.cc#L449
- https://github.com/mysql/mysql-server/blob/mysql-8.0.12/sql/rpl_utility.cc#L478-L485
478 479 480 481 482 483 484 485 |
for (uint col = 0; col < cols_to_check; ++col) { Field *const field = table->field[col]; int order; if (can_convert_field_to(field, type(col), field_metadata(col), rli, m_flags, &order)) { DBUG_PRINT("debug", ("Checking column %d -" " field '%s' can be converted - order: %d", col, field->field_name, order)); |
型変換に関するチェックをする際、
チョットワカル Row-Based Replication・その2でも言及した、DATETIME や TIMESTAMP もチェックされます。
265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 |
} else if (metadata == 0 && (timestamp_cross_check(field->real_type(), source_type) || datetime_cross_check(field->real_type(), source_type) || time_cross_check(field->real_type(), source_type))) { /* In the above condition, we are taking care of case where 1) Master having old TIME, TIMESTAMP, DATETIME and slave have new TIME2, TIMESTAMP2, DATETIME2 or 2) Master having new TIMESTAMP2, DATETIME2, TIME2 with fraction part zero and slave have TIME, TIMESTAMP, DATETIME. We need second condition, as when we are upgrading from 5.5 to 5.6 TIME, TIMESTAMP, DATETIME columns are not upgraded to TIME(0), TIMESTAMP(0), DATETIME(0). So to support these conversion we are putting this condition. */ /* TS-TODO: conversion from FSP1>FSP2. Can do non-lossy conversion from old TIME, TIMESTAMP, DATETIME to new TIME(0), TIMESTAMP(0), DATETIME(0). */ *order_var = -1; DBUG_RETURN(true); |
167 168 169 170 171 172 173 174 175 |
/** Check if the types are criss cross means type1 is MYSQL_TYPE_TIMESTAMP and type2 as MYSQL_TYPE_TIMESTAMP2 or vice versa. */ inline bool timestamp_cross_check(enum_field_types type1, enum_field_types type2) { return ((type1 == MYSQL_TYPE_TIMESTAMP && type2 == MYSQL_TYPE_TIMESTAMP2) || (type1 == MYSQL_TYPE_TIMESTAMP2 && type2 == MYSQL_TYPE_TIMESTAMP)); } |
そして、table の compatibility check の後、 table id と table のマッピングがされて
table_id で更新対象の table を取得できるようになります。
9785 9786 |
table = m_table = const_cast<Relay_log_info *>(rli)->m_table_map.get_table(m_table_id); |
チョットワカル Row-Based Replication・その1 の show binlog events の出力結果を見るとわかりますが、 UPDATE_ROWS_EVENT などの ROWS EVENT は、 table を名前ではなく、 TABLE_MAP_EVENT で割り当てた table_id 指定で更新するようです。で、UPDATE_ROWS_EVENT などはまず その table_id で更新対象の table を参照している、と。
そのあと、9799行目から 10074行目にかけて、UPDATE_ROWS_EVENT などの ROWS EVENT が処理されるんですが、これまた長いです
まずは (rli)->m_table_map.get_table(m_table_id) で NULL が返ってきた場合、 replication filter で更新対象から外されたってことになります。
9799 9800 9801 9802 9803 9804 |
if (table) { /* table == NULL means that this table should not be replicated (this was set up by Table_map_log_event::do_apply_event() which tested replicate-* rules). */ |
Columnのメタデータなどに関する補足
binlog_format=ROW かつ binlog_row_image=FULL の場合、更新前と更新後の行のデータがすべて保存されているので、 binlog は Change data capture の用途にも使うこともできそうなわけです(わたしは Change data capture に疎いですが)。
例えば、居住地の情報を岡山県から茨城県に書き換えるトランザクションがあった場合、binlog_format=STATEMENTでは、「茨城県に書き換わった」ということしかわからないかもしれません。しかし、 binlog_format=ROW かつ binlog_row_image=FULL だった場合、「岡山県から茨城県に書き換わった」ということまで、binlogを読んだだけで確実にわかるわけです。
ちなみに、 binlog を capture するためのソフトウェアは古くから開発されており、例えば uber/storagetapper といったものも公開されています。
MySQL8.0 では、 binlog_row_metadataというパラメータが追加されており、さらに詳細なColumnのメタデータを取得できるようになりました。
詳しくは、以下の MySQL High Availability の記事で解説されています。
More Metadata Is Written Into Binary Log
Column のメタデータを binlog に追加することにより、さらに binlog の使い勝手が良くなっていきそうですね。
終わり
こちらののスライドで言及していた以下のことについて、これでだいたい触れました。
あとは、今回の連載で取り上げたWorkLogやソースコードの関数など読み込んでもらうと、より理解が深まるでしょう。例えば、RBRにおけるColumnの型変換については、今回取り上げた Table_map_log_event や Rows_log_event は参考になるかと思います。
また、RBRに関するWorkLogはまだまだありますので、関心のある方はこちらにも目を通されると良いかもしれません。
以上で第一部・完となります。気が向いたら、またRBRに関するソースコード読んで、つらつらと書くかもしれません。