为什么在代理服务器上测试, http2 的转发性能比 http 1 更低?

📅 发布时间:2026/7/15 8:31:25 👁️ 浏览次数:
为什么在代理服务器上测试, http2 的转发性能比 http 1 更低?
作者:张富春(ahfuzhang)转载时请注明作者和引用链接谢谢cnblogs博客zhihuGithub公众号:一本正经的瞎扯我在测试 Http2 server 与 Http 1.1 server 的性能差异时最高测试数据是 http2 比 http1 快 3.6 倍。而 Carter 在测试 apisix 代理服务器的性能的时候得到的数据是 http2 的性能只有 http 1 的 80%。我们曾彼此质疑对方的数据并觉得不可思议我的观点是二进制协议一定快过文本协议没理由在代理服务器上测试的数据会导致 http2 慢于 http 1。今天终于想明白了原因其实我们都没错结论是在代理服务器上http2 的转发性能会低于 http 1。导致这一项差异的关键是splice()系统调用也就是代理服务器中实现零拷贝的关键。(后续大量引用 ChatGPT 的回答)splice() 是什么splice() 是 Linux 的一个系统调用C 里通过 splice(2) 暴露用来在 两个文件描述符之间搬运数据并且尽量走 零拷贝zero-copy 路径减少用户态缓冲区参与。最典型的用途文件 ↔ 管道、管道 ↔ socket用它可以把数据从磁盘文件直接“送”到网络连接里中间不需要 read() 到用户态再 write() 回内核。⸻它解决什么问题传统写法/* by 01022.hk - online tools website : 01022.hk/zh/qrcode.html */ read(file_fd, user_buf, n); write(sock_fd, user_buf, n);缺点数据会在内核 ↔ 用户态之间拷贝还可能污染 CPU cache、占用内存带宽。splice() 的目标让数据尽量在内核内部移动比如从 page cache / pipe buffer 直接到 socket buffer减少一次或多次拷贝。⸻它的基本形态语义/* by 01022.hk - online tools website : 01022.hk/zh/qrcode.html */ ssize_t splice(int fd_in, loff_t *off_in, int fd_out, loff_t *off_out, size_t len, unsigned int flags);• fd_in / fd_out输入/输出 fd • off_in / off_out • 传 NULL 表示使用 fd 当前偏移并推进像普通读写那样 • 非 NULL 则用你提供的偏移类似 pread/pwrite 的味道并且不一定修改 fd 的文件偏移具体取决于对象类型 • len最多搬运多少字节 • flags控制阻塞/移动行为例如 SPLICE_F_NONBLOCK 等返回值实际搬运的字节数0 通常表示 EOF-1 表示出错errno 说明原因。⸻常见组合file → pipe → socket高性能“发送文件”• splice(file_fd → pipe_write_end)• splice(pipe_read_end → sock_fd)这类组合常用于实现“类似 sendfile 的效果”但 splice 更通用尤其适合你还要在中间做点别的内核态处理的场景。pipe ↔ pipe用于在管道之间转移数据比如配合 tee() 做复制。⸻适用限制很重要• 并不是任意 fd 都支持。splice 需要底层文件操作实现支持VFS/驱动/协议栈。• 在 Linux 上pipe 经常是 splice 的“中转站”很多场景要求 fd_in 或 fd_out 至少有一端是 pipe或者最终在内核内部用 pipe buffer 做承载。• 数据来源如果是普通文件通常来自 page cache因此能做到少拷贝或零拷贝。⸻和 sendfile() 的关系• sendfile()更“专用”通常是 文件 → socket 的快速路径。• splice()更“通用”可以在更多类型的 fd 之间搬运常见用法是 通过 pipe 作为桥 来拼接成 file→socket 的零拷贝链路。就算 splice 把内核中 socket buffer 的数据转移到了 pipe 下面用户使用时也仍然要 read()似乎并没有做到零拷贝对你理解得很准确只要最终要在用户态“看见/处理”字节流就绕不开一次 copy_to_user。splice(socket → pipe) 只是把“从 socket 读到用户缓冲区”这一步换成了“从 socket 读到内核 pipe”因此它不能消除“内核 → 用户态”的拷贝你后面 read(pipefd, userbuf, …) 仍然会拷贝。更精确地说• 传统路径用户态消费recv()/read(socket, userbuf)→ 内核 socket 接收队列 → 用户态 userbuf一次 copy_to_user• splice 路径用户态仍要消费splice(socket → pipe)内核内搬运/重挂页避免 copy_to_user然后 read(pipe → userbuf)→ pipe → 用户态 userbuf还是一次 copy_to_user所以“用户态要拿到 payload”这一目标下splice 不会减少总拷贝次数到 0。它可能减少的是• 少一次“多余的中间 buffer 拷贝/搬运”比如你原来先 read 到用户再 write 到另一个 fd• 或减少 CPU 开销某些场景下从 socket 直接挂到 pipe、再 splice 到目标 fd什么时候 splice 真正有意义能把用户态拷贝降到 0纯转发不解析数据socket_in → pipe → socket_out / file你不在用户态触碰数据只做转发/落盘。典型反向代理、L7 但不解包、录流到文件等。“只看少量头部 大量 body” 的混合比如 HTTP你在用户态只 recv 一小段 header 做决策body 用 splice 走零拷贝转发。这类能显著减少 copy_to_user 的总量因为大头 body 不进用户态。⸻你真正关心的点TCP 通讯里的拷贝到底能不能减少• 应用层必须处理 payload解码、压缩、JSON、protobuf…至少一次内核→用户拷贝不可避免把字节交给进程。• 应用层不处理 payload只转发可以做到 0 次进入用户态splice/送 NIC 相关 offload 另说。对于代理服务器 是不是可以理解为 http 1 大部分都情况能做内核态的四层转发而 http 2 只能做用户态的七层转发?不完全对但你这个直觉抓到了一半决定能不能“像 L4 那样内核态转发”的关键不是 HTTP/1 vs HTTP/2而是代理要不要理解协议语义、要不要“终止连接并重建连接”。 HTTP/2 因为多路复用 流控 头压缩确实更容易把你“逼到”用户态 L7但 HTTP/1 也不是天然就能内核态转发。把它拆成几种代理形态你会更清楚真·L4 代理TCP/四层负载均衡• 不看 HTTP只转发字节流客户端 TCP ↔ 后端 TCP。• 这对 HTTP/1 和 HTTP/2 都一样都能做“内核友好”的转发例如 splice/零拷贝路径、甚至内核里的 TPROXY/IPVS 等。• 代价你做不了按 URL/Host/Header 的路由、鉴权、限速等 L7 能力。✅ 结论HTTP/2 也可以做 L4 转发只是此时你根本不知道它是 HTTP/2。HTTP/1 的 L7 反代常见 Nginx/Envoy 的那种• 你要读请求行/headers决定路由可能改 header可能缓存、压缩、重试等。• 这就已经是 用户态 L7 了不是“内核态 L4”。不过 HTTP/1 有一个“容易优化”的点• header/body 分界清晰且通常一个连接同一时刻服务一个请求即便 keep-alive也多是串行管线化很少用。• 所以可以做 “只在用户态读少量 header然后 body 用 splice 直通”大 body 上传/下载时很香。✅ 结论HTTP/1 不是“大部分都能内核态 L4 转发”它只是 更容易做“半 L7 半零拷贝直通”。HTTP/2 的 L7 反代stream 级路由、多路复用• 同一条 TCP 连接里交错多个 stream 的 frames。• 你要按 stream 拆帧、调度、维护流控窗口、处理 HPACK。• 前后两端通常是两条独立连接客户端一条 H2后端另一条 H2 或 H1stream id/窗口/HPACK 动态表都不可能原封不动“直通”。这使得 把大段 payload 当作连续字节“splice 过去”非常困难因为你必须先在用户态把属于某个 stream 的 DATA 帧摘出来再发到后端而且还可能要重排/限速/优先级。✅ 结论HTTP/2 做 L7 反代时基本只能用户态为主或者说必须有“协议终止点”在用户态/实现层。⸻更准确的一句话• HTTP/1更容易把“body”做成内核态直通splice/sendfile用户态只处理少量控制面headers。• HTTP/2只要你要做 L7按 stream/headers 路由就很难做到 body 完全内核态直通因为 framing/mux/flow-control 迫使你参与数据分发。• 但 HTTP/2 依然可以做纯 L4 转发只是你失去所有 L7 能力。总结http 1.1 的协议特点header 和 body 有明显的区分body 的长度非常明确一条 tcp 连接上同时只有一个请求响应 —— 因此在代理服务器的场景 http 1 很容易使用 splice 这样零拷贝机制来优化。特别是 body 部分这样更长的内容直接在内核态就能够实现数据交换完全不需要把数据拷贝到用户态再从用户态发送出去。http 2 在一条 tcp 连接上有多个流每个流超出SETTINGS_MAX_FRAME_SIZE后还会分成多个 frame —— 由此决定了 http2 上通过 splice 实现零拷贝优化很困难或者说能够真正零拷贝的场景变少了。对于服务器而言http 2 这样的二进制协议确实比 http 1 更快特别是像 api server / rcp server 这样的数据包小请求频繁的场景http2 比 http1 快 3.6 倍就不奇怪了。如果是图片下载/文件下载/大吞吐量的数据传输选择 http1 更好