0%

Envoy 限流系列(二)local rate limit

本地限流因为使用简单,执行高效,可以算是最常用的了

在 Envoy 里,local rate limit 可以用于不同的 Envoy Filter 扩展阶段,从而实现不同行为的限流:

  1. HTTP Filter,限制 HTTP 请求频率
  2. Network Filter,限制 TCP 建连频率
  3. Listener Filter,限制TLS 握手频率

配置用法

以常用的 HTTP localrate 为例来感受一下配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
http_filters:
- name: envoy.filters.http.local_ratelimit
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit
stat_prefix: http_local_rate_limiter
token_bucket:
max_tokens: 1000
tokens_per_fill: 1000
fill_interval: 2s
status: 429
response_headers_to_add:
- append_action: OVERWRITE_IF_EXISTS_OR_ADD
header:
key: x-local-rate-limit
value: 'true'

Envoy local ratelimit 采用令牌桶算法,限流的核心配置是这三个:

  1. max_tokens 桶的最大 token 容量,也是初始的默认值
  2. tokens_per_fill 每个周期增加的 token 数量
  3. fill_interval 填充 token 的时间周期

那么,上面配置的效果就是:

  1. 一个最大容量为 1000 的桶,每 2s 往桶里加 1000 个 token
  2. 每来一个请求,消耗桶里一个 token,如果桶里没有 token 了,就会拦截请求

对于拦截请求的响应,还可以配置:

  1. 响应状态码,默认是 429
  2. 和响应的请求头

细粒度限流

上面的示例是一个粗粒度的策略,针对这个 listener 端口上的所有请求。

如果想实现一个细粒度的,比如域名级,或者接口级的呢?

也是可以做到的,ratelimit 也支持 route 级别的配置:

1
2
3
4
5
6
7
8
9
10
11
routes:
- match: {prefix: "/path/with/rate/limit"}
route: {cluster: service_protected_by_rate_limit}
typed_per_filter_config:
envoy.filters.http.local_ratelimit:
"@type": type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit
stat_prefix: http_local_rate_limiter
token_bucket:
max_tokens: 10000
tokens_per_fill: 1000
fill_interval: 1s

如果是同一个路由内,还想实现更细粒度限流呢?

Envoy 还有路由级别的 descriptors,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
- match: {prefix: "/foo"}
route:
cluster: service_protected_by_rate_limit
rate_limits:
- actions: # any actions in here
- request_headers:
header_name: x-envoy-downstream-service-cluster
descriptor_key: client_cluster
typed_per_filter_config:
envoy.filters.http.local_ratelimit:
"@type": type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit
stat_prefix: test
token_bucket:
max_tokens: 1000
tokens_per_fill: 1000
fill_interval: 60s
descriptors:
- entries:
- key: client_cluster
value: foo
token_bucket:
max_tokens: 10
tokens_per_fill: 10
fill_interval: 60s

可以根据请求中的特征(上面示例是一个请求 header),实现路由内,不同的限流配置

实现机制

从配置方式理解,Envoy 的实现也不需要很复杂

  1. 在配置解析阶段,配置中的每一个 token_bucket,就会生成一个 RateLimitTokenBucket 桶实例

  2. 在请求处理阶段,通过 route/descriptors 匹配到 token_bucket 之后,就从对应的桶里取值即可

在 1.31.0 之前,是每个桶都有一个定时器,每 fill_interval 给桶里注入 tokens_per_fill 的令牌

在 1.31.0 里,做了个优化,不再依赖定时器

原因是,定时器运行在 main thread,而 main thread 在 Envoy 里面还有一个重要职责就是加载来自控制面的配置,当配置量很大的时候,可能会长时间的阻塞这些 timer,就影响限流了

与 NGINX 对比

在 NGINX 里,与 Envoy local rate limit 最接近的就是 limit_req

首先是限流算法上的区别,Envoy 是令牌桶算法,而 NGINX 是漏桶算法

漏桶算法可以实现流量整形,比如上面示例的 1000/2s 在 NGINX 会被处理为 500/s,也就是每 2ms 放行一个请求,从而让请求更均匀的转发给后端应用

而令牌桶算法的限制值,更接近人类的理解,X 时间放行 Y 个请求

动态限流 key

不过,在我看来,最大的区别还是,NGINX 可以指定动态限流 key,从而实现每个 IP 限制 10/s 这样的效果,而 Envoy 却做不到。

从实现机制上来说,NGINX 是基于 shared memory zone 来实现的,可以灵活的开辟大内存来实现很多 key 的存储,同时,基于 lru 的淘汰算法,也可以保证内存可控

shared memory zone 是 NGINX 内部的一个通用的底层组件,而 Envoy 还没有这类基建

从使用场景上来说,NGINX 主要用于南北向接入网关,防 DDOS/cc 攻击需求强烈,而基于 IP 的限流策略则是最常用的防攻击策略

而 Envoy 多用于东西向场景,流量来源多是可信的,限流策略主要是用于保护后端服务不被打爆,所以对于这种动态 key 的诉求相对没那么强烈

集群限流

Envoy 的 local ratelimit 虽然是本地限流,但是也可以配置为集群级别的限流效果

1.31.0 新增了 local_cluster_rate_limit 的配置,可以让 token 的限流值,按照 Envoy 集群数量来平均分配,并且随着 Envoy 实例数量自动调整

当前,这里有个假设前提是,流量在 Envoy 节点上是均匀的

NGINX 的 shared memory zone 是单机内共享的,不过商业版里,也增加了一个跨机同步的机制,这样也可以实现集群级别的限流

最后

个人观点,简单点评几句:

Envoy 的本地限流,好的是应用场景多,HTTP 请求,TCP 连接,TLS 握手 都可以用

NGINX 呢,优势是动态限流 key,用来防 cc,应对公网不可信的流量,确实很香

嗯,至少目前如此,后面 Envoy 加上动态限流 key,也不是太大的问题