关于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 主要考虑:

  1. 统一管理南北向和东西向流量:Kong 只能管外部流量,服务间调用需要另外一套方案
  2. Kubernetes 原生支持:所有配置都是 CRD,和 Kubernetes 完美集成
  3. 可观测性开箱即用:Metrics、Tracing、Logging 都能自动采集
  4. 灰度发布和流量镜像:支持按百分比、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 基准测试):

阶段基准 QPSP95 延迟P99 延迟
Nginx + OpenResty8,00025ms45ms
Kong6,50032ms58ms
Istio Gateway5,50038ms72ms

Istio 的延迟确实高一些,但考虑到功能完整性,这个 trade-off 是可以接受的。而且实际生产环境的 QPS 远没到瓶颈,瓶颈通常在后端服务。


架构演进的一点思考

回顾这次 API 网关的演进,有几个心得:

  1. 没有银弹:Nginx、Kong、Istio 各有适用场景,不是哪个更高级就用哪个
  2. 成本要算总账:维护成本、学习成本、迁移成本,都是实际开销
  3. 不要过度设计:如果 Nginx 能满足需求,没必要硬上 Istio
  4. 团队能力要匹配:新工具能带来新能力,但团队要是跟不上,就是负担

技术选型不是追新,而是解决问题。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/) 转载或引用必须申明原指尖魔法屋来源及源地址!