GA

2020/12/26

pt-table-checksumでよく使うオプション

メリークリスマイエスキューエル! (と、1日)

この記事は GMOペパボエンジニア Advent Calendar 2020 の26日目の記事のつもりです。
ちなみに私の中の人は GMOペパボではない会社 に勤めています。


最近 pt-table-checksum にお世話になる機会が多くなって、使い方をまとめておこうと思ったメモです。

公式ドキュメントはこちら。

pt-table-checksum 自体は簡単に説明した昔の記事が出てきた。


シンプルな例えにすると、 pt-table-checksum はこんな感じに binlog_format=STATEMENT にしてクエリーを実行します。

mysql57 19> SET SESSION binlog_format = STATEMENT;
Query OK, 0 rows affected (0.00 sec)

mysql57 19> BEGIN;
Query OK, 0 rows affected (0.00 sec)

mysql57 19> SELECT COUNT(*) FROM t1;
+----------+
| COUNT(*) |
+----------+
|        3 |
+----------+
1 row in set (0.00 sec)

mysql57 19> INSERT INTO checksum (table_name, master_cnt, this_cnd) SELECT 't1', 3, COUNT(*) FROM t1;

binlog_format=STATEMENT を押し込むことで INSERT INTO .. SELECT .. は「 master_cnt はリテラルでマスターの値がそのまま入り、 this_cnt はレプリカで SELECT が再実行されるためレプリカでの値が入る」ことになります。


master_crc, this_crc も同じように「マスターで計算済みのリテラルを入れるカラムとレプリカでリプレイされて計算された値を入れるカラム」に分けられて記録されます。
チェック対象のテーブルはチャンクに分けられて実行されます。1チャンクが1トランザクションで処理されるため、各チャンクをまたいだデータの一貫性はありません(2チャンク目を計算している間に1チャンク目の行の数が変わっているかもしれない、ということ)


バイナリログ直列化の恩恵を受けられるため、マスターの1チャンク目とレプリカの1チャンク目は「同じスナップショット」であることが期待できます。先の例だと、マスターで1チャンク目を更新したあとに1チャンク目に新しく行が追加されても、レプリカはその新しい行が追加される前のスナップショットでチャンクを再計算するため、よほどレアなケースでない限り(もともと binlog_format = STATEMENT && 非決定性関数が頻繁に使われている環境でない限り)計算済みのチャンクで新たな不整合が発生する可能性は低いはずです。
チャンク分けしているのはたぶん、 INSERT .. SELECT ..SELECT 対象のテーブルの行に共有ロックを置くためロックの範囲が大きくなりすぎないようにとか、そんなにでっかくするとチェックサムを計算するのにレプリケーションが遅れるからとかそういうのの配慮だと思います。

で、俺のよく使うオプションはこんな感じでした。

$ pt-table-checksum \
>   --host xxxx --port 3306 \
>   --user pt_tcs --password 'xxxx' \
>   --ignore-databases mysql,sys \
>   --replicate pt.pt_tcs \
>   --chunk-size=1000 \
>   --recursion-method=processlist \
>   --no-check-binlog-format \
>   --pause-file=/tmp/pt_tcs.pause \
>   --truncate-replicate-table

--host, --port はマスターのもの。
--user, --password もマスターのものですが、レプリカ監視用のアカウントを瞬間的に払い出すと楽です。

マスターとレプリカの間でバージョンが違う場合、 --ignore-databases=mysql,sys は必須です( information_schema, performance_schemapt-table-checksum が勝手に除けます )
pt-table-checksum のクエリーは個々のテーブルのカラムを全て参照するため、 マスターとレプリカでカラムの数が違ったりマスターにあってレプリカにないテーブルがあると転けます
転けるのはpt-table-checksumのみではなく、レプリケーションそのものが止まります

--replicate は「チェックサムの結果を記録するテーブル」を指定します。これを指定しないと、マスターで SELECT だけ投げて終わるのでレプリカとの比較ができません(更新止まってればできるだろうけど)

