网络知识总库

Book Notes · 08 / 14 · 综合进阶

网络算法学
Network Algorithmics 精读笔记

Varghese 问的不是「协议规定了什么」,而是「线速到来时,实现凭什么跟得上」。第 2 版与 Jun Xu 一起把端节点的拷贝、路由器的查找与交换机的调度写成一套可以迁移的原则。本笔记只抽六条主脉:原则总纲、端节点瓶颈、定时器与查找、最长前缀、包分类、调度与 QoS。

作者 George Varghese / Jun Xu难度 实现向进阶建议用时 两至三周笔记形式 精华档 · 六主题

全书骨架:让实现跟得上链路

协议书告诉你包长什么样;这本书告诉你:在链路速率涨得比 CPU 与存储器快的年代,复制一次、中断一次、查错一次表,都可能成为系统瓶颈。作者把端节点与路由器看成同一类「必须在包间隙里做完作业」的机器。

原则:少做、早做、特化→ 端节点:拷贝与切换→ 定时器 / 精确匹配→ 最长前缀→ 多域分类→ 交换 · 调度 · 整形
FIG.0 — 从原则到器件:端节点与路由器共享同一套思维
瓶颈在实现,不在规范

参考实现按可读性写,线速实现按存储器层次与包间隙写。两者都合法,成本模型完全不同。

先问一次操作多重

拷贝、上下文切换、随机访存、锁,比「算法大 O」更容易先把系统打死。先称重量,再谈巧妙。

查找是路由器的税

每个包都要做转发查找,一部分还要做 ACL 分类。表结构选错,再快的交换矩阵也在空等。

调度是策略的牙齿

没有可实现的排队与整形,QoS 只是配置项。公平、时延和线速三者不能同时装成最天真的那一版。

第 2 版把可编程数据面、更当代的查找与调度实现写了进去, 骨架仍是「端节点算法学 + 路由器算法学」。 本笔记是精华档:六节对应原则、主机瓶颈、时钟与精确匹配、 最长前缀、多域分类、交换与 QoS。 安全、流量测量等后半本专题,原则相同,不在此展开。 读法建议:每读完一节,找一个自己系统里的热路径, 标出拷贝、访存、锁和排队各发生在哪, 原则只有落到具体路径上才算读过。 数据包实验室适合用来对照「一个包在设备里被查了几次表」。 产业地图则帮助把算法对应回芯片与设备分工。

01

十五条实现原则总纲

书把反复出现的手法收成大约十五条原则。不必背编号,要能按簇调用:少做无用功、把计算挪出关键路径、放松不必那么严的规格、借已有子系统、为速度保存状态。

系统簇:别在包路径上做能不做的事

避免明显浪费同一数据拷两次、同一字段解析两次、为罕见错误在热路径上加锁,都是先该删的 把计算挪开能预计算就不要在收包时算;能惰性求值就不要为用不到的字段付账;能批处理就不要来一个包切一次上下文 放松规格用概率、近似或软状态换确定性和精度;许多控制面目标并不需要每包绝对正确 借用现成部件网卡校验和、页表映射、散列表、CAM,先问系统里谁已经会做这件事 必要时加硬件当存储器带宽或确定性时延已经不是软件能买到的,就把查找和调度沉到器件里

实现簇:别被通用性和参考代码绑住

为热路径写特化例程,而不是维护一个能处理所有地址族、所有选项的巨型函数。参考实现的职责是讲清楚协议,线速实现的职责是服务期望情形。协议头和接口调用都可以传「提示」:把已经算过的流标识、下一跳或校验和结果往下传,避免下层再猜。优化期望情形,把罕见情形留在慢路径——这与「先让正确的事发生在快路径」是同一句话的两面。

算法簇:状态、局部性、度量

为速度保存状态:缓存最近的查找结果、记住流所属的队列、把慢路径的结论留给后来的包。该用算法时不要只靠硬件堆容量——压缩 trie、定时轮、亏空轮转,都是用结构换存储器。所有优化都要能度量:加一条巧妙路径却让期望情形变慢,就是负优化。局部性(同一流、同一前缀、同一队列)往往比新发明的最坏情况结构更值钱。

