☰
Linux路由缓存排障:rt_acct与rt_cache的区别与实战用法
2026/10/19 3:21:29 网站建设 项目流程

最近调试一台跑老内核的系统时,我把cat /proc/net/rt_acct打出来的那一串数字反复核了几遍,旁边一位同事问了一句:“这和 rt_cache 是一回事吗?”这个问题很有代表性。在 Linux 路由子系统的 proc 文件里,rt_acct和rt_cache名字相近、路径相邻,但一个是路由缓存的统计账本,一个是当前哈希缓存的实时快照,用途、解析方式、排障价值完全不同。这篇文章就从一线排查角度出发,把这两个信息文件彻底讲透:它们从哪里来、内部记录了什么、用什么命令观察、怎么靠它们定位路由转发风暴和缓存抖动,以及新内核上为什么已经见不到它们了。如果你维护的是 2.6.x 到 3.5 之间的老系统,或者手头有还在用旧内核的嵌入式设备,这篇应该能帮你省不少时间。

1. 先分清两个文件:一个记账本,一个缓存快照

1.1 rt_acct 是路由子系统内部的“记账本”

/proc/net/rt_acct这个文件不是按网卡、也不是按 IP 统计流量的,它记录的是路由缓存散列表里各个 hash 桶的累计计数。旧内核里,路由查找结果会挂在以目的地址、源地址、TOS、mark 等为参数的哈希链上,内核为每个哈希桶维护一组计数器,统计从该桶命中的转发决策一共处理了多少字节、多少数据包。直接从命令行看,它输出的是一长串无符号整数,很多第一次接触的人会误以为能和ifconfig、/proc/net/dev对得上。实际上它更像一本内部账本,关心的是“路由查找层今天的工作量分布”,而不是“某块网卡收了几个包”。

文件每一行对应内部哈希桶索引,而不是接口编号。所以当你看到某个数字特别大时,正确解读是“某个缓存桶承担了很集中的查找压力”,而不是“某个接口流量爆了”。我见过有脚本把rt_acct的每一列硬编码成网卡的出入流量,最后数据对不上,查了很久才发现方向从一开始就错了。另一个需要留意的是,这个文件是否存在、列数有多少,很大程度上取决于内核配置和版本。在 2.6.x 到 3.5 的主流老内核上通常能看到,某些裁剪过的定制内核也可能直接没有。上手的第一步永远先ls /proc/net/rt_*确认一下。

1.2 rt_cache 是此刻路由缓存表的一张“现场照片”

/proc/net/rt_cache则完全不同,它把当前内存里所有有效路由缓存条目原样导出,每一行对应一个struct rtable对象。直接cat /proc/net/rt_cache能看到带表头的文本,典型字段包括Iface、Destination、Gateway、Flags、RefCnt、Use、Metric、Expires等。其中Destination/Gateway/Iface描述一条具体的转发路径,Use表示这个条目被命中了多少次,Expires表示距离过期还有多久。由于是实时快照,同一个目的地址可能因为缓存过期重建而反复出现,也可能因为 GC 回收而突然消失。想看更友好的表格,老命令route -C和ip route show cache读的其实也是这份缓存数据,只是把字段格式化展示。这个文件适合做瞬态观察,不适合单独用来做长时间统计,统计要去看/proc/net/stat/rt_cache,这是后文会重点说的坑。

2. rt_cache 为什么要存在:一次命中能省一次最长前缀匹配

2.1 一次转发,怎么就从查表变成了缓存命中

先描述常规路径。内核收到一个需要转发的 IPv4 包,第一步做合法性检查,第二步做路由查找,找到匹配的 nexthop 和输出设备。这里的“匹配”是在路由表上做长前缀匹配,路由表通常用多级 trie 组织,每查一次要经过若干次内存访问;在路由条目多、报文速率高的情况下,每个包都完整查一遍 table 是很大的开销。路由缓存就是在这个环节插入的一层加速:把“查表得到的结果”按照目的地址、源地址、TOS 等特征摘要成 key,存进一个哈希表。后续报文先查哈希表,命中就直接复用预先算好的转发结果,只有 miss 才回到路由表做完整查找。