--chunk-size はチャンク分けする時の基本サイズです。特に何も出なければそのまま1000でいいんですが、「チャンク分けが上手くいかずにこのテーブルはスキップするよ!」みたいなワーニングが出ることがあるので、その時はこの --chunk-size を大きくしてから --tables でそのテーブルを指定して流しなおします。

--recursion-method は3306以外のポートを使っている時は hosts が便利ですが、3306だけなら processlist で問題ないです。

--no-check-binlog-format は「レプリカの binlog_format <> STATEMENT 」な時にスクリプトを実行する時に必要なオプションです。
↓を読むと「マスターとレプリカの binlog_format が違ったらスクリプトを実行させない」のように見えますが、マスターには既に SET SESSION binlog_format = STATEMENT が実行されているので、レプリカの binlog_format = STATEMENT でない限り必ず引っ掛かります。

--replicate で指定したテーブルにFKやTriggerを仕掛けるようなことをしていない限りは(そんなもの好きな人もいないと思う)特に問題ない気がしますが、まあエラーメッセージの通り If you understand the risks, specify --no-check-binlog-format to disable this check. という感じでお願いします。

--pause-file を指定しておくと、そのパスに「ファイルが存在している間はスクリプトがsleepする」ようになります。 pt-online-schema-change と違って実行中ずっと貼りついて見守っていないと不安になるようなスクリプトではないですが、明示的に止めたい時に便利です。 --continue でもいいんですけどね。

--truncate-replicate-table を指定すると、 --replicate で指定したテーブルの中身を1回 TRUNCATE します。これをやらないと過去の行が残るので、前に pt-table-checksum を同じコマンドで流した場合に前の結果の残骸が残ることがあります(書き込み自体は REPLACE INTO なので、過去にも今にも存在するチャンクの情報は上書きされるけれど、過去にあって今ないチャンクの情報が消えない)

--truncate-replicate-table を指定 しない 場合、前の情報が残っているので --continue が使えます。

これはCtrl+Cとかで pt-table-checksum を止めた場合、「その手前までは --replicate で指定したテーブルにチャンクのチェックサムが残っているはず」ということで、テーブルに載っていないチャンクから処理を再開させることができるオプションです。

なお俺は何度か --truncate-replicate-table--continue を同時に指定して「あれーおかしいなーまた先頭からチェックサム取ってるなー」とかやってたことがあります。我ながらアホだ。

それでは、良いお年を!

2020/12/16

Re: foreign_key_checksの挙動を知る(あるいは、OPTION_NO_FOREIGN_KEY_CHECKSについて)

この記事は MySQL Advent Calendar 2020 の16日目の記事です。

MySQL Advent Calendar 2020 8日目、 lhfukamachi さんの foreign_key_checks に関する記事を見て思い付いたものです。

システム変数のforeign_key_checks の話は上記の記事によくまとまっています。

この記事では、「じゃあ foreign_key_checks はどういう挙動で外部キー制約をチェックしないような実装になっているのか」を説明します。興味がない方はここでタブを閉じてもらって大丈夫なんですがおいちょっと待てふかまち、君は閉じるな。

さて foreign_key_checks がシステム変数(my.cnfや SET GLOBAL で変更できるやつ)である以上、 sql/sys_vars.cc のどこかにある可能性が高いでしょう。
Ctrl + Fならブラウザでも検索できる時代です。簡単に見つかりました。

5121 static Sys_var_bit Sys_foreign_key_checks(
5122     "foreign_key_checks", "foreign_key_checks",
5123     HINT_UPDATEABLE SESSION_VAR(option_bits), NO_CMD_LINE,
5124     REVERSE(OPTION_NO_FOREIGN_KEY_CHECKS), DEFAULT(true), NO_MUTEX_GUARD,
5125     IN_BINLOG);

Sys_var_* 系の構造体は引数が多いので最初のうちは何が何やらですが、慣れれば脳死で読めるようになるので大丈夫です。