把十五条当成检查单来用,比当成口号有用。设计一个新转发路径时可以逐条问:热路径上有没有重复解析?哪一步可以预计算或批处理?规格是否要求了并不需要的精确?内核或网卡是不是已经做了这件事?参考实现的通用分支有没有漏进线速代码?接口能不能把上一层算过的提示传下去?期望情形有没有自己的特化例程?为了少访存,该缓存哪份状态?上线后用哪几个计数器证明它变快而不是变慢?能答完这些问题,原则就算读进手里了。

读第 2 版时可以把原则当成翻译器: 可编程交换机上的流水线表,是「把计算挪开」和「加硬件」; P4 里把流缓存进精确匹配表,是「为速度保存状态」; 控制面用近似计数做遥测,是「放松规格」。 器件换了,句子还能用。 这正是这本书比某一代芯片手册活得更久的原因。 原则不是加速插件:为速度加的状态会变成一致性负担, 放松规格在计费或安全场景里可能不可接受。 先写清「哪条路径、何种错误可容忍」,再选该用哪一簇。 后面五节都是在给这张检查单提供可以指认的实例, 而不是另一本算法竞赛题集。 指认得出实例,原则才算从目录走进实现。 只记住原则名称、指不出路径,等于没读。 先指路径,再谈器件名词。

少做→ 早做 / 懒做→ 放松规格→ 借部件→ 特化热路径→ 为速度留状态
FIG.1 — 原则不是口号:每一条都应能在后面的查找或调度章节里指认
易混淆:把十五条当成可以任意叠加上去的加速插件。原则会互相打架——为速度加的状态会变成一致性负担,放松规格会在计费或安全场景里不可接受。先写清「哪条路径、何种错误可容忍」,再选原则。
02

端节点瓶颈:拷贝与上下文切换

主机侧最先被链路速率打穿的,往往不是「协议逻辑」,而是数据从网卡搬到应用、以及内核为此付出的中断与系统调用。端节点算法学的第一课是给这两件事称重。

一次收包可能被拷几次

经典路径里,帧先进入网卡环形缓冲,再拷进内核套接字缓冲,再拷进用户缓冲;校验和若用软件再扫一遍,等于又读了一次。链路速率升高后,存储器带宽会先于 ALU 耗尽。对应原则很直接:能在拷贝的同时算校验和就不要分开走两趟;能把页映射给应用就不要再搬字节;能让网卡直接描述应用缓冲(一类「零拷贝」或内核旁路)就把内核排除在热路径外。

中断是最贵的函数调用

一包一中断在千兆以上会把 CPU 融化在进出内核的过程里。缓解手段按激进程度排列:中断合并(攒一批再叫 CPU)、轮询(NAPI 一类:忙的时候改抽网卡,闲的时候再听中断)、把协议处理从硬中断挪到软中断或专用线程。上下文切换的亲戚是系统调用:每个read /sendmsg 都是一次用户/内核往返。批量收发包、减少调用次数,往往比把单次调用本身再微优化更有效。

网卡 DMA→ 内核缓冲→ 用户缓冲‖ 每包中断→ 合并 / 轮询→ 批量系统调用
FIG.2 — 端节点的税:字节拷贝乘次数,中断乘包率

控制权转移也是瓶颈

除了数据面拷贝,还有「谁来处理这个包」的调度问题。每来一个包就唤醒一个进程,会把缓存跑冷;永远只在中断上下文里做完所有协议处理,又会让延迟不可控。书里把这类问题看成与拷贝对称的「控制转移」:能在同一条执行流里处理一串包,就不要为每个包重新找人。今天的用户态协议栈、DPDK 轮询线程,仍是同一条原则的当代写法。

网卡侧的卸载是「借用现成部件」的当代清单:校验和卸载、分段卸载、大型接收聚合、甚至把部分连接跟踪沉到器件。原则提醒两件事:卸载省的是主机周期,不是协议语义;卸载路径一旦与软件路径行为不一致(伪校验和、聚合边界、时间戳),排障会从「算法」变成「两条实现谁说了算」。端节点优化必须连测量口一起设计,否则加速和正确性会在同一周里打架。