这个设计很好理解,就像读书时遇到生词去翻词典,翻得多了,常用词自然就背下来了;下次再遇到同一个词,不需要再翻词典,大脑直接给出释义。路由缓存的命中代价通常远小于 trie 查找,所以理论上缓存命中率高时,转发路径可以明显变短。注意这里的缓存不是永远不失效,它依赖过期时间和 GC 来回收,下一节继续展开。

2.2 条目的生命线:创建、命中、过期与 GC

一个条目什么时候被创建?常见触发点是第一个未命中缓存的数据包,它在完成路由表查询后,会把rtable对象填入哈希表。条目里的关键字段,例如RefCnt是内核对象引用计数,Use是查询命中计数,两者不能混用。Use增长说明近期有流量在反复使用这条路径,RefCnt高说明还有其他内核对象正持有着它,比如正在处理的 socket 或 netfilter 引用。过期与回收由内核 GC 机制控制,老内核中常见参数包括net.ipv4.route.gc_thresh、net.ipv4.route.gc_timeout、net.ipv4.route.max_size。当缓存条目数逼近gc_thresh,GC 会加快扫描;超过max_size后触发更激进的回收,甚至可能拒绝创建新条目,这在压力测试中表现为转发性能突然断崖。

手动清空整张缓存表的命令是ip route flush cache。这条命令在很多老调优文档里被当成“万能刷新”,实际上它会把所有rtable一起回收,清空瞬间所有流量都会 miss,紧接着重新触发大量路由查找和缓存重建。如果使用频率过高,可能造成间歇性 CPU 尖峰和转发延迟抖动。后面排障章节会再展开“什么时候才应该手动 flush”。

2.3 命中率怎么量化:别对着一张快照做判断

要判断缓存到底起没起作用,不能只看/proc/net/rt_cache的条目多不多,而要看/proc/net/stat/rt_cache里的计数器。这个统计文件包含in_hit、in_miss、out_hit、out_miss、entries等字段,代表进入和离开两个方向上的命中次数与未命中次数。和rt_cache快照不同,这些是累计值,需要做两个时间点的差分。脚本可以直接读取并计算,核心逻辑就几行:

cat /proc/net/stat/rt_cache

比如第一次采样in_hit=1000000, in_miss=1000,10 秒后第二次采样in_hit=1100000, in_miss=1600,那这段时间内 miss 增量是 600,命中增量是 100000,命中率约 99.4%。如果 miss 增量比例长时间大于 10%,说明缓存收益很低,可能原因包括访问目的地址过于分散、有大量并发新建连接、或者 GC 阈值设置太紧导致条目活不过一个报文周期。至于优化方向,前者取决于业务流量模型,后者可以调整 sysctl。这一套逻辑放在今天的新内核上不适用,原因是路由缓存整体被移除,具体变化放到第 5 章细说。

3. rt_acct 的实操读法:把一串大数字变成监控数据

3.1 先看原始输出,再决定怎么解析

rt_acct不像普通文本文件那样有清晰表头,直接cat出来就是一行行的数字。如果你对自己机器上的格式没把握,最稳妥的方式是用od按 64 位无符号整数来读:

od -An -tu8 /proc/net/rt_acct

这样能看到比较规整的数字序列,也不会把多列数字误读成十进制字符串。旧内核中每一行通常对应一个缓存哈希桶,数值里包含字节数和包数的累计,具体排列方向(先入后出还是先出后入)受内核版本和编译选项影响。所以我的建议是:先观察,再解析,不要在没确认格式前就写死列号。

如果只想快速看一个总量,可以这样按列累加:

awk '{for(i=1;i<=NF;i++) total[i]+=$i} END {for(i=1;i<=NF;i++) printf "col_%d=%s ", i, total[i]; print ""}' /proc/net/rt_acct

