专业书籍精读 · SRE · 第 19 章
Site Reliability Engineering · Ch 19 · Google · 2016
你在手机上点开一个全球网站——邮箱、视频、跨国电商——这个请求最后落在某台机器上。凭什么是那一台?Google SRE 这本书的第 19 章讲的就是这件事:在请求还没进机房之前,流量是怎么被分派的。它不是「在机房前面摆一台设备」那么简单,而是三层各管一段的接力。
把它想成一家全球连锁餐厅。你要吃上饭,得走三步:① 先问路——「离我最近的门店在哪」;② 按导航过去,路上的岔路口决定你最终进了哪家店;③ 进门后,迎宾把你分到某张桌子。三步各自对应一层机制,而每一步能知道的信息、能反应的速度都不一样,所以谁都替不了谁。
你以为你在问餐厅总部,其实你问的是小区门口的「问路大爷」——你的手机并不直接问网站,而是委托一个中间人去问。于是总部只知道大爷站在哪儿,不知道你站在哪儿;大爷可能在两百公里外,你就被指去了一家远门店。
更麻烦的是,大爷会把答案记在小本子上,下次照着念。所以总部想临时关掉一家店,得等所有大爷的本子过期——这一层反应慢,慢到没法用来救急。
第一招,让大爷替你报个大概位置:他去问路时顺口加一句「我是替住城东那片的人问的」,总部就能指得准些。代价是大爷的本子得按片区分开记,本子变厚。
第二招,让一个门牌号在很多城市同时有效:全世界用同一个地址,路网自己把你送到最近那家。好处是不用问路也能就近,还能一撤了之;坏处是这个「近」按路网算,不一定真快,而且你走到半路万一路网改道、把你甩到另一座城市的同名门店,你刚点的单就作废了。
第三招,进门分桌子要「算」不要「记」:门口不止一个迎宾。如果靠账本记「谁坐哪桌」,换个迎宾接手就断了;改成按你的名字算出一张固定的桌子——不管谁接待你,算出来都是同一张。这样迎宾之间不用对账,坏掉一个也不影响。还有个配套动作:上菜别绕回迎宾台,后厨直接端到你桌上。因为端出去的菜,比你报的那句菜名重得多。
粗的事交给问路(按大区分流),细的事交给门口(真正挑机器)。诚实的代价:按名字算座位省掉了记账,可门店真的增开或关掉一家时,还是会有一小批人的位子被重算、手上那一顿被打断。
流量分派是三层接力:问路那层(DNS)只能粗、慢地按大区指方向,路网那层按「近」把你送进某个机房,真正挑机器的是机房门口那层——而它的诀窍是靠算不靠记,并且让回程绕开自己。
想进到具体机制、记号和示意图? → 切到精读版
你以为前端负载均衡(load balancing)是「在机房前面摆一台设备」,其实它是一条三层接力、且没有任何一层能独当一面的链条:DNS 只能做粗粒度、反应迟缓的地理分流;路由(任播 anycast)只能按网络拓扑意义上的「近」投递;真正把一条连接钉在一台后端上的是 VIP 层的网络负载均衡器——它的核心手艺是用无共享状态的一致性哈希代替连接表,并让回程流量绕开自己。
8.8.8.8)提供。权威服务器看见的是它,不是用户。本章属全书第 III 部分「实践」里的负载均衡二连:Ch19 管流量进机房之前(DNS、任播、VIP),Ch20 管进了机房之后(子集化、加权轮询、跛脚鸭);再往下是 Ch21「处理过载」——因为分派做得再好,也总有分不掉的那一刻。对应现实里的一整类系统:CDN 与全球接入层、云厂商的全球负载均衡器、自建机房前那一排四层 LB。
本章开篇立的论是「光有算力不解决问题」:机房再多、机器再强,也不等于用户被服务好。原因有三层。
70–90 毫秒,跨太平洋约 100–160 毫秒,而一次 TLS 1.2 完整握手要 2 个往返。接入点选错一次,等于在页面打开之前就白送用户一两百毫秒。不解决会怎样?两种病症:冷热不均——某站点排队到 p99 飙升、另一站点 CPU 三成闲着;以及更难受的撤不下来——站点要维护或已出故障,却没有任何旋钮能在几分钟内把流量挪走,因为决定权散落在全世界的 DNS 缓存里。
做法最朴素:权威 DNS 按查询来源返回不同地址,或一个域名挂多条 A 记录做轮询。便宜、通用、不占数据面,但本章把它的三道硬墙摆得很清楚。
第一,你看不见用户,只看见解析器。权威服务器几乎从不与用户直接对话,中间隔着递归解析器。你只知道解析器的地址,既不知道它背后有多少用户,也不知道那些用户离它多远。补丁是 EDNS0 的客户端子网扩展(后标准化为 RFC 7871,2016):解析器在查询里带上用户 IP 的一段前缀(如 /24),权威服务器就能按用户而非按解析器决策。代价有三:只有实现了它的解析器才有效;答案从「一份」变成「按子网各一份」,打碎解析器的缓存;还交出了用户地址的一部分,是公认的隐私折扣。
第二,你控制不了缓存。TTL 只是建议——解析器会缓存,有的会把过短的 TTL 抬上去,客户端和应用层还各有一层缓存。所以想把一个站点摘出去,你等的是最长的那份缓存过期,而不是你设的 TTL。压到几十秒能改善反应速度,代价是每次过期多一次解析往返(直接加进用户感知的首字节时间),以及成比例增长的查询量。
第三,你控制不了客户端怎么挑。返回多条记录时,客户端通常用第一条,也可能自己排序或并发尝试;而 DNS 响应本身有尺寸约束(传统 UDP 回包 512 字节,要靠 EDNS0 才放得开),能塞的地址数有限。
结论很克制:DNS 只适合粗粒度的地理分流,别指望它做真正的均衡——它既感知不到实时负载,也收不回已经发出去的答案。
把同一个 VIP 从多个站点通过 BGP 同时宣告出去,互联网的路由自己把用户的包送到其中一个。好处立竿见影:不必按地域发不同的 DNS 答案,不受缓存拖累;站点故障时只要撤掉宣告,路由在秒到分钟级自己收敛到别处——这是这一层最实用的应急开关。
权衡在哪?BGP 眼里的「近」是路径属性上的近(跳数、策略偏好),不是延迟上的近,你可能被送去一个绕路的站点。更硬的一条是连接稳定性:若路由在一条 TCP 连接活着的时候变化,后续包落到另一个站点,那里没有这条连接的任何状态,用户拿到的是重置。实践上靠两件事缓解——站点内多台负载均衡器做成无状态、同构、算得一致(见下一节),以及在接入层放全局代理:任一站点都能把请求转给该去的后端,「换了站点」于是不再必然等于「连接作废」。
VIP 不属于任何一张网卡,而由一组网络负载均衡器共同宣告;上游路由器用 ECMP 把包撒到其中任意一台,每台再决定这个包交给哪个后端。难点只有一个,但很硬:TCP 是有状态的,同一条连接的每个包都必须交给同一个后端,否则连接立即作废。两种做法:
Google 的 Maglev 把两者叠加:一致性哈希作地基(无状态、随时可重算),连接跟踪表只作上层优化——在后端集合真的变化时保护已建立的连接。
为什么非得是一致性哈希,普通的 hash mod N 不行?算一下就清楚:后端从 10 台加到 11 台,取模的结果几乎全变——只有约 1/11 ≈ 9% 的流碰巧落回原处,约 91% 的连接被打断。一致性哈希把这个数字倒过来:只有约 1/11 的流需要搬家,其余原地不动。代价是它只保证「谁都算得一致」,不保证「分得均匀」——偏斜要靠虚拟节点(或 Maglev 那种预生成的查找表)压下去。
选好后端还得把包送过去。两条路,代价完全不同。
NAT:改写包的目的地址。代价是回程必须再绕回 LB(否则客户端看到的源地址不对),于是 LB 同时背上行和下行。而下行远大于上行——请求几百字节、响应几十 KB 到几 MB,量级差常在 10 倍以上——LB 就成了带宽瓶颈,还得为每条流保状态。
封装 + DSR:把原包整包裹进一个新 IP 包(GRE 或 IPIP),外层目的地址是后端;后端解封装后看到的仍是原始目的地址(VIP 配在它的环回接口上),处理完直接把响应发回客户端,不经过 LB。于是 LB 只处理上行,容量按「上行包速率」算而不是「总带宽」算,横向扩起来便宜得多。
代价要写清:外层头吃掉 MTU(IPIP 20 字节、GRE 24 字节起),路径 MTU 发现一旦被中间设备掐断,症状就是「小请求好、大请求挂」这类最难查的故障;后端必须配好 VIP 且不能应答它的 ARP;LB 看不到响应,观测得另建一套。
表 1 · 三层分派手段:各自能做什么、代价是什么
| 手段 | 分派粒度 | 撤走流量要多久 | 能否感知真实负载 | 主要代价 |
|---|---|---|---|---|
| DNS | 站点 / 大区级 | 受最长缓存约束,分钟到小时 | 不能 | 缓存不可控;看见的是解析器不是用户;客户端怎么挑也不可控 |
| 任播 + BGP | 站点级 | 撤宣告后秒到分钟 | 不能(只看路由) | 「近」≠ 低延迟;中途改路可能重置连接 |
| VIP + 一致性哈希 | 单台后端 / 单条连接 | 改后端集合,立即生效 | 能(健康检查 / 权重) | 只管四层;后端集合变化时仍有少量连接受损 |
表 2 · 一条流怎么钉在一台后端上:连接表 vs 一致性哈希
| 连接跟踪表 | 一致性哈希 | |
|---|---|---|
| 状态 | 每条流一记录,几百万流即 GB 量级 | 无状态,随时可重算 |
| LB 重启 / ECMP 改撒 | 表失配,连接断 | 不受影响 |
| 后端 10 → 11 台 | 老流仍有效(这正是它的价值) | 约 1/11 的流被重映射 |
| 负载是否均匀 | 天然均匀 | 靠虚拟节点 / 查找表找平 |
| 工程定位 | 一致性哈希之上的优化层 | 地基 |
表 3 · 包怎么递给后端:NAT vs 封装 + DSR
| NAT | 封装 + DSR | |
|---|---|---|
| 回程路径 | 必须绕回 LB | 后端直连客户端 |
| LB 要背的带宽 | 上行 + 下行(下行常大 10 倍以上) | 只有上行 |
| 状态 | 每条流必须保 | 可以完全无状态 |
| MTU | 不变 | 外层头吃掉 20–24 字节,须管好 PMTUD |
| 可观测性 | LB 看得到响应与耗时 | LB 看不到响应,要另建 |
| 适用 | 小规模、要做七层 | 大规模四层接入(Maglev、Katran 都走这条) |
一句话:三层不是替代关系,是叠加关系——每往下一层,粒度更细、反应更快,但看见的范围更小。只允许留一层就留 VIP 层:它是唯一真正知道后端还活着没有的一层。
这一章的价值,在于它把「负载均衡」这一个词拆成了三个问题:用户引到哪个大区(DNS)、包送进哪个站点(路由)、连接钉在哪台机器(VIP 层)。三者由完全不同的组件回答,混着谈就会得出「把 TTL 调小就能做负载均衡」这类错结论。面试里最常追问的也正是这三点:DNS 为什么做不了负载均衡、一致性哈希凭什么比取模好、DSR 为什么能把 LB 的容量单位从带宽换成包速率。
① 前端负载均衡不是一台设备,是三层接力:DNS 定大区、任播 + BGP 定站点、VIP 层定机器。开篇立论是光有算力不解决问题——容量不在一处、距离直接换成延迟(跨洋一个往返 70–160 毫秒)、请求还不等价。
② DNS 的三道硬墙:看见的是递归解析器不是用户(补丁 EDNS0 客户端子网,代价是缓存碎片化 + 隐私折扣)、缓存与 TTL 不受控、客户端怎么挑也不受控。所以它是分布不是均衡。
③ 任播的好处是撤宣告就能撤流量(秒到分钟级);代价是 BGP 的「近」不等于低延迟,中途改路还会打断连接。
④ VIP 层的唯一难题:同一条连接的每个包必须落在同一台后端。连接表是状态(几百万流即 GB 级,重启或 ECMP 改撒就失配),一致性哈希无状态可重算。
⑤ 量级:后端 10 → 11 台,取模哈希打断约 91% 连接,一致性哈希只动约 9%;但后者只保证「算得一致」,均匀要靠虚拟节点。Maglev = 一致性哈希作地基 + 连接表作优化。
⑥ 递包两条路:NAT 让 LB 背上下行(下行常大 10 倍以上);封装 + DSR 让后端直接回客户端、LB 只背上行——代价是 MTU、ARP 与「看不到响应」。
⑦ 落地:Google Cloud 的全球 LB 是单一任播 VIP + Maglev + 一致性哈希,Meta 的 Katran 独立选了同一套。