← 返回学习路线 ◆ 贯穿项目
计算机与工程基础 · 阶段 4 · 计算机网络
Stage 04 / 17 · 计算机与工程基础

计算机网络 Computer Networks

为什么 AI 工程师必须懂网络?因为你交付的一切最终都是通过网络到达用户的:流式输出卡不卡,取决于 SSE 分帧和反压;推理吞吐上不去,可能是连接池只有 10 条;Agent 调工具超时乱重试,可能造成一次任务重复下单。更实际的是——分布式训练的性能瓶颈常常在网络上而不是算力上。这一阶段的目标很明确:让你在设计「模型怎么被调用」这件事上,不再靠试错。

⏱ 5–7 周 🎯 入门–进阶 ◆ 里程碑 M4 2026-09-29
TCP拥塞控制BBRTLSHTTP/2HTTP/3QUICSSEWebSocketgRPCNetty超时预算背压AllReduce

阶段总览

✔
学完你能做到
  • 完整讲出一次 HTTPS 请求从 DNS 到响应结束的全部阶段,并说明每段的延迟量级
  • 解释 TCP 三次握手为什么必须是三次、四次挥手为什么要 TIME_WAIT,以及这些对服务端连接管理的实际影响
  • 说清拥塞控制从 Reno / CUBIC 到 BBR 的演进逻辑,以及在长肥管道与弱网下各自的优劣
  • 手画 TLS 1.3 握手流程,说明它比 TLS 1.2 少了哪一次往返、0-RTT 的安全代价是什么
  • 在 SSE / WebSocket / gRPC streaming / 长轮询之间做有理有据的选型,并说清各自的队头阻塞与代理兼容性问题
  • 为一条多跳调用链设计端到端超时预算,并区分「可重试」与「不可重试」错误,给出幂等键方案
  • 实现连接池 + 限流 + 熔断 + 背压四件套,并用压测数字证明有效
  • 用 tcpdump / Wireshark / curl 的详细输出定位一次真实的网络问题
  • 算清一次分布式训练中 all-reduce 的通信量,并解释通信与计算重叠为什么能提速
