Nginx 作为 API 网关:路由转发、统一鉴权与灰度发布的完整实践

以 Nginx 为核心实现 API 网关模式,覆盖网关路由与请求转发、统一鉴权与会话校验、限流与配额管理、灰度发布与流量切分,并厘清网关与反向代理的能力边界,给出生产级网关的高可用实践。

微服务架构普及之后,服务数量从十几个增长到上百个,客户端直连服务端会面临鉴权分散、限流缺失、契约混乱与灰度困难等一系列问题。API 网关正是为了解决这些问题而出现的统一入口:它承接所有外部流量,把鉴权、限流、路由、灰度、日志与监控从业务服务中剥离出来,让下游服务只关注业务本身。Nginx 凭借事件驱动的高并发模型、丰富的模块生态与成熟的运维体系,是最常被选作 API 网关底座的开源组件之一。本文从网关的职责边界出发,完整讲解如何用 Nginx 构建一套可落地的 API 网关。

一句话总结: API 网关把横切关注点收敛到统一入口,Nginx 用它的事件模型与模块生态即可承担路由、鉴权、限流与灰度等网关核心职责。

1. API 网关的定位与职责边界

一句话总结: 网关解决的是跨服务的横切问题,职责上聚焦路由、鉴权、限流、灰度与观测,业务逻辑必须留在下游服务。

API 网关位于客户端与后端服务集群之间,是所有 API 流量的必经之地。它要解决的问题分为四类:一是接入问题,统一入口、协议转换与 TLS 终结;二是治理问题,鉴权、限流、配额与灰度;三是路由问题,按路径或请求头把流量分发到正确的服务;四是观测问题,在入口处采集统一格式的访问日志与指标。

# 一个 API 网关的最小骨架:定义上游服务集群
upstream order_service {
    server 10.0.1.10:8080 weight=3;
    server 10.0.1.11:8080 weight=3;
    keepalive 32;
}

upstream user_service {
    server 10.0.2.10:8080;
    keepalive 32;
}

server {
    listen 443 ssl;
    http2 on;
    server_name api.example.com;

    ssl_certificate     /etc/nginx/ssl/api.example.com.crt;
    ssl_certificate_key /etc/nginx/ssl/api.example.com.key;

    # 按路径前缀路由到不同服务
    location /api/order/ {
        proxy_pass http://order_service;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
    }

    location /api/user/ {
        proxy_pass http://user_service;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
    }
}

网关设计有一个反复被强调的边界:网关只做横切。把业务规则、服务编排、聚合逻辑塞进网关,会立刻让网关变成性能与迭代的双重瓶颈。网关应当保持无状态,鉴权结果通过请求头传递给下游,灰度标记通过标准化请求头下发,这样任何一台网关都能无差别地处理任何请求,水平扩容才成为可能。

2. 网关路由与请求转发

一句话总结: 网关路由把「外部路径」映射到「内部服务」,核心是路径前缀、重写规则与请求头透传三件事。

路由是网关最基础的能力。与普通反向代理不同,网关的路由通常不是一对一,而是多对多:多个消费方调用多个服务,路径空间需要精心设计。常见的设计是把服务名嵌入路径前缀,例如 /api/order/、/api/user/、/api/payment/,网关按前缀做第一层分发。

# 路径前缀路由 + URI 重写
location /api/order/ {
    # 去掉 /api 前缀,把 /api/order/create 转发为 /order/create
    rewrite ^/api/order/(.*)$ /order/$1 break;
    proxy_pass http://order_service;
    proxy_set_header X-Original-URI $request_uri;
}

location = /api/health {
    return 200 '{"status":"ok"}';
    default_type application/json;
}

转发时请求头透传需要刻意处理。Nginx 默认会把 Host 设置为 proxy_pass 的目标地址,网关通常需要保留原始的 Host 与客户端信息,并补上标准的 X-Forwarded-* 头,下游服务才能拿到真实的客户端 IP 与协议:

location /api/ {
    proxy_set_header Host              $host;
    proxy_set_header X-Real-IP         $remote_addr;
    proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Request-ID      $request_id;
    proxy_pass http://api_cluster;
}

$request_id 是 Nginx 内置变量,为每个请求生成唯一 ID。网关入口把它写入请求头透传给下游,并在访问日志中记录同一字段,排错时就能用一条 ID 串起「客户端 → 网关 → 下游服务」的完整调用链。路由规则建议独立成文件,按服务分片,用 include 拼装,避免单个 server 块膨胀到无法维护。

3. 统一鉴权与会话校验

一句话总结: 统一鉴权让网关成为唯一校验身份的位置,auth_request 把鉴权委托给独立服务,结果在网关层缓存避免重复开销。

没有网关时,每个服务都要自己校验 token,同一个用户要拿着不同的凭证访问不同服务。网关把鉴权收拢到一处:客户端只持有一种凭证,网关校验通过后才把身份信息以可信请求头的形式传递给下游。Nginx 提供了 auth_request 指令,把请求先发给一个内部鉴权服务做子请求校验,根据子请求的响应码决定放行还是拒绝。

