Percona Toolkit に含まれる pt-kill というツールがあります。
本ツールは、その名称からMySQLのセッションを効率的に切断するためのものという印象が強いですが、そのような緊急措置的な利用方法の他にも様々な機能を備えています。
今回のブログでは、pt-killの使い方について整理していきたいと思います。
なお、利用している環境情報は以下の通りです。
OS version | MySQL version | pt-kill version |
---|---|---|
CentOS Stream 8 | 8.0.32 | 3.5.1 |
pt-killのダウンロード、前提パッケージのインストールは以下の通りです。
1 2 |
# wget -O /usr/local/bin/pt-kill percona.com/get/pt-kill # dnf install -y perl-Time-HiRes perl-DBI perl-DBD-MySQL |
pt-killのログインの簡略化のために、設定ファイルを追加しておきます
Percona Toolkitにおける設定ファイルの書き方については、マニュアルを確認してください。
1 2 3 4 5 6 |
$ cat > ~/.pt-kill.conf <<-EOF user=root password=MySQL8.0 host=127.0.0.1 port=3306 EOF |
pt-killの基本的な使い方
例えば10秒以上クエリを実行しているappユーザをすべてkillしたい、という場合以下のようになります。
1 |
$ pt-kill --interval 10 --busy-time 10 --match-user app --victims all --print --kill |
appユーザの接続を10接続を作ると、killされる様子が確認できます。
1 |
$ for i in {1..10};do mysql -uapp -PMySQL8.0 -e 'select sleep(100)'& done |
1 2 3 4 5 6 7 8 9 10 11 |
$ pt-kill --interval 10 --busy-time 10 --match-user app --victim all --print --kill # 2023-03-01T01:43:48 KILL 87768 (Query 17 sec) select sleep(100) # 2023-03-01T01:43:48 KILL 87767 (Query 17 sec) select sleep(100) # 2023-03-01T01:43:48 KILL 87766 (Query 17 sec) select sleep(100) # 2023-03-01T01:43:48 KILL 87765 (Query 17 sec) select sleep(100) # 2023-03-01T01:43:48 KILL 87764 (Query 17 sec) select sleep(100) # 2023-03-01T01:43:48 KILL 87763 (Query 17 sec) select sleep(100) # 2023-03-01T01:43:48 KILL 87762 (Query 17 sec) select sleep(100) # 2023-03-01T01:43:48 KILL 87761 (Query 17 sec) select sleep(100) # 2023-03-01T01:43:48 KILL 87760 (Query 17 sec) select sleep(100) # 2023-03-01T01:43:48 KILL 87759 (Query 17 sec) select sleep(100) |
pt-kill の処理フローについて
pt-kill の処理フローは、以下のとおりです。
それぞれのカテゴリごとに複数のオプションがあり、複数のオプションを組み合わせて意図した動作を実現します。
一定の間隔でSHOW PROCESSLISTを取得するところから開始し、SHOW PROCESSLISTの結果について加工、評価、評価結果へのアクションを行います。
前項で指定した --interval
はこの一連のフローの実行間隔です。
--busy-time
、--match-user
はマッチ条件にあたります(QUERY MATCHESを参照)。
--victim
はアクション対象選定の部分にあたり、以下からいずれかを選択します。
値 | 説明 |
---|---|
oldest | 最も古い接続を対象とする |
all | すべてのマッチした接続を対象とする |
all-but-oldest | 最も古い接続以外を対象とする |
recent/latest が無いのが気になりますが、大体のケースでは最も新しい接続が全体のパフォーマンスを損なうということは無いためでしょう。
oldestは、ロックの大本のセッションを切断するような利用用途を想定しており、all-but-oldest は、「キャッシュスタンピード」が発生した場合の対策を想定した値のようです。
アクション実行については、以下から選択できます。
オプション | 説明 |
---|---|
--kill |
接続を切断する |
--kill-query |
実行中のクエリを中断する |
--print |
対象をログ(デフォルトではSTDOUT)に表示 |
--execute-command |
任意のコマンドを実行する |
アクションを拡張するオプションは以下です。
オプション | 説明 |
---|---|
--kill-busy-commands |
–busy-time オプションが対象とするCommand列の値を追加する |
--fingerprint |
–printで表示されるクエリをフィンガープリントに変更する |
特に理由がなければ --print
は指定しておいたほうが有用でしょう。
--busy-time
オプションは Command: Query
である接続のみを対象としますが、PREPARE
などのコマンドは Command: Execute
になるなど対象から外れてしまうため、 --busy-time
を指定した場合は --kill-busy-commands
も指定したほうがよいでしょう。
その名称から効果を勘違しがちですが、--kill-busy-command
はあくまで--busy-time
の対象範囲を広げるためのオプションであり、接続を切断したい場合は、さらに--kill
オプションをつける必要があります。
最後にグループ化について触れておきます。
--group-by <SHOW PROCESSLISTの列名 or fingerprint>
と指定すると、指定した値でグループ化してくれます。
デフォルトでは、1つのグループ(default)にすべてが含まれる形になり、特にグループ化は必須ではありません。
グループ化が役立つのは、CLASS MATCHES を使う場合です。
最もわかりやすいのは、 --query-count
になるかと思いますが、--group-by User
と組み合わせると 同じ User
の値を持つ接続のカウントが --query-count
以上になったグループがマッチするという結果になります。
より実践的には、--match-command
、--match-user
などとさらに組み合わせて使うとよいでしょう。
1 2 |
# 同じユーザの接続が10以上となった場合に、User: app, Command: Query or Execute の接続を切断する $ pt-kill --interval 10 --group-by command --query-count 10 --match-command="Query|Execute" --match-user app --victim oldest --print --kill |
pt-kill でパフォーマンス情報を取る
例として、接続数が 100 以上になったら検知するたびに通知し、パフォーマンス解析情報を取る、といった事も可能です。
以下のスクリプトを --execute-command
に指定します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
#!/usr/bin/env python3 from slack_sdk.webhook import WebhookClient import subprocess from datetime import datetime url = "https://hooks.slack.com/services/XXXXXXXX/XXXXXXXXX/XXXXXXXXXXXXXXXXXXXXXXXX" webhook = WebhookClient(url) commands = { "show_processlist": ["mysql", "-e", "show processlist"] , "show_global_status": ["mysql", "-e", "show global status"] , "show_engine_innodb_status": ["mysql", "-e", "show engine innodb status\G"] } processlist = "" for k, v in commands.items(): r = subprocess.run(v, text=True, stdout=subprocess.PIPE) stdout_txt = r.stdout with open('/var/log/pt-kill-' + k + '.log', mode='a') as f: f.write(datetime.now().__str__() + '\n') f.write(stdout_txt + '\n') if k == 'show_processlist': processlist = stdout_txt response = webhook.send( text="Over 100 connections", blocks=[ { "type": "header", "text": { "type": "plain_text", "text": ":exclamation: Number of connections has exceeded 100" } }, { "type": "section", "text": { "type": "mrkdwn", "text": "<code>\n" + processlist + "\n</code>" } } ] ) |
10秒ごとにパフォーマンス情報を取ることとします。
先の説明のとおり、--group-by
を指定しない場合、すべての接続は単一のグループに入ります。
そして、グループに対して --query-count 100
つまり含まれる接続数が100以上のグループがマッチ条件で評価されます。
--match-all
はすべてにマッチする条件ですので、単純に接続数が100以上になったらアクションが実行される、ということになります。
--ignore-user
では、MySQLにおけるシステムユーザを指定して除外しています。
1 2 3 4 5 6 7 |
$ pt-kill \ --interval 10 \ --query-count=100 \ --ignore-user=event_scheduler \ --match-all \ --print \ --execute-command /root/script/notify.py |
100 接続を作ってみます。
1 2 3 4 |
$ python3 -m pip install --user mysql-connector-python $ python3 >>> import mysql.connector as mysql >>> cnxs = [mysql.connect(user='root', password='MySQL8.0') for x in range(100)] |
pt-killのログには、以下が出力されます。
デフォルトでは、--victim oldest
なので、マッチするのは最古の1接続のみです。
--victim all
にすると、マッチした接続ごとに --execute-command
が実行されてしまうのでご注意ください。
1 |
# 2023-03-01T05:25:45 KILL 94632 (Sleep 6 sec) NULL |
ログが作成されていることを確認しました。
1 2 3 4 |
# ls -l /var/log/pt-kill*.log -rw-r--r--. 1 root root 13707 Mar 1 05:26 /var/log/pt-kill-show_engine_innodb_status.log -rw-r--r--. 1 root root 42618 Mar 1 05:26 /var/log/pt-kill-show_global_status.log -rw-r--r--. 1 root root 14553 Mar 1 05:26 /var/log/pt-kill-show_processlist.log |
Slackへ通知も行われました。
今回のコマンドには指定しませんでしたが、 --kill
や --kill-query
をあわせて指定すると、問題が表面化する前にボトルネックとなっている接続を中断することができます。
pt-killで実行中の接続のEXPLAINを取る
pt-killでアクション --kill
, --print
, --execute-command
の3つを組み合わせる場合、実行順序は以下となります。
--print
--execute-command
--kill
pt-killでは、--daemonize
モードで実行した際に、 --log
オプションで出力をログファイルに書き込みます。
出力は --print
オプションで行われますので、その後に実行される --execute-command
のスクリプトで対象となった接続の Id
を取得する事も可能です。
例えば、10秒以上かかっている遅いクエリが存在した場合に、それらのEXPLAINをログに出すという事もできます。
1 |
$ pt-kill --daemonize --log /var/log/pt-kill.log --busy-command 10 --kill-busy-commands 'Query|Execute' --print --execute-command="get-explain.sh" |
get-explain.sh
は以下の内容です。
1 2 3 4 5 6 |
#!/bin/bash PROCESSLIST_ID=$(tail -1 /var/log/pt-kill.log | awk '{print $4}') date >> /var/log/pt-kill-explain.log mysql -t -e "EXPLAIN FOR CONNECTION $PROCESSLIST_ID" >> /var/log/pt-kill-explain.log mysql -t -e "SELECT * FROM performance_schema.processlist WHERE iD = $PROCESSLIST_ID" >> /var/log/pt-kill-explain.log |
以下のクエリを実行してみます。
1 |
mysql> select sleep(100), user from mysql.user limit 1; |
EXPLAINコマンドの結果と、その接続が実行していたSQLを以下のように取得できました。
1 2 3 4 5 6 7 8 9 10 11 |
Wed Mar 1 06:09:26 UTC 2023 +----+-------------+-------+------------+-------+---------------+---------+---------+------+------+----------+-------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-------+------------+-------+---------------+---------+---------+------+------+----------+-------------+ | 1 | SIMPLE | user | NULL | index | NULL | PRIMARY | 351 | NULL | 6 | 100.00 | Using index | +----+-------------+-------+------------+-------+---------------+---------+---------+------+------+----------+-------------+ +-------+------+-----------------+--------------------+---------+------+------------+-------------------------------------------------+------------------+ | ID | USER | HOST | DB | COMMAND | TIME | STATE | INFO | EXECUTION_ENGINE | +-------+------+-----------------+--------------------+---------+------+------------+-------------------------------------------------+------------------+ | 97830 | root | localhost:42654 | performance_schema | Query | 18 | User sleep | select sleep(100), user from mysql.user limit 1 | PRIMARY | +-------+------+-----------------+--------------------+---------+------+------------+-------------------------------------------------+------------------+ |
惜しい点として、マッチした接続ごとに--execute-command
が実行されてしまうため、複数の接続をマッチさせてそれらすべてのEXPLAINが取りたいというニーズには応えられていません。
スクリプトの中で排他処理するなど、工夫が必要になります。
pt-kill VS
pt-killと似たような機能として、 MariaDB版のkillコマンドではユーザやホストを指定して一括killを行う機能が拡張されています。
MySQLでは見当たりませんが、一度それを実行するだけであれば簡単なワンライナーで可能なので特別必要としないということかもしれません。
1 |
$ mysql -NB -e "show processlist" | awk '$2 ~ /^app$/{printf "kill %d;", $1}' | mysql |
また、今回実装してみたEXPLAINを取るという仕組みについては、PerconaやMariaDBの機能である、log_slow_verbosityを利用することで、スロークエリに出力されたクエリと一緒に実行計画を出力するという機能が存在します。
https://mariadb.com/kb/en/explain-in-the-slow-query-log/
https://docs.percona.com/percona-server/5.7/diagnostics/slow_extended.html#log_slow_verbosity
pt-killでは好きなスクリプトをフックに使えるという点で柔軟性がありますが、これらのDBの機能を利用するのもよいでしょう。
まとめ
今回は、 pt-kill についてご紹介させていただきました。
基本的にはパフォーマンス上の問題となりうる前に、過剰な接続を切断、もしくはクエリを中断することで未然に防ぐことを目的としたツールではありますが、様々なアレンジを加えられることをご理解いただけたかと思います。
チャンスが有ればPercona Toolkitを活用してみていただけると幸いです。