楼层: 首页/ 软件技术/ 中间件全景/ API 网关:Kong 与 APISIX
16

API 网关:Kong 与 APISIX

API Gateway · Kong / APISIX / Rate Limiting / Auth

如果说服务网格管的是"服务之间怎么说话"(东西向流量),那 API 网关管的是"外面的人怎么进来"(南北向流量)。它是所有外部请求踏入你系统的第一道门,也常常是最后一道——认证、限流、路由、灰度、协议转换、审计日志,全在这一层做。这一章讲两个最主流的选择:Kong(生态最成熟)和 APISIX(国内团队主导、配置热更新最快),外加限流和鉴权的具体实现。

15.1 网关到底该干什么:先划清职责边界

客户端
App / 小程序 / Web / 第三方
→
CDN + WAF
静态加速、拦攻击(第 7 章)
→
API 网关
路由 / 鉴权 / 限流 / 灰度 / 日志
→
业务服务
订单 / 用户 / 支付(东西向走服务网格)
网关的职责清单(打勾的进网关,打叉的别塞进来)
能力说明
路由 Routing把 /api/order/** 转到订单服务,支持按路径、Host、Header、Method、Query 匹配。这是最基本的能力。
认证鉴权 AuthN校验 JWT 签名、API Key、OAuth2 / OIDC,把解析出的用户身份透传给下游(通常是 header)。注意:网关做的是"认证",业务授权(这个用户能不能改这个订单)必须在服务里做。
限流 Rate Limiting按 IP / 用户 / API / 租户限流,保护后端。单机限流 + 分布式限流两种,见 15.5。
灰度与流量染色按 header、用户 ID、百分比把流量导向新版本——和 Istio 的金丝雀是同一类需求,只是发生在入口。
协议转换外部 REST/JSON ↔ 内部 gRPC、WebSocket 代理、HTTP/2。让内部服务不必迁就外部协议。
请求/响应改写加/删 header、重写路径、统一包装响应格式({code,msg,data})、给下游带 trace id。
可观测与审计统一的访问日志、耗时统计、错误率;对外 API 的调用审计(谁在什么时候调了哪个接口)。这是网关最高价值的能力之一。
不该做的业务逻辑(订单状态机、金额计算)、复杂的数据聚合、长耗时的处理。网关是"薄"的一层,塞业务进去会让它变成瓶颈和发布风险源。

论有了 Nginx / Ingress,为什么还要 API 网关

Nginx 是极其优秀的反向代理,做静态托管、简单负载均衡、TLS 终止,它几乎是最好的选择。但它的短板在于"动态"和"可编程":

① 配置更新要 reload。Nginx 改配置需要 reload(虽然平滑,但频率高了仍有代价),在"每天上百次路由变更"的微服务环境里不够顺滑。

② 没有"API 生命周期"的概念。Nginx 只知道 server/location;网关知道"API、消费者、订阅、插件、配额",能回答"哪个应用调了哪些 API、用了多少配额"。

③ 鉴权/限流/配额不是一等公民。Nginx 也能用 Lua(OpenResty)写,但那基本等于自己造一个网关。Kong 和 APISIX 本质就是"用 OpenResty 造好的、带插件生态的网关"。

结论:K8s 内部简单暴露服务用 Ingress(或 Gwy API)就够了;一旦需要面向外部提供 API(认证、配额、计费、审计、多租户),就该上 API 网关。两者不冲突,常常是"网关在最外层 + Ingress 在里面"的叠加结构。

15.2 Kong:插件生态最成熟的网关

Kong 基于 Nginx + OpenResty(Lua),2024 年起把核心逐步迁移到 Rust 写的 OpenResty 替代品上(版本以官网最新稳定版为准),但对象模型和用法一直很稳定。它的核心是五个对象,理解这五个就理解了 Kong。

Kong 的核心对象模型
对象含义
Service对上游服务的抽象(上游地址、协议、超时)。可以理解成"我要代理的那个后端"。
Route匹配规则:什么样的请求(路径/Host/方法/Header)交给哪个 Service。
Upstream + Target上游的负载均衡池:多个 Target(真实实例 IP:Port),Upstream 负责健康检查和均衡策略。
Consumer调用方身份(一个应用/一个租户/一个开发者),用来挂配额和鉴权凭据。
Plugin插件,可以挂在全局 / Service / Route / Consumer 四个层级上,粒度从粗到细。这是 Kong 的灵魂。

用 Admin API 建一条完整的 API(生产建议用声明式配置 + decK / GitOps,见下)

# 1) 建 Service:指向后端 curl -s -X POST http://kong:8001/services \ --data 'name=order-service' \ --data 'url=http://order-service.shop.svc.cluster.local:8080' \ --data 'connect_timeout=2000' \ --data 'read_timeout=5000' \ --data 'write_timeout=5000' # 2) 建 Route:什么请求进这个 Service curl -s -X POST http://kong:8001/services/order-service/routes \ --data 'name=order-api' \ --data 'paths[]=/api/order' \ --data 'strip_path=true' \ --data 'methods[]=GET' --data 'methods[]=POST' \ --data 'hosts[]=api.example.com' # 3) 挂插件:给这条 Route 加"每秒 100 次、突发 200"的限流 curl -s -X POST http://kong:8001/routes/order-api/plugins \ --data 'name=rate-limiting' \ --data 'config.second=100' \ --data 'config.fault_tolerant=true' \ --data 'config.policy=redis' \ --data 'config.redis_host=redis.shop' \ --data 'config.redis_port=6379' # 4) 建 Consumer + 凭据(给某个第三方应用发一个 API Key) curl -s -X POST http://kong:8001/consumers --data 'username=partner-a' curl -s -X POST http://kong:8001/consumers/partner-a/key-auth --data 'key=ak_live_9f3a2b71c8e4' # 5) 校验 JWT(挂在 Service 上,consumer 端用 JWT 认证) curl -s -X POST http://kong:8001/services/order-service/plugins \ --data 'name=jwt' \ --data 'config.claims_to_verify=exp' \ --data 'config.header_names=authorization'

更好的做法:声明式配置 + GitOps(配置进 Git,变更走评审)

# kong.yaml:整份配置声明在一个文件里,用 decK 同步到网关 _format_version: "3.0" services: - name: order-service url: http://order-service.shop.svc.cluster.local:8080 routes: - name: order-api paths: [/api/order] strip_path: true plugins: - name: rate-limiting config: { second: 100, policy: redis, redis_host: redis.shop } - name: jwt config: { claims_to_verify: [exp] } upstreams: - name: order-pool healthchecks: active: http_path: /healthz healthy: { interval: 5, successes: 2 } unhealthy: { interval: 5, http_failures: 3 } targets: - target: 10.0.1.11:8080 weight: 100 # 同步(先 diff 再 apply,变更可控,能进 CI) deck gateway diff kong.yaml # 看这次会改什么 deck gateway sync kong.yaml # 真正下发 # DB-less 模式:配置全在文件里,不需要数据库(更适合 GitOps 和边缘部署) # KONG_DATABASE=off ; KONG_DECLARATIVE_CONFIG=/etc/kong/kong.yaml

15.3 APISIX:配置热更新最快的网关

APISIX 同样基于 OpenResty,但架构上有一个关键差别:它把配置放在 etcd 里,而不是数据库 + 定时同步。所有节点 watch etcd,配置变更毫秒级生效,无需 reload。这是它在国内云原生圈流行的核心原因之一。(版本以官网最新稳定版为准;插件数量已超过 100 个,并支持 Wasm/Java/Python 等多语言插件。)

论为什么"etcd + 全内存路由树"能带来毫秒级生效

① 配置存在 etcd 里,而不是网关自己的数据库。etcd 天然支持 watch(长连接监听变化)和强一致,多个网关节点订阅同一份配置,不会出现各节点配置不一致。

② 路由匹配用 radix tree(基数树),并且整棵树常驻内存。配置一变,只需在内存里重建树(毫秒级),没有磁盘 IO,也不需要重启 worker。

③ 对比 Kong 传统模式(配置在 Postgres/Cassandra,节点定期轮询数据库同步,默认几分钟),APISIX 的变更实时性明显更好。Kong 现在也可以用 DB-less + declarative config 达到近似效果,但那份配置需要你自己推送到每个节点。

代价:APISIX 强依赖 etcd。etcd 挂了,网关还能继续用已有配置转发流量,但你改不了配置(新上线、限流调整都做不了)。所以 etcd 必须三节点集群部署,这是上 APISIX 时必须一并规划的事。

APISIX 的核心配置:Upstream、Route、插件(Admin API 写法)

# 1) 建上游:定义后端实例与健康检查 curl -s http://apisix-admin:9180/apisix/admin/upstreams/1 \ -H "X-API-KEY: $ADMIN_KEY" -X PUT -d '{ "type": "roundrobin", "nodes": { "10.0.1.11:8080": 1, "10.0.1.12:8080": 1 }, "checks": { "active": { "type": "http", "http_path": "/healthz", "healthy": { "interval": 5, "successes": 2 }, "unhealthy": { "interval": 5, "http_failures": 3 } } } }' # 2) 建路由:路径匹配 + 挂上游 + 挂插件 curl -s http://apisix-admin:9180/apisix/admin/routes/order-api \ -H "X-API-KEY: $ADMIN_KEY" -X PUT -d '{ "uri": "/api/order/*", "methods": ["GET", "POST"], "host": "api.example.com", "plugins": { "limit-req": { "rate": 100, "burst": 200, "rejected_code": 429, "key": "remote_addr", "key_type": "var" }, "jwt-auth": { "header": "authorization" }, "proxy-rewrite": { "regex_uri": ["^/api/order/(.*)", "/$1"] }, "prometheus": {} }, "upstream_id": "1" }' # 3) 灰度:给路由挂 traffic-split,按 header 与权重分流 curl -s http://apisix-admin:9180/apisix/admin/routes/order-api \ -H "X-API-KEY: $ADMIN_KEY" -X PATCH -d '{ "plugins": { "traffic-split": { "rules": [{ "weighted_upstreams": [ { "upstream_id": "1", "weight": 90 }, { "upstream_id": "2", "weight": 10 } ] }] } } }' # 4) 配置确实存在 etcd 里(排障时可以直接看) etcdctl get /apisix/routes/order-api --prefix
APISIX / Kong 上线最容易翻车的五件事

① 网关自己成了单点。所有流量都过网关,网关一挂全站不可用。必须多实例 + 前面挂 SLB/Keepalived + 健康检查,并且网关实例要能随时被替换(无状态,配置来自 etcd / 声明式文件)。

② 配置下发"以为生效了"。Admin API 返回 200 只代表"配置写入成功",不代表"这个节点的 worker 已生效"。验证方法:用业务请求打一遍,或用 prometheus 指标 / 日志确认命中了新路由。Kong 用 DB 模式时还要注意节点同步间隔。

③ 插件里写了阻塞操作。网关是所有请求的必经之路,插件里一个 sleep、一次同步 HTTP 调用、一次慢查询,会把整个网关的吞吐拖垮。自定义插件里禁止同步阻塞,必须用 cosocket 做异步 IO。

④ 限流键选错了。只按 remote_addr 限流,在 NAT 出口或 CDN 后面等于把一整个公司/城市当成一个用户限。要按真实用户(JWT 里的 sub / UID),并让 CDN 传真实 IP 头(X-Forwarded-For),同时注意这个头必须由可信代理设置,否则能被伪造。

⑤ 请求体大小和超时没限制。攻击者上传一个 2GB 的 body,或者开一堆慢连接,网关的连接池和内存会被吃光(Slowloris)。必须设置 client_max_body_size、client_body_timeout、总超时,并开启连接数限制。

15.4 Kong 与 APISIX 怎么选

主流网关横向对比
维度KongAPISIXNginx / OpenResty云厂商网关
底座OpenResty(Lua)为主,部分组件 RustOpenResty(Lua)+ etcd纯 Nginx + 自写 Lua托管服务
配置存储Postgres / Cassandra / 声明式文件(DB-less)etcd(watch 实时同步)配置文件(reload 生效)控制台 / OpenAPI
配置生效速度DB 模式秒到分钟级;DB-less 需推送毫秒级(无需 reload)reload 生效(平滑但有开销)秒级
插件生态最成熟,商业插件丰富,文档/社区大数量多(100+),国内文档友好,支持多语言插件等于自己开发看厂商
运维复杂度中(要养 DB 或做 DB-less)中(要养 etcd 集群)低(但功能要自己写)最低(但贵、可定制性差)
适合谁需要成熟商业支持、插件市场、企业级 API 管理K8s 环境、要求配置实时生效、国内团队简单反向代理、静态+少量动态不想运维、预算充足、需求标准

选三条判断

① 已经是 K8s 原生环境、追求配置秒级生效 → APISIX(它和 K8s Ingress/Gateway API 的集成很自然,etcd 本来就是 K8s 的底座)。

② 需要成熟的插件市场、商业支持、复杂的 API 生命周期管理(开发者门户、订阅、计费)→ Kong。它的企业版在这方面做得最深。

③ 需求真的只是"转发 + 一点点重写" → 别上网关,Nginx 或 K8s Gateway API 就够。网关的价值来自"我要对 API 做治理",没有这个需求它就是多一个要维护的组件。

15.5 限流:四种算法和一次正确的落地

限流算法对比(面试高频,也是选型依据)
算法原理优点缺点
固定窗口计数每个时间窗口一个计数器,超过阈值拒绝,窗口结束清零最简单、内存最小(一个计数)临界问题:窗口交界处可能放过接近 2 倍的请求(59 秒来 100 个 + 61 秒来 100 个)
滑动窗口把窗口切成小格(如 1 秒 10 格),统计最近 N 格的请求数平滑、精度可控实现稍复杂,需要存多格计数
漏桶 Leaky Bucket请求先进桶,以恒定速率流出,桶满则拒绝输出速率绝对平滑,适合保护下游无法应对合理突发(明明有余量也不让过)
令牌桶 Token Bucket以固定速率往桶里放令牌,桶容量=允许的突发量,有令牌才能过既能限平均速率,又允许突发,最常用需要维护令牌数(有状态)

分布式限流的正确姿势:Redis + Lua 令牌桶(原子、跨节点一致)

-- KEYS[1]=限流 key(如 rate:uid:1001) -- ARGV[1]=速率(个/秒) ARGV[2]=桶容量(突发上限) ARGV[3]=当前时间戳(毫秒) ARGV[4]=本次请求令牌数 local key = KEYS[1] local rate = tonumber(ARGV[1]) local capacity = tonumber(ARGV[2]) local now = tonumber(ARGV[3]) local need = tonumber(ARGV[4]) -- 取出上次的状态(HGETALL 拿到 tokens 和 last_refill) local data = redis.call('HMGET', key, 'tokens', 'ts') local tokens = tonumber(data[1]) or capacity local last = tonumber(data[2]) or now -- 按流逝的时间补充令牌,最多补到桶容量 local delta = math.max(0, now - last) / 1000 tokens = math.min(capacity, tokens + delta * rate) local allowed = 0 if tokens >= need then tokens = tokens - need allowed = 1 end redis.call('HSET', key, 'tokens', tokens, 'ts', now) redis.call('EXPIRE', key, math.ceil(capacity / rate) + 1) -- 不活跃的 key 自动过期,防止内存膨胀 return allowed
# 业务侧:拿到 0 就返回 429,并带上重试提示(对客户端友好,也减少无效重试) Long allowed = redis.eval(TOKEN_BUCKET_LUA, singletonList("rate:uid:1001"), rate, capacity, System.currentTimeMillis(), 1); if (allowed == 0) { response.setStatus(429); response.setHeader("Retry-After", "1"); throw new TooManyRequestsException(); }
# 网关侧直接配置更省事: # APISIX(按 URI + 远程地址,100/秒,突发 200) # limit-req: { rate: 100, burst: 200, key: remote_addr } # limit-count: { count: 10000, time_window: 3600, key: consumer_name } ← 按小时配额 # limit-conn: { conn: 50, burst: 20, default_conn_delay: 0.1 } ← 并发连接数限制 # Kong: # rate-limiting: { second: 100, policy: redis, redis_host: redis.shop } # request-termination: { status_code: 503, message: "服务维护中" } ← 一键熔断某个 API
限流的五个常见错误

① 单机限流当成分布式限流。网关有 5 个实例,每个都配"100/秒",实际放过去的是 500/秒。分布式限流必须把计数放到 Redis/etcd,或者用"总配额 ÷ 实例数"的静态分配(简单但会被流量倾斜打偏)。

② 限流维度过粗。只有"全局限流"的话,一个大客户就能把配额吃光;只有"IP 限流"的话,NAT 后面的用户互相伤害。实践是分层限流:全局 → 租户/用户 → 单接口 → 单 IP。

③ 只限流量,不限并发。限流(QPS)保护的是吞吐,并发连接数限制保护的是连接资源。慢请求攻击(Slowloris)QPS 很低但会占满连接,必须配 limit-conn / 网关的并发上限。

④ Redis 挂了限流就"全放行"。要明确降级策略:放行(保护可用性,但可能压垮后端)还是拒绝(保护后端,但影响所有用户)?大多数业务选择"放行 + 告警",但核心接口宁愿拒绝。这个决定必须提前做,不能临场拍脑袋。

⑤ 通过 X-Forwarded-For 取客户端 IP 时不做校验。这个头是客户端可以伪造的,攻击者每次换一个值就能绕过基于 IP 的限流。必须只信任你前面的代理设置的最后一跳地址(Nginx 的 set_real_ip_from / 网关的 trusted proxies 配置)。

15.6 鉴权:网关该做到哪一步

常见的四种鉴权方式
方式怎么做适合注意
API Key调用方在 header 里带固定 key,网关查库/缓存校验内部服务、简单的第三方接入key 泄漏即失守,要能随时吊销;不要放在 URL 里(会进日志、被浏览器历史记录)
JWT 校验网关用公钥(JWKS)验签,解析出 sub/roles 并通过 header 透传给下游用户态 API,前后端分离的标准做法JWT 一旦签发就无法主动失效(除非维护黑名单)。所以 TTL 要短 + 配 refresh token;敏感操作要在服务侧再校验一次
OAuth2 / OIDC走标准授权码流程,网关校验 access token(通常也是 JWT)面向第三方开放平台、SSO要处理 token 的签发、刷新、撤销;scope 到具体接口的映射要清晰
HMAC 签名用密钥 + 时间戳 + 请求体算签名,网关验签并校验时间窗口开放 API(支付回调、金融接口)必须带时间戳 + nonce 防重放,否则抓到报文就能无限重放。密钥分发和轮换要有机制

一条完整的"网关认证 + 身份透传"链路(这是最实用的模式)

# 网关侧:APISIX 的 jwt-auth 插件(Kong 用 jwt 插件,思路一致) # 1) 建一个 consumer(代表一个调用方) curl -s http://apisix-admin:9180/apisix/admin/consumers \ -H "X-API-KEY: $ADMIN_KEY" -X PUT -d '{ "username": "web-app", "plugins": { "jwt-auth": { "key": "user-key-001", "secret": "a-very-long-random-secret", "algorithm": "HS256", "exp": 7200 } } }' # 2) 路由上挂 jwt-auth(不校验的接口另建路由不加插件,比如 /login、/public) # 3) 下游拿到的身份信息:网关会把 JWT payload 写进 header,应用只读 header,不重复验签 # X-User-Id: 1001 # X-User-Roles: user,vip # 应用侧(伪代码) public ResponseEntity<Order> getOrder(@RequestHeader("X-User-Id") long userId, @PathVariable long orderId) { Order o = orderService.find(orderId); // ★ 关键:网关只证明了"你是谁",这里必须自己判断"你能不能看" if (o.getUserId() != userId) throw new ForbiddenException(); return ResponseEntity.ok(o); }
鉴权最容易出的两个"致命"错

① 把"网关认证"当成"业务授权"。网关确认了"这是用户 1001 的合法 token",但"用户 1001 能不能看订单 9999"必须由业务服务判断。只做网关鉴权的系统,只要把 URL 里的订单号改一下就能看到别人的订单——这就是最典型的越权漏洞(IDOR)。网关解决"身份",服务解决"权限",两者缺一不可。

② 信任下游可以直接伪造的 header。如果网关通过 X-User-Id 传身份,那么任何人都能自己造一个 X-User-Id: 1001 打到服务上——除非你保证"服务只能被网关访问"(网络隔离 + 只监听内网 + 服务网格里校验来源身份)。正确做法:网关在校验后覆写这些 header(而不是追加),并在服务侧把"直接暴露公网"这条路径彻底堵死。

记
本章小结

① API 网关管南北向(外部怎么进来),服务网格管东西向(服务之间怎么说话)。职责不同,常同时存在。

② 网关该做:路由、认证、限流、灰度、协议转换、改写、审计;不该做:业务逻辑、复杂聚合、长耗时操作。

③ Kong 生态最成熟,对象模型清晰(Service/Route/Upstream/Consumer/Plugin),适合企业级 API 管理;APISIX 用 etcd 做配置中心,配置毫秒级生效,适合 K8s 环境,但 etcd 必须三节点高可用。

④ 限流选令牌桶(限平均速率又能应对突发);分布式限流必须把计数放 Redis/etcd,单机限流 × N 个实例 = 实际放行 N 倍。别忘了同时限并发。

⑤ 鉴权在网关,授权在服务。网关只回答"你是谁","你能不能做这件事"必须由业务判断,否则就是越权漏洞。

⑥ 上线前必须演练:网关多实例、配置变更可验证、限流降级策略已定、请求体大小与超时已设。这四件事没做,网关就是全站最大的风险点。