mysqld が起動の際、 innodb_buffer_pool_size に応じて buffer pool 以外で 確保しているメモリ+α

こんにちわ。せじまです。

ゴールデンウィーク中に自宅のLinuxマシンでMySQLの自由研究をやっていたとき、当初の目的は達成できなかったのですがその副産物としていろいろ気づいたことがあったので、いちおうさっくりとまとめることにしました。

公式の mysql-community-server-core 8.4.5-1ubuntu24.04 を、innodb_buffer_pool_size=40G, innodb_buffer_pool_dump_pct=0 という設定で起動したら、起動直後の時点で Resident Set Size が数GBになってました。そして、 innodb_buffer_pool_sizeを変更すると、起動直後の Residedent Set Size も変動します。

この Resident Set Size の大部分を占めているのは何なのか?という話です。

はじめに

いろいろ調べながらソースコード読んでいたところ、buf_chunk_t とかググってたら、最終的に Alibaba Cloud の Huaxiong Song さんが書かれたMySQL Memory Allocation and Management (Part II) という記事に行き着きました。こちらの 5. Summary に詳しくまとまっていますから、InnoDBにある程度くわしい人であれば、この一覧表を見ただけで「mysqld 起動直後に Resident Set Size の大部分を占めているのは何なのか」という疑問は明らかになるのかもしれません。

ちなみに Huaxiong Song さん、2025/05現時点において、MySQL 8.4のリリースノートの履歴を見る限り8.4.18.4.3にcontributeされているようで、とても優秀な方なんだろうなと思います。

あと、ついつい手癖で innodb_buffer_pool_dump_pct=0 としてしまいましたが、 innodb_buffer_pool_dump_pct の下限は10年以上前から 1 で、

innodb_buffer_pool_dump_pct=0 のように下限を下回る値は innodb_buffer_pool_dump_pct=1 扱いになり、次のようなログが出力されます。

ただ、mysqld を停止する際に buffer pool 上にほとんどデータが読み込まれていなかったなら、 innodb_buffer_pool_dump_pct=1 としても、mysqld 再起動時に強制的に1%分のデータが必ず buffer pool 読み込まれるわけではないでしょうし、「mysqld 起動直後に Resident Set Size の大部分を占めているのは何なのか」という今回のテーマにおいて「buffer pool 以外のものを中心に調査しています」といった意図が伝わりやすくなるかなぁとも思いましたので、敢えて innodb_buffer_pool_dump_pct=0 のままでやらせていただきます。

どうやって調べたか

ちょうど WSL2 上の Ubuntu 22.04 LTS でデバッグビルドした MySQL 8.4.5 があったので、それをgdbでステップ実行しながら「この関数抜けたら Resident Set Size けっこう増えたな!」とか泥臭い確認をしてたのですが、そもそも、デバッグビルドだとリリースビルドのものより Resident Set Size はかなり多かったりします。今回、リリースビルドのバイナリでそこまで精査して試したわけではありません。

例えば、 WSL2 上の Ubuntu 24.04 LTS で公式の mysql-community-server-core と mysql-community-server-debug のメモリ使用量を比べると

起動直後でこれだけ RSS が違います。ゆえに、デバッグビルドによる差分などはあるかもしれません。

予め雑なまとめ

最初にざっくりまとめておきます。

  • MySQLの起動シーケンスの中で、次のような関数でbuffer pool 以外に大量にメモリを確保するケースがあると考えられます。
  • これらのうち、 少なくとも buf_pool_register_chunk()、btr_search_sys_create()、 dict_init() は、innodb_buffer_pool_size に応じて確保されるメモリが増減します。
  • innodb_buffer_pool_size に応じて確保されるメモリの量は素数が絡んでくるので単純に試算することは難しいですが、innodb_buffer_pool_size の8~10%くらいは、追加でメモリが確保されるんじゃないかという気がします。

では詳細に入ります。

initialize_performance_schema()

これはもう改まっていうこともないですね。 Performance Schema はそれなりにメモリ食いますし設定値によって増減します。

gdb でメモリをまとめて確保してそうなところを洗い出していたらinitialize_performance_schema() も無視できてない程度にはメモリ確保してたので、いちおう挙げておこうかなくらいのところです。

例えば、公式の mysql-community-server-core 8.4.5-1ubuntu24.04 で sudo systemctl start mysql した起動直後に SHOW ENGINE PERFORMANCE_SCHEMA STATUS\G を叩くと

これくらいは行きますかな、まぁ P_S はそういうものなのかなと思います。

どんどん次に行きましょう。