# 内部鉴权端点,不对外暴露
location = /_internal/auth {
    internal;
    proxy_pass http://auth_service/auth/verify;
    proxy_pass_request_body off;
    proxy_set_header Content-Length "";
    proxy_set_header X-Original-URI $request_uri;
    proxy_set_header Authorization $http_authorization;
}

# 受保护的 API:先鉴权,后路由
location /api/protected/ {
    auth_request /_internal/auth;
    auth_request_set $auth_uid $upstream_http_x_auth_uid;
    auth_request_set $auth_roles $upstream_http_x_auth_roles;

    proxy_pass http://api_cluster;
    proxy_set_header X-Auth-UID   $auth_uid;   # 把身份透传给下游
    proxy_set_header X-Auth-Roles $auth_roles;
}

鉴权子请求会显著增加延迟,因为每个受保护请求都多了一次内部往返。缓解手段是把鉴权结果在网关层缓存起来。借助 OpenResty 的 lua_shared_dict 或外置 Redis,网关可以在首次校验通过后缓存「token → 身份」映射,在有效期内直接放行:

# 配合 OpenResty,用共享字典缓存鉴权结果
lua_shared_dict auth_cache 64m;

server {
    location /api/ {
        access_by_lua_block {
            local token = ngx.var.http_authorization
            local cache = ngx.shared.auth_cache
            if token and cache:get("auth:" .. token) then
                return  -- 命中缓存,直接放行
            end
            -- 未命中,回源鉴权服务(略)
        }
        proxy_pass http://api_cluster;
    }
}

网关鉴权有两个常见误区:一是把密钥、证书硬编码进配置,正确的做法是密钥统一由配置中心或环境变量下发;二是让下游信任 X-Auth-* 请求头却不做来源隔离——网关必须确保下游服务只接受来自网关的请求(通过内网网络策略或网关侧加密),否则任何客户端都能伪造身份头直接访问下游。

4. 限流与配额管理

一句话总结: 网关限流按消费方维度展开,limit_req 管请求速率、limit_conn 管并发连接,配合 map 就能实现按调用方分级的配额策略。

限流是网关治理能力的核心。Nginx 的 limit_req 基于漏桶算法控制请求速率,limit_conn 控制同一时刻的并发连接数。网关场景下限流必须按「谁在调用」区分:不同消费方、不同 API 有不同的配额,否则一个突发调用方就会挤占全部资源。

# 按消费方 API Key 区分限流键
map $http_x_api_key $req_key {
    default          "anonymous";
    "~^acct-(\w+)$"  $1;          # 按账号维度
}

limit_req_zone $req_key zone=per_key:10m rate=100r/s;
limit_req_zone $binary_remote_addr zone=per_ip:10m rate=20r/s;

server {
    location /api/ {
        # 账号维度限流 + 突发缓冲
        limit_req zone=per_key burst=50 nodelay;
        # IP 维度兜底限流
        limit_req zone=per_ip burst=10;
        limit_conn per_key_conn 200;

        proxy_pass http://api_cluster;
    }
}

配额管理还需要区分「速率」与「总量」。速率是每秒多少请求,总量是每个计费周期(如每天)多少请求。总量配额需要跨网关实例共享计数,通常借助 Redis 的 INCR 与过期时间实现:

-- OpenResty: 每日配额检查(伪代码示意)
local daily_key = "quota:" .. account_id .. ":" .. os.date("%Y%m%d")
local used = redis:incr(daily_key)
if used == 1 then
    redis:expire(daily_key, 86400)
end
if used > DAILY_QUOTA then
    return ngx.exit(429)  -- 配额耗尽,返回 429
end

限流触发时返回 429(Too Many Requests)并在响应头里告知消费方限流窗口与剩余额度,让调用方可以做退避。同时在日志与指标中记录限流事件,用于分析哪些调用方超量、是否需要扩容或调整配额。429 响应建议带上 Retry-After 头,客户端可以据此安排重试时机。

5. 灰度发布与流量切分

一句话总结: 灰度发布把新版本流量限制在可控比例内,Nginx 用 map 变量配合多 upstream 即可实现按头、按 Cookie、按权重的三种切分方式。

网关是灰度发布的天然执行点:新版本服务先接收一小部分真实流量,验证无误后再逐步放大,直到全量切换。Nginx 实现灰度有几种常用手段,核心都是「用变量选出目标 upstream」。

按请求头灰度,适合测试团队与内测用户通过固定请求头提前验证新版本:

# 按请求头 X-Canary: beta 切流到新版本
map $http_x_canary $upstream_group {
    default      v1_cluster;
    beta         v2_cluster;
}

upstream v1_cluster { server 10.0.1.10:8080; }
upstream v2_cluster { server 10.0.1.20:8080; }

server {
    location /api/ {
        proxy_pass http://$upstream_group;
    }
}

