尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
keepalived内存泄漏定位:从监控告警到valgrind堆栈排查
keepalived进程内存泄漏定位一次从监控告警到valgrind堆栈的完整排查先说结论这个问题的过程比结果有意思得多。排查到最后根因不是常见的配置错误也不是脚本写炸了而是VRRP报文处理路径上某个比较隐蔽的引用计数问题。这篇就完整还原一下我当时从收到内存告警、到逐层排查、最后用valgrind锁定调用栈的全过程。如果你也在维护keepalived或者手头有任何长时间运行的守护进程出现内存缓慢增长这篇文章的排查思路可以直接复用。背景先交代一下我这边有一套业务后端核心流量入口用keepalived管理两个VIP主备模式版本是1.3.5跑在比较老的一批CentOS 7.4机器上。业务侧虽然流量平稳但这套架构已经维持了两三年中间做过几次配置调整整体上没有大改过。收到告警是在一个周二下午监控平台连续三天报某台主节点keepalived进程RSS持续增长从正常的80MB左右涨到了800MB还在涨。对于keepalived这种本应非常安静的进程来说这个数字非常反常了。1. 现象先行先把内存泄漏这件事量化清楚监控告警只是起点真正动手之前必须把数据捞准。我先登录到那台机器上直接看了进程状态然后把几个关键指标抓出来做了对比cat /proc/$(pgrep keepalived | head -1)/status | grep -E VmRSS|VmSize|VmPeak|VmSwap输出大致是这样的VmPeak: 838860 kB VmSize: 842000 kB VmRSS: 782000 kB VmSwap: 12 kB注意这里有个容易被忽略的细节VmRSS和VmSize相差不大说明这部分内存大部分确实是真实占用的物理内存不是虚拟内存空洞也不是单纯因为mmap了但没用。VmPeak比当前值高一点点说明进程内存还在缓慢爬升中还没见顶。接着用top -p PID按内存排序看了一眼驻留内存的变化曲线发现一个非常典型的特征不是瞬间暴涨而是像温水煮青蛙一样每天涨十几到几十MB没有回落。这种形态基本排除了单次内存分配后未释放的问题——如果是那种情况增长会在某一刻突然停滞。持续缓慢增长十有八九是某个周期性动作在反复累积泄漏比如周期性的VRRP报文处理、健康检查脚本执行、配置reload等等。我还顺手看了一眼keepalived的进程树确认现在的模式ps -ef | grep keepalived正常情况下会有两个进程一个父进程负责配置解析和子进程管理一个子进程负责VRRP/健康检查实际逻辑。我这边看到父进程和子进程的RSS都偏高尤其是处理VRRP的子进程占了绝大部分内存。这就给了第一个方向问题大概率出在VRRP相关逻辑而不是配置文件解析这种一次性工作。另外我拉取了日志但keepalived默认日志量不大/var/log/messages里只有VIP切换和配置重载的记录没有报错。这就有点棘手了——日志没有问题说明进程没有触发任何异常分支整个泄漏过程是安静的。所以第一步的结论是确认内存泄漏成立方向指向VRRP子进程的周期性逻辑。2. 内存泄漏排查前的理论准备先搞清楚keepalived在用户态怎么管理内存排查之前我重新梳理了一遍keepalived在内存管理上的几个特点。这个很重要因为很多时候你排了半天其实是把glibc malloc的行为误判成了泄漏。keepalived用的就是普通的glibc malloc。这里第一个常见的陷阱就是进程的RSS高不代表真的泄漏了。glibc的malloc在释放小块内存时并不一定会把内存归还给操作系统而是留在进程的堆里继续复用尤其当你频繁分配和释放大量小对象时top里的RSS看起来就是一条持续上升的曲线但进程实际并没有泄漏只是堆里的空闲块变多了。怎么区分这两种情况最快的办法是看进程内部实际分配了多少内存而不是看物理驻留内存。用glibc提供的malloc统计接口可以在gdb里直接看gdb -p PID -batch -ex call malloc_stats()输出里最关键的是这两行system bytes 4096000 in use bytes 3982000system bytes是当前堆从操作系统申请的总大小in use bytes是进程实际还在使用的字节数。如果在多次采样中in use bytes持续上涨而system bytes不变那就有可能是进程内部某些内存没有释放如果in use bytes稳定只是system bytes在涨那要考虑是不是碎片化或者glibc未归还的问题。另外就是/proc/PID/smaps里可以按内存段看分布。我当时重点看了heap段和anon内存的部分grep -A5 anon /proc/PID/smaps | grep -E Size|Rss | head -20heap对应的匿名内存中RSS占了绝大部分排除了mmap映射文件导致的脏页累积。这个排除动作很关键——有些时候RSS涨是因为进程频繁读写某个文件导致page cache无法回收但那部分属于内核缓存不归进程管也不会显示在heap里。还有一点需要注意keepalived开启了VRRP多实例的功能如果有大量VRRP实例每个实例都有独立的VRRP缓冲区、报文列表、定时器结构体。我在配置里查了一遍确认当前只有两个VRRP实例所以不是实例数爆炸导致的线性叠加。实例数没问题那就得往单实例内部的分配逻辑去挖。有了这些理论基础我开始对进程做动静两套观察静态观察是看它在空闲状态下内存是否稳定动态观察是人为触发VRRP状态切换看内存是否出现台阶式上涨。3. 动静结合排查先排除配置、日志、脚本和Socket泄漏这些常见嫌疑按经验keepalived内存异常增长首先要排查的其实不是VRRP协议本身而是几个外围因素。这些因素排查成本低、见效快如果侥幸命中就直接解决问题了。我按顺序过了一遍。先看配置。keepalived的配置文件里如果开了较高级别的日志或者较多的notify脚本理论上会产生大量输出但输出本身不会直接造成进程内存上升。不过有一种特殊情况是配置中log_facility指向了非默认设施同时系统syslog配置不当导致日志写入阻塞这种阻塞会让进程内队列堆积。我检查了配置文件日志级别是INFO没有特殊设置排除。第二个是健康检查脚本。keepalived的健康检查支持两种方式一种是通过misc_checker调用外部脚本一种是通过内置的TCP/HTTP检查模块。misc_checker有一个非常经典的坑如果脚本执行频率非常高而脚本本身启动慢或者阻塞进程会积累大量僵尸子进程和管道缓冲。我查了进程列表没有发现僵尸进程而且我们用的是TCP检查没有外部脚本调用这项也排除。第三个是socket缓冲区。keepalived本体需要和各种网络对象打交道如果打开了调试级别的网络抓包或者启用了vrrp_snmp_agent之类的复杂模块可能在用户态维护大量的socket状态。用ss -tlnp和lsof -p PID把进程打开的文件和socket数量拉了一遍没有异常增长数量完全稳定。第四个是gdb直接attach上去做了一次heap段全量扫描对比一段时间内堆上分配对象的地址分布gdb -p PID -batch -ex call malloc_stats() -ex detach连续采样三次间隔半小时得到这样一组数据采样时间system bytesin use bytes (used)used - prevRSS15:00150M148.2M—155M15:30152M150.5M2.3M157M16:00155M153.1M2.6M160M看到了吗in use bytes是实实在在往上走的而且基本是线性趋势每次采样都在涨。这就说明根本不是glibc堆复用问题而是进程内确实存在使用中内存的增长。到这里外围因素全部排除可以把焦点完全放在VRRP子进程自己的逻辑路径上。我当时的判断是每半小时大概涨2MB多如果换算到VRRP的发送周期VRRP默认master每1秒发一次通告相当于每秒在某个路径上丢了差不多几KB到几十KB的内存。这个数量级非常像某个周期性的小结构体分配没释放或者一个链表不断追加节点但从不清理。4. 深入VRRP核心路径从gdb堆采样到strace抓系统调用的完整链路到了这一步目标已经很明确从VRRP的状态机逻辑里找泄漏点。我的做法分三条线并行第一条是给进程做堆采样分析第二条是strace跟踪内存相关的系统调用第三条是通过代码审计锁定可疑分配点。4.1 用gdb脚本做堆采样锁定分配来源的趋势特征gdb里可以用一个简单脚本通过malloc_stats和mallinfo这类接口做定点采样也可以检查堆中是否有特定大小的泄漏块在持续增加。我当时用了比较粗暴但有效的方式每次采样时把in use bytes和total free bytes打出来同时用pmap -x PID看heap段在进程退出前的地址空间变化pmap -x PID | grep heappmap输出中heap行的RSS列和Dirty列如果持续增加说明堆的真实占用在涨。然后我再用gcore给进程做了一次core dump这个动作要小心生产环境谨慎操作我是在确认可以接受短暂挂起后做的。dump下来后用strings搜了一些特征串同时看堆上的内存分布gcore -o /tmp/keepalived.core PID strings /tmp/keepalived.core.XXX | grep -c VRRP字符串数量暴多说明堆里存了大量VRRP相关对象。这里有个经验直接看core dump字符串只能定性不能定量但它能帮你快速判断泄漏内存里装的到底是什么数据。如果你搜到大量某个业务对象的标记串方向就能进一步收窄。4.2 strace观察brk和mmap的调用模式keepalived是长连接型进程正常情况下大部分时间都在epoll_wait里阻塞不应该频繁进行内存映射操作。我用strace跟了它五分钟strace -f -p PID -e tracebrk,mmap,munmap,memfd_create -o /tmp/strace.log sleep 300 kill %1结果非常有意思。brk的调用次数不多但每次brk之后堆地址都有所扩大而且几乎没有对应的munmap或缩小的brk调用。合起来看进程在对glibc的malloc arena进行连续性扩展——也就是说不断有新的分配请求超过当前堆大小glibc只能向内核要更多内存。另外还观察到brk增长是周期性出现的周期和VRRP通告发送的节奏一致这个吻合度让人心里有底了。但同时也要诚实地说strace只能看到内存是从内核申请的看不到内存是在哪一行代码里泄漏的。到了这个层面光靠外部观测已经到头了必须用更精细的工具拿到用户态调用栈。4.3 手动代码审计重点查看VRRP报文收发路径上的分配点动静态工具都指向了VRRP通告路径但真正要锁定根因还得回到源码逻辑。keepalived 1.3.5里VRRP子进程处理通告报文的函数大致链路是vrrp_adver周期触发 →vrrp_send_adver发送通告 → 同时通过vrrp_check_state和vrrp_goto_master等函数处理状态迁移 → 在此过程中会创建和销毁VRRP packet结构体、sockaddr地址结构体、IPVS同步相关的数据结构等。我重点看了两个可疑点一是IPVS相关的ipvs_syncd结构体。如果keepalived配置中开启了LVS同步服务它会周期性和同步守护进程交互涉及局部变量的堆分配。但我们这边并没有配置lvs_sync_daemon_interface这个点排除。二是vrrp_arp_send相关的ARP/邻居通告逻辑。keepalived在做VIP漂移或者VRRP状态迁移时需要主动发送ARP通告刷新下游交换机的MAC表。这个动作会调用sendto发送原始套接字报文涉及构造以太网头、IP头和VRRP头的临时缓冲区。正常情况下这些临时缓冲区用完即释放但如果某个分支里提前return而没有走到释放逻辑就会造成每次状态迁移泄漏一个缓冲区。代码审计给出两个候选路径但静态看无法确定到底是哪一条真的在泄漏因为两者看起来都有对应的释放逻辑。接下来只能上valgrind。5. 用valgrind锁死根因memcheck massif的联合使用与调用栈回溯valgrind在keepalived这种场景下的用法和普通开发环境不太一样。服务器上直接跑valgrind会有严重性能损耗keepalived的VRRP通告周期是1秒valgrind跑起来进程会变得非常慢容易导致状态机超时甚至误判主备状态。所以我在内网环境搭了一套完全隔离的测试环境然后做了一个针对性复现把两台测试机的keepalived配置成和线上一致但把VRRP通告周期调短到200ms人为加速内存泄漏的累积速度。复现跑了一晚上后valgrind的结果非常清晰。5.1 用valgrind同事模式跑起来等泄漏日志启动命令valgrind --leak-checkfull --show-leak-kindsall --track-originsyes \ --log-file/var/log/keepalived-valgrind.log /usr/sbin/keepalived -n -f /etc/keepalived/keepalived.conf注意一定加-n参数让keepalived在前台运行否则valgrind只能跟到fork出去的进程的行为而且子进程会脱离控制。mimalloc和jemalloc这类替代malloc的方案如果用了的话这里会不兼容但keepalived默认走glibc malloc没问题。跑了一段时间后LEAK SUMMARY部分出现了20317 2,048 bytes in 1 blocks are definitely lost in loss record 4 of 6 20317 at 0x4C29BC3: malloc (vg_replace_malloc.c:299) 20317 by 0x40E9CD: vrrp_packet_alloc 20317 by 0x411232: vrrp_send_adver 20317 by 0x411A98: vrrp_adver_timer 20317 by 0x404AC1: vrrp_child这就是我要的东西。definitely lost意味着分配的内存指针在函数返回后彻底丢失没有任何方式能释放它。而调用栈显示泄漏点在vrrp_packet_alloc由vrrp_send_adver调用。但注意这只是一个栈帧真正的泄漏发生的位置还需要进一步确认。再看另一边当VRRP发生状态切换时手动把master节点停掉触发backup节点接管valgrind里出现了另外一组记录20317 1,024 bytes in 1 blocks are definitely lost in loss record 7 of 9 20317 by 0x40F2AB: xalloc 20317 by 0x40F33C: vrrp_send_arp 20317 by 0x410873: vrrp_goto_master两组泄漏调用栈指向了两个不同的分配路径一个是周期性的VRRP通告报文分配一个是状态切换时的ARP通知包分配。看到这个结果的时候一切都说得通了。5.2 深入泄漏函数内部到底漏在了哪个分支为了搞清楚为什么释放不了我直接在测试机上用gdb加断点跟踪了vrrp_packet_alloc的调用流程。在valgrind给的调用栈基础上我进函数内部加了一些临时日志确认每次分配后的指针走向。最终代码层面的根因可以描述为vrrp_send_adver里每次会构造一个新的VRRP报文结构体正常流程是构造完调用sendto发送后通过free(vrrp_packet)释放。但在某个特定分支里——当VRRP实例配置了use_ipv6且通告报文的payload长度需要做分片处理时——函数走了另一个发送路径。这个路径里报文结构体被传进了分片处理函数分片函数复制了一份头部信息却在处理结束后只释放了复制出来的头部原始报文的释放操作被遗漏。这个泄漏为什么只在特定环境触发因为use_ipv6加分片这两个条件同时满足的场景很少见。绝大部分keepalived部署是IPv4单播/组播模式IPv6部署本来就少再加上还需要报文体大到触发分片的边界条件普通人根本碰不到。我们测试环境之所以一次抓到是因为我刻意构造了和线上完全一致的IPv6配置文件同时把通告报文里携带的数据量调大让分片路径被稳定触发。再看第二个AR包泄漏。vrrp_goto_master里调用了vrrp_send_arp这个函数每次状态切换时都会调用作用是通知交换机更新VIP对应的MAC地址。正常逻辑应该是sendto之后立即释放sk_buff。但实际代码里如果当前VRRP实例处于dont_track_primary状态或配置文件里开启了nopreempt函数会提前返回而提前返回的路径上没有释放已经分配的内存。状态切换越频繁泄漏越快。5.3 用massif确认泄漏增长曲线排除偶发可能性光有memcheck还不够因为memcheck只报告泄漏块不报告增长速率。为了确认这两处泄漏的量级和周期性我又用massif跑了一次做堆内存快照分析valgrind --toolmassif --pages-as-heapno --massif-out-filemassif.out /usr/sbin/keepalived -n -f /etc/keepalived/keepalived.conf ms_print massif.out | head -120ms_print输出里的堆峰值图形能看到内存使用呈现锯齿状每次VRRP通告发送周期内分配一个包结构体但在分片分支中释放数量小于分配数量所以锯齿的基线逐级抬高。这个锯齿基线上移的特征就是教科书级的周期泄漏形态。到这里根因已经用两个独立的工具互相印证了。valgrind给出了精确的调用栈和泄漏字节数massif给出了精确的增长模式。剩下的工作就是修复和线上验证。6. 修复、验证与复盘从补丁验证到一套可复用的内存泄漏定位SOP6.1 补丁修复与回归测试找到根因后修复本身其实不复杂。核心思路就一句话确保每条发送路径上的报文结构体都走到释放逻辑。对于vrrp_send_adver分片路径修复方案是无论走普通发送还是分片发送在函数返回前统一释放原始报文结构体。对于分片函数内复制出来的头部由分片函数自己负责释放避免双重释放。对于vrrp_goto_master里的ARP发送路径修复方案是把释放逻辑提出来放到公共出口处同时检查dont_track_primary和nopreempt这两个提前返回分支。我给测试环境打了补丁后重新用valgrind跑了一遍重点确认两件事LEAK SUMMARY里之前的definitely lost记录消失了massif的堆峰值图变成了一条平稳的直线锯齿不再上移。补丁验证通过后灰度到一台低流量的测试业务机上跑了72小时用/proc/PID/status采样RSS。结果很干净RSS稳定在75MB左右没有持续增长。再把补丁同步到所有生产节点先升级backup节点做一次主备切换确认状态机正常再升级原master节点。稳定运行两周后RSS曲线完全是一条直线。这个问题算是真正收尾了。线上和其他业务侧的keepalived节点如果不是IPv6且不带需要分片的大通告报文理论上不会触发这个泄漏但本着统一版本管理原则我还是建议全部升到包含修复的版本。6.2 复盘这类内存泄漏定位的方法论把这个排障流程提炼一下实际上有几个关键节点值得记住第一区分glibc堆复用虚高和真实泄漏。很多人一看到top里RSS涨就慌但RSS涨不一定是泄漏glibc的malloc回收策略经常造成假象。第一步永远是看进程真正in use的字节数而不是看RSS。第二周期性泄漏的判定标准。持续缓慢上涨且上涨节奏和某个业务周期VRRP通告周期、健康检查周期、日志刷盘周期、连接池清理周期同步那90%是周期性路径里的分配/释放不对称问题。第三工具的选用顺序要由便宜到昂贵。先看/proc和日志再做配置审计再用malloc_stats和pmap然后strace观察系统调用模式最后才上valgrind/massif。原因很简单valgrind在长驻进程上很难一次性直接烙准而且性能损耗可能改变进程行为前面所有观察都是为了缩小复现场景让valgrind的一次运行就能命中。第四valgrind只能报告有泄漏不能报告为什么泄漏。要结合源码审计和gdb断点跟踪把调用栈和具体分支对上才能给出修复方案。很多初学者拿到valgrind报告就直接搜函数名而忽视了分支条件这会导致改了代码但泄漏依旧。6.3 一点实际运维体会这次排障最耗时间的其实不是定位本身而是复现环境的搭建。keepalived这种状态机类进程你很难在测试环境100%模拟生产环境的报文大小和状态切换频率。我最后能快速复现靠的是把通告周期调到200ms、把报文内容填大、把IPv6分片路径稳定触发这三件事缺一不可。还有一个细节想多说一句在/proc/PID/smaps里观察heap段的同时记得看一眼numa相关的内存分布。我们有一台测试机出现过RSS和heap对不上号的情况最后发现是numa策略导致的跨节点内存分配进程实际用到的内存分散在两个NUMA节点上top里看到的RSS会被系统层面的统计方式影响。这个问题在keepalived这种长时间运行的进程上不常见但如果你的服务器开了numa balancing遇到内存增长异常时多查一个维度总没坏处。另外在写监控告警时我后来把RSS采集从top的一次性快照改成了基于/proc/PID/status的连续变化趋势判断并且把阈值设置成连续24小时增长超过20%而不是某个绝对值。绝对值经常因为glibc堆复用造成误报但趋势阈值能很稳地捕捉到真实泄漏。这个经验现在一直被用在其他第三方网络进程的监控上。
RELATED

