Go 微服务 API 网关:从手写路由到统一网关的演进

📅 2026/7/23 12:18:49
Go 微服务 API 网关:从手写路由到统一网关的演进
Go 微服务 API 网关从手写路由到统一网关的演进一、手写路由的幻觉为什么简单的发散最快很多 Go 项目的第一个版本都会走同一条路在main.go里用net/http配http.HandleFunc写路由服务数量多了就引入gorilla/mux或者gin每个服务自己搞一套中间件做认证、限流、日志。写起来很快看起来也清爽——每个服务就几百行代码路由表一目了然。但当服务数量从 5 个增长到 30 个时事情就开始失控。认证逻辑散落在每个服务里改一个 JWT 验证策略需要改 N 个服务。限流配置各写各的——A 服务限制每秒 100 请求B 服务限制 50但全局并没有一个统一的控制面来保证总并发不超过下游推理服务的容量。日志格式和 TraceID 不一致排障时需要在 3 个服务的日志里跳转对不上时序就无法还原一次请求的完整链路。最致命的是跨服务调用时的耦合。一个业务请求往往需要聚合多个模型服务的返回——先调 Embedding 服务、再调 Reranker、最后调 LLM——但这个编排逻辑如果写在业务代码里每次新增或替换模型都需要改业务服务、重新发布运维成本持续攀升。基础设施不需要漂亮话这些都不是设计问题是治理缺位的结果。二、统一网关的分层架构把横切关注点收敛到一层统一 API 网关的核心价值不是多了一个组件而是把散落在各个服务中的横切关注点Cross-cutting Concerns收敛到一层。典型的分层架构如下网关的分层设计遵循关注点分离。认证层只做身份校验和 Token 解析不关心请求要转发到哪个服务。限流层只做流量整形按 API Key 做多维度限流全局 QPS、单模型 QPS、每分钟 Token 上限。路由层负责根据model参数或path路径将请求导向正确的后端服务实例。协议转换层处理 HTTP 到 gRPC 的转换让调用方始终用 HTTP 协议通信后端服务可以根据需要自由选择通信协议。请求聚合层是最具业务价值的模块。它允许定义编排规则——比如先调 Embedding 服务获取向量再调 Reranker 服务对检索结果排序最后调 LLM 生成回答——这个编排逻辑在网关中配置业务方只需要一个POST /v1/rag/chat请求背后的多服务编排对调用方完全隐藏。三、Go 实现中间件链与路由配置驱动网关的核心实现是中间件链Middleware Chain。每个中间件独立处理一个关注点按顺序执行。Go 的net/http的中间件模式非常适合这个场景// Middleware 标准中间件函数签名 type Middleware func(http.Handler) http.Handler // Chain 将多个中间件串联成一条处理链 func Chain(h http.Handler, middlewares ...Middleware) http.Handler { for i : len(middlewares) - 1; i 0; i-- { h middlewares[i](h) } return h } // RateLimiter 令牌桶限流中间件 // 按 API Key 做独立限流而非全局一刀切 func RateLimiter(store LimiterStore) Middleware { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { apiKey : r.Header.Get(X-API-Key) if apiKey { apiKey r.URL.Query().Get(api_key) } limiter : store.GetOrCreate(apiKey, 100) // 默认 100 QPS if !limiter.Allow() { w.Header().Set(X-RateLimit-Limit, fmt.Sprintf(%d, limiter.Limit())) w.Header().Set(Retry-After, 1) http.Error(w, {error:rate_limit_exceeded}, http.StatusTooManyRequests) return } next.ServeHTTP(w, r) }) } } // ModelRouter 根据请求中的 model 参数动态路由到后端服务 func ModelRouter(registry ModelRegistry) Middleware { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var modelName string // 支持两种方式指定模型请求体中的 JSON 或 URL 路径 if r.Method http.MethodPost { var body map[string]interface{} if err : json.NewDecoder(r.Body).Decode(body); err nil { if name, ok : body[model].(string); ok { modelName name } } } if modelName { http.Error(w, {error:model_not_specified}, http.StatusBadRequest) return } // 从注册表查询模型对应的后端地址 backend, err : registry.LookupBackend(r.Context(), modelName) if err ! nil { http.Error(w, {error:model_not_found}, http.StatusNotFound) return } // 将后端地址注入请求上下文供下游代理使用 ctx : context.WithValue(r.Context(), CtxBackendURL, backend.URL) next.ServeHTTP(w, r.WithContext(ctx)) }) } }路由配置完全采用声明式管理。所有路由规则、限流策略、聚合编排逻辑都以 YAML 配置文件的形式存储在 Git 仓库中。网关启动时加载配置运行时通过 ConfigMap 挂载实现热更新——修改路由规则不需要重启网关进程。区别于通用的 API 网关如 Kong、APISIXAI 平台的网关增加了模型感知路由能力。路由规则不是按 URL 路径匹配而是按请求中的模型名称匹配——这个差异听起来不大但在实践中避免了为每个模型维护独立路由条目的重复工作。四、统一网关的阴暗面单点与性能代价统一网关的最大风险显而易见——它自身变成了单点故障。如果网关宕机所有推理服务的请求都会中断。缓解措施包括部署至少 3 个网关副本通过 HPA 自动扩缩、网关进程设计为无状态任何副本可以无差别处理请求、上游负载均衡器做健康检查和自动摘除。性能损耗是另一个需要量化的指标。经过网关的请求比直连后端服务平均增加 1-3ms 的延迟——这部分延迟主要来自中间件链的遍历和路由查表。在高 QPS 场景下可以用连接池复用和响应流式传输来降低开销但无法完全消除。另外注意一个陷阱网关的配置规则会随着平台规模线性增长。初期只有 5 条路由规则时清爽简洁但一年后可能有 200 条。如果团队的配置管理能力跟不上网关反而会成为最混乱的组件。建议从一开始就为网关配置建立 code review 流程并定期清理僵尸规则。五、总结从手写路由到统一网关本质变化是把横切关注点从每个服务各管各的收敛到一层统一处理。在网关中集中管理认证、限流、路由和聚合让业务服务可以专注于核心逻辑。落地路线分三个阶段。第一阶段上线最小可用网关——先实现认证和基础路由让新模型自动注册路由、业务方按 API Key 接入这是体验提升最大的一步。第二阶段完善限流和聚合编排能力把之前在业务代码中散落的编排逻辑迁移到网关。第三阶段配置管理全面 GitOps 化将网关配置纳入 CI/CD 流程实现配置变更的可追溯和可回滚。