buf_pool_register_chunk()

WL#6117 InnoDB: Resize the InnoDB Buffer Pool Online · mysql/mysql-server@0111e9c · GitHub

- 'innodb_buffer_pool_size' is changed to 'Dynamic Variable' (The Default value is still 128M. not changed.)

- able to monitor…

commit log を読むと、寡黙な紳士・木下靖文さんがMySQL5.7で実装された buffer pool のオンラインリサイズ機能に関連するところだとわかります。

WL#6117: InnoDB: Resize the InnoDB Buffer Pool OnlineのRequirementsを見ると

To optimize the resizing performance (the resizing affects to throughput, so shorter time is better), the chunk base size management is prepared also. (not needed copy whole of blocks. just add/delete chunks) The new global variable 'innodb_buffer_pool_chunk_size' is used to control the behavior.

とあります。 buffer pool は page という単位で管理されていますが、オンラインリサイズを最適化するために chunk という単位でも管理されているわけです。そしてそれは std::map で buffer pool とは異なる領域で管理されているので、 chunk が増えれば増えるほど buf_chunk_map_reg->insert() で chunk を登録する回数が増えるならば、それだけ buf_chunk_map_reg に割り当てられるヒープも拡張されうるわけですね。

https://github.com/mysql/mysql-server/blob/mysql-8.4.5/storage/innobase/buf/buf0buf.cc#L424-L429

https://github.com/mysql/mysql-server/blob/mysql-8.4.5/storage/innobase/buf/buf0buf.cc#L315-L324

buffer pool を chunk という単位で管理するための std::map であれば、 innodb_buffer_pool_size だけでなく innodb_buffer_pool_chunk_size によっても Resident Set Size は変動するわけです。

具体的にWSL2上で試してみましょう。環境は

とします。

innodb_buffer_pool_sizeを増やすことで RSS が増えるのに対し、 innodb_buffer_pool_chunk_sizeを減らすことでRSSは減りました。

btr_search_sys_create()

これは

buf_pool_init()で buffer pool の初期化をする際、

https://github.com/mysql/mysql-server/blob/mysql-8.4.5/storage/innobase/buf/buf0buf.cc#L1587

というように buffer pool のサイズに応じて

https://github.com/mysql/mysql-server/blob/mysql-8.4.5/storage/innobase/btr/btr0sea.cc#L186-L193

https://github.com/mysql/mysql-server/blob/mysql-8.4.5/storage/innobase/include/ut0new.h#L724-L762

https://github.com/mysql/mysql-server/blob/mysql-8.4.5/storage/innobase/btr/btr0sea.cc#L195-L207

https://github.com/mysql/mysql-server/blob/mysql-8.4.5/storage/innobase/include/ut0new.h#L2514-L2537
https://github.com/mysql/mysql-server/blob/mysql-8.4.5/storage/innobase/include/ut0new.h#L1738-L1800
https://github.com/mysql/mysql-server/blob/mysql-8.4.5/storage/innobase/include/ut0new.h#L1472-L1490

https://github.com/mysql/mysql-server/blob/mysql-8.4.5/storage/innobase/btr/btr0sea.cc#L209-L221

というわけで Adaptive Hash Index のための hash table を初期化しています。

innodb_buffer_pool_size に応じて btr_search_sys_create() に渡される hash_size が増減することを、先ほどのWSL2の環境で試してみましょう。
sudo apt install bpftrace しつつ mysql-community-server-core-dbgsym もインストールして

して別のターミナルでエディタで設定書き換えつつ

とすると、さきほど bpftrace したターミナルでは

といったように、 innodb_buffer_pool_size に応じて btr_search_sys_create() で指定される hash_size も変わりそうだと確認できます。

ただ、「Adaptive Hash Index は MySQL 8.4 だとデフォルトで無効化されたのでは?なぜ Adaptive Hash Index のための hash table が起動時に初期化されている?」と思うかもしれませんが、 8.4 でも動的に有効化できるので、起動時にAdaptive Hash Indexが無効化されていても、そのための hash table の領域は確保しておく方が無難なんでしょうね(たぶん)。

dict_init()

dict_init() 内で innodb_buffer_pool_size に応じて変化する要素のうち、サイズが大きくなるところとしては、 MySQL8.0.27や8.0.28あたりのmemory/innodb/hash0hashやut0new.hなどの話 で取り上げた dict_sys->table_hash や dict_sys->table_id_hash です。

https://github.com/mysql/mysql-server/blob/mysql-8.4.5/storage/innobase/dict/dict0dict.cc#L1021-L1025

