This repository has been archived on 2026-05-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
all-in-kingsoft/hhs/MS/02-服务治理/01-API网关.md
T
2026-05-17 22:00:24 +08:00

15 KiB
Raw Blame History

tags, create time
tags create time
microservice
api-gateway
kong
apisix
spring-cloud-gateway
nginx
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 以内。

关联笔记