Quantcast
Channel: 日々の覚書
Viewing all 581 articles
Browse latest View live

シングルプライマリーモードだろうとInnoDB Cluster内のデータをズラす方法

$
0
0

TL;DR

  • 単にプライマリーノードで SET sql_log_bin = 0で効くんだからびっくりだ

動作確認用にd1.t1テーブルを作って2行くらい入れておく。
### node1
mysql> CREATE DATABASE d1;
Query OK, 1 row affected (0.01 sec)

mysql> CREATE TABLE d1.t1 (num SERIAL, val VARCHAR(32));
Query OK, 0 rows affected (0.02 sec)

mysql> INSERT INTO d1.t1 VALUES (1, 'one');
Query OK, 1 row affected (0.01 sec)

mysql> INSERT INTO d1.t1 VALUES (2, 'two');
Query OK, 1 row affected (0.00 sec)
プライマリーノードで sysbenchを使って100万行くらいのテーブルを作る。
### node1
$ sysbench --mysql-user=root oltp_common --table_size=1000000 prepare
sysbench 1.0.17 (using system LuaJIT 2.0.4)

Creating table 'sbtest1'...
Inserting 1000000 records into 'sbtest1'
Creating a secondary index on 'sbtest1'...

### node1
mysql> SELECT COUNT(*) FROM sbtest.sbtest1;
+----------+
| COUNT(*) |
+----------+
| 1000000 |
+----------+
1 row in set (0.32 sec)

### node2
mysql> SELECT COUNT(*) FROM sbtest.sbtest1;
+----------+
| COUNT(*) |
+----------+
| 1000000 |
+----------+
1 row in set (0.14 sec)

### node3
mysql> SELECT COUNT(*) FROM sbtest.sbtest1;
+----------+
| COUNT(*) |
+----------+
| 1000000 |
+----------+
1 row in set (0.13 sec)
sql_log_bin=OFFでいけるかなと思ったらいけてしまった。
### node1
mysql> SET SESSION sql_log_bin = 0;
Query OK, 0 rows affected (0.00 sec)

mysql> DELETE FROM sbtest.sbtest1;
Query OK, 1000000 rows affected (9.65 sec)

### node1
mysql> SELECT COUNT(*) FROM sbtest.sbtest1;
+----------+
| COUNT(*) |
+----------+
| 0 |
+----------+
1 row in set (0.00 sec)

### node2
mysql> SELECT COUNT(*) FROM sbtest.sbtest1;
+----------+
| COUNT(*) |
+----------+
| 1000000 |
+----------+
1 row in set (0.24 sec)

### node3
mysql> SELECT COUNT(*) FROM sbtest.sbtest1;
+----------+
| COUNT(*) |
+----------+
| 1000000 |
+----------+
1 row in set (0.15 sec)
これで何が嬉しいかっていうと、この状態でnode1に対してテキトーな OPTIMIZE TABLEでも投げ続ければ、node2とnode3は十分遅れてくれるだろうっていうこと。
### node1
mysql> OPTIMIZE TABLE sbtest.sbtest1; -- これを5回くらい

mysql> SELECT MEMBER_ID, COUNT_TRANSACTIONS_IN_QUEUE AS queue, COUNT_TRANSACTIONS_REMOTE_IN_APPLIER_QUEUE FROM performance_schema.replication_group_member_stats; +--------------------------------------+-------+--------------------------------------------+
| MEMBER_ID | queue | COUNT_TRANSACTIONS_REMOTE_IN_APPLIER_QUEUE |
+--------------------------------------+-------+--------------------------------------------+
| ec62dfb5-66e4-11ea-a8d3-12c40920c9ad | 0 | 0 |
| ee6bca41-66e4-11ea-b904-123209691891 | 0 | 5 |
| f35fec8f-66e4-11ea-9a69-12dd5d4a2e3d | 0 | 5 |
+--------------------------------------+-------+--------------------------------------------+
3 rows in set (0.00 sec)
溜まってるのが確認できる。
溜まってる間に追い越してApplyされないのは前々回くらいに試した通りなんだけど。
### node1
mysql> INSERT INTO d1.t1 VALUES (3, 'three');
Query OK, 1 row affected (0.00 sec)

mysql> SELECT * FROM t1;
+-----+-------+
| num | val |
+-----+-------+
| 1 | one |
| 2 | two |
| 3 | three |
+-----+-------+
3 rows in set (0.00 sec)

$ for f in /var/lib/mysql/*.0000* ; do mysqlbinlog -vv $f | stdbuf -oL awk '{print "'$f'", $0}'; done | grep INSERT | grep d1
/var/lib/mysql/binlog.000003 ### INSERT INTO `d1`.`t1`
/var/lib/mysql/binlog.000003 ### INSERT INTO `d1`.`t1`
/var/lib/mysql/binlog.000003 ### INSERT INTO `d1`.`t1`

### node2
mysql> SELECT * FROM t1;
+-----+------+
| num | val |
+-----+------+
| 1 | one |
| 2 | two |
+-----+------+
2 rows in set (0.00 sec)

$ for f in /var/lib/mysql/*.0000* ; do mysqlbinlog -vv $f | stdbuf -oL awk '{print "'$f'", $0}'; done | grep INSERT | grep d1 [22/19445]
/var/lib/mysql/binlog.000002 ### INSERT INTO `d1`.`t1`
/var/lib/mysql/binlog.000002 ### INSERT INTO `d1`.`t1`
/var/lib/mysql/ip-172-31-1-120-relay-bin-group_replication_applier.000002 ### INSERT INTO `d1`.`t1`
/var/lib/mysql/ip-172-31-1-120-relay-bin-group_replication_applier.000002 ### INSERT INTO `d1`.`t1`
/var/lib/mysql/ip-172-31-1-120-relay-bin-group_replication_applier.000002 ### INSERT INTO `d1`.`t1`

### node3
mysql> SELECT * FROM t1;
+-----+------+
| num | val |
+-----+------+
| 1 | one |
| 2 | two |
+-----+------+
2 rows in set (0.00 sec)

$ for f in /var/lib/mysql/*.0000* ; do mysqlbinlog -vv $f | stdbuf -oL awk '{print "'$f'", $0}'; done | grep INSERT | grep d1 [22/18451]
/var/lib/mysql/binlog.000002 ### INSERT INTO `d1`.`t1`
/var/lib/mysql/binlog.000002 ### INSERT INTO `d1`.`t1`
/var/lib/mysql/ip-172-31-1-48-relay-bin-group_replication_applier.000002 ### INSERT INTO `d1`.`t1`
/var/lib/mysql/ip-172-31-1-48-relay-bin-group_replication_applier.000002 ### INSERT INTO `d1`.`t1`
/var/lib/mysql/ip-172-31-1-48-relay-bin-group_replication_applier.000002 ### INSERT INTO `d1`.`t1`
Applierの方のリレーログはまではちゃんと受け取っている。
DMLだと、DML適用の番になった時にフツーのレプリケーションと同じようにDUP_KEYなりROW_NOT_FOUNDでエラーになって面白くないだけだと思うので、この状態で「グループレプリケーションが遅延しまくっている」のを再現させるのが使いどころじゃないかなあとは思う。
遅延させて何を試していたかはタイトルにそぐわなくなってきたのでまた今度。

続きはこっち → 日々の覚書: わざと件数をズラしたグループレプリケーションで、「遅延中」の動きを観察する

わざと件数をズラしたグループレプリケーションで、「遅延中」の動きを観察する

$
0
0
件数をズラした上で OPTIMIZE TABLEをかけるといくらでもセカンダリーが遅延した状況が作り出せる、というとことまでは良いとして。
node3のグループレプリケーションを一旦止める。
### node3
mysql> STOP GROUP_REPLICATION;
Query OK, 0 rows affected (4.49 sec)
またnode1から OPTIMIZE TABLEを5発くらい叩き込んでついでに更新SQLを入れておく。
### node1
mysql> OPTIMIZE TABLE sbtest.sbtest1; -- これを5回くらい

mysql> INSERT INTO d1.t1 VALUES (4, 'four');
Query OK, 1 row affected (0.01 sec)

mysql> SELECT * FROM d1.t1;
+-----+-------+
| num | val |
+-----+-------+
| 1 | one |
| 2 | two |
| 3 | three |
| 4 | four |
+-----+-------+
4 rows in set (0.00 sec)

### node2
mysql> SELECT * FROM d1.t1; -- OPTIMIZE TABLEにやられて遅れてる
+-----+-------+
| num | val |
+-----+-------+
| 1 | one |
| 2 | two |
| 3 | three |
+-----+-------+
3 rows in set (0.00 sec)

### node3
mysql> SELECT * FROM d1.t1; -- そもそもグループレプリケーション止めてるので同期されない
+-----+-------+
| num | val |
+-----+-------+
| 1 | one |
| 2 | two |
| 3 | three |
+-----+-------+
3 rows in set (0.00 sec)
ここでnode3のグループレプリケーションを再開。
### node3
mysql> START GROUP_REPLICATION;
Query OK, 0 rows affected (4.93 sec)

$ for f in /var/lib/mysql/*.0000* ; do mysqlbinlog -vv $f | stdbuf -oL awk '{print "'$f'", $0}'; done | grep INSERT | grep d1
/var/lib/mysql/binlog.000002 ### INSERT INTO `d1`.`t1`
/var/lib/mysql/binlog.000002 ### INSERT INTO `d1`.`t1`
/var/lib/mysql/binlog.000002 ### INSERT INTO `d1`.`t1`
/var/lib/mysql/ip-172-31-1-48-relay-bin-group_replication_recovery.000003 ### INSERT INTO `d1`.`t1`
この時点で、「グループレプリケーション停止中に更新した行はrecoveryの方のリレーログに入る」のは実験済み。
じゃあ、 “RECOVERING” 中に新しく突っ込まれた更新の行は?
### node1
mysql> INSERT INTO d1.t1 VALUES (5, 'five');
Query OK, 1 row affected (0.01 sec)

### node3
$ for f in /var/lib/mysql/*.0000* ; do mysqlbinlog -vv $f | stdbuf -oL awk '{print "'$f'", $0}'; done | grep INSERT | grep d1
/var/lib/mysql/binlog.000002 ### INSERT INTO `d1`.`t1`
/var/lib/mysql/binlog.000002 ### INSERT INTO `d1`.`t1`
/var/lib/mysql/binlog.000002 ### INSERT INTO `d1`.`t1`
/var/lib/mysql/ip-172-31-1-48-relay-bin-group_replication_recovery.000003 ### INSERT INTO `d1`.`t1`
/var/lib/mysql/ip-172-31-1-48-relay-bin-group_replication_recovery.000003 ### INSERT INTO `d1`.`t1`
これもrecoveryの方に入った。recoveryが動きを止める
遅延が完全に追い付いてからnode1にINSERTした値はapplierの方に入る。
### node1
mysql> INSERT INTO d1.t1 VALUES (6, 'six');
Query OK, 1 row affected (0.01 sec)

### node3
for f in /var/lib/mysql/*.0000* ; do mysqlbinlog -vv $f | stdbuf -oL awk '{print "'$f'", $0}'; done | grep INSERT | grep d1
/var/lib/mysql/binlog.000002 ### INSERT INTO `d1`.`t1`
/var/lib/mysql/binlog.000002 ### INSERT INTO `d1`.`t1`
/var/lib/mysql/binlog.000002 ### INSERT INTO `d1`.`t1`
/var/lib/mysql/binlog.000002 ### INSERT INTO `d1`.`t1`
/var/lib/mysql/binlog.000002 ### INSERT INTO `d1`.`t1`
/var/lib/mysql/binlog.000002 ### INSERT INTO `d1`.`t1`
/var/lib/mysql/ip-172-31-1-48-relay-bin-group_replication_applier.000002 ### INSERT INTO `d1`.`t1`
/var/lib/mysql/ip-172-31-1-48-relay-bin-group_replication_applier.000002 ### INSERT INTO `d1`.`t1`
ん? 何故かapplierの方に (5, ‘five’) と思しきイベントも入ってる
### node3
$ mysqlbinlog -vv /var/lib/mysql/ip-172-31-1-48-relay-bin-group_replication_applier.000002
..
SET @@SESSION.GTID_NEXT= '14b471b9-66e5-11ea-888f-12c40920c9ad:454'/*!*/;
### INSERT INTO `d1`.`t1`
### SET
### @1=5
### @2='five'
..
SET @@SESSION.GTID_NEXT= '14b471b9-66e5-11ea-888f-12c40920c9ad:455'/*!*/;
### INSERT INTO `d1`.`t1`
### SET
### @1=6
### @2='six'
recoveryの方のリレーログすぐ消えちゃうからな… relay_log_purge=OFFしたら残ってくれるかしらん。
### node3
mysql> SELECT channel_name, service_state, last_applied_transaction AS last_gtid, last_applied_transaction_original_commit_timestamp AS orig_ts, last_applied_transaction_immediate_commit_timestamp AS my_ts FROM performance_schema.replication_applier_status_by_worker;
+----------------------------+---------------+------------------------------------------+----------------------------+----------------------------+
| channel_name | service_state | last_gtid | orig_ts | my_ts |
+----------------------------+---------------+------------------------------------------+----------------------------+----------------------------+
| group_replication_applier | ON | 14b471b9-66e5-11ea-888f-12c40920c9ad:455 | 2020-03-15 18:23:10.568424 | 0000-00-00 00:00:00.000000 |
| group_replication_recovery | OFF | 14b471b9-66e5-11ea-888f-12c40920c9ad:453 | 0000-00-00 00:00:00.000000 | 2020-03-15 18:18:30.913861 |
+----------------------------+---------------+------------------------------------------+----------------------------+----------------------------+
2 rows in set (0.00 sec)
performance_schemaで調べる限り、RECOVERING中に入ってきた2行はapplierの方が処理しているっぽい。
正直どっちが処理してくれても(recoveryが止まって初めてapplierが動くなら)構いはしない気がするけど、これがDMLだからなのかDDLなのかちょっと気になるね。
…と調べてみたら、DML/DDLに関わらずグループレプリケーションを再開した後に打たれたSQLはrecoveryのリレーログで一度受けた後にapplierの方に回すっぽい。
謎が深まった。

MySQL 8.0のデュアルパスワードを使った記念メモ

$
0
0

TL;DR

  • デュアルパスワードの情報は mysql.user.User_attributesカラムにJSONで入ってくるのでその辺で確認できる
    • SHOW CREATE USERには入ってこないのでここで確認するしかない?
  • SELECTステートメントでデュアルパスワード持ってるアカウントだけを引っ張るなら mysql.user.user_attributesadditional_passwordって要素を持ってるかどうかで判定ができる
mysql80 7> SELECT user, host FROM mysql.user WHERE user_attributes->>'$.additional_password' IS NOT NULL;
+----------+------+
| user | host |
+----------+------+
| yoku0825 | % |
+----------+------+
1 row in set (0.00 sec)

