2021 年某电商平台大促期间,由于所有外部流量直接打到后端的 200 多个微服务上,缺乏统一的入口管控,导致恶意刷单流量绕过了分散在各服务中的限流逻辑,在 30 秒内击穿了库存服务和订单服务。事后复盘发现,如果存在一个统一的 API 网关(API Gateway),在入口层完成流量识别、限流和认证,这次故障完全可以避免。
微服务架构将单体应用拆分为数十甚至数百个独立服务后,客户端与服务之间的交互复杂度呈指数级增长。每个服务都需要独立处理认证、限流、日志、协议转换等横切关注点(Cross-cutting Concerns),不仅造成大量重复代码,还使得运维团队无法从全局视角管控流量。API 网关正是为解决这一问题而生的基础设施组件——它作为系统的唯一入口,承担流量治理的核心职责。
但网关该做多少事?路由、限流、认证、协议转换——哪些该放在网关,哪些该留给服务自身?这条职责边界划得不好,网关要么成为什么都做的”胖网关”(Fat Gateway),要么退化为一个简单的反向代理而失去存在的意义。本文将系统探讨这个核心问题。
推荐先阅读 API 设计,了解 RESTful 与 gRPC 接口设计的基本原则,它是理解网关路由与协议转换的前提。
一、为什么微服务系统需要 API 网关
没有网关的世界
在没有网关的架构中,客户端需要直接与各个微服务通信:
客户端 → 用户服务(:8001)
客户端 → 订单服务(:8002)
客户端 → 支付服务(:8003)
客户端 → 商品服务(:8004)
这种架构带来的问题:
第一,客户端耦合。 客户端需要知道每个服务的地址和端口,任何服务的地址变更都需要客户端配合修改。当服务数量从 10 个增长到 100 个时,客户端的维护成本变得不可接受。 第二,横切关注点分散。 认证、限流、日志采集、链路追踪等功能需要在每个服务中重复实现。一旦限流策略需要调整,运维团队必须逐一修改所有服务的配置。
第三,协议不统一。 内部服务可能使用 gRPC、Thrift 等高效二进制协议,但移动端和浏览器通常只支持 HTTP/JSON。没有网关,协议转换的责任就落在了每个面向外部的服务上。
第四,安全边界模糊。 没有统一入口意味着每个服务都暴露在公网上,攻击面急剧扩大。
网关的定位
API 网关本质上是一个反向代理(Reverse Proxy),但它不仅仅做请求转发。它是系统与外部世界的唯一接触点,承担着”入口层”的所有职责:
graph TB
subgraph 外部
C1[Web 客户端]
C2[移动客户端]
C3[第三方系统]
end
GW[API 网关]
subgraph 内部服务集群
S1[用户服务]
S2[订单服务]
S3[支付服务]
S4[商品服务]
S5[搜索服务]
end
C1 -->|HTTPS| GW
C2 -->|HTTPS| GW
C3 -->|HTTPS| GW
GW -->|gRPC| S1
GW -->|gRPC| S2
GW -->|HTTP| S3
GW -->|gRPC| S4
GW -->|HTTP| S5
style GW fill:#f96,stroke:#333,stroke-width:2px
网关将客户端与内部服务解耦,客户端只需要知道网关地址即可。网关负责将请求路由到正确的后端服务,并在这个过程中执行各种策略。
二、网关的核心职责
网关的职责可以分为以下几个层次,从最基础到最上层依次为:
2.1 路由与负载均衡
这是网关最基本的职责。根据请求的路径、方法、头部信息等将请求分发到对应的后端服务。
# 以 APISIX 路由配置为例
routes:
- uri: /api/v1/users/*
upstream:
type: roundrobin
nodes:
"user-service-1:8080": 1
"user-service-2:8080": 1
"user-service-3:8080": 1
- uri: /api/v1/orders/*
upstream:
type: chash
key: $arg_user_id
nodes:
"order-service-1:8080": 1
"order-service-2:8080": 1常见的负载均衡策略:
| 策略 | 适用场景 | 缺点 |
|---|---|---|
| 轮询(Round Robin) | 服务实例配置一致 | 不考虑实例负载差异 |
| 加权轮询(Weighted Round Robin) | 实例配置不同 | 权重需要人工调整 |
| 一致性哈希(Consistent Hashing) | 需要会话亲和性 | 节点变化时部分请求迁移 |
| 最少连接(Least Connections) | 请求处理时间差异大 | 需要维护连接计数 |
2.2 限流与熔断
网关是实施限流最有效的位置,因为它是所有流量的必经之路。限流的具体算法将在第五节详细讨论。
2.3 认证与鉴权
在网关层完成身份认证(Authentication)和权限校验(Authorization),后端服务无需重复处理。详见第六节。
2.4 协议转换
网关可以将外部的 HTTP/JSON 请求转换为内部的 gRPC 调用,或将 HTTP 长轮询升级为 WebSocket 连接。这使得内部服务可以自由选择最适合的通信协议,而不受客户端能力的限制。
2.5 请求/响应改写
网关可以对请求和响应进行改写,包括:
- 添加、修改或删除请求头
- 请求体的格式转换
- 响应数据的裁剪和聚合
- API 版本兼容性适配
2.6 可观测性
网关是采集全局指标的最佳位置:
- 访问日志(Access Log): 记录每个请求的来源、路径、响应码、延迟等信息
- 指标采集(Metrics): 暴露 Prometheus 格式的指标,包括 QPS、延迟分布、错误率
- 链路追踪(Distributed Tracing): 注入 Trace ID,串联完整的请求链路
职责边界原则
并非所有事情都应该放在网关上做。核心原则是:网关只做与业务逻辑无关的横切关注点。
| 应该放在网关 | 不应该放在网关 |
|---|---|
| 路由分发 | 业务数据校验 |
| 全局限流 | 业务规则执行 |
| 身份认证 | 复杂的数据聚合 |
| TLS 终止 | 数据库访问 |
| 协议转换 | 业务异常处理 |
| 访问日志 | 状态机流转 |
| IP 黑白名单 | 领域事件发布 |
| 请求 ID 注入 | 业务缓存策略 |
三、Kong / Envoy / APISIX 架构对比
当前主流的开源 API 网关方案有三个:Kong、Envoy 和 Apache APISIX。它们的架构设计思路差异显著。
3.1 Kong 架构
Kong 基于 OpenResty(Nginx + LuaJIT)构建,通过插件系统(Plugin System)扩展功能。
┌─────────────────────────────────────┐
│ Kong Gateway │
│ │
│ ┌──────────┐ ┌───────────────┐ │
│ │ Nginx │ │ Plugin Chain │ │
│ │ (worker) │───▶│ auth → rate │ │
│ │ │ │ → log → ... │ │
│ └──────────┘ └───────────────┘ │
│ │ │
│ ┌──────┴──────┐ │
│ │ Admin API │ │
│ └──────┬──────┘ │
└─────────┼───────────────────────────┘
│
┌───────┴───────┐
│ PostgreSQL / │
│ Cassandra │
└───────────────┘
存储层: Kong 使用 PostgreSQL 或 Cassandra 存储路由、服务、插件等配置。PostgreSQL 适用于中小规模部署,Cassandra 适用于多数据中心同步的大规模场景。
插件系统: Kong 的核心竞争力在于其丰富的插件生态。插件分为多个阶段(Phase)执行:
-- Kong 插件示例:自定义请求头注入
local CustomHeaderHandler = { PRIORITY = 1000, VERSION = "1.0.0" }
function CustomHeaderHandler:access(conf)
kong.service.request.set_header("X-Request-Start", tostring(ngx.now() * 1000))
if conf.inject_consumer then
local consumer = kong.client.get_consumer()
if consumer then
kong.service.request.set_header("X-Consumer-ID", consumer.id)
end
end
end
function CustomHeaderHandler:header_filter(conf)
local start = tonumber(kong.request.get_header("X-Request-Start"))
if start then
local duration = ngx.now() * 1000 - start
kong.response.set_header("X-Processing-Time", tostring(duration) .. "ms")
end
end
return CustomHeaderHandler局限性: Kong 的配置变更依赖数据库轮询,延迟通常在秒级。在需要毫秒级配置生效的场景下,这一点可能不可接受。
3.2 Envoy 架构
Envoy 由 Lyft 开发,使用 C++ 编写,是云原生生态中最重要的数据平面(Data Plane)代理。
graph LR
subgraph Envoy 进程
L[Listener] --> FC[Filter Chain]
FC --> R[Router]
R --> C[Cluster Manager]
C --> U1[Upstream Host 1]
C --> U2[Upstream Host 2]
end
CP[控制平面] -->|xDS API| L
CP -->|xDS API| C
style CP fill:#9cf,stroke:#333
xDS API: Envoy 的核心设计理念是将数据平面与控制平面(Control Plane)分离。控制平面通过 xDS(x Discovery Service)协议族动态下发配置:
| xDS 协议 | 全称 | 用途 |
|---|---|---|
| LDS | Listener Discovery Service | 监听器配置 |
| RDS | Route Discovery Service | 路由规则 |
| CDS | Cluster Discovery Service | 后端集群信息 |
| EDS | Endpoint Discovery Service | 具体服务实例地址 |
| SDS | Secret Discovery Service | TLS 证书和密钥 |
过滤器链(Filter Chain): Envoy 的请求处理通过过滤器链完成。每个过滤器可以读取、修改或终止请求。过滤器分为三类:
- 监听器过滤器(Listener Filter): 处理连接级别的逻辑,如 TLS 检测
- 网络过滤器(Network Filter): 处理 L3/L4 层逻辑,如 TCP 代理
- HTTP 过滤器(HTTP Filter): 处理 L7 层逻辑,如路由、认证、限流
双重角色: Envoy 既可以作为边缘代理(Edge Proxy)部署在网关位置,也可以作为 Sidecar 代理部署在每个服务旁边,同一套过滤器配置可以在两种模式下复用。
3.3 APISIX 架构
Apache APISIX 同样基于 OpenResty 构建,但在配置存储上选择了 etcd 而非关系型数据库。
┌─────────────────────────────────┐
│ APISIX Node │
│ │
│ ┌─────────┐ ┌────────────┐ │
│ │ Nginx │ │ Lua 插件 │ │
│ │ Worker │──▶│ Pipeline │ │
│ └─────────┘ └────────────┘ │
│ │ │
│ ┌────┴────┐ │
│ │ Radixtree│ ← 高性能路由匹配│
│ │ Router │ │
│ └─────────┘ │
└────────┬────────────────────────┘
│ watch
┌────┴────┐ ┌───────────┐
│ etcd │ │ Dashboard │
│ Cluster │◀─────│ (Web) │
└─────────┘ └───────────┘
etcd 存储的优势:
- 实时性: APISIX 通过 etcd 的 watch 机制实现毫秒级配置生效,远快于 Kong 的数据库轮询
- 一致性: etcd 基于 Raft 协议保证强一致性
- 无单点: etcd 集群天然高可用
高性能路由: APISIX 使用基数树(Radixtree)实现路由匹配,时间复杂度为 O(k)(k 为路径长度),在路由规则数量增长时性能几乎不变。
三大网关对比总结
| 维度 | Kong | Envoy | APISIX |
|---|---|---|---|
| 开发语言 | Lua(OpenResty) | C++ | Lua(OpenResty) |
| 配置存储 | PostgreSQL / Cassandra | xDS(控制平面) | etcd |
| 配置生效延迟 | 秒级(DB 轮询) | 亚秒级(xDS 推送) | 毫秒级(etcd watch) |
| 插件生态 | 非常丰富(商业 + 社区) | 较丰富(WASM 扩展) | 丰富(Lua + WASM) |
| 插件开发语言 | Lua / Go | C++ / WASM | Lua / Java / Go / WASM |
| 控制平面 | Kong Manager(商业版) | Istio / 自研 | Dashboard(内置) |
| 云原生集成 | Kubernetes Ingress Controller | Service Mesh 标配 | Kubernetes Ingress Controller |
| 性能(单核 QPS) | 约 15,000 | 约 25,000 | 约 20,000 |
| 部署复杂度 | 中等(需数据库) | 较高(需控制平面) | 低(仅需 etcd) |
| 适用场景 | 传统 API 管理 | Service Mesh / 高性能边缘 | 高性能 API 网关 |
| 社区活跃度 | 高 | 非常高 | 高 |
| 商业支持 | Kong Inc. | Envoy 代理由多家提供 | API7.ai |
选型建议:
- 如果团队已经使用 Kubernetes 和 Istio,Envoy 是天然的选择
- 如果需要丰富的开箱即用插件和商业支持,Kong 是成熟之选
- 如果追求高性能和配置实时生效,APISIX 值得优先考虑
四、BFF 模式详解
4.1 什么是 BFF
BFF(Backend for Frontend)是一种在网关与后端服务之间插入一层专属聚合服务的架构模式。每种客户端类型拥有自己的 BFF 层,负责针对该客户端的特定需求进行数据聚合和裁剪。
graph TB
Web[Web 客户端] --> BFF_Web[Web BFF]
Mobile[移动客户端] --> BFF_Mobile[Mobile BFF]
MiniApp[小程序客户端] --> BFF_Mini[小程序 BFF]
subgraph API 网关层
GW[API 网关]
end
Web --> GW
Mobile --> GW
MiniApp --> GW
GW --> BFF_Web
GW --> BFF_Mobile
GW --> BFF_Mini
subgraph 后端微服务
US[用户服务]
OS[订单服务]
PS[商品服务]
RS[推荐服务]
end
BFF_Web --> US
BFF_Web --> OS
BFF_Web --> PS
BFF_Mobile --> US
BFF_Mobile --> PS
BFF_Mobile --> RS
BFF_Mini --> US
BFF_Mini --> OS
4.2 为什么需要 BFF
不同客户端的需求差异: Web 端的商品详情页可能需要展示完整的商品规格、评价、关联推荐等信息;移动端由于屏幕限制和带宽敏感,只需要核心字段和缩略图;小程序可能需要特殊的分享卡片数据。如果让后端服务直接满足所有客户端的需求,API 设计将变得臃肿且难以演进。
聚合需求: 一个页面的渲染通常需要来自多个服务的数据。例如,订单详情页需要同时调用订单服务、用户服务、商品服务和物流服务。这个聚合逻辑放在客户端会导致多次网络往返;放在后端服务会造成服务间的耦合;放在网关又违反了”网关不做业务逻辑”的原则。BFF 正是处理这种聚合需求的合适位置。
4.3 BFF 的 Go 实现示例
package main
import (
"context"
"encoding/json"
"log"
"net/http"
"sync"
"time"
)
// OrderDetail 是 Mobile BFF 返回给客户端的聚合数据结构
type OrderDetail struct {
OrderID string `json:"order_id"`
Status string `json:"status"`
UserName string `json:"user_name"`
UserAvatar string `json:"user_avatar"`
ProductName string `json:"product_name"`
ProductImg string `json:"product_img"`
TotalPrice int64 `json:"total_price"`
CreatedAt time.Time `json:"created_at"`
}
// 各后端服务的客户端接口
type OrderService interface {
GetOrder(ctx context.Context, orderID string) (*Order, error)
}
type UserService interface {
GetUser(ctx context.Context, userID string) (*User, error)
}
type ProductService interface {
GetProduct(ctx context.Context, productID string) (*Product, error)
}
type MobileBFF struct {
orderSvc OrderService
userSvc UserService
productSvc ProductService
}
func (bff *MobileBFF) HandleOrderDetail(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second)
defer cancel()
orderID := r.URL.Query().Get("order_id")
if orderID == "" {
http.Error(w, "order_id is required", http.StatusBadRequest)
return
}
// 第一步:获取订单基础信息
order, err := bff.orderSvc.GetOrder(ctx, orderID)
if err != nil {
http.Error(w, "failed to get order", http.StatusInternalServerError)
return
}
// 第二步:并发获取用户信息和商品信息
var (
user *User
product *Product
userErr error
prodErr error
wg sync.WaitGroup
)
wg.Add(2)
go func() {
defer wg.Done()
user, userErr = bff.userSvc.GetUser(ctx, order.UserID)
}()
go func() {
defer wg.Done()
product, prodErr = bff.productSvc.GetProduct(ctx, order.ProductID)
}()
wg.Wait()
if userErr != nil || prodErr != nil {
http.Error(w, "failed to fetch related data", http.StatusInternalServerError)
return
}
// 第三步:组装面向移动端的精简响应
detail := OrderDetail{
OrderID: order.ID,
Status: order.Status,
UserName: user.Name,
UserAvatar: user.AvatarSmall, // 移动端使用小图
ProductName: product.Name,
ProductImg: product.ThumbnailURL, // 移动端使用缩略图
TotalPrice: order.TotalPrice,
CreatedAt: order.CreatedAt,
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(detail)
}4.4 BFF 与网关的边界
| 职责 | 网关 | BFF |
|---|---|---|
| 路由分发 | ✓ | ✗ |
| 全局限流 | ✓ | ✗ |
| 身份认证 | ✓ | ✗ |
| 数据聚合 | ✗ | ✓ |
| 字段裁剪 | ✗ | ✓ |
| 客户端适配 | ✗ | ✓ |
| 协议转换 | ✓(通用) | ✓(业务相关) |
关键区分:网关处理的是与业务无关的横切关注点,BFF 处理的是与特定客户端相关的业务聚合逻辑。
五、限流算法在网关中的实现
限流(Rate Limiting)是网关最重要的流量治理能力之一。下面详细分析两种常见算法及其在网关中的实现。
5.1 令牌桶算法
令牌桶(Token Bucket)算法以固定速率向桶中添加令牌,每个请求消耗一个令牌。桶满时新令牌被丢弃,桶空时请求被拒绝。这种算法允许一定程度的突发流量(Burst)。
package ratelimit
import (
"sync"
"time"
)
// TokenBucket 实现令牌桶限流算法
type TokenBucket struct {
mu sync.Mutex
capacity int64 // 桶的容量(最大令牌数)
tokens float64 // 当前令牌数
refillRate float64 // 每秒填充速率
lastRefill time.Time
}
func NewTokenBucket(capacity int64, refillRate float64) *TokenBucket {
return &TokenBucket{
capacity: capacity,
tokens: float64(capacity),
refillRate: refillRate,
lastRefill: time.Now(),
}
}
// Allow 判断是否允许请求通过
func (tb *TokenBucket) Allow() bool {
tb.mu.Lock()
defer tb.mu.Unlock()
now := time.Now()
elapsed := now.Sub(tb.lastRefill).Seconds()
tb.lastRefill = now
// 补充令牌
tb.tokens += elapsed * tb.refillRate
if tb.tokens > float64(tb.capacity) {
tb.tokens = float64(tb.capacity)
}
// 尝试消耗一个令牌
if tb.tokens >= 1 {
tb.tokens--
return true
}
return false
}5.2 滑动窗口算法
滑动窗口(Sliding Window)算法将时间划分为多个小窗口,统计当前时间点往前一个完整周期内的请求数。相比固定窗口,它避免了窗口边界处的突发问题。
package ratelimit
import (
"sync"
"time"
)
// SlidingWindowCounter 实现滑动窗口计数器
type SlidingWindowCounter struct {
mu sync.Mutex
limit int64 // 窗口内允许的最大请求数
window time.Duration // 窗口大小
slots int // 窗口划分的槽数
slotSize time.Duration // 每个槽的大小
counts []int64 // 每个槽的计数
currentSlot int // 当前槽索引
lastUpdate time.Time // 上次更新时间
}
func NewSlidingWindowCounter(
limit int64,
window time.Duration,
slots int,
) *SlidingWindowCounter {
return &SlidingWindowCounter{
limit: limit,
window: window,
slots: slots,
slotSize: window / time.Duration(slots),
counts: make([]int64, slots),
lastUpdate: time.Now(),
}
}
// Allow 判断是否允许请求通过
func (sw *SlidingWindowCounter) Allow() bool {
sw.mu.Lock()
defer sw.mu.Unlock()
now := time.Now()
sw.advanceSlots(now)
// 统计所有槽的总计数
var total int64
for _, c := range sw.counts {
total += c
}
if total >= sw.limit {
return false
}
sw.counts[sw.currentSlot]++
return true
}
// advanceSlots 根据时间流逝推进槽位,清零过期槽
func (sw *SlidingWindowCounter) advanceSlots(now time.Time) {
elapsed := now.Sub(sw.lastUpdate)
slotsToAdvance := int(elapsed / sw.slotSize)
if slotsToAdvance <= 0 {
return
}
if slotsToAdvance >= sw.slots {
// 超过一个完整窗口,清零所有槽
for i := range sw.counts {
sw.counts[i] = 0
}
sw.currentSlot = 0
} else {
// 逐个推进,清零过期槽
for i := 0; i < slotsToAdvance; i++ {
sw.currentSlot = (sw.currentSlot + 1) % sw.slots
sw.counts[sw.currentSlot] = 0
}
}
sw.lastUpdate = now
}5.3 分布式限流
在网关多实例部署的场景下,需要使用分布式限流。通常基于 Redis 实现:
package ratelimit
import (
"context"
"fmt"
"time"
"github.com/redis/go-redis/v9"
)
// DistributedRateLimiter 基于 Redis 的分布式滑动窗口限流器
type DistributedRateLimiter struct {
rdb *redis.Client
limit int64
window time.Duration
}
func NewDistributedRateLimiter(
rdb *redis.Client,
limit int64,
window time.Duration,
) *DistributedRateLimiter {
return &DistributedRateLimiter{
rdb: rdb,
limit: limit,
window: window,
}
}
// Allow 使用 Redis Sorted Set 实现精确的滑动窗口限流
func (rl *DistributedRateLimiter) Allow(
ctx context.Context,
key string,
) (bool, error) {
now := time.Now().UnixMicro()
windowStart := now - rl.window.Microseconds()
redisKey := fmt.Sprintf("ratelimit:%s", key)
// 使用 Lua 脚本保证原子性
script := redis.NewScript(`
-- 移除窗口外的过期记录
redis.call('ZREMRANGEBYSCORE', KEYS[1], '-inf', ARGV[1])
-- 统计当前窗口内的请求数
local count = redis.call('ZCARD', KEYS[1])
if count < tonumber(ARGV[2]) then
-- 未超限,添加当前请求
redis.call('ZADD', KEYS[1], ARGV[3], ARGV[3])
redis.call('PEXPIRE', KEYS[1], ARGV[4])
return 1
end
return 0
`)
result, err := script.Run(
ctx,
rl.rdb,
[]string{redisKey},
windowStart, // ARGV[1]: 窗口起始时间
rl.limit, // ARGV[2]: 限流阈值
now, // ARGV[3]: 当前时间戳
rl.window.Milliseconds(), // ARGV[4]: 过期时间
).Int64()
if err != nil {
return false, err
}
return result == 1, nil
}5.4 限流策略的选择
| 算法 | 突发支持 | 精确度 | 内存开销 | 分布式难度 | 适用场景 |
|---|---|---|---|---|---|
| 令牌桶 | 支持 | 中等 | 低 | 中等 | 允许突发的 API 限流 |
| 漏桶 | 不支持 | 高 | 低 | 中等 | 平滑流量整形 |
| 固定窗口 | 边界突发 | 低 | 低 | 低 | 简单场景 |
| 滑动窗口 | 部分支持 | 高 | 中等 | 高 | 精确计数的 API 限流 |
六、网关的认证与安全
6.1 JWT 验证
网关层的 JWT(JSON Web Token)验证是最常见的认证方式。网关只负责验证 Token 的签名和有效期,然后将解码后的用户信息通过请求头传递给后端服务。
package middleware
import (
"fmt"
"net/http"
"strings"
"github.com/golang-jwt/jwt/v5"
)
type Claims struct {
UserID string `json:"user_id"`
Username string `json:"username"`
Roles []string `json:"roles"`
jwt.RegisteredClaims
}
type JWTAuthMiddleware struct {
publicKey *rsa.PublicKey
issuer string
skipPaths map[string]bool
}
func (m *JWTAuthMiddleware) Handle(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if m.skipPaths[r.URL.Path] {
next.ServeHTTP(w, r)
return
}
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
http.Error(w, "missing authorization header", http.StatusUnauthorized)
return
}
parts := strings.SplitN(authHeader, " ", 2)
if len(parts) != 2 || parts[0] != "Bearer" {
http.Error(w, "invalid authorization format", http.StatusUnauthorized)
return
}
claims := &Claims{}
token, err := jwt.ParseWithClaims(
parts[1], claims,
func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return m.publicKey, nil
},
jwt.WithIssuer(m.issuer),
jwt.WithExpirationRequired(),
)
if err != nil || !token.Valid {
http.Error(w, "invalid token", http.StatusUnauthorized)
return
}
// 将用户信息注入请求头,传递给后端服务
r.Header.Set("X-User-ID", claims.UserID)
r.Header.Set("X-Username", claims.Username)
r.Header.Set("X-User-Roles", strings.Join(claims.Roles, ","))
// 清除原始 Authorization 头,避免 Token 泄露到后端
r.Header.Del("Authorization")
next.ServeHTTP(w, r)
})
}6.2 OAuth2 代理模式
对于需要与第三方身份提供商(Identity Provider)集成的场景,网关可以作为 OAuth2 代理(OAuth2 Proxy),代替后端服务完成整个 OAuth2 流程:
sequenceDiagram
participant C as 客户端
participant GW as API 网关
participant IDP as 身份提供商
participant S as 后端服务
C->>GW: 请求受保护资源
GW->>GW: 检查 Session / Token
alt 未认证
GW->>C: 302 重定向到 IDP
C->>IDP: 用户登录
IDP->>C: 302 重定向回网关(携带 code)
C->>GW: 携带 authorization code
GW->>IDP: 用 code 换取 access_token
IDP->>GW: 返回 access_token
GW->>GW: 创建 Session,缓存 Token
end
GW->>S: 转发请求(注入用户信息头)
S->>GW: 返回业务数据
GW->>C: 返回响应
6.3 安全防护
除认证外,网关还承担以下安全职责:
IP 黑白名单: 基于 IP 地址或 CIDR 段进行访问控制。
WAF 基础能力: 检测和拦截常见的 Web 攻击,如 SQL 注入、XSS、路径穿越等。但完整的 WAF 能力通常由专用设备承担,网关只做基础防护。
TLS 终止(TLS Termination): 网关负责 HTTPS 的加解密,后端服务之间可以使用明文通信(在可信网络内),减少后端的加密开销。
CORS 处理: 跨域资源共享(Cross-Origin Resource Sharing)策略统一在网关管控:
# APISIX CORS 插件配置示例
plugins:
cors:
allow_origins: "https://example.com,https://app.example.com"
allow_methods: "GET,POST,PUT,DELETE,OPTIONS"
allow_headers: "Content-Type,Authorization,X-Request-ID"
expose_headers: "X-RateLimit-Remaining,X-RateLimit-Reset"
max_age: 86400
allow_credential: true七、网关的高可用设计
网关是系统的单一入口,一旦网关不可用,整个系统对外不可达。因此,网关的高可用(High Availability)设计至关重要。
7.1 多实例部署与负载均衡
网关本身应该是无状态的(Stateless),这样才能通过水平扩展(Scale Out)来提高可用性:
┌──────────────┐
│ DNS / VIP │
└──────┬───────┘
│
┌──────┴───────┐
│ L4 负载均衡 │
│ (LVS / NLB) │
└──────┬───────┘
│
┌────────────┼────────────┐
│ │ │
┌─────┴─────┐┌────┴─────┐┌────┴─────┐
│ 网关实例 1 ││ 网关实例 2 ││ 网关实例 3 │
└─────┬─────┘└────┬─────┘└────┬─────┘
│ │ │
└────────────┼────────────┘
│
┌──────┴───────┐
│ 后端服务集群 │
└──────────────┘
L4 负载均衡器 位于网关前方,将流量分发到多个网关实例。常用方案包括:
- 云厂商 NLB(Network Load Balancer): 全托管,自动弹性扩缩
- LVS(Linux Virtual Server): 性能极高,运维成本高
- DNS 轮询: 简单但生效慢,不适合故障快速切换
7.2 健康检查与优雅关闭
L4 负载均衡器需要对网关实例进行健康检查,及时摘除故障节点。健康检查分为两类:
- 存活检查(Liveness): 进程是否还在运行。失败意味着进程需要重启。
- 就绪检查(Readiness): 是否可以接收流量。在启动初始化阶段或滚动更新的排水阶段返回不就绪。
网关实例在滚动更新或缩容时,必须优雅关闭(Graceful Shutdown),确保正在处理中的请求不会被中断。标准流程:
func startGateway(handler http.Handler) {
srv := &http.Server{
Addr: ":8080",
Handler: handler,
ReadTimeout: 15 * time.Second,
WriteTimeout: 15 * time.Second,
IdleTimeout: 60 * time.Second,
}
go func() {
log.Printf("gateway listening on %s", srv.Addr)
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
log.Fatalf("listen error: %v", err)
}
}()
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
// 先标记为不就绪,等负载均衡器感知后再关闭
healthChecker.SetReady(false)
time.Sleep(5 * time.Second)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Printf("forced shutdown: %v", err)
}
}7.3 配置中心高可用
网关依赖的配置存储也必须是高可用的:
- Kong + PostgreSQL: 使用主从复制 + 自动故障转移(如 Patroni)
- APISIX + etcd: 部署 3 或 5 节点的 etcd 集群
- Envoy + 控制平面: 控制平面本身也需要多副本部署
当配置中心不可用时,网关应该能够使用本地缓存的配置继续工作,这是网关高可用的最后一道防线。
八、工程案例:某金融科技公司的网关演进
8.1 背景
某金融科技公司的核心业务系统由 120 个微服务组成,日均 API 调用量约 8 亿次,峰值 QPS 达到 5 万。系统经历了三个阶段的网关演进。
8.2 第一阶段:Nginx + Lua 自研网关
最初团队使用 Nginx 作为反向代理,配合自研的 Lua 脚本实现基础的路由和限流功能。
问题:
- 路由配置以静态文件形式管理,每次变更需要重新加载 Nginx,存在短暂的连接中断
- 限流逻辑硬编码在 Lua 脚本中,修改限流策略需要重新部署
- 缺乏统一的监控和日志采集,排查问题困难
- 认证逻辑分散在各个服务中,存在实现不一致的安全风险
8.3 第二阶段:迁移到 APISIX
团队评估了 Kong、Envoy 和 APISIX 后,最终选择 APISIX,原因包括:
- etcd 的 watch 机制可以实现路由的毫秒级生效,满足金融场景对配置变更实时性的要求
- 内置的 Dashboard 降低了运维门槛
- Lua 插件机制与原有的自研 Lua 脚本兼容性好,迁移成本低
- 性能测试显示在同等硬件条件下,APISIX 的 QPS 比 Kong 高约 30%
迁移架构:
graph TB
subgraph 外部流量
CLB[云负载均衡器]
end
subgraph 网关集群(6 节点)
A1[APISIX-1]
A2[APISIX-2]
A3[APISIX-3]
A4[APISIX-4]
A5[APISIX-5]
A6[APISIX-6]
end
subgraph 配置中心
E1[etcd-1]
E2[etcd-2]
E3[etcd-3]
end
subgraph 限流存储
R1[Redis Cluster]
end
subgraph 后端服务
SG1[用户服务组]
SG2[交易服务组]
SG3[风控服务组]
SG4[支付服务组]
end
CLB --> A1
CLB --> A2
CLB --> A3
CLB --> A4
CLB --> A5
CLB --> A6
A1 --> E1
A2 --> E2
A3 --> E3
A1 --> R1
A2 --> R1
A1 --> SG1
A2 --> SG2
A3 --> SG3
A4 --> SG4
实施步骤:
- 部署 APISIX 集群与 etcd 集群
- 将原有 Nginx 路由规则逐条迁移到 APISIX
- 使用灰度切流(Canary Release)逐步将流量从 Nginx 切换到 APISIX
- 在 APISIX 上启用 JWT 认证插件,统一所有服务的认证逻辑
- 配置分布式限流,基于 Redis Cluster 实现跨网关实例的全局限流
- 接入 Prometheus + Grafana 监控体系
8.4 第三阶段:引入 BFF 层
随着移动端、Web 端和开放平台三条业务线的分化,团队引入了 BFF 层:
- Mobile BFF: 负责移动端的数据聚合和裁剪,优化移动端的数据传输量
- Web BFF: 负责 Web 管理后台的复杂查询聚合
- Open API BFF: 负责开放平台的接口适配和版本管理
8.5 成果
| 指标 | 迁移前 | 迁移后 |
|---|---|---|
| 路由变更生效时间 | 30 秒(Nginx reload) | 50 毫秒(etcd watch) |
| P99 延迟(网关层) | 12 毫秒 | 3 毫秒 |
| 认证逻辑代码行数 | 12,000 行(分散在 40 个服务中) | 200 行(统一在网关插件中) |
| 限流策略调整耗时 | 2 小时(需逐服务部署) | 5 分钟(Dashboard 配置) |
| 故障排查平均耗时 | 45 分钟 | 10 分钟 |
| 网关可用性 | 99.95% | 99.99% |
九、网关反模式与最佳实践
9.1 反模式
反模式一:胖网关(Fat Gateway)
将业务逻辑放入网关,例如在网关中执行订单价格计算、库存扣减等操作。这导致网关变得臃肿且难以维护,每次业务逻辑变更都需要重新部署网关——而网关的变更影响全局。
// 错误示范:在网关插件中实现业务逻辑
function access(conf)
-- 网关不应该直接查询数据库
local db = connect_database()
local user = db:query("SELECT * FROM users WHERE id = ?", user_id)
-- 网关不应该执行业务规则
if user.vip_level > 3 and order.amount > 10000 then
apply_vip_discount(order)
end
end
反模式二:业务逻辑泄露(Business Logic Leak)
网关路由规则中隐含了业务逻辑。例如,根据用户类型将请求路由到不同的服务版本——这本质上是一个业务决策,不应该由网关做出。
反模式三:单体网关(Monolithic Gateway)
所有团队共享一个网关配置,任何团队的配置变更都可能影响其他团队。正确的做法是按团队或业务域划分网关,或至少在逻辑上隔离不同团队的路由配置。
反模式四:忽视网关自身的可观测性
只关注后端服务的监控,忽略了网关本身的内存、CPU、连接数等指标。网关作为全局入口,自身的性能问题会被放大到所有服务。
9.2 最佳实践
实践一:保持网关的无状态性。 网关不应该在内存中维护任何会话状态。所有需要持久化的数据(限流计数、会话信息)都应该存储在外部组件中。
实践二:分层限流。 在网关层实施全局限流(防止系统过载),在服务层实施细粒度限流(按业务场景调整)。两层限流各司其职。
实践三:统一认证,分散鉴权。 身份认证(你是谁)在网关完成,权限校验(你能做什么)由各服务自行处理。因为权限规则与业务逻辑紧密相关,放在网关中会导致胖网关。
请求流程:
客户端 → 网关(验证 JWT,提取用户身份)
→ 后端服务(根据用户角色检查操作权限)
实践四:灰度发布能力。 网关应该支持按比例、按用户标签、按地域等维度进行灰度路由,这是微服务系统安全发布的基础能力:
# APISIX 灰度路由配置示例
routes:
- uri: /api/v1/orders/*
plugins:
traffic-split:
rules:
- match:
- vars:
- ["http_x_user_group", "==", "beta"]
weighted_upstreams:
- upstream:
type: roundrobin
nodes:
"order-service-v2:8080": 1
weight: 1
- weighted_upstreams:
- upstream:
type: roundrobin
nodes:
"order-service-v1:8080": 1
weight: 1实践五:超时与重试策略。 网关应该为每个路由配置合理的超时时间和重试策略。重试只应该针对幂等请求(GET、PUT、DELETE),非幂等请求(POST)的重试需要谨慎处理。
实践六:协议转换的边界。 通用协议转换(如 TLS 终止、HTTP/2 到 HTTP/1.1 降级)放在网关;业务相关的协议适配(如将 REST 请求映射到特定的 gRPC 方法)放在 BFF 或服务自身。
9.3 协议转换:REST 到 gRPC
网关层的 REST 到 gRPC 转换是常见需求。以 Envoy 的 gRPC-JSON 转码器(Transcoder)为例:
# Envoy gRPC-JSON 转码配置
http_filters:
- name: envoy.filters.http.grpc_json_transcoder
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.grpc_json_transcoder.v3.GrpcJsonTranscoder
proto_descriptor: "/etc/envoy/proto.pb"
services:
- "user.UserService"
print_options:
add_whitespace: true
always_print_primitive_fields: true
preserve_proto_field_names: true对应的 Proto 定义:
syntax = "proto3";
package user;
import "google/api/annotations.proto";
service UserService {
rpc GetUser(GetUserRequest) returns (GetUserResponse) {
option (google.api.http) = { get: "/api/v1/users/{user_id}" };
}
rpc CreateUser(CreateUserRequest) returns (CreateUserResponse) {
option (google.api.http) = { post: "/api/v1/users" body: "*" };
}
}
message GetUserRequest { string user_id = 1; }
message GetUserResponse {
string user_id = 1;
string username = 2;
string email = 3;
int64 created_at = 4;
}
message CreateUserRequest {
string username = 1;
string email = 2;
string password = 3;
}
message CreateUserResponse { string user_id = 1; }客户端发送标准的 HTTP/JSON 请求,Envoy 自动将其转换为 gRPC 调用并转发给后端的 gRPC 服务,再将 gRPC 响应转回 JSON 返回给客户端。整个过程对客户端完全透明。
9.4 网关选型决策树
在实际选型中,可以按照以下决策路径进行判断:
graph TD
Q1{是否已使用 Service Mesh?}
Q1 -->|是| A1[Envoy 作为入口网关]
Q1 -->|否| Q2{是否需要丰富的商业插件?}
Q2 -->|是| A2[Kong Enterprise]
Q2 -->|否| Q3{是否需要毫秒级配置生效?}
Q3 -->|是| A3[APISIX]
Q3 -->|否| Q4{团队是否有 C++ 能力?}
Q4 -->|是| A4[Envoy + 自研控制平面]
Q4 -->|否| Q5{是否需要低运维成本?}
Q5 -->|是| A5[云厂商托管网关]
Q5 -->|否| A6[Kong 开源版]
参考资料
- Chris Richardson, “Microservices Patterns”, Manning Publications, 2018, Chapter 8: External API Patterns
- Sam Newman, “Building Microservices”, 2nd Edition, O’Reilly, 2021, Chapter 7: Build
- Kong 官方文档, https://docs.konghq.com/
- Envoy 官方文档, https://www.envoyproxy.io/docs/envoy/latest/
- Apache APISIX 官方文档, https://apisix.apache.org/docs/
- Matt Klein, “Introduction to Modern Network Load Balancing and Proxying”, 2017
- “Pattern: API Gateway / Backends for Frontends”, https://microservices.io/patterns/apigateway.html
- Gupta, A. “API Gateway vs. Service Mesh”, NGINX Blog, 2020
- 云原生社区, “APISIX 与 Kong 的对比分析”, 2022
- Google Cloud, “API Gateway Design Patterns”, https://cloud.google.com/architecture/api-gateway
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【系统架构设计百科】Service Mesh:Sidecar 的代价与无 Sidecar 的未来
2023 年,某头部电商平台在全量接入 Istio 后发现:每个 Pod 的内存占用增加了 40-70 MB,p99 延迟从 12 ms 上升到 18 ms,整个集群每月多出数万美元的计算成本。这并非个例。CNCF 2024 年度调查显示,超过 60% 的受访企业已在生产环境中使用或评估服务网格(Service Mes…
【系统架构设计百科】架构质量属性:不只是"高可用高性能"
需求评审时写下的'高可用、高性能、高并发',到了架构设计阶段几乎无法落地——因为它们不是可执行的需求。本文从 SEI/CMU 的质量属性理论出发,用 stimulus-response 场景模型把模糊需求变成可量化、可验证的架构约束,并拆解属性之间的冲突与联动关系。
【系统架构设计百科】告警策略:如何避免"狼来了"
大多数团队的告警系统都在制造噪声而不是传递信号。阈值告警看似直观,实则产生大量误报和漏报,值班工程师在凌晨三点被叫醒,却发现只是一次无害的毛刺。本文从告警疲劳的工业数据出发,拆解基于 SLO 的多窗口燃烧率告警算法,深入 Alertmanager 的路由、抑制与分组机制,结合 PagerDuty 的告警疲劳研究和真实工程案例,给出一套可落地的告警策略设计方法。
【系统架构设计百科】复杂性管理:架构的核心战场
系统复杂性是架构腐化的根源——本文从 Brooks 的本质复杂性与偶然复杂性划分出发,结合认知负荷理论与 Parnas 的信息隐藏原则,系统阐述复杂性的来源、度量与控制手段,并给出可操作的架构策略