跳到主要内容

Istio-限流配置

· 阅读需 11 分钟

线上接口被刷过一次之后,我把限流从应用层挪到了网关层。这篇笔记整理在 Istio IngressGateway 上基于 Envoy RateLimit + Redis 做全局限流的完整配置,以及几个容易踩的匹配细节。

背景与问题

在微服务架构中,系统通常会通过 Istio IngressGateway 对外提供统一入口。当流量突然增加(活动流量、爬虫、接口被刷)时,如果没有限流机制,后端可能出现这些问题:

  • 接口被高频调用,服务 CPU 或数据库压力过大
  • 突发流量导致系统雪崩或级联故障
  • 核心 API 被恶意刷接口,挤占正常用户的访问

在每个服务里各自实现限流当然可行,但规则分散、口径不一,而且请求已经打进了业务进程,资源照样被消耗。更合理的做法是在网关层统一限流,请求进入后端之前就完成流量治理。

Istio 的数据面就是 Envoy Proxy,天然具备这个能力:通过 Envoy RateLimit 过滤器 + 独立的 ratelimit 服务 + Redis,可以实现跨网关副本的全局限流,对特定路径做访问频率控制。

原理简述

整条链路上有三个角色,理解它们的关系,后面的 YAML 就不难读了:

1)Envoy RateLimit 过滤器。挂在 IngressGateway 的 HTTP 过滤器链上,每个请求经过时,按路由上配置的 actions 生成一组描述符(descriptor),带着 domain 一起通过 gRPC 询问 ratelimit 服务:这个请求放不放行。

2)ratelimit 服务(envoyproxy/ratelimit)。一个独立部署的 gRPC 服务,加载 ConfigMap 里的配额规则,收到描述符后查规则、扣配额,返回 OK 或 OVER_LIMIT。

3)Redis。配额计数器的存储。正因为计数在 Redis 里,多个网关副本共享同一份计数,这才叫"全局"限流——本地限流(local rate limit)则是每个 Envoy 实例各算各的。

匹配的关键在于:路由上的 actions 生成的描述符组合,必须与 ConfigMap 里 descriptors 的 key/value 层级完全对应,并且两边的 domain 一致,配额才会命中。任何一环对不上,限流就静默失效,请求全部放行。

什么时候会用到

典型的是这类低频但敏感、被刷的代价很高的接口:

/api/login
/api/send-code
/api/payment

登录接口被刷会触发大量密码校验和风控计算;发验证码接口被刷直接烧短信费;支付接口则关系到下游资源。这些接口本身 QPS 不高,给一个很小的配额就能挡掉绝大部分恶意流量,误伤面也小。

完整配置

下面是一套可以直接套用的配置,分四部分:Secret 存 Redis 密码、ConfigMap 定义配额、ratelimit 服务本体、两个 EnvoyFilter 分别负责挂过滤器和打描述符。

# --------------------------------------------
# 0) Redis 密码放 Secret(更安全)
# 用 Secret 存放敏感信息(如 Redis 密码),避免明文进入镜像或 Pod 环境变量历史
# --------------------------------------------
apiVersion: v1
kind: Secret
metadata:
name: ratelimit-redis-secret # Secret 名称,后续 Deployment 通过 secretKeyRef 引用
namespace: istio-system # 放在与 ratelimit 服务相同的命名空间,便于引用与管理
type: Opaque
stringData:
REDIS_AUTH: "password" # 明文由 K8s 负责 base64 编码存储

---
# --------------------------------------------
# 1) Ratelimit 配额配置(Envoy Ratelimit Server 的运行时配置)
# - 该 ConfigMap 被容器以只读方式挂载到 /data/ratelimit/config
# - domain 必须与 Envoy HTTP 过滤器里的 domain 完全一致,否则不会命中
# - descriptors 用来描述限流的“维度组合”(actions 组合后形成的描述符)
# --------------------------------------------
apiVersion: v1
kind: ConfigMap
metadata:
name: ratelimit-config
namespace: istio-system
data:
config.yaml: |
domain: ingress-ratelimit # 必须与 RateLimit 过滤器的 domain 一致
descriptors:
- key: header_match # 顶层 key:与 HTTP_ROUTE 中的 header_value_match 对应(actions 1/2)
value: path-api # 顶层 value:来自路由上 actions 的 descriptor_value
descriptors: # 二级描述符:继续细分 (组合成唯一的限流键)
- key: generic_key # 与 HTTP_ROUTE 中 generic_key 对应(actions 2/2)
value: istio-limit-v1 # 对应路由里设置的 descriptor_value
rate_limit:
unit: second # 时间单位:second / minute / hour / day
requests_per_unit: 5 # 配额:单位时间内允许的请求数(此处为 1 秒 1 次)

