Files
cs-note/hzh/MS/02-服务治理/01-API网关.md
T

450 lines
15 KiB
Markdown
Raw Normal View History

2026-05-24 11:42:38 +08:00
---
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-可观测性]] — 日志、指标、链路追踪的完整体系