第41章 更刁钻的流量:gRPC、长轮询、WebRTC、大文件、双向流
学习目标
- 在多跳链里正确代理 gRPC(端到端 h2/trailers)与浏览器侧的 gRPC-Web
- 理解长轮询为何是"伪装成 HTTP 的长连接",及它的代理要点
- 看清 WebRTC 为什么不能像 TCP 那样代理:信令走 WS、媒体走 UDP/TURN
- 处理大文件上传/下载的请求缓冲、体积上限与背压
前置知识
原理
第40章 通了常见 6 类,这章啃硬骨头——它们要么打破"HTTP 请求-响应"假设,要么根本不走 TCP。
gRPC:端到端 h2 + trailers,断一跳就废
gRPC = HTTP/2 + length-prefixed 消息 + trailers 里放 grpc-status(第07章/第36章)。在多跳链里,它的铁律是:
整条链必须端到端保持 h2 + 转发 trailers。 任何一跳把 h2 降级成 h1(再到后端),gRPC 就废——h1 没有 trailers、没有 h2 帧。
每跳的正确做法:
- nginx:用
grpc_pass(不是proxy_pass!)转发到 gRPC 后端,监听http2:server { listen 443 ssl http2; location / { grpc_pass grpc://backend:50051; } # grpcs:// 则到后端也 TLS } - ingress-nginx:加注解声明后端是 gRPC:
nginx.ingress.kubernetes.io/backend-protocol: "GRPC" - Envoy/Mesh:原生 h2,是 gRPC 代理首选(第15章)
gRPC 的负载不均(重灾区):一条 h2 长连接复用所有调用,L4 连接级 LB 把它焊死到一个后端(第09章/第39章),扩容后新 Pod 没流量。解法三选一:
- L7 请求级 LB(Envoy/Linkerd 在 stream 粒度分发)
- 客户端侧 LB:gRPC 客户端用 headless Service(DNS 返回所有 Pod IP)+ 内置
round_robin - proxyless gRPC(xDS 直连,去掉中间代理)
gRPC-Web:浏览器进不了原生 gRPC
浏览器无法发原生 gRPC——fetch/XHR 读不到 HTTP trailers,也控制不了 h2 帧。所以浏览器用 gRPC-Web:把 gRPC 编码成浏览器能处理的形式(trailer 放进 body 末尾),再由一个翻译代理还原成真 gRPC 发给后端:
浏览器 ──gRPC-Web(HTTP/1.1 or h2)──▶ [翻译代理] ──真 gRPC(h2+trailers)──▶ 后端
Envoy grpc_web filter / grpcwebproxy
所以浏览器要调 gRPC 服务,链里必须有一跳做 gRPC-Web 翻译(Envoy 的 envoy.filters.http.grpc_web,或独立 grpcwebproxy)。少了这一跳,前端报"trailers 缺失"类错误。
长轮询:伪装成 HTTP 的长连接
长轮询(long polling,Comet)是 WebSocket/SSE 不可用时的回退:
客户端 → GET /poll (普通 HTTP 请求)
服务端 → 【挂起这个请求】,直到有数据 or 超时,才返回
客户端 → 收到响应后【立即再发一个 /poll】,如此循环
它看起来是普通 HTTP,但每个请求被服务端held open 很久。代理要点:
- 长 read 超时(每跳,铁律一)——否则请求在"挂起等数据"时被代理当成超时掐断
- 不缓冲(铁律二)——响应来得晚,缓冲没意义还增延迟
- 大量 held-open 连接:N 个客户端 = N 条挂起的连接占着 fd/内存(第35章)——比普通 HTTP 吃资源
- 没有 Upgrade(区别于 WS),所以不用动逐跳头
WebRTC:信令走代理、媒体不走代理
WebRTC 是这章最反直觉的——它的媒体流根本不经过你的反向代理:
① 信令(offer/answer/ICE 候选)走 WebSocket/HTTPS → 经正常 L7 多跳链
② 媒体(音视频,SRTP over UDP)走 P2P 直连;NAT 打不通时经 TURN 中继
↑ STUN/ICE 做 NAT 穿透
- 信令:普通 WebSocket(第40章),走你的 nginx/Ingress L7 链
- 媒体:UDP(SRTP/DTLS),是 P2P 或经 TURN 中继。TURN 服务器(如 coturn)本质就是一个 UDP 中继代理(第09章 UDP 伪会话)——当双方 NAT 无法直连时,媒体经 TURN 转发
- K8s 落地难点:UDP + 大量端口 + NAT。TURN 通常用
hostNetwork或LoadBalancer(UDP),不走 ingress-nginx(它对 UDP 支持有限,虽有udp-servicesConfigMap)。媒体端口范围大,要在防火墙/SG 放行
一句话:"代理 WebRTC" = 用 L7 链代理它的信令(WS)+ 部署 TURN 中继它的媒体(UDP)。 别指望反向代理转发音视频。HTTP/3/QUIC(第07章)同理也是 UDP,代理方式和 TCP 完全不同。
大文件上传/下载:缓冲、体积上限与背压
大文件踩三个坑,全和前面的机制呼应:
- 请求体缓冲(上传):nginx 默认会把请求体缓冲完才转发给后端(
proxy_request_buffering on),大上传 → 占内存/落临时文件/增延迟。流式上传要proxy_request_buffering off,但关了就没法重试(body 已流走)——权衡。 - 体积上限(每跳,木桶):nginx 默认
client_max_body_size 1m,超了 413 Request Entity Too Large。每跳都要放开:client_max_body_size 0; # 0 = 不限(边缘)漏一跳就 413——又是木桶效应。nginx.ingress.kubernetes.io/proxy-body-size: "0" # ingress 跳 - 背压(下载,第34章):慢客户端下大文件,代理必须流式 + 背压,否则内存爆;下载侧配合
sendfile/零拷贝(第12章)。
双向流:半关闭 + 双向背压同时上
gRPC 双向流、或任何全双工长连接,两个方向同时活跃——第35章 的半关闭(一个方向先结束要 CloseWrite 而非 close)和 第34章 的双向独立背压(每个方向各自一套流控)在这里同时成为必须。玩具代理在双向流下最容易暴露 bug。
️ 实现 / 验证
gRPC 经 ingress-nginx
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: grpc-svc
annotations:
nginx.ingress.kubernetes.io/backend-protocol: "GRPC" # 关键
spec:
ingressClassName: nginx
tls: [{ hosts: [grpc.example.com], secretName: grpc-tls }] # gRPC 基本要 TLS(h2)
rules:
- host: grpc.example.com
http: { paths: [{ path: /, pathType: Prefix, backend: { service: { name: grpc-backend, port: { number: 50051 } } } }] }
grpcurl grpc.example.com:443 list # 通 = 端到端 h2+trailers OK
# 失败常见: "unexpected HTTP status" = 某跳把 h2 降级了
大文件上传放开每跳上限
# 故意传 100MB,验证没被 413 截
curl -X POST --data-binary @big-100mb.bin https://app.example.com/upload -w "%{http_code}\n"
# 413 = 某跳 client_max_body_size/proxy-body-size 没放开
WebRTC 信令 + TURN 媒体
# 信令是 WebSocket,按第40章 WS 方式代理验证(101)
# TURN 媒体:用 coturn,部署为 hostNetwork/LoadBalancer(UDP),不走 ingress
turnutils_uclient -v -t -u user -w pass turn.example.com # 测 TURN 中继可用性
排错
| 现象 | 根因 | 解决 |
|---|---|---|
| gRPC "unexpected HTTP status/RST_STREAM" | 某跳 h2 降级 h1 / 没转 trailers | 每跳 grpc_pass/backend-protocol: GRPC,用 Envoy |
| gRPC 负载严重不均 | L4 连接级焊死长 h2 连接 | L7 请求级 LB / headless 客户端 LB |
| 浏览器调 gRPC 报 trailers 缺失 | 没有 gRPC-Web 翻译跳 | 加 Envoy grpc_web / grpcwebproxy |
| 长轮询请求频繁 504 | 挂起请求被某跳超时掐 | 每跳长 read 超时 + 关缓冲 |
| 上传大文件 413 | 某跳 body size 限制 | 每跳放开 client_max_body_size/proxy-body-size |
| 上传大文件代理内存高/慢 | 请求体被缓冲 | proxy_request_buffering off(牺牲重试) |
| WebRTC 能连但无音视频 | 媒体 UDP/TURN 没通 | 部署 TURN(coturn),放行 UDP 端口范围 |
| 双向流一方向卡死/截断 | 半关闭或单向背压处理错 | CloseWrite 传 FIN + 双向独立背压(第34/35章) |
本章小结
- gRPC 要端到端 h2 + trailers(
grpc_pass/backend-protocol: GRPC/Envoy),负载不均要 L7/客户端 LB;浏览器侧必须有 gRPC-Web 翻译跳。 - 长轮询是伪装成 HTTP 的长连接:长超时、不缓冲、吃 fd。
- WebRTC 信令走 WS(经 L7 链)、媒体走 UDP 经 TURN 中继(不走反向代理);QUIC/HTTP3 同属 UDP 范畴。
- 大文件:每跳放开体积上限(木桶)、按需关请求缓冲、下载靠流式+背压+零拷贝;双向流让半关闭与双向背压同时成为必须。
下一章 第42章 可落地完整参考实现,把第 39-41 章全部落成一套能 kubectl apply 的 demo 栈:两级 nginx + Ingress + code-server + SSE + SSH + gRPC,含逐协议验证清单。