Files
cs-note/hzh/MS/02-服务治理/01-API网关.md
T
2026-05-24 11:42:38 +08:00

450 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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["负载均衡器<br/>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-可观测性]] — 日志、指标、链路追踪的完整体系