※ この記事はMySQL Advent Calendar 2024 1日目の記事です。
概要
MySQLをデバッグモードでビルドするとrpmやバイナリで配布されているリリースビルドでは確認できない情報が見られるようになります。
今回はそんな情報のうち、SQL実行して挙動を調査するような日常用途から、コードレベルで調査するときに便利なものまで順に紹介します。
- データディクショナリの隠しテーブルが直接参照できるようになる。
- デバッグビルド専用の隠し設定変数が設定できるようになる (設定変数に紐づく隠しコマンドが実行できる)
- gdbでオブジェクトを参照するときにoptimize outされなくなる
一方、デバッグビルドすると最適化オプションがすべてオフになり、コード中でデバッグビルドでのみのアサーションが実行されるようになるため、ユーザレベルで見るとかなりパフォーマンスが落ちます。
検証、調査用が主な用途であることはご承知おきください。
MySQLのデバッグビルドはcmakeの実行時に-DWITH_DEBUG=1
もしくは、-DCMAKE_BUILD_TYPE=Debug
を設定することで切り替えることが出来ます。
また、この記事中の内容は現時点最新のLTSバージョンであるMySQL 8.4.3をもとにしています。
1. データディクショナリの隠しテーブル
MySQLが動作するために必要なスキーマやテーブル、テーブルスペースなどのメタデータ情報を管理するシステムテーブルはデータディクショナリ(以降、DD)と呼ばれ、1つの一般テーブルスペース(general tablespace)としてmysql
スキーマで管理されています。これの情報はユーザには隠されたテーブルになっていて、INFORMATION_SCHEMAのVIEWを経由して加工された情報は見られますが、生のテーブルのデータを見ることは出来ません。
リファレンスマニュアルのDDの章の割と最初に説明されていますが、INFORMATION_SCHEMAのVIEWの定義を確認することで、なんとなくどんなテーブルが有るかは確認できますが、実際にどんなカラム、データが入っているか見るためにはデバッグビルドで確認する必要があります。
デバッグビルドされたMySQLを立ち上げたら以下のクエリを実行することで確認することができるようになります。
SET DEBUG = '+d,skip_dd_table_access_check';
mysql.schemataテーブルの定義と中身のデータを確認してみます。
mysql> show create table mysql.schemata\G
*************************** 1. row ***************************
Table: schemata
Create Table: CREATE TABLE `schemata` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`catalog_id` bigint unsigned NOT NULL,
`name` varchar(64) CHARACTER SET utf8mb3 COLLATE utf8mb3_bin NOT NULL,
`default_collation_id` bigint unsigned NOT NULL,
`created` timestamp NOT NULL,
`last_altered` timestamp NOT NULL,
`options` mediumtext COLLATE utf8mb3_bin,
`default_encryption` enum('NO','YES') COLLATE utf8mb3_bin NOT NULL,
`se_private_data` mediumtext COLLATE utf8mb3_bin,
PRIMARY KEY (`id`),
UNIQUE KEY `catalog_id` (`catalog_id`,`name`),
KEY `default_collation_id` (`default_collation_id`),
CONSTRAINT `schemata_ibfk_1` FOREIGN KEY (`catalog_id`) REFERENCES `catalogs` (`id`),
CONSTRAINT `schemata_ibfk_2` FOREIGN KEY (`default_collation_id`) REFERENCES `collations` (`id`)
) /*!50100 TABLESPACE `mysql` */ ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_bin STATS_PERSISTENT=0 ROW_FORMAT=DYNAMIC
mysql> select * from mysql.schemata;
+----+------------+--------------------+----------------------+---------------------+---------------------+---------+--------------------+-----------------+
| id | catalog_id | name | default_collation_id | created | last_altered | options | default_encryption | se_private_data |
+----+------------+--------------------+----------------------+---------------------+---------------------+---------+--------------------+-----------------+
| 1 | 1 | mysql | 255 | 2024-11-27 17:25:57 | 2024-11-27 17:25:57 | NULL | NO | NULL |
| 2 | 1 | information_schema | 33 | 2024-11-28 02:25:57 | 2024-11-28 02:25:57 | NULL | NO | NULL |
| 3 | 1 | performance_schema | 255 | 2024-11-28 02:25:58 | 2024-11-28 02:25:58 | NULL | NO | NULL |
| 4 | 1 | sys | 255 | 2024-11-28 02:26:00 | 2024-11-28 02:26:00 | NULL | NO | NULL |
+----+------------+--------------------+----------------------+---------------------+---------------------+---------+--------------------+-----------------+
4 rows in set (0.00 sec)
skip_dd_table_access_checkを設定する前は、以下のようにアクセスが拒否されます。
mysql> show create table schemata;
ERROR 3554 (HY000): Access to data dictionary table 'mysql.schemata' is rejected.
ただし、skip_dd_table_access_checkを有効にしても、アクセスできるようになったテーブルをshow tablesで確認することは出来ません。
mysql> show tables;
+------------------------------------------------------+
| Tables_in_mysql |
+------------------------------------------------------+
| columns_priv |
| component |
...
| user |
+------------------------------------------------------+
38 rows in set (0.01 sec)
見られることができるようになったテーブルの一覧は、skip_dd_table_access_checkを有効にした上で、mysql.tablesから、hiddenからむが'System'なものを抽出することで確認できます。 (information_schema.tablesでは確認できない)
私の環境でschema_id = 1はmysqlスキーマのschema_idです。mysql.schemataテーブルなどから確認できます。
mysql> select schema_id, name, type, hidden from tables where schema_id = 1 and hidden = 'System';
+-----------+------------------------------+------------+--------+
| schema_id | name | type | hidden |
+-----------+------------------------------+------------+--------+
| 1 | catalogs | BASE TABLE | System |
| 1 | character_sets | BASE TABLE | System |
| 1 | check_constraints | BASE TABLE | System |
| 1 | collations | BASE TABLE | System |
| 1 | column_statistics | BASE TABLE | System |
| 1 | column_type_elements | BASE TABLE | System |
| 1 | columns | BASE TABLE | System |
| 1 | dd_properties | BASE TABLE | System |
| 1 | events | BASE TABLE | System |
| 1 | foreign_key_column_usage | BASE TABLE | System |
| 1 | foreign_keys | BASE TABLE | System |
| 1 | index_column_usage | BASE TABLE | System |
| 1 | index_partitions | BASE TABLE | System |
| 1 | index_stats | BASE TABLE | System |
| 1 | indexes | BASE TABLE | System |
| 1 | innodb_ddl_log | BASE TABLE | System |
| 1 | innodb_dynamic_metadata | BASE TABLE | System |
| 1 | parameter_type_elements | BASE TABLE | System |
| 1 | parameters | BASE TABLE | System |
| 1 | resource_groups | BASE TABLE | System |
| 1 | routines | BASE TABLE | System |
| 1 | schemata | BASE TABLE | System |
| 1 | st_spatial_reference_systems | BASE TABLE | System |
| 1 | table_partition_values | BASE TABLE | System |
| 1 | table_partitions | BASE TABLE | System |
| 1 | table_stats | BASE TABLE | System |
| 1 | tables | BASE TABLE | System |
| 1 | tablespace_files | BASE TABLE | System |
| 1 | tablespaces | BASE TABLE | System |
| 1 | triggers | BASE TABLE | System |
| 1 | view_routine_usage | BASE TABLE | System |
| 1 | view_table_usage | BASE TABLE | System |
+-----------+------------------------------+------------+--------+
32 rows in set (0.00 sec)
これでバッファプールにある(LRUリストに積まれている)ページを眺めたときに、定義も確認できないし、アクセスできないテーブルが出てきても中身を見ることが出来ますね!
(↓ LRUリスト中のページを集計している様子)
mysql> select table_name, count(*) from INNODB_BUFFER_PAGE_LRU where space = 4294967294 group by table_name order by count(*) desc limit 10;
+----------------------------------------+----------+
| table_name | count(*) |
+----------------------------------------+----------+
| `mysql`.`tables` | 42 |
| `mysql`.`st_spatial_reference_systems` | 21 |
| `mysql`.`columns` | 20 |
| NULL | 13 |
| `mysql`.`routines` | 13 |
| `mysql`.`column_type_elements` | 4 |
| `mysql`.`indexes` | 4 |
| `mysql`.`column_statistics` | 3 |
| `mysql`.`foreign_keys` | 3 |
| `mysql`.`parameters` | 3 |
+----------------------------------------+----------+
10 rows in set (0.01 sec)
2. 隠し設定変数
上記のset debug = ...
のdebug
変数もそうですが、デバッグビルドではリファレンスマニュアルでも説明されていない隠し変数を設定できるようになります。
グローバル変数の差分をdebugビルドとリリースビルドで比較するとデバッグビルドでは次のような変数が設定されていることがわかります。
$ diff val_debug.txt val_relwithdebinfo.txt | grep '<'
< build_id 735669f0d549e41160d481131673643effe3ab1e
< debug
< debug_sensitive_session_string
< debug_set_operations_secondary_overflow_at
< innodb_background_drop_list_empty OFF
< innodb_buf_flush_list_now OFF
< innodb_buffer_pool_debug OFF
< innodb_buffer_pool_evict
< innodb_change_buffering_debug 0
< innodb_checkpoint_disabled OFF
< innodb_compress_debug none
< innodb_ddl_log_crash_reset_debug OFF
< innodb_dict_stats_disabled_debug OFF
< innodb_disable_background_merge OFF
< innodb_fil_make_page_dirty_debug 4294967295
< innodb_force_recovery_crash 0
< innodb_interpreter init
< innodb_interpreter_output The Default Value
< innodb_limit_optimistic_insert_debug 0
< innodb_log_checkpoint_fuzzy_now OFF
< innodb_log_checkpoint_now OFF
< innodb_log_flush_now OFF
< innodb_master_thread_disabled_debug OFF
< innodb_merge_threshold_set_all_debug 50
< innodb_page_cleaner_disabled_debug OFF
< innodb_page_hash_locks 16
< innodb_purge_run_now OFF
< innodb_purge_stop_now OFF
< innodb_saved_page_number_debug 0
< innodb_semaphore_wait_timeout_debug 600
< innodb_sync_debug OFF
< innodb_trx_purge_view_update_only_debug OFF
< innodb_trx_rseg_n_slots_debug 0
< version 8.4.3-debug
mysql -sse 'show global variables'
コマンドをファイルに出力して差分を表示すると抽出できます。
これらの変数の一部は、ONに設定することを契機になにかの処理を実行することができます。
例えば、set global innodb_log_checkpoint_now = ON;
はシャープチェックポイントの実行を、set global innodb_purge_run_now = ON;
はパージ処理の実行を開始することができます。
この他にもset global innodb_checkpoint_disabled = ON;
をすることで、チェックポイントを無効化したままmysqldを動作させることが出来ます。
すべての変数を使ったことはないですが、デバッグビルドしておけば--help --verboseをつけて簡単な説明を見ることも出来ます。
以下にこれらの説明部分を抜き出したものを列挙します。
--debug-sensitive-session-string=name
Debug variable to test sensitive session string variable.
--debug-set-operations-secondary-overflow-at=name
Error injection
--innodb-background-drop-list-empty
Wait for the background drop list to become empty
--innodb-buf-flush-list-now
Force dirty page flush now
--innodb-change-buffering-debug=#
Debug flags for InnoDB change buffering (0=none, 2=crash
at merge)
--innodb-checkpoint-disabled
Disable checkpoints
--innodb-compress-debug=name
Compress all tables, without specifying the COMPRESS
table attribute
--innodb-ddl-log-crash-reset-debug
Reset all crash injection counters to 1
--innodb-dict-stats-disabled-debug
Disable dict_stats thread
--innodb-disable-background-merge
Disable change buffering merges by the master thread
--innodb-fil-make-page-dirty-debug[=#]
Make the first page of the given tablespace dirty.
--innodb-force-recovery=#
Helps to save your data in case the disk image of the
database becomes corrupt.
--innodb-force-recovery-crash=#
Kills the server during crash recovery.
--innodb-interpreter[=name]
Invoke InnoDB test interpreter with commands to be
executed.
--innodb-limit-optimistic-insert-debug=#
Artificially limit the number of records per B-tree page
(0=unlimited).
--innodb-log-checkpoint-fuzzy-now
Force fuzzy checkpoint now
--innodb-log-checkpoint-now
Force sharp checkpoint now
--innodb-log-flush-now
Force flush of redo up to current lsn
--innodb-master-thread-disabled-debug
Disable master thread
--innodb-merge-threshold-set-all-debug=#
Override current MERGE_THRESHOLD setting for all indexes
at dictionary cache by the specified value dynamically,
at the time.
--innodb-page-cleaner-disabled-debug
Disable page cleaner
--innodb-page-hash-locks[=#]
Number of rw_locks protecting buffer pool page_hash.
Rounded up to the next power of 2
--innodb-purge-run-now
Set purge state to RUN
--innodb-purge-stop-now
Set purge state to STOP
--innodb-saved-page-number-debug[=#]
An InnoDB page number.
--innodb-semaphore-wait-timeout-debug=#
Number of seconds that a semaphore can be held. If
semaphore wait crossesthis value, server will crash
--innodb-sync-debug Enable the sync debug checks
--innodb-trx-purge-view-update-only-debug
Pause actual purging any delete-marked records, but
merely update the purge view. It is to create
artificially the situation the purge view have been
updated but the each purges were not done yet.
--innodb-trx-rseg-n-slots-debug=#
Debug flags for InnoDB to limit TRX_RSEG_N_SLOTS for
trx_rsegf_undo_find_free()
これらはUNIV_DEBUG
マクロで定義と実装を分けていますが、このマクロの定義範囲から出せばrelease buildでこれらの挙動を走らせることもできたりします。
3. gdbでのoptimize out対策
これはMySQLをデバッグビルドするとコンパイル時に-O0オプションが付くことで、コンパイラの最適化が抑制されるという話に過ぎませんが、releaseでビルドしたMySQLをgdbで捕まえてもほとんど情報が取れなくて困るのをよく見るので、3つ目に載せました。
例えばgdbでMySQLのコードにブレークポイントを張り、止まったところで変数を見ようとしたときに以下のようにoptimized outと言われてしまうことがよくありませうが、debugビルドなら起きません。
(gdb) p *page
$1 = <optimized out>
releaseビルドだとシンボルが見つからないと言われたりもします。
(gdb) p page_id
No symbol "page_id" in current context.
何はともあれ、調査したい現象がデバッグビルドで再現ができる限りはデバッグビルドを使うことをおすすめします。
(そういえば、@yoku0825さんが書かれている MINIMAL_RELWITHDEBINFO
はリファレンスマニュアルにないんですね、、、)
その他
debug変数には様々な値を設定できて、トレースファイルの作成など、リファレンスマニュアルで説明されているものもあります。私は活用できてなかったですが、面白そうです。
また、他にもset.*debug.*=
でgrepすると、SET DEBUG='+d,histogram_force_sampling';
など、リファレンスマニュアルにない設定が結構ありそうで気になりますが、公式に説明されているものは上記以外になさそうなので、ここまでにしておきます。
終わりに
MySQLをデバッグビルドで実行することで隠しテーブルや各しこ変数が使えることを紹介しましあ。
コンパイル時の最適化もなくなるので、検証時や挙動を探求するときにはデバッグビルドをおすすめします。
明日のMySQLアドベントカレンダーは @meijik さんです。