--- tags: [microservice, api-gateway, kong, apisix, spring-cloud-gateway, nginx] create time: 2026-05-05 12:00 --- # API 网关 ## 概述 API Gateway 是所有外部请求的统一入口,相当于微服务架构的 **"大门"**——所有来自客户端的请求必须先经过它,由它完成鉴权、限流、路由、转换等公共职责后,再转发给后端具体业务服务。 > [!question] 为什么要网关? > 假设你手上有 20 个微服务,每个都有独立的 URL。如果让客户端直接调用这些服务: > - 每次调用都要处理鉴权和签名验证? > - 前端要维护 20 个 CORS 配置? > - HTTPS 证书要在每台服务器上部署? > > 有没有一种方式让这些重复工作只处理一次? 答案就是 **网关做共性、服务做个性**。 ```mermaid graph TB subgraph External["外部世界"] App["移动端 App"] Web["Web 浏览器"] ThirdParty["第三方 Partner"] end subgraph Gateway["API Gateway 集群"] direction TB LB["负载均衡器
Nginx / CLB"] GW1["Gateway Node A"] GW2["Gateway Node B"] end subgraph Backend["微服务层"] USvc[[User Service]] OSvc[[Order Service]] PSvc[[Payment Service]] ISvc[[Inventory Service]] end subgraph Infra["基础设施"] Config[(配置中心)] Monitor[(Prometheus/Grafana)] Log[(ELK / Loki)] end App --> LB Web --> LB ThirdParty --> LB LB --> GW1 LB --> GW2 GW1 --> USvc GW1 --> OSvc GW2 --> OSvc GW2 --> PSvc GW1 -.-> Config GW2 -.-> Config GW1 -.-> Monitor GW2 -.-> Monitor ``` 常见实现方案: | 方案 | 语言 | 类型 | 适用场景 | |------|------|------|---------| | **Kong** | C (OpenResty/Lua) | 独立二进制 | 高性能、插件生态丰富 | | **APISIX** | Lua (OpenResty) | 独立二进制 | 国内广泛使用、动态路由 | | **Spring Cloud Gateway** | Java (Reactor) | 嵌入式 SDK | Spring 技术栈项目 | | **Envoy** | C++ | 代理 + SDK | Service Mesh 底层代理 | | **Nginx + Lua** | C/Lua | 手动组合 | 极致可控、团队有运维能力 | ## 网关应该做什么? > [!summary] 职责边界 > > | ✅ 适合放网关 | ❌ 不应该放网关 | > |---------|-----------| > | 鉴权与认证 | 业务逻辑(如订单创建) | > | 限流熔断 | 复杂的数据聚合查询 | > | HTTPS 终结 | 大量 CPU 密集型计算 | > | 请求/响应格式转换 | 涉及数据库写操作 | > | 路由分发 | 跨服务事务管理 | > | 日志 & 指标采集 | 邮件/短信发送等异步任务 | > | 协议转换(HTTP→gRPC) | 数据加工和报表生成 | > [!tip] 核心原则 > **网关保持瘦**——它是 traffic cop(交通警察),不是 warehouse manager(仓库管理员)。重逻辑应下沉到业务服务。 > > > [!note] 思考 > > 如果你在网关里发现需要写一个超过 30 行的 if-else 分支来处理"特殊业务规则",停下来问自己:这真的是公共关注点,还是某个服务的私有需求被错误地推到了上层? ### 鉴权职责的分界 网关层的鉴权只负责 **"你是谁"**——校验 JWT / Token 合法性、拦截非法请求。 更细致的权限判断留给下游服务自行处理。详细方案见 → [[02-服务治理/09-网关鉴权策略]] ## 路由配置 ### 基础路由示例 ```yaml # APISIX 路由配置 routes: - uri: /api/orders/* upstream: nodes: "order-service:8080": 1 type: roundrobin - uri: /api/payments/* upstream: nodes: "payment-service:8080": 1 type: roundrobin ``` ```go // Go 中的路由注册(以 Gin + Gateway 为例) r := gin.Default() r.Use(gateway.AuthMiddleware()) // 全局鉴权 r.Use(gateway.RateLimit(100)) // 全局限流 api := r.Group("/api") { api.POST("/orders", order.CreateHandler) api.GET("/orders/:id", order.GetHandler) api.POST("/payments", payment.ChargeHandler) } _ = r.ListenAndServe() ``` > [!note] 解释 > `AuthMiddleware` 和 `RateLimit` 是挂载在路由树最上层的中间件,会拦截所有通过 `/api` 前缀的请求。这意味着无需在每个 handler 里重复写鉴权逻辑——这正是网关集中式处理的优势。 ### 灰度发布与高级路由 流量权重分配、灰度策略、A/B 测试等内容已移至 → [[02-服务治理/08-流量治理]] ## 插件体系 网关的核心价值在于 **可插拔的中间件链**,类似 Express/Koa 的 middleware 概念。 ```mermaid graph LR Req["Request"] -->|Plugin 1| RateLimiter["限流"] RateLimiter -->|Plugin 2| Auth["鉴权"] Auth -->|Plugin 3| Router["路由匹配"] Router -->|Plugin 4| Transform["协议转换"] Transform -->|Plugin 5| Logger["日志记录"] Logger --> Resp["Response"] ``` ### Go 实现的插件框架 ```go // Plugin 接口定义 — 每个插件实现此接口即可接入网关流水线 type Plugin interface { Name() string Priority() int // 数字越小越先执行 OnRequest(ctx *Context) bool // 返回 false 则中断流水线 OnResponse(ctx *Context) // 响应阶段钩子 } // 网关执行管线 func (gw *Gateway) ExecutePlugins(ctx *Context, plugins []Plugin) { sort.Slice(plugins, func(i, j int) bool { return plugins[i].Priority() < plugins[j].Priority() }) for _, p := range plugins { ctx.Set("plugin", p.Name()) if !p.OnRequest(ctx) { ctx.AbortWithJSON(ctx.StatusCode(), gateway.ErrResp(ctx.ErrorCode())) return } } ctx.Next() // 转发到后端服务 for _, p := range plugins { p.OnResponse(ctx) } } ``` > [!note] 解释 > 这段代码展示了网关插件系统的核心设计: > - 每个插件通过 `Priority()` 控制执行顺序(例如限流必须在鉴权之前) > - `OnRequest` 返回 `false` 时立即截断请求,不会到达后端 > - `OnResponse` 在所有请求结束后执行,常用于埋点和日志记录 ### 常用插件速查 | 插件 | 作用 | 推荐算法 | |------|------|---------| | **Rate Limiting** | 防刷限流 | 令牌桶 / 漏桶 | | **CORS** | 跨域处理 | 预检缓存 | | **IP 黑白名单** | 访问控制 | Redis Bloom Filter | | **Request Transformation** | Header/Body 改写 | Map-based | | **Protocol Conversion** | HTTP↔gRPC 互转 | protobuf mapping | | **Prometheus Exporter** | 指标采集 | 自动埋点 | | **Fault Injection** | 混沌测试 | 按比例注入 | ## 错误处理 网关处于请求链路的最外层,它的错误处理质量直接影响用户体验。 ### 统一错误码体系 ```go // 自定义网关级错误码 const ( ErrUnauthorized = 1001 // 未认证或 Token 过期 ErrForbidden = 1002 // 认证通过但无权访问 ErrRateLimit = 1003 // 触发限流 ErrBackendUnavail = 1004 // 后端服务不可用(熔断中) ErrGatewayTimeout = 1005 // 后端超时 ErrMalformedReq = 1006 // 请求格式错误 ErrServiceOverload = 1007 // 服务过载(上游返回 503) ) func ErrResp(code int) map[string]interface{} { return map[string]interface{}{ "code": code, "message": errorMessages[code], "request": currentRequestID, } } ``` > [!note] 关键决策:网关 4xx vs 5xx > > | 场景 | 网关应返回 | 原因 | > |------|-----------|------| > | Token 无效 | **401** | 问题出在客户端凭证 | > | 后端服务宕机 | **502** | 网关正确转发了但收到坏响应 | > | 后端超时 | **504** | 网关等待超时而非业务错误 | > | 触发限流 | **429** | 语义明确,客户端可据此退避 | > | 参数错误 | **400** | 无论前后端,都是客户端输入有误 | > > > [!warning] 反模式 > > 不要把后端 500 原封不动返给客户端。网关应该将其转换为统一的 "内部错误" 提示,并附带唯一的 request ID 用于排查。避免在公网暴露堆栈信息。 ### 短路保护 —— 熔断机制 ```go func (gw *Gateway) forward(ctx *Context) error { svc := gw.routeTo(ctx.Path) cb := gw.CircuitBreaker(svc) // 熔断开启时直接短路,不再发请求 if cb.State() == CircuitOpen { return ctx.Error(ErrBackendUnavail, "%s is circuit-breaker open", svc) } resp, err := cb.Do(func() (*http.Response, error) { return http.DefaultClient.Do(ctx.Request) }) // 成功率低于阈值 → 打开熔断 return nil } ``` 熔断状态机:**Closed**(正常转发)→ **Open**(全部短路)→ **Half-Open**(放行少量探测请求)。详见 → [[02-服务治理/06-容错模式]] ## 可观测性 在生产环境中,网关是一站式的观测入口——所有流量的元数据都在这一层汇聚。 ```mermaid flowchart LR Client --> GW["API Gateway"] GW -- "access.log + trace_id" --> ELK["ELK / Loki"] GW -- "QPS / latency / errors" --> PM["Prometheus"] PM --> Grafana["Grafana Dashboard"] GW -- "trace span" --> Jager["Jaeger / Zipkin"] Jager --> Grafana ``` ### 三 pillars 实践 | Pillar | 做什么 | 关键指标 | |--------|--------|---------| | **结构化日志** | 每条请求写入 trace_id、source IP、耗时、上游地址 | `method path status latency upstream` | | **指标采集** | 按路由 / 状态码分桶暴露 Prometheus Metrics | `http_requests_total`, `http_request_duration_seconds` | | **分布式追踪** | 传递 `X-Trace-ID` 给下游,构建完整调用链 | Trace ID 透传率 ≥ 99% | ### 告警基线参考 | 指标 | 阈值 | 动作 | |------|------|------| | P99 延迟 | > 500ms 持续 5min | PagerDuty 告警 | | 5xx 比例 | > 1% | 立即通知值班 | | 熔断器打开数 | ≥ 3 个同时打开 | 检查下游健康 | ## 性能优化 网关作为所有流量的必经之路,自身性能瓶颈会成为整个系统的天花板。 ### 连接池复用 每个网关节点都会频繁向后端发起 HTTP 连接,为减少 TCP 握手开销: ```go // Go net/http 连接池配置 transport := &http.Transport{ MaxIdleConnsPerHost: 100, // 同一后端的空闲连接数 IdleConnTimeout: 90 * time.Second, TLSHandshakeTimeout: 5 * time.Second, ResponseHeaderTimeout: 10 * time.Second, } client := &http.Client{Transport: transport} ``` > [!note] 为什么重要 > 假设 QPS = 5000,平均响应时间 = 50ms,每个请求新建 TCP 连接的 overhead 约 3ms(含 TLS handshake 可能达 20ms)。节省下的 15~20ms 可以直接转化为吞吐量提升。对于网关这种每毫秒都计较的场景,连接池复用是最简单的性能手段。 ### DNS 预解析 避免每次请求都做 DNS lookup: ```lua -- OpenResty / Nginx 中的 dns_resolver 配置 resolver 10.0.0.2 valid=30s; # 30s 缓存 DNS 结果 resolver_timeout 2s; ``` ### 其他优化技巧 | 技巧 | 收益 | 复杂度 | |------|------|--------| | 静态资源本地缓存 | 消除对上游的冗余请求 | 低 | | gzip/brotli 压缩响应体 | 带宽降低 60~80% | 低 | | Keep-Alive 复用连接 | 减少 TCP/TLS 握手次数 | 低 | | 异步日志写入 | 不阻塞请求主线 | 中 | | 多进程 Worker 模型 | 利用多核 CPU | 低 | ## 实际案例 ### 场景一:HTTP 转 gRPC 后端团队用 gRPC 对外提供服务,但前端只能消费 HTTP/JSON。网关承担协议转换: ```typescript // 前端看到的仍然是 RESTful JSON POST /api/v1/users { "name": "Alice", "email": "alice@example.com" } // 网关内部将 JSON body 转为 Protobuf 后发给 gRPC 后端 // UserCreateRequest { name: "Alice", email: "alice@example.com" } // → POST grpc:///user-service/UserService/Create ``` Go 中可使用 [`grpc-ecosystem/grpc-gateway`](https://github.com/grpc-ecosystem/grpc-gateway) 自动生成反向代理,或通过 [`go-grpc-middleware`](https://github.com/go-grpc-middleware) 编写自定义转换器。 ### 场景二:静态页面兜底 当所有后端服务全部不可用时,提供友好的降级页面: ```nginx upstream backend { server app-1:8080; server app-2:8080; server app-3:8080; fail_timeout=30s max_fails=3; # 连续失败 3 次标记为 down } server { location @fallback { root /usr/share/nginx/html; try_files /maintain.html =503; } location / { proxy_pass http://backend; proxy_next_upstream error timeout http_502 http_503; error_page 503 @fallback; # 所有后端挂掉时展示维护页 } } ``` ### 场景三:大文件上传加速 用户头像/文档上传场景,网关层直接限速容易被打满,可以绕过网关直连存储: ```mermaid sequenceDiagram participant C as Client participant G as Gateway participant S3 as S3/OSS participant App as App Service C->>G: GET /upload-token G->>App: validate user App-->>G: { token, upload_url } G-->>C: presigned URL C->>S3: PUT (direct, bypass gateway) S3-->>C: 200 OK C->>G: notify-complete { token } G->>App: process uploaded file ``` > [!tip] 思路 > 上传/下载大文件这类 I/O 密集型操作,消耗的是网关的连接数和带宽。可以考虑网关只负责签发临时凭证,数据传输直连对象存储,从而释放网关的并发容量。 ## 反模式警示 > [!warning] 这些做法看起来合理,但实际上会带来问题 ### 反模式 1:网关成为"万能胶水" 某些团队把越来越多的业务逻辑往网关塞——拼接多个后端接口、汇总数据、做数据清洗……最终网关变成了一张巨型 spider web。 **后果**:网关变慢 → 全系统响应变慢 → 运维越来越痛苦。 **对策**:如果需要跨服务聚合数据,创建一个专门的 **BFF(Backend For Frontend)** 层,放在网关之后。 ### 反模式 2:没有健康检查 网关不知道后端什么时候挂掉了,继续把流量打过去直到超时耗尽用户耐心。 **对策**:启用 upstream health check,配合熔断器(参见 → [[02-服务治理/06-容错模式]])。 ### 反模式 3:日志没带 trace_id 用户在后台看到报错,去翻日志却无从定位——成千上万条日志里没有唯一标识串联整条链路。 **对策**:在网关入口处生成 trace_id,通过 `X-Trace-ID` header 透传到所有下游,并在 access log 中固定包含该字段。 ### 反模式 4:忽略请求大小限制 允许任意大小的请求体进入,攻击者可以轻松用超大 payload 打爆网关内存。 **对策**:设置 `client_max_body_size`(Nginx)或等效配置,对上传接口单独放宽,普通接口默认限制在几 MB 以内。 ## 关联笔记 - [[02-服务治理/09-网关鉴权策略]] — 网关鉴权的分层设计与差异化方案 - [[02-服务治理/08-流量治理]] — 灰度发布、权重路由、蓝绿部署 - [[02-服务治理/06-容错模式]] — 熔断、重试、降级 - [[02-服务治理/04-服务发现]] — 网关如何获取后端实例列表 - [[hzh/MS/API 设计原则]] — API Gateway 的设计与 REST/gRPC 选型 - [[04-可观测性]] — 日志、指标、链路追踪的完整体系