httputil.NewSingleHostReverseProxy仅是轻量转发器,非现成防火墙;必须手动重写Director、配置Transport、集成Coraza等中间件才能实现安全网关功能。

直接用 httputil.NewSingleHostReverseProxy 做转发底座,别封装成“网关框架”
单机缓存网关的核心不是功能堆砌,而是把请求转发链路压到最短。Gin 本身不处理代理,httputil.NewSingleHostReverseProxy 是 Go 原生最轻量、最可控的起点——它只做一件事:把 *http.Request 改写后发给上游,并把响应原样回传。任何在此之上加“路由匹配器”“策略引擎”的抽象,都会引入不必要的分配和判断开销。
常见错误是拿 gin.RouterGroup 当代理调度器用:比如为每个上游服务注册一个 router.Any("/svc-a/*path", proxyHandler)。这会让 Gin 的路由树参与每次请求匹配,而实际只需要路径前缀截断 + 重写 URL 即可。
- 正确做法:在 Gin 中间件里解析
c.Request.URL.Path,提取服务标识(如/api/user/...→user-svc),查本地sync.Map缓存拿到上游地址,然后调用proxy.ServeHTTP(c.Writer, c.Request) - 必须重写
req.URL.Path和req.URL.RawPath:否则上游收到重复路径(如/api/user/api/user/profile) - 别动
req.Host;若需透传原始 Host,改用req.Header.Set("Host", upstream.Host)
Redis 缓存响应时,只缓存 GET/HEAD 且带明确 Cache-Control 的响应
缓存不是越全越好。对 POST/PUT/DELETE 做响应缓存,等于制造数据不一致;对没设 Cache-Control 或设了 no-store 的响应缓存,等于违反 HTTP 协议语义。生产环境出问题,八成是因为缓存了不该缓存的东西。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 在
ModifyResponse回调里检查resp.StatusCode == 200 && (req.Method == "GET" || req.Method == "HEAD") - 解析
resp.Header.Get("Cache-Control"),只缓存含max-age=或public的响应 - 用
resp.Header.Get("ETag")作 Redis key 后缀,避免相同路径不同参数缓存冲突 - 缓存过期时间取
min(max-age, 300)秒——防止上游误配超长 TTL 拖垮本地内存
http.Transport 必须定制三项参数,否则连接池会成为瓶颈
默认 http.DefaultTransport 是为爬虫或 CLI 工具设计的,不是为高并发网关。它不做连接复用控制、DNS 缓存过长、超时策略僵硬,直接拿来用,500 QPS 就可能触发 too many open files 或上游响应延迟雪崩。
在 Golang 中使用 samber/hot 进行内存缓存,支持 LRU、LFU、TinyLFU、W‑TinyLFU、S3FIFO、ARC、TwoQueue、SIEVE、FIFO 等淘汰算法,提供 TTL、缓存加载器及分片功能。
关键改三项:
-
Timeout:设为5 * time.Second(比下游 P99 高 2 秒即可),太短误杀正常慢请求,太长拖垮并发 -
IdleConnTimeout:设为30 * time.Second,与大多数 upstream HTTP server 的 keep-alive timeout 对齐 -
MaxIdleConnsPerHost:设为100,避免单 host 耗尽文件描述符;别碰MaxIdleConns,它全局生效易误伤
重试逻辑不要塞进 Transport:单独写中间件,只对 502/503/504 和 net.ErrClosed、context.DeadlineExceeded 重试 1 次,并加 jitter。
JWT 鉴权中间件里 ParseWithClaims 报 expired,大概率是系统时间没同步
JWT 的 exp 是绝对时间戳(Unix 秒),jwt-go 默认校验误差 >1 秒就拒绝。而 systemd-timesyncd 默认 5 分钟同步一次,在压测或刚启动的容器里,时间偏差轻松超 2 秒。
解决方案不是调大校验窗口,而是从根上解决:
- 部署脚本里强制加
ntpdate -s time.windows.com或chronyd -q - 在
jwt.ParseWithClaims里传入jwt.WithValidator(func(t *jwt.Token) (bool, error) { ... }),容忍 ±1.5 秒偏差 -
keyFunc返回值必须是非 nil error 才算验证失败;返回nil+ 空 key 会导致 panic
缓存网关真正的复杂点不在代码行数,而在各层超时、重试、缓存策略的叠加效应——比如 Transport 的 Timeout、JWT 中间件的签名校验耗时、Redis 连接池等待时间,三者嵌套后 P99 容易翻倍。上线前必须用真实流量压测,而不是只跑单元测试。


