大事なのは、「このシステム変数の本体が option_bits であり、OPTION_NO_FOREIGN_KEY_CHECKS の反転で表されるらしいこと」です。ついでですがこのオプションはバイナリログに記録されるので、外部キー制約があるテーブルに対して TRUNCATE とかする時でも、「マスターで foreign_key_checks=0 していればレプリカでリプレイされる時にも foreign_key_checks=0 として振る舞」います。安心ですね。

OPTION_NO_FOREIGN_KEY_CHECKS は2^26なので67108864らしいですがまあそれはどうでもいいですが、 sql/query_options.h にはその他にもいろいろなオプションが羅列されていて面白いです。 SELECT_HIGH_PRIORITY とか OPTION_FOUND_ROWS (!!) とか、 SELECT 文の修飾句は割とここらへんにいることが多いです。楽しいですね。

というわけで、 foreign_key_checks=0option_bitsに OPTION_NO_FOREIGN_KEY_CHECKS が立っている状態 、というのはわかりました。
となればあとは option_bits & OPTION_NO_FOREIGN_KEY_CHECKS とかやっていそうなところを探せば実装に行きつけそうですね。調べてみると実際結構出てきます。

待つんだそこで満足げにブラウザを閉じようとしているふかまち。まだ世の中には面白いことがある。
思い出していただきたい、「外部キー制約はストレージエンジンの機能であって、MySQLサーバーのコア機能ではない」ことを。 option_bits は基本的にサーバーコアが触るためのスレッド単位の変数なので、InnoDBはおそらくここをほとんど見ない(この記事書きながら調べたけど、NDBもそうで一度他のflagにここの値を写し取っている)
もう一度さっきの検索結果を見てみるのだ。驚くほど storage/innobase のコードはないだろう? ほとんどがサーバーコアのコードだ。

GitHubの検索結果に出てこない理由は俺は知らないけれど、InnoDBとしての「外部キー制約をチェックしない」の起点はここだ。 innobase_trx_init という如何にもトランザクションの開始時に呼ばれそうなところで、 trx->check_foreigns というプロパティに詰めている。

………あれ? ってことはトランザクションの途中で SET SESSION foreign_key_checks = 0 とかやるとどうなるの?

mysql80 23> BEGIN;
Query OK, 0 rows affected (0.00 sec)

mysql80 23> DELETE FROM parent;
ERROR 1451 (23000): Cannot delete or update a parent row: a foreign key constraint fails (`d2`.`child`, CONSTRAINT `child_ibfk_1` FOREIGN KEY (`id`) REFERENCES `parent` (`id`))

mysql80 23> SET SESSION foreign_key_checks = 0;
Query OK, 0 rows affected (0.00 sec)

mysql80 23> DELETE FROM parent;
Query OK, 2 rows affected (0.00 sec)

mysql80 23> ROLLBACK;
Query OK, 0 rows affected (0.00 sec)

mysql80 23> SELECT * FROM parent;
+----+
| id |
+----+
|  1 |
|  2 |
+----+
2 rows in set (0.00 sec)

ちゃんと途中で変えても追随して動いた。これはどうも、Handlerの層からストレージエンジンの層に降りて来る時にInnoDB側でまだトランザクションを保持しているか確かめる(InnoDB側で保持していなかったら、サーバーコアの層に「ロールバックを知らせるエラー」を返すために) check_trx_existsをしょっちゅう呼んでいて、その中でも innobase_trx_init を呼んでいるからだ。

俺が最初に innobase_trx_init の響きから期待したようなものはinnobase_trx_allocate で、これも内部で innobase_trx_init を呼んでいる。こっちがトランザクションの起点かな。

gdb でアタッチしてとなりのコンソールから同じステートメントを流してみると、まあ大体合ってそう。