按权重灰度,适合按比例放量。利用 $request_id 的散列特性,把一定比例的请求导流到新版本:

# 用 request_id 的散列取模,实现 10% 流量进入新版本
map $request_id $canary {
    default    v1_cluster;
    "~^[0-9a-f]{0,6}" v2_cluster;   # 简化示意:hash 值小于某阈值的进入新版本
}

更精确的做法是在 Lua 里对 $request_id 做 sha256 后取模,值落在灰度区间内就选择新版本。生产实践中还会配合 mirror 指令做流量镜像——把线上请求复制一份发往新版本,新版本只接收不影响真实用户的影子流量,用于验证稳定性的同时不承担可用性风险。

6. 网关与反向代理的边界

一句话总结: 反向代理关注后端拓扑与接入,网关在此基础上叠加面向调用方的治理语义,二者能力有重叠但目标不同。

反向代理与 API 网关经常被混淆,因为技术上它们常常由同一个组件(Nginx)承担。区分二者要看「为谁服务」:反向代理为服务端服务,关心的是后端集群的负载均衡、TLS 终结、静态文件与缓存;API 网关为调用方服务,关心的是鉴权、配额、灰度与 API 契约。一个以 proxy_pass 为主的配置是反向代理,叠加了鉴权、配额、按消费方灰度等语义后才是网关。

维度反向代理API 网关
服务对象后端服务集群API 消费方
核心能力负载均衡、TLS、缓存鉴权、限流、灰度、配额
路由粒度主机/路径路径 + 消费方 + 版本
关注点后端可达性与吞吐调用方治理与契约
典型场景静态站、单服务代理微服务统一入口

实践中两者经常叠加:同一套 Nginx 集群对外是网关(承担治理),对内是反向代理(承担负载均衡)。边界不清的风险在于职责蔓延——网关层开始出现业务聚合、数据库访问等逻辑,导致网关难以水平扩容、发布周期被业务阻塞。判断标准很简单:这个功能是否是所有 API 调用方共需的横切能力,如果不是,就应当留在下游服务里。

7. 网关高可用与生产实践

一句话总结: 网关是无状态的多副本前置,前面挂云 LB,配置走版本管理与发布流水线,压测验证容量后即可支撑高可用。

网关自身必须高可用,因为它处在所有流量的必经路径上。架构上网关是无状态的多副本部署:多个 Nginx 实例挂在云负载均衡(SLB/ELB)后面,LB 做健康检查与流量分发,任意一台网关宕机都不会影响整体服务。Nginx 侧不需要 session 粘性,因为网关不保存会话状态,鉴权信息全部通过请求头传递。

# 网关健康检查端点:LB 与监控系统都探测它
location = /healthz {
    access_log off;
    return 200 '{"status":"ok"}';
    default_type application/json;
}

网关配置要纳入版本管理与发布流程。配置即代码:每个服务的路由规则独立成文件,提交 Git,经 CI 校验(nginx -t)后通过发布流水线分发到所有网关节点,最后 nginx -s reload 平滑生效。回滚时只需切回上一个配置版本重新 reload。配置发布遵循小步快跑原则,路由规则的变更要像代码变更一样走评审。

网关上线前必须做容量验证。压测要覆盖两条链路:一是正常路径的吞吐与延迟,二是限流触发路径的稳定性(限流本身也会消耗 CPU 与共享内存)。网关的瓶颈往往不在 Nginx 本身而在上游链路,压测时要把下游服务的真实能力考虑进去,避免把网关容量估计得过高。上线后持续观察网关的指标水位,为扩容留出提前量。

8. 总结

环节要点
职责边界网关只做路由、鉴权、限流、灰度与观测等横切能力
网关路由路径前缀映射服务,重写 URI,透传标准转发头与请求 ID
统一鉴权auth_request 委托鉴权服务,缓存鉴权结果,透传可信身份头
限流配额limit_req/limit_conn 按调用方维度,Redis 实现总量配额
灰度发布map 变量选择上游,按头/按权重/按 Cookie 切流,mirror 影子流量
网关边界反向代理服务后端,网关服务调用方,业务逻辑留在下游
高可用无状态多副本 + 云 LB,配置走版本管理与 reload 发布
实践检查压测验证容量,持续观察指标水位,为扩容留提前量

用 Nginx 构建 API 网关,本质是把「配置树」升级为「治理面」:路由规则、鉴权委托、限流配额、灰度切分都沉淀为可评审、可回滚的配置资产。网关层收敛的横切能力越多,下游服务就越能专注于业务,整个微服务体系的迭代速度与稳定性也越高。接下来可以继续探索四层代理场景,把 TCP/UDP 流量的治理也纳入同一套 Nginx 体系。

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「nginx」更多文章

  1. Nginx Ingress Controller:Kubernetes 云原生网关的路由、证书与金丝雀发布
  2. Nginx 监控与可观测性:stub_status 指标、Prometheus 集成与容量规划
  3. Nginx 静态资源与页面加速:零拷贝、缓存验证与 Brotli 压缩联动