前文提到过,HTTP/2 的分帧是不彻底的,仅仅是应用层 stream,而在传输层 TCP 看来仍然是一个 stream。HTTP/3 将 stream 拆分做到了传输层,在 UDP 层面就以 stream 为单位,声称解决了所有的 stream 间 HoLB 问题。
https://laisky.notion.site/HoL-Blocking-and-Priority-Inversion-LiteSpeed-Blog-4601095b01ed4f8d87bc3bc40ed665d7
很可惜现实是残酷的,其实 stream 间的阻塞也没解决。这次问题出在 QPACK: Field Compression for HTTP/3 上。
HTTP/1 时代,大量流量被浪费在重复传输 HTTP fields 上(如 HEADERS),所以从 HTTP/2 时代起,就引入了 HPACK 来压缩 HTTP fields。客户端和服务端各自维护一个 fields table,然后进行增量的更新。
在 HTTP/3 上保留了这个设计,并更名为 QPACK。但是 QPACK 的设计有个问题,它让 stream 间失去了独立,带来了依赖关系,而我们都知道,依赖关系会导致 HoLB。
具体来说,因为 QPACK 只传输增量更新,所以每一个 request/response 都需要指定一个
这就导致了一个很可怕的现象,一个高优先级的 stream,可能会因为缺乏必要的
更可怕的是,按照 RFC-9204 规定,当一个 stream 被
prev: https://t.me/laiskynotes/40
next: https://t.me/laiskynotes/62
https://laisky.notion.site/HoL-Blocking-and-Priority-Inversion-LiteSpeed-Blog-4601095b01ed4f8d87bc3bc40ed665d7
很可惜现实是残酷的,其实 stream 间的阻塞也没解决。这次问题出在 QPACK: Field Compression for HTTP/3 上。
HTTP/1 时代,大量流量被浪费在重复传输 HTTP fields 上(如 HEADERS),所以从 HTTP/2 时代起,就引入了 HPACK 来压缩 HTTP fields。客户端和服务端各自维护一个 fields table,然后进行增量的更新。
在 HTTP/3 上保留了这个设计,并更名为 QPACK。但是 QPACK 的设计有个问题,它让 stream 间失去了独立,带来了依赖关系,而我们都知道,依赖关系会导致 HoLB。
具体来说,因为 QPACK 只传输增量更新,所以每一个 request/response 都需要指定一个
REQUIRED INSERT COUNT
,表示这个 request/response 至少依赖于哪一个版本的 fields table 的更新。而 HTTP/3 的 fields 更新是以 HEADER 的形式和 DATA 一起传输的(gQUIC 原始设计中独立的 HEADER stream 被 IETF/QUIC 取消了),换言之,fields 的更新属于某一个 data stream。而这个 data stream 的优先级,是由 data 而不是 header 决定的。这就导致了一个很可怕的现象,一个高优先级的 stream,可能会因为缺乏必要的
REQUIRED INSERT COUNT
而被另一个携带 fields 更新的低优先级 stream 阻塞。更可怕的是,按照 RFC-9204 规定,当一个 stream 被
REQUIRED INSERT COUNT
阻塞时,接收方应该一直缓存这个 stream 直到 REQUIRED INSERT COUNT
满足为止。如前文所述,如果是高优先级的 stream 被低优先级的 stream 阻塞,那么接收方必须很傻地接受完全部高优先级数据后,才能继续处理低优先级的数据。而高优先级的数据就只能躺在缓存里傻等低优先级的增量更新完成后,才能被处理。prev: https://t.me/laiskynotes/40
next: https://t.me/laiskynotes/62