とあるサーバープログラムと書きましたがtinydnsというDNSサーバです。今回の話はtinydns以外の問題でも汎用的に使えそうなものが多いので、タイトルを「とあるサーバー」にしました。
経緯
AmazonLinux2からUbuntu24.04にOSを移行する際に、tinydnsをUbuntu24.04で稼働させるとしばらくはうまく動いているが、何度かDNSクエリを投げてみると名前解決ができなくなった。
tinydnsのプロセスにstraceを仕掛けて同じ現象を再現してみると次のようなメモリ不足のエラーが出ていた。
16:59:46.117588 openat(AT_FDCWD, "data.cdb", O_RDONLY|O_NONBLOCK) = 4
16:59:46.117687 fstat(4, {st_mode=S_IFREG|0644, st_size=2555, ...}) = 0
16:59:46.117760 mmap(NULL, 2555, PROT_READ, MAP_SHARED, 4, 0) = 0x756ac3a68000
16:59:46.117863 getrandom("\x00\xc8\x67\xe6\xb2\x9c\xbb\x89", 8, GRND_NONBLOCK) = 8
16:59:46.117935 brk(NULL) = 0x10fbe000
16:59:46.118056 brk(0x10fdf000) = 0x10fbe000
16:59:46.118128 mmap(NULL, 1048576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = -1 ENOMEM (Cannot allocate memory)
5行目のbrkシステムコールで132KBのメモリを確保しようとして失敗し、mmapでCannnot allocate memoryのエラーが出ている。
プロセスごとに色々な制限値があり、/proc//limitsで確認できる。今回はlimitsのMax data sizeのSoft Limitが300KBしかないことが分かった。
cat /proc/プロセスID/limits
Limit Soft Limit Hard Limit Units
Max data size 300000 unlimited bytes
これはtinydnsでよく使われる一般的な上限設定で、tinydnsを動かす時に実行するrunというシェルスクリプトのsoftlimitで設定されている。
今回はそのsoftlimitを
softlimit -d300000
という設定箇所を
softlimit -d5000000
のようにして5MBまで引き上げて解決した。
原因
この問題と同じ現象に遭遇した人のブログ記事があり、そこに原因が書かれている。
ソースコードから追いかけていて非常に参考になりました。
libc6 の 2.32 では以前よりデータセグメントのサイズが大きくなったようで、その影響でプログラムが supervise の run ファイルで指定されている softlimit を超えてしまい、
mallocでのメモリ確保に失敗してしまうらしい。
根本原因
データセグメントのサイズが大きくなったというのはそうらしいが、本当にどうだったのか気になってしまい原因追及をした。
まずAmazonLinux2とUbuntu24.04のstraceを使ってメモリ確保の違いを探った。
AmazonLinux2もUbuntu24.04もbrk()で132KBメモリを確保することが分かったので、mallocの挙動が変わって確保したいメモリサイズも変わったという線は無くなった。
どちらのOSでもDNSクエリを投げていない状態でどれぐらいRLIMIT_DATA(softlimit上限値)に関わるメモリが使われているか確認した。
cat /proc/プロセスID/status を実行するとその中にVmDataという項目があるので、それを見て起動時のVmData消費量を見ればデータセグメントサイズがわかる。
Amazon Linux 2
VmData: 156 KiB
+ brk 132 KiB
= 288 KiB
→ 293 KiB以内なので成功
Ubuntu 24.04
VmData: 204 KiB
+ brk 132 KiB
= 336 KiB
→ 293 KiB超過なので失敗
これをみると、起動時のsoftlimitで制限されるセグメントサイズが50KBほど違うため、その差で実際にDNSクエリを投げてから使われるヒープ(brkで確保されるメモリ)サイズが足されてsoftlimitをオーバーしている。
VmDataのセグメントは主に次のもの
- .data セグメント
- .bss セグメント
- brk() / sbrk() で拡張されるヒープ(heap)
結論と感想
OSが変わり、glibcのバージョンが変わったことでデータセグメントサイズも変わり、それが影響して今回のメモリ不足が起こったことが数値として確認できた。
DNSサーバプログラムがメモリ上限300KBで問題なく動いていたことに驚いたとともに、AmazonLinux2でもギリギリの状態で動いていたので問題が出なくてよかった。
メモ
今回のglibcのバージョン
- AmazonLinux2
- Linux 4.14.355
- glibc 2.26
- Ubuntu24.04
- Linux 6.8
- glibc 2.39
メモリマップ
pmap -x プロセスIDというコマンドを使うとメモリの状態が表示される。
実際のメモリ消費(RSS)ではなく仮想アドレス空間のサイズ(Kbytes)の方がsoftlimitで制限される値になる。
例えばAmazonLinux2のtinydns起動直後のpmapの結果
# pmap -x プロセスID
0000000000400000 28 28 0 r-x-- tinydns
0000000000606000 4 4 4 r---- tinydns
0000000000607000 4 4 4 rw--- tinydns
0000000000608000 112 4 4 rw--- [ anon ]
00007f167151f000 1680 824 0 r-x-- libc-2.26.so
00007f16716c3000 2044 0 0 ----- libc-2.26.so
00007f16718c2000 16 16 16 r---- libc-2.26.so
00007f16718c6000 8 8 8 rw--- libc-2.26.so
00007f16718c8000 16 8 8 rw--- [ anon ]
00007f16718cc000 144 144 0 r-x-- ld-2.26.so
00007f1671ae6000 8 8 8 rw--- [ anon ]
00007f1671aef000 4 4 4 r---- ld-2.26.so
00007f1671af0000 4 4 4 rw--- ld-2.26.so
00007f1671af1000 4 4 4 rw--- [ anon ]
00007ffd72425000 132 8 8 rw--- [ stack ]
00007ffd724ba000 12 0 0 r---- [ anon ]
00007ffd724bd000 8 4 0 r-x-- [ anon ]
ffffffffff600000 4 0 0 r-x-- [ anon ]
それ以外にも cat /proc/プロセスID/maps でメモリレイアウトが確認できる。
さらに、各メモリ領域がどれぐらい使われているか、スワップしたかなどは、
cat /proc/プロセスID/smaps で見ることができる。
VmData 156 KiB の内訳
/proc/pid/status の VmData は「書き込み可能・プライベート・スタック以外」のマッピング、つまり pmap の rw--- 行の合計です。
上にあるAmazonLinux2の起動直後のpmapのデータを計算すると
0x607000tinydns (rw) … 40x608000[anon](ヒープ/BSS)… 1120x…8c6000libc (rw) … 80x…8c8000[anon](libc の bss)… 160x…ae6000[anon] … 80x…af0000ld (rw) … 40x…af1000ld (rw) … 4
合計 = 156 KiB → VmData と一致します
ここから何度かDNSクエリを投げると、pmapの結果に次の行が追加されます。
0000000001b6b000 132 4 4 rw--- [ anon ]
これがヒープで後から確保されたメモリで132KBとなっているため、根本原因の章で書いている計算と一致します。
pmapの結果の中にstackもrwで132KBと出ていますがこちらはVmDataではなくVmStackという方にカウントされます。
メモリサイズ
cat /proc/プロセスID/status | egrep 'VmPeak|VmSize|VmRSS|VmData'
VmPeak: 2820 kB
VmSize: 2816 kB
VmRSS: 1508 kB
VmData: 336 kB
/proc/プロセスID/statusも色々な情報が出る。例えばピークのメモリサイズなど。