1. VPP FIB 表项到底是什么,为什么值得单独拆开看
刚接手 VPP 转发面的人,十有八九会在show ip fib的输出里迷路过:一个前缀下面挂着unicast-ip4-chain,再往下是dpo-load-balance,再往下是一串ipv4 via ...,中间还夹着locks:[API:1, ]、flags:[drop]、fib_index:0这些看着眼熟但说不清用途的字段。等到线上真出问题,比如路由明明配了却不通、表项数一夜之间涨了几十万、删路由提示成功但表里还在,能救命的恰恰是对 VPP FIB 表项内部结构的那点理解。这篇就把这条表项从数据结构、字段含义、写入路径到排查手法完整拆一遍。适合两类人:一类是刚开始用 VPP 做转发面、需要读懂show输出的新手;另一类是已经在跑现网、需要定位表项异常和规模瓶颈的老手。文中涉及的命令和代码都可以直接拿去复现,涉及参数的地方我会把估算过程写清楚,而不是甩一个数字让你照抄。
1.1 别把 FIB 当成一张哈希表
很多人第一反应是:转发表嘛,不就是“前缀 → 下一跳”的映射,用哈希表存一下,查的时候最长前缀匹配就完事。这个思路在表项规模小、前缀长度固定的时候能跑,但放到 VPP 的场景里立刻塌掉。原因有三个:第一,最长前缀匹配本身不是等值查询,10.0.0.0/8、10.1.0.0/16、10.1.1.0/24三条表项存在覆盖关系,哈希表要么退化成线性扫描,要么得为每个前缀长度建一张表再逐级回退,查一次包要打好几次表;第二,VPP 的设计目标是把一次查表压缩进几十个时钟周期,任何“查不到就回退一层”的分支都是灾难;第三,真实网络里表项不是孤立的,一条路由背后可能挂着一个邻接、一个多路径组、甚至一个递归解析链,这些东西需要被共享、被引用计数、被增量更新,扁平哈希表做不到。
所以 VPP 的 FIB 是一套“对象图”。前缀是对象,路径列表是对象,单条路径是对象,解析出来的转发动作(DPO)也是对象,它们之间用索引互相引用,用引用计数管理生命周期。你看到的show ip fib输出,本质是把这张对象图按前缀维度重新排版打印出来。
理解这一点非常关键,因为它决定了后面所有的排查思路:当你说“表项不对”的时候,要先确认是前缀对象错了、路径对象错了、还是 DPO 对象错了。这三层的排查手段完全不同。
1.2 四个必须分清的索引:table_id、fib_index、node index、DPO index
新手最容易踩的坑是把table_id和fib_index当成一个东西。table_id是你配置时写的那个路由表编号,比如ip table add 100;fib_index是 VPP 内部为这张表分配的连续索引,从 0 开始,按创建顺序递增。绝大多数情况下两者不相等,除非你只建了表 0。表 0 的fib_index是 0,表 100 的fib_index可能是 1,也可能是 7,取决于中间建过多少张表又删过多少张。
这个区别带来的直接后果是:数据平面查表用的是fib_index,不是table_id。你在 API 里下发路由写table_id: 100,框架内部会先把它翻译成fib_index,再往 mtrie 里写。如果翻译这一步出了问题,比如表还没建就下发路由,或者表删了重建导致fib_index变了,数据平面就会查到一张空表,表现出来就是“路由配了但不通”,而show命令按table_id查又能看到表项,特别迷惑人。
node index是 FIB 对象图里的通用节点编号,fib_entry、fib_path_list、fib_path都挂在这套编号体系下,类型由fn_type区分。DPO index则是另一个独立的编号空间,专门给转发动作对象用。这三套编号互不相通,输出里出现的[@0]、[@5]这种方括号数字,就是 DPO index。看到两个不同的前缀都指向[@5],说明它们共享同一个转发动作,这是正常且被鼓励的,不是 bug。
1.3 一条表项从配置到转发要经过几道手
完整链路大致是这样:你通过 CLI 或 API 下发一条路由,框架先根据table_id找到或创建对应的 FIB 表,再为这个前缀创建或复用一个fib_entry对象。接着,来源信息被记录成一份fib_entry_src,挂到这个前缀的源列表里;同时根据你给的下一跳组合,去全局路径列表池里查有没有现成的组合,有就复用,没有就新建一个fib_path_list,里面每条fib_path记录一个下一跳。
路径确定后进入解析阶段:如果下一跳是直连的,就会去找对应的邻接(adjacency),找到后生成一个终结型 DPO;如果下一跳需要再查一次路由(递归场景),就会往下递归,生成一个带协议的聚合 DPO。DPO 确定后,表项被“编码”成一个 32 位整数,写进 mtrie 对应前缀位置的桶里。到这一步,数据平面才真正能查到它。
这条链路上任何一环卡住,表项都会停在中间状态。比如邻接还没建立,DPO 会是dpo-adjacency-incomplete,表项本身是存在的,show也能看到,但包过来直接丢。这就是后面排查章节要反复用到的判断依据。
2. 一条 FIB 表项的内部结构逐个字段拆解
要把排查做扎实,光会看输出不够,得知道每个字段是从哪来的。这一节按“键 → 来源 → 路径 → DPO”的顺序拆,配合真实的字段名,方便你回头翻代码或者对 API 结构。
2.1 键的三件套:前缀地址、前缀长度、表索引
fib_entry的键类型叫fib_entry_key_t,里面就三样东西:前缀地址(IPv4 是 32 位,IPv6 是 128 位)、前缀长度、所属表的fib_index。三者组合才能唯一确定一条表项。这意味着10.0.0.0/8和10.0.0.0/16是两条完全独立的表项,跟“同一个地址”没有任何关系,前缀长度是键的一部分,不是附属属性。
这里有个细节值得留意:默认路由0.0.0.0/0的长度是 0,它在 mtrie 里落在根节点上,是所有前缀的最终兜底。很多人排查“为什么流量走了默认路由”时,会去翻具体前缀,其实只要看根节点的桶里有没有 DPO 就知道了。另外,长度 0 和长度 32(主机路由)是两个极端,前者最容易被忽略,后者最容易被误建。相邻前缀的覆盖关系全部由长度决定,跟配置顺序无关——后配的短前缀不会“盖掉”先配的长前缀,数据平面永远选最长匹配的那条。
顺便说一句,IPv4 和 IPv6 用的是两套独立的 FIB 表结构和两套独立的 mtrie,table_id相同的 IPv4 表和 IPv6 表在内部是两个不同的对象。排查双栈问题时,一定要确认你查的是哪个协议族,命令里的ip和ip6不能混。
2.2 一个前缀挂多个来源:best source 是怎么选出来的
一条表项可以同时被多个来源“持有”。典型场景:接口上配了地址,框架自动生成直连前缀(来源interface);你又手工加了一条同前缀的静态路由(来源API或CLI);如果跑着动态路由协议,还可能有一条协议学来的(来源protocol)。这三份来源不会互相覆盖,而是作为三个fib_entry_src同时挂在表项的源列表里,每份都带自己的路径列表。
真正决定转发行为的是“最佳来源”(best source)。框架按来源类型的优先级排一遍,优先级高的那份路径列表胜出,它的 DPO 被装进表项。输出里的locks:[API:1, ]就是在告诉你,当前有哪些来源持有这条表项、各持有了几份引用。如果某份来源被撤销,框架会把它从列表里摘掉,然后重新选一次最佳来源——这个动作叫重新解析(re-resolve),是 FIB 里最频繁也最容易出问题的操作之一。
来源优先级的顺序由枚举定义,序号靠前的优先。实际用的时候我不建议你死记这个顺序,因为它在不同版本之间调整过。更靠谱的做法是:show输出里会直接打出当前生效的来源名,你看到什么就是什么;如果怀疑被覆盖,把可疑来源的那条路由删掉再show一次,看 DPO 有没有变,比记顺序可靠得多。
注意:删掉高优先级来源后,表项不一定消失,可能只是“降级”到次优来源。如果你删路由的目的是让流量断掉,务必确认降级后没有别的来源在兜底。
2.3 路径列表与 fib_path:控制平面的“路”怎么变成数据平面的“动作”
fib_path_list的存在是为了共享。假设你有 500 条路由,下一跳都是“出接口 A + 网关 B”,那么这 500 条表项会指向同一个路径列表对象,而不是各存一份。这个设计在表项规模大、下一跳重复度高的场景下能省下非常可观的内存,也是 VPP 在百万级表项下还能保持可控占用的关键。
每条fib_path记录的信息包括:出接口索引、下一跳地址、路径类型(普通、直连、本地、丢弃、递归)、权重、优先级。权重和优先级是多路径场景的核心——权重决定流量分配比例,优先级决定哪一组路径先被选中。很多人配了多路径但流量全走一条,八成是优先级没给对,或者权重给了但路径列表被判定为“不等价”而退化成了单路径。
路径类型里最容易搞混的是“直连”和“递归”。直连路径的下一跳在同一个二层域内,可以直接解析到邻接;递归路径的下一跳需要再查一次路由表才能确定出接口。递归层数在 VPP 里是有上限的,超过上限的递归会被判定为不可解析,表项降级成丢弃。这个上限不是拍脑袋定的,因为每多一层递归就多一次查表,转发性能会线性下降,而且递归环路会导致解析永远不收敛。
2.4 DPO:真正干活的转发动作对象
路径解析完之后得到的是 DPO(Data Plane Object)。常见类型有这么几种:dpo-drop(丢弃)、dpo-receive(上送本机)、dpo-ip-null(空接口,静默丢)、dpo-load-balance(多路径组)、dpo-adjacency(邻接,已完成重写信息)、dpo-adjacency-incomplete(邻接未完成,通常是 ARP 或邻居发现没解析)、dpo-punt(上送控制面)。show输出里那个[@N]就是 DPO 的索引。
判断表项健不健康,最快的方法就是看它的 DPO 类型。看到dpo-adjacency且带完整的重写信息,说明这条路是通的;看到dpo-adjacency-incomplete,说明二层解析还没完成,包过来会丢;看到dpo-drop或者dpo-ip-null,说明这是一条黑洞路由,配的时候可能就是这个意图,也可能是某个来源覆盖导致的意外,得结合上面说的 best source 一起看。
dpo-load-balance是个容器,它自己不做转发,而是把包分给某个桶(bucket),每个桶里又是一个 DPO。输出里的buckets:N就是桶的数量。桶的数量和路径的权重是关联的:权重 1:2 的两条路径,通常会展开成 3 个桶,或者按更大公约数展开成更多桶以获得更细的分配粒度。桶多了分配更均匀,但内存和构建时间也会涨,这是个需要权衡的参数。
2.5 表项编码:那个 32 位整数到底是什么
mtrie 的叶子桶里存的不是指针,而是一个 32 位整数,由表项编码函数生成。这个整数里塞了表索引、前缀长度、前缀的一部分比特,目的是让数据平面在命中前缀之后,直接拿这个值去索引后续结构,不用再回查一层映射表。这是一次典型的“用编码换访存”的取舍:多花一点编码逻辑,省掉一次内存访问。
具体位域怎么分,各版本之间有过调整,我不建议你在业务代码里硬解这个整数。真要做关联分析,老老实实用show输出里的前缀文本,或者走 API 拿结构化的前缀字段。硬解编码的代码在升级之后会静默出错,而且错得很隐蔽——值还能用,只是指向了别的表项,排查起来非常痛苦。
还有一个相关问题:mtrie 的桶里除了当前前缀的 DPO,还可能有“指向父节点”的链接。当一条更长前缀没有匹配时,查表会沿着这个链接回退到更短的前缀,直到找到有 DPO 的节点。这就是覆盖关系的实现方式。它意味着删除一条前缀时,不只是把桶清空,还要处理指向它的那些子节点的回退行为,这也是删除操作比添加操作更容易出问题的原因。
3. 实操:把 FIB 表项信息真正读出来
理论说完了,进实操。这一节给的是能直接粘贴执行的命令和代码,场景我按“先看全局、再看单条、最后批量拉”的顺序排,跟实际排查的节奏一致。
3.1 CLI 的几组常用入口
最基础的是看整张表:
vppctl show ip fib输出开头会有一行表头,类似ipv4-VRF:0, fib_index:0, flow hash:[src dst sport dport proto ] epoch:0 flags:none locks:[]。fib_index前面说过,是内部索引;flow hash是这张表做多路径分流时用的哈希字段组合,改了它会影响同一条流的走向;epoch是个自增计数器,每次表项集合发生变化它就加一,适合做增量同步。
看指定表:
vppctl show ip fib table 100看单条表项,这个命令在排查时最好用,输出集中不含干扰:
vppctl show fib entry 10.1.0.0/24看 IPv6 就把ip换成ip6。另外show ip fib summary之类带 summary 的子命令在不同版本里支持程度不一样,如果没有,用show ip fib | grep -c之类的组合也能凑合统计。
3.2 逐字段解读一段典型输出
拿一段简化过的输出来说:
10.1.0.0/24 unicast-ip4-chain [@0]: dpo-load-balance: [proto:ip4 index:1 buckets:2 uRPF:0 to:[0:2]] [0] [@5]: ipv4 via 192.168.1.1 GigabitEthernet0/0/0: mtu:1500 [1] [@6]: ipv4 via 192.168.1.2 GigabitEthernet0/0/1: mtu:1500unicast-ip4-chain表示这条表项走的是单播 IPv4 转发链,是正常的。[@0]是顶层 DPO 的索引,类型是dpo-load-balance,说明这是多路径。buckets:2表示两个桶,to:[0:2]大致的含义是这个负载均衡组包含 2 个成员,具体数字格式各版本略有差异,看桶的数量更直接。下面两行分别是两个桶的内容,[@5]、[@6]是桶内 DPO 的索引,ipv4 via ...后面跟着下一跳地址、出接口、MTU。
如果这里出现的是dpo-adjacency-incomplete,那基本可以断定是邻接没解析。查一下 ARP 或者 IPv6 邻居表,show ip arp看看对应网关有没有条目,没有就手工加一条,或者检查接口是不是 down 的。如果出现dpo-drop ip4,说明这条前缀当前的最佳来源给出的动作是丢弃,要么你配的就是黑洞路由,要么有什么东西把它覆盖了。
3.3 走 API 批量拉表项
CLI 适合看单条,表项上万条的时候就得走 API 了。Python 绑定的用法大概是:
from vpp_papi import VPPApiClient vpp = VPPApiClient(apidir="/usr/share/vpp/api/", apifiles=["ip.api.json"]) vpp.connect("fib-dump") entries = [] for dump in vpp.ip_route_dump(table={"table_id": 0, "af": 0}): entries.append(dump) for e in entries[:5]: print(e.route.prefix.address, e.route.prefix.len, e.route.n_paths) for p in e.route.paths: print(" nh:", p.nh.address, "sw_if_index:", p.sw_if_index) vpp.disconnect()af字段 0 是 IPv4,1 是 IPv6。n_paths是路径数,paths里每条带下一跳、出接口、权重、优先级。注意apidir和apifiles要指向实际安装路径,不同打包方式的路径不一样;如果连的是运行中的实例,客户端会尝试读取运行时的 API 定义,但指定文件更稳。
不想折腾 API 又想要结构化数据,还有个偷懒的办法:走命令透传拿文本再解析。
reply = vpp.cli_inband(cmd="show ip fib").reply这个方法胜在兼容性极强,任何版本都能用,缺点是输出格式可能随版本变,解析脚本要跟着维护。表项规模不大的时候够用。
3.4 多路径、递归、覆盖关系的识别技巧
多路径看dpo-load-balance和buckets,这个前面说过。要验证流量分配是否符合预期,别只看桶数量,要看桶里各路径的权重。权重不写在桶里,得回到路径列表那一层看,API 里的paths[].weight更清楚。
递归的识别方法是看路径的下一跳地址是不是不在直连网段内。如果下一跳需要再过一跳才能到,框架会做递归解析,输出里可能显示成一个中间 DPO 再指向最终 DPO。递归层级深的时候,输出会拉得很长,这时候用show fib entry看单条反而更清爽。要确认递归有没有收敛,直接看最终 DPO 是不是 adjacency,如果不是,说明递归链上有一环断了。
覆盖关系的识别,靠看长度。同一个地址段下不同长度的表项在输出里是平铺的,不会有缩进层级关系,得自己按长度排序判断谁覆盖谁。有个实用技巧:想看某个前缀实际生效的是哪条,直接拿一个落在里面的具体地址去show fib entry 10.1.1.5/32,如果这条针对于具体地址的表项不存在,输出会告诉你没有表项,而不是回退显示,所以这个办法不太灵。更可靠的是构造一个真实流量,或者在测试环境里用抓包确认实际走的出接口。
4. 表项写入、覆盖与回收的实操细节
会看了之后,下一步是理解“为什么表项会变成这个样子”。这一节讲写入路径上的选择、优先级冲突导致的覆盖、以及删除时的引用计数问题,这三块是实操中出问题最多的地方。
4.1 加表项的两种路径,参数怎么选
CLI 和 API 是两条独立入口,走的是不同的来源类型。CLI 上加静态路由大致是:
vppctl ip route add 10.1.0.0/24 via 192.168.1.1 GigabitEthernet0/0/0接口名能不能直接写取决于版本,老版本可能要写sw_if_index,用show interface查索引。API 上则用ip_route_add_del,参数结构里最关键的是table_id、prefix、paths三块,paths里每条要写sw_if_index、nh、weight、preference。
选哪条路径取决于你的场景。CLI 适合临时验证和调试,改完立刻能看到效果;API 适合程序化下发,尤其是需要批量同步或者和外部路由源对接的时候。但要注意,CLI 和 API 下发的表项来源类型不同,优先级也不同,同一前缀两边都配会触发 best source 竞争,最终生效的可能是你不想要的那条。生产环境我建议统一走 API,CLI 只用于临时排查。
还有个参数容易被忽略:is_multipath。这个标志决定框架是把它当成一组等价路径还是只取第一条。配了多条路径但没开这个标志,只有一条会生效,输出里buckets:1,看着像配错了其实只是标志没给。
4.2 优先级冲突导致的覆盖,怎么验证
前面说过 best source 的机制,实操里验证的办法就三招。
第一招,加之前先看。下发之前show fib entry看一眼当前状态,记下 DPO 索引,下发之后再show一次,对比 DPO 有没有变。变了说明生效,没变说明被更高优先级来源挡住了。
第二招,删掉验证。把怀疑的更高优先级来源删掉,再show,看 DPO 是否切到你要的那条。这个方法破坏性强,只在测试环境用。
第三招,看locks。输出里的locks:[...]会列出当前持有表项的来源,如果列表里出现了你没预期的来源名,那就是它在捣鬼。比如你只配了静态路由,但locks里有interface,说明接口上自动生成的直连前缀和你配的前缀撞了。
注意:直连前缀(attached)是自动生成的,接口配了地址、状态 up 之后就会出现。它和手工配的同前缀静态路由会同时存在,谁生效取决于优先级。如果你配的静态路由“不生效”,先怀疑是不是被直连前缀挡住了。
4.3 删表项为什么删不掉
“删了但还在”是最高频的疑问之一,原因通常有三类。
第一类是引用计数没归零。表项被别的东西持有,比如还有一条路由的递归解析依赖它,或者有个邻接对象挂在它上面。这时候删除请求会被记录成“标记删除”,等引用归零才真正回收。输出里的flags可能会出现对应的状态标记,locks里也能看到还有谁在持有。
第二类是删除来源不对。用 API 加的路由,用 CLI 删不掉,因为来源类型不匹配,删除操作只会去摘对应来源的那份,摘不到就什么都不做。这种情况得用同一条路径删,或者干脆把整张表清掉重建。
第三类是表本身还在被引用。删表之前必须先把表里的表项清干净,或者确认没有接口绑在这张表上,否则表删不掉,表项自然也就跟着留着。
排查顺序我一般是:先show fib entry看locks还有谁,再查有没有接口或路由表绑着这张表,最后才怀疑框架状态不对。绝大多数情况在前两步就解决了。
4.4 百万级表项的内存账
表项规模上来之后,内存估算必须提前做。VPP 默认的堆大小通常不够撑百万级 IPv4 表项,得在启动参数里调。粗略的估算方式如下:
| 对象 | 单条大致开销 | 说明 |
|---|---|---|
| fib_entry | 约 120 到 200 字节 | 含键、标志、来源列表头 |
| fib_entry_src | 约 40 到 60 字节 | 每个来源一份 |
| fib_path_list | 约 40 字节加共享 | 相同下一跳组合可共享 |
| fib_path | 约 60 到 80 字节 | 每条下一跳一份 |
| adjacency | 约 200 到 300 字节 | 含二层重写信息 |
| DPO | 约 32 到 64 字节 | 常与其他对象复用 |
| mtrie 节点 | 随前缀分布变化 | 每层固定槽位 |
按单路径、无多路径复用的最坏情况估算,100 万条 IPv4 表项大约需要 300 到 600 MB 堆空间,具体落点取决于前缀分布(前缀越分散,mtrie 节点越多)和邻接复用程度。IPv6 表项单条更大,因为键是 128 位,同样规模下占用通常是 IPv4 的两倍往上。
这个数字的意义在于:部署之前先按表项规模算一遍,把堆大小留出 1.5 倍余量,别等跑起来 OOM 再回头调,那时候重启代价很大。
5. 常见问题排查实录
这一节按症状整理,都是实打实遇到过的情况,每条给出判断依据和处理方式,最后附一张速查表。
5.1 表项在、命令能查到、但流量不通
这是最典型的一组症状,判断顺序按下面走。
先看 DPO 类型。dpo-adjacency-incomplete说明二层解析没完成,查 ARP 或 IPv6 邻居表,确认网关有没有学到。学不到的原因可能是接口 down、VLAN 配置不对、对端没响应,逐个排。
再看接口状态。show interface确认出接口是 up 的,show hardware-interfaces看底层链路。接口 down 但表项还在是正常的,FIB 不会因为接口 down 就删表项,只会让邻接失效。
再看表索引。确认你配的路由进的是数据平面查的那张表。方法是对比show ip fib表头里的fib_index和接口上绑定的fib_index,两者不一致就是表配错了地方。这个坑在多 VRF 场景下特别常见。
最后看 uRPF。如果开了反向路径检查,源地址在 FIB 里查不到对应路径的包会被直接丢,而且丢得很安静,抓包只能看到单向流量。show输出里的uRPF字段可以辅助判断。
5.2 表项显示不对,疑似被覆盖
先对比locks里的来源列表和你预期的是否一致。多出来的来源就是嫌疑人。然后看 DPO 是不是你想要的动作类型,如果是dpo-drop但你配的是正常转发,那肯定有东西覆盖了。
处理方式是二选一:把冲突来源删掉,或者把你这条路由的来源优先级提到更高。前者直接,后者需要确认框架支持你调整来源优先级,通常 CLI 和 API 两条路都能给到比较高的优先级。
还有一种隐蔽情况:前缀写错了。10.1.0.0/24写成10.1.0.0/16,输出里看着有表项,但和你以为的不是同一条,而且长前缀可能被短前缀覆盖。查的时候一定要把长度带上,只看地址会误判。
5.3 表项数量异常增长
突然涨几十万条表项,通常是三个原因之一。
一是路由抖动导致的重复下发。外部路由源反复增删同一条路由,每次删除没清干净,新的又加上,累积起来数量就爆了。判断方法是看locks里同一个来源的引用数是不是大于 1,大于 1 就是重复持有。
二是递归解析产生的中间表项。递归路由会在解析过程中生成辅助表项,正常情况下解析完成后会被回收,如果递归链断在半路,这些中间表项会残留下来。判断方法是看这些表项的前缀长度和地址是不是都很奇怪、不成规律。
三是邻接对象泄漏。每条新邻接都对应一份内存,邻接不回收,表项引用也就释放不掉。这个要看邻接表规模是不是也在同步涨。
处理上,短期的办法是清表重建,把异常表项一次性清掉;长期得找到重复下发的源头,把下发逻辑里的幂等性补上——同一条路由重复下发应该是“更新”而不是“新增一份”。
5.4 常见问题速查表
| 症状 | 优先检查 | 常见原因 | 处理方式 |
|---|---|---|---|
| 表项在但 ping 不通 | DPO 类型 | 邻接未完成、接口 down | 查 ARP、查接口状态 |
| 路由配了查不到 | table_id 与 fib_index | 表未创建、表索引不匹配 | 先建表再下发,核对 fib_index |
| 表项被意外覆盖 | locks 列表 | 直连前缀或更高优先级来源 | 删冲突来源或调整优先级 |
| 删路由提示成功但表项还在 | 引用计数 | 被其他对象持有 | 找到持有者先释放 |
| 多路径只走一条 | buckets 数量 | 权重或优先级配置问题 | 核对 paths 里的 weight |
| 表项数暴涨 | locks 引用数 | 重复下发、递归残留 | 清表重建,补幂等逻辑 |
| 递归路由不收敛 | 最终 DPO 类型 | 递归链断裂或有环 | 检查下一跳可达性 |
这张表我平时就贴在工位旁边,遇到问题先对一遍,能省掉大部分瞎猜的时间。表里的“优先检查”那一列是有顺序的,别跳着看,跳着看容易漏。
6. 规模上去之后才暴露的几个坑
前面讲的都是单条表项层面的东西,这一段讲规模效应。表项从一万涨到一百万,很多在实验室里完全正常的行为会突然变得很糟,下面这几条都是踩出来的。
6.1 mtrie 的槽位密度和前级分布的关系
mtrie 每一层固定槽位数量,前缀分布越稀疏,中间节点越多,内存占用越大,查表深度也可能增加。同样是 100 万条表项,全是 /24 的规整分布和随机长度的散乱分布,占用的内存能差出一大截。这也意味着:你在测试环境用小规模规整前缀压出来的内存数字,不能直接线性外推到生产。
判断方法是在接近真实的分布下测。如果拿不到真实前缀分布,至少构造几组不同长度比例的集合分别测一遍,取最差的那组做容量规划。另外,前缀分布还会影响表项编码之后的桶填充率,桶太稀疏意味着查表时要多走几层,虽然不至于错,但性能会掉。
6.2 邻接和表项的耦合带来的连锁反应
邻接对象不是孤立存在的,它被表项引用。这意味着邻接的一次失效会连带影响一批表项。典型场景:某个出接口的链路闪断,这条链路上的所有邻接失效,引用这些邻接的几千条表项同时降级成dpo-adjacency-incomplete,如果这时候正好在批量下发路由,两件事叠在一起,就会出现“表项数剧烈波动 + 转发大面积中断”的复合故障,比单一故障难定位得多。
我的做法是在接口状态变化和路由批量下发之间加个限流,别让两件事同时发生。具体实现方式看你的控制面架构,核心思路就是给邻接重建留出时间窗口,别让 FIB 在高频变更期间还要处理大量新表项。
6.3 怎么盯住表项变化
线上环境必须能感知表项变化,不然出了问题只能事后翻日志。三个层次的做法,按投入从小到大排。
最轻的是定时采样。每分钟show ip fib统计一次条数,画出曲线,数量突变立刻能看出来。缺点是只能看总数,看不出是哪些前缀变了,而且采样间隔内发生的抖动会被漏掉。
中间一档是走 API 的全量对比。定期拉一次全量表项,和上一份做差异比对,输出新增和删除的列表。这个方法能定位到具体前缀,代价是每次全量拉取对控制面有压力,表项大的时候要控制频率。
最重的是订阅变化事件。VPP 有一些机制可以在表项变化时发出通知,具体可用性取决于版本和编译选项,我这里不展开,因为这部分的接口在不同版本之间稳定性一般。如果要做严格实时的监控,建议在控制面自己的下发逻辑里埋点,而不是完全依赖数据面的通知。
6.4 我自己的几条经验
第一条,任何改 FIB 的操作,改之前先show fib entry存一份,改完再存一份,对比着看。养成这个习惯之后,很多“莫名其妙变了”的问题会自己浮出水面,不用再去猜。
第二条,别迷信“删除成功”的返回。删除接口返回成功只代表请求被接受,不代表表项真的没了。凡是要确认删除效果的场景,一律以show的结果为准。
第三条,表项规模大的环境,show ip fib全量输出慎用。这个命令会遍历整张表并格式化打印,百万级表项下执行一次可能卡住几秒甚至更久,期间转发面虽然不受影响,但控制面其他操作会被拖慢。要看单条就用show fib entry,要看统计就用计数,别为了看一眼总数把整张表打出来。
第四条,前缀长度一定要带上。我见过太多次因为省略长度导致的误判,10.1.0.0和10.1.0.0/24在 FIB 里是完全不同的东西,敲命令的时候手快漏掉一个斜杠,排查方向就全偏了。这条听起来最基础,但真到了着急的时候,出错的往往就是最基础的地方。