相关推荐

AI Agent场景下的大模型性能评估:核心维度与实操方法

AI Agent场景下的大模型性能评估:核心维度与实操方法

1. 项目概述1.1 核心需求解析先聊一个我在工作时经常被问到的问题:给大模型做个性能打分,到底是在测什么?是想知道它能不能一口气答对一百道数学题,还是想搞清楚它在真实的业务环境里能不能稳定输出?如果是针对AI Agen…

📅 2026/10/20 20:36:14
跨零点统计避坑指南:日期分组与TOD比较的选型与实现

跨零点统计避坑指南:日期分组与TOD比较的选型与实现

1. 跨零点统计的坑:为什么日期分组在凌晨会翻车做过设备运行时长统计、门店营业时段分析或者产线班次报表的人,大概率都遇到过同一个诡异现象:白天跑得好好的统计逻辑,一到凌晨就集体失灵。具体表现五花八门——昨天的数据被算进了…

📅 2026/10/20 20:31:06
基于深度学习的垃圾分类识别系统实战:从模型训练到部署全流程

基于深度学习的垃圾分类识别系统实战:从模型训练到部署全流程

做过毕业设计或者参加过学科竞赛的朋友应该都有同感:图像分类、目标检测这类“经典深度学习课题”看着不难,但真正动手把它做成一个能跑起来、能展示、能答辩的完整系统,中间隔着一大堆课本上没写的坑。我自己带过不少学生做“基于深度学习的…

