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.1と8.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
扱いになり、次のようなログが出力されます。
1 2 |
2025-05-12T10:47:25.842731Z 0 [Warning] [MY-000081] [Server] option 'innodb-buffer-pool-dump-pct': unsigned value 0 adjusted to 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 のメモリ使用量を比べると
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
$ my_print_defaults mysqld --pid-file=/var/run/mysqld/mysqld.pid --socket=/var/run/mysqld/mysqld.sock --datadir=/var/lib/mysql --log-error=/var/log/mysql/error.log --innodb_buffer_pool_dump_pct=0 --innodb_buffer_pool_size=20G $ dpkg -S /usr/sbin/mysqld mysql-community-server-core: /usr/sbin/mysqld $ sudo -u mysql /usr/sbin/mysqld & [1] 1646 $ ps axuf | grep -e CPU -e mysqld USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND root 1646 0.0 0.0 14320 6912 pts/0 S 16:11 0:00 | \_ sudo -u mysql /usr/sbin/mysqld root 1647 0.0 0.0 14320 1224 pts/2 Ss+ 16:11 0:00 | | \_ sudo -u mysql /usr/sbin/mysqld mysql 1648 28.3 14.2 24925688 2311768 pts/2 Sl 16:11 0:02 | | \_ /usr/sbin/mysqld sejima 1710 0.0 0.0 3956 1928 pts/0 S+ 16:11 0:00 | \_ grep --color=auto -e CPU -e mysqld $ |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
$ my_print_defaults mysqld --pid-file=/var/run/mysqld/mysqld.pid --socket=/var/run/mysqld/mysqld.sock --datadir=/var/lib/mysql --log-error=/var/log/mysql/error.log --innodb_buffer_pool_dump_pct=0 --innodb_buffer_pool_size=20G $ dpkg -S /usr/sbin/mysqld-debug mysql-community-server-debug: /usr/sbin/mysqld-debug $ sudo -u mysql /usr/sbin/mysqld-debug & [1] 1718 $ ps axuf | grep -e CPU -e mysqld USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND root 1718 0.0 0.0 14312 6908 pts/0 S 16:12 0:00 | \_ sudo -u mysql /usr/sbin/mysqld-debug root 1719 0.0 0.0 14312 1220 pts/2 Ss+ 16:12 0:00 | | \_ sudo -u mysql /usr/sbin/mysqld-debug mysql 1720 29.5 19.3 27472688 3143904 pts/2 Sl 16:12 0:07 | | \_ /usr/sbin/mysqld-debug sejima 1784 0.0 0.0 3956 2040 pts/0 S+ 16:12 0:00 | \_ grep --color=auto -e CPU -e mysqld $ |
起動直後でこれだけ 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
を叩くと
1 2 3 4 |
Type: performance_schema Name: performance_schema.memory Status: 235246464 |
これくらいは行きますかな、まぁ 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
424 425 426 427 428 429 |
/** Registers a chunk to buf_pool_chunk_map @param[in] chunk chunk of buffers */ static void buf_pool_register_chunk(buf_chunk_t *chunk) { buf_chunk_map_reg->insert( buf_pool_chunk_map_t::value_type(chunk->blocks->frame, chunk)); } |
https://github.com/mysql/mysql-server/blob/mysql-8.4.5/storage/innobase/buf/buf0buf.cc#L315-L324
315 316 317 318 319 320 321 322 323 324 |
/** Map of buffer pool chunks by its first frame address This is newly made by initialization of buffer pool and buf_resize_thread. Note: mutex protection is required when creating multiple buffer pools in parallel. We don't use a mutex during resize because that is still single threaded. */ typedef std::map<const byte *, buf_chunk_t *, std::less<const byte *>, ut::allocator<std::pair<const byte *const, buf_chunk_t *>>> buf_pool_chunk_map_t; static buf_pool_chunk_map_t *buf_chunk_map_reg; |
buffer pool を chunk という単位で管理するための std::map であれば、 innodb_buffer_pool_size だけでなく innodb_buffer_pool_chunk_size によっても Resident Set Size は変動するわけです。
具体的にWSL2上で試してみましょう。環境は
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
$ uname --kernel-release 5.15.167.4-microsoft-standard-WSL2 $ lsb_release -a No LSB modules are available. Distributor ID: Ubuntu Description: Ubuntu 24.04.2 LTS Release: 24.04 Codename: noble $ dpkg --list mysql-community-server Desired=Unknown/Install/Remove/Purge/Hold | Status=Not/Inst/Conf-files/Unpacked/halF-conf/Half-inst/trig-aWait/Trig-pend |/ Err?=(none)/Reinst-required (Status,Err: uppercase=bad) ||/ Name Version Architecture Description +++-======================-==================-============-================================= ii mysql-community-server 8.4.5-1ubuntu24.04 amd64 MySQL Server $ |
とします。
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 |
$ my_print_defaults mysqld --pid-file=/var/run/mysqld/mysqld.pid --socket=/var/run/mysqld/mysqld.sock --datadir=/var/lib/mysql --log-error=/var/log/mysql/error.log --innodb_buffer_pool_dump_pct=0 --innodb_buffer_pool_size=10G $ sudo systemctl restart mysql $ ps axuf | grep -e CPU -e mysqld USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND sejima 1854 0.0 0.0 3956 2080 pts/0 S+ 19:48 0:00 | \_ grep --color=auto -e CPU -e mysqld mysql 1791 37.8 8.7 13502720 1415764 ? Ssl 19:48 0:01 /usr/sbin/mysqld $ sudo emacs /etc/mysql/mysql.conf.d/mysqld.cnf $ my_print_defaults mysqld --pid-file=/var/run/mysqld/mysqld.pid --socket=/var/run/mysqld/mysqld.sock --datadir=/var/lib/mysql --log-error=/var/log/mysql/error.log --innodb_buffer_pool_dump_pct=0 --innodb_buffer_pool_size=20G $ sudo systemctl restart mysql $ ps axuf | grep -e CPU -e mysqld USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND sejima 1965 0.0 0.0 3956 1924 pts/0 S+ 19:49 0:00 | \_ grep --color=auto -e CPU -e mysqld mysql 1902 22.2 14.2 24729080 2315376 ? Ssl 19:48 0:02 /usr/sbin/mysqld $ sudo emacs /etc/mysql/mysql.conf.d/mysqld.cnf $ my_print_defaults mysqld --pid-file=/var/run/mysqld/mysqld.pid --socket=/var/run/mysqld/mysqld.sock --datadir=/var/lib/mysql --log-error=/var/log/mysql/error.log --innodb_buffer_pool_dump_pct=0 --innodb_buffer_pool_size=20G --innodb_buffer_pool_chunk_size=1G $ sudo systemctl restart mysql $ ps axuf | grep -e CPU -e mysqld USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND sejima 2076 0.0 0.0 3956 1828 pts/0 S+ 19:49 0:00 | \_ grep --color=auto -e CPU -e mysqld mysql 2013 55.9 13.3 24794060 2164376 ? Ssl 19:49 0:02 /usr/sbin/mysqld $ |
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
1587 |
btr_search_sys_create(buf_pool_get_curr_size() / sizeof(void *) / 64); |
というように buffer pool のサイズに応じて
https://github.com/mysql/mysql-server/blob/mysql-8.4.5/storage/innobase/btr/btr0sea.cc#L186-L193
186 187 188 189 190 191 192 193 |
void btr_search_sys_create(ulint hash_size) { /* Copy the initial SYSVAR value. While the Server is starting, the updater for SYSVARs is not called to set their initial value. */ btr_search_enabled = srv_btr_search_enabled; btr_search_sys = ut::new_withkey<btr_search_sys_t>( ut::make_psi_memory_key(mem_key_ahi), hash_size); mutex_create(LATCH_ID_AHI_ENABLED, &btr_search_enabled_mutex); } |
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
195 196 197 198 199 200 201 202 203 204 205 206 207 |
btr_search_sys_t::btr_search_sys_t(size_t hash_size) { using part_type = btr_search_sys_t::search_part_t; parts = ut::make_unique_aligned<part_type[]>( ut::make_psi_memory_key(mem_key_ahi), alignof(part_type), btr_ahi_parts); static_assert(alignof(part_type) >= ut::INNODB_CACHE_LINE_SIZE); /* It is written only from one thread during server initialization, so it is safe. */ btr_ahi_parts_fast_modulo = ut::fast_modulo_t{btr_ahi_parts}; for (ulint i = 0; i < btr_ahi_parts; ++i) { parts[i].initialize(hash_size); } } |
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
209 210 211 212 213 214 215 216 217 218 219 220 221 |
void btr_search_sys_t::search_part_t::initialize(size_t hash_size) { /* Step-1: Init latches. */ rw_lock_create(btr_search_latch_key, &latch, LATCH_ID_BTR_SEARCH); /* Step-2: Allocate hash tables. */ hash_table = ib_create((hash_size / btr_ahi_parts), LATCH_ID_HASH_TABLE_MUTEX, 0, MEM_HEAP_FOR_BTR_SEARCH); hash_table->heap->free_block_ptr = &free_block_for_heap; #if defined UNIV_AHI_DEBUG || defined UNIV_DEBUG hash_table->adaptive = true; #endif /* UNIV_AHI_DEBUG || UNIV_DEBUG */ } |
というわけで 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 もインストールして
1 2 3 4 5 6 7 8 9 |
$ dpkg --list mysql-community-server-core-dbgsym Desired=Unknown/Install/Remove/Purge/Hold | Status=Not/Inst/Conf-files/Unpacked/halF-conf/Half-inst/trig-aWait/Trig-pend |/ Err?=(none)/Reinst-required (Status,Err: uppercase=bad) ||/ Name Version Architecture Description +++-==================================-==================-============-============================================= ii mysql-community-server-core-dbgsym 8.4.5-1ubuntu24.04 amd64 debug symbols for mysql-community-server-core $ |
1 2 3 4 5 6 7 |
$ sudo bpftrace -l 'uprobe:/usr/sbin/mysqld:*btr_search_sys_create*' uprobe:/usr/sbin/mysqld:_Z21btr_search_sys_createm uprobe:/usr/sbin/mysqld:_Z21btr_search_sys_createm.cold $ sudo bpftrace -e 'uprobe:/usr/sbin/mysqld:_Z21btr_search_sys_createm* {printf("%d\n", arg0);}' Attaching 2 probes... |
して別のターミナルでエディタで設定書き換えつつ
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
$ my_print_defaults mysqld --pid-file=/var/run/mysqld/mysqld.pid --socket=/var/run/mysqld/mysqld.sock --datadir=/var/lib/mysql --log-error=/var/log/mysql/error.log --innodb_buffer_pool_dump_pct=0 --innodb_buffer_pool_size=10G $ sudo systemctl restart mysql $ sudo emacs /etc/mysql/mysql.conf.d/mysqld.cnf $ my_print_defaults mysqld --pid-file=/var/run/mysqld/mysqld.pid --socket=/var/run/mysqld/mysqld.sock --datadir=/var/lib/mysql --log-error=/var/log/mysql/error.log --innodb_buffer_pool_dump_pct=0 --innodb_buffer_pool_size=20G $ sudo systemctl restart mysql |
とすると、さきほど bpftrace したターミナルでは
1 2 3 4 5 6 |
$ sudo bpftrace -e 'uprobe:/usr/sbin/mysqld:_Z21btr_search_sys_createm* {printf("%d\n", arg0);}' Attaching 2 probes... 20971520 41943040 |
といったように、 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 です。
1021 1022 1023 1024 1025 |
dict_sys->table_hash = ut::new_<hash_table_t>( buf_pool_get_curr_size() / (DICT_POOL_PER_TABLE_HASH * UNIV_WORD_SIZE)); dict_sys->table_id_hash = ut::new_<hash_table_t>( buf_pool_get_curr_size() / (DICT_POOL_PER_TABLE_HASH * UNIV_WORD_SIZE)); |
改めて MySQL8.0.27や8.0.28あたりのmemory/innodb/hash0hashやut0new.hなどの話 を読み返していて「あーここ間違ってたなー」「でも、都度都度malloc()してるところが完全にないわけじゃないんだよなー」と反省したところがあったので、訂正させていただきますと、例えば
1021 1022 |
dict_sys->table_hash = ut::new_<hash_table_t>( buf_pool_get_curr_size() / (DICT_POOL_PER_TABLE_HASH * UNIV_WORD_SIZE)); |
で ut::new_<hash_table_t>()
すると
376 377 378 379 380 381 382 383 384 385 386 |
/* The hash table structure */ class hash_table_t { public: hash_table_t(size_t n) { const auto prime = ut::find_prime(n); cells = ut::make_unique<hash_cell_t[]>(prime); set_n_cells(prime); /* Initialize the cell array */ hash_table_clear(this); |
で hash_table_clear() しているので、
https://github.com/mysql/mysql-server/blob/mysql-8.4.5/storage/innobase/include/hash0hash.ic#L44-L51
44 45 46 47 48 49 50 51 |
/** Clears a hash table so that all the cells become empty. */ static inline void hash_table_clear( hash_table_t *table) /*!< in/out: hash table */ { ut_ad(table); ut_ad(table->magic_n == hash_table_t::HASH_TABLE_MAGIC_N); memset(table->cells.get(), 0x0, table->get_n_cells() * sizeof(hash_cell_t)); } |
で memset() で 0x0 を設定しているので、ここで table->cells は page fault 起きて実際にメモリが割り当てられてると思います。table->cells の定義は
450 451 452 453 454 455 |
/** The pointer to the array of cells. If type==HASH_TABLE_SYNC_RW_LOCK it is: - modified when holding X-latches on all n_sync_obj - read when holding an S-latch for at least one n_sync_obj */ ut::unique_ptr<hash_cell_t[]> cells; |
こちらで、
都度都度メモリが割り当てられるのは、
https://github.com/mysql/mysql-server/blob/mysql-8.4.5/storage/innobase/include/hash0hash.h#L61-L63
61 62 63 |
struct hash_cell_t { void *node; /*!< hash chain node, NULL if none */ }; |
こっちでした。
つまるところ、 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 を比較してみましょう。
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 46 47 48 49 50 51 52 |
$ my_print_defaults mysqld --pid-file=/var/run/mysqld/mysqld.pid --socket=/var/run/mysqld/mysqld.sock --datadir=/var/lib/mysql --log-error=/var/log/mysql/error.log --innodb_buffer_pool_dump_pct=0 --innodb_buffer_pool_size=1G $ sudo systemctl restart mysql $ ps axuf | grep -e CPU -e mysqld USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND sejima 3400 0.0 0.0 3956 1932 pts/0 S+ 21:21 0:00 | \_ grep --color=auto -e CPU -e mysqld mysql 3339 25.0 3.7 3236412 600680 ? Ssl 21:21 0:00 /usr/sbin/mysqld $ sudo emacs /etc/mysql/mysql.conf.d/mysqld.cnf $ my_print_defaults mysqld --pid-file=/var/run/mysqld/mysqld.pid --socket=/var/run/mysqld/mysqld.sock --datadir=/var/lib/mysql --log-error=/var/log/mysql/error.log --innodb_buffer_pool_dump_pct=0 --innodb_buffer_pool_size=2G $ sudo systemctl restart mysql $ ps axuf | grep -e CPU -e mysqld USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND sejima 3511 0.0 0.0 3956 1916 pts/0 S+ 21:22 0:00 | \_ grep --color=auto -e CPU -e mysqld mysql 3448 33.3 4.3 4873688 707752 ? Ssl 21:21 0:00 /usr/sbin/mysqld $ sudo emacs /etc/mysql/mysql.conf.d/mysqld.cnf $ my_print_defaults mysqld --pid-file=/var/run/mysqld/mysqld.pid --socket=/var/run/mysqld/mysqld.sock --datadir=/var/lib/mysql --log-error=/var/log/mysql/error.log --innodb_buffer_pool_dump_pct=0 --innodb_buffer_pool_size=10G $ sudo systemctl restart mysql $ ps axuf | grep -e CPU -e mysqld USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND sejima 3622 0.0 0.0 3956 1988 pts/0 S+ 21:22 0:00 | \_ grep --color=auto -e CPU -e mysqld mysql 3558 65.8 8.7 13175040 1412272 ? Ssl 21:22 0:01 /usr/sbin/mysqld $ sudo emacs /etc/mysql/mysql.conf.d/mysqld.cnf $ my_print_defaults mysqld --pid-file=/var/run/mysqld/mysqld.pid --socket=/var/run/mysqld/mysqld.sock --datadir=/var/lib/mysql --log-error=/var/log/mysql/error.log --innodb_buffer_pool_dump_pct=0 --innodb_buffer_pool_size=20G $ sudo systemctl restart mysql $ ps axuf | grep -e CPU -e mysqld USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND sejima 3733 0.0 0.0 3956 1916 pts/0 S+ 21:22 0:00 | \_ grep --color=auto -e CPU -e mysqld mysql 3669 60.0 14.2 24794616 2314380 ? Ssl 21:22 0:02 /usr/sbin/mysqld $ |
表にまとめると次のようになります。
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 の初期化は
379 380 381 |
hash_table_t(size_t n) { const auto prime = ut::find_prime(n); cells = ut::make_unique<hash_cell_t[]>(prime); |
というように素数が絡んでくるので単純に試算することは難しいですが、 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弱程度だとわかります。
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 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 |
$ my_print_defaults mysqld --pid-file=/var/run/mysqld/mysqld.pid --socket=/var/run/mysqld/mysqld.sock --datadir=/var/lib/mysql --log-error=/var/log/mysql/error.log --innodb_buffer_pool_dump_pct=0 --innodb_buffer_pool_size=1G $ sudo systemctl restart mysql $ mysql -u root -p Enter password: Welcome to the MySQL monitor. Commands end with ; or \g. Your MySQL connection id is 8 Server version: 8.4.5 MySQL Community Server - GPL Copyright (c) 2000, 2025, Oracle and/or its affiliates. Oracle is a registered trademark of Oracle Corporation and/or its affiliates. Other names may be trademarks of their respective owners. Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. mysql> select @@innodb_buffer_pool_size, @@innodb_page_size, @@innodb_buffer_pool_size/@@innodb_page_size; +---------------------------+--------------------+----------------------------------------------+ | @@innodb_buffer_pool_size | @@innodb_page_size | @@innodb_buffer_pool_size/@@innodb_page_size | +---------------------------+--------------------+----------------------------------------------+ | 1073741824 | 16384 | 65536.0000 | +---------------------------+--------------------+----------------------------------------------+ 1 row in set (0.00 sec) mysql> show global status like 'innodb_buffer_pool_pages_free'; +-------------------------------+-------+ | Variable_name | Value | +-------------------------------+-------+ | Innodb_buffer_pool_pages_free | 64573 | +-------------------------------+-------+ 1 row in set (0.00 sec) mysql> select ((@@innodb_buffer_pool_size/@@innodb_page_size)-64573)*@@innodb_page_size/1024.0/1024.0; +-----------------------------------------------------------------------------------------+ | ((@@innodb_buffer_pool_size/@@innodb_page_size)-64573)*@@innodb_page_size/1024.0/1024.0 | +-----------------------------------------------------------------------------------------+ | 15.046875000000 | +-----------------------------------------------------------------------------------------+ 1 row in set (0.00 sec) mysql> exit Bye $ sudo emacs /etc/mysql/mysql.conf.d/mysqld.cnf $ my_print_defaults mysqld --pid-file=/var/run/mysqld/mysqld.pid --socket=/var/run/mysqld/mysqld.sock --datadir=/var/lib/mysql --log-error=/var/log/mysql/error.log --innodb_buffer_pool_dump_pct=0 --innodb_buffer_pool_size=20G $ sudo systemctl restart mysql $ mysql -u root -p Enter password: Welcome to the MySQL monitor. Commands end with ; or \g. Your MySQL connection id is 8 Server version: 8.4.5 MySQL Community Server - GPL Copyright (c) 2000, 2025, Oracle and/or its affiliates. Oracle is a registered trademark of Oracle Corporation and/or its affiliates. Other names may be trademarks of their respective owners. Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. mysql> select @@innodb_buffer_pool_size, @@innodb_page_size, @@innodb_buffer_pool_size/@@innodb_page_size; +---------------------------+--------------------+----------------------------------------------+ | @@innodb_buffer_pool_size | @@innodb_page_size | @@innodb_buffer_pool_size/@@innodb_page_size | +---------------------------+--------------------+----------------------------------------------+ | 21474836480 | 16384 | 1310720.0000 | +---------------------------+--------------------+----------------------------------------------+ 1 row in set (0.00 sec) mysql> show global status like 'innodb_buffer_pool_pages_free'; +-------------------------------+---------+ | Variable_name | Value | +-------------------------------+---------+ | Innodb_buffer_pool_pages_free | 1309647 | +-------------------------------+---------+ 1 row in set (0.00 sec) mysql> select ((@@innodb_buffer_pool_size/@@innodb_page_size)-1309647)*@@innodb_page_size/1024.0/1024.0; +-------------------------------------------------------------------------------------------+ | ((@@innodb_buffer_pool_size/@@innodb_page_size)-1309647)*@@innodb_page_size/1024.0/1024.0 | +-------------------------------------------------------------------------------------------+ | 16.765625000000 | +-------------------------------------------------------------------------------------------+ 1 row in set (0.00 sec) mysql> |
システムデータベースに含まれるアカウント情報や権限情報がロードされることなども考えると、 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
- MySQL Memory Allocation and Management (Part II)
- WL#6117: InnoDB: Resize the InnoDB Buffer Pool Online