拷贝网卡 → 内核 → 用户,每一跳都吃存储器带宽;合并校验和与拷贝是经典手法 中断按包收费;合并、轮询、软中断分担,都是在减少控制权转移 系统调用一次往返一份开销;批量收发比抠单次调用更有效 卸载把已有器件用起来;必须核对软件路径与硬件路径语义是否一致
易混淆:「零拷贝」常被说成拷贝次数为零。真实系统往往仍有 DMA 进主机内存这一跳;它省掉的是内核与用户之间的第二次搬移,或把校验和与拷贝合并。没有测量就宣称零拷贝,通常只是换了个会说话的驱动。
03

定时器与查找数据结构

主机要为海量 TCP 连接维护重传与保活时钟,路由器要为每包做精确匹配或流表查找。两者都要求:启动、停止、到期处理必须摊到接近常数,而不能按连接数线性扫描。

定时轮:用环形数组代替有序堆

若把每个定时器丢进按到期时间排序的堆,启动和取消都要付对数代价,到期再取堆顶。连接数到十万时,光维护时钟就先输了。定时轮(timing wheel)把时间离散成槽,到期时刻对轮长取模后挂进对应槽;时间推进一格,只处理当前槽里的链表。启动是把一项接到槽上,取消是从链表摘下,摊还代价接近常数。时间跨度很大时,用多层轮:低层转完一圈,向高层进位——和机械钟表是同一个主意。

精确匹配:哈希仍是主力

流表、ARP、连接控制块,多数是精确键。哈希在期望情形下是常数查找,但要为碰撞、重哈希和恶意输入留后路。硬件可以用 CAM / TCAM 做「一拍比完」,容量和功耗是它的税。软件侧则靠:好的哈希函数、每桶足够浅的链或开放寻址、以及把热点流钉在缓存里(又是「为速度保存状态」)。

有序堆实现直观,启动/取消对数级,不适合海量短定时器 定时轮时间离散化后 O(1) 挂摘;粒度受槽宽限制 分层轮用进位覆盖大动态范围,避免为远期事件准备巨型数组 精确匹配软件哈希 + 缓存;硬件 CAM;二者都要处理最坏情况

数据结构要匹配访问模式

定时器的访问是「插入、删除、按时间到期」,不是「按连接号随机查」——所以按时间分桶。精确匹配的访问是「这个键在不在」,所以按键分桶。把两种需求塞进同一棵树,会得到处处正确、处处不快的结构。书反复强调:先写清操作集合和期望/最坏代价,再选结构。

精确匹配的最坏情况同样要设计:攻击者若能制造大量碰撞,哈希会退化成链上的线性扫描。盐、种子定期更换、每桶深度上限、超出后改用更稳的结构,都是「期望情形走快路径、恶意情形走慢但有界路径」。硬件 CAM 把最坏情况买成一拍,换来的是容量与功耗;软件不能假装自己也有这种一拍,除非测量证明碰撞已被关进笼子。

TCP 重传、连接保活、邻居发现、流老化, 都会同时产生大量定时器。 实现上往往是「每条连接一个时钟结构」, 但调度器看到的应是按时间分桶的轮,而不是按连接扫描的循环。 把连接表和定时轮拆开, 取消定时器才不会变成「先找到那条连接再从堆里删」。

易混淆:定时轮的槽宽选得太粗,许多定时器挤在同一格,到期时又退化成线性扫描;选得太细,轮太大、缓存不友好。它不是「免费的 O(1)」,而是把误差预算和存储器预算显式化。
04

最长前缀匹配

转发查找不是精确匹配:目的地址可能命中多个前缀,必须取最长的那一条。这是 IP 路由可扩展的代价,也是路由器算法里被写得最透的一章。

问题为什么难

