15 KiB
tags, create time
| tags | create time | ||||||
|---|---|---|---|---|---|---|---|
|
2026-05-05 12:00 |
API 网关
概述
API Gateway 是所有外部请求的统一入口,相当于微服务架构的 "大门"——所有来自客户端的请求必须先经过它,由它完成鉴权、限流、路由、转换等公共职责后,再转发给后端具体业务服务。
[!question] 为什么要网关? 假设你手上有 20 个微服务,每个都有独立的 URL。如果让客户端直接调用这些服务:
- 每次调用都要处理鉴权和签名验证?
- 前端要维护 20 个 CORS 配置?
- HTTPS 证书要在每台服务器上部署?
有没有一种方式让这些重复工作只处理一次?
答案就是 网关做共性、服务做个性。
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-网关鉴权策略
路由配置
基础路由示例
# 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 中的路由注册(以 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 概念。
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 实现的插件框架
// 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 | 混沌测试 | 按比例注入 |
错误处理
网关处于请求链路的最外层,它的错误处理质量直接影响用户体验。
统一错误码体系
// 自定义网关级错误码
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 用于排查。避免在公网暴露堆栈信息。
短路保护 —— 熔断机制
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-容错模式
可观测性
在生产环境中,网关是一站式的观测入口——所有流量的元数据都在这一层汇聚。
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 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:
-- 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。网关承担协议转换:
// 前端看到的仍然是 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 自动生成反向代理,或通过 go-grpc-middleware 编写自定义转换器。
场景二:静态页面兜底
当所有后端服务全部不可用时,提供友好的降级页面:
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; # 所有后端挂掉时展示维护页
}
}
场景三:大文件上传加速
用户头像/文档上传场景,网关层直接限速容易被打满,可以绕过网关直连存储:
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-可观测性 — 日志、指标、链路追踪的完整体系