「セカンダリーパスワード」とかそんな名前かと思ったら、「デュアルパスワード」が公式用語っぽいのも今回初めて気が付いた。
やり方そのものは日本語でも記事が存在するしドキュメントもある。
試しにやってみるだけなら、 IDENTIFIED WITH mysql_native_password BY '..'でmysql_native_passwordプラグインを使った方が出力が化けなくて良いかも ( print_identified_with_as_hexSELECTで直接のぞき込む時には使えないから)
mysql80 82> CREATE USER yoku0825 IDENTIFIED BY 'yoku0826';
Query OK, 0 rows affected (0.01 sec)

mysql80 82> SHOW CREATE USER yoku0825;
+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| CREATE USER for yoku0825@% |
+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| CREATE USER 'yoku0825'@'%' IDENTIFIED WITH 'caching_sha2_password' AS '$A$005$]f|)Pd<I#(i
ij[HzpqNoMwkaEO.dS2orkm5nQ/2Mgulxlh.8aumbjfYa8' REQUIRE NONE PASSWORD EXPIRE DEFAULT ACCOUNT UNLOCK PASSWORD HISTORY DEFAULT PASSWORD REUSE INTERVAL DEFAULT PASSWORD REQUIRE CURRENT DEFAULT |
+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)

mysql80 82> ALTER USER yoku0825 IDENTIFIED BY 'yoku0827' RETAIN CURRENT PASSWORD;
Query OK, 0 rows affected (0.00 sec)

mysql80 82> SHOW CREATE USER yoku0825;
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| CREATE USER for yoku0825@% |
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| CREATE USER 'yoku0825'@'%' IDENTIFIED WITH 'caching_sha2_password' AS '$A$005$V=]bHk2\r~l8f\"\ZpB]DoKqTjmFtEz20yaFYDbvkXx0kOOCmGzuhCByA5lXPi5' REQUIRE NONE PASSWORD EXPIRE DEFAULT ACCOUNT UNLOCK PASSWORD HISTORY DEFAULT PASSWORD REUSE INTERVAL DEFAULT PASSWORD REQUIRE CURRENT DEFAULT |
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)
SHOW CREATE USERには以前のパスワードっぽいものは見えない。デュアルパスワードになってるのかどうかも見えない。
mysql.userあたりを眺めていたら user_attributesというカラムに突っ込んでいるっぽかった(caching_sha2_passwordプラグインだと authentication_stringカラムはやっぱり潰れちゃうねえ…)
mysql80 82> SELECT user, host, authentication_string, user_attributes FROM mysql.user WHERE user = 'yoku0825'\G
*************************** 1. row ***************************
user: yoku0825
host: %
~l8f"pB]DoKqTjmFtEz20yaFYDbvkXx0kOOCmGzuhCByA5lXPi5
user_attributes: {"additional_password": "$A$005$]\u000ff|)Pd<\u0017I#(i\u0006 \t\u000bij[HzpqNoMwkaEO.dS2orkm5nQ/2Mgulxlh.8aumbjfYa8"}
1 row in set (0.00 sec)

mysql80 82> ALTER USER yoku0825 DISCARD OLD PASSWORD;
Query OK, 0 rows affected (0.02 sec)

mysql80 82> SELECT user, host, authentication_string, user_attributes FROM mysql.user WHERE user = 'yoku0825'\G
*************************** 1. row ***************************
user: yoku0825
host: %
~l8f"pB]DoKqTjmFtEz20yaFYDbvkXx0kOOCmGzuhCByA5lXPi5
user_attributes: NULL
1 row in set (0.00 sec)
user_attributesの構造体は このへんで、更新してるのは このへん、使ってるフラグを管理してるのは このへんなので、8.0.19現在ではデュアルパスワードと ログインの失敗回数制御partial_revokesで使っているっぽい。
mysql80 7> SELECT user, host, authentication_string, user_attributes FROM mysql.user WHERE user = 'yoku0825'\G
*************************** 1. row ***************************
user: yoku0825
host: %
authentication_string: $A$005$LMJP\MS%cmy/Gv NIMCTPSdqYBmL2txa9mWq7ki0jYncHsWW99iyXk8CyD
user_attributes: {"Restrictions": [{"Database": "mysql", "Privileges": ["SELECT"]}], "Password_locking": {"failed_login_attempts": 4, "password_lock_time_days": -1}, "additional_password": "$A$005$>PuHNir\n\t&\b\u0004B/Kf\u000b\u000e\u0010\u000bYZEIELtEpe7xFBDXqiQhC6EFNSOy.DQy.11P5iFl4v4"}
1 row in set (0.00 sec)
SELECTステートメントでデュアルパスワード持ってるアカウントだけを引っ張るならこうかな。
mysql80 7> SELECT user, host FROM mysql.user WHERE user_attributes->>'$.additional_password' IS NOT NULL;
+----------+------+
| user | host |
+----------+------+
| yoku0825 | % |
+----------+------+
1 row in set (0.00 sec)

my.cnfの plugin_load の記法

$
0
0

TL;DR

  • INSTALL PLUGIN a SONAME 'b.so'plugin_load= a=b.soと書く
  • 複数つなげる時は ;で区切る、 plugin_load=rpl_semi_sync_master=semisync_master.so;rpl_semi_sync_slave=semisync_slave.soみたいに

…言いたいことは全部書いてしまった気がするので、ドキュメントを読んでつらつらと。
plugin_load_addは使ったことなかったけど、 ;で複数区切りを「したくない」時に後ろに書けるらしい。 地味にMySQL 5.6.3の新機能だったらしい。知らなかった。
plugin_load_addが追加で plugin_loadは上書きな関係上、my.cnfの上では必ず plugin_loadが先で plugin_load_addは後に書かないといけないらしい。
5.7のInnoDB暗号化の時から姿を現した early_plugin_loadはプラグインをロードするタイミングが違うだけみたい。
(gdb) bt
+bt
#0 ReplSemiSyncMaster::initObject (this=this@entry=0x7fffee604ca0 <repl_semisync>)
at /home/yoku0825/mysql-5.7.29/plugin/semisync/semisync_master.cc:449
#1 0x00007fffee3fecfa in semi_sync_master_plugin_init (p=0x1fdc998)
at /home/yoku0825/mysql-5.7.29/plugin/semisync/semisync_master_plugin.cc:596
#2 0x0000000000ca3fd4 in plugin_initialize (plugin=plugin@entry=0x1fdc998) at /home/yoku0825/mysql-5.7.29/sql/sql_plugin.cc:1252
#3 0x0000000000ca89ef in plugin_init_initialize_and_reap () at /home/yoku0825/mysql-5.7.29/sql/sql_plugin.cc:1388
#4 0x0000000000ca8d1f in plugin_register_early_plugins (argc=argc@entry=0x1dd5df0 <remaining_argc>, argv=0x1e9f930, flags=0)
at /home/yoku0825/mysql-5.7.29/sql/sql_plugin.cc:1500
#5 0x000000000078950a in init_server_components () at /home/yoku0825/mysql-5.7.29/sql/mysqld.cc:4053
#6 0x000000000078cfb1 in mysqld_main (argc=4, argv=0x1e9f930) at /home/yoku0825/mysql-5.7.29/sql/mysqld.cc:4755
#7 0x00007ffff5ecf505 in __libc_start_main (main=0x7652a0 <main(int, char**)>, argc=4, argv=0x7fffffffe3c8, init=<optimized out>,
fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7fffffffe3b8) at ../csu/libc-start.c:266
#8 0x00000000007829ec in _start ()
(gdb) bt
+bt
#0 ReplSemiSyncMaster::initObject (this=this@entry=0x7fffe3b22ca0 <repl_semisync>)
at /home/yoku0825/mysql-5.7.29/plugin/semisync/semisync_master.cc:449
#1 0x00007fffe391ccfa in semi_sync_master_plugin_init (p=0x26eab48)
at /home/yoku0825/mysql-5.7.29/plugin/semisync/semisync_master_plugin.cc:596
#2 0x0000000000ca3fd4 in plugin_initialize (plugin=plugin@entry=0x26eab48) at /home/yoku0825/mysql-5.7.29/sql/sql_plugin.cc:1252
#3 0x0000000000ca89ef in plugin_init_initialize_and_reap () at /home/yoku0825/mysql-5.7.29/sql/sql_plugin.cc:1388
#4 0x0000000000cab1ee in plugin_register_dynamic_and_init_all (argc=argc@entry=0x1dd5df0 <remaining_argc>, argv=0x1e9f930,
flags=flags@entry=0) at /home/yoku0825/mysql-5.7.29/sql/sql_plugin.cc:1673
#5 0x0000000000789561 in init_server_components () at /home/yoku0825/mysql-5.7.29/sql/mysqld.cc:4074
#6 0x000000000078cfb1 in mysqld_main (argc=4, argv=0x1e9f930) at /home/yoku0825/mysql-5.7.29/sql/mysqld.cc:4755
#7 0x00007ffff5ecf505 in __libc_start_main (main=0x7652a0 <main(int, char**)>, argc=4, argv=0x7fffffffe3c8, init=<optimized out>,
fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7fffffffe3b8) at ../csu/libc-start.c:266
#8 0x00000000007829ec in _start ()
間に ビルトインプラグインと主要ストレージエンジンのロードが入っているので、InnoDB暗号化は確かにInnoDBの起動より先にやらないといけなくてearlyの方じゃないとダメそう。
逆にInnoDB MemcachedとかInnoDBに依存してそうだからearlyでやると転けた。
plugin_loadっていうとプラグインInnoDBとか思い出しますね :D

MySQLのLAG()とかLEAD()に ERROR 1690 (22003): BIGINT UNSIGNED value is out of range と言われたら

$
0
0

TL;DR

  • sql_modeNO_UNSIGNED_SUBTRACTIONを追加してから実行する