(gdb) b innobase_trx_allocate
+b innobase_trx_allocate
Breakpoint 1 at 0x21c8405: file /home/yoku0825/mysql-8.0.22/storage/innobase/handler/ha_innodb.cc, line 2509.
(gdb) b check_trx_exists
+b check_trx_exists
Breakpoint 2 at 0x21d2c2b: check_trx_exists. (2 locations)
(gdb) c
+c
Continuing.
Breakpoint 2, check_trx_exists (thd=0x68bbd40) at /home/yoku0825/mysql-8.0.22/storage/innobase/handler/ha_innodb.cc:2544
2544      if (trx == nullptr) {
(gdb) c
+c
Continuing.

Breakpoint 1, innobase_trx_allocate (thd=thd@entry=0x68bbd40)
    at /home/yoku0825/mysql-8.0.22/storage/innobase/handler/ha_innodb.cc:2509
2509      trx = trx_allocate_for_mysql();
(gdb) c
+c
Continuing.

Breakpoint 2, check_trx_exists (thd=0x68bbd40) at /home/yoku0825/mysql-8.0.22/storage/innobase/handler/ha_innodb.cc:2544
2544      if (trx == nullptr) {
(gdb) c
+c
Continuing.

Breakpoint 2, check_trx_exists (thd=0x68bbd40) at /home/yoku0825/mysql-8.0.22/storage/innobase/handler/ha_innodb.cc:2544
2544      if (trx == nullptr) {
(gdb) c
+c
Continuing.

Breakpoint 2, check_trx_exists (thd=0x68bbd40) at /home/yoku0825/mysql-8.0.22/storage/innobase/handler/ha_innodb.cc:2544
2544      if (trx == nullptr) {
(gdb) c
+c
Continuing.

Breakpoint 2, check_trx_exists (thd=0x68bbd40) at /home/yoku0825/mysql-8.0.22/storage/innobase/handler/ha_innodb.cc:2544
2544      if (trx == nullptr) {
..

あ、ちなみにここから先は row_ins_check_foreign_constraint, row_ins_check_foreign_constraints(複数形、ややこし…)あたりが trx->check_foreign == FALSE の時は即座にreturnするってだけなのでセルフでお願いします。
それでは良い年末を!

2020/12/06

今年もConoHa VPSでひたすらMySQLをビルドする

この記事は ConoHa Advent Calendar 2020MySQL Advent Calendar 2020 の6日目の記事です。

どうやら5年目のようです。「今年は気分を変えてCentOS 8.xでやってみようかな」と思ったんですが既に去年やっていた。まあいいか。


というわけで今年もConoHa VPSの1GBプランのVMの上にCnetOS 8.2のインスタンスを新しく立ち上げてMySQLをビルドします。 ちなみに、MySQL公式としてはCentOS 8.xがサポートしているのはMySQL 8.0だけです

とはいえ一応ビルドのターゲットは5.6, 5.7, 8.0。MySQL 5.6は2021/02にEOLになるので、(来年も書くとしても)このシリーズに登場するのはこれが最後でしょう。

去年の自分の記事 を見ながら、必要そうなものを一式入れる。

$ sudo dnf install --enablerepo=PowerTools cmake git make gcc gcc-c++ openssl-devel ncurses-devel libtirpc-devel rpcgen

まずは5.6から。

$ wget https://dev.mysql.com/get/Downloads/MySQL-5.6/mysql-5.6.50.tar.gz
$ tar xf mysql-5.6.50.tar.gz
$ cd mysql-5.6.50/
$ cmake -DCMAKE_INSTALL_PREFIX=/usr/mysql/5.6.50
$ time make
..
real    23m6.670s
user    20m35.659s
sys     2m7.294s
$ sudo make install

5.7。

$ wget https://dev.mysql.com/get/Downloads/MySQL-5.7/mysql-boost-5.7.32.tar.gz
$ tar xf mysql-boost-5.7.32.tar.gz
$ cd mysql-5.7.32/
$ cmake -DCMAKE_INSTALL_PREFIX=/usr/mysql/5.7.32 -DWITH_BOOST=./boost
$ time make
..
real    61m54.120s
user    46m26.974s
sys     5m7.050s
$ sudo make install

8.0。最近、「mysql-test-runを走らせる機会があるならインソースビルドの方が楽だな」と思ったので -DFORCE_INSOURCE_BUILD=1 でインソースビルド。

$ wget https://dev.mysql.com/get/Downloads/MySQL-8.0/mysql-boost-8.0.22.tar.gz
$ tar xf mysql-boost-8.0.22.tar.gz
$ cd mysql-8.0.22/
$ cmake -DCMAKE_INSTALL_PREFIX=/usr/mysql/8.0.22 -DWITH_BOOST=./boost -DFORCE_INSOURCE_BUILD=1
$ time make
..
real    153m11.756s
user    113m6.386s
sys     11m57.793s
$ sudo make install

インストール済みのバイナリのサイズはこんなもの(datadirの初期化はしていない)

$ du -sh /usr/mysql/*
1.1G    /usr/mysql/5.6.50
2.3G    /usr/mysql/5.7.32
2.4G    /usr/mysql/8.0.22

そういえば、 CMAKE_BUILD_TYPE を Release (指定しなかった場合は RelWithDebInfo` でシンボルが含まれる) にするとバイナリサイズが小さくなるって教えてもらった気がするので試してみる。

$ cmake -DCMAKE_INSTALL_PREFIX=/usr/mysql/8.0.22 -DWITH_BOOST=./boost -DFORCE_INSOURCE_BUILD=1 -DCMAKE_BUILD_TYPE=Release
$ time make
..
real    91m45.861s
user    80m34.327s
sys     6m15.648s
$ sudo make install
$ du -sh /usr/mysql/8.0.22*
810M    /usr/mysql/8.0.22
2.4G    /usr/mysql/8.0.22_RelWithDebInfo

うおお、ビルドも速いしバイナリ小さい!!!
ご活用ください(?)

2020/10/26

MySQL 8.0.22のperformance_schema.error_logってエラーログのサイズと同じだけメモリを食うかも、5MBが上限かも

TL;DR


実験したメモ。

MySQL 8.0.22から performance_schema.error_log が導入されて、エラーログをSQLで、MySQLプロトコルに載せて確認できるようになった。

これ自体はすごく嬉しい機能なんだけど、 performance_schema ということは当然オンメモリにデータを持つわけで、昔から(吊るしのコンフィグで調整せずに使うと)メモリ食いで足りなくなる要因になりがちだった。

それが、エラーログもRSSに乗っけちゃうって大丈夫なんだろうかと思ったり思わなかったりしたので実験してみた。


実験。

  • 存在するスキーマ(d1)に対して接続して “quit” を投げる(特にエラーログは吐かない)
while true ; do 
  mysql80 d1 -e "quit" 2> /dev/null
done
  • 存在しないスキーマ(slap)に対して接続しようとして “quit” を投げる( log_error_verbosity=3 に設定しているのでNOTEをエラーログに吐く)
while true ; do 
  mysql80 slap -e "quit" 2> /dev/null
done

時間と ps auxwww の結果だけログに取りながら雑にドーン。

左の縦赤線が「エラーログを吐かないぶん回し」を始めた時刻、右の縦赤線が「エラーログを吐くぶん回し」を始めた時刻。

この差はたかだか1MBなのでどこかで頭打ちが来るの鴨わからない。

ちなみに増えたエラーログも約1MBなので「どこかで頭打ちは来るかもしれないけれどエラーログのサイズと同じくらいずつ実メモリーを削りにかかる」で合ってるような気がします。

$ sed -n '15628,$p' /usr/mysql/8.0.22/data/error.log | wc
  10287   82296  936117

p_sのメモリーを一旦解放(というか再利用可能)にする TRUNCATE も使えないようだし、頭打ちがあるのかどうかを調べたい気分(なぜなら、 p_s.error_log はOFFにできないようなので!!!

$ mysql80 performance_schema -e "TRUNCATE error_log"
ERROR 1142 (42000) at line 1: DROP command denied to user 'root'@'localhost' for table 'error_log'

2020/10/22

InnoDBのHistory list lengthの監視と原因スレッドの特定と

TL;DR

  • yt-healthckeckHistory list length ( trx_rseg_history_len ) の監視を追加した
    • --history-list-enable=1 すると有効になる(そのうちこっちをデフォルトにする…)
      • MySQL 5.6とそれ以降のみ対応、もう5.5は知らない…
    • デフォルトで10万がワーニング、50万をクリティカルにしてたんだけどあっさり食いちぎられた
      • その時の確認方法を主に

History list lengthとは(これは SHOW ENGINE INNODB STATUS の出力に載ってる用語、 information_schema.innodb_metrics 的には trx_rseg_history_len という名前で出てくる)「パージされずに残っているUNDOログレコードの数」らしく、主に「トランザクション開始したまま COMMITROLLBACKQUIT もせずに残ってるコネクション」があると増えていく。
これが伸びるのはデフォルトの REPEATABLE-READ を保証するためなので、ロックの有無には一切関係ない(ロックフリーな SELECT だけしか含まないトランザクションでも、残っている限り降り積もる)

で、まあコイツが伸びていくと無関係なテーブルでも重くなる(UNDOログがバッファプールに占める割合が増えて、バッファプールミスヒットが多くなるから?)ので、コイツを監視する機能をウチで使っている監視スクリプトである yt-healthckeck に搭載した。
SHOW ENGINE INNODB STATUS のパースとかはしたくなかったので、MySQL 5.6とそれ以降の information_schema.innodb_metrics に頼り切っている。

mysql80 15> SELECT name, subsystem, comment, count FROM information_schema.innodb_metrics WHERE name = 'trx_rseg_history_len';
+----------------------+-------------+-------------------------------------+---------+
| name                 | subsystem   | comment                             | count   |
+----------------------+-------------+-------------------------------------+---------+
| trx_rseg_history_len | transaction | Length of the TRX_RSEG_HISTORY list | 2695887 |
+----------------------+-------------+-------------------------------------+---------+
1 row in set (0.01 sec)

Nagiosコンパチな作りになっているのでこんな風に検出できる。

$ bin/yt-healthcheck -S /usr/mysql/8.0.22/data/mysql.sock  -uroot --history-list-enable=1
CRITICAL on xxxxx: trx_rseg_history_len is 2776982 (master)

$ echo $?
2

と、まあ検出するところまではどうでも良くて、ここから「History listを伸ばしているスレッドを特定して、そいつが KILL しても良さそうなものかダメっぽいか」を確認する方法を考える。

まずは information_schema.innodb_trx から長そうなトランザクションを探す。具体的には trx_started が古そうなヤーツ。
History list lengthを何らかの方法でグラフ化しているのであれば、だいたい「増え始めた時間帯」に開始されたトランザクションが残っていることが多いのではないか。

mysql80 4532569> SELECT * FROM information_schema.innodb_trx ORDER BY trx_started ASC\G
*************************** 1. row ***************************
                    trx_id: 421499581279640
                 trx_state: RUNNING
               trx_started: 2020-10-22 15:40:04
     trx_requested_lock_id: NULL
          trx_wait_started: NULL
                trx_weight: 0
       trx_mysql_thread_id: 15
                 trx_query: NULL
       trx_operation_state: NULL
         trx_tables_in_use: 0
         trx_tables_locked: 0
          trx_lock_structs: 0
     trx_lock_memory_bytes: 1136
           trx_rows_locked: 0
         trx_rows_modified: 0
   trx_concurrency_tickets: 0
       trx_isolation_level: REPEATABLE READ
         trx_unique_checks: 1
    trx_foreign_key_checks: 1
trx_last_foreign_key_error: NULL
 trx_adaptive_hash_latched: 0
 trx_adaptive_hash_timeout: 0
          trx_is_read_only: 0
trx_autocommit_non_locking: 0
       trx_schedule_weight: NULL
*************************** 2. row ***************************
                    trx_id: 5909317
                 trx_state: RUNNING
               trx_started: 2020-10-22 18:45:04
     trx_requested_lock_id: NULL
          trx_wait_started: NULL
                trx_weight: 3
       trx_mysql_thread_id: 4558875
                 trx_query: COMMIT
       trx_operation_state: NULL
         trx_tables_in_use: 0
         trx_tables_locked: 1
          trx_lock_structs: 2
     trx_lock_memory_bytes: 1136
           trx_rows_locked: 1
         trx_rows_modified: 1
   trx_concurrency_tickets: 0
       trx_isolation_level: REPEATABLE READ
         trx_unique_checks: 1
    trx_foreign_key_checks: 1
trx_last_foreign_key_error: NULL
 trx_adaptive_hash_latched: 0
 trx_adaptive_hash_timeout: 0
          trx_is_read_only: 0
trx_autocommit_non_locking: 0
       trx_schedule_weight: NULL
2 rows in set (0.08 sec)

ここで手に入れた trx_mysql_thread_idSHOW PROCESSLISTID になる。

mysql80 4532569> SHOW PROCESSLIST;
+---------+-----------------+-----------+--------+---------+-------+----------------------------+------------------+
| Id      | User            | Host      | db     | Command | Time  | State                      | Info             |
+---------+-----------------+-----------+--------+---------+-------+----------------------------+------------------+
|       5 | event_scheduler | localhost | NULL   | Daemon  | 95512 | Waiting on empty queue     | NULL             |
|      15 | root            | localhost | d1     | Sleep   |   745 |                            | NULL             |
| 3300152 | root            | localhost | NULL   | Sleep   |     0 |                            | NULL             |
| 4233344 | root            | localhost | d1     | Sleep   |   630 |                            | NULL             |
| 4532569 | root            | localhost | NULL   | Query   |     0 | init                       | SHOW PROCESSLIST |
| 4626846 | root            | localhost | sbtest | Query   |     0 | waiting for handler commit | COMMIT           |
+---------+-----------------+-----------+--------+---------+-------+----------------------------+------------------+
6 rows in set (0.03 sec)

SHOW PROCESSLIST だと接続元IPとポートくらいはわかる(↑は root@localhost だけど…)ので、接続元にログインしてそのポートを使っているプロセスを特定すれば、「何をしていたのか、 KILL して良いのかどうか」はある程度判断が付く鴨。

バッチとかで掴んでいるようなわかりやすい場合はこれだけで何とかなるけど、「GUIなクライアントを開いている間ずっとトランザクションを掴みっぱなしになる」みたいな時は trx_mysql_thread_idperformance_schema.threadsthread_id に変換して performance_schema.events_statements_history から引くのが良い感じだった(バージョンによっては performance_schema.setup_consumers の設定が必要)
過去に発行したクエリーが何となくわかると特定はしやすかった。
ただし、 events_statements_history は直前の10件しか保持しないので、それより前に何をしていたのかはわからない。

mysql80 4532569> SELECT * FROM performance_schema.threads WHERE processlist_id = 15\G
*************************** 1. row ***************************
          THREAD_ID: 49
               NAME: thread/sql/one_connection
               TYPE: FOREGROUND
     PROCESSLIST_ID: 15
   PROCESSLIST_USER: root
   PROCESSLIST_HOST: localhost
     PROCESSLIST_DB: d1
PROCESSLIST_COMMAND: Sleep
   PROCESSLIST_TIME: 888
  PROCESSLIST_STATE: NULL
   PROCESSLIST_INFO: NULL
   PARENT_THREAD_ID: NULL
               ROLE: NULL
       INSTRUMENTED: YES
            HISTORY: YES
    CONNECTION_TYPE: Socket
       THREAD_OS_ID: 29875
     RESOURCE_GROUP: USR_default
1 row in set (0.10 sec)

mysql80 4532569> SELECT sql_text FROM performance_schema.events_statements_history WHERE thread_id = 49 ORDER BY timer_start ASC;
+-------------------------------------------------------------------------------------------------------------------+
| sql_text                                                                                                          |
+-------------------------------------------------------------------------------------------------------------------+
| SELECT * FROM sbtest.sbtest1 WHERE id = 2                                                                         |
| SELECT * FROM sbtest.sbtest1 WHERE id = 2                                                                         |
| SELECT * FROM information_schema.innodb_buffer_page LIMIT 1                                                       |
| SELECT table_name, index_name, COUNT(*) FROM information_schema.innodb_buffer_page GROUP BY 1, 2                  |
| SELECT table_name, index_name, page_state, COUNT(*) FROM information_schema.innodb_buffer_page GROUP BY 1, 2, 3   |
| SELECT space, COUNT(*) FROM information_schema.innodb_buffer_page GROUP BY 1                                      |
| SELECT page_type, COUNT(*) FROM information_schema.innodb_buffer_page GROUP BY 1                                  |
| SELECT * FROM information_scheam.innodb_metrics WHERE name = 'trx_rseg_history_len'                               |
| SELECT * FROM information_schema.innodb_metrics WHERE name = 'trx_rseg_history_len'                               |
| SELECT name, subsystem, comment, count FROM information_schema.innodb_metrics WHERE name = 'trx_rseg_history_len' |
+-------------------------------------------------------------------------------------------------------------------+
10 rows in set (0.04 sec)

「そのトランザクションが一度でも UPDATEINSERT をしたかどうか」というだけなら、 SHOW ENGINE INNODB STATUSTRANSACTIONS セクションに undo log entries と一緒に現れるかどうかで判断できる。行を更新してコミットしていないなら、必ずUNDOログを抱えているから。

------------
TRANSACTIONS
------------
Trx id counter 6437808
Purge done for trx's n:o < 43475 undo n:o < 0 state: running but idle
History list length 3153058
LIST OF TRANSACTIONS FOR EACH SESSION:
---TRANSACTION 421499581282208, not started
0 lock struct(s), heap size 1136, 0 row lock(s)
---TRANSACTION 421499581281352, not started
0 lock struct(s), heap size 1136, 0 row lock(s)
---TRANSACTION 421499581278784, not started
0 lock struct(s), heap size 1136, 0 row lock(s)
---TRANSACTION 421499581277928, not started flushing log
mysql tables in use 1, locked 1
0 lock struct(s), heap size 1136, 0 row lock(s)
---TRANSACTION 6437806, ACTIVE (PREPARED) 0 sec
2 lock struct(s), heap size 1136, 1 row lock(s), undo log entries 1
MySQL thread id 4819118, OS thread handle 140024457672448, query id 14114909 localhost root waiting for handler commit
COMMIT

おまけ。
トランザクションを生かしたまま、他のスレッドからひたすら UPDATE sbtest.sbtest1 SET k = k + 1 WHERE id = 1 だけを突っ込み続けてHistory list lengthを270万くらいまで伸ばした時のバッファプールの様子。

mysql80 15> SELECT page_type, COUNT(*) FROM information_schema.innodb_buffer_page GROUP BY 1;
+-------------------+----------+
| page_type         | COUNT(*) |
+-------------------+----------+
| UNDO_LOG          |     5200 |
| UNKNOWN           |     1024 |
| INDEX             |     1513 |
| SYSTEM            |      384 |
| INODE             |       57 |
| EXTENT_DESCRIPTOR |        4 |
| IBUF_BITMAP       |        4 |
| FILE_SPACE_HEADER |        5 |
| TRX_SYSTEM        |        1 |
+-------------------+----------+
9 rows in set (0.12 sec)

半分以上UNDO_LOGに持っていかれてしまった…。
道理でよくミスヒットするわけだ(空っぽのテーブルに対するSELECTでも散発的に10msくらいかかる)