---
# --------------------------------------------
# 2) Ratelimit Service(连接你现有 Redis 单机)
# - 通过 envoyproxy/ratelimit 实现全局滑窗/令牌桶限流的配置服务
# - replicas=1:测试环境单副本;生产建议至少 2-3 副本并配合 Redis 高可用
# - readiness/liveness:通过 6070 端口健康检查
# - 关键环境变量:REDIS_URL / REDIS_AUTH / RUNTIME_ROOT / RUNTIME_SUBDIRECTORY
# --------------------------------------------
apiVersion: v1
kind: Service
metadata:
name: ratelimit # 供 Envoy 通过集群名解析 (outbound|8081||ratelimit.istio-system.svc.cluster.local)
namespace: istio-system
spec:
selector: { app: ratelimit } # 与 Deployment 的 labels 匹配
ports:
- name: grpc
port: 8081 # gRPC 服务端口(Envoy RateLimit 过滤器将连此端口)
targetPort: 8081
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: ratelimit
namespace: istio-system
spec:
replicas: 1 # 测试设 1,生产建议 ≥3 并做好 HPA/PodDisruptionBudget
selector:
matchLabels: { app: ratelimit }
template:
metadata:
labels: { app: ratelimit }
spec:
containers:
- name: ratelimit
image: envoyproxy/ratelimit:875d418c # 指定 commit/tag,保证版本可重现;可考虑升级到官方较新 tag
imagePullPolicy: IfNotPresent
command: ["/bin/ratelimit"] # 启动入口
ports:
- containerPort: 8080 # HTTP Admin(可选:metrics/调试)
- containerPort: 8081 # gRPC 主服务端口
- containerPort: 6070 # 健康检查端口(/healthcheck)
env:
- name: REDIS_SOCKET_TYPE
value: tcp # 通过 TCP 连接 Redis
- name: REDIS_URL
value: "redis://1.1.1.1:6379"# 你的 Redis 地址(生产建议内网域名 + 哨兵或集群)
- name: REDIS_POOL_SIZE
value: "20" # 连接池大小;视并发/副本数调优
- name: REDIS_AUTH
valueFrom:
secretKeyRef:
name: ratelimit-redis-secret # 从 Secret 注入密码,避免明文
key: REDIS_AUTH
- name: USE_STATSD
value: "false" # 如有 statsd/Prometheus sidecar 可打开
- name: RUNTIME_ROOT
value: /data # 对应挂载点根目录
- name: RUNTIME_SUBDIRECTORY
value: ratelimit # 子目录,最终配置路径 /data/ratelimit/config
- name: RUNTIME_IGNOREDOTFILES
value: "true" # 忽略隐藏文件
volumeMounts:
- name: config
mountPath: /data/ratelimit/config # 与上面 runtime 路径一致,确保装载到 config 子目录
readinessProbe:
httpGet:
path: /healthcheck
port: 6070
initialDelaySeconds: 2 # 初始延迟,避免冷启动误判
periodSeconds: 5 # 探测频率
livenessProbe:
httpGet:
path: /healthcheck
port: 6070
initialDelaySeconds: 10
periodSeconds: 10
volumes:
- name: config
configMap:
name: ratelimit-config # 装载上面的 ConfigMap

