TT Lab
开始
学习 学习路径 课程

Envoy 内部结构

两台 Envoy,一个计数器

在 TT Lab 中继续学习

目标

启动真正的速率限制服务(envoyproxy/ratelimit)和 Redis,并把调用同一个服务的两台 Envoy 共用一个限额的情况,与本地限制并排统计。

为什么重要

只使用本地限制时,每增加一台代理,限额就会增加。在自动扩缩容面前,想要阻止的流量激增来得越猛,门就开得越宽。全局限制解决了这个问题,但它让每个请求多经过一个服务,如果不了解那个服务挂掉时的行为(默认:放行),故障时就找不到为什么没有任何人被拦住。亲手统计这两点,就能记住为什么要把二者一起使用。

步骤

  1. 以守护进程方式在 127.0.0.1:6379 上启动 Redis,不写入磁盘(--save ''、--appendonly no)。然后把 redis-cli ping 的输出和 redis-cli config get save 的输出依次保存到 /root/envd-rl/01-redis.txt。
  2. 在 /root/envd-rl/runtime/config/edge.yaml 中为域 edge 编写两条规则——描述符 plan=free 每小时 3 次,plan=pro 每小时 100 次。然后启动镜像中自带的 ratelimit(envoyproxy/ratelimit)。配置通过环境变量给出:Redis 127.0.0.1:6379、RUNTIME_ROOT=/root/envd-rl/runtime、RUNTIME_SUBDIRECTORY=.、RUNTIME_WATCH_ROOT=false、USE_STATSD=false,以及 HTTP 8180、gRPC 8181、调试 6070 端口(三者都是 127.0.0.1)。
  3. 启动上游 python3 /opt/lab/envoy/echo.py 8092,并用 /root/envd-rl/envoy-a.yaml 启动管理端口 9983、监听器 127.0.0.1:10083 的 Envoy(--base-id 21)。HTTP 过滤器的顺序为 local_ratelimit(stat_prefix 为 local_rl)→ ratelimit(域 edge,通过 gRPC 调用集群 ratelimit,failure_mode_deny: false,enable_x_ratelimit_headers: DRAFT_VERSION_03)→ router。路由有两条——/local 只在该路由上设置本地令牌桶(2 个令牌,每 60 秒补充 2 个),/ 则在该路由上用 rate_limits 把请求头 x-plan 生成为描述符键 plan。
  4. 在 /root/envd-rl/envoy-b.yaml 中编写与第一个 Envoy 相同、但管理端口为 9984、监听器为 127.0.0.1:10084 的配置,并用 --base-id 22 启动。然后向两台 Envoy 各发送一个 x-plan: pro 请求,查看响应中的 x-ratelimit-remaining 是否接续递减,把两个值以 a=、b= 两行写入 /root/envd-rl/04-remaining.txt(先发送 a)。
  5. 把八个 x-plan: free 请求交替(从 A 开始)发往 A(10083)和 B(10084)的 /,依次把结果以 a1=코드、b1=코드、a2=코드 …… b4=코드(占位符为 HTTP 状态码)八行写入 /root/envd-rl/05-global.txt。
  6. 用同样的方式,这次把八个请求发往 /local,并以 a1=코드 …… b4=코드(占位符为 HTTP 状态码)八行写入 /root/envd-rl/06-local.txt。
  7. 在 ratelimit 进程停止的状态下,向 A 发送一个 x-plan: free 请求,并把它的状态码以 down_code= 写入 /root/envd-rl/07-fail.txt。之后再追加 A 的统计 cluster.echo.ratelimit.error 和 cluster.echo.ratelimit.failure_mode_allowed 的值,写成 error=、allowed= 两行,然后重新启动服务。
  8. 在 /root/envd-rl/08-report.md 中写入四行——global_ok_total=(第 5 步中 200 的个数)、local_ok_total=(第 6 步中 200 的个数)、fail_open_code=(第 7 步的 down_code)、counted_in=(全局限制中实际计数的地方:envoy 或 redis)——并在下面至少写四行学到的内容。

参考

先搭建计数的地方——Redis

以守护进程方式在 127.0.0.1:6379 上启动 Redis,不写入磁盘(--save ''、--appendonly no)。然后把 redis-cli ping 的输出和 redis-cli config get save 的输出依次保存到 /root/envd-rl/01-redis.txt。

全局速率限制服务没有状态。来过几次,全部放在后面的存储(Redis)中,服务只是在每个窗口(window)给一个键加一。所以即使启动多台服务,限额也保持为一个。计数器在窗口过去之后就会被丢弃,所以没有必要保存到磁盘。像 redis-server --port 6379 --bind 127.0.0.1 --save '' --appendonly no --daemonize yes 这样启动。

启动速率限制服务

在 /root/envd-rl/runtime/config/edge.yaml 中为域 edge 编写两条规则——描述符 plan=free 每小时 3 次,plan=pro 每小时 100 次。然后启动镜像中自带的 ratelimit(envoyproxy/ratelimit)。配置通过环境变量给出:Redis 127.0.0.1:6379、RUNTIME_ROOT=/root/envd-rl/runtime、RUNTIME_SUBDIRECTORY=.、RUNTIME_WATCH_ROOT=false、USE_STATSD=false,以及 HTTP 8180、gRPC 8181、调试 6070 端口(三者都是 127.0.0.1)。