地址空间按前缀划分,短前缀覆盖大块,长前缀覆盖例外。线性扫表在线速下不可接受;普通哈希又不认识「前缀包含」。一比特 trie 从高位往低位走,命中节点可以记下「目前见到的最长前缀」,走到叶子或空枝就出答案。它正确,但对 32 位已嫌深,对 128 位更不可能按一比特走完。

用步宽和压缩换深度

多比特 trie 一次看若干位,深度立刻下降,但每个节点要准备 2 k 个指针,存储器膨胀,还要处理「前缀长度不是步宽倍数」的填充。路径压缩跳过只有一个孩子的长链;位图压缩(一类 tree bitmap / Luleå 风格)把稀疏子树收成紧凑数组,让一次缓存行装下更多决策。软件实现常把前 16 或 24 位做成直接索引(DIR-24-8 一类两级表):期望情形一到两次访存结束,把长前缀留给第二级。

一比特 trie→ 多比特 / 填充→ 压缩与位图→ 两级直接索引→ TCAM 并行比较
FIG.4 — 前缀查找的谱系:用深度、宽度和并行度交换访存次数

硬件与更新

TCAM 把「所有前缀同时比较、按优先级选出最长」做成一拍操作,更新和功耗是它的账单。软件结构则要同时回答两个问题:查找几拍完成,以及路由更新来时要锁住多少、复制多少。只优化查找不谈更新,会在收敛期把路由器自己打停。第 2 版还会把可编程数据面的表流水线看成「把多级查找拆到级联器件」的当代版本,原则未变。

IPv6 把问题从 32 位拉到 128 位,一比特 trie 的深度立刻不可接受,多比特步宽和直接索引的第一级也不能再按「前 24 位一张表」天真放大。工程上往往对常见长度做特化,把其余长度丢进更通用的慢结构——又是期望情形与罕见情形分家。默认路由、丢弃路由和「查不到」必须在结构里有明确出口,否则压缩节点会把未覆盖的地址悄悄送去错误的下一跳。

正确性命中的必须是该地址所能匹配的最长前缀,含默认路由 查找代价用步宽、压缩、直接索引把访存次数压到缓存能忍受的量级 更新代价收敛期不能锁死查找;常靠双缓冲或写时复制 IPv6深度不可按 32 位方案线性放大,必须对常见长度特化
易混淆:把最长前缀当成「选数值最大的前缀长度」就结束。实现上必须保证:查找路径上记下的候选,确实是该地址所能命中的最长者;填充、压缩和默认路由都可能让「看起来更长」的空枝骗过粗心的遍历。
05

包分类:多域规则如何线速命中

防火墙、QoS 分类、流表都在问同一件事:这个包匹配哪一条最高优先级的多域规则。域通常包括源/目的前缀、协议、端口范围。它比最长前缀难一个数量级,因为维度不再是一条数轴。

几何图像与朴素算法

两条前缀加两个端口范围,可以看成高维空间里的矩形;分类就是找到覆盖该点、优先级最高的矩形。线性扫描最清楚,也最慢。分层 trie 按域依次下降,简单但会因规则相关而膨胀;为避免回溯做的集剪枝(set-pruning)用空间换时间,最坏可以爆炸。网格 trie 一类结构专门修补「源、目的两棵前缀树如何交叉」的指针问题。

分解:先各自查找,再组合

交叉乘积把每个域的查找结果当成坐标,再查一张组合表——域一多,组合表就不可接受。递归流分类(RFC 算法,这里的 RFC 是一种分类方法名,不是请求评论)用多级交叉表逐级压缩中间结果,用存储器换确定的访存次数。HiCuts / HyperCuts 则对几何空间做切分,把规则桶切到足够小再用局部搜索。工程选择几乎总是:规则数、域的分布、能否接受更新时重建、以及有没有 TCAM。

线性 / 缓存规则少或热点明显时够用;最坏情况与规则数成正比 分层 trie按域下降,实现直观,相关规则会制造膨胀或回溯 交叉 / 递归把多域拆成几次精确查找加组合表,存储器压力换确定时延 切空间按决策树切几何,叶节点规则足够少再暴力

