土法炼钢兴趣小组的算法知识备份

【系统架构设计百科】API 网关设计:入口层的职责边界

文章导航

分类入口
architecture
标签入口
#API-gateway#Kong#Envoy#APISIX#BFF#reverse-proxy

目录

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 请求/响应改写

网关可以对请求和响应进行改写,包括:

2.6 可观测性

网关是采集全局指标的最佳位置:

职责边界原则

并非所有事情都应该放在网关上做。核心原则是:网关只做与业务逻辑无关的横切关注点。

应该放在网关 不应该放在网关
路由分发 业务数据校验
全局限流 业务规则执行
身份认证 复杂的数据聚合
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 的请求处理通过过滤器链完成。每个过滤器可以读取、修改或终止请求。过滤器分为三类:

双重角色: 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 使用基数树(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

选型建议:

四、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 负载均衡器 位于网关前方,将流量分发到多个网关实例。常用方案包括:

7.2 健康检查与优雅关闭

L4 负载均衡器需要对网关实例进行健康检查,及时摘除故障节点。健康检查分为两类:

网关实例在滚动更新或缩容时,必须优雅关闭(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 配置中心高可用

网关依赖的配置存储也必须是高可用的:

当配置中心不可用时,网关应该能够使用本地缓存的配置继续工作,这是网关高可用的最后一道防线。

八、工程案例:某金融科技公司的网关演进

8.1 背景

某金融科技公司的核心业务系统由 120 个微服务组成,日均 API 调用量约 8 亿次,峰值 QPS 达到 5 万。系统经历了三个阶段的网关演进。

8.2 第一阶段:Nginx + Lua 自研网关

最初团队使用 Nginx 作为反向代理,配合自研的 Lua 脚本实现基础的路由和限流功能。

问题:

8.3 第二阶段:迁移到 APISIX

团队评估了 Kong、Envoy 和 APISIX 后,最终选择 APISIX,原因包括:

迁移架构:

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

实施步骤:

  1. 部署 APISIX 集群与 etcd 集群
  2. 将原有 Nginx 路由规则逐条迁移到 APISIX
  3. 使用灰度切流(Canary Release)逐步将流量从 Nginx 切换到 APISIX
  4. 在 APISIX 上启用 JWT 认证插件,统一所有服务的认证逻辑
  5. 配置分布式限流,基于 Redis Cluster 实现跨网关实例的全局限流
  6. 接入 Prometheus + Grafana 监控体系

8.4 第三阶段:引入 BFF 层

随着移动端、Web 端和开放平台三条业务线的分化,团队引入了 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 开源版]

参考资料

  1. Chris Richardson, “Microservices Patterns”, Manning Publications, 2018, Chapter 8: External API Patterns
  2. Sam Newman, “Building Microservices”, 2nd Edition, O’Reilly, 2021, Chapter 7: Build
  3. Kong 官方文档, https://docs.konghq.com/
  4. Envoy 官方文档, https://www.envoyproxy.io/docs/envoy/latest/
  5. Apache APISIX 官方文档, https://apisix.apache.org/docs/
  6. Matt Klein, “Introduction to Modern Network Load Balancing and Proxying”, 2017
  7. “Pattern: API Gateway / Backends for Frontends”, https://microservices.io/patterns/apigateway.html
  8. Gupta, A. “API Gateway vs. Service Mesh”, NGINX Blog, 2020
  9. 云原生社区, “APISIX 与 Kong 的对比分析”, 2022
  10. Google Cloud, “API Gateway Design Patterns”, https://cloud.google.com/architecture/api-gateway

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-04-13 · architecture

【系统架构设计百科】Service Mesh:Sidecar 的代价与无 Sidecar 的未来

2023 年,某头部电商平台在全量接入 Istio 后发现:每个 Pod 的内存占用增加了 40-70 MB,p99 延迟从 12 ms 上升到 18 ms,整个集群每月多出数万美元的计算成本。这并非个例。CNCF 2024 年度调查显示,超过 60% 的受访企业已在生产环境中使用或评估服务网格(Service Mes…

2026-04-13 · architecture

【系统架构设计百科】架构质量属性:不只是"高可用高性能"

需求评审时写下的'高可用、高性能、高并发',到了架构设计阶段几乎无法落地——因为它们不是可执行的需求。本文从 SEI/CMU 的质量属性理论出发,用 stimulus-response 场景模型把模糊需求变成可量化、可验证的架构约束,并拆解属性之间的冲突与联动关系。

2026-04-13 · architecture

【系统架构设计百科】告警策略:如何避免"狼来了"

大多数团队的告警系统都在制造噪声而不是传递信号。阈值告警看似直观,实则产生大量误报和漏报,值班工程师在凌晨三点被叫醒,却发现只是一次无害的毛刺。本文从告警疲劳的工业数据出发,拆解基于 SLO 的多窗口燃烧率告警算法,深入 Alertmanager 的路由、抑制与分组机制,结合 PagerDuty 的告警疲劳研究和真实工程案例,给出一套可落地的告警策略设计方法。

2026-04-13 · architecture

【系统架构设计百科】复杂性管理:架构的核心战场

系统复杂性是架构腐化的根源——本文从 Brooks 的本质复杂性与偶然复杂性划分出发,结合认知负荷理论与 Parnas 的信息隐藏原则,系统阐述复杂性的来源、度量与控制手段,并给出可操作的架构策略


By .