极简 API 网关:手写一个支持限流和断路器的轻量反向代理
极简 API 网关手写一个支持限流和断路器的轻量反向代理一、网关的肥胖症为什么 Nginx 插件不足以说服你另起炉灶Nginx 是优秀的反向代理Kong 是强大的 API 网关。但有一种场景会让你动摇你的后端只有三个微服务每天几万次调用。这时候引入 Kong需要 PostgreSQL 配置管理 插件生态运维复杂度超过了业务价值。极简主义的回答是如果需求足够明确200 行 Go 代码写一个网关比配 Nginx 插件更快、更可控、更易维护。核心需求通常就三个反向代理把请求路由到正确的后端、限流防止某个客户端打爆服务、断路器后端挂了就不再转发避免雪崩。加上健康检查和基本的日志这就是一个够用的网关。graph LR C1[客户端A] -- GW[API 网关] C2[客户端B] -- GW C3[客户端C] -- GW GW --|限流器检查| RL[令牌桶] GW --|断路器检查| CB[熔断器] CB --|闭合| S1[用户服务 :8081] CB --|熔断| FB[快速失败 503] CB --|闭合| S2[订单服务 :8082] S1 --|失败计数| CB style FB fill:#ff6b6b,color:#fff style GW fill:#4dabf7,color:#fff本文将实现一个不到 300 行的 Go API 网关覆盖反向代理、令牌桶限流和断路器三个核心能力。二、反向代理的深层机制ReverseProxy 的请求改写与错误传播Go 标准库的net/http/httputil.ReverseProxy是网关的基础。它的核心逻辑是接收*http.Request构造一个新的*http.Request发送到后端然后将后端的*http.Response写回给客户端。在使用ReverseProxy时有三个容易出错的点请求头改写默认情况下ReverseProxy会复制客户端的 Host 头到后端。如果后端服务依赖 Host 头做路由这会导致请求被路由到错误的服务。用Director函数可以覆盖。SSE/流式响应ReverseProxy默认会缓冲后端的响应体。对于 SSEServer-Sent Events这种流式响应需要设置FlushInterval为负数或较小正数以关闭缓冲。错误响应格式统一后端返回 4xx/5xx 时网关不应该原样透传。应该用ModifyResponse统一错误响应格式把后端的内部错误信息替换为对客户端友好的提示。三、完整实现限流器 断路器 反向代理的组合package gateway import ( context fmt log net/http net/http/httputil net/url sync time ) // 令牌桶限流器 type TokenBucket struct { rate float64 // 每秒生成的令牌数 burst int // 桶容量 tokens float64 lastRefill time.Time mu sync.Mutex } func NewTokenBucket(rate float64, burst int) *TokenBucket { return TokenBucket{ rate: rate, burst: burst, tokens: float64(burst), lastRefill: time.Now(), } } func (tb *TokenBucket) Allow() bool { tb.mu.Lock() defer tb.mu.Unlock() now : time.Now() elapsed : now.Sub(tb.lastRefill).Seconds() tb.tokens elapsed * tb.rate if tb.tokens float64(tb.burst) { tb.tokens float64(tb.burst) } tb.lastRefill now if tb.tokens 1.0 { tb.tokens - 1.0 return true } return false } // 断路器 type CircuitState int const ( CircuitClosed CircuitState iota CircuitOpen CircuitHalfOpen ) type CircuitBreaker struct { failureThreshold int cooldownPeriod time.Duration failureCount int state CircuitState lastFailureTime time.Time mu sync.Mutex } func NewCircuitBreaker(threshold int, cooldown time.Duration) *CircuitBreaker { return CircuitBreaker{ failureThreshold: threshold, cooldownPeriod: cooldown, state: CircuitClosed, } } func (cb *CircuitBreaker) Allow() bool { cb.mu.Lock() defer cb.mu.Unlock() switch cb.state { case CircuitClosed: return true case CircuitOpen: if time.Since(cb.lastFailureTime) cb.cooldownPeriod { cb.state CircuitHalfOpen return true } return false case CircuitHalfOpen: return true } return false } func (cb *CircuitBreaker) RecordSuccess() { cb.mu.Lock() defer cb.mu.Unlock() cb.failureCount 0 cb.state CircuitClosed } func (cb *CircuitBreaker) RecordFailure() { cb.mu.Lock() defer cb.mu.Unlock() cb.failureCount cb.lastFailureTime time.Now() if cb.failureCount cb.failureThreshold { cb.state CircuitOpen } } // 网关核心 type Gateway struct { routes map[string]*Route mu sync.RWMutex } type Route struct { path string target *url.URL proxy *httputil.ReverseProxy limiter *TokenBucket breaker *CircuitBreaker } func NewGateway() *Gateway { return Gateway{ routes: make(map[string]*Route), } } // AddRoute 注册一个后端服务 func (g *Gateway) AddRoute(pathPrefix, targetURL string, rate float64, burst int, cbThreshold int, cbCooldown time.Duration) error { target, err : url.Parse(targetURL) if err ! nil { return fmt.Errorf(parse target URL: %w, err) } proxy : httputil.NewSingleHostReverseProxy(target) // 自定义 Director修改请求路径 originalDirector : proxy.Director proxy.Director func(req *http.Request) { originalDirector(req) // 去掉前缀后转发 req.URL.Path req.URL.Path[len(pathPrefix):] } // 统一的错误处理 proxy.ErrorHandler func(w http.ResponseWriter, r *http.Request, err error) { log.Printf(proxy error for %s: %v, r.URL.Path, err) http.Error(w, {error:backend_unavailable}, http.StatusBadGateway) } g.mu.Lock() defer g.mu.Unlock() g.routes[pathPrefix] Route{ path: pathPrefix, target: target, proxy: proxy, limiter: NewTokenBucket(rate, burst), breaker: NewCircuitBreaker(cbThreshold, cbCooldown), } return nil } func (g *Gateway) ServeHTTP(w http.ResponseWriter, r *http.Request) { route : g.matchRoute(r.URL.Path) if route nil { http.Error(w, {error:route_not_found}, http.StatusNotFound) return } // Step 1: 限流检查 if !route.limiter.Allow() { http.Error(w, {error:rate_limit_exceeded}, http.StatusTooManyRequests) return } // Step 2: 断路器检查 if !route.breaker.Allow() { http.Error(w, {error:circuit_open}, http.StatusServiceUnavailable) return } // Step 3: 记录响应用于断路器状态更新 recorder : statusRecorder{ResponseWriter: w, statusCode: http.StatusOK} route.proxy.ServeHTTP(recorder, r) // Step 4: 根据响应状态更新断路器 if recorder.statusCode 500 { route.breaker.RecordFailure() } else { route.breaker.RecordSuccess() } } func (g *Gateway) matchRoute(path string) *Route { g.mu.RLock() defer g.mu.RUnlock() // 最长前缀匹配 var bestMatch *Route for prefix, route : range g.routes { if len(path) len(prefix) path[:len(prefix)] prefix { if bestMatch nil || len(prefix) len(bestMatch.path) { bestMatch route } } } return bestMatch } type statusRecorder struct { http.ResponseWriter statusCode int } func (r *statusRecorder) WriteHeader(code int) { r.statusCode code r.ResponseWriter.WriteHeader(code) } // 使用示例 func main() { gw : NewGateway() gw.AddRoute(/api/users, http://localhost:8081, 100, 20, 5, 30*time.Second) gw.AddRoute(/api/orders, http://localhost:8082, 50, 10, 3, 20*time.Second) log.Println(Gateway listening on :8080) http.ListenAndServe(:8080, gw) }关键设计statusRecorder拦截 WriteHeader 调用记录状态码用于断路器状态更新。路由匹配使用最长前缀匹配保证/api/users/profile优先匹配/api/users而非/api。四、边界分析这个网关不能做什么不支持动态路由路由在启动时注册不能运行时热更新。如果你的路由需要频繁变更考虑引入 etcd/Consul 做服务发现。不支持 TLS 终止本实现只处理 HTTP。生产环境建议在网关前面加一层 Nginx 或 Caddy 处理 TLS网关本身处理业务逻辑。没有请求体改写能力如果需要在网关层修改请求体如注入认证信息需要实现ModifyResponse。断路器的简单假设本实现使用简单的失败计数模式。更精细的做法是监控错误率而非绝对计数但复杂度会翻倍。在微服务数量 5 的场景下简单方案已经够用。性能上限单进程 Go 网关可以支撑数万 QPS。但如果后端的延迟 100ms网关的并发连接数会快速膨胀。配置合理的连接池和超时可以缓解这个问题。五、总结当你只有三个微服务时300 行 Go 代码的网关比任何全家桶方案都更值得。核心就三个组件令牌桶限流防过度调用、断路器防雪崩、反向代理请求路由。每个组件的代码不超过 50 行阅读和维护成本极低。以后需求增长时你可以选择逐步替换组件如把令牌桶换成 Redis 分布式限流而不是上来就引入整个 Kong 生态。少即是多。工程化的本质不是堆砌工具而是在合适的时候用合适的复杂度解决问题。