改めて MySQL8.0.27や8.0.28あたりのmemory/innodb/hash0hashやut0new.hなどの話 を読み返していて「あーここ間違ってたなー」「でも、都度都度malloc()してるところが完全にないわけじゃないんだよなー」と反省したところがあったので、訂正させていただきますと、例えば

ut::new_<hash_table_t>() すると

https://github.com/mysql/mysql-server/blob/mysql-8.4.5/storage/innobase/include/hash0hash.h#L376-L385

で hash_table_clear() しているので、

https://github.com/mysql/mysql-server/blob/mysql-8.4.5/storage/innobase/include/hash0hash.ic#L44-L51

で memset() で 0x0 を設定しているので、ここで table->cells は page fault 起きて実際にメモリが割り当てられてると思います。table->cells の定義は

https://github.com/mysql/mysql-server/blob/mysql-8.4.5/storage/innobase/include/hash0hash.h#L450-L455

こちらで、

都度都度メモリが割り当てられるのは、

https://github.com/mysql/mysql-server/blob/mysql-8.4.5/storage/innobase/include/hash0hash.h#L61-L63

こっちでした。

つまるところ、 mysqld 起動時に dict_sys->table_id_hash->cells が初期化されてそれらにはヒープで実メモリが割り当てられますが、それぞれの cells にぶら下がる node については、実際に使うときにメモリが割り当てられるのだと私は認識しています。

innodb_buffer_pool_size を変更しながら起動時の Resident Set Size を確認

ではもう一度、 innodb_buffer_pool_size を1G, 2G, 10G, 20G で、mysqld 起動直後の Resident Set Size を比較してみましょう。

表にまとめると次のようになります。

innodb_buffer_pool_size Resident Set Size(KiB)
1G 600680
2G 707752
10G 1412272
20G 2314380

innodb_buffer_pool_size が1G のときと2GBのときの Resident Set Size の差分は 707752KiB - 600680KiB = 約104.5MiBくらいですが、10Gと20Gのときの差分は 2314380KiB - 1412272KiB = 約880.9MiBにもなりました。dict_sys->table_hash など hash table の初期化は

https://github.com/mysql/mysql-server/blob/mysql-8.4.5/storage/innobase/include/hash0hash.h#L379-L381

というように素数が絡んでくるので単純に試算することは難しいですが、 innodb_buffer_pool_size の+8~10%くらいは、 buffer pool 以外の領域のために、追加でヒープが割り当てられると見ておいて良さそうな気がします。

mysqld 起動直後で buffer pool で使用済みの領域もいちおう確認

いちおう、 mysqld 起動直後に buffer pool で使用済みの領域がどれくらいあるかも確認しておきましょう。

MySQLをクリーンインストールした状態で innodb_buffer_pool_dump_pct=0 にして innodb_buffer_pool_size=1G, 20Gで比較してみると、せいぜい 15~17MB弱程度だとわかります。

システムデータベースに含まれるアカウント情報や権限情報がロードされることなども考えると、 0 にはならないでしょうがそう多いものでもないとわかります。initialize_performance_schema()、buf_pool_register_chunk()、btr_search_sys_create()、 dict_init() などの関数内で確保されるメモリと比較すれば、わずかなものと言えるでしょう。

おわりに

私が本来検証したかったのは、

  • mysqld は、たくさんテーブルを開くと buffer pool 以外に大量にメモリを確保することが経験上わかっている

という経験則に対して

  • dict_sys->table_hash->cells にnode を割り当てるとして、テーブルのメタデータにはテーブル名やカラム名が含まれるのだから、これらがglibc mallocなどで割り当てられるなら arena に割り当てられるのでは
  • 例えば CREATE TABLE というDDLを実行した場合、 CREATE TABLE の STATEMENT やその Result Set と、テーブルのメタデータに含まれるテーブル名やカラム名が同じ arena に割り当てられた場合、Result Set を返せば STATEMENT や Result Set は free() で arena から開放できるけど、メタデータのテーブル名やカラム名が dict_sys->table_hash や dict_sys->table_id_hash などでキャッシュされ続けるなら、 arena はフラグメントしていくのでは?

という仮説を立証できるテストケースを作れるか、ということだったんですが、いろいろ試しているうちに mysqld 起動直後の Resitent Set Size が気になってしまったので、そちらを調べていたらゴールデンウィークが終わってしまいました。

glibc malloc でなく tcmalloc や jemalloc を使うようにしていろいろ試しながら pmap -x を見てたら思うところなどあったので、それについてはまたそのうち blog に書けたら良いかなと思うなどしています。

References