450 lines
15 KiB
Markdown
450 lines
15 KiB
Markdown
---
|
||
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-可观测性]] — 日志、指标、链路追踪的完整体系
|