MySQLデバッグビルドのすゝめ

※ この記事は[MySQL Advent Calendar 2024](https://qiita.com/advent-calendar/2024/mysql)　1日目の記事です。

# 概要

MySQLをデバッグモードでビルドするとrpmやバイナリで配布されているリリースビルドでは確認できない情報が見られるようになります。
今回はそんな情報のうち、SQL実行して挙動を調査するような日常用途から、コードレベルで調査するときに便利なものまで順に紹介します。

1. データディクショナリの隠しテーブルが直接参照できるようになる。
1. デバッグビルド専用の隠し設定変数が設定できるようになる (設定変数に紐づく隠しコマンドが実行できる)
1. 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の章](https://dev.mysql.com/doc/refman/8.4/en/data-dictionary.html)の割と最初に説明されていますが、INFORMATION_SCHEMAのVIEWの定義を確認することで、なんとなくどんなテーブルが有るかは確認できますが、実際にどんなカラム、データが入っているか見るためにはデバッグビルドで確認する必要があります。

デバッグビルドされたMySQLを立ち上げたら以下のクエリを実行することで確認することができるようになります。

```sql
SET DEBUG = '+d,skip_dd_table_access_check';
```

mysql.schemataテーブルの定義と中身のデータを確認してみます。

```sql
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)
```
:::note info
skip_dd_table_access_checkを設定する前は、以下のようにアクセスが拒否されます。

```sql
mysql> show create table schemata;
ERROR 3554 (HY000): Access to data dictionary table 'mysql.schemata' is rejected.
```
:::

ただし、skip_dd_table_access_checkを有効にしても、アクセスできるようになったテーブルをshow tablesで確認することは出来ません。

```sql
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テーブルなどから確認できます。

```sql
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リスト中のページを集計している様子)

```sql
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ビルドとリリースビルドで比較するとデバッグビルドでは次のような変数が設定されていることがわかります。

```sh
$ 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さんが書かれている](https://yoku0825.blogspot.com/2023/11/mysqldthdgdb.html) `MINIMAL_RELWITHDEBINFO` はリファレンスマニュアルにないんですね、、、)


# その他

debug変数には様々な値を設定できて、[トレースファイルの作成](https://dev.mysql.com/doc/refman/8.4/en/making-trace-files.html)など、リファレンスマニュアルで説明されているものもあります。私は活用できてなかったですが、面白そうです。
また、他にも`set.*debug.*=`でgrepすると、`SET DEBUG='+d,histogram_force_sampling';`など、リファレンスマニュアルにない設定が結構ありそうで気になりますが、公式に説明されているものは上記以外になさそうなので、ここまでにしておきます。


# 終わりに

MySQLをデバッグビルドで実行することで隠しテーブルや各しこ変数が使えることを紹介しましあ。
コンパイル時の最適化もなくなるので、検証時や挙動を探求するときにはデバッグビルドをおすすめします。


明日のMySQLアドベントカレンダーは @meijik さんです。