Code Day's Night

ichikawayのブログ

とあるサーバプログラムが不安定だったのでstraceやメモリを見ながら解決と原因を究明した

とあるサーバープログラムと書きましたが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まで引き上げて解決した。

 

原因

この問題と同じ現象に遭遇した人のブログ記事があり、そこに原因が書かれている。

ソースコードから追いかけていて非常に参考になりました。

debiru.hatenablog.com

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のデータを計算すると

  • 0x607000 tinydns (rw) … 4
  • 0x608000 [anon](ヒープ/BSS)… 112
  • 0x…8c6000 libc (rw) … 8
  • 0x…8c8000 [anon](libc の bss)… 16
  • 0x…ae6000 [anon] … 8
  • 0x…af0000 ld (rw) … 4
  • 0x…af1000 ld (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も色々な情報が出る。例えばピークのメモリサイズなど。