这段脚本会把每一列数字分别求和,输出col_1=... col_2=...。它不在于给每个列强行安一个业务含义,而是先让你拿到文件整体的数量级。如果总量级比/proc/net/dev的流量小很多,很可能你采样的方向本身就不对,或者文件里只有转发流量计数,不包含本机收发流量。后续再做更细的语义推断。

3.2 一个轻量采样脚本:把 rt_acct 变成每秒速率

要拿rt_acct做趋势监控,正确姿势是定期采样差分。我写过一个很简单的 Python 脚本,只依赖标准库,逻辑就是把两次快照逐列相减,再除以时间间隔,输出每秒字节数和每秒包数:

#!/usr/bin/env python3 import time import sys def read_rt_acct(path): with open(path) as f: return [int(x) for x in f.read().split()] def main(path=sys.argv[1]): prev = read_rt_acct(path) prev_ts = time.time() while True: time.sleep(5) cur = read_rt_acct(path) cur_ts = time.time() delta = cur_ts - prev_ts diffs = [b - a for a, b in zip(prev, cur)] print(f"{time.strftime('%H:%M:%S')}", end=" ") print("rates:", " ".join(f"{d / delta:.0f}" for d in diffs)) prev, prev_ts = cur, cur_ts if __name__ == "__main__": main()

脚本里第几列对应什么,留给你先确认文档再填列号;如果某些列出现负数,说明计数器发生过清零或文件格式比你预期的更复杂。这种脚本的好处是零依赖、可快速验证;坏处是缺少持久化,单机看一眼可以,若要接监控系统,建议直接把差分结果输出为key=value给采集端。速率异常时,可以用perf top或top -H看看softirq是否高,再对照/proc/net/stat/rt_cache判断是查找 miss 导致,还是单纯流量增长。记住rt_acct是哈希桶维度的统计,不是接口维度的用量统计,要报“eth0 每秒多少包”还是去看/proc/net/dev更直接。

3.3 rt_acct 和 /proc/net/dev 的区别,一张表说清

对比项/proc/net/rt_acct/proc/net/dev
统计目标路由缓存哈希桶的工作量网络接口收发统计
关键维度缓存桶索引、出入向、累计字节/包接口名、收/发、错误、丢包
主要用途分析路由查找压力、哈希分布看网卡性能、入/出流量、丢包
常见误区把列号当成接口号当路由查找压力指标
采集建议差分后看趋势直接读取按差分得到速率

表格背后核心逻辑是职责不同。查路由查找瓶颈,优先看 rt_acct 与 stat/rt_cache;查网卡瓶颈,看 /proc/net/dev。实际线上问题往往两者同时飙高,这时应该先看是哪一层先触发。比如软中断高但 netdev 丢包不明显,而 rt_acct 某些列异动明显,多半是路由层压力大;反过来 netdev 的 dropped 持续涨,优先查环形队列、中断合并和设备驱动。

4. 用 rt_cache 排查转发问题的三个经典现场

4.1 现场一:miss 比例长期偏高,缓存形同虚设

现象很好发现:/proc/net/stat/rt_cache里in_miss增量比例很高,entries却一直上不去,rt_cache里翻来覆去就那么几个条目。这种场景下路由缓存没有起到应有的加速作用,每次报文几乎都在完整查表。原因要分几类:一类是访问模型太分散,比如大范围的端口扫描或 CDN 节点轮询,命中的时间窗太短,刚建好就过期;另一类是策略路由或策略规则变化频繁,导致缓存 key 中的 mark、优先级等字段不断变化,原本可以复用的条目无法命中;再有一类是 GC 参数设置太紧,比如在低内存设备上把gc_thresh调得很低。

排查时先连续采样统计文件,确认 miss 不是偶发。再用ip route show cache | head -50看一眼条目里Expires普遍是多少,如果大部分快到零点几秒,说明缓存基本是“秒建秒删”,调大gc_timeout和gc_thresh可能直接把命中率拉起来。但调大也会增加内存占用,并有把攻击流量打进缓存的风险,生产环境要先做容量估算,别一把梭。若发现策略路由导致的 key 抖动,优先从路由规则收敛入手,而不是动缓存参数。

