用 nginx 搭建网关 — 密钥、配额、版本
目标
用 nginx 构造 API 网关的原理——路径代理、API 密钥认证(401)、按消费者的配额(429)、版本路由与 Sunset、请求 ID 的传播与日志、上游超时(504)。
为什么重要
认证、调用量限制和版本,与业务无关,对所有 API 都同样需要。如果在每台 API 服务器上各自实现,就会渐渐变得各不相同,某个消费者的失控会让所有人变慢,也没有人知道谁在使用旧版本。网关在没有业务逻辑的情况下,在一个地方对这些进行管控。
步骤
- 启动两个上游:
nohup python3 /opt/lab/fixtures/eaimw/gateway/api.py --port 9601 --version v1 > /root/eaimw/gw/v1.out 2>&1 &,… --port 9602 --version v2 …。用curl各调用一次,把两行响应 JSON 保存到/root/eaimw/gw/up.txt。 - 编写
/root/eaimw/gw/nginx.conf(完整的配置文件:pid /root/eaimw/gw/nginx.pid;,日志位于/root/eaimw/gw/logs/,listen 8090;,上游127.0.0.1:9601·127.0.0.1:9602)。把/v1/原样路径转给 v1,/v2/原样路径转给 v2。用nginx -p /root/eaimw/gw -c /root/eaimw/gw/nginx.conf启动。 - API 密钥认证:在
/root/eaimw/gw/keys.map中放置key-channel-7f3a channel;·key-partner-19c2 partner;两行,并用map $http_x_api_key $consumer引入。如果没有消费者(没有密钥、密钥错误),就返回 401 和 JSON 正文。对上游传递X-Consumer: <소비자>(占位符为消费者),不传递X-API-Key。 - 配额:
limit_req_zone $consumer … rate=5r/s、limit_req … burst=5 nodelay、limit_req_status 429。一个消费者集中发送时返回 429,而其他消费者必须不受影响。 - 版本:
/api/在X-API-Version: 2时转给 v2,没有则转给 v1(map $http_x_api_version)。对发往 v1 的响应(/v1/和/api/中的 v1)加上Sunset: Thu, 31 Dec 2026 23:59:59 GMT头,v2 则不加。 - 请求 ID:如果收到了
X-Request-ID,就原样使用,没有则把$request_id以X-Request-ID传给上游。把访问日志格式按'<요청ID> <소비자> <상태> "<요청줄>"'(占位符依次为请求 ID、消费者、状态、请求行;例如log_format gw '$rid $consumer $status "$request"';)记录到/root/eaimw/gw/logs/access.log。 - 通过
proxy_read_timeout 2s,当上游很慢(/v1/slow需要 5 秒)时,在 2 秒左右返回 504。
参考
- 修改配置之后:
nginx -t -p /root/eaimw/gw -c /root/eaimw/gw/nginx.conf && nginx -s reload -p /root/eaimw/gw -c /root/eaimw/gw/nginx.conf - 确认:
curl -s -H 'X-API-Key: key-channel-7f3a' localhost:8090/v1/hello | jq .headers - 评分器会把配置文件复制到临时目录,把
8090·9601·9602和/root/eaimw/gw/路径换成它自己的值再重新启动。请在配置中把这些数字和路径按字面写出来。 map的值中可以使用变量("" $request_id;)。add_header默认只会附加在 2xx、3xx 响应上。- 常见错误:把配额的键设为
$binary_remote_addr(无法区分消费者);漏掉limit_req_status而使 503 发了出去;忘记proxy_set_header X-API-Key ""而让密钥泄漏到上游。
启动上游的两个版本
用 api.py 启动 v1(9601)和 v2(9602),并把两行响应 JSON 保存到 /root/eaimw/gw/up.txt。
api.py 会把收到的请求以 JSON 映射出来。用 curl -s 调用两个端口,并用 >> 汇总到一个文件。
按路径分版本转发
通过 /root/eaimw/gw/nginx.conf,在 8090 上把 /v1/→9601、/v2/→9602 按原路径转发。
两个 upstream 块和两个 location。proxy_pass 不加 URI 部分,请求路径就会原样传过去。
用 API 密钥识别消费者
用引入了 /root/eaimw/gw/keys.map 的 map 来确定消费者,没有则返回 401 JSON,有则只传递 X-Consumer,并去掉密钥。
map $http_x_api_key $consumer { default ""; include …; } 之后,在 server 中 if ($consumer = "") { return 401 '…'; }。要去掉请求头,就给 proxy_set_header 一个空值。
为每个消费者设置配额
用 limit_req_zone $consumer rate=5r/s、burst=5 nodelay、limit_req_status 429,按消费者设置配额。
关键全在于 zone 的键是什么。超出限额的默认状态码是 503,必须改成 429,调用方才知道“是我发得太多了”。
用请求头选择版本,并告知即将下线的版本
/api/ 在 X-API-Version: 2 时转给 v2,没有则转给 v1。给 v1 响应加上 Sunset 头。
用 map $http_x_api_version $api_ver 选择上游名称,再 proxy_pass http://$api_ver; 即可。Sunset 也用 map,只在 v1 时给出值,值为空的 add_header 就不会附加。
串联请求 ID 并记入日志
如果有 X-Request-ID 就原样传递,没有则传递 $request_id,并以 '<请求 ID> <消费者> <状态> "<请求行>"' 的格式留下访问日志。
像 map $http_x_request_id $rid { "" $request_id; default $http_x_request_id; } 这样,map 的值中可以使用变量。定义 log_format,并把它的名称用在 access_log 上。
缓慢的上游要尽快断开
用 proxy_read_timeout 2s,在 2 秒左右以 504 断开 /v1/slow(5 秒)。
默认值是 60 秒。如果是调用方先放弃的 API,网关就没有理由抓着更久。请在每个 location 中统一加入。