资讯动态

zebra进程内存泄漏排查:从定位到修复的完整链路

发布时间:2026/10/15 12:22:14 来源:尧图企业网站定制
1. 从一次“内存凭空消失”的排查说起如果你在运维岗位上待过一段时间大概率遇到过这种场景一台服务器的内存监控曲线缓慢爬升free 命令看到的可用内存一天比一天少但翻遍 top 和 ps 的常驻内存排行前几名都是些不起眼的进程加起来也解释不了那消失的几个 G。更让人头疼的是重启服务没用清理缓存也只是暂时缓解过不了多久内存又被吃掉。这种“隐形内存占用”往往比一个进程直接 OOM 更折磨人因为它不报错、不崩溃只是安静地把系统资源一点点蚕食掉。这次要聊的就是一个非常典型的案例zebra 进程导致的内存占用异常。zebra 是很多网络设备或 Linux 服务器上都会跑的一个路由管理守护进程属于 Quagga 或 FRR 这类路由套件的一部分。它平时看起来人畜无害CPU 占用低、内存占用也不高但恰恰是这种“看起来没问题”的进程最容易在长时间运行后暴露出内存泄漏问题。本文会从排查思路、定位手段、根因分析到修复验证完整还原一次真实的排查链路适合有一定 Linux 运维基础、正在被内存问题困扰的工程师参考。哪怕你之前没接触过 zebra这套方法论也能迁移到其他守护进程的内存排查上。我先把结论放在前面zebra 的内存泄漏往往不是它自己“主动”吃内存而是某些配置或交互模式下触发的数据结构只增不减比如路由表项、邻居表、或者某些事件回调注册后没有正确释放。这类问题在短时间压测里根本看不出来只有跑上几天甚至几周才会显现。所以排查的核心不是“看谁占用高”而是“看谁在持续增长”。2. 为什么常规内存排查手段会漏掉 zebra2.1 top 和 ps 的视角盲区大多数人排查内存问题的第一反应是敲top按M排序看谁占用高。但 zebra 这类进程有个特点它的 RSS常驻内存可能只有几十 MB在 top 里排到几十名开外你根本不会注意到它。可问题在于它可能同时在内核态或者通过共享内存、mmap 映射占用了大量内存而这些在 top 的默认视图里是不体现的。举个例子zebra 在处理大量路由条目时会通过 netlink 和内核交互内核里的路由缓存、邻居表这些结构占用的内存是算在 slab 里的不会计入某个进程的 RSS。你用ps aux看 zebra 只占 30MB但slabtop一看kmalloc-256、fib6_nodes这些 slab 缓存涨了好几百 MB。这就是典型的“进程看起来小系统内存却没了”。2.2 free 命令的误导性free -h输出的buff/cache经常被当成“可回收内存”很多人看到 available 还够就放心了。但 zebra 泄漏的内存如果落在不可回收的 slab 或者匿名页上available 会持续下降而 buff/cache 可能纹丝不动。这时候你如果只是echo 3 /proc/sys/vm/drop_caches会发现根本没释放多少因为问题不在 page cache。我自己的习惯是遇到内存缓慢增长先跑一条组合命令watch -n 5 free -m; echo ---; cat /proc/meminfo | grep -E Slab|SReclaimable|SUnreclaim|AnonPages重点看SUnreclaim和AnonPages这两个值。如果SUnreclaim持续涨基本可以锁定是内核对象泄漏再结合 slabtop 找具体缓存名如果AnonPages涨那就是用户态进程的匿名内存得回到进程层面查。2.3 zebra 的特殊性多进程架构zebra 通常不是单独跑的它和 bgpd、ospfd、ripd 这些兄弟进程一起工作彼此之间通过 ZAPIZebra API通信。内存泄漏可能发生在 zebra 主进程也可能发生在某个协议进程甚至可能是 ZAPI socket 缓冲区堆积导致的。如果你只盯着 zebra 一个进程看很容易漏掉真正的元凶。所以排查前先理清楚这台机器上跑了哪些路由相关进程它们之间的依赖关系是什么用ps -ef | grep -E zebra|bgpd|ospfd|ripd先摸清底数。这一步看着简单但我见过太多人一上来就扎进 zebra 的源码结果发现是 bgpd 把路由灌给 zebra 时触发了泄漏方向从一开始就错了。3. 定位 zebra 内存泄漏的完整排查链路3.1 第一步确认内存增长的时间规律别急着上工具先看监控。把过去一周的内存曲线拉出来重点观察三点增长是线性的还是阶梯式的有没有和某个操作比如路由震荡、配置变更对应的时间点重启 zebra 后内存是否回落线性增长通常意味着每次事件处理都有小量泄漏比如每收到一条路由更新就漏一点阶梯式增长往往对应批量操作比如一次性下发几千条路由。如果重启后内存明显回落那基本可以确认是 zebra 用户态的问题如果重启也不回落那泄漏可能在内核侧zebra 只是触发者。我遇到的那次是典型的线性增长每天涨 200MB 左右重启 zebra 后回落但过一天又开始涨。这个规律直接帮我排除了内核 slab 泄漏的可能把范围缩小到 zebra 进程本身。3.2 第二步用 valgrind 做粗筛确认是用户态问题后下一步是抓现场。最直接的工具是 valgrind虽然它会让 zebra 跑得慢十倍但用来定位泄漏点非常有效。操作上建议在测试环境复现不要直接在生产上挂 valgrind否则路由收敛会慢到影响业务。启动方式大概是这样valgrind --leak-checkfull --show-leak-kindsall --log-file/tmp/zebra_valgrind.log /usr/lib/frr/zebra -f /etc/frr/zebra.conf -d跑上几个小时等内存涨到一定程度后停掉看日志里的definitely lost和indirectly lost。如果这两项很大说明确实有泄漏而且 valgrind 会给出调用栈直接指向泄漏的代码位置。但 valgrind 有个坑zebra 这种事件驱动的进程很多内存是在事件回调里分配的valgrind 的调用栈可能被优化得看不清。这时候可以配合--track-originsyes虽然更慢但能追溯到内存最初分配的地方。3.3 第三步用 gdb 和 malloc 统计做精确定位如果 valgrind 因为性能问题没法在接近生产的环境跑或者日志太乱看不清可以换 gdb 加 malloc 统计的思路。具体做法是让 zebra 链接一个带统计功能的 malloc 库比如libtcmalloc或者自己写个简单的 hook记录每次分配和释放的调用栈。更轻量的办法是用 gdb attach 到 zebra 进程定期 dump 堆内存的统计信息。比如gdb -p $(pidof zebra) -batch -ex call malloc_stats() -ex detachmalloc_stats()会输出当前堆的总分配量、空闲量等信息。如果你隔一段时间执行一次看到Total (incl. mmap)持续上涨而Free不涨就说明有泄漏。再结合malloc_info或者malloc_trim可以进一步分析。我实际用下来最有效的组合是先用 valgrind 在测试环境定位到可疑函数再到生产环境用 gdb 定期采样确认增长趋势两者互相印证。3.4 第四步检查 ZAPI 和路由表相关数据结构zebra 的内存泄漏十有八九和路由表、邻居表、或者 ZAPI 消息处理有关。重点检查这几个地方路由条目释放逻辑当一条路由被撤销时zebra 是否真的 free 了对应的route_node和rib结构有些版本里如果路由被多个协议进程引用引用计数没减到零就不会释放。ZAPI 消息队列如果某个协议进程异常退出zebra 这边对应的 ZAPI 连接可能没有正确清理导致消息缓冲区一直挂着。邻居表老化IPv6 邻居发现或者 ARP 表项如果老化逻辑有问题表项会越积越多。可以用vtysh连上 zebra执行show memory看各模块的内存统计。FRR 和 Quagga 都内置了这个命令输出会按模块列出当前分配的内存块数量和总大小。如果你发现某个模块的块数只增不减那就是泄漏点。vtysh -c show memory输出里重点关注Route table、RIB、ZAPI、Neighbor这几项。正常情况下路由撤销后这些计数应该回落如果只涨不跌问题就找到了。4. 根因分析zebra 内存泄漏的几种典型模式4.1 路由引用计数未正确递减这是最常见的一种。zebra 内部用引用计数管理路由节点当多个协议进程比如 bgpd 和 ospfd同时下发同一条路由时route_node的引用计数会累加。如果某个协议进程撤销路由时引用计数没有正确减一这个节点就永远不会被释放。这种泄漏的特点是泄漏量和路由震荡频率成正比。网络越不稳定路由反复撤销下发泄漏越快。排查时可以在show memory里看Route node的数量如果它和实际路由数对不上而且持续增长基本就是这个问题。修复思路通常是检查route_unlock_node的调用路径确保每个route_lock_node都有对应的解锁。有些老版本代码里错误处理分支忘了解锁正常路径没问题一旦出错就漏。4.2 ZAPI 消息缓冲区堆积zebra 和协议进程之间通过 Unix socket 通信每个连接都有发送和接收缓冲区。如果某个协议进程处理消息很慢或者干脆卡死了zebra 这边的发送缓冲区就会堆积。这些缓冲区占用的内存不会体现在 zebra 的 RSS 里因为可能是 mmap 或者内核 socket buffer但会实实在在吃掉系统内存。判断方法用ss -x看 Unix socket 的发送队列和接收队列。如果某个 ZAPI socket 的Send-Q持续很大那就是对端处理不过来。ss -x -p | grep zebra这种情况的“修复”不一定是改代码有时候调整对端进程的处理逻辑或者给 ZAPI 加流控就能缓解。但如果是对端进程已经崩溃但 socket 没关闭那就得检查 zebra 的连接清理逻辑了。4.3 定时器和事件回调未注销zebra 大量使用事件驱动模型每个定时器、每个文件描述符可读可写事件都会注册回调。如果某个回调在对象销毁时没有注销那么即使对象已经不用了事件循环每次还是会调用它而且回调里可能又分配了新内存。这种泄漏比较隐蔽因为show memory里可能看不到明显的模块增长但进程的匿名内存会慢慢涨。用perf或者gdb抓一下调用栈如果发现某个回调被频繁调用但对应的对象已经不在路由表里了那就是它了。我见过一个案例是接口 down 掉后接口相关的定时器没注销每次定时器触发都重新分配一个结构体结果接口虽然 down 了内存却一直在涨。这种问题只能靠代码审查加动态跟踪来定位。4.4 配置解析阶段的临时对象泄漏还有一种容易被忽略的情况zebra 在加载配置文件或者执行vtysh命令时会创建大量临时对象。如果解析逻辑里有分支没释放这些临时对象每次配置变更都会漏一点。这种泄漏在稳定运行的网络里可能几个月都不明显但如果你经常改配置内存就会涨得很快。排查方法是反复执行同一条配置命令观察show memory里相关模块的计数是否回落。比如反复ip route添加删除看Route node是否回到初始值。5. 修复方案与验证手段5.1 代码层面的修复要点如果确认是引用计数问题修复的核心是保证lock和unlock严格配对。建议在代码里加断言比如在route_unlock_node里检查引用计数不能小于零一旦小于零就打印警告。这样能在测试阶段就暴露问题而不是等到生产环境内存涨爆。对于 ZAPI 缓冲区问题可以在 zebra 侧加一个上限当某个连接的发送队列超过阈值时主动断开或者丢弃消息避免无限堆积。同时要确保连接关闭时相关的缓冲区和事件回调都被清理干净。定时器泄漏的修复相对直接在对象销毁函数里显式调用THREAD_OFF或者对应的取消注册接口确保所有挂在这个对象上的事件都被摘掉。FRR 的thread.c里有现成的宏可以用关键是别漏。5.2 验证修复是否生效修完不能只看代码得用数据验证。我的做法是搭一个和现网类似的测试环境用脚本模拟路由震荡比如每秒钟撤销再下发一批路由持续跑 24 小时。同时用监控脚本每 5 分钟记录一次show memory的输出和进程 RSS。如果修复有效内存曲线应该是平稳的show memory里各模块的计数在震荡结束后能回落到基线。如果还在涨说明要么没修干净要么还有第二个泄漏点。验证时要注意一点zebra 本身有一些缓存和预分配机制内存不会立刻回落到最低点只要不持续增长就算正常。别看到内存没降到零就以为没修好。5.3 生产环境的灰度与回滚生产环境不能直接全量替换建议先在一台非核心设备上灰度。灰度期间重点观察内存增长是否停止、路由收敛是否正常、CPU 占用有没有异常升高。如果一切正常再逐步扩大范围。回滚方案也要提前准备好。zebra 的二进制替换很简单但要注意配置文件的兼容性。如果新版本改了配置语法回滚时得把配置也回退否则 zebra 可能起不来。我一般会在灰度前把旧二进制和旧配置一起备份出问题一条命令切回去。6. 日常运维中如何提前发现这类隐形泄漏6.1 建立内存基线监控别等出问题了才看内存。平时就要给关键进程建立基线记录正常运行时的 RSS、show memory各模块计数、slab 占用等。一旦发现某个指标偏离基线超过一定比例就触发告警。监控项不用太多但要有针对性。对于 zebra我建议至少监控这几个监控项采集方式告警阈值建议zebra RSSps 或 /proc/pid/status比基线高 50%SUnreclaim/proc/meminfo持续增长超过 1 小时Route node 数量vtysh show memory比实际路由数高 20%ZAPI socket 队列ss -xSend-Q 持续大于 0这些指标采集成本低但能提前几天甚至几周发现异常。6.2 定期做路由震荡测试如果条件允许可以在业务低峰期做小规模的路由震荡测试主动触发潜在的泄漏路径。比如模拟某个邻居断开重连或者撤销再下发一批路由然后观察内存是否回落。这种主动测试比被动等故障要可控得多。测试时要注意控制范围别把核心路由震没了。可以先在测试环境验证脚本再到生产上小范围执行。6.3 关注版本更新和社区反馈zebra 的内存泄漏很多是已知问题在 FRR 或 Quagga 的更新日志里会有修复记录。平时多留意社区邮件列表和 issue 跟踪看到和自己版本相关的内存修复及时评估升级。别等到自己踩坑了才去搜那时候业务可能已经受影响了。另外升级前一定要看 changelog 里的“已知问题”部分有些版本修了旧泄漏但引入了新问题盲目升级反而更糟。7. 几个容易踩的坑和实操心得第一个坑是只盯 RSS 不看 slab。前面说过zebra 触发的内核对象泄漏不会体现在进程 RSS 上。我建议排查内存问题时养成同时看free、slabtop、show memory三个地方的习惯缺一个都可能漏掉线索。第二个坑是在生产上直接跑 valgrind。valgrind 会让 zebra 慢十倍以上路由收敛时间从秒级变成分钟级核心业务可能直接中断。正确做法是在测试环境复现或者用 gdb 采样这种轻量方式。第三个坑是忽略配置变更的影响。很多人觉得内存泄漏一定是代码 bug但实际上不合理的配置也可能触发泄漏。比如配置了大量的静态路由或者策略路由zebra 内部的数据结构可能因为设计上限而表现异常。排查时不妨回顾一下最近有没有配置变更有时候改回旧配置就能定位问题。第四个坑是修复后不验证直接全量。我见过有人改了一行代码就全量推生产结果新代码引入了更严重的泄漏。灰度验证这一步不能省哪怕你觉得改动很小。最后一个心得是内存泄漏的排查本质是耐心活。它不像崩溃那样有明确的现场你需要反复采样、对比、排除。别指望一两个命令就找到根因做好打持久战的准备。但只要思路对工具用对再隐蔽的泄漏也能揪出来。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