秒速五厘米,要多久才能传达到你
樱花下落的速度是每秒五厘米,一句话要多久才能传达到你心里。
打开一个古早的网页,标题会先出现,正文跟着铺开,图片的位置留着几块空白。 右上角的小圆圈继续转,它在等待。
一个页面往往由 HTML、样式、脚本、图片等多个资源组成。 它们可能需要分别请求,可能来自不同服务器,也可能有些已经躺在缓存里。 有些请求能并行进行,有些得等浏览器发现新的资源才开始。 屏幕上像是同一个页面,网络上却有许多不同进度的工作。
我们习惯把这些等待压缩成一句话:网不好。
但“网不好”到底是哪一处不好?服务器还没处理请求?域名没解析出来?包在路上排队? 某个包丢了,正在重传?带宽明明很高,为什么游戏里的一次操作仍然迟迟没有回应?
我们来逐渐把这个模糊的“网”,拆成可以观察、计算和解释的机制。 我们先跟着一个网页请求走,路上的会带出一个个概念。
2. 出发之前,先知道去哪里 樱花离开枝桠时,是没有方向的随风而行,而人们的旅途大多都在出发时便有了目的地。
浏览器拿到的是一个 URL,例如 https://example.com/notes。 其中 example.com 是主机名,/notes 标识所请求的资源。 浏览器还需要找到可用于通信的地址。
域名、IP 地址、端口号,看上去都像“地址”,但它们承担的工作不同。
域名是应用使用的名字。IP 地址参与网络层的寻址与转发。 端口号则帮助传输层区分通信端点:到达同一台主机的数据,可能分别属于网页服务、邮件服务和其他应用。
如果借用物流的语言,名字帮助我们指认目的地,网络地址帮助系统组织递送,端口进一步区分到达之后该交给哪个通信端点。不过这张地图不能画得太死:一台设备可能拥有多个 IP,一个名字也可能对应多个地址,IP 还可能改变。它不是刻在机器额头上的永久身份证。
DNS 就在这里出场。它是一个分布式、分层的命名系统,保存不同类型的资源记录。查询主机地址只是其中一种用途。
一次典型的冷缓存解析中,主机向 recursive resolver 请求答案,resolver 再向 root、TLD 和 authoritative server 逐步查询。有些回复提供最终记录,有些告诉它下一步该问谁。一个参与者不必保存全世界的名字,也能参与找到答案。机制可参见 RFC 1034。
这张图表现的是典型查询关系。主机请求递归服务,resolver 对外逐步查询;不要把它理解成所有服务器都一路替主机递归。
缓存让很多旅程提前结束。resolver 如果已经保存了有效记录,就不必从根重新问一遍。TTL 限制记录可以缓存多久,因此一次 DNS 更新也不会瞬间改变所有地方的旧答案。
这里藏着一个很普遍的系统设计问题:重复查询可以省掉,但省下的工作意味着要暂时接受旧状态。DNS 里的 TTL 正在管理这种时间差。
还有一个需要留住的边界:DNS 可以使用 UDP,也支持 TCP。完整 DNS 实现需要支持 TCP,所以“DNS 用 UDP”只能作简化介绍,不能当成排他关系。RFC 7766
3. 找到你了,但我不知道要怎样说话 樱花落地,路途抵达,我遇到你,却不知该说些什么,只有耳边一片一片樱花落下。
知道地址之后,应用需要一套共同的表达方式。 请求怎样开始?字段是什么意思?哪里是内容? 对方说“找不到”,究竟是资源不存在,还是消息格式有问题?
协议便把这些问题变成规则:消息的格式、含义、发送次序,以及收到消息之后应采取的动作。
HTTP 是其中一套。客户端用 method、目标资源和 headers 等信息组织请求,服务器用 status code、headers 和内容组织响应。语法让双方能解析消息,语义让双方知道解析出来的东西该怎样处理。
协议很像软件接口的契约。调用者不需要知道服务器内部怎样查数据库,服务器也不需要知道用户为什么突然想读这篇文章。双方先约定边界上的行为,内部才有更换实现的空间。
互联网的分层把这种分工继续向下延伸。应用层关心消息的意义,传输层提供进程之间的通信,网络层组织跨网络递送,链路层处理相邻节点之间的传输,物理层用信号承载比特。
这是封装关系,并不是一次网页加载的完整时间线。一个应用消息也未必恰好装进一个 segment。分层允许我们暂时只处理某一层的任务;它并没有消除其他层的限制。
HTTP 本身也在节约等待。持久连接让多个请求复用连接;缓存让满足条件的响应可以复用;conditional request 可以检查已有版本是否仍然有效。所谓 stateless,描述的是协议不会自动替应用维持跨请求会话,不意味着网站不能保存登录状态。Cookie 等机制可以帮助应用关联请求。
就像随着时代变化,表达的方式的蝶变,现代 HTTP 提醒我们,协议栈会演化:HTTP/1.1、HTTP/2 通常运行在 TCP 上,HTTP/3 则使用基于 UDP 的 QUIC。因此 UDP 自身没有提供可靠字节流,并不妨碍上层协议实现可靠传输。HTTP Semantics
4. 带宽很高,为什么还是要等?
想象一条传送带。加宽传送带,可以让更多货物同时通过; 而把仓库搬近,才能缩短货物必须走的距离。
网络里也有类似的区分。一个包长为 $L$ bits,链路速率为 $R$ bits/s,把整个包送上链路所需的 transmission delay 是: $$ d{\text{trans}}=\frac{L}{R} $$
信号还得沿链路传播。如果距离是 $D$,传播速度是 $v$,propagation delay 是:
$$ d{\text{prop}}=\frac{D}{v} $$
提高带宽会缩短前一种时间,却不会直接让信号跨过同一段距离的速度变快。传送带类比在这里解释的是“装载速度”和“运输距离”的区别;网络实际传输的是信号,不是一直保持形状的货物。
一个节点上的延迟还包括处理与排队:
$$ d{\text{nodal}}= d{\text{proc}}+d{\text{queue}}+d{\text{trans}}+d{\text{prop}} $$
其中排队最有脾气。当前这个包要等多久,取决于前面有多少工作,也取决于工作怎样到来。平均流量相同,均匀抵达与突然一拥而上,可能有很不同的等待体验。
这就是网络和运筹学、制造系统相遇的地方:包、顾客和待加工工件,都可能在有限服务能力前积累。
为了看清一种机制,我们借用一个简化的 M/M/1 排队模型:到达是 Poisson process,服务时间独立且服从指数分布,只有一个服务台,队列容量无限;当到达率 $\lambda$ 小于服务率 $\mu$ 时,稳态平均系统停留时间为:
$$ W=\frac{1}{\mu-\lambda} $$
这里的 $W$ 包括排队和服务。若 $\mu=100$ 次/秒,$\lambda=80$ 次/秒,则 $W=50\,ms$;若到达率增加到 $95$ 次/秒,则 $W=200\,ms$。这些是模型计算值,不是实测网络数据。MIT:M/M/1 Queue
现实链路未必满足这些分布假设,有限缓冲也会使部分数据被丢弃。但这个模型带回一个重要观察:容量接近耗尽时,随机波动更难被消化,等待可以增长得非常快。
因此,“还没有达到满负载”不等于“等待已经足够短”。 下载吞吐量高,也不等于一次交互的 latency 低。
5. 未寄到的信 从前路途遥远,寄信靠着驿站,一站一站,一月一月。 在收到回信前,是不知道信有没有寄到的。
等待已经很长久,包还可能损坏、丢失、重复或乱序。 IP 提供 best-effort delivery;如果应用需要可靠通信,就需要某处增加相应机制。
我们先从最简单的情况开始。发送方发出一个包,接收方检查它,再返回确认。 发送方等到确认,才发送下一包。这叫 stop-and-wait。
如果数据损坏,checksum 可以帮助检测。 如果接收方发回 ACK,发送方就多得到了一条关于远端状态的信息。
但 ACK 也在网络里走。
假设数据已经抵达,ACK 却丢了。发送方等不到消息,只好重传;接收方又收到了一份一模一样的数据。它该把这当作新消息,还是旧消息的重复?
序号在这里承担身份信息。 同一数据的重传保持相应编号,接收方才能识别重复,避免再次交付。 计时器则在迟迟等不到确认时触发后续动作,让发送方不必永远停在那里。
到这里,checksum、ACK、sequence number、timeout 不再是一排待背的单词。它们分别处理不同的信息缺口。Checksum 也有漏检可能,不能保证发现所有差错,更不等同于密码学认证。
这段机制与分布式系统的关系很近:发送方无法直接读取接收方的内部状态,只能观察收到的消息。 没有回应,可能是请求没到,可能是回应没到,也可能只是晚了。
超时是一种行动依据,不能单凭它证明远端究竟发生了什么。
可靠传输在规定的故障模型下处理字节交付,应用还要判断业务操作是否完成。数据被 TCP 接收,与服务器已经保存文件,是不同层次的成功。
6. 链路忙了八微秒,接着等三十毫秒
Stop-and-wait 把事情做得井井有条,可惜链路大部分时间都没事做。
取课程中的一组参数:速率 $1\,Gbps$,包长 $8000\,bits$,单向传播延迟 $15\,ms$。 忽略排队、处理、丢包以及 ACK 的发送时间。
发送一个包需要:
$$ \frac{8000}{10^9}=8\,\mu s=0.008\,ms $$
往返传播时间约为 $30\,ms$。发送方忙于发送数据的时间比例是:
$$ U=\frac{L/R}{RTT+L/R} =\frac{0.008}{30+0.008} \approx0.000267 $$
也就是约 $0.0267\%$。这里的 RTT 表示往返传播时间,数据包的发送时间另行计入。
链路发得飞快,协议却每次都请它休息半天。效率低的原因已经从硬件转到了工作组织方式。
制造业里的流水线提供了一个合适的结构类比。第一件工件还没走完整条生产线,第二件已经开始加工。网络也可以让多个包同时处于“已发送、尚未确认”的状态,这就是 pipelining。
它提高并发程度,却不会自动缩短第一个包的传播时间。就像增加生产线上的在制品,能减少设备闲置,却不会凭空消除每一道工序。
Window 限制允许存在多少未确认的数据。它也要求发送方保存重传所需的信息,要求协议管理更多序号和状态。在无损、固定 RTT、ACK 开销可忽略的理想模型中,窗口为 $Wp$ 个包时:
$$ U\approx\min\left(1,\frac{Wp(L/R)}{RTT+L/R}\right) $$
另一个有用尺度是 bandwidth-delay product:
$$ BDP=R\cdot RTT $$
在刚才的例子中,它为 $30\,Mbits$,即十进制的 $3.75\,MB$。它估计了维持链路忙碌需要允许在途的数据量,精确条件还与包发送时间等因素有关。
窗口太小,链路可能等确认;窗口过大,在瓶颈已经忙碌时,多出来的数据可能只是排队。流水线让并发成为可能,控制机制还得决定并发到什么程度。
7. 窗口里少了一块,怎样补回来?
多个包同时在途之后,故障处理也变得更复杂。
发送方发出 0、1、2、3,其中 1 丢了,2、3 都到了。我们采用丢弃乱序包的经典 Go-Back-N 模型:接收方保留连续进度,收到后面的包仍然重复确认 0;发送方超时后重传尚未确认的 1、2、3。
Selective Repeat 则保存更多状态。接收方缓存并分别确认 2、3,发送方重传缺失的 1。缺口补齐后,接收方再按序向上交付。
GBN 简化了接收方状态,但可能重传已经抵达的数据。SR 减少这类重复工作,却需要缓存、分别确认以及管理更多计时信息。课程讲义允许 GBN 的乱序缓存有实现差异,因此比较之前要先说清采用的版本。
序号也不是越短越省事。假设 SR 的序号只有 0、1、2、3,窗口大小却为 3:接收方交付了 0、1、2,但对应 ACK 全部丢失。它的新接收窗口已经包含 3、0、1,发送方却还在重传旧 0。
昨天的 0,正好穿着明天的 0 的衣服。
接收方无法仅凭这个编号判断它属于哪一轮。标准 SR 模型中,发送与接收窗口大小相同为 $Wp$,序号空间大小为 $M$,需要满足:
$$ M\ge2Wp $$
这个限制让当前可接受的新编号与仍可能重传的旧编号能够区分。看到公式之前,先看到混淆发生在哪里,它就不再像一条突然落下来的考试规定。
顺便记住 ACK 的语义约定:这里 GBN 的 ACK(n) 确认到包 $n$;TCP 的 ACK 通常指下一个期望字节的序号。相似的名字,不能代替检查定义。
8. TCP:端点怎样根据反馈调整行动?
TCP 把可靠传输、连接管理、流量控制与拥塞控制组合起来。它向应用提供有序字节流,应用仍然需要规定消息从哪里开始、到哪里结束。RFC 9293 §2.2
发送两次,并不意味着对方恰好接收两次。recv(1024) 表示这次调用最多取这么多字节,不保证取得 1024 字节,更不保证取得一条完整消息。应用可以使用长度字段、分隔符或其他 framing 规则。这里可以回头读课程的 socket 示例:短消息很容易让不完整的接收处理暂时显得没有问题。
TCP 按字节编号。若一段数据从序号 1000 开始,长度为 500 字节,且此前数据已经连续收到,那么累计 ACK 可以是 1500:下一个期望字节在这里。这个例子只讨论数据,SYN 和 FIN 也占用序号空间。
窗口还受到两类不同限制。接收方缓冲有限,应用可能来不及读取,这需要 flow control;途中链路与缓冲有限,多个发送方还在共享资源,这需要 congestion control。
rwnd 表示接收窗口,cwnd 表示拥塞窗口。简化地说,未确认数据量受 $\min(rwnd,cwnd)$ 约束;能够新发送多少,还要减去已经在途的数据,并考虑其他实现限制。
从反馈控制的角度看,发送方调整窗口,网络产生延迟、ACK、丢包或拥塞标记,发送方再根据这些信号调整下一步。经典 TCP 的 slow start、congestion avoidance 等机制,可以放在这个闭环里理解。AIMD 的逐步增加与遇到特定拥塞信号时的乘性降低,是一种重要的控制思路;不同 TCP 算法及不同丢包检测路径会有不同规则,不能一概写成“丢包就减半”。RFC 5681
这个类比带回来的关键是反馈延迟。发送方收到的信息描述的是稍早的网络状态;它调整行动之后,也要等一段时间才看见结果。其他连接又在同时行动,所以网络不是一个固定不变的被控对象。
计时器也需要面对变化。TCP 的重传超时计算会考虑平滑后的 RTT 及其波动,避免把一次偶然的延迟当成稳定规律。超时太短可能产生不必要重传,太长则使真正丢失的数据等待更久。RFC 6298
可靠性要求记住过去发生了什么,效率又要求在尚未得到完整反馈时继续行动。 窗口和计时器就在这两种要求之间工作。
9. 远处的山,与眼前的下一站
端点管理着传输,包还需要穿过多个网络。
Routing 负责建立或更新路径信息,forwarding 则依据已有规则处理眼前的包。可以把它们理解成制定交通规则与执行一次转向的区别,但网络规则会随拓扑和协议状态变化,不能把它当成一张永远固定的路线图。
IPv4 的 CIDR 前缀描述地址范围。转发表可能同时匹配多个前缀,最长前缀匹配选择最具体的一项。例如 10.1.2.3 同时匹配 10.0.0.0/8 和 10.1.0.0/16 时,后者更具体。找到下一跳之后,当前节点还要把包交给本地链路。
一个很值得停下来想的问题是:访问远程服务器,为什么电脑可能先查询默认网关的 MAC 地址?
因为远程 IP 是跨网络的目的地,默认网关是当前这一跳的递送对象。ARP 可以帮助 IPv4 主机在本地链路上获得下一跳的 MAC 地址;它不会替你查询远处服务器的 MAC。IPv6 使用相应的邻居发现机制。
在没有 NAT 或隧道等变换的普通转发路径上,IP 目的地址通常保持,IPv4 的 TTL 则逐跳减少;链路层封装随着所在链路改变。NAT 又增加了一种边界:它可能改写地址与端口,并保存映射状态。
以太网交换机通常学习源 MAC 所在端口,按目的 MAC 转发;目的未知时可能在相应广播域内泛洪。路由器通常按 IP 前缀工作。两者都是“把数据送出去”,但依据的信息和服务的范围不同。
往后学习 link-state 与 distance-vector 时,可以继续追问:一个节点究竟掌握全局拓扑,还是只知道邻居报告的距离?不同的信息结构,会带来不同的计算方式、收敛过程与故障表现。
10. 等待樱花落下,信件送达。
回到开头。文字已经出现,图片还在等。
现在我们知道,等待可以发生在许多地方。名字的答案可能仍在查询;连接可能正在建立;服务器可能尚未完成处理;响应可能在瓶颈前排队;某个丢失的片段可能正在重传。也可能已有资源命中缓存,另一个资源却必须从远处重新取得。
甚至“不等了”也有机制上的含义。缓存减少重复获取,持久连接减少反复建立连接,流水线减少等确认时的闲置。它们分别省掉不同的工作,效果不能互相替代。
屏幕上的小圆圈没有解释它在等谁。学完这些机制,我们至少可以沿着消息走过的路,开始寻找答案。
而我们在等待什么? 是等待樱花落下,信件送达; 是等待雪花飘下,又冷又暖; 还是等待心意的能传达给TA呢。