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