微服务架构普及之后,服务数量从十几个增长到上百个,客户端直连服务端会面临鉴权分散、限流缺失、契约混乱与灰度困难等一系列问题。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 反向代理与负载均衡 — upstream 与 proxy_pass 转发语义
- Nginx 核心配置结构 — location 匹配与指令上下文基础
- Nginx 限流与安全防护 — limit_req 与 limit_conn 深度实践
- Nginx HTTPS 与 TLS 加固 — 网关入口的 TLS 终结配置
- Nginx 缓存与压缩优化 — 网关响应的缓存与压缩
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。