memleax で起動中のプロセスのメモリリークを調べる
memleax というツールを見つけました。 プロセスのメモリリークを調査するためのツールで、既に動いているプロセスに attach してメモリリーク調査ができるようです。
インストール
CentOS 7.4 環境で試してみました。
## 今回の動作環境
$ cat /etc/redhat-release
CentOS Linux release 7.4.1708 (Core)
## 依存パッケージをインストール
$ sudo yum install elfutils-libelf libdwarf libunwind
## GitHub から rpm をダウンロードしインストール
$ wget https://github.com/WuBingzheng/memleax/releases/download/v1.0.3/memleax-1.0.3-1.el7.centos.x86_64.rpm
$ sudo rpm -ivh memleax-1.0.3-1.el7.centos.x86_64.rpm
サンプルプログラムで動作を確かめてみる
メモリリークを起こすプログラムとして、メモリを次々と確保し free しないコードを書いてみた。(あまりおもしろい例が思いつかなかった…)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void) {
char *ptr;
while (1) {
ptr = (char*)malloc(1024);
sleep(1);
}
return 0;
}
上記コードをコンパイルして実行、そして別窓で実行したプログラムの PID を指定して memleax を立ち上げる。
memleax は、指定した時間よりも長くメモリブロックが確保されていた場合にメモリリークであると判断する。閾値として指定する expire time は -e
オプションで指定することができます (デフォルト 10 sec)。
$ memleax `pgrep a.out`
== Begin monitoring process 6059...
CallStack[1]: memory expires with 1024 bytes, backtrace:
0x00007f4ba1f9f0c0 libc-2.17.so __GI___libc_malloc()+0
0x000000000040058f a.out main()+18 main.c:9
CallStack[1]: memory expires with 1024 bytes, 2 times again
CallStack[1]: memory expires with 1024 bytes, 3 times again
CallStack[1]: memory expires with 1024 bytes, 4 times again
CallStack[1]: memory expires with 1024 bytes, 5 times again
CallStack[1]: memory expires with 1024 bytes, 6 times again
CallStack[1]: memory expires with 1024 bytes, 7 times again
CallStack[1]: memory expires with 1024 bytes, 8 times again
CallStack[1]: memory expires with 1024 bytes, 9 times again
CallStack[1]: memory expires with 1024 bytes, 10 times again
== Target process exit.
== Callstack statistics: (in ascending order)
CallStack[1]: may-leak=10 (10240 bytes)
expired=10 (10240 bytes), free_expired=0 (0 bytes)
alloc=20 (20480 bytes), free=0 (0 bytes)
freed memory live time: min=0 max=0 average=0
un-freed memory live time: max=19
0x00007f4ba1f9f0c0 libc-2.17.so __GI___libc_malloc()+0
0x000000000040058f a.out main()+18 main.c:9
メモリリークが発見されると、コールスタックの ID と共に、発生箇所の詳細が表示されます。コンパイル時にデバッグオプションが有効だったプロセスは、メモリを確保した箇所のソースファイル名と行番号も出てくるようです。
実例: redis-sentinel のメモリリークを調べてみる
redis-sentinel がメモリリークを起こしたと思われる事象に遭遇した際に、実際に memleax を使ってみました。
現象としては、時が経つにつれ redis-sentinel
プロセス (v2.8.12) が、メモリをモリモリと食っていくというもの。
Redis 2.8 系の release notes を見ると、 sentinel のメモリリークに関する Bug Fix がいくつかあり、おそらくバグ修正前のメモリリークによるものだろうと思われますが、きちんとツールでその事実を確認したほうが良いですね、っということで memleax を使ってみました。
$ memleax `pgrep redis-sentinel`
Warning: no debug-line found for /usr/sbin/redis-sentinel
== Begin monitoring process 21564...
CallStack[1]: memory expires with 64 bytes, backtrace:
0x00007f78cce4bac0 libc-2.12.so malloc()+0
0x00007f78ccea123b libc-2.12.so gaih_inet()+2715 0x00007f78ccea3dc0 libc-2.12.so getaddrinfo()+336
0x000000000045d58c redis-sentinel
0x000000000045dc93 redis-sentinel redisConnectBindNonBlock()+67
0x000000000045cdf9 redis-sentinel redisAsyncConnectBind()+9
0x0000000000455e19 redis-sentinel sentinelReconnectInstance()+249
0x0000000000458569 redis-sentinel sentinelHandleRedisInstance()+9
0x0000000000458629 redis-sentinel sentinelHandleDictOfRedisInstances()+41
0x000000000045863d redis-sentinel sentinelHandleDictOfRedisInstances()+61
0x00000000004586a5 redis-sentinel sentinelTimer()+21
0x000000000041f235 redis-sentinel serverCron()+837
0x0000000000417982 redis-sentinel aeProcessEvents()+514
0x0000000000417b7b redis-sentinel aeMain()+43
0x000000000041fd11 redis-sentinel main()+689
CallStack[2]: memory expires with 64 bytes, backtrace:
0x00007f78cce4bac0 libc-2.12.so malloc()+0
0x00007f78ccea123b libc-2.12.so gaih_inet()+2715
0x00007f78ccea3dc0 libc-2.12.so getaddrinfo()+336
0x000000000045d58c redis-sentinel
0x000000000045dc93 redis-sentinel redisConnectBindNonBlock()+67
0x000000000045cdf9 redis-sentinel redisAsyncConnectBind()+9
0x0000000000455da0 redis-sentinel sentinelReconnectInstance()+128
0x0000000000458569 redis-sentinel sentinelHandleRedisInstance()+9
0x0000000000458629 redis-sentinel sentinelHandleDictOfRedisInstances()+41
0x000000000045863d redis-sentinel sentinelHandleDictOfRedisInstances()+61
0x00000000004586a5 redis-sentinel sentinelTimer()+21
0x000000000041f235 redis-sentinel serverCron()+837
0x0000000000417982 redis-sentinel aeProcessEvents()+514
0x0000000000417b7b redis-sentinel aeMain()+43
0x000000000041fd11 redis-sentinel main()+689
CallStack[3]: memory expires with 64 bytes, backtrace:
0x00007f78cce4bac0 libc-2.12.so malloc()+0
0x00007f78ccea123b libc-2.12.so gaih_inet()+2715
0x00007f78ccea3dc0 libc-2.12.so getaddrinfo()+336
0x000000000045d58c redis-sentinel
0x000000000045dc93 redis-sentinel redisConnectBindNonBlock()+67
0x000000000045cdf9 redis-sentinel redisAsyncConnectBind()+9
0x0000000000455e19 redis-sentinel sentinelReconnectInstance()+249
0x0000000000458569 redis-sentinel sentinelHandleRedisInstance()+9
0x0000000000458629 redis-sentinel sentinelHandleDictOfRedisInstances()+41
0x000000000045864a redis-sentinel sentinelHandleDictOfRedisInstances()+74
0x00000000004586a5 redis-sentinel sentinelTimer()+21
0x000000000041f235 redis-sentinel serverCron()+837
0x0000000000417982 redis-sentinel aeProcessEvents()+514
0x0000000000417b7b redis-sentinel aeMain()+43
0x000000000041fd11 redis-sentinel main()+689
CallStack[1]: memory expires with 64 bytes, 2 times again
CallStack[2]: memory expires with 64 bytes, 2 times again
CallStack[3]: memory expires with 64 bytes, 2 times again
CallStack[1]: memory expires with 64 bytes, 3 times again
CallStack[2]: memory expires with 64 bytes, 3 times again
CallStack[3]: memory expires with 64 bytes, 3 times again
〜 省略 〜
CallStack[1]: memory expires with 64 bytes, 130 times again
CallStack[2]: memory expires with 64 bytes, 130 times again
CallStack[3]: memory expires with 64 bytes, 130 times again
CallStack[1]: memory expires with 64 bytes, 131 times again
CallStack[2]: memory expires with 64 bytes, 131 times again
CallStack[3]: memory expires with 64 bytes, 131 times again
^C
== Terminate monitoring.
== Callstack statistics: (in ascending order)
CallStack[2]: may-leak=131 (8384 bytes)
expired=131 (8384 bytes), free_expired=0 (0 bytes)
alloc=245 (15680 bytes), free=0 (0 bytes)
freed memory live time: min=0 max=0 average=0
un-freed memory live time: max=21
0x00007f78cce4bac0 libc-2.12.so malloc()+0
0x00007f78ccea123b libc-2.12.so gaih_inet()+2715
0x00007f78ccea3dc0 libc-2.12.so getaddrinfo()+336
0x000000000045d58c redis-sentinel
0x000000000045dc93 redis-sentinel redisConnectBindNonBlock()+67
0x000000000045cdf9 redis-sentinel redisAsyncConnectBind()+9
0x0000000000455da0 redis-sentinel sentinelReconnectInstance()+128
0x0000000000458569 redis-sentinel sentinelHandleRedisInstance()+9
0x0000000000458629 redis-sentinel sentinelHandleDictOfRedisInstances()+41 0x000000000045863d redis-sentinel sentinelHandleDictOfRedisInstances()+61
0x00000000004586a5 redis-sentinel sentinelTimer()+21
0x000000000041f235 redis-sentinel serverCron()+837
0x0000000000417982 redis-sentinel aeProcessEvents()+514
0x0000000000417b7b redis-sentinel aeMain()+43
0x000000000041fd11 redis-sentinel main()+689
CallStack[3]: may-leak=131 (8384 bytes)
expired=131 (8384 bytes), free_expired=0 (0 bytes)
alloc=245 (15680 bytes), free=0 (0 bytes)
freed memory live time: min=0 max=0 average=0
un-freed memory live time: max=21
0x00007f78cce4bac0 libc-2.12.so malloc()+0
0x00007f78ccea123b libc-2.12.so gaih_inet()+2715
0x00007f78ccea3dc0 libc-2.12.so getaddrinfo()+336
0x000000000045d58c redis-sentinel
0x000000000045dc93 redis-sentinel redisConnectBindNonBlock()+67
0x000000000045cdf9 redis-sentinel redisAsyncConnectBind()+9
0x0000000000455e19 redis-sentinel sentinelReconnectInstance()+249
0x0000000000458569 redis-sentinel sentinelHandleRedisInstance()+9
0x0000000000458629 redis-sentinel sentinelHandleDictOfRedisInstances()+41
0x000000000045864a redis-sentinel sentinelHandleDictOfRedisInstances()+74
0x00000000004586a5 redis-sentinel sentinelTimer()+21
0x000000000041f235 redis-sentinel serverCron()+837
0x0000000000417982 redis-sentinel aeProcessEvents()+514
0x0000000000417b7b redis-sentinel aeMain()+43
0x000000000041fd11 redis-sentinel main()+689
CallStack[1]: may-leak=131 (8384 bytes)
expired=131 (8384 bytes), free_expired=0 (0 bytes)
alloc=245 (15680 bytes), free=0 (0 bytes)
freed memory live time: min=0 max=0 average=0
un-freed memory live time: max=21
0x00007f78cce4bac0 libc-2.12.so malloc()+0
0x00007f78ccea123b libc-2.12.so gaih_inet()+2715
0x00007f78ccea3dc0 libc-2.12.so getaddrinfo()+336
0x000000000045d58c redis-sentinel
0x000000000045dc93 redis-sentinel redisConnectBindNonBlock()+67
0x000000000045cdf9 redis-sentinel redisAsyncConnectBind()+9
0x0000000000455e19 redis-sentinel sentinelReconnectInstance()+249
0x0000000000458569 redis-sentinel sentinelHandleRedisInstance()+9
0x0000000000458629 redis-sentinel sentinelHandleDictOfRedisInstances()+41
0x000000000045863d redis-sentinel sentinelHandleDictOfRedisInstances()+61
0x00000000004586a5 redis-sentinel sentinelTimer()+21
0x000000000041f235 redis-sentinel serverCron()+837
0x0000000000417982 redis-sentinel aeProcessEvents()+514
0x0000000000417b7b redis-sentinel aeMain()+43
0x000000000041fd11 redis-sentinel main()+689
おそらくこちらの Issue に該当するものと思われます。getaddrinfo
で確保したメモリを freeaddrinfo
で開放していなかったようです。
Redis をアップデートをしなければいけませんね ☺️
このように、Valgrind のようなツールのように再コンパイルやプロセスの再起動なしに使うことができるのが嬉しい。本番環境でのトラブルシューティングなど、プロセスを停止することが難しい状況で役立ちそうです。
Previous Post
Next Post