QUIC over UDP 与 TCP 各得其所的传输选择

HTTP3 走 UDP,HTTP2 仍走 TCP 兜底

HTTP/3 把 QUIC 跑在 UDP:443 上,值得在意的理由是丢一个包的代价。TCP 连接上所有流共用一个字节流,丢一个段就会挡住它后面所有数据的交付,直到重传到达。QUIC 让每条流各自拥有丢失恢复,同一个丢包只挡住数据真的在那个包里的流。 别扭的地方在于这不是一个开关。客户端必须先知道服务器会说 HTTP/3 才可能去试,而它得知这件事所走的那条路径,恰恰是你想优化的那条。 两种传输,一个端口号 UDP:443 和 TCP:443 属于彼此独立的命名空间,所以一个监听可以同时占住两者。这就是为什么 nginx 的 server 块可以把 listen 443 quic reuseport; 和 listen 443 ssl; http2 on; 并排放着,用同一个端口号同时服务 QUIC 和 HTTP/2。reuseport 比看起来更重要:没有它,第二次绑定 UDP:443 会拿到 EADDRINUSE,而这正是两个 server 块都声称要占用 QUIC 监听时看到的报错。 ...

二月 12, 2028 · 1 分钟 · XB Field Notes
观察、决策、行动三个方框,旁边强调取消单条流后还要验证其余请求。

HTTP/2 多路复用消除排队

HTTP/2 通过多路复用解除 HTTP 响应的顺序约束,但不同流仍然共享 TCP 连接及其故障边界。取消就是一个值得单独验证的场景:终止某个请求,不一定需要破坏相邻请求或可复用连接。这是一项有明确作用范围的能力,不能扩写成所有流在任何条件下都互不影响。 流不是底层套接字 HTTP/2 在连接内部标识单条流。RFC 9113 定义的 RST_STREAM 用于终止某条流,既可以表达取消,也可以报告流错误;它并不是重置整个套接字的 TCP reset。只要连接本身仍然可用,实现就可以停止一次交换,同时让其他流继续推进,而不必为每次取消重新建立连接。 这与 HTTP/1.1 的持久连接并不矛盾,后者本来就允许一条连接承载多次交换。HTTP/2 新增的是交错传输与流级生命周期控制,而不是首次发明连接复用。TCP 的有序交付、拥塞控制、连接级 DATA 流控以及服务端资源仍然约束进度;即使一次请求取消没有影响其他流,连接故障也可能同时中断多个请求。 取消还不等于撤销已经完成的业务。服务器可能早已提交事务,或调用了另一个服务。客户端读取正文时得到取消错误,无法证明这些操作没有发生。因此,是否重试仍须依据业务语义、幂等保证或可查询的执行结果,不能仅凭网络层已经取消,就把再次提交认定为安全操作。 收到响应头不代表正文成功 成功状态码出现在正文之前。服务端可以先发送并刷新 HTTP 200,再长时间等待,或者在传输内容时失败。状态、正文完整性和业务结果应分别记录。Client.Do 返回了响应对象,只说明客户端已经拿到响应头,不能据此把整个响应标为读取成功,更不能省略后续读取错误的处理。 Go 的出站请求上下文控制取消;对于入站 HTTP/2 请求,一次取消可以让服务端对应请求的上下文结束,而不需要关闭整条连接。处理函数在等待或访问依赖时,应观察并传递该上下文。运行时不会自动撤销任意业务动作。Client.Timeout 还覆盖响应头到达之后的正文读取,不能把它理解成只约束握手或等候首字节。 坏掉的样子 如果几个请求共用一个父上下文,取消父上下文本来就会影响它们;关闭底层连接也比取消单个请求的范围更大。请求自己的时限应由请求自己的取消范围管理。不能对共享套接字设置整体期限后,又声称产生的超时只作用于一条 HTTP/2 流;排障时必须核对取消信号的真正拥有者。 遗弃正文可能保留资源或占用流控额度。适合完整消费的响应应读完,已经结束或取消的正文也必须关闭。未读数据是否妨碍相邻流,与库的缓冲和窗口更新策略有关。流数量上限、共享连接额度和后端容量仍然存在;一个小实验能成功取消,不代表高负载时也没有相互影响。 快速验证 保存为 cancel_probe.go,使用 Go 1.23 或更新版本运行 go run cancel_probe.go。本地服务使用经过验证的 TLS 1.3 协商 HTTP/2。两个处理函数先刷新 HTTP 200 响应头,再让正文等待。客户端随后只取消第一个请求;另一个响应保持打开,双方都使用同一条被编号的 TCP 连接。 ...

一月 16, 2027 · 3 分钟 · XB Field Notes