Linux服务器内存泄漏排查实战:从slab缓存到内核对象追踪

Linux服务器内存泄漏排查的核心思路

Linux服务器内存泄漏是运维工作中最棘手的问题之一。进程占用的内存持续增长却不释放,最终触发OOM Killer杀掉关键进程,导致服务中断。内存泄漏的排查需要区分用户态和内核态两个层面——用户态泄漏相对容易定位,内核态泄漏(尤其是slab缓存膨胀)则需要更专业的工具和方法。

用户态内存泄漏的定位流程

用户态内存泄漏最直接的信号是进程RSS(Resident Set Size)持续增长。通过tophtop可以快速发现异常进程:

# 按RSS排序查看内存占用前10的进程
ps aux --sort=-rss | head -11

# 查看进程的详细内存映射
cat /proc/<pid>/smaps_rollup

确认进程后,分析其内存分布:

# 使用pmap查看进程内存布局
pmap -x <pid> | tail -1

# 查看进程的堆内存使用
cat /proc/<pid>/status | grep -E 'VmRSS|VmSize|VmData|VmStk'

对于C/C++程序,Valgrind的memcheck是定位泄漏的利器:

# Valgrind内存泄漏检测
valgrind --leak-check=full --show-leak-kinds=all --track-origins=yes \
    --log-file=valgrind_report.log ./your_program

对于无法停机的生产环境,使用AddressSanitizer(ASan)编译的版本配合LeakSanitizer可以在运行时捕获泄漏点:

# GCC/Clang编译时启用ASan+LSan
gcc -fsanitize=address -fsanitize=leak -g -O1 your_program.c -o your_program

# 运行时控制LSan行为
ASAN_OPTIONS=leak_check_at_exit=1 LSAN_OPTIONS=verbosity=1:log_threads=1 ./your_program

slab缓存膨胀的排查方法

内核态内存泄漏最常见的表现是slab缓存持续增长。通过/proc/slabinfoslabtop可以观察各slab对象的使用情况:

# 实时监控slab缓存使用
slabtop -o -s c

# 查看特定slab对象的详细信息
cat /proc/slabinfo | grep -E 'dentry|inode|buffer_head|kmalloc'
cat /sys/kernel/slab/dentry/objects  # dentry对象数量
cat /sys/kernel/slab/dentry/objsize  # 每个对象大小

dentry和inode缓存是最常见的膨胀对象。大量文件操作(如日志轮转、临时文件创建删除)会产生dentry缓存,即使文件已删除,dentry仍然驻留内存。手动回收方法:

# 查看可回收的slab内存
cat /proc/meminfo | grep -E 'Slab|SReclaimable|SUnreclaim'

# 回收可回收的slab缓存
echo 2 > /proc/sys/vm/drop_caches   # 回收dentry和inode
echo 3 > /proc/sys/vm/drop_caches   # 回收page cache + dentry + inode

注意:drop_caches在生产环境中执行可能导致短暂的IO性能下降,因为后续文件访问需要重新从磁盘读取。

内核对象追踪:kmemleak和ftrace

当slab缓存膨胀但无法确定具体来源时,需要开启内核内存泄漏检测器kmemleak:

# 启用kmemleak(需要内核编译时开启CONFIG_DEBUG_KMEMLEAK)
echo scan > /sys/kernel/debug/kmemleak

# 查看检测到的内核内存泄漏
cat /sys/kernel/debug/kmemleak

# 清除已确认的非泄漏项
echo clear > /sys/kernel/debug/kmemleak

kmemleak的输出会显示未释放内存块的地址、大小和分配调用栈:

unreferenced object 0xffff888012345678 (size 64):
  comm "kworker/0:1", pid 1234, jiffies 4294901234
  backtrace:
    [<ffffffff8105a000>] kmalloc+0x34/0x70
    [<ffffffff812345678>] some_kernel_function+0x56/0x120
    [<ffffffff8123459ab>] another_function+0x78/0x200

对于无法开启kmemleak的内核,ftrace的function_graph追踪可以辅助定位:

# 追踪kmalloc/kfree调用
echo function_graph > /sys/kernel/debug/tracing/current_tracer
echo kmalloc > /sys/kernel/debug/tracing/set_graph_function
echo 1 > /sys/kernel/debug/tracing/tracing_on
# 运行复现操作
cat /sys/kernel/debug/tracing/trace

eBPF实时内存分配追踪

现代内核(4.14+)推荐使用eBPF/BCC工具集进行实时内存追踪,开销远低于kmemleak:

# 使用BCC的memleak工具
memleak-bpf -p <pid>        # 追踪特定进程
memleak-bpf -t              # 追踪所有线程

# 使用slabratios查看slab对象的增长比例
slabratios-bpf

# 使用kmem追踪内核内存分配
kmem-bpf

BCC的memleak输出会按调用栈聚合未释放的分配:

1 allocations from 1 stack traces totaling 4096 bytes
  4096 bytes from:
        my_alloc_handler+0x45  [my_module]
        process_request+0x1a2 [my_module]
        handle_work+0x8c       [my_module]

典型泄漏场景与解决方案

场景1:日志服务导致dentry缓存膨胀

Fluentd/Filebeat高频采集日志时,大量文件stat操作产生dentry缓存。解决方式是在/etc/sysctl.conf中调整vm.vfs_cache_pressure参数,增大该值(默认100,建议150-200)让内核更积极回收dentry:

vm.vfs_cache_pressure = 200
vm.min_free_kbytes = 1048576

场景2:TCP连接不释放导致内核socket缓存增长

大量短连接场景下,TIME_WAIT状态的socket占用内核内存。通过ss -s查看socket统计:

ss -s
# 输出中关注 TCP/transport total 和 time_wait 数量

# 调整TIME_WAIT回收参数
net.ipv4.tcp_max_tw_buckets = 65536
net.ipv4.tcp_tw_reuse = 1

场景3:容器场景的cgroup内存统计不准

容器内free命令看到的是宿主机内存,需查看cgroup内存控制器:

# 容器内查看实际内存限制和使用量
cat /sys/fs/cgroup/memory/memory.limit_in_bytes
cat /sys/fs/cgroup/memory/memory.usage_in_bytes
cat /sys/fs/cgroup/memory/memory.kmem.usage_in_bytes  # 内核内存

原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/linux-fu-wu-qi-nei-cun-xie-lou-pai-cha-shi-zhan-cong-slab/

(0)
小编小编
上一篇 15小时前
下一篇 15小时前

相关推荐