たとえば、 performance_schema.events_statements_summary_by_digestの結果を延々とため込んでいるようなテーブルがあるとするじゃろ?
sum_rows_examinedは累計値なので、グラフにする時なぞは前回との差分を取りたくなるので、MySQL 8.0からようやく使えるようになった LAGなぞ使うではないか。
mysql> WITH base AS
-> (
-> SELECT
-> digest,
-> sum_rows_examined - LAG(sum_rows_examined) OVER w AS diff_exam,
-> last_update,
-> TIMESTAMPDIFF(second, LAG(last_update) OVER w, last_update) AS diff_time
-> FROM ps_digest_info
-> WINDOW w AS (PARTITION BY digest ORDER BY seq)
-> )
-> SELECT digest, last_update, diff_exam / diff_time, diff_time FROM base;
ERROR 1690 (22003): BIGINT UNSIGNED value is out of range in '(`admintool`.`ps_digest_info`.`sum_rows_examined` - `tmp_field_5`)'
エラった(´・ω・`)
これは再起動でもはさんだのか、 sum_rows_examined - LAG(sum_rows_examined) OVER w (これが tmp_field_5の正体) が負になるところがあると BIGINT UNSIGNEDの範囲に収まらないから…ってこうなる。
確かに sum_rows_examinedBIGINT UNSIGNEDで定義していたのでこれを BIGINT SIGNEDにデータ型を変えてやれば動くようにはなるんだけど、必ずしもそうできるわけでもない。
mysql> show create table ps_digest_info\G
*************************** 1. row ***************************
Table: ps_digest_info
Create Table: CREATE TABLE `ps_digest_info` (
`seq` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`ipaddr` varchar(15) NOT NULL,
`port` smallint(5) unsigned NOT NULL,
`schema_name` varchar(255) NOT NULL,
`digest` varchar(255) NOT NULL,
`digest_text` text NOT NULL,
`count_star` bigint(20) unsigned NOT NULL,
`sum_rows_examined` bigint(20) unsigned NOT NULL,
`sum_rows_sent` bigint(20) unsigned NOT NULL,
..
MySQLは何故か整数の加減算の時に「片方が符号無しの場合は結果の型を符号無しにしようとする」という謎なポリシー(?)があって、それに違反すると出るエラーが ERROR 1690 (22003): BIGINT UNSIGNED value is out of range .
このポリシーを曲げさせて「結果の型が負になりそうなら符号ありにキャストする」ようにするのが NO_UNSIGNED_SUBTRACTION
mysql> SET sql_mode = CONCAT(@@sql_mode, ',NO_UNSIGNED_SUBTRACTION');
Query OK, 0 rows affected (0.00 sec)

mysql> WITH base AS ( SELECT digest, sum_rows_examined - LAG(sum_rows_examined) OVER w AS diff_exam, last_update, TIMESTAMPDIFF(second, LAG(last_update) OVER w, last_update) AS diff_time FROM ps_digest_info WINDOW w AS (PARTITION BY digest ORDER BY seq)) SELECT digest, last_update, diff_exam / diff_time, diff_time FROM base;
+----------------------------------+---------------------+-----------------------+-----------+
| digest | last_update | diff_exam / diff_time | diff_time |
+----------------------------------+---------------------+-----------------------+-----------+
| | 2019-06-25 10:30:08 | NULL | NULL |
| | 2019-06-25 18:30:08 | 26.7571 | 28800 |
| | 2019-07-02 10:30:08 | 68.2178 | 576000 |
..
結果が返ってくるようになった。良かった良かった。

performance_schema.clone_progress が何となくそれっぽい順番に並ぶ理由

$
0
0

TL;DR

  • datadir/#clone/#view_progressという平文のファイルがこのテーブルの本体だから

俺は途中まで作業をしていて聞き逃したんですけど、 Open Source Conference 2020 Online/Springかじやまさんのセッションでそんな話題が挙がったらしく。






performance_schema.clone_progressCLONEステートメントが走っている間の進捗どうですかを人間に見えるようにするためのテーブルで、
mysql> SELECT * FROM performance_schema.clone_progress;
+------+-----------+-----------+----------------------------+----------------------------+---------+------------+------------+------------+------------+---------------+
| ID | STAGE | STATE | BEGIN_TIME | END_TIME | THREADS | ESTIMATE | DATA | NETWORK | DATA_SPEED | NETWORK_SPEED |
+------+-----------+-----------+----------------------------+----------------------------+---------+------------+------------+------------+------------+---------------+
| 1 | DROP DATA | Completed | 2020-04-28 08:28:31.995013 | 2020-04-28 08:28:32.196990 | 1 | 0 | 0 | 0 | 0 | 0 |
| 1 | FILE COPY | Completed | 2020-04-28 08:28:32.197136 | 2020-04-28 08:30:19.792994 | 2 | 7608587553 | 7608587553 | 7609004364 | 0 | 0 |
| 1 | PAGE COPY | Completed | 2020-04-28 08:30:19.863918 | 2020-04-28 08:30:21.697040 | 2 | 0 | 0 |197 | 0 | 0 |
| 1 | REDO COPY | Completed | 2020-04-28 08:30:26.150594 | 2020-04-28 08:30:26.803841 | 2 | 2560 | 2560 | 3129 | 0 | 0 |
| 1 | FILE SYNC | Completed | 2020-04-28 08:30:28.247954 | 2020-04-28 08:30:35.971538 | 2 | 0 | 0 | 0 | 0 | 0 |
| 1 | RESTART | Completed | 2020-04-28 08:30:35.971538 | 2020-04-28 08:30:41.140485 | 0 | 0 | 0 | 0 | 0 | 0 |
| 1 | RECOVERY | Completed | 2020-04-28 08:30:41.140485 | 2020-04-28 08:30:41.562447 | 0 | 0 | 0 | 0 | 0 | 0 |
+------+-----------+-----------+----------------------------+----------------------------+---------+------------+------------+------------+------------+---------------+
こんな風に見える。 CLONE INSTANCE FROMでリモートにデータ転送をしている場合、このテーブルが見えるのはレシピエント ( CLONE INSTANCE FROMを実行した側) であってドナー (データを供給している側) ではない。
そもそもよく考えれば、 PERFORMANCE_SCHEMAストレージエンジンは「統計情報格納専用のインメモリストレージエンジン」だと散々聞かされてきたのに、コイツは揮発しない( CLONE INSTANCE FROMはそもそもデータをコピーしてきたあと自動で再起動するし、そのあと何度 mysqldの停止起動を挟んでもこのテーブルは同じ値を返し続ける)
さてさて…取り敢えずソースコードをテーブル名の “clone_progress” で検索してみた。
変数の名前が CLONE_VIEW_PROGRESS_FILEでなんかもうここでオチが見えた。
Progress_pfs::Data::writeで書いてるんだろうけど、 std::ofstreamでファイルを開いて書いてるだけ。
ファイルを読み出してテーブルとして見せるところで頑張っているっぽい。
となるとやることは1つで、このファイル(確かに今はシンクロしている)を
$ cat /var/lib/mysql/#clone/#view_progress
1
2 1 1588062511995013 1588062512196990 0 0 0
2 2 1588062512197136 1588062619792994 7608587553 7608587553 7609004364
2 2 1588062619863918 1588062621697040 0 0 197
2 2 1588062626150594 1588062626803841 2560 2560 3129
2 2 1588062628247954 1588062635971538 0 0 0
2 0 1588062635971538 1588062641140485 0 0 0
2 0 1588062641140485 1588062641562447 0 0 0
こうじゃ
$ cat /var/lib/mysql/#clone/#view_progress
1
2 1 082508250825 1588062512196990 0 0 0
2 2 1588062512197136 1588062619792994 7608587553 7608587553 7609004364
2 2 1588062619863918 1588062621697040 0 0 197
2 2 1588062626150594 1588062626803841 2560 2560 3129
2 2 1588062628247954 1588062635971538 0 0 0
2 0 1588062635971538 1588062641140485 0 0 0
2 0 1588062641140485 1588062641562447 0 0 0
ただ SELECTし直しても FLUSH TABLESでテーブルを閉じても反映はされなかった。これを読むのは mysqldの起動時のみっぽい(と予測)
mysql> RESTART;

mysql> SELECT * FROM performance_schema.clone_progress;
+------+-----------+-----------+----------------------------+----------------------------+---------+------------+------------+------------+------------+---------------+
| ID | STAGE | STATE | BEGIN_TIME | END_TIME | THREADS | ESTIMATE | DATA | NETWORK | DATA_SPEED | NETWORK_SPEED |
+------+-----------+-----------+----------------------------+----------------------------+---------+------------+------------+------------+------------+---------------+
| 1 | DROP DATA | Completed | 1970-01-01 22:55:08.250825 | 2020-04-28 08:28:32.196990 | 1 | 0 | 0 | 0 | 0 | 0 |
| 1 | FILE COPY | Completed | 2020-04-28 08:28:32.197136 | 2020-04-28 08:30:19.792994 | 2 | 7608587553 | 7608587553 | 7609004364 | 0 | 0 |
| 1 | PAGE COPY | Completed | 2020-04-28 08:30:19.863918 | 2020-04-28 08:30:21.697040 | 2 | 0 | 0 | 197 | 0 | 0 |
| 1 | REDO COPY | Completed | 2020-04-28 08:30:26.150594 | 2020-04-28 08:30:26.803841 | 2 | 2560 | 2560 | 3129 | 0 | 0 |
| 1 | FILE SYNC | Completed | 2020-04-28 08:30:28.247954 | 2020-04-28 08:30:35.971538 | 2 | 0 | 0 | 0 | 0 | 0 |
| 1 | RESTART | Completed | 2020-04-28 08:30:35.971538 | 2020-04-28 08:30:41.140485 | 0 | 0 | 0 | 0 | 0 | 0 |
| 1 | RECOVERY | Completed | 2020-04-28 08:30:41.140485 | 2020-04-28 08:30:41.562447 | 0 | 0 | 0 | 0 | 0 | 0 |
+------+-----------+-----------+----------------------------+----------------------------+---------+------------+------------+------------+------------+---------------+
7 rows in set (0.00 sec)
DROP DATAのBEGIN_TIMEが 08秒250825になってるので書き換え成功してますね ;)
なるほど道理で、InnoDB Clusterの実験している時に mysqldを停止して起動させてから mysqlshで戻そうとする時に、CLONEもしてないbinlogの差分同期だけでも Status:CLONEINGみたいになる訳だ…(このファイルはずっと残り続けるから…
ちなみに、 yoku0825とか整数型でなさそうな文字列を入れたらその行は NULLになりました。ためしてみましょう!

pt-query-digestでtcpdumpから集約せずに全てのクエリーを取り出す

$
0
0

TL;DR


書き出しが全てなのでそれは置いておいてハマったこと。

  • pt-query-digest --type=tcpdumpの2020年問題

  • --outputのバリエーションは --helpでは調べられない

    • man pt-query-digestか、 ドキュメントを見る
    • 俺はパッチ当てようとソースコード泳いでたら気が付いた…
  • サンプル取るのに sysbenchを8.0クライアントライブラリにリンクしてコンパイルしたけど、TCP経由の通信はデフォルトでSSL/TLSを使ってた

    • Connector/Cのデフォルトをそのまま使ってた

127.0.0.1:64080と3306以外のポートを使っているので pt-query-digest --watch-serverで指定する必要がある。

$ ./src/sysbench --mysql-ssl=DISABLED --mysql-user=sbtest --mysql-host=127.0.0.1 --mysql-port=64080 oltp_point_select --table_size=100 run

$ sudo tcpdump -s 65535 -x -nn -q -tttt -i any port 64080 | pt-query-digest --type=tcpdump --watch-server=127.0.0.1:64080 --no-report --output=slowlog
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on any, link-type LINUX_SLL (Linux cooked), capture size 65535 bytes
Reading from STDIN ...
TCP session 127.0.0.1:54906 had errors, will save them in /tmp/pt-query-digest-errors.fAJEZ24
# Time: 200518 19:21:39.084382
# Client: 127.0.0.1:54906
# Thread_id: 4294967296
# Query_time: 0.000168 Lock_time: 0.000000 Rows_sent: 0 Rows_examined: 0
PREPARE SELECT c FROM sbtest1 WHERE id=?;
# Time: 200518 19:21:39.084792
# Client: 127.0.0.1:54906
# Thread_id: 4294967296
# Query_time: 0.000246 Lock_time: 0.000000 Rows_sent: 0 Rows_examined: 0
EXECUTE SELECT c FROM sbtest1 WHERE id=42;
# Time: 200518 19:21:39.084989
# Client: 127.0.0.1:54906
# Thread_id: 4294967296
# Query_time: 0.000121 Lock_time: 0.000000 Rows_sent: 0 Rows_examined: 0
EXECUTE SELECT c FROM sbtest1 WHERE id=46;

うむ。

yt-rename-databaseでかつての RENAME DATABASE っぽいことをする

$
0
0

MySQLに RENAME DATABASEが存在していたのは 5.1.7から5.1.22の短い期間だけ(5.1のGAは5.1.30なのでその時にはもう消えていた)

にも拘わらず、かつての日本語版ドキュメントのカバー範囲が「5.1.15-betaまで」なため、俺と似たような時期に日本語ドキュメントを使って勉強していた人はよく知っているイメージ。

ちなみに無くなった理由は「色々考慮してないものが多すぎて色々壊れまくったから」だと思う(個人の見解です)


とはいえごく稀に、「現在のデータベースを他の名前にリネームして切り戻し用にとっておきたい(データだけでも)」ということはあるので、 RENAME DATABASEを模した動きをするスクリプトを作った。

その名も yt-rename-database
Yoku-san no ToolKITの仲間なので、CentOS 7.xなら releaseごとにrpmを作ってある。
(ただし 0.2.1-0 にはバグがあって yt-rename-database --helpができない…(゚∀。)

$ /usr/local/bin/yt-rename-database --help
..
* --ask-pass, --ask-password, --askpass
Ask --password by prompt

* --debug
Set debug output

* --dest=value, --destination=value, --dst=value, --to=value
Database-name moving to

* --execute
Execute statements. If --execute is not specified, only print statements.

* --force, -f
Force RENAME if --to has TRIGGERS, EVENTS, ROUTINES, VIEWS, and Foreign Keys.

* --from=value, --source=value, --src=value
Database-name moving from

* --help, --usage
Show this message

* --host=value, -h=value { Default: localhost }
MySQL host

* --ignore-event, --ignore-events
Force RENAME if --to has EVENTS.

* --ignore-fk
Force RENAME if --to has ROUTINES.

* --ignore-routine, --ignore-routines
Force RENAME if --to has ROUTINES.

* --ignore-trigger, --ignore-triggers
Force RENAME if --to has TRIGGERS.

* --ignore-view, --ignore-views
Force RENAME if --to has VIEWS.

* --password=value, -p=value
Password for the user specified by --user

* --port=value, -P=value
MySQL port number

* --quiet, --silent, -q, -s
No output any messages

* --socket=value, -S=value
Path to mysql.sock
(this parameter is used when --host=localhost)

* --timeout=value { Default: 1 }
Seconds before timeout
(Set into read_timeout, write_timeout, connect_timeout)

* --user=value, -u=value
MySQL account using for connection and checking
(need REPLICATION CLIENT, PROCESSLIST and global SELECT priv)

* --verbose, -v
Verbose output mode

* --version, -V
Show ytkit version

..

接続関連のオプションは mysqlコマンドラインクライアントのものとほぼ一緒。
地味に ytkitで一番気に入ってるのはオプションパーサーで、MySQL公式のクライアント群を模して --socket=/tmp/mysql.sock, --socket /tmp/mysql.sock, -S /tmp/mysql.sock, -S/tmp/mysql.sockが全部ちゃんとパースできる(おまけに公式ではできない -S=/tmp/mysql.sockもできる)

-pの引数が空っぽの時のハンドルは模すのが面倒だったので percona-toolkit や innotopみたいに --askpassオプションがついている(これ、ptとinnotopで確か —ask-passと—askpassと揺れててたまに間違えるので両方とも受け取れるようにしてる)

--from--toが必須オプションになっていて、そいつらを指定しているとこんな風にペロッと「 RENAME TABLEで中身を全部新しいスキーマに移す」ようなSQLスクリプトを吐く。

$ /usr/local/bin/yt-rename-database -uroot -S /usr/mysql/8.0.20/data/mysql.sock --askpass --to=d11 --from=d1
Password:
-- Emulating RENAME DATABASE d1 TO d11
-- I'm dry-run mode. Specify --execute if you wish to execute statements by script.
CREATE DATABASE `d11`;
-- I'm dry-run mode. Specify --execute if you wish to execute statements by script.
RENAME TABLE `d1`.`t1` TO `d11`.`t1`;
-- I'm dry-run mode. Specify --execute if you wish to execute statements by script.
DROP DATABASE `d1`;

もとの RENAME DATABASE時代にひどい目に遭ったらしいForeignKeyやTrigger, Viewなどがあるとそのままではエラーになる。

$ /usr/local/bin/yt-rename-database -uroot -S /usr/mysql/8.0.20/data/mysql.sock --askpass --from=admintool --to=_admintool
Password:
-- Emulating RENAME DATABASE admintool TO _admintool
admintool has Foreign Keys, not supporting.(For *forcing-execution*, use --ignore-fk or --force) at /home/yoku0825/git/ytkit/bin/../lib/Ytkit/RenameDatabase.pm line 99.

警告を無視する(その辺の不整合はセルフでカバーする)場合のオプションはエラー出力に含まれるようになっている(さっきコミットしたばっか)。

$ /usr/local/bin/yt-rename-database -uroot -S /usr/mysql/8.0.20/data/mysql.sock --askpass --from=admintool --to=_admintool --ignore-fk  --execute
-- Emulating RENAME DATABASE admintool TO _admintool
-- admintool has Foreign Keys, not supporting.(For *forcing-execution*, use --ignore-fk or --force) at /home/yoku0825/git/ytkit/bin/../lib/Ytkit/RenameDatabase.pm line 93.

--executeをつけると実際にスクリプトを実行してリネームする。
あんまり使うこと考えてなくて、 --executeなしでSQLスクリプトとして出力させてぺたこんと貼ればいいんじゃないかなと思っている。

基本的に SHOW TABLESRENAME TABLEを使っているだけなので、ワンライナーでも余裕でいけると思う。

$ mysql -uroot -sse "SHOW TABLES FROM <from_db>" | while read table ; do
> mysql -uroot -ve "RENAME TABLE <from_db>.$table TO <to_db>.$table"
> done

ふと思ったので作ってみたのでした。


yt-healthcheckが使っている、そのMySQLがマスターなのかスレーブなのかを判定する方法

$
0
0

Yoku-san no ToolKITyt-healthcheckはデフォルトでは「接続先のMySQLがマスターなのかスレーブなのか mikasafabricなのか」によって監視項目を切り替える。

—roleによって明示的に “master” なり “slave” なりを押し込むこともできるけれど、デフォルトは “auto” で、 yt-healthcheck自身が勝手にマスターかスレーブかあるいは中間マスター(カスケード構成の2段目、子スレーブ(孫スレーブの親))かを判定する。

https://github.com/yoku0825/ytkit/blob/0.2.1/lib/Ytkit/HealthCheck.pm#L181-L207

判定ロジックはこんな感じ。

  • SHOW PROCESSLISTBinlog Dumpまたは Binlog GTID Dumpがいれば、スレーブがつなぎにきているから $masterフラグを立てる
  • SHOW SLAVE STATUSの出力結果があれば親がいるから $slaveフラグを立てる

で、こう。

$slave = true$slave = false
$master = true中間マスターマスター
$master = falseスレーブレプリケーション構成を組んでいない(監視の扱いとしてはマスター)

案外たったこれだけのことで正しくマスタースレーブを判定できるので、案外オススメ。 SHOW SLAVE STATUSには GRANT REPLICATION CLIENTが、 SHOW PROCESSLISTには GRANT PROCESSが必要(そうでないと「自分と同じアカウント以外」が SHOW PROCESSLISTに出て来なくてBinlog Dumpスレッドを検出できない)

ProxySQLとか「read_only=OFFならマスター」みたいな判定をしているやつもあるけど、アレはあてにならない(フェイルオーバーに失敗したら2台以上がread_only=OFFになるのは目に見えてるので)し、だいたい「スレーブがread_only=OFFでした」みたいなのも監視したかったのでそれはできなかった。

ただし $masterフラグが立っていないMySQLに mysqlbinlog -R --stop-neverとかでつなぎにいくと、フラグが立っちゃって監視がおかしくなる(スレーブは read_only= ON, マスターや中間マスターは read_only= OFFでない場合にWARNやCRITICALを吐くようにしているから)

これはもうこういうものだと思ってやっている。


いつの日になるかわからないけど、 SELECT * FROM performance_schema.replication_group_membersの結果が空でなければ「グループレプリケーション」という判定も入れる気がする。必要になったら。

information_schema.tables を定期的に貯めてASCIIグラフにしている

$
0
0

最近(でもないけど) information_schema.tablesの中身を1日1回程度取得してMySQLに突っ込んでいる。

/*!80013 SET SESSION information_schema_stats_expiry = 0; */

SELECT
table_schema AS table_schema,
table_name AS table_name,
table_rows AS table_rows,
data_length AS data_length,
index_length AS index_length,
data_free AS data_free,
engine AS engine,
NOW() AS last_update
FROM
information_schema.tables
WHERE
table_schema NOT IN ('mysql', 'information_schema', 'performance_schema', 'sys') AND
table_type = 'BASE TABLE'
ORDER BY
data_length + index_length DESC;

CREATE TABLE `table_status_info` (
`seq` bigint unsigned NOT NULL AUTO_INCREMENT,
`ipaddr` varchar(15) NOT NULL,
`port` smallint unsigned NOT NULL,
`table_schema` varchar(255) NOT NULL,
`table_name` varchar(255) NOT NULL,
`table_rows` bigint unsigned NOT NULL,
`data_length` bigint unsigned NOT NULL,
`index_length` bigint unsigned NOT NULL,
`data_free` bigint unsigned NOT NULL,
`engine` varchar(32) NOT NULL,
`last_update` datetime NOT NULL,
PRIMARY KEY (`seq`),
KEY `idx_lastupdate` (`last_update`),
KEY `table_status_info_ibfk_1` (`ipaddr`,`port`),
CONSTRAINT `table_status_info_ibfk_1` FOREIGN KEY (`ipaddr`, `port`) REFERENCES `instance_info` (`ipaddr`, `port`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=211 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

INSERT INTO table_status_info
(ipaddr, port, data_free, data_length, engine, index_length, last_update, table_name, table_rows, table_schema)
VALUES
('localhost', '3306', '0', '143310848', 'InnoDB', '9977856', '2020-06-16 10:47:08', 'stock', '391992', 'tpcc'),
..
ON DUPLICATE KEY UPDATE
data_free = VALUES(data_free), /* この書き方は8.0.19でdeprecated.. */
data_length = VALUES(data_length),
engine = VALUES(engine),
index_length = VALUES(index_length),
last_update = VALUES(last_update),
table_name = VALUES(table_name),
table_rows = VALUES(table_rows),
table_schema = VALUES(table_schema);

採取自体は yt-collectを使ってやってる(というよりは、これをやるためにyt-collectを作ったのだから)

$ yt-collect -h${ipaddr} -P${port} -u${watch_user} -p${watch_password} --output=sql --sql-update | mysql -u${local_user} -S${local_socket} -p${local_password} admintool

これに いくつかのViewを噛ませて、こんなSQLで対象を引っ張り出す。

WITH maybe_maintained AS (
SELECT DISTINCT hostname, datadir, table_schema, table_name
FROM adminview.table_status_list_analyze_90
WHERE _diff < 0) -- 差分がマイナスになるタイミングがあるってことはたぶんお掃除バッチが仕事してる
SELECT DISTINCT
hostname, datadir, table_schema, table_name, CAST(_first AS UNSIGNED) AS _fist, CAST(_last AS UNSIGNED) AS _last
FROM
adminview.table_status_list_analyze_90
WHERE
_first > 100000 AND -- 90日くらい前の時点で10万行を超えていて
_last > _first * 1.05 AND -- 90日くらい経って5%以上増えてる
(hostname, datadir, table_schema, table_name) NOT IN (SELECT hostname, datadir, table_schema, table_name FROM maybe_maintained)
AND
(ipaddr, port) NOT IN (SELECT ipaddr, port FROM admintool.slave_info) -- スレーブを除外してマスターだけチェック

set terminal dumbをおぼえて gnuplotのグラフをASCIIで出せるようになってからめっきり gnuplot派になった(was. redashでグラフだけ作る、グラフだけExcel)

sql=$(cat << EOF
SELECT _date, table_rows
FROM adminview.table_status_list_analyze_90
WHERE (hostname, datadir, table_schema, table_name) = ('$hostname', '$datadir', '$table_schema', '$table_name')
ORDER BY _date

EOF
)

mysql -u${local_user} -S${local_socket} -p${local_password} -e "${sql}" | gnuplot -e "set terminal dumb; set xdata time; set timefmt '%Y-%m-%d'; set format y '%10.0f'; plot '< cat -' using 1:2 with lines title ''"

多少なりともお掃除がされているもの ( WITH maybe_maintainedで指定してる _diff < 0になるタイミングがあるやつ ) はこんな感じになる。

   12300000 ++---+---+----+---+----+---+----+---+----+---+----+---+----+--++
+ ***** + + + + + +
12200000 ++ *** * ++
| ** |
12100000 ++ ** ++
| * * |
12000000 ++ ** ++
| * * * |
11900000 ++ * * * * ++
| * * * ** |
| * * * * * |
11800000 ++ * ** * * * ++
| *** ** * * * |
11700000 ++ ** ** * * * ++
| ** * * ***** |
11600000 ++ ** * * ++
| * * |
11500000 ++ * * ++
+ + + + + + * + +
11400000 ++---+---+----+---+----+---+----+---+----+---+-*--+---+----+--++
03/14 03/28 04/11 04/25 05/09 05/23 06/06 06/20

増えているにしても「消し込まれている上で増えてる」ので、必要な容量なのであろう。ということで対象からは除外。

クエリー全体で引っ掛かるのはこんな線形に増えているやつか(要らないレコードは消してね!)

   12800000 ++---+---+----+---+----+---+----+---+----+---+----+---+----**-++
+ + + + + + + *** +
12700000 ++ **** ++
| *** |
12600000 ++ ** ++
| *** |
12500000 ++ ***** ++
| *** |
12400000 ++ *** ++
| *** |
| **** |
12300000 ++ **** ++
| ** |
12200000 ++ *** ++
| *** |
12100000 ++ ** ++
| * |
12000000 ++ *** ++
+ ***+ + + + + + +
11900000 ++---+---+----+---+----+---+----+---+----+---+----+---+----+--++
03/14 03/28 04/11 04/25 05/09 05/23 06/06 06/20

何か良いことでもあったのかな、みたいなグラフ。

   45500000 ++---+---+----+---+----+---+----+---+----+---+----+---+----+--++
+ + + + + + + +
45000000 ++ ************** ++
| * |
44500000 ++ * ++
| * |
44000000 ++ * ++
| * |
43500000 ++ * ++
43000000 ++ * ++
| * |
42500000 ++ * ++
| * |
42000000 ++ * ++
| * |
41500000 ++ ********* ++
| ************ |
41000000 ++ **************** ++
+ + + + + + + +
40500000 ++---+---+----+---+----+---+----+---+----+---+----+---+----+--++
03/14 03/28 04/11 04/25 05/09 05/23 06/06 06/20

Window関数とWITH句と gnuplotだけで案外楽しんでいる毎日でした。

MySQL 8.0.13とそれ以降ではibtmp1は肥大化しない(あるいは、 /var/lib/mysql/#innodb_temp ディレクトリの正体)

$
0
0

TL;DR

  • MySQL :: MySQL 8.0 Reference Manual :: 15.6.3.5 Temporary Tablespaces
  • MySQL 8.0.13とそれ以降ではテンポラリーテーブルの実データ格納に「セッション単位のテンポラリーテーブルスペース」が使われるようになった
    • セッションが終われば領域が解放されるので、ibtmp1のように「mysqldを再起動しないとDisk Fullから復帰できない」ことがなくなった
    • この「セッション単位のテンポラリーテーブルスペース」の格納ディレクトリが datadir/#innodb_tempディレクトリ

PoC

### ダミーデータを1000万行ほど
$ perl -MDigest::MD5 -E 'for (my $n= 1; $n <= 10000000 ; $n++) { printf("%d\t%s\n", $n, Digest::MD5::md5_hex($n)) }'> /tmp/md5

$ ll -h /tmp/md5
-rw-r--r-- 1 yoku0825 yoku0825 390M Jun 18 22:42 /tmp/md5

$ mysql -h172.17.0.2 --local-infile -uroot
mysql> SET GLOBAL local_infile= 1;
mysql> CREATE TABLE t1 (num serial, val varchar(32));
mysql> LOAD DATA LOCAL INFILE '/tmp/md5' INTO TABLE t1;

### InnoDBに落ちるようにTempTableを使わせない
mysql> SET SESSION internal_tmp_mem_storage_engine = Memory;
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT SUBSTR(val, 1, 10) AS v, COUNT(*) AS c FROM t1 LEFT JOIN (SELECT DISTINCT num /2 AS num FROM t1) AS tt1 USING(num) WHERE tt1.num IS NULL GROUP BY v ORDER BY c DESC, v ASC; -- なんでも良いけどとにかくでかそうな暗黙のテンポラリーテーブルを発生させる
..
4999985 rows in set (3 min 0.64 sec)

8.0.12の挙動

$ ll -h /var/lib/mysql/ibtmp1
-rw-r----- 1 mysql mysql 1.1G Jun 19 05:31 /var/lib/mysql/ibtmp1

無事(?)太ってらっしゃる。

8.0.13の挙動

$ ll -h /var/lib/mysql/ibtmp1
-rw-r----- 1 mysql mysql 12M Jun 19 05:33 /var/lib/mysql/ibtmp1

全然太ってない。代わりに、

$ ll -h /var/lib/mysql/#innodb_temp/
total 1.1G
-rw-r----- 1 mysql mysql 80K Jun 19 05:33 temp_1.ibt
-rw-r----- 1 mysql mysql 1.1G Jun 19 05:39 temp_10.ibt
-rw-r----- 1 mysql mysql 80K Jun 19 05:33 temp_2.ibt
-rw-r----- 1 mysql mysql 80K Jun 19 05:33 temp_3.ibt
-rw-r----- 1 mysql mysql 80K Jun 19 05:33 temp_4.ibt
-rw-r----- 1 mysql mysql 80K Jun 19 05:33 temp_5.ibt
-rw-r----- 1 mysql mysql 80K Jun 19 05:33 temp_6.ibt
-rw-r----- 1 mysql mysql 80K Jun 19 05:33 temp_7.ibt
-rw-r----- 1 mysql mysql 80K Jun 19 05:33 temp_8.ibt
-rw-r----- 1 mysql mysql 80K Jun 19 05:33 temp_9.ibt

#innodb_tempディレクトリの.ibtファイルが太っている。
od -cとかで見るとちゃんとデータが入っていて、コイツがテンポラリーテーブルの実体なのだということが見て取れた。

ただしコイツがibtmp1と違うのは、「セッションがクリアされれば.ibtファイルが消える」こと(クエリーが終了すれば、ではない。接続を切らないとダメ)

mysql> quit
Bye

$ ll -h /var/lib/mysql/#innodb_temp/
total 224K
-rw-r----- 1 mysql mysql 80K Jun 19 05:33 temp_1.ibt
-rw-r----- 1 mysql mysql 80K Jun 19 05:41 temp_10.ibt
-rw-r----- 1 mysql mysql 80K Jun 19 05:33 temp_2.ibt
-rw-r----- 1 mysql mysql 80K Jun 19 05:33 temp_3.ibt
-rw-r----- 1 mysql mysql 80K Jun 19 05:33 temp_4.ibt
-rw-r----- 1 mysql mysql 80K Jun 19 05:33 temp_5.ibt
-rw-r----- 1 mysql mysql 80K Jun 19 05:33 temp_6.ibt
-rw-r----- 1 mysql mysql 80K Jun 19 05:33 temp_7.ibt
-rw-r----- 1 mysql mysql 80K Jun 19 05:33 temp_8.ibt
-rw-r----- 1 mysql mysql 80K Jun 19 05:33 temp_9.ibt

やった! すごい! これでInnoDBのテンポラリーテーブルがあふれても安心だ!()

想定Q&A

Q. デカいテンポラリーテーブル作ってたセッションが切れた途端、数ギガバイトがファイルシステムから消されるって怖くない?
A. 暗黙のテンポラリーテーブルがMyISAMの時だってそうだったから、それで問題なければ問題ないんじゃなかろうか

Q. 10個しかないの? 11スレッド以上で同時に使おうとするとどうなるの?
A. temp_11以降の.ibtファイルがどんどん増える。 ドキュメント的には40万(400 thousand)まで行けるとか書いてある

Q. コネクションプールを使っているので、セッションがクリアされないんですが
A. ストレージがあふれたらAPサーバーを再起動だ!

Q. 逆に、今までは512MBを超えたらエラーにするとか頭打ちができたけど、今後はサイズによる頭打ちが出来なくなる?
A. 今のところできなさそう。瞬間的に天井まで行かなきゃいけないMyISAM時代に逆戻り…?

utf8mb3なマスターに絵文字を突っ込んだ時にバイナリログってどうなるんだっけ

$
0
0

TL;DR

  • マスター上で、3バイトUTF-8なカラムとコネクションのcharsetの対応(いずれもsql_mode = ‘STRICT_TRANS_TABLES’ でない )
SET NAMES utf8SET NAMES utf8mb4
utf8なカラム絵文字から後ろが切れる絵文字が ‘?’ になる
utf8mb4なカラム絵文字が ‘????’ になる絵文字が入る
  • マスターが3バイトutf8でスレーブが4バイトutf8(utf8mb4)の場合と↑の対応
ROWSTATEMENT
マスターでは後ろが切れたスレーブでも後ろが切れるスレーブでは絵文字が入る
マスターでは ‘?’ になったスレーブでも ‘?’ になるスレーブでは絵文字が入る
  • 「後ろが全部切れる」のが「’?’ に変換される」のはまだインパクト少なそう(絵文字を入れたら後ろが全部切れるのを前提にしているエンドユーザーはきっといない)
  • binlog_format=ROWの方が安牌
    • マスター切り替え前後でエンドユーザーに見える値は変わらない
    • スレーブの CONVERT TO CHARSET utf8mb4後すぐにマスターを切り替えるなら binlog_format= STATEMENTにしたくなる気持ちはわからなくもない(が、やりたくない)

以下、ログ

binlog_format = ROW

CREATE TABLE .. CHARACTER SET utf8mb3

mysql80 54> CREATE TABLE t1 (num int, val varchar(32)) CHARSET utf8mb3;
Query OK, 0 rows affected, 1 warning (0.03 sec)

mysql80 54> SHOW WARNINGS;
+---------+------+---------------------------------------------------------------------------------------------+
| Level | Code | Message |
+---------+------+---------------------------------------------------------------------------------------------+
| Warning | 1287 | 'utf8mb3' is deprecated and will be removed in a future release. Please use utf8mb4 instead |
+---------+------+---------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)

mysql80 54> SHOW CREATE TABLE t1\G
*************************** 1. row ***************************
Table: t1
Create Table: CREATE TABLE `t1` (
`num` int DEFAULT NULL,
`val` varchar(32) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8
1 row in set (0.01 sec)

SET NAMES utf8mb3

mysql80 54> INSERT INTO t1 VALUES (1, 'utf8mb3による🍣だよ');
Query OK, 1 row affected, 2 warnings (0.00 sec)

mysql80 54> SHOW WARNINGS;
+---------+------+---------------------------------------------------------------------------------+
| Level | Code | Message |
+---------+------+---------------------------------------------------------------------------------+
| Warning | 1300 | Invalid utf8 character string: 'F09F8D' |
| Warning | 1366 | Incorrect string value: '\xF0\x9F\x8D\xA3\xE3\x81...' for column 'val' at row 1 |
+---------+------+---------------------------------------------------------------------------------+
2 rows in set (0.00 sec)

mysql80 54> SELECT * FROM t1;
+------+------------------+
| num | val |
+------+------------------+
| 1 | utf8mb3による |
+------+------------------+
1 row in set (0.00 sec)

# at 772
#200623 13:53:33 server id 1080 end_log_pos 851 CRC32 0x9517e90e Rows_query
# INSERT INTO t1 VALUES (1, 'utf8mb3による🍣だよ')
### INSERT INTO `d11`.`t1`
### SET
### @1=1 /* INT meta=0 nullable=1 is_null=0 */
### @2='utf8mb3による' /* VARSTRING(96) meta=96 nullable=1 is_null=0 */

SET NAMES utf8mb4

mysql80 54> INSERT INTO t1 VALUES (1, 'utf8mb4による🍣だよ');
Query OK, 1 row affected, 1 warning (0.01 sec)

mysql80 54> SHOW WARNINGS;
+---------+------+---------------------------------------------------------------------------------+
| Level | Code | Message |
+---------+------+---------------------------------------------------------------------------------+
| Warning | 1366 | Incorrect string value: '\xF0\x9F\x8D\xA3\xE3\x81...' for column 'val' at row 1 |
+---------+------+---------------------------------------------------------------------------------+
1 row in set (0.00 sec)

mysql80 54> SELECT * FROM t1;
+------+-------------------------+
| num | val |
+------+-------------------------+
| 1 | utf8mb3による |
| 1 | utf8mb4による?だよ |
+------+-------------------------+
2 rows in set (0.00 sec)

# at 1145
#200623 13:54:54 server id 1080 end_log_pos 1224 CRC32 0x4357ac1b Rows_query
# INSERT INTO t1 VALUES (1, 'utf8mb4による🍣だよ')
### INSERT INTO `d11`.`t1`
### SET
### @1=1 /* INT meta=0 nullable=1 is_null=0 */
### @2='utf8mb4による?だよ' /* VARSTRING(96) meta=96 nullable=1 is_null=0 */

CREATE TABLE .. CHARSET utf8mb4

mysql80 54> CREATE TABLE t1 (num int, val varchar(32)) CHARSET utf8mb4;
Query OK, 0 rows affected (0.04 sec)

mysql80 54> SHOW CREATE TABLE t1\G
*************************** 1. row ***************************
Table: t1
Create Table: CREATE TABLE `t1` (
`num` int DEFAULT NULL,
`val` varchar(32) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
1 row in set (0.00 sec)

SET NAMES utf8mb3

mysql80 54> INSERT INTO t1 VALUES (1, 'utf8mb3による🍺だよ');
Query OK, 1 row affected, 2 warnings (0.01 sec)

mysql80 54> SHOW WARNINGS;
+---------+------+---------------------------------------------------------------------------------+
| Level | Code | Message |
+---------+------+---------------------------------------------------------------------------------+
| Warning | 1300 | Invalid utf8 character string: 'F09F8D' |
| Warning | 1366 | Incorrect string value: '\xF0\x9F\x8D\xBA\xE3\x81...' for column 'val' at row 1 |
+---------+------+---------------------------------------------------------------------------------+
2 rows in set (0.00 sec)

mysql80 54> SELECT * FROM t1;
+------+----------------------------+
| num | val |
+------+----------------------------+
| 1 | utf8mb3による????だよ |
+------+----------------------------+
1 row in set (0.00 sec)

# at 1948
#200623 13:57:06 server id 1080 end_log_pos 2027 CRC32 0xccae4843 Rows_query
# INSERT INTO t1 VALUES (1, 'utf8mb3による🍺だよ')
### INSERT INTO `d11`.`t1`
### SET
### @1=1 /* INT meta=0 nullable=1 is_null=0 */
### @2='utf8mb3による????だよ' /* VARSTRING(128) meta=128 nullable=1 is_null=0 */

SET NAMES utf8mb4

mysql80 54> INSERT INTO t1 VALUES (1, 'utf8mb4による🍺だよ');
Query OK, 1 row affected (0.01 sec)

mysql80 54> SELECT * FROM t1;
+------+----------------------------+
| num | val |
+------+----------------------------+
| 1 | utf8mb3による????だよ |
| 1 | utf8mb4による🍺だよ |
+------+----------------------------+
2 rows in set (0.00 sec)

# at 2333
#200623 13:58:52 server id 1080 end_log_pos 2412 CRC32 0xbc81cf0a Rows_query
# INSERT INTO t1 VALUES (1, 'utf8mb4による🍺だよ')
### INSERT INTO `d11`.`t1`
### SET
### @1=1 /* INT meta=0 nullable=1 is_null=0 */
### @2='utf8mb4による🍺だよ' /* VARSTRING(128) meta=128 nullable=1 is_null=0 */

binlog_format = STATEMENT

CREATE TABLE .. CHARACTER SET utf8mb3

SET NAMES utf8mb3

mysql80 57> INSERT INTO t1 VALUES (1, 'utf8mb3による🍣だよ');
Query OK, 1 row affected, 1 warning (0.00 sec)

mysql80 57> SELECT * FROM t1;
+------+------------------+
| num | val |
+------+------------------+
| 1 | utf8mb3による |
+------+------------------+
1 row in set (0.00 sec)

# at 1584
#200623 14:05:34 server id 1080 end_log_pos 1714 CRC32 0xb7219e87 Query thread_id=57 exec_time=0 error_code=0
SET TIMESTAMP=1592888734/*!*/;
INSERT INTO t1 VALUES (1, 'utf8mb3による🍣だよ')

SET NAMES utf8mb4

mysql80 57> INSERT INTO t1 VALUES (1, 'utf8mb4による🍣だよ');
Query OK, 1 row affected, 1 warning (0.00 sec)

mysql80 57> SELECT * FROM t1;
+------+-------------------------+
| num | val |
+------+-------------------------+
| 1 | utf8mb3による |
| 1 | utf8mb4による?だよ |
+------+-------------------------+
2 rows in set (0.00 sec)

# at 1904
#200623 14:06:17 server id 1080 end_log_pos 2034 CRC32 0xae911ee8 Query thread_id=57 exec_time=0 error_code=0
SET TIMESTAMP=1592888777/*!*/;
INSERT INTO t1 VALUES (1, 'utf8mb4による🍣だよ')

CREATE TABLE .. CHARACTER SET utf8mb4

SET NAMES utf8mb3

mysql80 57> INSERT INTO t1 VALUES (1, 'utf8mb3による🍺だよ');
Query OK, 1 row affected, 2 warnings (0.00 sec)

mysql80 57> SELECT * FROM t1;
+------+----------------------------+
| num | val |
+------+----------------------------+
| 1 | utf8mb3による????だよ |
+------+----------------------------+
1 row in set (0.00 sec)

# at 2647
#200623 14:07:40 server id 1080 end_log_pos 2777 CRC32 0xf21371cd Query thread_id=57 exec_time=0 error_code=0
SET TIMESTAMP=1592888860/*!*/;
INSERT INTO t1 VALUES (1, 'utf8mb3による🍺だよ')
/*!*/;

SET NAMES utf8mb4

mysql80 57> INSERT INTO t1 VALUES (1, 'utf8mb4による🍺だよ');
Query OK, 1 row affected (0.01 sec)

mysql80 57> SELECT * FROM t1;
+------+----------------------------+
| num | val |
+------+----------------------------+
| 1 | utf8mb3による????だよ |
| 1 | utf8mb4による🍺だよ |
+------+----------------------------+
2 rows in set (0.00 sec)

# at 2967
#200623 14:08:25 server id 1080 end_log_pos 3097 CRC32 0x29b3d896 Query thread_id=57 exec_time=0 error_code=0
SET TIMESTAMP=1592888905/*!*/;
INSERT INTO t1 VALUES (1, 'utf8mb4による🍺だよ')
/*!*/;

最近のMySQLにテンポラリーなファイル/ディレクトリを指定するオプションが多い気がする件

$
0
0

TL;DR


mysql80 9> SELECT @@version;
+-----------+
| @@version |
+-----------+
| 8.0.20 |
+-----------+
1 row in set (0.00 sec)

mysql80 9> SELECT variable_name, variable_value FROM performance_schema.global_variables WHERE variable_name LIKE '%tmp%' OR variable_name LIKE '%temp%';
+---------------------------------+-----------------------+
| variable_name | variable_value |
+---------------------------------+-----------------------+
| avoid_temporal_upgrade | OFF |
| default_tmp_storage_engine | InnoDB |
| innodb_temp_data_file_path | ibtmp1:12M:autoextend |
| innodb_temp_tablespaces_dir | ./#innodb_temp/ |
| innodb_tmpdir | |
| internal_tmp_mem_storage_engine | TempTable |
| show_old_temporals | OFF |
| slave_load_tmpdir | /tmp |
| temptable_max_ram | 1073741824 |
| temptable_use_mmap | ON |
| tmp_table_size | 16777216 |
| tmpdir | /tmp |
+---------------------------------+-----------------------+
12 rows in set (0.00 sec)
mysql57 6> SELECT @@version;
+------------+
| @@version |
+------------+
| 5.7.30-log |
+------------+
1 row in set (0.00 sec)

mysql57 6> SELECT variable_name, variable_value FROM performance_schema.global_variables WHERE variable_name LIKE '%tmp%' OR variable_name LIKE '%temp%';
+----------------------------------+-----------------------+
| variable_name | variable_value |
+----------------------------------+-----------------------+
| avoid_temporal_upgrade | OFF |
| default_tmp_storage_engine | InnoDB |
| innodb_temp_data_file_path | ibtmp1:12M:autoextend |
| innodb_tmpdir | |
| internal_tmp_disk_storage_engine | InnoDB |
| max_tmp_tables | 32 |
| show_old_temporals | OFF |
| slave_load_tmpdir | /tmp |
| tmp_table_size | 16777216 |
| tmpdir | /tmp |
+----------------------------------+-----------------------+
10 rows in set (0.00 sec)
  • 5.6は performance_schemaではなくて information_schema
mysql56> SELECT @@version;
+------------+
| @@version |
+------------+
| 5.6.48-log |
+------------+
1 row in set (0.00 sec)

mysql56> SELECT variable_name, variable_value FROM information_schema.global_variables WHERE variable_name LIKE '%tmp%' OR variable_name LIKE '%temp%';
+----------------------------+----------------+
| variable_name | variable_value |
+----------------------------+----------------+
| INNODB_TMPDIR | |
| SHOW_OLD_TEMPORALS | OFF |
| TMPDIR | /tmp |
| MAX_TMP_TABLES | 32 |
| DEFAULT_TMP_STORAGE_ENGINE | InnoDB |
| AVOID_TEMPORAL_UPGRADE | OFF |
| TMP_TABLE_SIZE | 16777216 |
| SLAVE_LOAD_TMPDIR | /tmp |
+----------------------------+----------------+
8 rows in set (0.00 sec)

performance_schema.data_locks.ENGINE_LOCK_ID is 何

$
0
0

TL;DR

  • MySQL 8.0.20のInnoDBにおいては row->lock_trx_immutable_id ":" row->lock_space ":" row->lock_page ":" row->lock_rec ":" row->lock_immutable_idらしい
  • ちなみにこのENGINE_LOCK_ID(実体は pk_pos_data_lock::m_engine_lock_id ?)を真面目に実装しているのはInnoDBだけっぽく見える
    • NDBCLUSTERは読んでない

そう思って見てみると、行単位でどこのページに乗っかってるのかとか調べられたりするかなあと思ったり思わなかったり。

mysql80 18> SELECT SUBSTRING_INDEX(engine_lock_id, ':', 1) AS lock_trx_id, SUBSTRING_INDEX(SUBSTRING_INDEX(engine_lock_id, ':', 2), ':', -1) AS space_id, SUBSTRING_INDEX(SUBSTRING_INDEX(engine_lock_id, ':', 3), ':', -1) AS page_id, SUBSTRING_INDEX(SUBSTRING_INDEX(engine_lock_id, ':', 4), ':', -1) AS record_id, SUBSTRING_INDEX(SUBSTRING_INDEX(engine_lock_id, ':', 5), ':', -1) AS lock_id, lock_data FR
OM data_locks WHERE lock_type = 'RECORD';
+-----------------+----------+---------+-----------+-----------------+------------------------+
| lock_trx_id | space_id | page_id | record_id | lock_id | lock_data |
+-----------------+----------+---------+-----------+-----------------+------------------------+
| 140293334355320 | 166 | 4 | 1 | 140293227750848 | supremum pseudo-record |
| 140293334355320 | 166 | 4 | 2 | 140293227750848 | 1 |
| 140293334355320 | 166 | 4 | 3 | 140293227750848 | 2 |
| 140293334355320 | 166 | 4 | 4 | 140293227750848 | 3 |
| 140293334355320 | 166 | 4 | 5 | 140293227750848 | 4 |
| 140293334355320 | 166 | 4 | 6 | 140293227750848 | 5 |
| 140293334355320 | 166 | 4 | 7 | 140293227750848 | 6 |
+-----------------+----------+---------+-----------+-----------------+------------------------+
7 rows in set (0.00 sec)

mysql80 19> OPTIMIZE TABLE t1; -- これで↓のspace_idが変わった

mysql80 18> SELECT SUBSTRING_INDEX(engine_lock_id, ':', 1) AS lock_trx_id, SUBSTRING_INDEX(SUBSTRING_INDEX(engine_lock_id, ':', 2), ':', -1) AS space_id, SUBSTRING_INDEX(SUBSTRING_INDEX(engine_lock_id, ':', 3), ':', -1) AS page_id, SUBSTRING_INDEX(SUBSTRING_INDEX(engine_lock_id, ':', 4), ':', -1) AS record_id, SUBSTRING_INDEX(SUBSTRING_INDEX(engine_lock_id, ':', 5), ':', -1) AS lock_id, lock_data FROM data_locks WHERE lock_type = 'RECORD';
+-----------------+----------+---------+-----------+-----------------+------------------------+
| lock_trx_id | space_id | page_id | record_id | lock_id | lock_data |
+-----------------+----------+---------+-----------+-----------------+------------------------+
| 140293334355320 | 167 | 4 | 1 | 140293227750848 | supremum pseudo-record |
| 140293334355320 | 167 | 4 | 2 | 140293227750848 | 1 |
| 140293334355320 | 167 | 4 | 3 | 140293227750848 | 2 |
| 140293334355320 | 167 | 4 | 4 | 140293227750848 | 3 |
| 140293334355320 | 167 | 4 | 5 | 140293227750848 | 4 |
| 140293334355320 | 167 | 4 | 6 | 140293227750848 | 5 |
| 140293334355320 | 167 | 4 | 7 | 140293227750848 | 6 |
+-----------------+----------+---------+-----------+-----------------+------------------------+
7 rows in set (0.00 sec)

↓i_sほにゃららのくせにp_sから呼ばれている図

(gdb) bt
+bt
#0 trx_i_s_create_lock_id (row=row@entry=0x7f4918052850,
lock_id=lock_id@entry=0x7f4918052970 "139951965544648:1248:139951844968944", lock_id_size=lock_id_size@entry=105)
at /home/yoku0825/mysql-8.0.20/storage/innobase/trx/trx0i_s.cc:1154
#1 0x000000000205a373 in print_record_lock_id (lock=lock@entry=0x7f4914013dd0, heap_no=heap_no@entry=2,
lock_id=lock_id@entry=0x7f4918052970 "139951965544648:1248:139951844968944", lock_id_size=lock_id_size@entry=105)
at /home/yoku0825/mysql-8.0.20/storage/innobase/handler/p_s.cc:519
#2 0x000000000205ac5a in Innodb_data_lock_iterator::scan_trx (this=this@entry=0x7f48c400ff40,
container=container@entry=0x7f48c42ca828, with_lock_data=with_lock_data@entry=true, trx=trx@entry=0x7f491b3120c8,
with_filter=with_filter@entry=false, filter_lock_immutable_id=filter_lock_immutable_id@entry=0, filter_heap_id=0)
at /home/yoku0825/mysql-8.0.20/storage/innobase/handler/p_s.cc:804
#3 0x000000000205ae8f in Innodb_data_lock_iterator::scan_trx_list (this=this@entry=0x7f48c400ff40,
container=container@entry=0x7f48c42ca828, with_lock_data=with_lock_data@entry=true, read_write=read_write@entry=true,
trx_list=<optimized out>) at /home/yoku0825/mysql-8.0.20/storage/innobase/handler/p_s.cc:684
#4 0x000000000205b76a in scan (with_lock_data=true, container=0x7f48c42ca828, this=0x7f48c400ff40)
at /home/yoku0825/mysql-8.0.20/storage/innobase/handler/p_s.cc:593
#5 Innodb_data_lock_iterator::scan (this=0x7f48c400ff40, container=0x7f48c42ca828, with_lock_data=<optimized out>)
at /home/yoku0825/mysql-8.0.20/storage/innobase/handler/p_s.cc:572
#6 0x000000000249252b in table_data_locks::rnd_next() () at /home/yoku0825/mysql-8.0.20/storage/perfschema/table_data_locks.cc:191
#7 0x000000000243fc6e in ha_perfschema::rnd_next (this=0x7f48c42bf838, buf=0x7f48c42d8e38 "\377\377")
at /home/yoku0825/mysql-8.0.20/storage/perfschema/ha_perfschema.cc:1641
#8 0x00000000010df114 in handler::ha_rnd_next (this=0x7f48c42bf838, buf=0x7f48c42d8e38 "\377\377")
at /home/yoku0825/mysql-8.0.20/sql/handler.cc:2966
#9 0x0000000000e0d72d in TableScanIterator::Read (this=0x7f48c42d8390) at /home/yoku0825/mysql-8.0.20/sql/row_iterator.h:275
#10 0x0000000000f702b3 in SELECT_LEX_UNIT::ExecuteIteratorQuery(THD*) () at /home/yoku0825/mysql-8.0.20/sql/sql_union.cc:1183
#11 0x0000000000f7047c in SELECT_LEX_UNIT::execute(THD*) () at /home/yoku0825/mysql-8.0.20/sql/sql_union.cc:1235
#12 0x0000000000f0245b in Sql_cmd_dml::execute_inner(THD*) () at /home/yoku0825/mysql-8.0.20/sql/sql_select.cc:945
#13 0x0000000000f0c449 in Sql_cmd_dml::execute(THD*) () at /home/yoku0825/mysql-8.0.20/sql/sql_select.cc:725
#14 0x0000000000eb6435 in mysql_execute_command(THD*, bool) () at /home/yoku0825/mysql-8.0.20/sql/sql_parse.cc:4489
#15 0x0000000000eb8378 in mysql_parse (thd=thd@entry=0x7f48c4010140, parser_state=parser_state@entry=0x7f49180544d0)
at /home/yoku0825/mysql-8.0.20/sql/sql_parse.cc:5306
#16 0x0000000000eba4d5 in dispatch_command(THD*, COM_DATA const*, enum_server_command) ()
at /home/yoku0825/mysql-8.0.20/sql/sql_parse.cc:1776
#17 0x0000000000ebb314 in do_command (thd=thd@entry=0x7f48c4010140) at /home/yoku0825/mysql-8.0.20/sql/sql_parse.cc:1274
#18 0x0000000000fccc70 in handle_connection (arg=arg@entry=0x757af30)
at /home/yoku0825/mysql-8.0.20/sql/conn_handler/connection_handler_per_thread.cc:302
#19 0x000000000244315c in pfs_spawn_thread (arg=0x55f8070) at /home/yoku0825/mysql-8.0.20/storage/perfschema/pfs.cc:2854
#20 0x00007f49276caea5 in start_thread (arg=0x7f4918055700) at pthread_create.c:307
#21 0x00007f49258538dd in clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:111
(gdb) frame 14
+frame 14
#14 0x0000000000eb6435 in mysql_execute_command(THD*, bool) () at /home/yoku0825/mysql-8.0.20/sql/sql_parse.cc:4489
4489 res = lex->m_sql_cmd->execute(thd);
(gdb) p thd->m_query_string
+p thd->m_query_string
$3 = {
str = 0x7f48c42d4608 "SELECT * FROM performance_schema.data_locks",
length = 43
}

MySQL徹底入門 第4版の執筆に参加しました

$
0
0

TL;DR


掲題の通り、MySQL徹底入門の執筆に参加させていただきました。

第3版を「読んで勉強していた」のが、第4版を書く側として声をかけていただいて、人生何があるかわからないものだなあ。

飽くまで「入門」なので、「実践ハイパフォーマンスMySQL」のような玄人向け成分はほぼありません。たぶん。

俺の担当は 6章(運用), 8章(レプリケーション), 9章(バックアップとリストア), 10章(プログラミングのうちPerl), 14章(逆引きMySQL辞典の一部) となっています。

6章(運用)

  • いわゆる MySQLを運用しましょう的な内容ではない
  • 「運用するにはこれくらいの基礎知識は必要だよね?」な内容が詰まっています
    • SET GLOBALで即時有効にならないパラメーターの話とか
    • my.cnfの読み込まれる順番の話とか
    • MySQLが「インデックスをどう使うからWHEREやORDER BY .. LIMITが速くなるのか」とか
      • 一部の方にご好評いただいている「トランプを使ったたとえ」が初めて文書化されています :)
    • レプリケーションを使ったローリングアップグレードの仕組みの話とか

8章(レプリケーション)

  • (非同期)レプリケーションとグループレプリケーションの「仕組み」に言及しています
    • 非同期レプリケーションではBinlog Dumpスレッド、I/Oスレッド、SQLスレッド、じゃあグループレプリケーションは? :)
      • というくらいのレベル感です
  • レプリケーション関連のオプションのオススメ設定や、設定時に考えることが割と列挙されています
  • なんとレプリケーションの構築手順が 全てMySQL Shellから構築する方法しか紹介されていませんよってgtid_mode=ONも前提です。
    • 8.0のこの時代に「入門」する人は、もうMySQL Shellからの方法だけおぼえればいいんじゃないかな…

9章(バックアップとリストア)

  • 目新しいところは Cloneステートメントを使ったサンプルがあるところと、 mysqlbinlog --read-from-remote-server --stop-never --rawを使ったサンプルがあるところでしょうか
  • GTIDを前提に解説しているのでGTIDに不慣れな人には良い鴨

10章(プログラミング - Perl)

  • たぶん中括弧と代入の使い方がキモチワルイと思うんですが、これは(CにできてPerlにできない書き方を除いて)「 昔の MySQLのコーディング規約」に毒された俺の末路です。

14章(逆引きMySQL辞典)

  • 本編(?)とは違ったテンションでお送りする一問一答…とは限らないQ&Aっぽいコーナー
  • ここ書いてて本当に楽しかった章で、読み返しの時に「俺が書いたような気がしたものが別は他の人が書いた部分」「この人が書いただろうなと思ったものを書いたのは実は俺」みたいな錯覚がありました

参加できたことを嬉しく思いますし、全体通してもなかなか面白く読み応えのある本になっていますので、是非お手に取っていただければと。

俺が一番読んでて面白かったのは第11章の「文字コードと日本語環境」です :D


MySQLで CURDATE() - 1 は「昨日の日付」を返さない

$
0
0

TL;DR

  • 意図したことをMySQLでやるには CURDATE() - INTERVAL 1 DAY
  • というか俺はむしろDATE型から数値を引くことに違和感があるんですがこれってOracleの書き方なんでしたっけ?

昨日の朝、こんなのを見た。

昨日の夜、このブログを読んだ。

けんつさんのブログは「文字列とDATE型」だけれど、バグレポートの方は「数値とDATE型」に起因する。
折角なのでちょっと書いておこうかと思った。


まず、DATE型と数値型を演算しようとするとDATE型が数値型にキャストされて戻り値も数値型になる。
DATE型から数値型へのキャストは 年 * 1000000 + 月 * 1000 + 日で行われる(DATETIME型の場合は時分秒を格納するために更に桁数が増える)。逆の操作で有効な日付が生成できれば、数値型からDATE型へのキャストもできる。

mysql80 13> SELECT CURDATE(), CAST(CURDATE() AS SIGNED), CAST(20200702 AS DATE);
+------------+---------------------------+------------------------+
| CURDATE() | CAST(CURDATE() AS SIGNED) | CAST(20200702 AS DATE) |
+------------+---------------------------+------------------------+
| 2020-07-02 | 20200702 | 2020-07-02 |
+------------+---------------------------+------------------------+
1 row in set (0.00 sec)

よって(2020/7/2から)1を引くと20200701になるし、これはDATE型にキャストし返せる。 - INTERVAL 1 DAYにしておけば最初からDATE型で返ってくる。

mysql80 13> SELECT CURDATE() - 1, CAST(CURDATE() - 1 AS DATE), CURDATE() - INTERVAL 1 DAY;
+---------------+-----------------------------+----------------------------+
| CURDATE() - 1 | CAST(CURDATE() - 1 AS DATE) | CURDATE() - INTERVAL 1 DAY |
+---------------+-----------------------------+----------------------------+
| 20200701 | 2020-07-01 | 2020-07-01 |
+---------------+-----------------------------+----------------------------+
1 row in set (0.00 sec)

これが月をまたぐと雲行きが怪しくなってくる。

mysql80 13> SELECT CURDATE() - 2, CAST(CURDATE() - 2 AS DATE), CURDATE() - INTERVAL 2 DAY;
+---------------+-----------------------------+----------------------------+
| CURDATE() - 2 | CAST(CURDATE() - 2 AS DATE) | CURDATE() - INTERVAL 2 DAY |
+---------------+-----------------------------+----------------------------+
| 20200700 | 2020-07-00 | 2020-06-30 |
+---------------+-----------------------------+----------------------------+
1 row in set (0.00 sec)

mysql80 13> SELECT CURDATE() - 3, CAST(CURDATE() - 3 AS DATE), CURDATE() - INTERVAL 3 DAY;
+---------------+-----------------------------+----------------------------+
| CURDATE() - 3 | CAST(CURDATE() - 3 AS DATE) | CURDATE() - INTERVAL 3 DAY |
+---------------+-----------------------------+----------------------------+
| 20200699 | NULL | 2020-06-29 |
+---------------+-----------------------------+----------------------------+
1 row in set, 1 warning (0.00 sec)

閑話休題。今年1年分の日付だけをDATE型で突っ込んだ cal というテーブルを用意した。そういえば今年は閏年だったのか。

mysql80 15> SELECT * FROM cal;
+------------+
| _date |
+------------+
| 2020-01-01 |
| 2020-01-02 |
| 2020-01-03 |

..
| 2020-12-29 |
| 2020-12-30 |
| 2020-12-31 |
+------------+
366 rows in set (0.00 sec)

これで >=を使って比較してみる(「n日前から今日まで」みたいなイメージ)。
やはり月をまたぐとおかしくなるが、「本当に6月分のレコードが存在しなければこういう結果セットでもおかしくないよね」みたいな結果セットであるところがにくい。

mysql80 15> SELECT MIN(_date), CURDATE() - 1, CURDATE() - INTERVAL 1 DAY FROM cal WHERE _date >= CURDATE() - 1;
+------------+---------------+----------------------------+
| MIN(_date) | CURDATE() - 1 | CURDATE() - INTERVAL 1 DAY |
+------------+---------------+----------------------------+
| 2020-07-01 | 20200701 | 2020-07-01 |
+------------+---------------+----------------------------+
1 row in set (0.00 sec)

mysql80 15> SELECT MIN(_date), CURDATE() - 2, CURDATE() - INTERVAL 2 DAY FROM cal WHERE _date >= CURDATE() - 2;
+------------+---------------+----------------------------+
| MIN(_date) | CURDATE() - 2 | CURDATE() - INTERVAL 2 DAY |
+------------+---------------+----------------------------+
| 2020-07-01 | 20200700 | 2020-06-30 |
+------------+---------------+----------------------------+
1 row in set, 1 warning (0.00 sec)

mysql80 15> SELECT MIN(_date), CURDATE() - 3, CURDATE() - INTERVAL 3 DAY FROM cal WHERE _date >= CURDATE() - 3;
+------------+---------------+----------------------------+
| MIN(_date) | CURDATE() - 3 | CURDATE() - INTERVAL 3 DAY |
+------------+---------------+----------------------------+
| 2020-07-01 | 20200699 | 2020-06-29 |
+------------+---------------+----------------------------+
1 row in set, 1 warning (0.00 sec)

なおこのパターン、体感として「30日」か「90日」を超えたあたりで気が付く人が多い。

30日以上過去の日付は MONTH(_date)の値が変わることが多くの場合期待されるが、数値型になって30を引かれても100の位は変わらないから、先月ぶんはまるまる引っ掛からない。これに違和感を覚えるのが「30日」のパターン。

mysql80 15> SELECT MIN(_date), CURDATE() - 60, CURDATE() - INTERVAL 60 DAY FROM cal WHERE _date >= CURDATE() - 60;
+------------+----------------+-----------------------------+
| MIN(_date) | CURDATE() - 60 | CURDATE() - INTERVAL 60 DAY |
+------------+----------------+-----------------------------+
| 2020-07-01 | 20200642 | 2020-05-03 |
+------------+----------------+-----------------------------+
1 row in set, 1 warning (0.00 sec)

90日以上過去の日付を数値型にキャストするとだいたい 再びDATE型にキャスト可能な10の位の範囲に戻ってきて、到底期待されない日付として扱われる。これに違和感をおぼえるのが「90日」のパターン。

mysql80 15> SELECT MIN(_date), CURDATE() - 90, CURDATE() - INTERVAL 90 DAY FROM cal WHERE _date >= CURDATE() - 90; -- 7/1で70だと上手くいかないので90にしたけど

+------------+----------------+-----------------------------+
| MIN(_date) | CURDATE() - 90 | CURDATE() - INTERVAL 90 DAY |
+------------+----------------+-----------------------------+
| 2020-06-12 | 20200612 | 2020-04-03 |
+------------+----------------+-----------------------------+
1 row in set (0.00 sec)

こっちはワーニングすら出ないので、仕様を知らないと混乱するかも。

90日以外のパターンは SHOW WARNINGSでも拾える。
8.0なら performance_schema.events_errors_*でそのワーニングが発生しているかどうかも多少はあたりがつけられる(文字列を数値型にキャストしようとした時にも出るワーニングだから100%とは言えないけれども…

mysql80 15> SHOW WARNINGS;
+---------+------+--------------------------------------------------------------+
| Level | Code | Message |
+---------+------+--------------------------------------------------------------+
| Warning | 1292 | Incorrect date value: '20200699' for column '_date' at row 1 |
+---------+------+--------------------------------------------------------------+
1 row in set (0.00 sec)

mysql80 15> SELECT * FROM performance_schema.events_errors_summary_global_by_error WHERE error_number= 1292;
+--------------+--------------------------+-----------+------------------+-------------------+---------------------+---------------------+
| ERROR_NUMBER | ERROR_NAME | SQL_STATE | SUM_ERROR_RAISED | SUM_ERROR_HANDLED | FIRST_SEEN | LAST_SEEN |
+--------------+--------------------------+-----------+------------------+-------------------+---------------------+---------------------+
| 1292 | ER_TRUNCATED_WRONG_VALUE | 22007 | 1110 | 0 | 2020-07-02 11:56:07 | 2020-07-02 14:15:55 |
+--------------+--------------------------+-----------+------------------+-------------------+---------------------+---------------------+
1 row in set (0.00 sec)

ワーニングは拾った方が後々楽だと思うけれど、自作しないとなかなか挟むのムズカシイんだよなぁ。。

DEBUG_SYNCことはじめ

$
0
0

TL;DR


DEBUG_SYNCとは、デバッグビルドのmysqldの中だけでブレークポイントみたいなものを設定して、ブレークポイントに差し掛かったら他のコネクションを使って操作を試してみられる…と思ってもらえれば多分大丈夫。

ただし任意の箇所でブレークできるわけではなく、コードの中に埋め込まれた “synchronization points” の場所にだけ仕掛けられる。以下、わかりやすさ優先で ブレークポイントと呼ぶことにする。

基本的な使い方はこんな感じ。

  1. mysqld --debug-sync-timeout=nで起動する (デフォルトは0でDEBUG_SYNC無効)
  2. ブレークポイントを通らせたいスレッドで SET DEBUG_SYNC = 'synchronization_pointsの名前 SIGNAL 送信シグナル名 WAIT_FOR テキトーなシグナル名'
  3. 2.のスレッドがブレークポイントを通った時に操作をしたいスレッドで SET DEBUG_SYNC = 'now WAIT_FOR 送信シグナル名
  4. 3.のスレッドはその時点で debug_sync_timeout秒間の待機に入るので、その間に 2.のスレッドでブレークポイントを通るような操作をする。
  5. 2.のスレッドがブレークポイントを通ると SIGNAL句で設定した 送信シグナル名がブロードキャストされて、 3.のスレッドがそれを受け取って復帰する。 2.のスレッドはそのまま WAIT_FOR句で テキトーなシグナル名がブロードキャストされるのを待つ
  6. 3.のスレッドなりシェルなりで好きな操作をする
  7. 3.のスレッドで SET DEBUG_SYNC = 'now SIGNAL テキトーなシグナル名'テキトーなシグナル名がブロードキャストされて 2.のスレッドが復帰して残りの処理をする

3.のスレッドは本質ではない(別にこれがなくてもブレークポイントを通ったあと待ちに入る)けれど、これがないと「ブレークポイントを通るまでの処理をしているのか、通ったあとに来ないシグナルを待っているのか」の区別がつかないのでこうするのが吉だと思う。


デバッグビルドしたmysqldを --debug-sync-timeout=n (n > 0)で起動。

$ ./bin/mysqld --max-allowed-packet=1G --gtid_mode=ON --enforce_gtid_consistency=ON --debug-sync-timeout=600 --daemonize

autocommit環境下でストレージエンジンへのINSERT後COMMITになる前の状態、とかを観測してみたりできる。

conn1> SET DEBUG_SYNC = 'ib_after_row_insert SIGNAL here WAIT_FOR yoku0825';
Query OK, 0 rows affected (0.01 sec)

conn2> SET DEBUG_SYNC = 'now WAIT_FOR here'; -- nowは特殊なキーワードで常にブレークする。nowにヒットしたのでhereシグナル待ちに入る

conn1> INSERT INTO d1.t1 SET lo = REPEAT('a', 128 * 1024 * 1024); -- ib_after_row_insertに到達したあと hereシグナルを送ってyoku0825シグナルを待つ

conn2> SET DEBUG_SYNC = 'now WAIT_FOR here'; -- hereシグナルを受け取って叩き起こされる
Query OK, 0 rows affected (7.64 sec)

conn2> SHOW PROCESSLIST;
+----+-----------------+-----------+------+---------+------+---------------------------------------+-----------------------------------------------------------+
| Id | User | Host | db | Command | Time | State | Info |
+----+-----------------+-----------+------+---------+------+---------------------------------------+-----------------------------------------------------------+
| 1 | event_scheduler | localhost | NULL | Daemon | 87 | Waiting on empty queue | NULL |
| 3 | root | localhost | NULL | Query | 0 | starting | SHOW PROCESSLIST |
| 4 | root | localhost | NULL | Query | 20 | debug sync point: ib_after_row_insert | INSERT INTO d1.t1 SET lo = REPEAT('a', 128 * 1024 * 1024) |
+----+-----------------+-----------+------+---------+------+---------------------------------------+-----------------------------------------------------------+
3 rows in set (0.03 sec)

conn2> SELECT COUNT(*) FROM d1.t1; -- ib_after_row_insertはCOMMITの手前なのでautocommitでも行は見えない
+----------+
| COUNT(*) |
+----------+
| 0 |
+----------+
1 row in set (0.00 sec)

conn2> quit -- 1回抜けてシェルに
Bye

### バイナリログにも書かれていない
$ mysqlbinlog /usr/mysql/5.7.23/data/bin.000011 | tail
mysqlbinlog: [Warning] unknown variable 'loose-default-character-set=utf8mb4'.
# at 134218325
#200721 15:34:00 server id 1 end_log_pos 134218402 CRC32 0x8f60827e Query thread_id=4 exec_time=1 error_code=0
SET TIMESTAMP=1595313240/*!*/;
TRUNCATE d1.t1
/*!*/;
SET @@SESSION.GTID_NEXT= 'AUTOMATIC' /* added by mysqlbinlog */ /*!*/;
DELIMITER ;
# End of log file
/*!50003 SET COMPLETION_TYPE=@OLD_COMPLETION_TYPE*/;
/*!50530 SET @@SESSION.PSEUDO_SLAVE_MODE=0*/;

$ mysql -S /tmp/mysql.sock -uroot
mysql> SET DEBUG_SYNC = 'now SIGNAL yoku0825'; -- yoku0825シグナルを送ってやる
Query OK, 0 rows affected (0.00 sec)

conn1> INSERT INTO d1.t1 SET lo = REPEAT('a', 128 * 1024 * 1024); -- yoku0825シグナルに叩き起こされてINSERTが完了する
Query OK, 1 row affected (2 min 28.38 sec)

楽しい、いろいろ捗りそう(要出典: 何が)

MySQL 8.0.17とそれ以前では、CREATE USER .. DEFAULT ROLE ..構文を使うと、ロールの情報が正しくレプリケーションされない

$
0
0

TL;DR

  • mysql.role_edgesテーブルと mysql.default_rolesテーブルがマスターとスレーブでズレる
    • マスターでは登録されるけどスレーブでは登録されない
    • つまりスレーブでは「そのロールを使う権限もそのロールがデフォルトロールである情報も失われる」
  • See MySQL Bugs: #93252: Default role is not logged into the binary log
    • Fixed in 8.0.18

8.0.15を使ってレプリケーションを組んでいたらハマった。
バイナリログへの記録がそもそもおかしいことになる。

mysql> SELECT @@version;
+-----------+
| @@version |
+-----------+
| 8.0.17 |
+-----------+
1 row in set (0.00 sec)

mysql> CREATE ROLE myrole;
Query OK, 0 rows affected (0.01 sec)

mysql> CREATE USER yoku0825 DEFAULT ROLE myrole;
Query OK, 0 rows affected (0.00 sec)

mysql> SHOW BINLOG EVENTS IN 'binlog.000002';
+---------------+-----+----------------+-----------+-------------+--------------------------------------------------------------------------------+
| Log_name | Pos | Event_type | Server_id | End_log_pos | Info |
+---------------+-----+----------------+-----------+-------------+--------------------------------------------------------------------------------+
| binlog.000002 | 4 | Format_desc | 1 | 124 | Server ver: 8.0.17, Binlog ver: 4 |
| binlog.000002 | 124 | Previous_gtids | 1 | 155 | |
| binlog.000002 | 155 | Anonymous_Gtid | 1 | 232 | SET @@SESSION.GTID_NEXT= 'ANONYMOUS' |
| binlog.000002 | 232 | Query | 1 | 333 | CREATE ROLE myrole /* xid=7 */ |
| binlog.000002 | 333 | Anonymous_Gtid | 1 | 410 | SET @@SESSION.GTID_NEXT= 'ANONYMOUS' |
| binlog.000002 | 410 | Query | 1 | 559 | CREATE USER 'yoku0825'@'%' IDENTIFIED WITH 'caching_sha2_password' /* xid=8 */ |
+---------------+-----+----------------+-----------+-------------+--------------------------------------------------------------------------------+
6 rows in set (0.00 sec)

そもそも、 binlog_formatによらずSQLがそのまま転記されるタイプの CREATE USERステートメントだが「俺が打ったものと若干違う(そしてそこには DEFAULT ROLE句はない)」ことがお分かりいただけるだろうか。

この CREATE USERステートメントの書き換え自体は ずっと昔からやっていてIDENTIFIED BYで平文パスワードを指定しても「平文はバイナリログに載せず、ハッシュ後の値でバイナリログに載せる」ために構文を書き換えたりしていた。あとは認証プラグインの情報とかも追記している(これはいつからだったか…)

で、その書き換え部分にバグがあったっぽい。
修正コミットはこれ。

Bug#28948915 DEFAULT ROLE IS NOT LOGGED INTO THE BINARY LOG · mysql/mysql-server@d278a87

CREATE USER .. DEFAULT ROLE ..は「ユーザー作成」「ロールの許可」「デフォルトロールの指定」がいっぺんに指定できて便利なんだけど、MySQL 8.0.17とそれ以前を使っている場合は注意。

日々の覚書: CREATE USER .. DEFAULT ROLE .. で指定すると一発でROLEも許可される

( ´-`).oO(↑の時点では気が付いてなくて、最近スレーブをスイッチオーバーさせようとして初めて気が付いた…つらい…

pt-table-checksum で mysql.role_edges とmysql.default_roles だけがズレてたらこれかも知れないので気を付けて!

MyISAMで第2カラムのAUTO_INCREMENTを使ってるテーブルを洗い出すSQL

$
0
0

TL;DR

  • SELECT table_schema, table_name, column_name, seq_in_index FROM information_schema.statistics WHERE (table_schema, table_name, column_name) IN (SELECT table_schema, table_name, column_name FROM information_schema.columns WHERE extra LIKE '%auto_increment%') AND (table_schema, table_name, column_name) NOT IN (SELECT DISTINCT table_schema, table_name, column_name FROM information_schema.statistics WHERE seq_in_index = 1);
  • 無理にSQLでどうにかしなくても、mysqldump --no-dataで引っこ抜いて sed 's/MyISAM/InnoDB/'してテキトーなところにリストアしてみればいいと思うよ

MyISAMストレージエンジンとInnoDBストレージエンジンの非互換(というか、他のストレージエンジンの中でもMyISAMだけに特有な機能)な点に「MyISAMは複合キーの2つ目以降のカラムにAUTO_INCREMENT属性を指定できる」というのがある。

サンプルは↓の4つ。

mysql56> SHOW CREATE TABLE t1\G -- 第1カラムのAUTO_INCREMENT。InnoDBでも有効。
*************************** 1. row ***************************
Table: t1
Create Table: CREATE TABLE `t1` (
`one` int(11) NOT NULL AUTO_INCREMENT,
`two` int(11) DEFAULT NULL,
PRIMARY KEY (`one`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4
1 row in set (0.00 sec)

mysql56> SHOW CREATE TABLE t2\G -- 第2カラムのAUTO_INCREMENT。InnoDBでは使えない
*************************** 1. row ***************************
Table: t2
Create Table: CREATE TABLE `t2` (
`one` int(11) NOT NULL AUTO_INCREMENT,
`two` int(11) NOT NULL DEFAULT '0',
PRIMARY KEY (`two`,`one`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4
1 row in set (0.00 sec)

mysql56> SHOW CREATE TABLE t3\G -- 第3カラムのAUTO_INCREMENT。これも使えない。
*************************** 1. row ***************************
Table: t3
Create Table: CREATE TABLE `t3` (
`one` int(11) NOT NULL AUTO_INCREMENT,
`two` int(11) NOT NULL DEFAULT '0',
`three` int(11) NOT NULL DEFAULT '0',
PRIMARY KEY (`two`,`three`,`one`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4
1 row in set (0.00 sec)

mysql56> SHOW CREATE TABLE t4\G -- 第3カラムのAUTO_INCREMENTだけど第1カラムのAUTO_INCREMENTでもあるのでInnoDBでも有効
*************************** 1. row ***************************
Table: t4
Create Table: CREATE TABLE `t4` (
`one` int(11) NOT NULL AUTO_INCREMENT,
`two` int(11) NOT NULL DEFAULT '0',
`three` int(11) NOT NULL DEFAULT '0',
PRIMARY KEY (`two`,`three`,`one`),
KEY `one` (`one`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4
1 row in set (0.00 sec)

このうちt2とt3だけが引っ掛けられれば正解。
まずはAUTO_INCREMENT属性を持ったカラムを引っこ抜くか。

mysql56> SELECT table_schema, table_name, column_name, extra FROM information_schema.columns WHERE extra LIKE '%auto_increment%';
+--------------+------------+--------------+----------------+
| table_schema | table_name | column_name | extra |
+--------------+------------+--------------+----------------+
| d1 | t1 | one | auto_increment |
| d1 | t2 | one | auto_increment |
| d1 | t3 | one | auto_increment |
| d1 | t4 | one | auto_increment |
| mysql | time_zone | Time_zone_id | auto_increment |
+--------------+------------+--------------+----------------+
5 rows in set (0.02 sec)

ここから「第1カラムにインデックスを持っているものを除外」すればいいはず。
「インデックスの第1カラムに指定されているカラム」は seq_in_index = 1で表現できる。

mysql56> SELECT table_schema, table_name, column_name, index_name, seq_in_index FROM information_schema.statistics WHERE table_schema = 'd1';
+--------------+------------+-------------+------------+--------------+
| table_schema | table_name | column_name | index_name | seq_in_index |
+--------------+------------+-------------+------------+--------------+
| d1 | t1 | one | PRIMARY | 1 |
| d1 | t2 | two | PRIMARY | 1 |
| d1 | t2 | one | PRIMARY | 2 |
| d1 | t3 | two | PRIMARY | 1 |
| d1 | t3 | three | PRIMARY | 2 |
| d1 | t3 | one | PRIMARY | 3 |
| d1 | t4 | two | PRIMARY | 1 |
| d1 | t4 | three | PRIMARY | 2 |
| d1 | t4 | one | PRIMARY | 3 |
| d1 | t4 | one | one | 1 |
+--------------+------------+-------------+------------+--------------+
10 rows in set (0.00 sec)

mysql56> SELECT DISTINCT table_schema, table_name, column_name FROM information_schema.statistics WHERE table_schema = 'd1' AND seq_in_index = 1;
+--------------+------------+-------------+
| table_schema | table_name | column_name |
+--------------+------------+-------------+
| d1 | t1 | one |
| d1 | t2 | two |
| d1 | t3 | two |
| d1 | t4 | two |
| d1 | t4 | one |
+--------------+------------+-------------+
5 rows in set (0.00 sec)

という訳でサブクエリーを使ってこんな感じになるかしらん。

SELECT 
table_schema,
table_name,
column_name
FROM
information_schema.statistics
WHERE
(table_schema, table_name, column_name) IN
(SELECT table_schema, table_name, column_name
FROM information_schema.columns
WHERE extra LIKE '%auto_increment%') /* ←AUTO_INCREMENTなカラムの一覧サブクエリ */ AND
(table_schema, table_name, column_name) NOT IN
(SELECT DISTINCT table_schema, table_name, column_name
FROM information_schema.statistics
WHERE seq_in_index = 1) /* ←第1カラムなインデックス一覧サブクエリ */;
+--------------+------------+-------------+
| table_schema | table_name | column_name |
+--------------+------------+-------------+
| d1 | t2 | one |
| d1 | t3 | one |
+--------------+------------+-------------+
2 rows in set (0.03 sec)

まあこんなことしなくても、 mysqldump --no-dataでテーブル定義だけ引っこ抜いて、MyISAMをInnoDBに書き換えてやればエラーになるんでわかるんですけどね。

$ mysqldump56 --set-gtid-purged=OFF --no-data d1  > tables.sql
$ sed -i 's/MyISAM/InnoDB/' tables.sql
$ mysql56 -v -f d1 < tables.sql
..
--------------
CREATE TABLE `t1` (
`one` int(11) NOT NULL AUTO_INCREMENT,
`two` int(11) DEFAULT NULL,
PRIMARY KEY (`one`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
--------------
..
--------------
CREATE TABLE `t2` (
`one` int(11) NOT NULL AUTO_INCREMENT,
`two` int(11) NOT NULL DEFAULT '0',
PRIMARY KEY (`two`,`one`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
--------------

ERROR 1075 (42000) at line 39: Incorrect table definition; there can be only one auto column and it must be defined as a key
..
--------------
CREATE TABLE `t3` (
`one` int(11) NOT NULL AUTO_INCREMENT,
`two` int(11) NOT NULL DEFAULT '0',
`three` int(11) NOT NULL DEFAULT '0',
PRIMARY KEY (`two`,`three`,`one`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
--------------

ERROR 1075 (42000) at line 53: Incorrect table definition; there can be only one auto column and it must be defined as a key
..
--------------
CREATE TABLE `t4` (
`one` int(11) NOT NULL AUTO_INCREMENT,
`two` int(11) NOT NULL DEFAULT '0',
`three` int(11) NOT NULL DEFAULT '0',
PRIMARY KEY (`two`,`three`,`one`),
KEY `one` (`one`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4
--------------
..

MySQL 8.0 vs 外部キー制約 vs ALTER TABLEでメタデータロック待ちになったら疑うこと

$
0
0

TL;DR


論より証拠。
サンプルスキーマはこんなかんじ。

CREATE TABLE `item` (
`item_id` int NOT NULL,
`registered` datetime NOT NULL,
PRIMARY KEY (`item_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

CREATE TABLE `user` (
`user_id` int NOT NULL,
`registered` datetime NOT NULL,
PRIMARY KEY (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

CREATE TABLE `item_box` (
`user_id` int NOT NULL,
`item_id` int NOT NULL,
`item_amount` int NOT NULL,
`last_updated` datetime NOT NULL,
PRIMARY KEY (`user_id`,`item_id`),
KEY `idx_itemid` (`item_id`),
CONSTRAINT `item_box_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `user` (`user_id`),
CONSTRAINT `item_box_ibfk_2` FOREIGN KEY (`item_id`) REFERENCES `item` (`item_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

CREATE TABLE `item_log` (
`item_log_seq` bigint unsigned NOT NULL,
`user_id` int NOT NULL,
`item_id` int NOT NULL,
`changed_amount` int NOT NULL,
`recorded` datetime NOT NULL,
PRIMARY KEY (`item_log_seq`),
KEY `idx_userid` (`user_id`),
KEY `idx_itemid` (`item_id`),
CONSTRAINT `item_log_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `item_box` (`user_id`),
CONSTRAINT `item_log_ibfk_2` FOREIGN KEY (`item_id`) REFERENCES `item_box` (`item_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

item, userテーブルがあって、 item_boxがそれぞれに外部キー制約を持ち、 item_boxの履歴用に item_logテーブルがある、みたいなテーブル。
2段外部キー制約を張って試したかったので、 item_logの親は item_boxになっている(本筋とは関係ないけど、こういうアレにするということは item_boxには残数がゼロになったアイテムもDELETEされずに行としては残す感じになるね)

この時、

  • item, userテーブルに対するDDLは自分自身に対するMDLだけを欲しがる。これは従前からの動作といっしょ
mysql80 31> BEGIN;
Query OK, 0 rows affected (0.00 sec)

mysql80 31> SELECT * FROM item; -- 行ロックは取らないけど共有MDLは取る
Empty set (0.00 sec)

mysql80 34> SELECT * FROM performance_schema.metadata_locks\G
..
*************************** 2. row ***************************
OBJECT_TYPE: TABLE
OBJECT_SCHEMA: d1
OBJECT_NAME: item
COLUMN_NAME: NULL
OBJECT_INSTANCE_BEGIN: 140136396228576
LOCK_TYPE: SHARED_READ
LOCK_DURATION: TRANSACTION
LOCK_STATUS: GRANTED
SOURCE: sql_parse.cc:6161
OWNER_THREAD_ID: 66
OWNER_EVENT_ID: 5
2 rows in set (0.00 sec)

mysql80 34> SET SESSION lock_wait_timeout = 1;
Query OK, 0 rows affected (0.00 sec)

mysql80 34> ALTER TABLE item Engine = InnoDB; -- MDL待ちがタイムアウト
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
  • item_boxは自分自身とその親である item, userに対してMDLを置こうとする。
mysql80 31> BEGIN;
Query OK, 0 rows affected (0.00 sec)

mysql80 31> SELECT * FROM item_box;
Empty set (0.00 sec)

mysql80 34> ALTER TABLE item_box Engine = InnoDB;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

mysql80 34> ALTER TABLE item Engine = InnoDB;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

mysql80 34> ALTER TABLE user Engine = InnoDB;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

mysql80 34> ALTER TABLE item_log Engine = InnoDB; -- 子であるitem_logには関係ない
Query OK, 0 rows affected (0.03 sec)
Records: 0 Duplicates: 0 Warnings: 0
  • item_logは自信と親である item_boxには共有MDLを置くが、祖父母の代(?)である item, userには置かない
mysql80 31> BEGIN;
Query OK, 0 rows affected (0.00 sec)

mysql80 31> SELECT * FROM item_log;
Empty set (0.00 sec)

mysql80 34> ALTER TABLE item_log Engine = InnoDB; -- 自分自身
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

mysql80 34> ALTER TABLE item_box Engine = InnoDB; -- 親テーブル
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

mysql80 34> ALTER TABLE item Engine = InnoDB; -- 祖父母世代(?)
Query OK, 0 rows affected (0.03 sec)
Records: 0 Duplicates: 0 Warnings: 0

mysql80 34> ALTER TABLE user Engine = InnoDB; -- 祖父母世代(?)
Query OK, 0 rows affected (0.03 sec)
Records: 0 Duplicates: 0 Warnings: 0

実際食らったのは単なる親子関係のテーブルだけど、「あんまり更新されないsbtest1にインデックス追加したい」(都道府県マスタ的な役割だと思いねぇ)「子テーブルのsbtest2はバンバン参照される」みたいな時にハマった。

mysql80 42> ALTER TABLE sbtest.sbtest1 Engine = InnoDB; -- MDL待ちでブロックされなければこれくらいかかるALTER TABLEがあるじゃろ?
Query OK, 0 rows affected (8.96 sec)
Records: 0 Duplicates: 0 Warnings: 0

mysql80 42> ALTER TABLE sbtest.sbtest1 Engine = InnoDB; -- ALTER TABLEを開始してから

mysql80 31> BEGIN; SELECT * FROM sbtest.sbtest2 LIMIT 1; -- 他のコネクションで共有MDLを置くじゃろ?
Query OK, 0 rows affected (0.00 sec)

+----+--------+-------------------------------------------------------------------------------------------------------------------------+-------------------------------------------------------------+
| id | k | c | pad |
+----+--------+-------------------------------------------------------------------------------------------------------------------------+-------------------------------------------------------------+
| 1 | 499284 | 83868641912-28773972837-60736120486-75162659906-27563526494-20381887404-41576422241-93426793964-56405065102-33518432330 | 67847967377-48000963322-62604785301-91415491898-96926520291 |
+----+--------+-------------------------------------------------------------------------------------------------------------------------+-------------------------------------------------------------+
1 row in set (0.00 sec)

mysql80 31> SHOW PROCESSLIST; -- State: altering table でフツーに進むんだけど
+----+-----------------+-----------------+--------+---------+------+------------------------+--------------------------------------------+
| Id | User | Host | db | Command | Time | State | Info |
+----+-----------------+-----------------+--------+---------+------+------------------------+--------------------------------------------+
| 42 | root | localhost | NULL | Query | 7 | altering table | ALTER TABLE sbtest.sbtest1 Engine = InnoDB |
+----+-----------------+-----------------+--------+---------+------+------------------------+--------------------------------------------+

mysql80 31> SHOW PROCESSLIST; -- しばらくするとMDL待ちで詰まる
+----+-----------------+-----------------+--------+---------+------+---------------------------------+--------------------------------------------+
| Id | User | Host | db | Command | Time | State | Info |
+----+-----------------+-----------------+--------+---------+------+---------------------------------+--------------------------------------------+
| 42 | root | localhost | NULL | Query | 13 | Waiting for table metadata lock | ALTER TABLE sbtest.sbtest1 Engine = InnoDB |
+----+-----------------+-----------------+--------+---------+------+---------------------------------+--------------------------------------------+

mysql80 31> COMMIT; -- COMMITして共有MDLをリリースしたタイミングで
Query OK, 0 rows affected (0.00 sec)

mysql80 42> ALTER TABLE sbtest.sbtest1 Engine = InnoDB; -- MDL取れたのでALTER TABLEが終われる
Query OK, 0 rows affected (2 min 23.94 sec)
Records: 0 Duplicates: 0 Warnings: 0

オンラインALTER TABLEは終了時にMDLを取るのでこんな風になった。

更に、sbtest1へのALTER TABLEが排他MDL待ちになってしまうので、sbtest1への全てのDMLもMDL待ちのキューを追い越せなくて共有MDLが取れなくなって待たされる。
もしもっと早くこれを知っていたら、「sbtest2を掴んでるトランザクションをKILLすればいい」と、ALTER TABLEを完走させることができたかもしれない。

俺基本的に「ヘビーなMySQLでもちゃんと正規化してれば外部キー制約は張っても大丈夫」って主張してたけど、これを食らってから危なっかしいかなと思うようになりました。
ちなみに回避策はレプリカにALTER TABLEしてからマスター切り替え。これで取り敢えず問題は出ていない。

Viewing all 581 articles
Browse latest View live