验证当 golang httputil.ReverseProxy 代理的后端服务(Backend Server)没有启动或无法连接时,发往代理的请求是否会被代理服务缓存,等待后端恢复后再转发?结论是:不会。
核心问题分析
httputil.ReverseProxy 本质上是一个 流式 的反向代理。它的设计目标是高效地将客户端的请求流向后端服务器,并将后端服务器的响应流回客户端。它本身 不包含 任何请求缓存、排队或重试的逻辑。
当一个请求到达 ReverseProxy 时,它会立即尝试与后端服务器建立连接并转发该请求。如果此时后端服务不存在或无法建立连接(例如,端口未监听,网络不通),TCP 连接会失败。ReverseProxy 会将这个连接失败的错误视为一次失败的请求,并立即向客户端返回一个错误响应,通常是 502 Bad Gateway。
这个请求的处理流程在此时就已经结束了。ReverseProxy 不会保存这个失败的请求。当后端服务恢复后,ReverseProxy 也不会记起之前失败的请求并重新发送它们。只有新的、后续的客户端请求才会被成功转发到已恢复的后端服务。
实验验证
为了证明这一点,我们将搭建一个简单的场景:
- 一个反向代理服务 (
proxy_server.go):监听在 localhost:8080,它会把所有请求转发到 localhost:9090。
- 一个后端服务 (
backend_server.go):监听在 localhost:9090,但我们稍后会手动启动和停止它。
- 使用
curl 作为客户端来发送请求。
第 1 步:编写反向代理服务 (proxy_server.go)
这个服务使用了 httputil.NewSingleHostReverseProxy 来创建一个基本的反向代理。
// proxy_server.go
package main
import (
"log"
"net/http"
"net/http/httputil"
"net/url"
)
func main() {
// 定义后端服务的地址
backendURL, err := url.Parse("http://localhost:9090")
if err != nil {
log.Fatalf("无法解析后端 URL: %v", err)
}
// 创建一个新的反向代理
proxy := httputil.NewSingleHostReverseProxy(backendURL)
// 自定义 Director 来打印请求日志,以便观察
originalDirector := proxy.Director
proxy.Director = func(req *http.Request) {
log.Printf("接收到请求,将转发到后端: %s%s", req.Host, req.URL.Path)
originalDirector(req) // 执行默认的 Director 逻辑
}
// 代理本身就是一个 http.Handler
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
proxy.ServeHTTP(w, r)
})
log.Println("反向代理服务器正在启动,监听地址 http://localhost:8080")
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatalf("代理服务器启动失败: %v", err)
}
}
第 2 步:编写后端 Web 服务 (backend_server.go)
这是一个非常简单的 HTTP 服务器,只是为了验证代理是否能成功转发。
// backend_server.go
package main
import (
"log"
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
log.Println("后端服务接收到请求")
w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(http.StatusOK)
w.Write([]byte("你好,这里是后端服务!请求处理成功。"))
})
log.Println("后端 Web 服务器正在启动,监听地址 http://localhost:9090")
if err := http.ListenAndServe(":9090", nil); err != nil {
log.Fatalf("后端服务器启动失败: %v", err)
}
}
第 3 步:使用 curl 构造请求并进行验证
现在我们开始实验。请打开两个终端窗口。
场景一:正常情况(后端服务正在运行)
-
在终端 A 中,启动后端服务:
你应该会看到输出:后端 Web 服务器正在启动,监听地址 http://localhost:9090
-
在终端 B 中,启动我们的代理服务:
你应该会看到输出:反向代理服务器正在启动,监听地址 http://localhost:8080
-
在第三个终端 C 中,使用 curl 向代理发送请求:
curl http://localhost:8080/
# 或大文件测试
curl -XPOST -T big-file.iso http://localhost:8080
结论: 一切正常,代理成功将请求转发给了后端。
场景二:关键验证(后端服务未建立)
-
在终端 A 中,按 Ctrl + C 停止后端服务。现在 localhost:9090 没有任何服务在监听。
-
代理服务(终端 B)保持运行。
-
在终端 C 中,再次使用 curl 向代理发送请求。这次我们加上 -v 选项来查看详细的 HTTP 交互过程。
curl -v http://localhost:8080/
分析:
- 代理服务确实收到了请求(日志显示
接收到请求...)。
- 然后它立即尝试连接
localhost:9090,但连接被拒绝 (connection refused)。
- 这个错误被代理捕获 (
http: proxy error: ...)。
- 代理 立即 向客户端
curl 返回了 502 Bad Gateway 错误,并将底层错误信息作为响应体发送了回去。
- 请求处理流程到此结束。请求没有被任何地方缓存或保留。
场景三:后端服务恢复
-
在终端 A 中,重新启动后端服务:
-
代理服务(终端 B)仍然在运行。
-
在终端 C 中,我们发送一个 新的 请求:
curl http://localhost:8080/a-new-request
分析:
- 当后端服务恢复后,新的请求可以被正常处理。
- 但是,上一个失败的请求 (
/) 并没有被自动重发。它已经失败并永远消失了。