4.2 现场二:缓存条目数爆炸,软中断 CPU 一路涨停

另一种更典型的现象是rt_cache行数从几千涨到几十万,/proc/net/stat/rt_cache里entries持续高位,同时系统软中断 CPU 接近 100%。常见诱因是来自外部的随机源地址或随机目的地址扫描,每个包对应一个全新缓存条目,哈希表不断增长,GC 回收速度追不上创建速度,最终大量 CPU 时间消耗在缓存链表的插入、查找和释放上。遇到这种情况,先别急着调参数,应该尽快确认流量来源和特征。

排查指令可以参考这样:先把表头打出来确认列位置,再统计目的地址 Top N:

head -1 /proc/net/rt_cache cat /proc/net/rt_cache | awk 'NR>1 {print $2}' | sort | uniq -c | sort -rn | head -20

如果 Top 地址换来换去、每个地址的出现次数都不高,基本可以断定是非正常流量。临时缓解手段是ip route flush cache,但要注意这会瞬间清空所有条目,紧接着所有流量 miss,极短时间里 CPU 可能更高,并且很快又会塞满新的条目。更有效的措施是从源头限速或加访问控制,从根上断绝无意义的新条目产生;如果是老内核设备被逼到这个程度,升级并放弃路由缓存往往是最彻底的解法。

4.3 现场三:路由抖动导致缓存反复重建,转发时通时断

第三种问题看起来很像故障,但根因不在缓存本身:同一个目的地址在rt_cache里的Gateway字段或者Expires不断变化,Use计数不连续,业务侧表现为时通时断。这时候要意识到rt_cache只是转发决策的结果表,上游路由协议收敛慢、ECMP 路径切换、邻居表重建,都会让缓存的条目变得不稳定。排查顺序建议是先ip route get <目标地址>看当前实时路径,再ip route show看路由表是否一直变化,然后看邻居表和 ARP 是否正常,最后才回到缓存层。

如果确认是路由表本身在抖动,那问题在动态路由协议或物理链路,清 cache 只是暂时把旧路径清掉,新路径很快又会写入,症状会反复。正确的做法是先把路由收敛问题解决,或者通过策略路由固定下一跳,再观察缓存是否稳定。这里有一个很容易犯的错误:看到rt_cache里条目变来变去,就直接脚本循环ip route flush cache,结果路由表抖动被放大,问题更难看。缓存是结果,不是原因,这条判断标准在排障时特别重要。

5. 新内核里 rt_acct 和 rt_cache 消失了,路由性能还怎么调

5.1 3.6 内核为什么删掉路由缓存

从 3.6 内核开始,Linux 删掉了传统路由缓存。这个决定不是没原因的:路由缓存虽然在“命中率高、流量模式集中”时能提速,但代价也很明显。第一,多核环境下缓存哈希表的锁竞争会掩盖 trie 查找本身的进步;第二,攻击者可以用大量随机目的地址制造哈希链冲突,把系统 CPU 拖进链表遍历和缓存释放的泥潭;第三,内存压力和 GC 逻辑让慢路径变得非常复杂。在高速转发场景中,缓存带来的收益被这些问题抵消甚至反超,内核最终选择去掉整个独立缓存层。

对应的用户可见变化很直观:/proc/net/rt_acct和/proc/net/rt_cache不再生成,route -C、ip route show cache这类命令在新内核上要么无输出要么报错,net.ipv4.route.gc_*、net.ipv4.route.max_size等参数写入时会提示找不到文件。路由查找回到纯粹的 FIB 查找路径上,配合数据结构优化,表现并没有变得更差,反而消除了缓存一致性和安全问题。这也是为什么你在新版系统上按老教程找不到这两个文件,不是系统坏了,是机制换掉了。

5.2 老调优文档失效,新内核上应该关注什么

