关于API 网关的几点记录
这次做 API 网关改造,从 Nginx 到 Kong,再到 Istio Gateway,。
项目最开始是单体架构,一个 nginx.conf 就能搞定所有请求路由。
为什么需要 API 网关
项目最开始是单体架构,一个 nginx.conf 就能搞定所有请求路由。后来拆成微服务,服务数量从 3 个涨到 20 个,问题就来了:
- 每个服务都要单独实现鉴权,代码重复
- 某个服务被恶意刷接口,没法统一限流
- 调用链路太长,出问题找不到是哪个服务挂了
- 新旧服务共存,需要做灰度发布
不是没想过在服务层解决,但维护成本太高。最后还是决定在统一入口做这件事。
第一阶段:Nginx 反向代理
最初的方案很简单,用 Nginx 做反向代理:
upstream user_service {
server 10.0.1.10:8080;
server 10.0.1.11:8080;
}
upstream order_service {
server 10.0.1.20:8080;
server 10.0.1.21:8080;
}
server {
listen 80;
server_name api.example.com;
location /user/ {
proxy_pass http://user_service;
proxy_set_header Host $host;
}
location /order/ {
proxy_pass http://order_service;
proxy_set_header Host $host;
}
}
做了基本的负载均衡和服务路由,算是能用。但很快发现不够用:
坑一:鉴权逻辑散落
每个服务都要自己验证用户身份,要么重复实现 JWT 验证,要么在 Nginx 层统一处理。选了后者,用 OpenResty + Lua:
location /api/ {
access_by_lua_block {
local cjson = require "cjson"
local http = require "resty.http"
local auth_header = ngx.var.http_authorization
if not auth_header then
ngx.status = 401
ngx.say("Missing authorization header")
ngx.exit(401)
end
-- 调用认证服务验证 token
local httpc = http.new()
local res, err = httpc:request_uri("http://auth-service:8080/validate", {
method = "POST",
body = cjson.encode({token = auth_header}),
headers = {
["Content-Type"] = "application/json"
}
})
if not res or res.status ~= 200 then
ngx.status = 401
ngx.say("Invalid token")
ngx.exit(401)
end
-- 把用户信息塞到 header 里传给后端
local user_info = cjson.decode(res.body)
ngx.req.set_header("X-User-Id", user_info.user_id)
ngx.req.set_header("X-User-Role", user_info.role)
}
proxy_pass http://upstream_service;
}
能用,但问题来了:
- Lua 代码不好维护,出了问题不好调试
- 每次请求都要调用一次认证服务,性能损耗明显
- Redis 连接池管理麻烦,经常超时
- 新人看不懂这套 Lua 逻辑
坑二:限流不好做
Nginx 自带的 limit_req 只能做简单的限流:
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
location /api/ {
limit_req zone=api_limit burst=20 nodelay;
proxy_pass http://upstream_service;
}
这个限制是全局的,没法针对不同 API 或不同用户做差异限流。而且限流策略改了要 reload Nginx,影响所有流量。
折腾了几个月,感觉 Nginx 承载不了这些需求,决定换专业的 API 网关。
第二阶段:Kong
选 Kong 主要是因为基于 OpenResty,但把 Lua 逻辑封装成了插件,配置和管理都更方便。
安装和配置
用 Docker 部署:
docker network create kong-net
docker run -d --name kong-database \
--network=kong-net \
-p 5432:5432 \
-e "POSTGRES_USER=kong" \
-e "POSTGRES_DB=kong" \
-e "POSTGRES_PASSWORD=kong" \
postgres:13
docker run --rm \
--network=kong-net \
-e "KONG_DATABASE=postgres" \
-e "KONG_PG_HOST=kong-database" \
-e "KONG_PG_USER=kong" \
-e "KONG_PG_PASSWORD=kong" \
-e "KONG_PASSWORD=kong" \
kong/kong-gateway:2.8 kong migrations bootstrap
docker run -d --name kong \
--network=kong-net \
-p 8000:8000 \
-p 8443:8443 \
-p 8001:8001 \
-p 8444:8444 \
-e "KONG_DATABASE=postgres" \
-e "KONG_PG_HOST=kong-database" \
-e "KONG_PG_USER=kong" \
-e "KONG_PG_PASSWORD=kong" \
-e "KONG_PROXY_ACCESS_LOG=/dev/stdout" \
kong/kong-gateway:2.8
服务和路由配置
用 Admin API 创建服务和路由:
# 创建服务
curl -X POST http://localhost:8001/services \
-d "name=user-service" \
-d "url=http://user-service:8080"
# 创建路由
curl -X POST http://localhost:8001/services/user-service/routes \
-d "paths[]=/user" \
-d "strip_path=true"
Kong 的优势在于插件系统。比如限流:
# 启用限流插件
curl -X POST http://localhost:8001/services/user-service/plugins \
-d "name=rate-limiting" \
-d "config.minute=100" \
-d "config.policy=local"
或者针对不同用户级别做差异化限流:
# 高级用户限流宽松
curl -X POST http://localhost:8001/services/user-service/plugins \
-d "name=rate-limiting" \
-d "config.minute=500" \
-d "config.policy=redis" \
-d "config.redis_host=redis" \
-d "config.redis_port=6379"
# 普通用户限流严格
curl -X POST http://localhost:8001/services/user-service/plugins \
-d "name=key-auth" \
-d "config.key_names=apikey"
JWT 认证
Kong 原生支持 JWT:
# 启用 JWT 插件
curl -X POST http://localhost:8001/plugins \
-d "name=jwt" \
-d "config.secret_is_base64=false"
# 创建消费者
curl -X POST http://localhost:8001/consumers \
-d "username=test-user"
# 为消费者创建 JWT credential
curl -X POST http://localhost:8001/consumers/test-user/jwt \
-d "algorithm=HS256" \
-d "key=my-api-key" \
-d "secret=my-api-secret"
客户端请求时带上 JWT token:
curl -X GET http://localhost:8000/user/profile \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
Kong 会自动验证 JWT,通过后才会转发到后端服务。
踩过的坑
Kong 用起来比 Nginx 方便,但也不是没有坑。
坑一:插件配置复杂度
插件多了之后,配置管理很头疼。比如同一个服务既有 JWT 认证,又有限流,还有请求/响应转换,插件的执行顺序容易搞错。最后写了脚本统一管理,但维护成本还是高。
坑二:集群部署麻烦
Kong 的插件配置存储在数据库里,多节点部署时要确保配置同步。遇到主从延迟或者数据库挂了,所有节点都受影响。
坑三:灰度发布不友好
Kong 能做金丝雀发布,但配置很复杂:
# 创建主路由
curl -X POST http://localhost:8001/services/user-service/routes \
-d "paths[]=/user" \
-d "strip_path=true"
# 创建灰度路由(新版本)
curl -X POST http://localhost:8001/services/user-service/routes \
-d "paths[]=/user" \
-d "strip_path=true" \
-d "hosts[]=canary.example.com"
如果要按流量百分比灰度,还得用 A/B 测试插件,或者改 header 规则。这套逻辑维护起来很累。
坑四:和 Kubernetes 集成不够顺
项目开始上 Kubernetes,用了 Kong Ingress Controller,但很多配置还是得通过 Admin API,不能直接在 Ingress YAML 里声明。团队熟悉 Kubernetes 的,更希望一切都用 Kubernetes 原生的方式管理。
第三阶段:Istio Gateway
项目规模扩大后,服务通信变得更复杂。除了南北向流量(外部请求进来),东西向流量(服务间调用)也需要管理。Istio 这个时候就顺理成章地进来了。
为什么选 Istio
不是没有考虑过其他方案,比如 Spring Cloud Gateway、Envoy 直接用,或者继续优化 Kong。但最终选 Istio 主要考虑:
- 统一管理南北向和东西向流量:Kong 只能管外部流量,服务间调用需要另外一套方案
- Kubernetes 原生支持:所有配置都是 CRD,和 Kubernetes 完美集成
- 可观测性开箱即用:Metrics、Tracing、Logging 都能自动采集
- 灰度发布和流量镜像:支持按百分比、header、权重等多种灰度策略
安装 Istio
# 下载 Istio
curl -L https://istio.io/downloadIstio | sh -
cd istio-1.18.0
# 安装
istioctl install --set profile=demo
# 自动注入 sidecar
kubectl label namespace default istio-injection=enabled
Gateway 配置
Istio 的 Gateway 定义了入口点:
apiVersion: networking.istio.io/v1beta1
kind: Gateway
metadata:
name: api-gateway
namespace: istio-system
spec:
selector:
istio: ingressgateway
servers:
- port:
number: 80
name: http
protocol: HTTP
hosts:
- "api.example.com"
- port:
number: 443
name: https
protocol: HTTPS
tls:
mode: SIMPLE
credentialName: api-cert
hosts:
- "api.example.com"
VirtualService 定义路由规则:
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: user-service
spec:
hosts:
- "api.example.com"
gateways:
- api-gateway
http:
- match:
- uri:
prefix: "/user"
route:
- destination:
host: user-service
port:
number: 8080
timeout: 5s
retries:
attempts: 3
perTryTimeout: 2s
跨命名空间的请求:
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: user-service
spec:
hosts:
- "api.example.com"
gateways:
- api-gateway
http:
- match:
- uri:
prefix: "/user"
route:
- destination:
host: user-service.orders-namespace.svc.cluster.local
port:
number: 8080
鉴权和授权
Istio 用 RequestAuthentication 做 JWT 认证:
apiVersion: security.istio.io/v1beta1
kind: RequestAuthentication
metadata:
name: jwt-auth
spec:
selector:
matchLabels:
app: user-service
jwtRules:
- issuer: "https://auth.example.com"
jwksUri: "https://auth.example.com/.well-known/jwks.json"
audiences:
- "api.example.com"
用 AuthorizationPolicy 做授权:
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: allow-admin
spec:
selector:
matchLabels:
app: user-service
action: ALLOW
rules:
- from:
- source:
requestPrincipals: ["*"]
when:
- key: request.auth.claims[role]
values: ["admin"]
限流
Istio 原生限流能力有限,但可以用 Envoyfilter 实现精细控制:
apiVersion: networking.istio.io/v1beta1
kind: EnvoyFilter
metadata:
name: rate-limit
spec:
workloadSelector:
labels:
app: ingressgateway
configPatches:
- applyTo: HTTP_FILTER
match:
context: GATEWAY
listener:
filterChain:
filter:
name: envoy.filters.network.http_connection_manager
subFilter:
name: envoy.filters.http.router
patch:
operation: INSERT_BEFORE
value:
name: envoy.filters.http.local_ratelimit
typed_config:
"@type": type.googleapis.com/udpa.type.v1.TypedStruct
type_url: type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit
value:
filter_enabled:
runtime_key: local_rate_limit_enabled
default_value:
numerator: 100
denominator: HUNDRED
token_bucket:
max_tokens: 10
tokens_per_fill: 10
fill_interval: 1s
filter_enforced:
runtime_key: local_rate_limit_enforced
default_value:
numerator: 100
denominator: HUNDRED
response_headers_to_add:
- append: false
header:
key: x-local-rate-limit
value: 'true'
但 Envoyfilter 配置很复杂,最后还是用了 Redis-based 方案:
apiVersion: networking.istio.io/v1beta1
kind: EnvoyFilter
metadata:
name: redis-rate-limit
spec:
workloadSelector:
labels:
app: ingressgateway
configPatches:
- applyTo: HTTP_FILTER
match:
context: GATEWAY
listener:
filterChain:
filter:
name: envoy.filters.network.http_connection_manager
patch:
operation: INSERT_BEFORE
value:
name: envoy.filters.http.ratelimit
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.ratelimit.v3.RateLimit
domain: "api-ratelimit"
rate_limit_service:
grpc_service:
envoy_grpc:
cluster_name: rate_limit_cluster
transport_api_version: V3
灰度发布
Istio 的灰度发布很直观,用权重或 header:
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: user-service
spec:
hosts:
- "api.example.com"
gateways:
- api-gateway
http:
- match:
- headers:
x-canary:
exact: "true"
route:
- destination:
host: user-service
subset: v2
weight: 100
- route:
- destination:
host: user-service
subset: v1
weight: 90
- destination:
host: user-service
subset: v2
weight: 10
用流量镜像做 A/B 测试:
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: user-service
spec:
hosts:
- "api.example.com"
gateways:
- api-gateway
http:
- route:
- destination:
host: user-service
subset: v1
mirror:
host: user-service
subset: v2
mirrorPercentage:
value: 10
迁移过程中的坑
从 Kong 迁到 Istio,中间也踩了不少坑。
坑一:服务发现机制变了
Kong 的 Service 配置用 upstream name 或者 DNS,Istio 用 Kubernetes Service。有些老服务还用的手动 IP,迁移时得先改 Kubernetes Service:
apiVersion: v1
kind: Service
metadata:
name: user-service
spec:
selector:
app: user-service
ports:
- port: 8080
targetPort: 8080
坑二:TLS 证书管理
Kong 用自带的证书存储,Istio 用 Kubernetes Secret:
# 创建 TLS secret
kubectl create secret tls api-cert \
--cert=path/to/cert.crt \
--key=path/to/cert.key \
-n istio-system
坑三:Prometheus 指标不一样
Kong 的指标是 kong_*,Istio 是 istio_*,监控大盘要重新做。而且 Istio 的指标更细粒度,一开始不知道看哪些,浪费了不少时间。
坑四:运维团队不适应
Kong 的运维主要改配置文件,Istio 要理解控制面和数据面的概念。Sidecar 的注入、版本兼容性、升级策略,这些都是新东西。前两周出了好几次 Sidecar 没注入导致服务调不通的问题。
性能对比
不同阶段的性能数据(基于 QPS 基准测试):
| 阶段 | 基准 QPS | P95 延迟 | P99 延迟 |
|---|---|---|---|
| Nginx + OpenResty | 8,000 | 25ms | 45ms |
| Kong | 6,500 | 32ms | 58ms |
| Istio Gateway | 5,500 | 38ms | 72ms |
Istio 的延迟确实高一些,但考虑到功能完整性,这个 trade-off 是可以接受的。而且实际生产环境的 QPS 远没到瓶颈,瓶颈通常在后端服务。
架构演进的一点思考
回顾这次 API 网关的演进,有几个心得:
- 没有银弹:Nginx、Kong、Istio 各有适用场景,不是哪个更高级就用哪个
- 成本要算总账:维护成本、学习成本、迁移成本,都是实际开销
- 不要过度设计:如果 Nginx 能满足需求,没必要硬上 Istio
- 团队能力要匹配:新工具能带来新能力,但团队要是跟不上,就是负担
技术选型不是追新,而是解决问题。Nginx 满足不了鉴权和限流的需求,才上了 Kong。Kong 满足不了服务网格和灰度发布的需求,才上了 Istio。每一步都是问题驱动,不是跟风。
写在最后
这次 API 网关改造花了一个月,从 Nginx 到 Kong,再到 Istio Gateway。改造完成后,鉴权、限流这些功能统一管理,维护成本降低了 40%。
解决了:
- 统一鉴权,不用每个服务重复实现
- 差异化限流,能针对不同 API 和用户做策略
- 灰度发布和流量镜像,发布更平滑
- 可观测性提升,Metrics、Tracing 都能自动采集
带来了:
- 学习成本,团队要理解 Istio 的概念
- 性能损耗,延迟比 Nginx 高一些
- 运维复杂度,要管理控制面和数据面
如果你的项目还在用 Nginx 做 API 网关,而且已经感受到局限性,可以考虑升级。但升级之前先想清楚:到底要解决什么问题?团队能力能不能跟上?迁移成本能不能接受?
毕竟,工具是为业务服务的,不是为了炫技。
这次 API 网关改造花了一个月,从 Nginx 到 Kong,再到 Istio Gateway。改造完成后,鉴权、限流这些功能统一管理,维护成本降低了 40%。
可用性说明:本文发布于 2021 年 3 月,距今已超过五年。文中涉及的软件版本、接口、下载地址、命令参数和操作界面可能已经发生变化,部分方案在当前环境下可能失效。请结合官方最新文档核对后再操作,生产环境使用前务必先行验证。
版权声明: 本文首发于 指尖魔法屋-关于API 网关的几点记录(https://blog.thinkmoon.cn/post/110-api-gateway-nginx-istio-architecture-evolution/) 转载或引用必须申明原指尖魔法屋来源及源地址!
评论
使用 GitHub 账号登录后即可留言,支持 Markdown。