memleax で起動中のプロセスのメモリリークを調べる

memleax というツールを見つけました。 プロセスのメモリリークを調査するためのツールで、既に動いているプロセスに attach してメモリリーク調査ができるようです。

GitHub - WuBingzheng/memleax: debugs memory leak of running process. Not maintained anymore, try `libleak` please.

debugs memory leak of running process. Not maintained anymore, try `libleak` please. - WuBingzheng/memleax

インストール

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 で開放していなかったようです。

Sentinel 2.8 branch memory leak · Issue #2012 · redis/redis

Looks like I have 2 sentinel instances with memory leak. From what I was able to collect, the memory increases indefinitely between 50kb and 100kb every second. My setup: 3 machines each machine ru...

Redis をアップデートをしなければいけませんね ☺️

このように、Valgrind のようなツールのように再コンパイルやプロセスの再起動なしに使うことができるのが嬉しい。本番環境でのトラブルシューティングなど、プロセスを停止することが難しい状況で役立ちそうです。


comments powered by Disqus