---
# --------------------------------------------
# 3) IngressGateway 注入全局 RateLimit 过滤器 + 指向 ratelimit 的 CLUSTER
# - 通过 EnvoyFilter 在 HTTP Router 之前插入全局 ratelimit HTTP 过滤器
# - workloadSelector:限定只作用于网关工作负载(label: istio=ingressgateway)
# - rate_limit_service:连到上面定义的 ratelimit gRPC(通过集群名/authority)
# - 追加一个 Lua 响应过滤器:当后端返回 429 时,改写为统一 JSON
# --------------------------------------------
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
name: gateway-global-limit-filter
namespace: istio-system
spec:
workloadSelector:
labels:
istio: ingressgateway # 只作用在 IngressGateway 实例
configPatches:
# 3.1 在 router 之前插入 ratelimit HTTP 过滤器(全局生效)
- applyTo: HTTP_FILTER
match:
context: GATEWAY
listener:
filterChain:
filter:
name: envoy.filters.network.http_connection_manager # HTTP 连接管理器
subFilter:
name: envoy.filters.http.router # 在 Router 之前插入
patch:
operation: INSERT_BEFORE
value:
name: envoy.filters.http.ratelimit
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.ratelimit.v3.RateLimit
domain: ingress-ratelimit # 必须与 ConfigMap 里的 domain 一致
failure_mode_deny: false # 后端 ratelimit 服务异常时是否拒绝请求;false=放行(推荐)
timeout: 10s # 访问 ratelimit 服务的超时
rate_limit_service:
grpc_service:
envoy_grpc:
cluster_name: outbound|8081||ratelimit.istio-system.svc.cluster.local # Istio 自动生成的集群名
authority: ratelimit.istio-system.svc.cluster.local # HTTP/2 Host (SNI),可省略
transport_api_version: V3 # 使用 v3 API(推荐)
# 3.2 基于端口 1035 的 Listener 再插入 Lua 响应过滤器(可选:仅对该监听端口生效)
- applyTo: HTTP_FILTER
match:
context: GATEWAY
listener:
portNumber: 1035 # 仅对 1035 端口的 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.lua
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua
inlineCode: |
function envoy_on_response(handle)
local s = tonumber(handle:headers():get(":status") or "0")
if s == 429 then
local body = '{"code":429,"message":"Too many requests. Please try again later."}'
handle:headers():replace("content-type", "application/json")
handle:headers():replace("content-length", tostring(#body))
handle:body(true):setBytes(body)
end
end
# 注意:
# - 如果你的网关并非监听 1035(或多端口),请确认此过滤器是否应该扩大到所有端口(去掉 portNumber 条件)
# - 也可用本地速率限制本地拒绝时在本机生成 429,再由该 Lua 统一改写

---
# --------------------------------------------
# 4) 给 VirtualHost 注入 per-route 限流动作
# - 在指定的 Route(或整个 vhost)下配置 actions,从而生成与 ConfigMap 匹配的 descriptors
# - 这里使用两段 action:
# a) header_value_match:当 :path 以 /api 前缀命中时,写入 descriptor_value=path-api
# b) generic_key:再追加 descriptor_value=istio-limit-v1
# - 两段组合 => (key=header_match,value=path-api) + (key=generic_key,value=istio-limit-v1)
# 正好与 ConfigMap 对应,从而激活“1 秒 1 次”的限流
# --------------------------------------------
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
name: gateway-limit-descriptor-svc
namespace: istio-system
spec:
workloadSelector:
labels:
istio: ingressgateway # 作用范围限定在网关
configPatches:
- applyTo: HTTP_ROUTE
match:
context: GATEWAY
routeConfiguration:
vhost:
name: host:port # 注意此 vhost 名:通常为 "<host>:<port>"
patch:
operation: MERGE
value:
route:
rate_limits:
- actions:
- header_value_match:
headers:
- name: ":path" # 匹配伪首部 :path,基于前缀判断
prefix_match: "/api"
expect_match: true # 仅当匹配时才添加该描述符
descriptor_value: "path-api" # 对应 ConfigMap 顶层 key/value
- generic_key:
descriptor_value: "istio-limit-v1" # 对应 ConfigMap 二级 key/value

几个配置点展开说一下:

1)两个 EnvoyFilter 缺一不可。第一个只是把 ratelimit 过滤器挂到过滤器链上、告诉它去哪找 ratelimit 服务;真正决定"哪些请求带什么描述符去问限流"的是第二个——路由上没有 rate_limits.actions,过滤器对请求就什么都不做。

2)failure_mode_deny: false。ratelimit 服务或 Redis 挂掉时选择放行而不是拒绝。限流是保护手段,不该成为新的单点故障;除非接口敏感到宁可不可用也不能被刷,否则建议保持 false。

3)Lua 过滤器是体验优化。Envoy 限流命中时默认返回一个裸的 429,前端不好统一处理;这段 Lua 在响应阶段把 429 的 body 改写成统一的 JSON 结构,对客户端更友好。

踩坑与注意

  • domain 不一致,限流静默失效。过滤器里的 domain 和 ConfigMap 里的 domain 必须逐字符相同。对不上时不会报错,ratelimit 服务只是查不到规则,全部返回 OK。
  • descriptor 层级必须完全对应。ConfigMap 里 header_match/path-api 嵌套 generic_key/istio-limit-v1 的两层结构,对应路由上两个 action 的先后顺序。少一个 action、value 写错、层级颠倒,都命中不了。
  • vhost 名不是随便写的routeConfiguration.vhost.name 通常是 "<host>:<port>" 的形式,要和 Envoy 实际生成的路由配置里的 vhost 名一致,可以从网关的 config_dump 里确认,写错了 MERGE 不会生效。
  • 注意 EnvoyFilter 匹配的端口。上面 Lua 过滤器限定了 portNumber: 1035,如果网关监听的是别的端口或多个端口,要相应调整或去掉这个条件,否则 429 改写只对部分流量生效。
  • ratelimit 服务自身的可用性。测试环境单副本没问题,生产建议多副本,Redis 也要考虑高可用;REDIS_POOL_SIZE 按并发量和副本数调整。
  • EnvoyFilter 是较底层的扩展机制,直接 patch Envoy 配置,对 Envoy 内部 API 有版本耦合。升级 Istio 大版本前,先在测试集群验证这两个 filter 还能正常生效。
提示

排查限流不生效时,顺着链路依次确认:ratelimit 服务日志里有没有收到请求 → domain 是否一致 → 描述符组合是否与 ConfigMap 对齐 → vhost 名是否正确。绝大多数问题出在后三个"对不上"。

小结

网关层全局限流的核心就是三件事:路由上的 actions 负责给请求打描述符,ratelimit 服务负责按 ConfigMap 里的规则查配额,Redis 负责让所有网关副本共享计数。配置本身不复杂,难点集中在 domain 和 descriptor 的匹配上——这些地方错了不报错,只是默默不生效。把匹配链路理清楚,剩下的就是按业务需要调配额了。

评论 / COMMENTS