TCAM 与软件的分工

TCAM 天然适合「多域同时比较」,范围匹配却要拆成多条前缀,规则一拆就更占容量。软件分类器适合控制面或中等速率;数据面则把热点流记下第一次分类的结果,后续包走精确流表——又是「为速度保存状态」。分类错一次的代价往往是错误的丢弃或错误的优先权,所以慢路径的正确性不能用「近似」随便放松。

规则重叠时,必须先定义「命中」:是第一条匹配、最高优先级,还是最具体的矩形。三种定义会选出不同的规则,实现与配置界面必须用同一种。更新比查找更刺手:插入一条新 ACL 可能迫使交叉表或切分树重建。能接受「更新时短暂走慢路径」的系统,往往比追求无锁热更新却偶尔分错包的系统更安全。分类器的测试集里应包含重叠、缺口、默认丢弃和「只差一个域」的邻近规则,而不是只丢几条互不相关的样例。

多域规则的「维度诅咒」是实在的: 每多一个域,朴素交叉表就按该域取值数膨胀。 所以工程上先问: 有多少条规则、更新有多勤、能否接受重建、 以及是否允许把热点流钉死成精确匹配。 答完这四个问题,再在 trie、切分树、TCAM 之间挑, 比先挑一个论文名字再硬套流量更接近书的读法。

易混淆:把包分类等同于「再做一次最长前缀」。前缀只是若干域中的两类;端口是范围,协议是精确值,优先级还可能让短前缀压过长前缀。用单域 LPM 硬件硬套多域策略,会在范围和优先级上悄悄算错。
06

交换、调度与 QoS

查找决定包该从哪扇门离开;交换结构决定它何时能走进那扇门;调度与整形决定谁先走、走多快。QoS 若没有这三件套,就只是配置文件里的愿望。

交换:队头阻塞与虚拟输出队列

输入排队交换机便宜,但一个出不去的队头会堵住后面要去往空闲出口的包——这就是队头阻塞。虚拟输出队列(VOQ)在每个入口为每个出口各建一队,把「去不同门的包」拆开。接下来要解决的是匹配:每个入口每拍最多选一个出口,每个出口每拍最多收一个入口。iSLIP 一类迭代匹配用几轮请求/许可/接受逼近最大匹配,目标是在可实现的拍数里吃饱和流量,而不是求出图论最优。

调度:公平、时延、可实现性

严格优先高优先级有包就先送;实现最简单,低优先级可能饿死 加权公平按权重分享链路;理想 WFQ 跟踪虚拟时间,实现重 亏空轮转DRR 用每轮配额逼近公平,每包常数工作,工程上更常见 工作守恒只要还有包且出口空闲就送;对比「为了整形故意空转」

理想公平排队按「每个流应得的服务量」精细记账,线速下算不起;亏空轮转、加权轮转把公平放松成「每轮能拿走的字节配额」。原则再次出现:放松规格,优化期望情形,把真正要隔离的流才升级到更重的调度器。

整形:令牌桶与漏桶

调度管「出口此刻送给谁」,整形管「这条流长期能多快」。漏桶强调平滑,令牌桶允许突发(桶里存了令牌就能短时超过均速)。它们出现在入口监管、出口整形和切片带宽保证里。和拥塞控制不同,整形是设备主动推迟发送,即使出口空闲——所以它不是工作守恒的。配置时要把「承诺信息速率、峰值、突发尺寸」翻译成桶深和加令牌的速率,而不是只写一个百分比。

输出排队交换机实现简单、没有队头阻塞,但要把所有入口的突发都吞进出口存储器,线速端口一多就先在缓存上破产。组合输入输出排队(CIOQ)用内部加速比缓和两端的压力,加速比做不到理论值时,匹配与缓冲仍然要一起设计。信用或反压则防止上游把下游的 VOQ 灌死。读这一章时不要只记住算法名字:问清排队发生在输入还是输出、匹配几拍一轮、整形是否工作守恒,才能把 QoS 从幻灯片还原成会丢包的机器。