这个服务会读取 RUNTIME_ROOT/RUNTIME_SUBDIRECTORY/config/ 下的 YAML。一个文件对应一个域,描述符的 key、value 必须与请求发送的内容完全一致,规则才会命中。没有匹配的规则时,不做限制。是否正确读取了,调试端口的 curl localhost:6070/rlconfig 会显示——应该出现类似 edge.plan_free: unit=HOUR requests_per_unit=3 的行。启动时写成 env 변수=값 … setsid --fork nohup ratelimit > 로그 2>&1 </dev/null(占位符依次为变量名与值、日志文件)的形式。

第一台 Envoy 为每个请求生成描述符并询问

启动上游 python3 /opt/lab/envoy/echo.py 8092,并用 /root/envd-rl/envoy-a.yaml 启动管理端口 9983、监听器 127.0.0.1:10083 的 Envoy(--base-id 21)。HTTP 过滤器的顺序为 local_ratelimit(stat_prefix 为 local_rl)→ ratelimit(域 edge,通过 gRPC 调用集群 ratelimit,failure_mode_deny: false,enable_x_ratelimit_headers: DRAFT_VERSION_03)→ router。路由有两条——/local 只在该路由上设置本地令牌桶(2 个令牌,每 60 秒补充 2 个),/ 则在该路由上用 rate_limits 把请求头 x-plan 生成为描述符键 plan。

全局限制过滤器自己不计数。由路由(或虚拟主机)的 rate_limits 从请求中生成描述符,过滤器只是把它发给服务。如果把 rate_limits 放在虚拟主机上,它会作用于该主机的所有路由,即使在路由上写一个空列表(rate_limits: [])也无法关闭(实测)——所以要放在 / 路由上。用 gRPC 询问的集群必须启用 HTTP/2。没有 x-plan 头的请求不会生成描述符,会不受限制地通过。

第二台 Envoy 也调用同一个服务

在 /root/envd-rl/envoy-b.yaml 中编写与第一个 Envoy 相同、但管理端口为 9984、监听器为 127.0.0.1:10084 的配置,并用 --base-id 22 启动。然后向两台 Envoy 各发送一个 x-plan: pro 请求,查看响应中的 x-ratelimit-remaining 是否接续递减,把两个值以 a=、b= 两行写入 /root/envd-rl/04-remaining.txt(先发送 a)。

如果两台 Envoy 各自计数,两个响应的 remaining 会从同一个数开始。如果给同一个服务、同一个 Redis 键加一,第二个会比第一个小一。头名称不区分大小写,所以请用 curl -s -D - -o /dev/null 只获取头再查找。如果用相同的 --base-id 启动两台 Envoy,第二台会因为共享内存名称冲突而无法启动。

交替发送,限额也只有一个

把八个 x-plan: free 请求交替(从 A 开始)发往 A(10083)和 B(10084)的 /,依次把结果以 a1=코드、b1=코드、a2=코드 …… b4=코드(占位符为 HTTP 状态码)八行写入 /root/envd-rl/05-global.txt。

free 是每小时 3 次。如果两台 Envoy 共用限额,八个请求中应该只有三个是 200,其余是 429——与哪台 Envoy 收到无关。如果在这个时间窗口内已经发送过 free,200 可能会更少,这同样是全局计数的证据。在 Redis 中用 redis-cli --scan --pattern 'edge_plan_free_*' 查看键并 get,就会发现两台 Envoy 的请求汇集在同一个键上。

同样的八个请求改用本地限制会怎样

用同样的方式,这次把八个请求发往 /local,并以 a1=코드 …… b4=코드(占位符为 HTTP 状态码)八行写入 /root/envd-rl/06-local.txt。

/local 路由上没有全局限制,只有本地令牌桶(2 个令牌)。令牌桶在一台 Envoy 之内,所以 A 给 2 个,B 也给 2 个。与全局的结果并排放在一起,增加代理时会发生什么变化就以数字呈现出来了。桶每 60 秒补充一次,所以重做时请等待一分钟。

限制服务挂掉,就放行

在 ratelimit 进程停止的状态下,向 A 发送一个 x-plan: free 请求,并把它的状态码以 down_code= 写入 /root/envd-rl/07-fail.txt。之后再追加 A 的统计 cluster.echo.ratelimit.error 和 cluster.echo.ratelimit.failure_mode_allowed 的值,写成 error=、allowed= 两行,然后重新启动服务。

free 已经用完了限额,所以如果服务还活着,结果是 429。服务挂掉时,failure_mode_deny: false(默认值)会让请求通过。速率限制是一种保护机制,这个选择的含义是:它出了故障,也不要让整个服务停下来。作为代价,那一刻只会记录在统计中。统计名称中的 cluster.echo 是请求所前往的上游集群的名称。

写下增加台数后限额会怎样

在 /root/envd-rl/08-report.md 中写入四行——global_ok_total=(第 5 步中 200 的个数)、local_ok_total=(第 6 步中 200 的个数)、fail_open_code=(第 7 步的 down_code)、counted_in=(全局限制中实际计数的地方:envoy 或 redis)——并在下面至少写四行学到的内容。

值请从前面步骤的文件中转写。说明行中请写下“把代理增加到三台时,本地限制和全局限制的实际限额分别会怎样”,以及“把两者一起使用有什么好处(本地限制是保护限制服务的第一道墙)”。