bigywater.tech
微服务 · 38

Go 手写一个反向代理:httputil.ReverseProxy 的 Director 与错误处理

用 Go 标准库写一个反向代理要注意什么?Director 和 Rewrite 对请求头的处理有什么不同?各种后端故障 client 会看到什么?Transport 的连接池默认值会怎样?这篇用约 70 行的 Go 代理逐项量出来,并和 nginx 的行为对照。

伪造的 XFF 保留
Director 写法下,client 伪造的 X-Forwarded-For: 6.6.6.6 到后端是 6.6.6.6, 127.0.0.1;Rewrite + SetXForwarded 才被覆盖成 127.0.0.1
502 空体
默认 ErrorHandler 对后端不可达和响应头超时的响应,换成自定义 ErrorHandler 才能回 503 加 JSON
725 对 50
2000 个请求、50 个并发时,MaxIdleConnsPerHost 默认 2 与 100 下后端 accept 的连接数

请求头

实测 · 本机执行输出
  写法            | 后端看到的 Host     | X-Forwarded-For    | X-Forwarded-Host | X-Forwarded-Proto | Keep-Alive | Te       | X-Fake(Connection 里点名的逐跳头)
  --------------+----------------+--------------------+------------------+-------------------+------------+----------+---------------------------
  director      | shop.test      | 6.6.6.6, 127.0.0.1 | evil             | NULL              | NULL       | trailers | NULL                      
  director-host | 127.0.0.1:9151 | 6.6.6.6, 127.0.0.1 | evil             | NULL              | NULL       | trailers | NULL                      
  rewrite-nox   | 127.0.0.1:9151 | NULL               | NULL             | NULL              | NULL       | trailers | NULL                      
  rewrite       | 127.0.0.1:9151 | 127.0.0.1          | shop.test        | http              | NULL       | trailers | NULL                      
  Director 写法:不改 req.Host,所以后端看到的是 client 的 Host(shop.test);ReverseProxy 自己追加 X-Forwarded-For(在 client 带的值后面加上 client 地址,伪造的 6.6.6.6 仍保留在最左边);director-host 把 req.Host 也改成后端地址。Rewrite 写法(Go 1.20 起):SetURL 会把 Host 设成后端的;X-Forwarded-* 不会自动加(rewrite-nox 里 client 带的 X-Forwarded-* 被丢掉,不转发),SetXForwarded 才会按“覆盖”的方式设置它们(伪造的 6.6.6.6 不再出现)。逐跳头 Keep-Alive、Te(除 trailers 外)以及 Connection 里点名的头(X-Fake)都被去掉了

Director 保留 client 带的 X-Forwarded-*;Rewrite 先丢掉再由 SetXForwarded 重新设置。

错误处理

实测 · 本机执行输出
  ErrorHandler | (a) 后端不可达                              | (b) 响应头超过 300 ms                       | (c) 响应体只发了一半      
  -------------+----------------------------------------+----------------------------------------+-------------------
  default      | 502 ''                                 | 502 ''                                 | RemoteDisconnected
  custom       | 503 '{"error":"upstream_unavailable"}' | 503 '{"error":"upstream_unavailable"}' | RemoteDisconnected
  (d) client 关闭连接之后,后端那一侧: ('收到 EOF', 0.3)
  默认 ErrorHandler 对所有传输错误回 502 Bad Gateway(响应体为空),超时和连接被拒没有区别;自定义 ErrorHandler 可以换成 503 加 JSON。(c) 后端发了一半响应体就断开时,两种 ErrorHandler 的结果一样:client 的 HTTP 库抛 RemoteDisconnected,一个字节的响应都没收到,不是一个 5xx(我的解释:ReverseProxy 在拷贝响应体出错时用 panic(http.ErrAbortHandler) 直接中断,响应头还在代理的写缓冲里没有发出去;这个解释没有单独验证)。(d) client 一关连接,后端那边很快就读到 EOF,说明代理把取消传给了后端

默认 502 空体;响应体发一半就断时 client 看到连接被直接关闭。

连接池

实测 · 本机执行输出
  Transport                 | 成功   | 后端 accept 的连接数 | 连接复用率
  --------------------------+------+----------------+------
  MaxIdleConnsPerHost = 2   | 2000 | 725            | 63.7%
  MaxIdleConnsPerHost = 100 | 2000 | 50             | 97.5%
  idle=2 时,并发 50 个请求用完之后,只有 2 条连接会被留在池子里,其余的立刻关掉,下一批请求又要新建;idle=100 之后连接几乎全部复用。和 nginx 的 upstream keepalive N 是同一类问题(见第 23 篇),只是 Go 的默认值更小

MaxIdleConnsPerHost 默认只有 2,并发高时连接复用率低。

流式

实测 · 本机执行输出
  配置                 | 第 1 个 chunk 收到 | 第 5 个 chunk 收到 | 收到的 chunk 数
  -------------------+----------------+----------------+------------
  FlushInterval = 0  | 0.00 s         | 1.22 s         | 5          
  FlushInterval = -1 | 0.00 s         | 1.23 s         | 5          
  两个配置的到达时刻几乎一样:据我对 Go 源码里 flushInterval 函数的记忆,ReverseProxy 对 ContentLength 为 -1(如 chunked)的响应和 Content-Type 是 text/event-stream 的响应总是立刻 flush,FlushInterval 只对其它响应起作用;这里只验证了 chunked 这一种,没有单独测 SSE,也没有读源码核对

chunked 响应默认就逐块 flush。

分步演示

Go 手写反向代理
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Python 3.14.8 / Go / nginx / Envoy / etcd / Consul / Nacos / OpenTelemetry / Prometheus(按各篇注明用到哪些及版本);所有服务都是本机自己起的进程,端口 9150 以上,只监听 127.0.0.1,不连任何外部服务,不碰本机已有的实例;故障用 kill -9 进程、让连接断开或在代码里注入延迟与错误来制造;计时用 time.perf_counter,数字每次运行会有小幅抖动。代理是 Go 1.27.1 标准库的 net/http/httputil.ReverseProxy(vtmp/v38/main.go,约 70 行,用参数切换写法);后端是 Python 服务或原始 TCP 服务;client 是 Python;(c) 与 (d) 的解释部分已在正文里标明是推测或未单独验证;每个场景只测一次
    • 没有验证:Go 源码里 flushInterval、ErrorHandler 的实现细节(上面的说明凭记忆,已标明);HTTP/2 后端;WebSocket 的升级处理(ReverseProxy 自动处理 101);请求体很大时的 Transport 行为;ModifyResponse;httputil.ReverseProxy 与 context 取消在更多情形下的行为
    京ICP备2026067502号-1京公网安备11010502063487号