把第六章读回原则总纲: VOQ 是「避免明显浪费」(别让队头挡住去往空闲门的包); DRR 是「放松规格」换可实现的公平; 令牌桶是「为速度保存状态」(桶里的令牌就是未来发送权); 内部加速比是「必要时加硬件」。 调度章节看起来像排队论, 其实仍是同一套实现思维在出口上的投影。

合上书可以带三句口令出门: 第一句,先称量拷贝、访存和切换,再谈大 O; 第二句,查找结构必须同时回答查找拍数和更新代价; 第三句,没有可实现的排队与整形,QoS 只是配置愿望。 第 2 版换了一些器件名词,这三句没有过期。

若要把书读成可以动手的地图,建议按这条作业链走: 先给自己的收发包路径数清拷贝次数和中断次数; 再实现一个哈希定时轮,对比有序堆在十万时钟下的开销直觉; 然后用一棵多比特 trie 解释一条默认路由加一条更长例外; 最后把一条五元组 ACL 画成几何矩形,看它和 LPM 差在哪。 不必复现书中每张结构图, 但这四步能把原则从形容词变成你能指出瓶颈的位置。 做完再回头看十五条,会发现它们其实只有几簇: 少做、挪走、放松、借用、特化、留状态。 能用这六簇给自己系统里的热路径贴标签,精华档就算读完。 贴不上的地方,就是下一轮该回原书细看的章节。 原则是地图,原书的结构图才是街道。

分类→ 监管 / 令牌桶→ VOQ / 匹配→ 出口调度→ 可选整形
FIG.6 — QoS 数据路径:先认出是谁,再决定能否进、何时出
易混淆:把「队列很长」一律当成带宽不够。可能是匹配算法让某些 VOQ 饿死,可能是严格优先抢走了低优先级,也可能是整形在出口空闲时仍在憋包。先分清排队发生在输入、输出还是整形器,再谈加带宽。

读后自测 · 8 题

1. 网络算法学里的「原则」解决的是协议问题还是实现问题?
实现问题。协议已经规定语义; 原则处理的是:在包间隙、存储器层次和器件约束下, 如何少做、早做、特化,以及何时该加状态或加硬件。
2. 端节点上,为什么「再快一点的协议处理」往往不是第一瓶颈?
因为字节可能被拷多次,中断和系统调用按包率收费。 存储器带宽与控制权转移常常先于协议状态机把 CPU 吃完。 先减少拷贝和切换,再谈更聪明的处理逻辑。
3. 定时轮相对有序堆的关键交换是什么?
用时间离散化和按槽挂链,把启动/取消从对数降到接近常数, 代价是粒度误差,以及槽太粗时单槽链表退化。 分层轮再用进位覆盖大的时间跨度。
4. 最长前缀匹配为什么不能直接用普通哈希解决?
一个地址可能同时命中长短不同的多条前缀,必须选最长者。 普通哈希只回答精确键是否存在,不理解包含关系。 要用 trie、多级表或能并行比较并按优先级选出的硬件。
5. 包分类比最长前缀难在哪里?
它是多域的:前缀、范围、精确值混在一起,还带优先级。 几何上是高维矩形点定位。 单域 LPM 结构和算法都不能直接套用,范围匹配还会把 TCAM 规则拆爆。
6. 虚拟输出队列解决了输入排队的什么病?
队头阻塞:一个出不去的队头挡住后面要去往空闲出口的包。 VOQ 按出口拆队,再靠匹配算法在每拍选择无冲突的入口—出口对。
7. 为什么工程上更常见 DRR,而不是理想 WFQ?
理想加权公平排队要维护精细的虚拟时间,每包工作重。 亏空轮转用每轮配额逼近公平,每包常数开销, 符合「放松规格、优化可实现性」的原则。
8. 令牌桶在出口空闲时为什么仍可能不发?
整形不是工作守恒的:即使链路空着, 没有令牌也不能超过约定速率(突发由桶深限制)。 它主动推迟发送,和「有包就送」的调度器职责不同。 配置时要把承诺速率、峰值和突发翻译成加令牌的速率与桶深。