如果你维护的是新内核,要做路由性能优化时,别再引用老文档里的缓存参数,重点应放在路由表本身和邻居子系统。比较有用的几个方向包括:确认路由表是否使用了合适的前缀聚合,避免出现大量离散的 /32 路由;调整net.ipv4.fib_multipath_use_neigh之类的多路径选路参数;关注net.core.netdev_budget与软中断预算,保证转发线程拿到的 CPU 时间足够;另外就是及时清理无效邻居项,避免邻居缓存成为新的瓶颈。路由缓存消失后,瓶颈往往就转移到 FIB 表、邻居表、netfilter conntrack 这三处。

新内核上若想观察路由查找行为,可以借助perf或者 tracepoint 看fib_table_lookup调用频率,但这些手段更偏研发;日常运维更多是直接看转发吞吐和延迟变化。相比之下,老内核的 proc 体系对运维友好得多,这也是很多老网工一直怀念的地方。既然文件不存在,就别硬造工具,改用流量压测与系统计数器组合来评估更实际。但也要现实一点:很多嵌入式设备、老版本发行版、定制电信设备仍在跑旧内核,rt_acct和rt_cache在那些系统上依然存在。我最近处理的一台老设备,sysctl 里net.ipv4.route.gc_thresh还被人从默认值改成 0,导致 GC 行为异常,整个转发面都受影响。所以这套知识没有过时,只要存量系统还在,排查能力就有价值。新旧机制最大的共同点是:先确认运行环境,再决定用什么工具。

6. 避坑清单与参数速查:老内核路由缓存调优的完整备忘

6.1 我踩过的三个坑

第一个坑是把/proc/net/rt_cache当成计数器用。它只是一份瞬时快照,同一时刻“正在缓存了哪些路由”,不是累计计数。真想看命中率要看/proc/net/stat/rt_cache,两个路径差一个stat,含义天差地别。第二个坑是脚本里定时执行ip route flush cache。我以前为了“让路由更新生效”加过这种任务,结果每次执行后都会带来一段 CPU 尖峰和转发抖动,后来发现手动刷新只应该在极少数紧急场景用,不能变成常驻操作。

第三个坑是拿到老文档参数后不校验直接改。老内核的gc_thresh、max_size等参数在不同版本里语义有细微差别,而且有大量定制内核会改默认值甚至裁剪参数节点。修改前先sysctl -a | grep route看现有值,修改后观察entries、in_hit/in_miss变化,再决定是否保留。今天新系统上这些节点全部不存在,直接照抄会得到完全相反的效果。如果让我给一个优先级,我会说“先确认环境,再改参数,最后再做批量操作”。

6.2 旧内核常用路由缓存 sysctl 速查表

参数名作用注意事项新内核状态
net.ipv4.route.max_size缓存条目数上限,超过后强制回收调大提命中率,但增加内存与攻击面已无效
net.ipv4.route.gc_thresh触发 GC 的条目数阈值太小会导致缓存“秒建秒删”已无效
net.ipv4.route.gc_timeout基础缓存有效期调大后旧条目留存更久,适合目的集中场景已无效
net.ipv4.route.gc_intervalGC 扫描间隔过低会频繁扫描,浪费 CPU已无效
net.ipv4.route.secret_interval定时更换哈希密钥间隔用于抗哈希碰撞,不要频繁改动已无效

这些参数只适用于带传统路由缓存的旧内核,且永远是“基于场景调优”。比如一个只访问少量目的地址的网关,适当调大gc_timeout能够把命中率提得很高;而一个流量模型就是海量短连接的场景,调大只会加速缓存膨胀,危害更大。先采样、后调参、再复测是唯一稳妥的流程。

真正帮到我的不是某一个参数,而是一直保留的排查习惯:拿到一台陌生设备,先查内核版本,再看/proc/net下有哪些相关文件,然后才考虑用哪套工具。rt_acct和rt_cache这对组合,一个负责记账,一个负责快照,背后靠的是stat/rt_cache的累计计数来评价健康度。现在新内核的机器越来越多,传统路由缓存越来越少见,可只要你接触到一台还在跑 3.5 之前内核的设备,这套方法就依然能派上用场。希望这篇能帮你少翻几次文档、少踩几个我踩过的坑。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询