← 全部文章

一名网络工程师的单元测试

当网络本身是唯一的判定基准时,你要怎么测试一个拓扑引擎?

Michel Wijnberg

测试一个拓扑引擎有一个别扭的性质:对它所做的大部分事情来说,你根本写不出预期答案

你可以拿自己编的一张图去给 Dijkstra 实现做单元测试。那只能证明你的 Dijkstra 确实是 Dijkstra。它完全不能说明:你从一份真实 LSDB 构建出来的那张图,是不是路由器真正在用的那 张图。而后者才是唯一重要的事,也是唯一可能以有意思的方式出错的地方。

于是问题变成了:判定基准是什么?


路由器就是参考实现

我最后定下来的答案把它拆成两半。RFC 2328 是规范:它规定协议应该做什么。实验室里的路由器 是行为判定基准:它们说明一个实现被问到时实际会做什么。Osprey 必须同时满足两者,而它们分 歧的地方,那个分歧本身就是发现。好处在于,这个判定基准就在那儿摆着,随时等你去问。

这样一来,验证就变成了一条流水线:

64 台真实路由器
      ↓  show ip route ospf  /  show ip ospf database summary
基准真相:每台路由器自己安装的路由表

Osprey 的重建结果,同一时刻,同一作用域

在四条互相独立的断言上比对全部 4,032 个有序对

这是那次比对中重建的那一侧:一台路由器自己的路由表,在 Osprey 里的样子:

lax1-gw1 的路由表:428 条路由,带类型徽标、度量值和 ECMP 下一跳对
lax1-gw1 的路由表:428 条路由,带类型徽标、度量值和 ECMP 下一跳对

428 条路由,按类型划分(C 直连、O 区域内、O IA 区域间、E2 外部),每条都带度量 值、通告路由器,以及对接下来这段很重要的完整下一跳集合,所以 172.16.2.31, 172.16.2.32 会作为它本来就是的双路 ECMP 出现,而不是显示成排序在前的那一个。

这四条断言比数量更重要:

断言为什么要单独列
度量值精确一致抓住那种恰好长度正确的错误路径
路径类型精确一致(intra / inter / external抓住用错误规则得出正确开销的情况
完整 ECMP 下一跳集合一致抓住只挑了一个合法成员就当成答案的情况
终止状态 = 在请求的目的地 delivered抓住提前停下、看起来却挺像回事的遍历

其中任意三条都可能通过,而模型仍然是错的。这不是假想。上一篇文章里那个缺陷 产生的路径端点是对的开销比真实值更低,而这恰恰是一次更懒的比对会放行的东西。

如今这四条在 4,032 对里全部通过了 4,032 对。我在意的数字不是 4,032,而是:我给自己 留出的、互相独立的失败方式的数量。


没有判定基准时,就做模糊测试

基准真相在你能去问网络的时候管用。它对问题的另一半不管用:当一台设备给你发来畸形的、被 截断的、或者干脆就是奇怪的东西时会发生什么。

Osprey 对协议解析器有一条绝对规则,而它是一条产品约束,不是代码质量上的愿望:**记录异常 并继续;绝不崩溃。**一个会因为一条畸形 LSA 而死掉的监控系统,恰好会在有意思的事情正在发 生的那一刻把你的可见性夺走。

你没法穷举厂商能怎么给你惊喜。所以要做模糊测试:

make fuzz                          # every target, 10s each
make fuzz FUZZTIME=60s             # longer
make fuzz FUZZPKG=./internal/isis/ # one package

这个仓库里当前的数量:

模糊测试目标110
测试文件419
基准测试98

110 个模糊测试目标不是虚荣指标。它大致对应每一个”攻击者或有缺陷的 agent 掌控着字节”的解析 边界:OSPFv2 与 OSPFv3 报文以及每一种 LSA 类型、IS-IS 的 PDU 与 TLV、BGP 消息与路径属性、 BMP 头部、SNMP varbind 解码。它们每一个都是这样一个地方:对”这台设备会给我发什么?“这个问 题,诚实的答案是”我完全不知道”。

基准测试的存在是出于相关的理由。协议解析器处在热路径上,是按”每报文零分配”写的,缓冲区用 sync.Pool,而带 b.ReportAllocs() 的基准测试是唯一能阻止这件事在下次有人加一句顺手的 fmt.Sprintf 时悄悄退化的东西。


那些需要真实数据库的测试

第三类测试对上面两种办法都免疫。Osprey 里比较难的查询是带聚合和多表连接的 CTE:“哪些设备 是活跃的但从未被轮询过”、“哪些区域被切分了”、“重建 T 时刻的图”。用 mock 数据库去测这些,只 能证明你的 mock 和你的预期是一致的。

所以这些测试通过 testcontainers 跑在真实的 PostgreSQL 上,而且项目规则写得很明确:带 CTE、 聚合或多表连接的查询必须有真实数据库测试,覆盖空集、NULL、边界条件,以及那条抓出最多缺陷 的:互斥性。如果一条查询把设备分进若干类别,那么各类别之和必须等于总数。它有多经常对不 上,以及这件事在一块”每个数字单看都很合理”的仪表板上有多不可见,都很惊人。


厂商不会把 RFC 念给你听

最后一类是任何内部测试都找不到的,也正是实验室挣回自己饭钱的地方。真实设备实现的并不是你读 过的那份标准。它实现的是与之相邻的某个东西。

从这个实验室里挑几个,每一个都实打实地花掉了时间:

**不存在的那份 MIB。**实验室 EVPN 服务 pod 里的 vEOS 叶子只能通过 OSPF 而不是 IS-IS 被发 现,理由一点也不体面:vEOS 两棵 ISIS-MIB 树都没实现。不是部分没实现,是完全没有。再正确的 SNMP 代码也修不好这件事;唯一正确的应对,是用一个它们确实支持的协议去发现它们,并把原因记录 下来。

先于标准的方言。MPLS L2VPN 的 pseudowire 有一份标准 MIB,RFC 5601。真实的 IOS 出厂带的 却是一套先于标准的 Cisco 方言。所以轮询器把两套都实现了,并且会自动回退。而由于”我们两套都 支持”正是那种会烂掉的说法,这两个驱动被一道对等性闸门约束着,让标准 MIB 那条路径不可能悄 悄偏离 Cisco 那条。

**被时代遗忘的密码学。**这里的路由器跑着两个 IOL 镜像(124 台是 IOS 15.2(4)S7,另外 68 台是 15.4(2)T4),而每一台都只提供 SHA-1 密钥交换和 ssh-rsa 主机密钥;15.2 那一批在此之上还最多 只支持 CBC 加密算法。一个原装的现代 OpenSSH 客户端会直接拒绝这种密钥交换,于是连接在还没轮到 认证之前就已经死了。Osprey 的 SSH 客户端显式地带着这些遗留回退项,因为另一种选择是:一个在 厂商幻灯片上能用、在客户真实资产上不能用的终端功能。

**从来没被启动过的那个服务。**这个实验室更早的一版 64 路由器构建里,到处都配了 ip ssh version 2,却从来没生成过 RSA 密钥,所以 SSH 服务器根本就没在跑,每一个节点上都 是,而且悄无声息。show ip ssh 显示 Disabled,而配置看起来是完整的。

这些没有一件是靠读规范能发现的。它们是把你的代码指向真实设备、然后看着它失败才发现的。


拓扑引擎究竟是什么

在搭建验证工装的中途,这件事的形状在我眼里清晰了起来:

输入:   杂乱、残缺、互相矛盾的网络状态
输出:   一个确定性的模型
判定基准:路由器自己

那差不多就是一个编译器。而用这种方式去想它,一个有用的推论是:编译器是按符合性来评判 的,不是按看起来说得通。没有人会接受”生成的代码看起来差不多是对的”。

我之所以能写一整个系列去讲一个拒绝猜测的产品,不是因为我特别有原则。是因为我有 64 台机器, 随叫随到地反驳我,还有一套一次性问遍它们的工装。

没有这个,“Osprey 算出的路径是正确的”只是一种意见。有了它,它是 4,032 比 4,032,在四条断言 上成立,在某个星期二的下午可以复现。

差别全在这里。


这是第 2 系列的最后一篇。第 1 系列, 三种 IGP,一张图零足迹逐跳的真相Osprey 拒绝猜测的东西,讲的正是这一篇拿去接受检验的 那些原则。