蓝图 · 135 字 · 1 分钟阅读

分组交换:网络如何承载任何内容

电路交换把带宽浪费在沉默上。分组交换把消息切成独立的小块,让网络忘掉这段对话。正是这一个转变,才有了互联网。

#TL;DR

1965 年之前,网络传输数据的方式和电话系统传输语音一样:预留一条端到端的线路,在通话期间一直保持,结束后再拆掉。Paul Baran 和 Donald Davies 独立提出了相反的方案——把每条消息切成小小的、自描述的块,各自独立地发送,到目的地再组装起来。网络变得无状态、可替换。所有聪明的工作都交给两端。五十年后,你发过的每一个字节仍然是这样传输的。

#电路交换:电话模式

在电路交换网络中,拨打一个电话会分配一条物理路径。你和对方之间的每个交换机都会为这次对话保留一部分容量,即便双方都不说话,这部分容量也不会被释放。

对于模拟语音来说,这样是行得通的。两个人用电话线的效率还算不错。但对计算机来说,这是灾难:

  • 一个终端发送一条命令只占用线路几毫秒,然后就空闲几秒甚至几分钟等着回应。
  • 建立连接需要真实的时间。在 1960 年代拨号并协商一条跨国电路可能要几十秒才能流出第一个字节。
  • 一个交换机故障就会让通话中断。会话没了,得重来。
  • 网络随并发连接数扩展,而不是随真正流动的数据量扩展。峰值计费,大部分时间空闲。

到 1960 年,计算机流量已经明显是突发性的。电路交换在为错误的目标做优化。

#Baran 的洞见:可生存性

Paul Baran 在 1960 年代初期就职于 RAND Corporation,研究美国空军非常迫切的一个问题:在一次核打击摧毁网络的大部分节点之后,如何让指挥网络继续运转?

Baran 的答案,以一系列 RAND 备忘录的形式在 1960 到 1964 年间发表,包含三部分:

  1. 冗余网状结构 — 足够多的备选路径,使得丢失任一节点、甚至许多节点,也不会把网络切成几块。
  2. 分布式路由 — 没有中央控制器。每个节点根据自己当前对邻居是否存活的判断,在本地决定如何转发每条消息。
  3. 消息块 — 把每条消息切成小的、均匀的片段。每个片段带着自己的头部,里面有源地址、目的地址和序列号。片段可以走不同的路线。目的地负责重新组装。

他称之为分布式自适应消息块交换。经营美国电话网络的 AT&T 告诉他这行不通。他们错了,但他们也不感兴趣。

#Davies 的洞见:效率

Donald Davies,在英国国家物理实验室(NPL),于 1965 年从完全不同的起点得到了同样的技术。他想的不是核战生存,而是分时计算机——为了一次交互会话,在一条跨越海洋的电路上保持打开状态是多么浪费。

Davies 的版本强调统计复用:如果你把每条消息切成小片段,再把它们交错放到共享链路上,你就能在同样的带宽里塞进多得多的对话。没人独占这条线。每个人按实际发送量获得相应份额。

Davies 还给了它那个保留下来的名字。他嫌 Baran 的”消息块”太笨重,选了一个更短的词:packet(分组)。

到 1967 年 ARPA 开始设计 ARPANET 时,Larry Roberts 已经读过 Baran 和 Davies 两人的工作,并围绕他们共同的想法构建了架构。

#一个分组的解剖

让其他一切都能工作的诀窍在于每个分组都是自描述的。你可以把一个分组交给一个从没见过你这段对话其他部分的路由器,它照样知道该怎么处理它。

大致上,每个分组有两部分:

┌───────────────────────────────────────────┐
│  Header(头部)                            │
│  ──────                                   │
│  源地址                                    │
│  目的地址                                  │
│  序列号                                    │
│  长度                                      │
│  校验和                                    │
│  (标志位、版本、TTL 等)                   │
├───────────────────────────────────────────┤
│  Payload(负载)                           │
│  ───────                                  │
│  最多 ~1500 字节,装什么都行               │
└───────────────────────────────────────────┘

头部是网络要读的内容。负载是网络会忽略的内容。这种分离本身就是全部诀窍。路由器看一个分组时,并不在乎负载是电子邮件、视频帧、数据库查询,还是三十年后才会被发明的东西。它看目的地址然后转发。

#乱序到达、丢失的分组

因为每个分组都独立传输,有几件事会发生,这在电路交换的通话里从不会发生:

  • 乱序到达。 分组 3 走了更短的路,比分组 2 先到。
  • 重复。 路由器重传了它以为丢了的分组;原来的那个也到了。
  • 丢失。 路由器的队列溢出;一个分组被丢弃,连声抱歉都没有。

网络不会试图修复这些。两端来做。

import random

def packetize(message, size=8):
    return [
        {"seq": i, "total": -(-len(message) // size), "data": message[i*size:(i+1)*size]}
        for i in range(-(-len(message) // size))
    ]

def simulate_network(packets, drop_rate=0.1):
    delivered = [p for p in packets if random.random() > drop_rate]
    random.shuffle(delivered)  # 不同路径、不同延迟
    return delivered

def reassemble(packets):
    seen = {p["seq"]: p for p in packets}  # 按序列号去重
    if len(seen) < packets[0]["total"]:
        return None  # 有东西丢了;请求重传
    return "".join(seen[i]["data"] for i in range(packets[0]["total"]))

msg = "Packet switching makes the network dumb and the endpoints smart."
received = simulate_network(packetize(msg))
print(reassemble(received))

这是端到端原则的缩影:可靠性不是网络的属性,而是两端如何处理网络给它们的东西。TCP,在 TCP/IP 中,就是这种模式的库版本。

#为什么这个抽象拒绝死去

自 1969 年以来,每一次重要的网络演进都替换了某些东西——链路层、传输协议、路由算法、地址方案——却没有替换分组交换。原因是分组交换是一种契约,而不是一种技术:

  • 网络不理解负载。 当应用变化时,网络不必升级。YouTube 流、SSH 和 BitTorrent 都跑在同一批 IP 路由器上。
  • 端点不知道拓扑。 光纤替代铜线,5G 替代 LTE,卫星链路加入进来——分组不在乎。应用也不在乎。
  • 状态住在边缘。 路由器不保留对话状态。丢了一台路由器,另一条路径吸收流量,没有会话被丢弃。这就是 Baran 的可生存性,也是为什么互联网从未有过一个可以被关掉的中央交换机。

电路交换对特定工作负载仍然存在——电信骨干网中的同步光纤网络、某些形式的 MPLS、数据中心在训练过程中在 GPU 之间建立的专用”通道”。但作为默认方式,分组交换在所有被尝试过的地方都赢了。

ARPANET 那篇文章开头有一张四节点示意图。只有当你明白节点之间的 IMP 在做什么时,这张图才有意思。它们在移动分组——读头部,忽略负载——而正是这个看起来平平无奇的选择,让一个为十二台计算机设计的网络,如今为一百亿台计算机工作。