阶段知识结构总览 · 从一次请求的旅程到可靠网络层
阶段 4 · 计算机网络Computer Networks · 8 大章 · 210+ 知识点
1. 分层模型与一次请求的完整旅程TCP/IP 四层模型封装头部开销DNS→TCP→TLS 旅程TTFB / TTFT / TPOT延迟四分量Little 定律QUIC 0–1 RTT
2. IP、路由与数据中心网络CIDR / 子网掩码NAT 与主动连接MTU 与 PMTUDVXLAN Pod MTU 1450RDMA / RoCEv2Ring AllReduceNVLink 900 GB/s
3. TCP:可靠传输的代价与拥塞控制三次握手 / TIME_WAIT滑动窗口 rwnd / cwndCUBIC vs BBR队头阻塞 HOL缓冲膨胀 BufferbloatNagle + 延迟确认TCP_NODELAY
4. TLS 与安全传输TLS 1.3 1 RTT0-RTT 重放风险证书链验证SNI / ECHmTLS 双向认证
5. HTTP 演进与流式协议选型HTTP/1.1→2→3多路复用 streamSSE Last-Event-IDWebSocket 双向gRPC 四种流式HTTP 缓存 304语义缓存阈值
6. RPC、序列化与可靠调用的工程学protobuf 字段号gRPC 四种模式连接池 / 负载均衡分层超时预算幂等键熔断 / 限流 / 背压
7. 网络诊断与可观测curl -w 分层计时tcpdump / tshark重传率 / RTT 分位TTFT / TPOT / 排队OpenTelemetry容量规划 Little
8. 小结:网络层定稿(M4)对外 SSE 对内 gRPCTCP_NODELAY + 批幂等 + DB 唯一索引按 token 限流M4 验收清单
贯穿项目 · M4 服务协议与流式网关第 17–22 周FastAPI 网关,SSE 流式输出、心跳保活、断线重连(基于 Last-Even…gRPC 定义 + Python/gRPC 客户端与服务端骨架httpx 连接池上限、队列水位、429/503 语义与重试退避端到端超时预算表(各跳超时之和 ≤ 总预算),含幂等键设计
学习路径
周次主题动手产出
第 1 周分层模型、一次请求的完整旅程、延迟构成为 Hamauls Orion 画一张端到端网络时序图(含各段延迟标注)
第 2 周IP、路由、NAT、MTU、数据中心网络与 RDMA用 tcpdump 分析一次请求的包序列,解释每个包的作用
第 3 周TCP:握手/挥手、滑动窗口、拥塞控制、队头阻塞做弱网实验(tc netem),对比 CUBIC 与 BBR 的表现
第 4 周TLS 1.3、证书、mTLS、会话复用与 0-RTT用 openssl 手工完成一次握手并解读每个字段
第 5 周HTTP/1.1→2→3、SSE / WebSocket / gRPC 流式选型实现 SSE 流式网关与 gRPC 服务端,做协议对比
第 6 周可靠调用工程:超时预算、重试、幂等、熔断、限流、背压给 Hamauls Orion 实现四件套并压测出数字
第 7 周抓包诊断 + 分布式训练集合通信(里程碑 M4 收口)输出超时预算表、网络诊断手册、通信量核算
✔
有 10 年 Java 背景的人怎么走:你的优势很明显:Netty / 线程池 / 连接池 / 熔断限流(Hystrix、Sentinel)/ HTTP 客户端调优 这些你大概率都做过,属于「可迁移资产」,本阶段约 40% 的内容你能快速通过。真正的增量在三处:① 流式协议(SSE / WebSocket / gRPC streaming 的取舍与代理兼容性)——这是 LLM 应用独有的新问题;② QUIC / HTTP/3 与 TCP 队头阻塞——移动端与弱网场景的关键;③ 集合通信与分布式训练网络(all-reduce、NVLink、IB、拓扑感知)——完全是 AI 领域的新知识。建议把省下的时间投到这三块。

1. 分层模型与一次请求的完整旅程

知识结构图 · 分层模型与请求旅程
分层模型与一次请求的完整旅程4 大知识域 · 26 个知识点
分层与封装开销TCP/IP 四层模型TCP 头 20–60B + IP 20B以太网 14B + FCS 4B40B payload 只占 33.9%curl/ss/tcpdump 分层映射
学习路径
  1. 读 1.1:把一次小 HTTP 请求的各层头部开销算成百分比
  2. 跑内置代码,用分层表把 40B payload 的开销占比算出来
  3. 完成练习自测:遇到慢请求先定位到层再动手
  4. 对接 M4:用分层思维审视网关各阶段该在哪一层排查
✔ 能说出为什么小请求的网络效率低,并把慢的问题定位到具体层
核心知识点详解
  • 头部开销在小请求上吃掉效率:一个 40B 的探活请求在物理层还要包 TCP 头 20B + IP 头 20B + 以太网头 14B + FCS 4B 等,应用层占比约 33.9%。头部开销比数据还大是 Agent 频繁调小接口变慢的根本原因,对策是合并 keepalive / 批量调用,而不是单纯减字节。
  • 分层是为「定位」不是学术:遇到「慢 / 不通 / 出错」按层排序排查:应用层用 curl -v、传输层用 ss、网络层用 ping/mtr、链路层用 tcpdump -e。30 秒内先锁层再动手,比在整段上乱猜高效得多。
  • 常见坑:跨层排障漏掉代理:LB / Nginx / 代理既是 L4 也是 L7 参与者,本地工具正常不代表链路 OK。要在每一跳分别计时(curl -w 分段、mtr 逐跳),否则会把「代理缓冲半秒」误判成服务端慢。
一次请求的完整旅程DNS 5–50msTCP 三次握手 1 RTTTLS 1.3 1 RTTTTFB / TTFTTPOT 逐 tokencurl -w 分段时间连接复用省握手
学习路径
  1. 读 1.2:把 DNS、TCP、TLS、TTFB、TTFT 各段延迟拆开看
  2. 跑内置代码,用 curl -w 分段时间打印出一笔真实请求
  3. 完成练习自测:核算连接复用能省下几次握手
  4. 对接 M4:为网关的 SSE 请求画出逐段延迟明细
✔ 能用 curl -w 把一次请求的延迟拆到各层并解释每个数字
核心知识点详解
  • 一次 HTTPS 请求的延迟大头在哪:DNS 5–50ms → TCP 三次握手 1 RTT → TLS 1.3 握手 1 RTT → 请求 + 服务端处理 → TTFB/TTFT → 逐 token 传输。多数场景最大耗时在服务端处理与排队,而不是网络——区分这两者才是排查关键。
  • TTFB / TTFT / TPOT 是不同尺度:TTFB(首字节)对 LLM 就是 TTFT,由 prefill + 排队决定;TPOT(每输出 token 间隔)由解码(memory-bound)决定。优化手段不同:TTFT 看并发与 prefill 优化,TPOT 看模型与批量。
  • 连接复用省掉两次握手:Keep-Alive / 连接池把后续请求的 TCP + TLS 握手都摊掉(同机房 0.1–0.5ms、跨地域 30–150ms)。高频小请求叠加的握手成本才是连接池价值的量化来源,用 curl -w 的 time_connect 与 time_appconnect 分段即可验证。
延迟四分量处理 / 排队 / 传输 / 传播传播 ≈ 200 km/ms带宽只影响传输延迟Little 定律 L = λ × W排队延迟拥塞暴涨长肥管道 LFNBDP 带宽时延积
学习路径
  1. 读 1.3:分清处理、排队、传输、传播并记住传播约 200 km/ms
  2. 跑内置代码,算出 1Gbps×100ms 的 BDP 与长肥管道的要求
  3. 完成练习自测:用 Little 定律 L=λ×W 估算排队长度
  4. 对接 M4:为 TTFT 预算分配各跳目标值
✔ 能把一次慢请求归属到四个分量之一并给出可操作的结论
核心知识点详解
  • 四分量里只有两项可控:处理 = 服务器忙、排队 = 拥塞、传输 = 仅带宽决定、传播 ≈ 200 km/ms 只随距离变。你只能优化处理与排队,传输受带宽、传播受物理距离约束——所以优化「延迟」常常就是优化排队。
  • Little 定律 L = λ × W:稳定态下系统内请求数 = 到达率 × 平均耗时。排队延迟在逼近容量时会非线性暴涨,用 L = λ × W 估算队列长度与并发槽预算,是容量规划的理论根基。
  • BDP 与长肥管道:BDP = 带宽 × RTT,1Gbps × 100ms ≈ 12.5MB;要让窗口 ≥ BDP 才能跑满带宽,还需开启 TCP 窗口缩放。长肥管道(LFN)上丢包重传会把有效吞吐瞬间打到近乎零,需配合 BBR/大窗口。
前沿与练习QUIC 0–1 RTT 建连HTTP/3 连接迁移anycast 就近接入延迟预算核算
学习路径
  1. 读 1.4:了解 QUIC 0–1 RTT 建连与 HTTP/3 连接迁移
  2. 跑内置代码,做一次延迟预算核算并核对数值
  3. 完成综合练习:把延迟预算表交到网关设计里
  4. 对接 M4:确认网关所选协议是 0-RTT 友好还是重连有代价
✔ 能给出一次端到端请求的延迟预算数字并逐段核对
核心知识点详解
  • QUIC 把握手压到 0–1 RTT:QUIC 建连 = 1 RTT,携带上次会话信息走 0-RTT 恢复,并基于 UDP 规避 TCP 队头阻塞。0-RTT 早到数据可被重放,含副作用的请求不能直接消费 0-RTT。
  • 连接迁移是移动端杀手锏:切 WiFi/4G 时 TCP 需重握手,而 QUIC 靠连接 ID 无缝迁移,避免弱网下「断连 → 重连握手 → TTFT 重刷」,对长连接流式服务收益明显。
  • 常见坑:预算分配与协议错配:给 TTFT 预算逐段分配时,若某跳用了 0-RTT 不友好的协议,预算会整链超限。先确认所选协议建连是几 RTT 再填预算表,否则数字好看却落不了地。
学习路径

1.1 分层:不是为了学术,而是为了定位问题

分层的实用价值只有一条:把「慢 / 不通 / 出错」定位到某一层。当你面对「页面加载慢」时,分层思维让你能在 30 秒内排出排查顺序:DNS?TCP 建连?TLS?服务端处理?还是响应传输?

层TCP/IP 模型做什么典型协议排障工具
7–5应用层业务语义、数据格式、安全会话HTTP/2、gRPC、SSE、TLScurl -v、Chrome DevTools、日志
4传输层端到端可靠传输、流控、拥塞控制TCP、UDP、QUICss、netstat、tcpdump
3网络层寻址与路由(主机到主机)IP、ICMP、BGPping、traceroute、mtr
2链路层同一链路上的帧传输以太网、Wi-Fi、ARPip link、tcpdump -e
1物理层电/光信号1000BASE-T、光模块交换机端口计数、ethtool
ℹ
分层的关键副作用:封装与开销:每一层都要给自己的头(header)留空间,结果是一个 HTTP 请求在物理层要包上 TCP 头(20–60 字节)+ IP 头(20 字节)+ 以太网头(14 字节 + 4 字节 FCS)。当你传输的 payload 只有几十字节时,头部开销可能比数据还大——这就是为什么 Agent 频繁调用小接口会比批量调用慢很多,也是为什么「减少往返次数」比「减少数据量」更重要。
python# 把头部开销算清楚:为什么小请求的网络效率极低
# 一个最小 HTTP/1.1 请求在各层的实际占用(字节)
layers = [
    ('应用层 payload', 40),        # "GET /v1/health HTTP/1.1" + Host 头
    ('TCP 头', 20),                # 无选项时 20 字节
    ('IP 头', 20),
    ('以太网头 + FCS', 14 + 4),
    ('前导码 + 帧间隙', 8 + 12),    # 链路层还要求 >=12 字节 IFG
]
total = sum(sz for _, sz in layers)
for name, sz in layers:
    print(f'{name:>18}: {sz:>3} B  ({sz/total:6.1%})')
print(f'{"合计":>18}: {total:>3} B')
print(f'应用层占比 = 40/{total} = {40/total:.1%}')

# 预期输出(逐行):
#       应用层 payload:  40 B  ( 33.9%)
#              TCP 头:  20 B  ( 16.9%)
#               IP 头:  20 B  ( 16.9%)
#      以太网头 + FCS:  18 B  ( 15.3%)
#      前导码 + 帧间隙:  20 B  ( 16.9%)
#               合计: 118 B
# 应用层占比 = 40/118 = 33.9%
★
2026 现状:小请求往返的成本与去重:一个 40 字节的探活请求,协议开销比数据还大,有效载荷只占 1/3。2026 年的 Agent 架构里这个问题被进一步放大:一次工具调用(如 MCP 的 tools/call)在 JSON-RPC 之上还要包 HTTP/2 帧与 TLS 记录,实际 payload 利用率先掉到 20% 以下。对策是批量与复用:把 N 个探活合并成一个 keepalive;MCP 用 streamable HTTP 的单个 POST + SSE 长连接代替「每次调用建一次连接」;服务网格(Istio/Envoy)用 mTLS 会话复用与 HTTP/2 连接池把握手成本摊到近零。

1.2 一次 HTTPS 请求的完整旅程(把延迟拆开看)

① DNS 解析
域名 → IP。有本地缓存、系统缓存、递归解析器缓存三级。未命中时需 1–2 次往返。典型 5–50 ms。优化:DNS 预解析、长连接复用、就近解析(GSLB)。
② TCP 三次握手
SYN → SYN+ACK → ACK,1 个 RTT(可优化为 TCP Fast Open 的 0-RTT)。同机房约 0.1–0.5 ms,跨地域约 30–150 ms。
③ TLS 握手
TLS 1.3 只需 1 个 RTT(TLS 1.2 需 2 个)。恢复会话时可 0-RTT(但有重放风险)。
④ 发送请求 + 服务端处理
请求上行、排队、查询、调用模型。这一段的耗时常常是最大的——但它不是网络问题,区分这两者是排查的关键。
⑤ 首字节返回(TTFB)
服务端开始产出第一个字节。对 LLM 而言这就是 TTFT(Time To First Token),主要取决于预填充(prefill)耗时与排队时间。
⑥ 流式传输 + 逐 token 返回
每个 token 的产生间隔是 TPOT(Time Per Output Token),由解码速度决定(memory-bound,见阶段 2)。
⑦ 连接关闭或复用
保持连接可省掉后续请求的 ②③ 两次握手。这是「连接池」价值的量化来源。
bash# 用 curl 把时间拆开看 —— 这是最快的「分层诊断」
curl -o /dev/null -s -w "\
  DNS解析:      %{time_namelookup}s\n\
  TCP建连:      %{time_connect}s\n\
  TLS握手完成:  %{time_appconnect}s\n\
  首字节到达:   %{time_starttransfer}s\n\
  全部完成:     %{time_total}s\n\
  重定向次数:   %{num_redirects}\n\
  下载字节:     %{size_download}\n" https://api.example.com/v1/chat

# 读法(这是关键):
#   time_namelookup 大               → DNS 慢(换 DNS / 加缓存 / 预解析)
#   time_connect - time_namelookup 大 → TCP 建连慢(网络 RTT 大 / 丢包重传)
#   time_appconnect - time_connect 大 → TLS 握手慢(证书链长 / 未复用会话 / 跨地域)
#   time_starttransfer - appconnect 大 → 服务端处理慢(不是网络问题!去查服务端)
#   time_total - time_starttransfer 大 → 响应传输慢(数据量大 / 带宽受限)

# 再看一次「流式」请求的表现:TTFT 才是用户感知的等待
curl -N -s -w "\nTTFB: %{time_starttransfer}s  总计: %{time_total}s\n" \
  https://api.example.com/v1/chat/stream -d '{"q":"hi"}' | head -40
场景RTT 量级握手+握手总成本(TCP+TLS1.3)说明
同机房(同 AZ)0.1–0.5 ms< 1 ms微服务间调用,几乎可忽略
同地域跨 AZ0.5–2 ms1–4 ms多可用区部署的常见代价
中国跨省20–50 ms40–100 ms连接复用价值显著
跨太平洋120–200 ms
240–400 ms
不建长连接的话,每次请求光握手就 0.4 s
移动网络(4G/5G)30–100 ms 且波动大60–200 ms弱网,丢包与切换频繁
★
这张表最重要的一条推论:跨地域场景下,TCP + TLS 握手成本(约 3 个 RTT)可能比请求本身还大。所以:① 一定要用连接池 / 长连接;② 把服务部署在离用户近的地方(边缘 / 多区域);③ 用 HTTP/2 的多路复用避免为每个并发请求各建一条连接。这三条优化在跨地域场景下能省掉 80% 的延迟,而在同机房场景下几乎无收益——所以优化前必须先测量。
python# 用 Python 精确测量各阶段(比 curl 更细,可直接做成探针)
import socket, ssl, time
HOST, PATH = 'api.example.com', '/v1/health'
t = {}
t0 = time.perf_counter()
raw = socket.create_connection((HOST, 443), timeout=5)
t['tcp_ms'] = (time.perf_counter() - t0) * 1000          # 纯 TCP 建连
ctx = ssl.create_default_context()
t1 = time.perf_counter()
tls = ctx.wrap_socket(raw, server_hostname=HOST)
t['tls_ms'] = (time.perf_counter() - t1) * 1000          # 纯 TLS 握手
t2 = time.perf_counter()
req = f'GET {PATH} HTTP/1.1\r\nHost: {HOST}\r\nConnection: close\r\n\r\n'
tls.sendall(req.encode())
tls.recv(1)
t['ttfb_ms'] = (time.perf_counter() - t2) * 1000         # 请求到首字节
t['total_ms'] = (time.perf_counter() - t0) * 1000
print({k: round(v, 1) for k, v in t.items()})

# 典型输出(同城跨 AZ,未复用连接):
# {'tcp_ms': 1.2, 'tls_ms': 3.8, 'ttfb_ms': 42.0, 'total_ms': 47.2}
# 判读:ttfb 远大于 tcp+tls → 瓶颈在服务端而非网络(见 7.1 分层诊断)
#       复用连接(连接池)后 tcp_ms 与 tls_ms 应接近 0
ℹ
2026 前沿:QUIC 把握手压到 0–1 RTT:HTTP/3(QUIC)把 TLS 1.3 集成进传输握手:首次连接 1 RTT 同时完成「传输 + 加密」,会话恢复时 0-RTT 即可发数据;而 TCP + TLS 1.3 首次要 2 RTT、恢复要 1–2 RTT。加上 QUIC 的连接迁移(换 Wi-Fi/5G 不断连),移动端 App 的 p99 建连时间通常能降 30–50%。代价是企业防火墙常封 UDP 443,必须保留 TCP 回退路径。Cloudflare、Google、Meta 的公开数据里 HTTP/3 已占其流量的 30% 以上。

1.3 延迟的四个组成部分:别把「慢」都归给带宽

网络延迟不是一个数字,而是四部分之和。分不清它们,你就无法判断该优化什么(这是面试高频,也是排障必备)。

text总延迟 = 处理延迟 + 排队延迟 + 传输延迟 + 传播延迟

  处理延迟 (processing):路由器/网卡查表、校验耗时。通常微秒级,可忽略。
                        但用户态协议栈(如 gRPC 的序列化)会显著增加它。

  排队延迟 (queueing)  :在缓冲区里等待被发送。拥塞时会指数级增长!
                        这是「为什么一拥塞延迟就爆炸」的原因(见 1.3 后的排队论)。

  传输延迟 (transmission) = 数据长度 / 带宽
                        100 KB 在 1 Gbps 上 = 0.8 ms;这是「带宽」真正影响的部分。

  传播延迟 (propagation) = 距离 / 光速(约 200 km/ms 在光纤中)
                        北京→上海 1200 km ≈ 6 ms(单程),往返 12 ms(理论下限)

关键结论:
  ① 传播延迟是「物理下限」,无法通过加带宽改善。跨大西洋往返最少 ~60 ms。
  ② 带宽只影响传输延迟。把一个 1 GB 文件从 100 Mbps 升到 1 Gbps,确实快 10 倍;
     但把一次 1 KB 的 API 调用从 100 Mbps 升到 1 Gbps,几乎没变化。
  ③ 排队延迟在拥塞时主导一切——这也是拥塞控制的全部意义。
python# 把 1.3 的四个延迟分量写成可复算的函数 —— 评估方案时直接用
C = 200_000_000        # 光纤中光速 2e8 m/s(约 200 km/ms)
def total_latency(payload_bytes, bandwidth_bps, distance_km,
                  hops=8, queue_ms=0.0, process_ms=0.05):
    prop  = (distance_km * 1000 / C) * 1000                 # ms(单程)
    trans = payload_bytes * 8 / bandwidth_bps * 1000        # ms
    return {'prop': prop, 'trans': trans,
            'proc': process_ms * hops, 'queue': queue_ms,
            'sum': prop + trans + process_ms * hops + queue_ms}

print('跨太平洋 1 KB 请求')
r = total_latency(1024, 1_000_000_000, 10_000)
print(f"  传播(单程) {r['prop']:.1f} ms → RTT {2*r['prop']:.1f} ms")
print(f"  传输         {r['trans']:.3f} ms  <- 1 KB 在 1 Gbps 上几乎为 0")
print(f"  处理(8 跳)   {r['proc']:.2f} ms")

print('同机房传 1 GB 模型权重')
r2 = total_latency(1024**3, 25_000_000_000, 0.01)
print(f"  传输 {r2['trans']:.0f} ms  <- 这里带宽才是主角")

# 预期输出:
#   传播(单程) 50.0 ms → RTT 100.0 ms
#   传输         0.008 ms
#   处理(8 跳)   0.40 ms
#   同机房 1 GB 传输 343 ms
# 关键推论:小请求的延迟 = 传播 + 握手,与带宽无关;
#           大传输的延迟 = 数据量/带宽。二者优化手段完全不同。

1.4 动手练习与自测

✔
练习目标:把「猜测」换成「测量」:下面 5 题都能在本地或一台云主机上跑。判据是关键——没跑出数字就不算完成。
题目关键数字 / 判据
分层计时DNS/TCP/TLS/TTFB 四段,两法误差 < 20%
连接复用同城总耗时降 30–60%
带宽 vs 传播跨洋 1 KB ≈ 100 ms RTT;同机房 1 GB ≈ 343 ms
头部开销有效载荷占比约 1/3
2026 选型HTTP/3+QUIC:0-RTT、连接迁移、UDP 回退
  1. 分层计时:用 curl -w 与上面的 Python 探针分别测一个真实 HTTPS 站点,把 DNS/TCP/TLS/TTFB 四段填进表格。判据:两法结果同量级(误差 < 20%),并能指出瓶颈段。
  2. 连接复用收益:对同一域名连续发 20 次请求,分别在「每次新建连接(Connection: close)」与「复用连接」下测总耗时。判据:复用后总耗时下降应接近握手成本 × 次数(同城可省 30–60%)。
  3. 带宽 vs 传播:用上面的复算函数预测「北京↔上海 1 KB」与「同机房 1 GB」的延迟,再用 scp/curl 实测对比。判据:预测与实测同一数量级,并能解释偏差来源(排队/抖动)。
  4. 头部开销:抓一次真实请求的包,量出 payload 与总包字节比。判据:能算出有效载荷占比,并说出它对你的探活/心跳设计的影响。
  5. 2026 选型:给定「移动端 AI 助手、弱网、需断线续传」的场景,判断该用 HTTP/2+SSE 还是 HTTP/3+QUIC,并写出至少 2 条量化理由。判据:提到 0-RTT、连接迁移、UDP 被封的回退。

2. IP、路由与数据中心网络

知识结构图 · IP 与数据中心网络
IP、路由与数据中心网络3 大知识域 · 24 个知识点
IP、CIDR 与 NATCIDR /24 = 254 可用子网掩码与运算NAT 后无法被主动连接MTU 1500 与分片PMTUD 被阻断卡死VXLAN 扣 50 字节K8s Pod MTU 1450
学习路径
  1. 读 2.1:算清 /24 与 /20 的可用地址并做掩码与运算
  2. 跑内置代码,用 ping -M do 找出真实 MTU 与分片点
  3. 完成练习自测:解释为何 NAT 后外部无法主动发起连接
  4. 对接 M4:为网关确认服务发现与公网出口的策略
✔ 能随手算出任意 CIDR 的可用地址,并用命令实测 MTU 卡点
核心知识点详解
  • CIDR 可用地址 = 2^(32−前缀) − 2:/24 有 254 个可用(去掉网络号与广播址),10.0.0.0/8 可容纳 2²⁴ 个。判断同子网用掩码与运算:(ip & mask) == (net & mask),手算不掉是日常排障的硬伤。
  • NAT 让外部无法主动连内网:NAT 把私网地址映射到公网,但对外部没有该主机的映射就无法主动发起连接——这正是 WebSocket / SSE / gRPC 都要「客户端先连」、以及 P2P 走 STUN/TURN 的根本原因。
  • MTU / PMTUD 是「大包卡死」的头号嫌疑:以太网 MTU 1500,K8s overlay(VXLAN 加 50 字节)下有效 MTU 降到 1450。用 ping -M do -s 试探真实 MTU;PMTUD 被防火墙阻断时会出现小包正常、大包超时——AI 大 payload 在 K8s 卡死优先查 MTU 而非代码。
数据中心与 RDMARoCEv2 100–400 GbpsInfiniBand NDR 400–800 GbpsNVLink 900 GB/sRDMA 绕过内核零拷贝Ring / Tree AllReduce通信计算重叠 overlapNCCL_DEBUG=INFO
学习路径
  1. 读 2.2:对比 RoCEv2、InfiniBand 与 NVLink 的带宽数量级
  2. 跑内置代码,估算一次 all-reduce 280 GB 所需的秒数
  3. 完成练习自测:解释 RDMA 为何绕过内核可零拷贝
  4. 对接 M4:认清训练瓶颈在网络,网关设计要避开带宽红线
✔ 能算清给定规模 all-reduce 的耗时并说出训练瓶颈在网络
核心知识点详解
  • 三种带宽数量级要分清:RoCEv2 100–400 Gbps(以太网)、InfiniBand NDR 400–800 Gbps、NVLink 900 GB/s(芯片直连私有互连)。训练瓶颈往往在网络:一次 all-reduce 280 GB 在数百 Gbps 下也要数秒,这就是集群上 NCCL 通信必须与计算重叠的原因。
  • RDMA 绕过内核 = 零拷贝低延迟:RDMA 把数据从网卡直接送到用户态内存,跳过内核协议栈与多次拷贝,延迟到微秒级、CPU 占用极低。代价是必须配合 PFC / 无损以太网,否则流控防护失效会带来大范围丢包抖动。
  • 常见坑:NCCL 只看算力不看通信量:调并行方案前先用 NCCL_DEBUG=INFO 看 all-reduce 实际耗时量级,别把通信当成免费;PP/TP/DP 组拓扑不同会导致通信不均,必要时做拓扑感知(ring 变 tree)并让通信与计算 overlap。
练习自测172.16.0.0/20 = 4094all-reduce 280 GB / 5.6sNVLink 约 400 GB/s
学习路径
  1. 读 2.3:复算 172.16.0.0/20=4094 与 all-reduce 的耗时
  2. 跑内置代码,把 NVLink 约 400 GB/s 的数字核一遍
  3. 完成综合练习:给出数据中心组网的三个关键数字
  4. 对接 M4:把网卡与带宽预算写进网关容量表
✔ 能报出 CIDR、all-reduce、NVLink 三个数量级数字且解释来源
核心知识点详解
  • 数字要能解释来源:172.16.0.0/20 可用 4094(2¹²−2);「all-reduce 280 GB / 5.6s」对应约 400 Gbps 真实聚合带宽;NVLink 单向约 400 GB/s。要能报数字还能报计算式,否则排障时不知道偏差出在哪。
  • 组网三数字落到容量表:把「链路带宽 → MTU → 网络延迟」三个数字写进网关容量表,作为协议选型与 payload 分片判断的前提;容量表里用实测值而非标称。
  • 常见坑:把理想带宽当真实带宽:官方标称是线速上限,真实可用还要乘上流控、重传与 overlap 效率。用 iperf3 / NCCL 测试实测填表,否则容量预算会系统性高估。
学习路径

2.1 IP、CIDR、NAT 与 MTU

bash# 排查 MTU 问题的标准手法
# 逐步加大包大小做「不分片」测试,看从多大开始失败
for sz in 1472 1442 1400 1372 1200; do
  echo -n "payload=$sz: "
  ping -c 1 -M do -s $sz 10.0.1.5 >/dev/null 2>&1 && echo OK || echo "失败(超过 MTU)"
done
# 注意 -s 是 payload 大小,实际包大小 = payload + 28(IP 20 + ICMP 8)
# 所以 1472 + 28 = 1500 是标准以太网;若 1442 就失败,说明有效 MTU 是 1470(隧道封装)

# 查看接口 MTU
ip link show | grep mtu

# 若是 K8s,检查 CNI 的 MTU 配置是否与宿主网络匹配
kubectl get nodes -o jsonpath='{.items[*].status.nodeInfo}' | head -c 200
python# CIDR / 子网计算:面试与排障都要手算
import ipaddress as ip
net = ip.ip_network('10.12.0.0/22')
print('网络地址     :', net.network_address)      # 10.12.0.0
print('广播地址     :', net.broadcast_address)    # 10.12.3.255
print('可用主机数   :', net.num_addresses - 2)    # 1022
print('子网掩码     :', net.netmask)              # 255.255.252.0
print('是否含该 IP  :', ip.ip_address('10.12.3.7') in net)   # True
print('10.12.4.1 in?', ip.ip_address('10.12.4.1') in net)    # False
# 预期输出首三行:10.12.0.0 / 10.12.3.255 / 1022

# K8s overlay(VXLAN)MTU 推导:宿主 1500 - VXLAN 50
# => Pod 有效 MTU 1450;若再叠 IPsec/WireGuard,常见降到 1400 甚至 1380
print('建议 Pod MTU =', 1500 - 50)   # 1450
ℹ
2026 前沿:anycast 与 CDN 如何让「物理下限」变小:跨太平洋 RTT 约 100–200 ms 是物理下限,但用户实际感受到的延迟由「到最近边缘节点」决定。anycast(同一 IP 在多地宣告)让 DNS/BGP 把用户导向最近接入点,Cloudflare 的 1.1.1.1、Google 的 8.8.8.8 以及各家 CDN 都用它。AI 推理的边缘化是 2026 的明确趋势:把 embedding、小型分类/改写模型放到边缘(Cloudflare Workers AI、Fastly Compute),TTFT 可从跨区的 300 ms 降到 20–40 ms;但大模型仍集中在少数数据中心,靠 HTTP/3 的 0-RTT 与连接迁移补偿跨区成本。

2.2 数据中心网络与 RDMA:AI 训练的真实瓶颈

这一节是「计算机网络」在 AI 领域最有增量的部分。分布式训练的性能极限往往不在算力,而在梯度同步的网络带宽。

互联技术带宽(双向)延迟用途说明
以太网 10/25 GbE10–25 Gbps~10 μs普通服务通信传统数据中心网络
RoCEv2(RDMA over Ethernet)100–400 Gbps~2 μsAI 集群主流需无损网络(PFC/ECN 调优)
InfiniBand NDR/XDR400–800 Gbps~1 μsAI 集群高端专用网络,NCCL 原生支持
NVLink 4/5900 GB/s(每 GPU)极低同节点多卡比 PCIe 快 10 倍以上
NVSwitch全互联 900 GB/s极低同节点 8 卡全互联避免拓扑拥塞
PCIe 5.0 x1664 GB/s 单向~1 μsCPU↔GPU常成为隐形瓶颈
text算一下 all-reduce 的通信量 —— 这是评估分布式训练可行性的第一步

Ring AllReduce 总通信量(每张卡)= 2 × (N−1)/N × S
   其中 N = 卡数,S = 模型参数字节数

例子:70B 模型,bf16(2 字节/参数),S = 140 GB
  单卡通信量 = 2 × (N−1)/N × 140 GB ≈ 280 GB(N 很大时)

  用 400 Gbps RDMA(= 50 GB/s)互联:
     理论最短耗时 = 280 GB / 50 GB/s = 5.6 秒  ← 每训练一步!
  用 25 GbE(= 3.1 GB/s):
     理论最短耗时 = 280 / 3.1 = 90 秒        ← 完全不可行

反推:若希望通信耗时不超过计算的 20%,而单步计算耗时 2 秒,
     则要求通信 < 0.4 秒 → 需要带宽 ≥ 280/0.4 = 700 GB/s → 必须用 NVLink + IB 组合

这个计算解释了为什么:
  ① 大模型训练必须用高带宽互联(RDMA / IB),普通以太网不可行;
  ② 张量并行(TP)通信量大,只在节点内(NVLink)做;数据并行(DP)通信量小,可跨节点;
  ③ ZeRO / FSDP 通过分片优化器状态来降低通信量,是「用通信换显存」的精心权衡。
bash# 在真实 GPU 机器上验证互联拓扑与带宽(NCCL 自带工具)
nvidia-smi topo -m
#   输出是一张 8x8 矩阵:NV4/NV5 表示 NVLink,SYS 表示跨 NUMA(慢),
#   PIX/PXB/PHB 表示经 PCIe 交换机/桥。看到 SYS 出现在关键路径就要调插卡。

# 官方 all-reduce 带宽测试(8 卡):
./build/all_reduce_perf -b 8 -e 8G -f 2 -g 8
#   关注输出列:algbw(算法带宽)与 busbw(总线带宽)。
#   预期(NVLink 4,8×H100):busbw 可达约 400 GB/s;纯 PCIe 5 仅约 50 GB/s。
#   若实测远低于此,先查 nvidia-smi topo -m 的拓扑,再查 NCCL_IB_* 环境变量。

# 查 RDMA 设备与端口状态
ibv_devinfo | grep -E 'hca_id|state|rate'
#   期望:state: PORT_ACTIVE,rate: 400 Gb/sec(NDR)
export NCCL_DEBUG=INFO   # 打印 NCCL 选择的算法与通道数
⚠
失败模式:为什么「换一批卡」训练就慢了 30%:分布式训练性能对拓扑极其敏感,常见失败模式:① TP 组跨了 NVLink 边界(本该同节点却被调度到两台机器),all-reduce 从 NVLink 掉到 25/100 GbE,单步时间暴涨;② RoCE 网络没做无损(未配 PFC/ECN),拥塞丢包导致 RDMA 降速重传;③ PCIe 通道不足(GPU 只跑在 x8 而非 x16);④ NCCL 选择了 Ring 而非 Tree(小消息下 Tree 延迟更低)。排查顺序:nvidia-smi topo -m → NCCL_DEBUG=INFO 日志 → 网络计数器。

2.3 动手练习与自测

✔
练习目标:把拓扑与地址算清:这两块知识在面试里是硬指标,在集群调优里是钱。
题目关键数字 / 判据
手算子网172.16.0.0/20 → 4094 可用,掩码 255.255.240.0
MTU 复现VXLAN 扣 50 → 有效 MTU 1450
拓扑检查NV4/NV5 = NVLink,SYS = 跨 NUMA
通信量核算280 GB / 5.6 s(400 Gbps)
总线带宽NVLink 约 400 GB/s、PCIe 约 50 GB/s
  1. 手算子网:给定 172.16.0.0/20,写出网络地址、广播地址、可用主机数与掩码;再判断 172.16.15.5 是否同网段。判据:答案为 172.16.0.0 / 172.16.15.255 / 4094 / 255.255.240.0,且判断为是。
  2. MTU 复现:在 K8s 里(或本地 mock)用 ping -M do -s 逐步探测,找出有效 MTU。判据:能测出失败阈值并解释隧道封装扣了多少字节。
  3. 拓扑检查:在有 GPU 的机器上跑 nvidia-smi topo -m,指出哪些卡在同一 NVLink 域。判据:能读懂矩阵并说出 TP 应限制在哪个范围。
  4. 通信量核算:70B 模型 bf16、128 卡 DP,算每步 all-reduce 通信量与 400 Gbps 下的理论耗时。判据:约 280 GB / 5.6 s(N 大时)。
  5. 总线带宽判读:跑 all_reduce_perf 8 卡,记录 busbw 并与预期(NVLink 约 400 GB/s、PCIe 约 50 GB/s)对比。判据:能解释实测值偏低时的首个排查动作。

3. TCP:可靠传输的代价与拥塞控制

知识结构图 · TCP 可靠传输
TCP:可靠传输的代价与拥塞控制4 大知识域 · 30 个知识点
握手与挥手三次握手确认收发能力四次挥手全双工TIME_WAIT 2×MSL 60sCLOSE_WAIT = 代码 bug端口耗尽估算somaxconn / backlog
学习路径
  1. 读 3.1:搞清三次握手与四次挥手,以及 TIME_WAIT 的 2×MSL
  2. 跑内置代码,用 ss 观察 TIME_WAIT 与 CLOSE_WAIT 计数
  3. 完成练习自测:做一次端口耗尽估算并核对 somaxconn
  4. 对接 M4:让网关长连接复用,避免握手风暴
✔ 能把 CLOSE_WAIT 对应到具体代码 bug 并给出修复方向
核心知识点详解
  • 三次握手确认互收能力:SYN → SYN+ACK → ACK 各确认对方收发能力与初始序号。两次不够(无法确认双方都能收);因 TCP 全双工,四次挥手需两侧各自 FIN+ACK。TIME_WAIT 固定约 2×MSL = 60s,用于清理可能残留的旧报文。
  • CLOSE_WAIT 是代码 bug 的指纹:被动关闭方收到对端 FIN 后若不及时 close 本地 socket,就卡在 CLOSE_WAIT。服务端 CLOSE_WAIT 大量堆积 = 有连接没被正确关闭(典型是 keep-alive 下 read 未循环到 EOF),用 ss -tan state CLOSE-WAIT 定位。
  • 端口耗尽与 backlog:客户端本地端口约 2¹⁶,用尽会报 Cannot assign requested address;服务端半连接队列受 tcp_max_syn_backlog / somaxconn / 应用 backlog 三者的最小者限制,溢出丢 SYN。高并发网关务必调大并监控,否则体现为偶发连接超时。
滑动窗口与拥塞控制rwnd 流控 / cwnd 拥塞发送窗口 = min(rwnd, cwnd)CUBIC 三次函数 W(t)BBR 最大带宽 × 最小 RTT队头阻塞 HOL缓冲膨胀 BufferbloatBDP 1Gbps×100ms = 95MB
学习路径
  1. 读 3.2:理解发送窗口=min(rwnd,cwnd) 与 CUBIC、BBR 的区别
  2. 跑内置代码,算出 1Gbps×100ms 的 BDP=95MB
  3. 完成练习自测:解释队头阻塞与缓冲膨胀的成因
  4. 对接 M4:为流式场景选择合适的拥塞控制并核对 BDP
✔ 能讲清 cwnd 与 rwnd 的分工,并算出给定链路的 BDP
核心知识点详解
  • rwnd 与 cwnd 分工不同:rwnd 是接收方流控(我还能收多少),cwnd 是发送方自己维护的拥塞窗口,实际发送窗口 = min(rwnd, cwnd)。要让链路跑满,窗口需 ≥ BDP(1Gbps × 100ms ≈ 12.5MB),还需开启窗口缩放。
  • CUBIC 与 BBR 的取舍:CUBIC 三次函数探测带宽、丢包即减窗(适合有大量缓冲的链路);BBR 以「最大带宽 × 最小 RTT」估算而不依赖丢包反馈,弱网 / 长肥管道下吞吐更稳。对流式交互建议先验 BBR。
  • 队头阻塞与缓冲膨胀:TCP 一个报文段丢失后其后续都在等重传(HOL);路由器大缓冲让 RTT 漂高、丢包推迟(bufferbloat)。流式被卡几十 ms 常常不是应用慢而是 TCP 重传与排队,用 ss -tin 看 retrans 与 rtt 分位来证伪。
Nagle 与延迟确认Nagle 攒小包延迟确认 40ms两者叠加每块延迟 40msTCP_NODELAY 禁用20–50ms 批大小
学习路径
  1. 读 3.3:弄懂 Nagle 攒小包与延迟确认 40ms 叠加的抖动
  2. 跑内置代码,开 TCP_NODELAY 对比首字延迟差异
  3. 完成练习自测:测出 20–50ms 批大小对吞吐的影响
  4. 对接 M4:在网关的流式连接上打开 TCP_NODELAY
✔ 能复现 Nagle 与延迟确认叠加导致的延迟并解释禁用理由
核心知识点详解
  • Nagle 在 LLM 流式下的反效果:Nagle 会把小包攒满段或有 ACK 才发,配合延迟确认(约 40ms),让每个小包多等约 40ms——对逐 token 的流式输出直接拉高 TPOT,长文本生成里累计抖动非常明显。
  • TCP_NODELAY 是流式的基本配置:在 socket 上开 TCP_NODELAY 禁用 Nagle,让 token 一产生就发出。对 SSE / WebSocket / gRPC 流式通道必须开,否则默认行为会让前台像打字机卡住。
  • 注意 20–50ms 批与首字延迟的权衡:批量聚合能提吞吐但会抬 TTFT,吞吐与首字延迟是一对 trade-off;开不开、开多大要按目标选:重首字就小批实时刷发,重吞吐就 20–50ms 批并接受更高 TTFT。
练习自测tc netem 弱网实验重传率判读
学习路径
  1. 读 3.4:用 tc netem 制造弱网并读重传率
  2. 跑内置代码,观察重传率并判读正常与异常阈值
  3. 完成综合练习:给出一份弱网下的连接建议
  4. 对接 M4:用弱网实验验证流式网关能自动重连
✔ 能在弱网模拟下复现丢包并对接到重连策略
核心知识点详解
  • 用 tc netem 制造可复现弱网:tc qdisc add dev eth0 root netem delay 100ms loss 1% rate 10mbit 可注入延迟 / 丢包 / 限速;tc qdisc del 清理。比真机便宜且可复现,是压测流式网关的标准手段。
  • 重传率是网络健康晴雨表:重传率 > 0.5% 应告警,正常环境应 < 0.1%。用 ss -tin 读 retrans 与 rtt 分位;重传率飙高往往伴随 TTFT / TPOT 长尾,先于业务优化处理它。
  • 弱网实验要接到应用而非只看协议:不只有握手层丢包值得测——流式中断率、客户端重连成功率、TTFT 分布才是对「LLM 体验」的判据;把 netem 弱网接进网关压测脚本才闭环。
学习路径

3.1 三次握手、四次挥手与 TIME_WAIT

text三次握手(建立连接)
  Client ── SYN(seq=x) ────────────────► Server      客户端说:我想连,我的初始序号是 x
  Client ◄─ SYN(seq=y) + ACK(x+1) ───── Server      服务端说:好,我的序号是 y,确认你的 x
  Client ── ACK(y+1) ──────────────────► Server      客户端说:收到你的 y
  ─────────────────────────────────────────────────
  为什么必须三次?因为要让「双方的收发能力」都得到确认:
    第 1 次:服务端确认「客户端能发」
    第 2 次:客户端确认「服务端能收能发」
    第 3 次:服务端确认「客户端能收」
  两次不够(服务端无法确认客户端收到了自己的 SYN);
  四次多余(第 2、3 步可以合并)。

四次挥手(关闭连接)
  Client ── FIN ───────────────────────► Server
  Client ◄─ ACK ───────────────────────  Server       半关闭:客户端不再发,但还能收
  Client ◄─ FIN ───────────────────────  Server
  Client ── ACK ───────────────────────► Server
  ─────────────────────────────────────────────────
  为什么必须四次?因为 TCP 是全双工的,两个方向要分别关闭。
  服务端收到 FIN 后可能还有数据要发,所以 ACK 与自己的 FIN 不能合并。

TIME_WAIT:主动关闭方在发完最后一个 ACK 后要等 2×MSL(通常 60 秒)
  两个原因:
    ① 确保最后的 ACK 能到达(若丢失,对端会重发 FIN,此时还有连接上下文可响应)
    ② 让本次连接的旧报文在网络中彻底消散,避免「串味」到新连接(同四元组复用)
bash# 连接状态一览 —— 排障的第一步
ss -s                                  # 汇总:各状态连接数
ss -tan state time-wait | wc -l        # 有多少 TIME_WAIT
ss -tan state close-wait | head -20    # CLOSE_WAIT 堆积(代码 bug 信号)
netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'

# 内核可调参数(了解即可,生产改前先压测)
sysctl net.ipv4.tcp_tw_reuse=1              # 允许复用 TIME_WAIT 连接(需 timestamps)
sysctl net.core.somaxconn=32768             # accept 队列上限
sysctl net.ipv4.tcp_max_syn_backlog=8192    # 半连接队列上限
sysctl net.ipv4.ip_local_port_range="10240 65535"   # 本地端口范围

# 应用层最有效的三招(优先级高于调内核参数)
#  ① 连接池:复用连接,把「握手成本」摊掉
#  ② 客户端主动关闭(通过配置 http 客户端的 keepalive 策略与超时)
#  ③ 正确的超时与重试(避免僵尸连接长期占位)
字段位宽作用AI 服务相关点
源端口 / 目的端口16+16标识连接(四元组的一半)NAT 后大量连接会耗尽本地端口(见 3.1)
序号 seq32字节流编号,保证有序大 payload 传输与重排的基础
确认号 ack32期望收到的下一个字节延迟确认会推迟它(见 3.3)
数据偏移4TCP 头长度(含选项)带时间戳选项时头 32 字节
标志位9SYN/ACK/FIN/RST/PSH/URG/ECE/CWRECE/CWR 用于 ECN(拥塞显式通知)
窗口 rwnd16接收方还能收多少字节零窗口会让发送方停发(需 persist timer)
校验和16端到端校验—
紧急指针16极少使用常被 NAT/防火墙改写导致握手失败
bash# TIME_WAIT 与端口耗尽的定量判断
ss -s                                  # 看 timewait 总数
ss -tan state time-wait | wc -l        # 逐条计数
# 可用本地端口数 = 65535 - ip_local_port_range 起点(常见 10240)= 约 55295
# 每条 TIME_WAIT 占用 1 个端口 60 秒:
#   若 QPS=1000 个短连接 -> 60 秒窗口需 1000*60 = 60000 个端口
#   > 55295 可用 -> 必然出现 "Cannot assign requested address"
# 结论:短连接 + 高 QPS = 端口必然耗尽。
# 换成长连接后,端口占用从「每请求 1 个」降到「每后端 1–2 个」。

3.2 滑动窗口、拥塞控制与 BBR

这是 TCP 最核心的机制,也是「为什么网络慢」的最主要答案之一。流量控制是「别把接收方撑爆」(接收窗口 rwnd),拥塞控制是「别把网络撑爆」(拥塞窗口 cwnd)。实际发送窗口 = min(rwnd, cwnd)。

算法核心思想优点缺点适用
Tahoe / Reno(经典)慢启动 + 拥塞避免 + 快重传快恢复;以丢包为拥塞信号实现简单、公平性好高带宽高延迟下恢复慢;把「非拥塞丢包」误判为拥塞已基本被替代
CUBIC(Linux 默认)用三次函数替代线性增长,与 RTT 无关高带宽下增长快、公平、稳定仍以丢包为信号;缓冲膨胀时填满缓冲区导致延迟暴涨通用默认,有线网络表现好
BBR(Google)基于「瓶颈带宽 × 最小 RTT」建模,主动探测而不等丢包高延迟高丢包链路下吞吐显著更高;队列延迟低公平性对 CUBIC 有争议;共享链路需调参跨洋、移动网络、有丢包的链路
QUIC 的拥塞控制用户态可插拔,可自定义灵活、可快速迭代需要应用自己实现HTTP/3
bash# 弱网实验:亲手看拥塞控制的影响(Linux 用 tc netem 模拟)
# 查看当前算法
sysctl net.ipv4.tcp_congestion_control

# 模拟「跨洋 + 丢包」链路:100ms 延迟、1% 丢包、限速 50Mbps
sudo tc qdisc add dev eth0 root netem delay 100ms loss 1% rate 50mbit

# 用 CUBIC 下载
sudo sysctl -w net.ipv4.tcp_congestion_control=cubic
curl -o /dev/null -w "CUBIC 用时 %{time_total}s  平均速度 %{speed_download} B/s\n" \
     https://speed.example.com/100mb.bin

# 切换到 BBR(需内核支持,需先加载模块)
sudo modprobe tcp_bbr
sudo sysctl -w net.ipv4.tcp_congestion_control=bbr
curl -o /dev/null -w "BBR   用时 %{time_total}s  平均速度 %{speed_download} B/s\n" \
     https://speed.example.com/100mb.bin

# 清理
sudo tc qdisc del dev eth0 root
# 在 1% 丢包 / 100ms RTT 的链路上,BBR 的吞吐通常能比 CUBIC 高 2–5 倍,
# 因为它不会把丢包当成「拥塞必须退让」的唯一信号。
⚠
两个与 AI 服务直接相关的拥塞现象:① 队头阻塞(Head-of-Line Blocking):TCP 保证字节流有序,因此第 N 个包丢了,即使 N+1、N+2 已到达也必须等重传 —— 整个连接都被卡住。这就是为什么在 HTTP/1.1 上开多个连接来并发,也是 HTTP/3 换用 QUIC(每个流独立)的根本动机。
② 缓冲膨胀(Bufferbloat):路由器缓冲区过大时,CUBIC 会把缓冲区填满,导致 RTT 从 20 ms 涨到 200 ms 而吞吐不变。表现是「服务没报错、吞吐正常,但 P99 延迟高得离谱」。排查方法:看 RTT 随时间的变化趋势,而不是只看平均值。

把拥塞控制的公式写出来,你就能定量判断「这条链路理论上能跑多快」,也能解释 BBR 与 CUBIC 的行为差异。

python# 吞吐上限公式 + CUBIC / BBR 的行为模拟
def tcp_throughput(window_bytes, rtt_s):
    """丢包前的稳态吞吐 = 窗口 / RTT(Mathis 公式的简化形式)"""
    return window_bytes / rtt_s
print('窗口 64 KB、RTT 100 ms ->',
      round(tcp_throughput(64*1024, 0.1) / 1024, 1), 'KB/s')
# 预期 640.0 KB/s 约等于 5.2 Mbps —— 跨洋链路上老默认窗口完全跑不满带宽

def bdp(bw_bps, rtt_s):
    """带宽时延积:填满管道所需的最小窗口(字节)"""
    return bw_bps * rtt_s
print('1 Gbps x 100 ms 的 BDP =',
      round(bdp(1e9, 0.1) / 1024 / 1024, 1), 'MB')
# 预期 95.4 MB —— 窗口必须 >= 此值才可能跑满 1 Gbps

# CUBIC 的窗口增长是三次函数 W(t) = C*(t-K)^3 + W_max,且与 RTT 无关
C, W_max = 0.4, 100
K = (W_max * 0.3 / C) ** (1/3)
print('CUBIC 参数 K =', round(K, 2), ' -> 从 W_max 恢复到 W_max 附近约需 K 个往返')
# 预期 K = 4.22 —— 与 RTT 解耦是 CUBIC 在长肥管道上胜过 Reno 的关键

# BBR 核心:用「最大带宽估计 x 最小 RTT」建模,而不是等丢包
def bbr_estimate(bw_samples, rtt_samples):
    return max(bw_samples), min(rtt_samples)
print('BBR 估计 (带宽, 最小RTT) =', bbr_estimate([100, 250, 900], [0.1, 0.12, 0.25]))
# 预期 (900, 0.1) —— 用最大带宽与最小 RTT 算 BDP,主动探测瓶颈
ℹ
2026 前沿:拥塞控制在 QUIC 与数据中心的新格局:① BBRv3 已成为 Google 全网默认,Linux 内核自 5.x 起内置 tcp_bbr;但 BBR 与 CUBIC 共存时的公平性仍是研究热点。② QUIC 把拥塞控制搬到用户态,可插拔 —— Cloudflare 的 quiche、Google 的 chromium/quiche、Meta 的 mvfst 都实现了 BBR/CUBIC/自定义算法,可在灰度中快速迭代。③ 数据中心内的新方案:AI 训练集群大量使用 RoCE,靠 DCQCN(基于 ECN)+ PFC 做无损;交换机侧还有 HPCC(利用 INT 精确遥测)与 Swift(基于延迟)等,目标是把 RDMA 的尾延迟压到微秒级。④ 这正是「拥塞控制不只是 TCP 的事」的证据。

3.3 Nagle 算法、延迟确认与 TCP_NODELAY

这两个机制是流式输出的经典杀手。如果你实现过 SSE 或 WebSocket 推送,一定会遇到「服务端明明立刻写了,客户端却等了几十毫秒才收到」——根源就在这里。

python# 流式输出:正确的批处理与 TCP_NODELAY 设置
import socket, time

async def stream_tokens(ws, tokens, batch_ms=25, max_batch=16):
    """把逐 token 输出聚成小批再发:
       既避免 Nagle 造成的 40ms 延迟,也避免每个 token 一个包的开销。
       这是生产流式服务的标准做法。"""
    import asyncio
    buf, last = [], time.monotonic()

    async def flush():
        nonlocal buf, last
        if not buf: return
        await ws.send(''.join(buf))
        buf, last = [], time.monotonic()

    async for tok in tokens:
        buf.append(tok)
        now = time.monotonic()
        # 触发条件:攒够一批 或 超过时间窗
        if len(buf) >= max_batch or (now - last) * 1000 >= batch_ms:
            await flush()
    await flush()

# 服务端 socket 层禁用 Nagle(如果框架允许直接操作 socket)
def disable_nagle(sock: socket.socket):
    sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
# 大多数 ASGI 服务器(uvicorn/uvloop)已默认启用 TCP_NODELAY,
# 但如果你自己写 asyncio server 或走代理,一定要确认这一点。
python# 实测 Nagle + 延迟确认带来的约 40 ms 卡顿(本地回环即可复现)
import socket, time
def run(nodelay):
    s = socket.socket(); s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    s.bind(('127.0.0.1', 0)); s.listen(1); port = s.getsockname()[1]
    c = socket.socket(); c.connect(('127.0.0.1', port))
    conn, _ = s.accept()
    if nodelay:
        conn.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
    t0 = time.perf_counter(); gaps = []
    for i in range(3):
        time.sleep(0.001); c.send(b'x' * 10)
        conn.recv(10); gaps.append((time.perf_counter() - t0) * 1000)
    for x in (s, c, conn): x.close()
    return gaps

print('未设 NODELAY:', [round(x, 1) for x in run(False)])
print('已设 NODELAY:', [round(x, 1) for x in run(True)])
# 典型输出:
#   未设 NODELAY: [1.2, 41.5, 42.0]   <- 第 2、3 个包被延迟确认卡住约 40 ms
#   已设 NODELAY: [1.1, 2.3, 3.4]     <- 逐个小包立即发出
# 结论:逐 token 流式输出必须 TCP_NODELAY,或按 20–50 ms 批量聚合。
★
2026 前沿:SSE 长连接下的 TCP 调优清单:LLM 流式网关的默认开局:① TCP_NODELAY=1(关 Nagle);② 不依赖延迟确认,而是靠应用层批处理控制节奏;③ 内核 tcp_rmem/tcp_wmem 增大以支撑长肥管道;④ 反向代理(Nginx/Envoy)关闭缓冲并透传 chunked;⑤ 客户端 fetch/EventSource 侧把 read 循环立刻消费,避免自身缓冲。这几条叠加,才能把 TPOT 稳定在 20–40 ms,而不是偶发 40 ms 抖动。

3.4 动手练习与自测

✔
练习目标:把 TCP 从背概念变成能算能测:下面每题都要求给出数字或代码证据。
题目关键数字 / 判据
画握手三次握手/四次挥手的 seq/ack 变化与状态机
TIME_WAIT60000 > 55295 → 端口耗尽
吞吐公式640 KB/s;BDP 95.4 MB
弱网对比BBR 吞吐高 2–5 倍
NODELAY第 2 个 chunk 延迟约 40 ms
cwnd 观察丢包时乘性减小
  1. 画握手:手绘三次握手/四次挥手,标出 seq/ack 变化与两个方向的状态机。判据:能解释为什么第 2、3 步不能合并。
  2. TIME_WAIT 定量:给定 QPS=1000 的短连接服务,估算端口耗尽时间。判据:60×1000=60000 > 约 55295,判定必然耗尽,并给出连接池方案。
  3. 吞吐公式:窗口 64 KB、RTT 100 ms,算理论吞吐;再算 1 Gbps×100 ms 所需 BDP。判据:约 640 KB/s 与约 95.4 MB。
  4. 弱网对比:用 tc netem 造 100 ms+1% 丢包,对比 CUBIC/BBR 下载同一文件的用时与吞吐。判据:BBR 吞吐通常高 2–5 倍,并能解释原因。
  5. NODELAY 实验:跑上面的 Python 脚本,复现 40 ms 间隔。判据:能观察到未设 NODELAY 时第 2 个 chunk 延迟约 40 ms。
  6. 拥塞窗口观察:在真机用 ss -tin 观察 cwnd 随传输的变化,解释它为何在丢包时刻骤降。判据:能关联到 CUBIC 的乘性减小。

4. TLS 与安全传输

知识结构图 · TLS 与安全传输
TLS 与安全传输2 大知识域 · 16 个知识点
TLS 1.3 握手TLS 1.3 仅 1 RTTTLS 1.2 需 2 RTT0-RTT PSK 有重放风险证书链验证SNI 明文 / ECH 加密mTLS 双向认证ECDHE X25519证书剩余 < 30 天告警
学习路径
  1. 读 4.1:数清 TLS 1.3 只要 1 RTT、1.2 要 2 RTT
  2. 跑内置代码,用 openssl 观察证书链与所选密码套件
  3. 完成练习自测:解释 0-RTT PSK 的重放风险与 mTLS 场景
  4. 对接 M4:给网关配好证书并验证握手成功
✔ 能画出 1.3 握手的时间线并说出 0-RTT 的代价
核心知识点详解
  • TLS 1.3 比 1.2 少一次往返:TLS 1.2 需 2 RTT(ServerHello 后还要 ChangeCipherSpec / Finished),1.3 精简为 1 RTT,会话恢复时最多 0-RTT。移动端长距离下少 1 RTT = 直接省一次往返时延,对 TTFT 体感影响明显。
  • 0-RTT PSK 的重放代价:0-RTT 允许客户端在首包就带数据,但这段早到数据可被攻击者重放,不能用于下单 / 写库等副作用操作;只读幂等查询或配合服务端防重放(early data 令牌)才安全。
  • 证书链验证与到期告警:服务器下发完整证书链,客户端逐级验证到受信根;用 openssl s_client -connect host:443 -servername host 看链与套件。证书剩余 < 30 天应告警,避免过期瞬间全线中断。
练习与前沿session_reused True会话复用时延 < 5msX25519MLKEM768 后量子
学习路径
  1. 读 4.2:用抓包确认 session_reused 与会话复用时延
  2. 跑内置代码,观察复用后握手 < 5ms 的现象
  3. 完成综合练习:评估后量子 X25519MLKEM768 的接入影响
  4. 对接 M4:为网关打开会话复用并压测握手成本
✔ 能测出复用与否的握手时延差并解释安全与性能取舍
核心知识点详解
  • 会话复用把握手压到 < 5ms:开启 TLS 会话复用(session ticket / session id)后,复用连接握手几乎零成本,同机压测可 < 5ms。用 curl -w %{time_appconnect} 即可见复用与否的差异,复用时数值应明显下降。
  • 后量子 X25519MLKEM768:为抵抗量子解密,标准在推 ML-KEM 混合后量子密钥交换;迁移要确认 CDN / 网关 / 客户端都支持且 TLS 版本为 1.3,否则普遍握手不下来就协商不到后量子套件。
学习路径

4.1 TLS 1.3 握手:为什么只需 1 个 RTT

textTLS 1.3 完整握手(1-RTT)
  Client ── ClientHello ─────────────────► Server
           (支持的套件 + key_share + 随机数)
  Client ◄─ ServerHello + {证书} + {Finished}  Server
           (选定的套件 + 自己的 key_share + 签名证书)
  Client ── {Finished} + 应用数据 ───────► Server
  ─────────────────────────────────────────────
  特点:服务端在第二个消息里就把证书、密钥交换参数、Finished 一起发过来,
        把 TLS 1.2 的两次往返压到一次。

TLS 1.3 会话复用 / PSK(0-RTT)
  Client ── ClientHello + 早期数据 ───────► Server     ← 0 往返即可发数据
  ─────────────────────────────────────────────
  代价:早期数据可被重放攻击!所以只能用于「幂等」请求(GET);
        非幂等操作(下单、写入)绝不能放进 0-RTT 数据里。

TLS 1.2(对比)
  ClientHello → ← ServerHello+证书+ServerKeyExchange+ServerHelloDone
  → ClientKeyExchange+ChangeCipherSpec+Finished → ← Finished
  共 2 个 RTT,且密钥协商与证书验证分两部分,需要更多次交互。
bash# 手工完成一次 TLS 握手并解读(这是最好的学习方式)
openssl s_client -connect api.example.com:443 -servername api.example.com -tls1_3

# 在输出里重点看这几行:
#   Protocol    : TLSv1.3
#   Cipher      : TLS_AES_128_GCM_SHA256        ← 套件(1.3 已移除不安全的算法)
#   Verify return code: 0 (ok)                  ← 证书链验证结果,0 才是 ok
#   Server Temp Key: X25519, 253 bits           ← 密钥交换(1.3 常用 ECDHE)
#   ---
#   Certificate chain                           ← 是否有完整的中间证书
#     0 s:CN = api.example.com
#     1 i:C = US, O = Let's Encrypt, CN = R3     ← 中间 CA(重要!)
#     2 i:O = ISRG, CN = ISRG Root X1            ← 根 CA

# 测量握手耗时(1.3 应明显小于 1.2)
for v in tls1_2 tls1_3; do
  echo -n "$v: "
  for i in 1 2 3; do
    openssl s_client -connect api.example.com:443 -$v </dev/null 2>/dev/null >/dev/null
  done
  # 用 time 或 curl -w "%{time_appconnect}" 得到具体数字
done

# 检查证书剩余有效期(放进监控)
echo | openssl s_client -connect api.example.com:443 2>/dev/null \
  | openssl x509 -noout -dates -subject
python# 会话复用:把 TLS 握手从 1 RTT 降到 0 RTT(Python 标准库即可验证)
import ssl, socket, time
ctx = ssl.create_default_context()
sess = None
for i in range(2):
    with socket.create_connection(('api.example.com', 443)) as s:
        t0 = time.perf_counter()
        with ctx.wrap_socket(s, server_hostname='api.example.com',
                             session=sess) as ts:
            sess = ts.session                 # 保存会话票据,下次复用
            reused = ts.session_reused
        print(f'第 {i+1} 次: reused={reused}  耗时={(time.perf_counter()-t0)*1000:.1f} ms')

# 预期输出(同城):
#   第 1 次: reused=False  耗时=118.4 ms   <- 完整 1-RTT 握手
#   第 2 次: reused=True   耗时=  1.9 ms   <- PSK 恢复,省掉证书交换与密钥计算
# 关键:把 session 缓存起来(连接池/进程级),跨请求复用可省约 1 个 RTT。
# 注意 0-RTT 早数据有重放风险,只对幂等请求启用。
ℹ
2026 前沿:TLS 1.3 之上的三件事:① ECH(Encrypted Client Hello):把明文 SNI 也加密,Cloudflare 与 Firefox 已大范围启用,防止基于域名的窥探与阻断。② 后量子密钥交换:Google 自 Chrome 131(2024)起默认开启 X25519MLKEM768 混合密钥交换,2026 年主流云与浏览器基本铺开 —— 你的服务端证书链、TLS 库版本要确认支持,否则会出现握手协商失败。③ mTLS 与零信任:服务网格(Istio/Linkerd)默认给每个 Pod 签发短期证书做 mTLS,证书有效期常短到 24 小时以内,靠自动轮转;这正是「证书过期」事故的现代解法。

4.2 动手练习与自测

✔
练习目标:能手工读懂一次握手:TLS 是「看得见就不可怕」的典型 —— 会用 openssl 就够了。
题目关键数字 / 判据
手工握手Protocol=TLSv1.3,Verify code=0
1.2 vs 1.31.3 少约 1 个 RTT
会话复用第 2 次 < 5 ms(reused=True)
0-RTT 风险下单/写入/转账不可放进早数据
证书链缺中间证书导致验证失败
  1. 手工握手:用 openssl s_client -tls1_3 连一个真实站点,读出协议、套件、临时公钥类型、证书链层数与 Verify 结果。判据:4 个字段全对,且 Verify code = 0。
  2. 1.2 vs 1.3:同一站点分别用 -tls1_2/-tls1_3 测 time_appconnect。判据:1.3 明显小于 1.2(同城可差 1 个 RTT)。
  3. 会话复用:跑上面的 Python 脚本,观察 session_reused 从 False 到 True 与耗时骤降。判据:第 2 次耗时降到 5 ms 以内。
  4. 0-RTT 风险:写出 3 个绝不能放进 0-RTT 早数据的操作,并说明重放后果。判据:下单/写入/转账等非幂等操作,重放会造成重复副作用。
  5. 证书链诊断:人为只配叶子证书(去掉中间证书),用 s_client 观察验证失败。判据:能复现验证错误并说明浏览器为何可能「看起来正常」。

5. HTTP 演进与流式协议选型

知识结构图 · HTTP 与流式协议
HTTP 演进与流式协议选型4 大知识域 · 28 个知识点
HTTP 版本演进HTTP/1.1 每域 6 连接HTTP/2 多路复用 streamHPACK / QPACK 头压缩HTTP/3 QUIC 独立流ALPN 协商 h2TCP 层 HOL
学习路径
  1. 读 5.1:梳理 1.1 到 3 的演进逻辑与多路复用
  2. 跑内置代码,用 ALPN 确认协商到 h2
  3. 完成练习自测:说清 TCP 层 HOL 与 HTTP/2 的取舍
  4. 对接 M4:让网关对外跑 HTTP/2 + SSE
✔ 能讲清每个版本解决了什么又引入什么共性问题
核心知识点详解
  • HTTP/1.1 每域最多 6 连接:浏览器对同域默认约 6 条 TCP,并发高时在应用层排队;HTTP/2 用一个连接承载并行 stream,消除应用层队头阻塞,配合 HPACK 头压缩显著省带宽。
  • HTTP/3 用 QUIC 解掉 TCP 层 HOL:HTTP/2 仍有 TCP 层的队头阻塞——一个丢包卡整个连接;HTTP/3 跑在 QUIC(UDP)上,每个 stream 独立流控、不受其他流丢包影响,适合移动弱网,代价是需 ALPN 协商 h3 与 UDP 支持。
  • 常见坑:网关根本没跑在 h2/h3 上:组件(代理 / LB / CDN)若未开 ALPN 或各自建池,会话会退回 HTTP/1.1,多路复用与 0-RTT 全部失效;用 curl -I -w %{http_version} 实测协商版本,而不是假设已经在用 h2。
流式协议选型SSE Last-Event-ID 续传WebSocket 双向 UpgradegRPC 四种流式模式X-Accel-Buffering: no心跳防代理超时text/event-stream首字延迟对比
学习路径
  1. 读 5.2:对比 SSE 续传、WebSocket 与 gRPC 四种流式模式
  2. 跑内置代码,实现一条 SSE 并验证 Last-Event-ID 续传
  3. 完成练习自测:写清 X-Accel-Buffering:no 与心跳防代理超时
  4. 对接 M4:为网关交付可续传的流式输出
✔ 能按场景从四种流式协议里选出一种并给出理由
核心知识点详解
  • SSE 单向续传是 LLM 默认答案:SSE 走普通 HTTP + text/event-stream,天然支持 Last-Event-ID 断点续传、跨代理兼容性好、实现极简;LLM 输出本就是「服务器→客户端单向推送」,选它最划算。
  • WebSocket 双向但更复杂:全双工、需 Upgrade 握手,多数 LB/代理需显式配置才放行,断线重连要自己实现;只有需要客户端双向实时(如 Agent 交互)才值得引入。
  • gRPC streaming 适合对内:gRPC 四种模式(unary / server / client / bidi stream)跑在 HTTP/2 + protobuf;LLM 网关到模型常用 server-streaming,但浏览器不支持原生 gRPC(需 gRPC-Web / Connect),所以「对外 SSE、对内 gRPC」成为常见分层。
HTTP 缓存Cache-Control max-age 强缓存ETag / 304 协商缓存no-transform 保流式确定性请求缓存prefix cache 省 80–95%语义缓存阈值 0.92–0.97
学习路径
  1. 读 5.3:分清强缓存与协商缓存,以及 ETag 与 304
  2. 跑内置代码,为确定性请求开缓存并统计命中
  3. 完成练习自测:评估 prefix cache 省 80–95% 的前提
  4. 对接 M4:给网关加语义缓存并记录成本节省
✔ 能说明缓存选择题并给出已测量的命中率数字
核心知识点详解
  • 强缓存与协商缓存:强缓存 Cache-Control: max-age 本地直接命中、不发请求;协商缓存 ETag / 304 需回源但命中后只回 304 无 body。LLM 给确定性请求开缓存能省大量带宽与重复推理。
  • prefix cache 与语义缓存:同前缀 prompt 可共享 KV 缓存、中间重复 token 省 80–95% 计算;语义缓存按相似度阈值(0.92–0.97)命中,但要配合 X-Accel-Buffering: no 保流式不被代理缓冲。
  • 常见坑:缓存命中前提是「确定性」:只有确定性的请求才可安全缓存;带随机 / 温度 / 时间戳的请求缓存会出错。给带副作用请求开缓存会把二次结果当首次返回,务必用请求哈希 + 幂等键一起收紧缓存键范围。
练习与前沿MCP Streamable HTTPConnect 浏览器直连对外 SSE / 对内 gRPC
学习路径
  1. 读 5.4:了解 MCP Streamable HTTP 与 Connect 浏览器直连
  2. 跑内置代码,把对外 SSE、对内 gRPC 的接线写完
  3. 完成综合练习:为一组接口做协议选型并解释取舍
  4. 对接 M4:用选型结果填进网关的协议矩阵
✔ 能为一组真实接口给出协议选型并说明每个选择的代价
核心知识点详解
  • MCP Streamable HTTP 是 Agent 工具的主流:MCP 提供 streamable HTTP 单端点协议:POST 启动 + 经 SSE 回传流,一次请求里可携带工具调用结果;浏览器侧 Connect / streaming 是 gRPC-Web 的替代,能减少复杂协议栈。
  • 常见坑:协议矩阵与流控没对齐:选型表定了协议却没把心跳、Last-Event-ID、断线重试与代理超时一并给定,线上会忽然断流。填协议矩阵时把「建连 RTT、续传机制、断线行为、LB 兼容」四列一起填完才算选型完成。
学习路径

5.1 HTTP/1.1 → HTTP/2 → HTTP/3 的演进逻辑

版本传输层并发方式队头阻塞头部压缩典型问题
HTTP/1.1TCP每域名 6 个连接 + 请求排队有(应用层 + TCP 层)无并发靠多连接,握手成本高
HTTP/2TCP单连接多路复用(stream)TCP 层仍有(见下)HPACK丢包时所有 stream 一起卡住
HTTP/3QUIC(基于 UDP)独立流,应用层多路复用基本消除QPACKUDP 可能被企业防火墙拦截
ℹ
对 AI 服务的影响:LLM 服务高度依赖流式。HTTP/1.1 下一个连接只能跑一个请求,所以「一个用户开两个会话」就已经占用两条连接;HTTP/2 让同一连接承载多个流,是聊天类应用的明显优势。但如果你在 HTTP/2 上做长时流式(单 stream 持续几十秒),要特别注意流控窗口(HTTP/2 有 per-stream 与 connection 级 flow control)与中间代理的缓冲行为——很多代理默认会缓冲响应,导致流式失效。
bash# 一句话验证服务端到底支持哪个版本(含 h2 协商)
curl -sI --http2 https://api.example.com/health | head -1
#   HTTP/2 200              <- Upgrade 协商成功
curl -sI --http2-prior-knowledge https://api.example.com/health | head -1
#   HTTP/2 200              <- 跳过 Upgrade,直接 h2c/h2(服务端需支持)
curl -sI --http3 https://api.example.com/health | head -1
#   HTTP/3 200              <- 走 UDP/QUIC(curl 需编译 ngtcp2/quiche)
# 若 --http3 报 "HTTP/3 not supported",说明本地 curl 未编译 QUIC 支持。

# 看 ALPN 协商出的协议
openssl s_client -connect api.example.com:443 -alpn h2,http/1.1 </dev/null 2>/dev/null | grep ALPN
#   ALPN protocol: h2       <- 服务端优先 h2

# 验证 HTTP/2 是否真的单连接多路复用(两条请求复用同一连接)
curl -s --http2 -o /dev/null -w 'connects=%{num_connects}\n' \
  https://api.example.com/a https://api.example.com/b
#   connects=1              <- HTTP/2 多路复用生效
ℹ
2026 前沿:MCP 的传输层与 Connect 协议:① MCP(Model Context Protocol)的传输从早期的 stdio/SSE 演进到 Streamable HTTP(单个 HTTP 端点,POST 发请求、SSE 或 JSON 流式回响应),解决「SSE 需要两个端点 + 长连接难负载均衡」的问题;远程 MCP server 现多用流式 HTTP 部署在网关后。② Connect(Buf 出品)是「gRPC 但兼容普通 HTTP/1.1 与浏览器」的 RPC 协议:同一份 protobuf 定义可产出 gRPC / gRPC-Web / Connect 三种客户端,无需代理即可浏览器直连,2026 年在 BFF 层被大量采用。③ 结论:对外用 SSE/Connect、对内用 gRPC,正在成为 AI 服务的标准分层。

5.2 SSE / WebSocket / gRPC streaming / 长轮询:怎么选

方案方向协议基础断线重连代理友好适合
短轮询客户端拉HTTP天然最好低频状态查询(成本高,不推荐)
长轮询客户端拉(挂起)HTTP需自己实现好兼容性要求极高的老环境
SSE服务端单向推HTTP/1.1 或 2(text/event-stream)内置 Last-Event-ID好(但需关缓冲)LLM 流式输出(首选)
WebSocket双向HTTP Upgrade → 独立协议需自己实现心跳与重连中(部分代理/网关不支持)实时协作、双向交互、语音
gRPC streaming双向(四种模式)HTTP/2 + protobuf需自己实现(有 retry 策略)差(需支持 h2 的代理)服务间调用、内部 RPC
python# SSE 服务端最小实现(含心跳、断点续传、正确头部)
from fastapi import FastAPI, Request
from fastapi.responses import StreamingResponse
import asyncio, json, time

app = FastAPI()

@app.get('/v1/chat/stream')
async def stream(req: Request, q: str):
    async def gen():
        last_id = req.headers.get('Last-Event-ID')
        start = int(last_id) + 1 if last_id else 0
        # 说明:真实实现要能从 start 处续传(依赖服务端保存生成进度或从缓存重放)
        try:
            for i, tok in enumerate(generate(q)):
                if i < start: continue
                # SSE 格式:id / event / data / 空行分帧
                yield f'id: {i}\n'
                yield 'event: token\n'
                yield f'data: {json.dumps({"t": tok}, ensure_ascii=False)}\n\n'
                if i % 8 == 0:
                    yield ': ping\n\n'          # 注释行充当心跳,防代理超时断开
        except asyncio.CancelledError:
            raise                                # 客户端断开 → 正确清理
        yield 'event: done\ndata: {}\n\n'

    return StreamingResponse(gen(), media_type='text/event-stream', headers={
        'Cache-Control': 'no-cache, no-transform',   # no-transform 阻止代理改内容
        'X-Accel-Buffering': 'no',                   # 关键:让 Nginx 不缓冲
        'Connection': 'keep-alive',
    })
javascript// 浏览器端 SSE:断线自动续传 + 心跳看门狗 + 错误降级
function connect(url, onToken) {
  let lastId = localStorage.getItem('sse-last-id') || '';
  const es = new EventSource(url + (lastId ? '?last_event_id=' + lastId : ''));
  let lastMsg = Date.now();
  es.addEventListener('token', (e) => {
    lastMsg = Date.now();
    const { t } = JSON.parse(e.data);
    lastId = e.lastEventId;                 // 服务端发的 id: 字段
    localStorage.setItem('sse-last-id', lastId);
    onToken(t);
  });
  // 心跳看门狗:超过 30 s 无任何事件就主动重连(防中间设备静默断连)
  const wd = setInterval(() => {
    if (Date.now() - lastMsg > 30000) { es.close(); clearInterval(wd); connect(url, onToken); }
  }, 5000);
  es.addEventListener('done', () => { es.close(); clearInterval(wd); });
  es.onerror = () => { /* EventSource 会自动重连;持续失败则降级到轮询 */ };
  return es;
}
// 关键数字:EventSource 默认重连约 3 s,可用服务端 retry: 字段改为 1000–2000 ms;
// 服务端每 8–15 s 发一次注释心跳(": ping"),网关 60 s 空闲超时不会误断。
★
选型量化:让用户「看到第一个字」要多久:在 p95 网络条件下(RTT 80 ms),几种方案的首字延迟:短轮询平均 = 轮询间隔/2 + RTT(间隔 1 s → 约 580 ms,且大量空请求);长轮询约 1 RTT + 服务端处理(约 100–200 ms,但每次问答要重建);SSE 约 1 RTT 建流 + 服务端 TTFT,之后零额外控制往返;WebSocket 首次要 HTTP Upgrade(约 1.5 RTT)再推,之后最低。对 LLM 逐 token 输出,SSE 的「一次建流、全程零额外控制往返」几乎总是最优。

5.3 HTTP 缓存与条件请求:省钱的直接手段

python# 确定性请求缓存 + 语义缓存的骨架(Hamauls Orion 的成本优化组件之一)
import hashlib, json, time

def cache_key(model, messages, **params):
    """确定性请求的缓存键:模型 + 消息 + 影响输出的所有参数都必须进键
       注意:temperature > 0 的请求本质是随机的,缓存会改变语义,
             所以只对 temperature == 0 做严格缓存。"""
    if params.get('temperature', 0) != 0:
        return None                      # 非确定性请求不进严格缓存
    blob = json.dumps({'m': model, 'msg': messages,
                       'p': {k: params[k] for k in sorted(params)}},
                      sort_keys=True, ensure_ascii=False)
    return 'llm:' + hashlib.sha256(blob.encode()).hexdigest()

class TTLRedisCache:
    """带 TTL 与命中率统计的缓存包装(配合第 3 阶段的 LRU 使用)"""
    def __init__(self, redis, ttl=86400):
        self.r, self.ttl, self.hits, self.total = redis, ttl, 0, 0
    def get(self, key):
        if key is None: return None
        self.total += 1
        v = self.r.get(key)
        if v is not None: self.hits += 1
        return v
    def set(self, key, val):
        if key: self.r.setex(key, self.ttl, val)
    @property
    def hit_rate(self): return self.hits / self.total if self.total else 0.0
bash# 条件请求(协商缓存)实测:第二次响应应为 304 且无 body
curl -sI https://api.example.com/config | grep -i etag
#   ETag: "abc123"                    <- 记下这个值
curl -s -o /dev/null -w 'status=%{http_code} size=%{size_download}\n' \
  -H 'If-None-Match: "abc123"' https://api.example.com/config
#   预期输出:status=304 size=0        <- 省带宽,但仍有一次往返
# 对比强缓存 Cache-Control: max-age=86400:第二次请求根本不到服务端。

# 对 LLM:temperature=0 + 相同输入的响应可强缓存(用确定性缓存键)
curl -s -o /dev/null -w 'status=%{http_code} time=%{time_total}\n' \
  -H 'X-LLM-Cache-Key: sha256:...' https://api.example.com/v1/chat
#   命中缓存时 time_total 通常 < 10 ms;未命中(真调模型)为 500–3000 ms。
ℹ
2026 前沿:语义缓存与提示前缀缓存省下的钱:两类「缓存」在 AI 服务里价值巨大:① prompt prefix cache(前缀缓存):vLLM/SGLang 复用相同系统提示的 KV,命中时 prefill 成本可降 80–95%,TTFT 从数百 ms 降到几十 ms —— 这也是为什么「把稳定内容放系统提示最前面」是硬性工程约定。② 语义缓存:用 embedding 相似度判定可复用,阈值通常取 0.92–0.97(越高越安全),配合租户隔离与敏感数据排除;生产上命中率 15–35% 即可省下可观成本。两者都要有命中率监控与「错误复用」的抽样审计。

5.4 动手练习与自测

✔
练习目标:协议选型要有数字支撑:把「我觉得 SSE 好」变成「SSE 首字延迟低 X ms」。
题目关键数字 / 判据
版本探测ALPN 报 h2 / HTTP/3 走 UDP
多路复用num_connects = 1
流式缓冲X-Accel-Buffering: no 修复攒批
断线续传重复 token 数 0–1
缓存收益命中时延 < 10 ms
选型对比SSE 首字不劣于长轮询,空请求 0
  1. 版本探测:对一个真实站点用 curl 分别测 --http2/--http3,并看 ALPN。判据:能报出协商出的协议,判断是否支持 HTTP/3。
  2. 多路复用验证:用 --http2 一次请求两个 URL,看 num_connects 是否为 1。判据:HTTP/2 下应为 1(复用)。
  3. 流式被缓冲诊断:写一个最小 SSE 服务,前面套 Nginx 默认配置,观察内容是否被攒批;再加 X-Accel-Buffering: no 对比。判据:能复现并修复「一次性全部出现」。
  4. 断线续传:用浏览器脚本模拟中断(关闭再重连),确认从 Last-Event-ID 之后续传。判据:重复 token 数为 0 或可容忍 1 个。
  5. 缓存收益:为 temperature=0 的接口加确定性缓存,压测命中率与 P50 延迟。判据:命中时延 < 10 ms,报告命中率。
  6. 选型对比:同一场景分别用短轮询/长轮询/SSE 实现,测首字延迟。判据:SSE 首字不劣于长轮询,且空请求数为 0。

6. RPC、序列化与可靠调用的工程学

知识结构图 · RPC 与可靠调用
RPC、序列化与可靠调用的工程学4 大知识域 · 30 个知识点
序列化与 gRPCprotobuf 体积小 3–10 倍字段号永不复用reserved 标记删字段四种流式模式proto3 向后兼容Connect / gRPC-Web
学习路径
  1. 读 6.1:理解 protobuf 体积小与字段号永不复用
  2. 跑内置代码,定义 .proto 并生成客户端与服务端骨架
  3. 完成练习自测:用 reserved 演练向后兼容的字段演进流程
  4. 对接 M4:写好对内 gRPC 的协议定义与流式模式
✔ 能写一个 .proto 并在改字段时保持向后兼容
核心知识点详解
  • protobuf 用字段号省体积:protobuf 用整数字段号(tag)代替字段名字符串,体积小 3–10 倍、序列化快 2–10 倍;但字段号不能复用,删除字段要保留并用 reserved 标记,否则老客户端会解码错位。
  • proto3 向后兼容靠字段号与 wire 类型:field number + wire type 变化会破坏兼容;加字段用未用过的号、改类型视为不同字段。纪律:不复用号、不改变已有号类型、reserved 保留废弃号,才能放心改 .proto。
  • gRPC 四种流式与浏览器限制:unary / server-stream / client-stream / bidi-stream 四种模式,对 LLM 输出常用 server-streaming;但浏览器不支持原生 gRPC(需 gRPC-Web / Connect),proto 只在服务内部、对外仍走 SSE。
连接池与负载均衡池大小 = QPS × 耗时 ×1.5HTTP/2 每后端 1–2 连接L4 / L7 / 客户端侧 LB最小连接数 / 最少延迟服务发现 Consul / K8s长连接扩容难题keepalive_expiry < LB 超时
学习路径
  1. 读 6.2:用池大小≈QPS×耗时×1.5 估算连接数
  2. 跑内置代码,实测 HTTP/2 每后端 1–2 连接是否够
  3. 完成练习自测:对比 L4、L7 与客户端侧三种负载均衡
  4. 对接 M4:让 keepalive_expiry 小于 LB 超时以避免断连
✔ 能按公式定池大小并讲清长连接扩容为何难
核心知识点详解
  • 池大小公式 ≈ QPS × 耗时 × 1.5:并发连接需求 ≈ QPS × 平均耗时,留 ~1.5 倍余量应对 P99 长尾;HTTP/2 本身每个后端只需 1–2 条连接(多路复用),无需 1:1 建池。估池后先压测再收紧。
  • 长连接扩容比想得难:把池或半池调大,TCP 半连、TIME_WAIT、NAT 表、内核连接表都会被压爆;扩容要一起调整 somaxconn、fs.file-max、连接空闲超时清理,否则数字上去了连接反而崩。
  • 常见坑:keepalive 时间比 LB 超时长:客户端 keepalive expir 若长于 LB 空闲超时,空闲连接被 LB 静默切断,下次复用读到 EOF / 连接重置。让 keepalive_expiry < LB 超时 才能消除「闲置后首请求失败」的诡异错误。
可靠性六件套分层超时预算只重试 408/429/5xx指数退避 + 抖动重试预算防风暴幂等键 idempotency key熔断三态机令牌桶按 token 限流背压有界队列
学习路径
  1. 读 6.3:把超时、重试、幂等、熔断、限流、背压六件套各记一条要点
  2. 跑内置代码,实现带抖动与重试预算的指数退避
  3. 完成练习自测:做一次故障注入确认不雪崩
  4. 对接 M4:把幂等键与数据库唯一索引接到网关写路径
✔ 能在弱网下证明重试与熔断生效且不触发风暴
核心知识点详解
  • 超时预算决定哪里可放弃:端到端预算(如 P99 < 8s)分配到每一跳(模型 / 检索 / 工具),每跳之和 ≤ 上游预算;任一跳按自己预算用 asyncio.wait_for 取消,并把剩余时间逐级下传。
  • 重试只对「可重试」错误:只重试 408 / 429 / 5xx、连接错与超时;401/403/400 属于永久失败,重试只会浪费配额并放大故障。用指数退避 + 抖动 + 重试预算(如最多 3 次、总窗口 < 1s)防风暴。
  • 幂等键是重复副作用的唯一防线:同一请求携带同 Idempotency-Key,服务端用 DB 唯一键只执行一次;没有它,超时后的自动重试就可能重复下单 / 重复生成消耗 token。熔断三态(CLOSED / OPEN / HALF-OPEN)、令牌桶按 token 限流、有界背压队列共同保证不雪崩。
练习自测超时预算 P99 < 8s故障不雪崩验证
学习路径
  1. 读 6.4:按超时预算表核对端到端 P99 < 8s
  2. 跑内置代码,验证每一跳超时都控制在预算内
  3. 完成综合练习:做一次故障注入确认不雪崩
  4. 对接 M4:把超时预算表写进网关设计文档
✔ 能证明端到端超时预算成立且单一故障不外溢
核心知识点详解
  • 每一跳超时都要进预算表:预算表把「P99 < 8s」拆到 DNS / TCP / TLS / 模型 / 检索 / 输出,逐跳 wait_for 并取消下传;验收是注入慢下游后整体仍能在预算内降级,而不是整链卡死。
  • 常见坑:只测 happy path:不注入延迟 / 丢包 / 500,超时与熔断的代码可能根本没生效。三个故障用例(不雪崩 / 延迟有界 / 副作用不重复)是硬门槛,缺一个都不能验收。
学习路径

6.1 序列化与 gRPC 的四种流式模式

维度JSON over HTTPProtobuf over gRPC
体积较大(字段名重复)小 3–10 倍(字段用数字 tag,无冗余字段名)
序列化速度中等快 2–10 倍
模式演进无强制约束,易不兼容字段号机制 + 向后兼容规则
人类可读是(调试友好)否(需工具解码)
浏览器直连原生需 gRPC-Web + 代理
流式SSE / 分块原生四种模式
典型用途对外 API(LLM 接口、Web)内部服务间调用
protobuf// Hamauls Orion 的内部服务契约(节选):注意字段号一旦发布就不能改用途
syntax = "proto3";
package hamauls_orion.v1;

service Retriever {
  // 一元:单次查询
  rpc Search (SearchRequest) returns (SearchResponse);
  // 服务端流:边检索边推送候选(降低首字节延迟)
  rpc SearchStream (SearchRequest) returns (stream ScoredDoc);
  // 客户端流:批量上传文档(大文件分片)
  rpc Ingest (stream DocChunk) returns (IngestSummary);
  // 双向流:交互式调试(一边发查询一边收中间结果)
  rpc Debug (stream DebugCmd) returns (stream DebugEvent);
}

message SearchRequest {
  string query = 1;
  int32 top_k = 2;
  repeated string filters = 3;         // 新增字段必须用新编号,永不复用旧编号
}

message ScoredDoc {
  string doc_id = 1;
  float score = 2;
  string snippet = 3;
}
⚠
protobuf 演进的硬规则:① 字段号一旦使用永不复用——删字段要把编号标记为 reserved,否则新老版本会错位解析(这是最隐蔽的线上事故);② 新增字段必须是 optional 语义(proto3 默认行为),老客户端会忽略;③ 不要改字段类型(如 int32 → string);④ 不要依赖字段顺序或 repeated 的顺序。这些规则与你在 Java 里做接口兼容的经验一致,但因为没有强类型编译期检查,更容易踩坑。
python# 量一量 protobuf vs JSON 的真实体积差(决定内部 RPC 用什么)
import json
obj = {'doc_id': 'd-000123', 'score': 0.87,
       'snippet': 'Hamauls Orion 检索结果片段',
       'tags': ['rag', 'vector', 'rerank'], 'ts': 1730000000}
js = json.dumps(obj, ensure_ascii=False)
print('JSON 字节数   :', len(js.encode()))
# protobuf 近似:字段名不传输,只有 tag + 值(varint/length-delimited)
proto_approx = (2 + 7) + (1 + 4) + (1 + len('Hamauls Orion 检索结果片段'.encode())) \
             + (3 * 5) + (1 + 4)
print('proto 近似字节:', proto_approx)
print('压缩比        :', round(len(js.encode()) / proto_approx, 1), 'x')
# 预期:JSON 约 155 字节,proto 约 40 字节,压缩比约 3.5x
# 结论:高 QPS 内部调用用 protobuf;对外(浏览器/调试)用 JSON。
ℹ
2026 前沿:gRPC 的三个新变化:① gRPC-Web 与 Connect 让浏览器直连成为可能,很多团队因此删掉了专门的 REST 网关。② gRPC 原生重试/超时策略(service config 里的 retryPolicy、hedgingPolicy)可在 proto 侧声明,但要注意「重试会在服务端产生重复负载」,必须配合幂等。③ HTTP/3 上的 gRPC(gRPC over QUIC)已在实验/早期生产,弱网下多流不再互相阻塞;Envoy 与 grpc-go 都有实现。④ 另外,MCP 与 A2A 这类 Agent 互操作协议都倾向「HTTP + JSON-RPC / SSE」,而非纯 gRPC,原因正是浏览器与网关的兼容性。

6.2 连接池、负载均衡与服务发现

python# HTTP 客户端连接池的工程配置(httpx 示例,aiohttp/requests 思路一致)
import httpx
limits = httpx.Limits(
    max_connections=200,          # 全局并发上限(保护自己与下游)
    max_keepalive_connections=50, # 保活连接数(探查后按单连接吞吐调)
    keepalive_expiry=30.0,        # 空闲 30 s 回收,避免被对端/LB 静默断开
)
timeout = httpx.Timeout(connect=1.0, read=30.0, write=5.0, pool=2.0)
client = httpx.AsyncClient(limits=limits, timeout=timeout, http2=True)

# 为什么这些数字要分开设:
#   connect=1.0s  -> 建连慢就直接失败(不要拖到 read 超时)
#   pool=2.0s     -> 连接池排队超时,防止请求无声堆积(背压的一种)
#   keepalive_expiry 要 < 负载均衡的空闲超时(常见 60 s,但要留余量)
# 经验起点:池大小 约等于 目标 QPS x 单请求平均耗时(s),再乘 1.5 安全系数。
#   例:200 QPS x 0.05 s = 10 并发 -> 池取 15–30 足够;盲目设 500 只会压垮下游。
ℹ
2026 前沿:服务网格把可靠性从 SDK 搬到基础设施:Istio/Linkerd + Envoy 的典型能力:自动 mTLS、按路由的 L7 负载均衡(least-request / ring-hash)、本地限流、熔断(outlier detection,连续 5xx 自动摘除实例)、超时与重试、连接池管理,全部通过 CRD/YAML 声明,业务代码零侵入。对 AI 服务的价值:① 长流式连接的负载均衡与优雅重启由 sidecar 处理;② 多租户限流可按 header(tenant)在网格层做;③ 全链路 trace 自动注入。代价是额外一跳(约 0.5–1 ms)与运维复杂度,小团队可先用 SDK 层方案,规模上来再上网格。

6.3 超时、重试、幂等、熔断、限流、背压:AI 服务的可靠性六件套

这一节是本阶段最实用的部分。你的 Java 经验在这里高度可迁移,但 AI 服务有它自己的特点:单次请求耗时长(秒级到分钟级)、成本高(每次调用真金白银)、且天然不确定(可能超时也可能慢而有结果)。这三点让传统的「短超时 + 激进重试」策略完全失效。

(1)超时预算:自上而下分配,而不是各自随便设

text超时预算的正确设计方式(Hamauls Orion 的端到端链路)

  用户请求总 SLA:P99 < 8 s
  ├─ 网关鉴权 + 限流                    50 ms
  ├─ 查询改写(小模型 / 规则)          300 ms   ← 预算必须显式分配
  ├─ 检索(向量 + BM25 + 融合 + 重排)  800 ms
  │    ├─ 向量召回                     200 ms
  │    ├─ BM25 召回                    100 ms
  │    └─ 交叉编码器重排              500 ms
  ├─ 工具调用(Agent 场景,可多次)    2000 ms   ← 每次调用单独限时
  ├─ 模型生成(TTFT + 流式)           4000 ms
  └─ 余量(应对抖动)                   850 ms

规则:
 ① 下游超时之和 + 余量 ≤ 上游超时。绝不允许「下游比上游还长」——
    那会导致上游已经放弃、下游还在白白计算,浪费算力。
 ② 每层都设超时,且越靠内层越短(因为外层要留时间处理失败与降级)。
 ③ 超时值基于实测分布设定(P99 的 1.5–2 倍),而不是拍脑袋。
 ④ 超时必须能「真正取消」下游工作。Python 里注意 asyncio.CancelledError
    的传播,以及在同步阻塞调用上是无法取消的(要换成 async 或子进程)。

(2)重试:只重试该重试的,且必须带退避与抖动

pythonimport asyncio, random, httpx

# 可重试 vs 不可重试 —— 这个分类是重试逻辑的全部基础
RETRYABLE_STATUS = {408, 429, 500, 502, 503, 504}
NON_RETRYABLE_STATUS = {400, 401, 403, 404, 409, 422}      # 重试只会浪费配额

class Budget:                     # 重试预算:防止重试风暴放大故障
    def __init__(self, ratio=0.1):
        self.ratio, self.total, self.failed = ratio, 0, 0
    def allow(self):
        return self.total == 0 or self.failed / self.total < self.ratio
    def record(self, ok):
        self.total += 1
        if not ok: self.failed += 1

async def call_with_retry(fn, budget: Budget, tries=3, base=0.5, cap=8.0):
    """要点:① 只重试可重试错误 ② 指数退避 + 抖动(避免惊群同步重试)
             ③ 尊重 Retry-After 头 ④ 有全局重试预算,防止雪崩放大"""
    last = None
    for i in range(tries):
        try:
            r = await fn()
            if r.status_code in NON_RETRYABLE_STATUS:
                budget.record(False); return r              # 不重试,直接返回
            if r.status_code in RETRYABLE_STATUS:
                if not budget.allow():
                    return r                                 # 预算用尽,放弃重试
                last = r
                wait = min(cap, base * 2 ** i) * (0.5 + random.random() * 0.5)
                ra = r.headers.get('Retry-After')            # 服务端显式指示优先
                if ra and ra.isdigit(): wait = max(wait, float(ra))
                await asyncio.sleep(wait)
                continue
            budget.record(True); return r
        except (httpx.ConnectError, httpx.ReadTimeout, httpx.RemoteProtocolError) as e:
            last = e
            if not budget.allow(): break
            await asyncio.sleep(min(cap, base * 2 ** i) * (0.5 + random.random() * 0.5))
    raise RuntimeError(f'调用失败,已重试 {tries} 次: {last}')

# ⚠️ 最关键的一条:不可重试的错误绝不重试
#   401/403 是凭证问题;400/422 是参数问题;409 是状态冲突。
#   重试它们只会放大故障、烧掉配额。

(3)幂等性:让重试是安全的

(4)熔断、限流与背压

机制解决什么实现要点AI 场景注意
超时单次调用不超过预期分层设置、真正可取消LLM 调用要区分 TTFT 超时与整体超时
重试应对瞬时故障只重试可重试、退避 + 抖动、预算LLM 长调用重试成本高,优先考虑降级到小模型
熔断防止故障扩散三态机:closed → open → half-open;基于错误率对下游模型服务熔断后要走降级路径(缓存 / 小模型 / 模板回复)
限流保护自己不被压垮令牌桶 / 漏桶 / 滑动窗口;按用户、按租户、按接口按 token 数限流比按请求数更公平(长短请求成本差异巨大)
隔离(舱壁)限制故障影响范围线程池 / 信号量分区,不同租户或不同下游用不同池多租户 SaaS 必须做,否则一个客户的高并发拖垮所有人
背压让生产者感知消费者能力队列水位、拒绝策略、流控信号(HTTP/2 window、gRPC flow control)流式输出的写入速度必须能被客户端消费速度约束
pythonimport asyncio, time

class CircuitBreaker:
    """熔断器:closed → open → half-open 三态
       目的是「快速失败」,避免把请求都压在已经坏掉的下游上(那会让它更坏)"""
    def __init__(self, fail_threshold=5, window=10.0, cool_down=30.0, half_open_trials=3):
        self.fail_threshold, self.window = fail_threshold, window
        self.cool_down, self.trials = cool_down, half_open_trials
        self.state, self.fails, self.opened_at = 'closed', 0, 0.0
        self.trial_ok = 0

    def allow(self) -> bool:
        if self.state == 'closed': return True
        if self.state == 'open':
            if time.monotonic() - self.opened_at >= self.cool_down:
                self.state, self.trial_ok = 'half-open', 0
                return True
            return False
        return True                                  # half-open:放少量请求试探

    def record(self, ok: bool):
        if self.state == 'closed':
            self.fails = 0 if ok else self.fails + 1
            if self.fails >= self.fail_threshold:
                self.state, self.opened_at = 'open', time.monotonic()
        elif self.state == 'half-open':
            if ok:
                self.trial_ok += 1
                if self.trial_ok >= self.trials: self.reset()
            else:
                self.state, self.opened_at = 'open', time.monotonic()

    def reset(self):
        self.state, self.fails, self.trial_ok = 'closed', 0, 0

class TokenBucket:
    """令牌桶限流:平均速率 + 突发容量。AI 场景建议按 token 数而非请求数限流"""
    def __init__(self, rate, burst):
        self.rate, self.burst = rate, burst
        self.tokens, self.ts = burst, time.monotonic()
    def take(self, n=1.0) -> bool:
        now = time.monotonic()
        self.tokens = min(self.burst, self.tokens + (now - self.ts) * self.rate)
        self.ts = now
        if self.tokens >= n:
            self.tokens -= n
            return True
        return False

class BoundedQueue:
    """背压:队列满时不是无限堆积,而是明确拒绝或阻塞
       无限队列 = 延迟无限增长 + 内存爆掉,是最常见的设计错误"""
    def __init__(self, cap, reject_when_full=True):
        self.q, self.cap, self.reject = asyncio.Queue(maxsize=cap), cap, reject_when_full
        self.rejected = 0
    async def put(self, item):
        if self.q.full() and self.reject:
            self.rejected += 1
            raise RuntimeError('queue full: 触发背压,请客户端退避重试')
        await self.q.put(item)
★
背压是被严重低估的一环:「无限队列」看起来是「不丢请求」的友善设计,实际是最危险的隐患:请求在队列里等待的时间会无限增长,用户等到的是「超时」而不是「忙」,同时内存被撑爆、进程 OOM。正确的做法是让队列有界,满了就明确拒绝(返回 429 或 503)并让客户端退避重试。这样系统的延迟是有界的、行为是可预测的。
在流式场景里,背压还有一层含义:如果客户端消费慢,服务端必须能感知并降低生成速度,否则会产生「生成了但发不出去」的积压。HTTP/2 与 gRPC 的 flow control 就是为这个设计的。

(5)把超时预算变成会真正取消的代码

python# 分层的超时预算:外层时间到,内层必须真的被取消(而不是继续空算)
import asyncio, time

async def fake_io(sec):
    await asyncio.sleep(sec); return f'ok({sec})'

async def retrieve(q, budget_ms):
    # 内部再拆预算:向量 200、BM25 100、重排 500,用整体截止时间约束
    deadline = time.monotonic() + budget_ms / 1000
    async def stage(name, ms, coro):
        left = deadline - time.monotonic()
        if left <= 0: raise asyncio.TimeoutError(name)
        return await asyncio.wait_for(coro, timeout=min(ms / 1000, left))
    parts = await asyncio.gather(
        stage('vector', 200, fake_io(0.12)),
        stage('bm25',   100, fake_io(0.08)),
        return_exceptions=True,      # 单路失败不拖垮整体
    )
    return [p for p in parts if not isinstance(p, Exception)]

async def handle():
    t0 = time.monotonic()
    try:
        docs = await asyncio.wait_for(retrieve('q', 800), timeout=0.8)
        print('检索完成', round((time.monotonic() - t0) * 1000, 1), 'ms', docs)
    except asyncio.TimeoutError:
        print('整体超时 -> 走降级(用缓存 / 仅 BM25)')

asyncio.run(handle())
# 预期输出:检索完成 120.x ms ['ok(0.08)', 'ok(0.12)']
# 关键点:① wait_for 会真正 cancel 内部协程(IO 必须是 async 的,
#   同步阻塞调用无法被取消);② gather(return_exceptions=True) 做部分降级;
#   ③ 预算要留余量,别把各层之和写成上限(见 6.3 超时预算表)。
⚠
失败模式:超时了,但请求还在花钱:最隐蔽的成本泄漏:上游超时返回了,下游模型还在生成。因为多数「取消」只断了 HTTP 连接,而 vLLM 的推理是 batch 内共享计算、单请求取消不一定能立即释放算力。对策:① 服务端监听连接断开(await request.is_disconnected() / 客户端断开事件)并主动 abort 生成;② 用 max_tokens 与流式早停;③ 网关侧对取消的请求记指标(cancel rate),它常是「GPU 利用率高但有效产出低」的真凶。

6.4 动手练习与自测

✔
练习目标:六件套都要有可观测的数字:把每一条都写成代码并用故障注入验证。
题目关键数字 / 判据
超时预算下游之和 + 余量 ≤ 上游
分类重试只 408/429/5xx,退避 + 抖动 + 预算
幂等键并发重复仅 1 条副作用
熔断open 期快速失败 < 5 ms
token 限流长请求拒绝率更高
背压队列满后 P99 不爆
  1. 超时预算表:为你的链路写出自上而下的预算表,并断言「下游之和 + 余量 ≤ 上游」。判据:每层有数字,且代码里可取消。
  2. 分类重试:实现只重试 408/429/5xx、带指数退避+抖动+全局预算的调用器,注入 500 验证行为。判据:不可重试错误不重试;重试有上限与抖动。
  3. 幂等键:给一个写操作加 idempotency key + DB 唯一索引,重复提交返回相同结果。判据:并发重复请求只产生 1 条副作用记录。
  4. 熔断验证:注入下游连续失败,观察熔断器从 closed→open→half-open→closed。判据:熔断期间请求快速失败(5 ms 内),恢复后放量试探。
  5. 按 token 限流:令牌桶按 token 数计费,验证长请求比短请求更受约束。判据:相同请求数下,长请求的拒绝率更高。
  6. 背压:有界队列满时返回 429 + Retry-After,压测观察延迟有界、内存稳定。判据:队列满后 P99 不随负载增长而爆炸。

7. 网络诊断与可观测

知识结构图 · 网络诊断与可观测
网络诊断与可观测3 大知识域 · 22 个知识点
抓包与诊断curl -w 分层计时tcpdump / tshark 过滤ss -tin retrans / cwnd重传率 > 0.5% 报警RTT 分位 P50 vs P99bufferbloat 指纹分层排除服务端
学习路径
  1. 读 7.1:用 curl -w 分层计时与 ss -tin 提炼诊断数据
  2. 跑内置代码,抓一次包定位重传率与 RTT 分位
  3. 完成练习自测:用分层排除法把慢卡在服务端之外
  4. 对接 M4:为网关配好 OpenTelemetry 全链路 trace
✔ 能从一份抓包里读出延迟分位与重传率并排查到层
核心知识点详解
  • curl -w 把时间拆到各层:curl -w 给出 DNS / connect / TLS / first-byte / total 分段;先用它定位是网络还是服务端慢,再决定是否 tcpdump 深挖,是最快的分层诊断入口。
  • 重传率与 RTT 分位:ss -tin 读 retrans 与 rtt/rto;重传率 > 0.5% 报警,P50 与 P99 差异揭示排队抖动。单点 P99 高多半是丢包 / 重传 / 排队,不是服务端慢,配合 bufferbloat 指纹(延迟随流量涨)判读。
  • 常见坑:拿总时间一刀切定位:「整段慢」无法归因;要按层切出 DNS / TCP / TLS / 首字节 / 传输分别计时,否则把代理 / 链路的锅误判给网关,修错地方。
核心指标与容量TTFT P95 < 1–2sTPOT P95 < 50ms排队 P95 < 200ms按输出长度分桶OpenTelemetry 打点Little 定律算并发槽GPU 利用率 60–80%
学习路径
  1. 读 7.2:记住 TTFT P95 < 1–2s、TPOT P95 < 50ms 的预算
  2. 跑内置代码,用 Little 定律 20×3/0.7≈86 估并发槽
  3. 完成练习自测:按输出长度分桶统计分位
  4. 对接 M4:把 GPU 利用率 60–80% 作为容量基线
✔ 能按预算核出并发槽并给出可观测面板的关键指标
核心知识点详解
  • TTFT / TPOT / 排队预算:TTFT P95 < 1–2s、TPOT P95 < 50ms、排队 P95 < 200ms,并按输出长度分桶统计;这些预算钉死流式体验,超过即触发告警与扩容,是 LLM 网关的可观测核心。
  • Little 定律算并发槽:并发槽 ≈ 目标 QPS × 平均耗时,再除预期利用率,如 20×3/0.7 ≈ 86。用公式定容量而非拍脑袋,才能给出可复核、可回滚的数字依据。
  • GPU 利用率 60–80% 是健康带:训练 / 推理把 GPU 提到 60–80% 利用率但不满载,留出抖动与重算裕量;冲满 100% 往往是排队与背压的头号来源,务必配合 OpenTelemetry 打点观测瓶颈在哪一段。
练习自测20×3/0.7 ≈ 86 槽故障注入定位
学习路径
  1. 读 7.3:复算并发槽并做一次故障注入定位
  2. 跑内置代码,在注入的故障下观察 trace 能否定位
  3. 完成综合练习:写出网关的可观测清单
  4. 对接 M4:让每次请求可凭 trace ID 回放
✔ 能在注入故障下用 trace 复现并定位到具体环节
核心知识点详解
  • 故障注入回放定位:OpenTelemetry 全链路 trace 让每次请求凭 trace ID 回放各环节耗时;注入延迟 / 丢了 / 500 后应能在 trace 里一屏定位到具体环节,而不是靠猜去重跑。
  • 常见坑:trace 没接全链路:otlp 只打了网关、没打进模型 / 检索段,故障时会丢一环。打点要贯通 DNS → TCP → TLS → 网关 → 检索 → 模型 → 输出,才能真正回放并定位。
学习路径

7.1 抓包与诊断:从「猜」到「看」

症状首选工具看什么
整体慢curl -w(分段时间)把延迟拆成 DNS/建连/TLS/TTFB/传输
建连慢tcpdump / mtr是否 SYN 重传(说明丢包或对端不可达)
丢包 / 重传ss -i / netstat -s / Wireshark重传率、拥塞窗口变化
连接异常ss -tan / netstatTIME_WAIT、CLOSE_WAIT 堆积
协议层问题Wireshark 跟随 TCP 流HTTP/2 帧交错、SSE 分帧、TLS 告警
MTU 问题ping -M do -s从多大包开始失败(见 2.1 节)
服务端处理慢应用 trace(OpenTelemetry)注意:这不是网络问题,要分层排除
bash# 一套可复制的网络诊断流程

# ① 先用 curl 分段,判断问题在哪一层
curl -w "@-" -o /dev/null -s https://api.example.com/health <<'FMT'
    dns: %{time_namelookup}  conn: %{time_connect}  tls: %{time_appconnect}
    ttfb: %{time_starttransfer}  total: %{time_total}
FMT

# ② 判断是否丢包/重传(重传率高说明链路质量差)
ss -tin state established '( dport = :443 )' | grep -E 'retrans|rtt|cwnd'
#   retrans:x/y  → 重传数 / 总包数
#   cwnd         → 拥塞窗口大小(长期很小说明在持续退让)

# ③ 抓包(只抓关键包,避免文件爆炸)
sudo tcpdump -i eth0 -nn -s 0 -c 200 'tcp port 443 and (tcp[tcpflags] & (tcp-syn|tcp-fin|tcp-rst) != 0)'
# 导出 pcap 后用 Wireshark 看:Follow → TCP Stream,能看到完整交互

# ④ 看 TCP 层统计(内核视角的全局信息)
netstat -s | grep -iE 'retrans|timeout|overflow|drop'

# ⑤ 检查 TLS 与证书
echo | openssl s_client -connect api.example.com:443 -servername api.example.com 2>/dev/null \
  | openssl x509 -noout -dates -issuer -subject

# ⑥ 最后才看应用日志 —— 但前五步能排除掉「其实是网络问题」的误判
⚠
一个必须建立的思维习惯:线上「接口慢」的反馈里,真正是网络的通常不到三成:更多时候是服务端排队(GPU 打满、连接池耗尽)、下游调用串联(一次请求触发 5 次模型调用)、或者慢查询。所以永远先分层量化再归因——用 curl 分段、用 trace 看 span 耗时分布。凭直觉说「网络慢」然后去调内核参数,是浪费时间最常见的方式。
bash# tcpdump/tshark 常用过滤配方(直接抄)
# 只看握手与挥手(快速确认建连是否有重传)
sudo tcpdump -i any -nn 'tcp[tcpflags] & (tcp-syn|tcp-fin|tcp-rst) != 0 and port 443'
# 抓重传(同一序号重复出现)—— 最直接的质量指标
sudo tcpdump -i any -nn 'tcp port 443' -w retrans.pcap
tshark -r retrans.pcap -q -z io,stat,1 -Y 'tcp.analysis.retransmission'
#   输出按秒的重传计数;重传率 > 0.5% 就该查链路或换 BBR

# 统计 TLS 握手告警(协议层问题)
tshark -r t.pcap -Y 'tls.alert_message' -T fields -e tls.alert_message.desc
#   关注:handshake_failure / unrecognized_name / certificate_expired

# 观察 RTT 随时间膨胀(bufferbloat 的指纹)
tshark -r t.pcap -T fields -e tcp.analysis.ack_rtt | sort -n | tail -5
#   若 RTT 从 20 ms 持续爬到 200 ms 而吞吐不变 -> 缓冲膨胀(见 3.2)
★
判读清单:三个数字定性一次网络故障:拿到抓包后先算三个数:① 重传率 = 重传包/总包(1% 偏差,5% 严重)——高则查链路/拥塞控制;② RTT 分位(P50 vs P99,若 P99 远大于 P50 说明存在排队或 bufferbloat);③ 零窗口/重传超时次数(大于 0 说明接收端或链路成为瓶颈)。三个数字都不异常,那「慢」几乎一定不在网络层——去查服务端排队与下游串联。

7.2 AI 服务的核心指标与排队论直觉

指标全称 / 含义为什么关键目标量级
TTFTTime To First Token(首 token 延迟)用户感知的「开始响应」时间,主要受排队 + 预填充影响P95 < 1–2 s
TPOT / ITLTime Per Output Token(token 间隔)决定流式输出的流畅度,受解码速度影响P95 < 50 ms
端到端延迟请求到完整响应的总时长与输出长度强相关,要按长度分桶统计按 SLA 定
吞吐tokens/s 或 req/s决定成本与容量越高越好
并发数同时在处理的请求数与延迟共同决定所需 GPU 数—
排队时间进入队列到开始处理拥塞的先行指标,比延迟更早报警P95 < 200 ms
错误率 / 超时率按类型细分(4xx / 5xx / 超时 / 取消)区分「客户端问题」与「服务端问题」< 0.5%
GPU 利用率 + 显存计算与显存水位显存不足会直接 OOM;利用率过高会让延迟失控利用率 60–80%
python# 用 OpenTelemetry 给 Hamauls Orion 的关键路径打点(骨架)
from opentelemetry import trace, metrics
tracer = trace.get_tracer('hamauls_orion')
meter = metrics.get_meter('hamauls_orion')

ttft = meter.create_histogram('hamauls_orion.llm.ttft_ms', unit='ms')
tpot = meter.create_histogram('hamauls_orion.llm.tpot_ms', unit='ms')
tokens = meter.create_counter('hamauls_orion.llm.tokens_total')
queue_wait = meter.create_histogram('hamauls_orion.queue.wait_ms', unit='ms')

async def handle(req):
    with tracer.start_as_current_span('chat.request') as span:
        span.set_attribute('tenant', req.tenant)
        span.set_attribute('output_bucket', req.max_tokens // 256)   # 分桶标记
        # ① 排队:把「进队 → 出队」单独作为一个 span/指标
        t_enq = monotonic_ms()
        async with acquire_slot():
            queue_wait.record(monotonic_ms() - t_enq)
            # ② 检索:内部再拆子 span,才能定位到是哪个子步骤慢
            with tracer.start_as_current_span('retrieve'):
                docs = await retrieve(req.q)
            # ③ 生成:分别记录 TTFT 与 TPOT
            t0 = monotonic_ms(); first = None; n = 0
            async for tok in generate(req.q, docs):
                if first is None:
                    first = monotonic_ms()
                    ttft.record(first - t0)
                else:
                    tpot.record(monotonic_ms() - first - n * 0)   # 实际实现按上次时刻差
                n += 1
            tokens.add(n, {'model': req.model, 'tenant': req.tenant})
        span.set_attribute('output_tokens', n)
python# 容量规划:用 Little 定律反推需要多少并发槽位 / 多少张卡
def channels(lambd, service_time_s, utilization_cap=0.7):
    """所需并发槽位 c 约等于 lambda * service_time / rho,
       rho 为目标利用率上限(不要取 1.0)"""
    rho = min(utilization_cap, 0.99)
    return round(lambd * service_time_s / rho)

c70 = channels(lambd=20, service_time_s=3.0)               # 利用率 0.7
print(f'到达率 20 req/s、单请求 3 s、目标利用率 0.7 -> 需并发槽 {c70} 个')
# 预期:需并发槽 86 个(20*3/0.7 约 85.7)
# 推论:单实例 4 路并发扛不住 —— 需要足够并行度 + 排队控制。

c95 = channels(20, 3.0, utilization_cap=0.95)
print(f'利用率 0.95 时仅需 {c95} 个槽,但排队时间会非线性暴涨')
# 预期:约 63 个槽 —— 看似省资源,实则 P99 延迟失控(见 1.3 Little 定律)
# 结论:容量规划要按 cap=0.6–0.75 设计,而不是按 100% 利用率。
ℹ
2026 前沿:LLM 可观测的三个新指标:除了 TTFT/TPOT,2026 的生产实践新增:① prefill vs decode 时间占比(在 vLLM/SGLang 的 metrics 里可直接取,用于判断瓶颈在算力还是显存带宽);② KV cache 命中率 / prefix 复用率(直接对应成本与 TTFT);③ 取消率 cancel rate(客户端断开或超时导致的无效生成占比,是「GPU 很忙但不赚钱」的直接信号)。把这三点和排队时间 P95 一起做成面板,就能在延迟恶化前扩容。

7.3 动手练习与自测

✔
练习目标:让「慢」变得可定位:所有结论都必须有抓包或指标证据。
题目关键数字 / 判据
抓包配方重传率 / RTT 分位 / 零窗口次数
故障注入定位到「服务端处理」段
指标打点TTFT/TPOT/排队时间,按长度分桶
容量估算20×3/0.7 ≈ 86 槽
bufferbloatRTT 上升而吞吐不变
  1. 抓包配方:用上面的过滤抓一次真实请求,导出重传率、RTT 分位、零窗口次数。判据:三个数字都能给出并解释含义。
  2. 故障注入:给服务端注入 200 ms 延迟,用 curl -w 与 trace 分别定位到「服务端处理」段。判据:能证明这不是网络问题。
  3. 指标打点:给一个流式接口加 TTFT/TPOT/排队时间三个 histogram,按输出长度分桶。判据:能回答「P95 慢在排队还是解码」。
  4. 容量估算:给定 20 req/s、单请求 3 s,估算所需并发槽(利用率 0.7)。判据:约 86,并说明 100% 利用率的危害。
  5. bufferbloat 识别:在 tc netem 下观察 RTT 随时间膨胀,用 tshark 的 ack_rtt 佐证。判据:能指出 RTT 上升而吞吐不变的特征。

8. 小结:给 Hamauls Orion 的网络层定稿(里程碑 M4)

知识结构图 · 网络层定稿
小结:给 Hamauls Orion 的网络层定稿3 大知识域 · 20 个知识点
设计决策表对外 HTTP/2 + SSE对内 gRPC + protobufTCP_NODELAY + 25ms 批proxy_buffering off下游超时之和 ≤ 上游幂等键 + DB 唯一索引按 token 数限流OTel 全链路 trace
学习路径
  1. 读 8.1:对照决策表逐项核对外 SS、内 gRPC、TCP_NODELAY 等
  2. 跑内置代码,把 proxy_buffering off 与 25ms 批落地验证
  3. 完成练习自测:确认下游超时之和 ≤ 上游与幂等唯一索引
  4. 对接 M4:把这张决策表写进网关定稿文档
✔ 能给出一份可直接落地的网关决策表并逐项有依据
核心知识点详解
  • 对外 HTTP/2 + SSE / 对内 gRPC + protobuf:对外用 SSE 保续传与代理兼容,对内用 gRPC server-stream 走 protobuf 高效流式;两层协议分离、各自按最适选型,是 LLM 网关的推荐形态。
  • TCP_NODELAY + 关闭代理缓冲:流式连接开 TCP_NODELAY 避免 Nagle 拖慢 token,proxy_buffering off / X-Accel-Buffering: no 让代理不攒包直接转发。否则后端明明逐 token,前台却整段卡顿。
  • 下游超时之和 ≤ 上游 / 幂等唯一索引:调用链每跳透明下传剩余预算、逐跳可取消,端到端 ≤ 预算;写路径用幂等键 + DB 唯一索引兜底,单点故障不外溢。按 token 限流 + OTel trace 一并进决策表,逐项有依据才算定稿。
阶段收获5 分钟定位到层设计词汇统一训练瓶颈在网络故障注入测试
学习路径
  1. 读 8.2:把七张知识图收拢成一套统一网络词汇
  2. 跑内置代码,用 5 分钟定位一次慢请求到层
  3. 完成练习自测:讲清为什么训练瓶颈在网络
  4. 对接 M4:用故障注入给网关定稿做回归
✔ 能 5 分钟内把慢问题定位到层并说出统一决策词汇
核心知识点详解
  • 5 分钟定位到层:把七张知识图收拢成统一词汇:DNS / TCP / TLS / TTFT / TPOT / 重传 / 排队 / 背压。遇到慢请求按模板 5 分钟内锁到某一层,是这套网络知识的度量红利。
  • 训练瓶颈在网络:一次 all-reduce 的耗时(280 GB 在数百 Gbps 下也要数秒)往往大于单步计算,分布式训练提速的第一杠杆是减少通信量、让通信与计算 overlap,而非一味加卡。
  • 常见坑:决策词汇与诊断词汇分离:设计决策与排障如果两套语言,评审与定位会互相污染;故障注入回归应作为定稿的收口动作而非可选,避免「决策表好看、线上照样卡」。
综合练习时序图 ≥ 8 阶段故障注入三连协议选型 SSE / gRPC / MCP
学习路径
  1. 读 8.3:画一张大于等于 8 阶段的端到端时序图
  2. 跑内置代码,跑一轮故障注入三连并记录结果
  3. 完成综合练习:完成 SSE、gRPC、MCP 的协议选型表
  4. 对接 M4:把时序图与选型表合成网络层定稿
✔ 能凭记忆画出完整时序图并完成一次故障注入回归
核心知识点详解
  • 时序图 ≥ 8 阶段含 DNS/TCP/TLS/TTFT/TPOT:从点击 → DNS → TCP → TLS → HTTP/2 → 网关 → 检索 → 生成 → SSE 回传,每段标注延迟量级、协议要点与优化手段;这张图是架构基线,也是后续故障回归的对照物。
  • 故障注入三连是硬验收:分别注入延迟、丢包、下游 500,验证不雪崩、能降级、能恢复(pytest 三个用例);判据是「故障下仍有界」而不是一直转圈重试。
  • 常见坑:选型表不给数字:SSE / gRPC / MCP 的选型若没有「建连 RTT、续传、断线重连、LB 兼容」几列与数字,看起来合理实则落不了地;给出测得的 token 节省比例与容量提升才算闭环。
学习路径

8.1 一份可以直接落地的设计决策表

决策点选择理由
对外协议HTTP/2 + SSE浏览器原生支持、代理兼容好、单向流式足够
对内协议gRPC(HTTP/2 + protobuf)高效、四模式齐全、原生流控、契约清晰
双向交互(语音/协作)WebSocket只有双向场景才用,避免为它承担代理复杂度
连接管理连接池 + keep-alive,每后端 1–2 条 HTTP/2 连接摊薄握手成本;HTTP/2 多路复用已提供并发
流式优化TCP_NODELAY + 25ms/16 token 批处理 + 心跳避免 Nagle 与延迟确认叠加造成 40ms 卡顿
代理配置Nginx proxy_buffering off + X-Accel-Buffering: no否则流式失效(最常见的坑)
超时分层预算,下游之和 + 余量 ≤ 上游避免上游放弃而下游空算
重试只重试 408/429/5xx,指数退避 + 抖动 + 全局预算防重试风暴;幂等键保证安全
幂等工具调用与写操作一律带 idempotency key + DB 唯一索引Agent 重试不产生重复副作用
熔断基于错误率三态机,熔断后降级到小模型/缓存/模板故障隔离与体验兜底
限流按 token 数令牌桶(而非按请求数)长短请求成本差几十倍,按请求数不公平
背压有界队列 + 明确拒绝(429/503 + Retry-After)延迟有界、行为可预测
安全对外 TLS 1.3 + HSTS;对内 mTLS服务间不可信网络
观测OTel 全链路 trace + TTFT/TPOT/排队时间分桶指标能回答「慢在哪一段」
MTUK8s overlay 网络显式配置 MTU(1500 − 隧道开销)避免大 payload 幽灵超时
★
阶段验收标准:① 能画出并讲解 Hamauls Orion 的完整请求时序图,标注每段的延迟量级与优化手段;② 超时预算表已落地为代码(不是文档),且每一跳都能真正取消;③ 可靠性六件套有压测数字:在注入延迟、丢包、下游故障三种故障场景下,服务不雪崩、能降级、能恢复;④ 弱网实验有对比数据(如 tc netem 100ms 延迟 + 1% 丢包下,流式输出的中断率与自动重连成功率)。
bash# M4 收口自检脚本:一条命令跑完关键验证(骨架)
set -e
echo '== 1. 连接复用 =='
curl -s -o /dev/null -w 'connects=%{num_connects} total=%{time_total}\n' \
  https://orion.example.com/v1/health https://orion.example.com/v1/health
echo '== 2. 流式未被打断(应持续输出 > 5s)=='
curl -N -s https://orion.example.com/v1/chat/stream -d '{"q":"talk"}' | head -200
echo '== 3. HTTP 版本 =='
curl -sI --http2 https://orion.example.com/ | head -1
echo '== 4. TLS 配置 =='
echo | openssl s_client -connect orion.example.com:443 -tls1_3 2>/dev/null | grep -E 'Protocol|Cipher'
echo '== 5. 证书有效期 =='
echo | openssl s_client -connect orion.example.com:443 2>/dev/null | openssl x509 -noout -enddate
echo '== 6. 重传率(需在服务端执行)=='
ss -tin state established '( dport = :443 )' | grep -o 'retrans:[0-9/]*' | head
# 期望:connects=1;流式输出平滑;Protocol=TLSv1.3;证书 > 30 天;重传率低
ℹ
2026 形势提示:AI 服务网络层的三个确定性趋势:① HTTP/3 普及:移动端与跨区服务默认开 QUIC,TCP 回退必须保留;② 协议分层定型:对外 SSE/Connect、对内 gRPC、Agent 互操作用 MCP/A2A(HTTP + JSON-RPC/SSE),选型不再纠结;③ 可靠性与成本绑定:超时取消率、语义缓存命中率、prefix cache 命中率直接决定单位成本,网络层设计第一次成为「省钱」而不只是「稳定」的手段。把这三条写进你的架构基线文档。

8.2 这一阶段真正给了你什么

一种定位能力
面对「慢 / 断了 / 报错」,你能在 5 分钟内把问题锁定到具体某一层,而不是凭感觉猜测。这个能力在面试与故障复盘里价值极高。
一套设计词汇
超时预算、幂等键、背压、熔断、队头阻塞、TTFB/TTFT/TPOT——这些词让你能和架构师、SRE 用同一套语言讨论你的 AI 服务。
一个被忽视的事实
分布式训练的性能瓶颈常常在网络(all-reduce 通信量、拓扑感知、通信计算重叠)。懂网络的算法工程师,在集群调优上有明显优势。
python# 故障注入测试:验证六件套在"网络故障"下不雪崩(pytest 风格骨架)
import asyncio, pytest

@pytest.mark.asyncio
async def test_timeout_budget_cancels_downstream():
    # 下游故意慢 2 s,但上游预算 0.5 s
    async def slow(): await asyncio.sleep(2); return 'late'
    with pytest.raises(asyncio.TimeoutError):
        await asyncio.wait_for(slow(), timeout=0.5)
    # 判据 1:超时后下游确实被取消(计时断言整体 < 1 s 完成)
    # 判据 2:熔断器连续 5 次失败后进入 open,请求快速失败

@pytest.mark.asyncio
async def test_backpressure_rejects_when_full():
    q = asyncio.Queue(maxsize=2)
    await q.put(1); await q.put(2)
    assert q.full()
    # 判据:满队列时应拒绝(429/503 + Retry-After),而不是无限堆积

@pytest.mark.asyncio
async def test_idempotency_single_side_effect():
    # 同一 idempotency key 并发提交 10 次,DB 只应有 1 条记录
    results = await asyncio.gather(*[submit(key='k-1') for _ in range(10)])
    assert len({r['id'] for r in results}) == 1
# 运行:pytest -q  ->  期望 3 passed
# 这三个用例覆盖「故障不雪崩 / 延迟有界 / 副作用不重复」三条硬标准。

8.3 综合练习与自测

✔
练习目标:把整阶段收束成 M4 交付物:这 5 题对应 M4 里程碑的验收清单。
题目关键数字 / 判据
时序图≥ 8 阶段,含 DNS/TCP/TLS/TTFT/TPOT
超时预算注入慢下游仍在预算内降级
故障注入三连延迟/丢包/500 全过
协议选型SSE / gRPC / MCP+SSE
成本-可靠性token 节省比例 + 容量提升
  1. 时序图:画出 Hamauls Orion 从点击到流式结束的完整时序,标注每段延迟量级与协议。判据:至少 8 个阶段,含 DNS/TCP/TLS/TTFT/TPOT 数字。
  2. 超时预算落地:把预算表写成代码并证明每跳可取消。判据:注入慢下游时,整体在预算内返回降级结果。
  3. 故障注入三连:分别注入延迟、丢包、下游 500,验证不雪崩、能降级、能恢复。判据:三个用例全过,且有指标佐证。
  4. 协议选型:为「对外聊天 + 对内部检索 + Agent 工具调用」分别选协议并给理由。判据:SSE / gRPC / MCP(HTTP+SSE) 或 Connect,理由含兼容性与流控。
  5. 成本-可靠性:给出「缓存命中率上升与取消率下降」对单位成本的量化影响。判据:能算出省下的 token 比例与对应的 QPS 容量提升。

项目里程碑

贯穿项目 · Hamauls Orion
M4 服务协议与流式网关 第 17–22 周

为 Hamauls Orion 设计对外协议:面向前端的 HTTP/2 + SSE 流式网关,面向内部的 gRPC 服务间调用;实现连接池、背压、超时预算、重试语义与幂等键。这是「模型能力」变成「产品体验」的那一层。

本阶段产出(直接进入项目仓库)
验收标准:能画出一张完整的请求时序图(含握手、多路复用、流式分帧、超时与重试);在 10% 丢包 / 200ms 延迟的弱网模拟下,流式体验不中断且能自动重连。

阶段练习项目

PROJECT 1
端到端网络时序图
为 Hamauls Orion 画一张完整时序图:从浏览器点击 → DNS → TCP → TLS → HTTP/2 → 网关 → 检索 → 生成 → SSE 流式回传,标注每段的延迟量级、协议要点与优化手段。这张图会成为整个项目的架构基线文档。
要达成的效果
  • 标注 ≥ 8 个阶段,每段给出延迟量级(如 DNS 5–50ms、TCP 1 RTT、TLS 1 RTT)并区分网络与服务端耗时
  • 每个阶段至少一条可落地的优化手段(连接复用、DNS 预取、缓存、100 批等)
功能需求
  • 按「点击 → DNS → TCP → TLS → HTTP/2 → 网关 → 检索 → 生成 → SSE 回传」逐段画独立节点,标明请求/响应方向与协议
  • 分别标注 TTFT(prefill + 排队)与 TPOT(解码),并注明为何它们不是网络问题
  • 在图中标出至少 2 处易错点(如代理缓冲、Nagle、MTU 分片、握手不复用)
  • 用 Mermaid / draw.io 生成,附 Markdown 说明文档一起提交
交付物
  • net-seq.md 架构时序图 + 逐段延迟说明表
  • 一份「排障顺序」速查表(从哪一层先查、用什么命令)
边界 · 不做

不做真实抓包与压测(那是弱网/诊断手册练习的职责);只交付静态时序与标注文档。

PROJECT 2
流式网关实现
用 FastAPI 实现 SSE 网关:正确头部、心跳保活、Last-Event-ID 断点续传、客户端断开时的资源清理、以及 25ms 批处理策略。用 curl -N 与浏览器分别验证。
要达成的效果
  • curl -N 与浏览器均能实时逐 token,首 token 在预算内(TTFT P95 < 2s)
  • 客户端断开后网关能快速取消并回收协程/连接,不泄漏内存
  • 断线后续传:用 Last-Event-ID 能从最后一个成功事件继续而非从头跑
功能需求
  • 返回正确的 text/event-stream 头部(Cache-Control:no-cache、Connection:keep-alive、X-Accel-Buffering:no)
  • 实现心跳保活(如 15s)防代理超时,事件带 id 以支持 Last-Event-ID 续传
  • 25ms 批处理:把小事件按批 flush,分别测量有/无批的首字延迟与吞吐
  • 客户端断开时取消后台模型任务(不烧多余 token)并清理连接资源
交付物
  • gateway/sse.py(StreamingResponse 示例 + 心跳 + 续传)
  • tests/:头部、续传、断开清理、批处理四组测试
  • 一张数字对比表(有/无心跳、有/无批、断开回收是否生效)
边界 · 不做

不做鉴权与多用户会话;不接真实模型,用可配置的模拟流式后端即可。

PROJECT 3
可靠性六件套
实现超时预算 + 分类重试(含抖动与预算)+ 幂等键 + 熔断器 + 按 token 限流 + 有界背压队列,并写故障注入测试(延迟、丢包、下游 500/超时)验证行为正确。
要达成的效果
  • 三个故障用例全过:不雪崩、延迟有界、副作用不重复
  • 注入下游 500/超时时整体仍能在预算内降级,无请求风暴
  • 同一幂等键并发提交 10 次,DB 只落 1 条记录
功能需求
  • 超时预算:端到端 P99 < 8s,逐跳用 asyncio.wait_for 可取消并下传剩余时间
  • 分类重试:仅 408/429/5xx/超时走指数退避 + 抖动,401/400 立即失败;设重试预算(如最多 3 次、总窗口 < 1s)防风暴
  • 幂等:idempotency key + DB 唯一索引,重试不产生重复副作用
  • 熔断三态(CLOSED/OPEN/HALF-OPEN)+ 令牌桶按 token 限流 + 有界背压队列(满则返回 429/503 + Retry-After)
  • 提供脚本注入延迟/丢包/500 并在 pytest 中验证三个判据
交付物
  • reliability/:budget、retry、idempotency、breaker、limiter、backpressure 模块
  • 故障注入测试 tests/fault_injection.py
  • 故障注入报告(注入项、观测指标、是否在预算内)
边界 · 不做

不做限流的持久化与多实例共享;熔断状态放本地内存即可(单实例演示)。

PROJECT 4
弱网实验报告
用 tc netem 模拟 100ms 延迟 + 1% 丢包 + 带宽限制,对比 CUBIC 与 BBR 的吞吐、以及流式体验(中断率、重连成功率、TTFT),输出带数字的报告。
要达成的效果
  • 给出 CUBIC 与 BBR 在 100ms + 1% 丢包下的吞吐(iperf3)对比并说明成因
  • 量化弱网对流式体验的影响:中断率、重连成功率、TTFT P50/P99
  • 报告能直接支撑「真机走 UDP + 重连」还是「TCP + Keep-Alive」的选型结论
功能需求
  • 用 tc netem 注入 delay 100ms / loss 1% / rate 限制到同一条链路
  • 分别用 CUBIC 与 BBR 跑吞吐并记录 RTT 分位与重传率
  • 对同一流式负载统计中断率、客户端重连成功率与 TTFT 分布
  • 给出机制解释(BBR 不依赖丢包反馈、重连对断流体验的影响)
交付物
  • 实验脚本(netem 注入 + 压测 + 数据采集)
  • 弱网实验报告.md(数字 + 图表 + 选型建议)
边界 · 不做

不做多机异地/真实弱网(用本机 netem 模拟即可);不接真实模型推理。

PROJECT 5
抓包诊断手册
针对 6 种典型故障(建连失败、TLS 失败、大包超时、流式被缓冲、TIME_WAIT 堆积、CLOSE_WAIT 泄漏),各给出「现象 → 抓包命令 → 判读依据 → 修复方式」的完整处方。
要达成的效果
  • 6 种故障每个都有「现象 → 抓包命令 → 判读依据 → 修复方式」四段式处方
  • 每条处方给出可直接运行的 tcpdump/ss/curl 命令与关键判读特征
  • 手册足够让零基础排障者按步骤复现并自愈
功能需求
  • 覆盖:建连失败、TLS 失败、大包超时、流式被缓冲、TIME_WAIT 堆积、CLOSE_WAIT 泄漏
  • 每条含抓包命令(如 tcpdump -nnXs0 -w x.pcap)与 1–2 条关键判读特征
  • 「流式被缓冲」给 proxy_buffering off / X-Accel-Buffering: no 的解法
  • 手册按「先 curl -w 分层 → 再 tcpdump 定位 → 修复验证」组织
交付物
  • net-diagnostics.md(6 条处方)
  • 可复用预检脚本 diag.sh(一条命令跑完各层初检)
边界 · 不做

不做自动化告警系统与指标面板(属可观测练习的范畴);只交付诊断处方与脚本。

PROJECT 6
集合通信核算脚本
写一个脚本:输入模型参数量、卡数、互联带宽与并行策略(DP/TP/PP),输出每步 all-reduce 通信量、理论通信耗时、以及推荐的并行方案与互联要求。
要达成的效果
  • 给定参数量/卡数/带宽/并行策略,输出每步 all-reduce 通信量(GB)
  • 得到理论通信耗时(通信量 ÷ 聚合带宽 × 系数)并给出推荐的并行方案
  • 能对比至少 3 种策略(DP/TP/PP)并给出互联要求结论
功能需求
  • 按模型参数量推导 each-step all-reduce 通信量(量级参考 2×P 与卡数分摊)
  • 支持 DP/TP/PP 三种并行策略的通信量与耗时估算,参数可改
  • 输出「推荐的并行方案 + 所需互联带宽」建议(结合 NVLink / IB / RoCEv2)
  • 脚本参数化:改参数量与卡数即可直接重算,不写死数字
交付物
  • comm_accounting.py(CLI + 参数化输入)
  • 一组示例输入与对应输出说明(数字可复核)
边界 · 不做

不做真实 NCCL 基准实测与自动调优;只做理论估算与决策建议。

常见误区

面试高频问题速答

一次 HTTPS 请求的延迟由哪些部分组成?怎么优化?

按阶段拆:① DNS 解析(5–50 ms,优化:缓存、预解析、就近解析);② TCP 三次握手 1 RTT + ③ TLS 1.3 握手 1 RTT(合计 2–3 RTT,同机房 <1 ms、跨洋可达 300–400 ms,优化:连接池/长连接、HTTP/2 多路复用、会话复用、0-RTT、就近部署);④ 服务端处理(常常是最大的一块,但这不是网络问题,要分开归因);⑤ 首字节传输(TTFB);⑥ 数据回传(传输延迟 = 数据量/带宽,以及拥塞排队)。物理层还有传播延迟(距离/光速),是跨地域的硬下限。优化的优先级取决于测量结果:跨地域先解决握手成本,同机房则要去看服务端。

TCP 为什么是三次握手?四次挥手又为什么不能是三次?

三次握手是为了让双方都确认「自己的发送能力」和「对方的接收能力」:第 1 次服务端确认客户端能发,第 2 次客户端确认服务端能收能发,第 3 次服务端确认客户端能收。两次不够(服务端无法确认客户端收到了自己的 SYN,历史重复 SYN 会导致错误建连)。四次挥手是因为 TCP 是全双工的,两个方向必须分别关闭:客户端发 FIN 只表示「我不再发送」,服务端先 ACK(此时处于半关闭,仍可继续发送数据),等自己数据发完再发 FIN,客户端最后 ACK。第 2、3 步不能合并,因为服务端可能还有数据要发。

TIME_WAIT 和 CLOSE_WAIT 分别说明什么问题?

TIME_WAIT 出现在主动关闭方,等待 2×MSL(约 60 秒)。作用是:① 确保最后那个 ACK 能到达对端(若丢失,对端会重发 FIN,此时还需有连接上下文响应);② 让旧连接的报文在网络中彻底消散,避免串到同四元组的新连接上。服务端大量 TIME_WAIT 通常说明在频繁主动关闭短连接,解法优先级是:改用连接池/长连接(根本)> 让客户端主动关闭 > 谨慎使用 tcp_tw_reuse。CLOSE_WAIT 表示「对端已关闭,但本端还没调用 close()」,堆积说明代码里有连接泄漏(异常路径没释放),这是 bug 而不是配置问题。

拥塞控制和流量控制有什么区别?BBR 相比 CUBIC 好在哪?

流量控制(rwnd)是「别把接收方撑爆」,由接收方通过窗口字段告知;拥塞控制(cwnd)是「别把网络撑爆」,由发送方根据网络反馈推断;实际发送窗口 = min(rwnd, cwnd)。拥塞控制的演进:Reno/CUBIC 以「丢包」作为拥塞信号——CUBIC 用三次函数增长、与 RTT 解耦,在有线网上表现好,但会把非拥塞原因的丢包误判为拥塞而激进退让,且在缓冲区大的链路上会填满缓冲导致延迟暴涨(bufferbloat)。BBR 不依赖丢包,而是主动测量「瓶颈带宽 × 最小 RTT」来建模管道容量,因此在高延迟、有丢包的链路(跨洋、移动网络)上吞吐显著更高,同时队列延迟更低。代价是与 CUBIC 共存的公平性存在争议。

SSE、WebSocket、gRPC streaming 分别适合什么场景?

SSE 是服务端单向推送,基于普通 HTTP(text/event-stream),代理与 CDN 兼容性最好,内建 Last-Event-ID 重连语义,是 LLM 流式输出的首选。WebSocket 是全双工,通过 HTTP Upgrade 切换到独立协议,适合需要客户端持续上传或极低延迟双向交互的场景(语音、实时协作、游戏),代价是代理兼容性与重连复杂度更高。gRPC streaming 有四种模式(一元、服务端流、客户端流、双向流),基于 HTTP/2 + protobuf,效率高、契约清晰、原生流控,适合服务间调用;浏览器直连需 gRPC-Web + 代理,所以对外通常用 SSE/HTTP、对内用 gRPC。选型判据:方向(单向还是双向)、部署环境(是否有严格的代理限制)、以及是否需要跨语言强契约。

为什么流式输出会卡顿?怎么排查和解决?

最常见的原因有三个:① Nagle 算法与延迟确认叠加——发送端攒小包等 ACK,接收端等最多 40 ms 再回 ACK,导致每个小块可能被延迟 40 ms。解法是设置 TCP_NODELAY,并把 token 聚成小批(如 20–50 ms 或 16 个 token 一批)再发送。② 中间代理缓冲——Nginx 默认会缓冲响应,导致内容被攒起来一次性下发。解法是设置 X-Accel-Buffering: no 与 proxy_buffering off,并在响应头加 Cache-Control: no-transform。③ 中间设备超时断开——长时间无数据会被判定为空闲连接,需要发心跳(SSE 的注释行 `: ping`)。此外还要注意 HTTP/1.1 单域名 6 连接上限导致的排队,以及客户端消费慢造成的反向积压(需要背压)。排查顺序:先 curl -N 直连服务端(排除代理)→ 再加代理 → 再看客户端解析逻辑。

为什么说分布式训练的性能瓶颈可能在网络?怎么算?

以数据并行为例,每次迭代都要做一次梯度 all-reduce。Ring AllReduce 的每卡通信量约为 2(N−1)/N × S(S 是模型参数字节数),N 大时趋于 2S。以 70B 模型 bf16 为例,S ≈ 140 GB,每步通信量约 280 GB;用 400 Gbps(50 GB/s)RDMA 需要 5.6 秒,用 25 GbE(3.1 GB/s)需要 90 秒——后者完全不可行。这解释了为什么大模型训练必须用 RDMA/InfiniBand 与高带宽 NVLink,为什么张量并行只在节点内(NVLink 域)做而数据并行可以跨节点,以及为什么需要通信计算重叠(按层计算完梯度立刻发起 all-reduce,把通信藏在后续反向传播里)与 ZeRO/FSDP(分片降低通信量与显存)。NCCL 还会做拓扑感知选路,所以 GPU 的插卡方式也会影响速度。

学习资源

计算机网络:自顶向下方法(Kurose & Ross)书 gaia.cs.umass.edu/kurose_ross/index.php 本阶段主教材。自顶向下的讲法最适合工程师:从应用层开始,到传输层、网络层。重点第 2 章(应用层:HTTP、DNS、CDN)、第 3 章(传输层:TCP 与拥塞控制)、第 4 章(网络层)。 High Performance Browser Networking(免费在线)书 hpbn.co/ 把「TCP/TLS/HTTP2/QUIC 如何影响真实性能」讲到极致的一本书,免费在线。对做流式与低延迟服务的工程师几乎是必读。重点章节:TCP、TLS、HTTP/2、浏览器网络。 Beej's Guide to Network Programming教程 beej.us/guide/bgnet/ 如果要用 socket 写点东西(哪怕只是实验),这是最短上手路径。讲 getaddrinfo、socket 选项、非阻塞 IO 的实现细节。 HTTP/3 explained(Daniel Stenberg)文档 http3-explained.haxx.se/ curl 作者写的 HTTP/3 与 QUIC 科普,免费在线。清晰解释 QUIC 为什么能消除 TCP 队头阻塞、连接迁移、以及 0-RTT 的代价。 MDN — Server-Sent Events文档 developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events SSE 的权威参考:事件格式、重连机制(Last-Event-ID)、与 WebSocket 的对比。做 LLM 流式输出必读。 gRPC 官方文档 — 核心概念与四种流式模式文档 grpc.io/docs/what-is-grpc/core-concepts/ 一元/服务端流/客户端流/双向流的定义与适用场景,以及 HTTP/2 之上的映射方式。 NCCL / 集合通信文档与 Megatron-LM 论文文档 docs.nvidia.com/deeplearning/nccl/user-guide/docs/usage/collectives.html all-reduce / all-gather / reduce-scatter 的定义、通信量公式与 NCCL 的算法选择逻辑。理解分布式训练网络必读。 Latency Numbers Every Programmer Should Know参考 gist.github.com/jboner/2841832 一份流传很广的延迟量级清单(含 L1、主存、SSD、同机房网络、跨洲网络)。配合第 2 阶段的存储层次一起记,能建立完整的数量级直觉。 Cloudflare / AWS 博客 — 架构中心(Architecture Center)文档 aws.amazon.com/architecture/ 生产级网络架构的参考实现:超时、重试、熔断、背压、多区域部署的官方最佳实践文档,含具体数值建议。 opentelemetry-python + 分布式追踪入门文档 opentelemetry.io/docs/languages/python/ 给你的 Hamauls Orion 服务加上全链路追踪。重点理解 span 的父子关系如何映射到「一次请求的各个阶段」,这是定位「慢在哪一段」的唯一可靠手段。
★
把这一阶段与 Hamauls Orion 的对应关系记住:你在这门课里学的每一个概念,都会在 Hamauls Orion 里变成一个具体文件:超时预算 → serving/timeout.py;SSE 网关与心跳 → serving/gateway.py;gRPC 契约 → proto/hamauls_orion.proto;重试与幂等 → serving/retry.py 与工具调用的 idempotency key;熔断限流背压 → serving/resilience.py;追踪与指标 → observability/。学完这一阶段,你就能把这些文件从骨架填成生产级代码——这就是 M4 里程碑的全部内容。