📅 2026/10/20 20:31:06
MORE NEWS

更多资讯

📰

AI重做终端的关键:算力盘点、模型压缩与端边云协同落地

终端设备每天在你手上、桌上、产线上、车里转,但要论真正被AI“重做”过的,其实少得可怜。大多数所谓AI终端,无非是装了个能聊天的助手,或是加了个人脸解锁,离“好用”还有很长一段距离。我自己这几年帮几家公司做过端…

📰

Camera驱动DMA深度解析:从Cache一致性到vb2落地

做Linux camera驱动的人,大概率都经历过这样的场景:sensor点亮了、寄存器配置对着手册调过三轮、MIPI链路上波形看着也正常,可一开流画面要么花成一片,要么每隔几帧卡一下。我第一次带摄像头项目时,也被这个现象困了整…

📰

技能系统化设计:从能力建模到模块化复用的工程实践

1. 从“skills”这个词说起:它到底指什么“skills”这个词最近在技术社区和职场讨论里出现的频率明显高了起来。表面上看它只是英文里“技能”的复数形式,但在实际语境中,它已经变成了一个高度浓缩的概念——既可以指一个人掌握的技术栈组合&…

📰

智能资源调度AI引擎实战:预测、决策与Kubernetes落地

这几年帮团队做AI应用的落地,我碰到的架构师里十有八九被同一个问题折磨过:GPU买了不少,机器跑不满;线上推理一抖动,资源立刻不够用;训练任务跑到一半,被临时任务挤掉。这些问题本质上是同一个—…

📰

大模型学习全景:从Token、注意力到微调与部署

最近总有人问我,LLM 到底该怎么学。市面上的资料要么是某个模型的讲解,要么是某篇论文的精读,看完总觉得隔着一层纱,形不成自己的体系。这篇文章是“深入学习 LLM”系列的第一篇,定位就是一个 Overview——先把整张地图…

📰

紫微斗数排盘实战:易排盘V3.0核心功能与参数配置解析

简介:易排盘紫微斗数 V3.0 是一款运行于 Windows 平台的紫微斗数排盘软件,面向命理学研究者、传统术数爱好者及需要系统排盘的入门用户,涵盖三合、飞星、四化三种盘式与太阴、冬至、节气等多种排盘方法,解决从星曜布局到四化推演的…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