插入补丁,分辨 istioctl 与 Envoy 的判定
目标
用 EnvoyFilter 把 fault 过滤器插入 router 之前,并亲手重现这个补丁会变成 Envoy 配置的哪个位置。再做出插在 router 之后的版本和值有误的版本,观察 istioctl 与 Envoy 的判定出现分歧,并确认版本限定与作用范围。
为什么重要
EnvoyFilter 是网格中最强大的资源,同时也是最容易悄悄出问题的资源。即使 istioctl 放行,只要 Envoy 拒绝,在生产环境中看到的就只是“配置没有变化”。了解补丁作用的位置和检查的局限,就能在合并之前把它筛出来。
步骤
- 创建
/root/ist2-ef,并在/root/ist2-ef/ef.yaml中编写 EnvoyFilter——apiVersion: networking.istio.io/v1alpha3,名称reviews-fault,命名空间default,workloadSelector为app: reviews。补丁只有一个:applyTo: HTTP_FILTER,match.context: SIDECAR_INBOUND,match.listener.filterChain.filter.name为envoy.filters.network.http_connection_manager,其subFilter.name为envoy.filters.http.router,patch.operation: INSERT_BEFORE。要插入的值,名称为envoy.filters.http.fault,typed_config(@type为type.googleapis.com/envoy.extensions.filters.http.fault.v3.HTTPFault)中设置abort.http_status: 418,abort.percentage为numerator: 100和denominator: HUNDRED。把istioctl validate -f /root/ist2-ef/ef.yaml的输出(包含标准错误)和退出码保存到/root/ist2-ef/01-validate.txt(最后一行为rc=)。 - 用
yq从/root/ist2-ef/ef.yaml的第一个补丁中取出五个值,写入/root/ist2-ef/02-fields.txt——applyTo=、context=(match.context)、operation=(patch.operation)、anchor=(match作为基准的 HTTP 过滤器,即subFilter.name)、filter=(要插入的值的name)。 - 在
/root/ist2-ef/envoy-before.yaml中编写 Envoy 配置——管理端口9989,监听器virtualInbound监听127.0.0.1:10089,HTTP 连接管理器的stat_prefix为inbound_0.0.0.0_9080,把所有路径发往集群inbound|9080||(127.0.0.1:8112)。http_filters的顺序是:与 ef.yaml 的补丁值完全相同的 fault 过滤器条目,然后是 router。以ok模式在8112上启动上游并启动 Envoy 之后,发送三次curl localhost:10089/reviews,并在/root/ist2-ef/03-before.txt中写入三行——codes=(三个响应状态码,用逗号分隔)、stat_name=(统计 fault 所中止的请求数的统计的完整名称)、aborts_injected=(该统计的值)。 - 把
/root/ist2-ef/ef.yaml复制为/root/ist2-ef/ef-after.yaml,只把patch.operation改为INSERT_AFTER。再把/root/ist2-ef/envoy-before.yaml复制为/root/ist2-ef/envoy-after.yaml,只把http_filters的顺序颠倒为 router、fault(即该补丁生效之后的结果)。分别用istioctl validate -f和envoy --mode validate -c检查这两个文件,并写入/root/ist2-ef/04-after.txt——第一行istioctl_rc=,第二行envoy_rc=,下面原样附上包含 Envoy 拒绝原因的输出行。 - 把
/root/ist2-ef/ef.yaml复制为/root/ist2-ef/ef-badfield.yaml,把/root/ist2-ef/envoy-before.yaml复制为/root/ist2-ef/envoy-badfield.yaml,然后把两个文件中 fault 的abort.percentage改成在 VirtualService 中使用的形式{ value: 100 }(其余保持不变)。分别用istioctl validate -f和envoy --mode validate -c检查,并写入/root/ist2-ef/05-gap.txt——第一行istioctl_rc=,第二行envoy_rc=,下面原样附上 istioctl 针对这个字段给出的警告行,以及 Envoy 拒绝原因的行。 - 把
/root/ist2-ef/ef.yaml复制为/root/ist2-ef/ef-pinned.yaml,并在第一个补丁的match.proxy.proxyVersion中加入正则表达式^1\.24.*(其余保持不变)。用istioctl validate确认它能通过;为了弄清这个实验中的代理版本,请在/root/ist2-ef中用istioctl kube-inject对/opt/lab/fixtures/istio/inject-target.yaml进行注入,并保存为/root/ist2-ef/inject.yaml(要把三个注入配置文件/opt/istio/inject-config.yaml、mesh-config.yaml、values-config.yaml全部传入)。在/root/ist2-ef/06-version.txt中写四行——regex=(原样写下 ef-pinned.yaml 中的正则表达式)、proxy_version=(istio-proxy镜像的标签)、matches_proxy=(该版本匹配正则表达式则写yes,否则写no)、matches_1_25_0=(假设的版本1.25.0匹配则写yes,否则写no)。 - 在
/root/ist2-ef/ef-ratelimit.yaml中编写 EnvoyFilter——名称inbound-ratelimit,命名空间default,不使用workloadSelector,补丁形态与 ef.yaml 相同(HTTP_FILTER、SIDECAR_INBOUND、router 之前 INSERT_BEFORE),值的名称为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: 1、tokens_per_fill: 1、fill_interval: 300s,filter_enabled和filter_enforced的default_value都是numerator: 100、denominator: HUNDRED。用istioctl validate确认没有关于值的警告之后,把这个值放在 router 之前的 Envoy 配置写入/root/ist2-ef/envoy-rl.yaml(管理端口、监听器、集群与第 3 步相同)。启动并发送三次curl localhost:10089/reviews之后,在/root/ist2-ef/07-ratelimit.txt中写四行——codes=(三个响应状态码,用逗号分隔)、rate_limited=(统计http_local_rate_limit.rate_limited的值)、scope=(这个 EnvoyFilter 作用的范围:workload、namespace、mesh之一)、mesh_wide_namespace=(如果把同一个文件放进去,就会作用于整个网格的命名空间,取自/opt/istio/mesh-config.yaml)。 - 在
/root/ist2-ef/08-report.md中写入before_status=、after_envoy_rc=、badfield_istioctl_rc=、ratelimit_codes=四行(依次为第 3 步中 fault 返回的状态码、第 4 步中 Envoy 的退出码、第 5 步中 istioctl 的退出码、第 7 步的三个响应状态码),并在下面写至少四行以-开头的说明。
参考
- 这个 Pod 中既没有真正的 istiod,也没有真正的 sidecar。所以无法用
istioctl proxy-config查看实际生成的内容,需要了解转换规则,并亲手编写等价的 Envoy 配置来确认行为。同样的规则会原样出现在生产集群的proxy-config输出中。 - 即使是正确的 EnvoyFilter,
istioctl validate也会给出“exposes internal implementation details”警告。这个警告是正常的,请观察是否另外出现了针对值中字段的警告。警告也会输出到标准错误,所以保存到文件时要加上2>&1。 - Envoy 的错误文案中,单词之间的空格是故意不规整的(
no such field)。肉眼阅读时不必惊讶。 - 每一步的配置文件名称各不相同。前面步骤的文件请复制后使用,不要直接修改——前面步骤的评分会查看那些文件。
- 启动 Envoy 时,请用
setsid --fork nohup envoy -c <파일> --log-level warn > <로그> 2>&1 </dev/null(占位符依次为配置文件与日志文件)使它与 shell 完全脱离。重新启动之前,用pkill -x envoy清理(pkill -f 'envoy -c'会把包含该字符串的 shell 自己也杀掉)。 - 镜像中有模拟上游的服务器:
python3 /opt/lab/envoy/upstream.py <포트> ok|fail|slow(占位符为端口)。响应正文为<모드>:<포트> <경로>(占位符依次为模式、端口与路径)。 - 修改配置之后,启动之前先用
envoy --mode validate -c <파일>(占位符为配置文件)筛查一遍。集群名称中含有|,所以在 YAML 中必须用引号括起来。
编写把 fault 插入 router 之前的 EnvoyFilter
创建 /root/ist2-ef,并在 /root/ist2-ef/ef.yaml 中编写 EnvoyFilter——apiVersion: networking.istio.io/v1alpha3,名称 reviews-fault,命名空间 default,workloadSelector 为 app: reviews。补丁只有一个:applyTo: HTTP_FILTER,match.context: SIDECAR_INBOUND,match.listener.filterChain.filter.name 为 envoy.filters.network.http_connection_manager,其 subFilter.name 为 envoy.filters.http.router,patch.operation: INSERT_BEFORE。要插入的值,名称为 envoy.filters.http.fault,typed_config(@type 为 type.googleapis.com/envoy.extensions.filters.http.fault.v3.HTTPFault)中设置 abort.http_status: 418,abort.percentage 为 numerator: 100 和 denominator: HUNDRED。把 istioctl validate -f /root/ist2-ef/ef.yaml 的输出(包含标准错误)和退出码保存到 /root/ist2-ef/01-validate.txt(最后一行为 rc=)。
EnvoyFilter 的一个补丁是“在哪里”(applyTo、match)和“做什么”(operation、value)的组合。对 HTTP_FILTER 用 subFilter 选定基准过滤器之后,包含该过滤器的 HTTP 连接管理器的过滤器列表就成了舞台。value 不是 Istio 的语法,而是原样的 Envoy 配置,所以百分比也必须写成 Envoy 的 FractionalPercent(分子、分母)。即使文件是正确的,istioctl 也会针对 EnvoyFilter 本身给出一行警告——这个警告是正常的,但如果还出现针对值中字段的警告,就需要修改。
把补丁的“在哪里”和“做什么”按四项来读
用 yq 从 /root/ist2-ef/ef.yaml 的第一个补丁中取出五个值,写入 /root/ist2-ef/02-fields.txt——applyTo=、context=(match.context)、operation=(patch.operation)、anchor=(match 作为基准的 HTTP 过滤器,即 subFilter.name)、filter=(要插入的值的 name)。
applyTo 是补丁作用的 Envoy 对象的种类(监听器、过滤器链、网络过滤器、HTTP 过滤器、集群、路由……),context 是哪个方向的代理(sidecar 的入站一侧、出站一侧,或网关),match 是在该种类中选出具体哪一个的条件。像 INSERT_BEFORE 这样的相对位置操作,必须有这个基准过滤器才有意义。请不要混淆 filter.name 和 subFilter.name——前者是网络过滤器,后者是 HTTP 过滤器。
建立补丁生效之后的样子,用 Envoy 得到 418
在 /root/ist2-ef/envoy-before.yaml 中编写 Envoy 配置——管理端口 9989,监听器 virtualInbound 监听 127.0.0.1:10089,HTTP 连接管理器的 stat_prefix 为 inbound_0.0.0.0_9080,把所有路径发往集群 inbound|9080||(127.0.0.1:8112)。http_filters 的顺序是:与 ef.yaml 的补丁值完全相同的 fault 过滤器条目,然后是 router。以 ok 模式在 8112 上启动上游并启动 Envoy 之后,发送三次 curl localhost:10089/reviews,并在 /root/ist2-ef/03-before.txt 中写入三行——codes=(三个响应状态码,用逗号分隔)、stat_name=(统计 fault 所中止的请求数的统计的完整名称)、aborts_injected=(该统计的值)。
istiod 应用 EnvoyFilter 之后,这个 sidecar 入站一侧监听器中的 HTTP 过滤器列表就会变成 [..., fault, router]。这个 Pod 中没有 istiod,所以要亲手做出这个结果。关键在于,补丁的 value 会原样成为列表中的一个条目——转写时不要修改其中的值。HTTP 过滤器的统计会累积在 http.<stat_prefix>. 之下。请用管理端口的 /stats?filter= 缩小范围。
插在 router 之后,istioctl 会放行,Envoy 却拒绝
把 /root/ist2-ef/ef.yaml 复制为 /root/ist2-ef/ef-after.yaml,只把 patch.operation 改为 INSERT_AFTER。再把 /root/ist2-ef/envoy-before.yaml 复制为 /root/ist2-ef/envoy-after.yaml,只把 http_filters 的顺序颠倒为 router、fault(即该补丁生效之后的结果)。分别用 istioctl validate -f 和 envoy --mode validate -c 检查这两个文件,并写入 /root/ist2-ef/04-after.txt——第一行 istioctl_rc=,第二行 envoy_rc=,下面原样附上包含 Envoy 拒绝原因的输出行。
router 是把请求发往上游并就此结束的过滤器,所以 Envoy 在配置阶段就会禁止在它之后再有过滤器。istioctl 只了解 EnvoyFilter 的外层结构,不了解 Envoy 的过滤器顺序规则,所以会放行这个补丁。在生产环境中,istiod 会照样推送,而 Envoy 拒绝这次监听器更新(NACK)——看上去 Pod 一切正常,但配置没有变化。退出码请用命令之后紧接着的 $? 获取。
照搬 VirtualService 风格的百分比,只会出现警告
把 /root/ist2-ef/ef.yaml 复制为 /root/ist2-ef/ef-badfield.yaml,把 /root/ist2-ef/envoy-before.yaml 复制为 /root/ist2-ef/envoy-badfield.yaml,然后把两个文件中 fault 的 abort.percentage 改成在 VirtualService 中使用的形式 { value: 100 }(其余保持不变)。分别用 istioctl validate -f 和 envoy --mode validate -c 检查,并写入 /root/ist2-ef/05-gap.txt——第一行 istioctl_rc=,第二行 envoy_rc=,下面原样附上 istioctl 针对这个字段给出的警告行,以及 Envoy 拒绝原因的行。
VirtualService 的 fault.abort.percentage.value 是 Istio 的百分比类型,而 Envoy 的 fault 过滤器使用由分子和分母构成的 FractionalPercent。这是人们常犯的转写错误。istioctl 会尝试把值解析为 Envoy 类型,但对不认识的字段只以警告提示,退出码为 0——如果 CI 只看退出码,就会照样部署。相反,如果 @type 名称本身写错,istioctl 也会以错误拦截。应该相信哪一边,标准就在这里分野。用 yq 只修改一个字段,就不会动到其余部分。
用 proxyVersion 把补丁限定在一个版本上
把 /root/ist2-ef/ef.yaml 复制为 /root/ist2-ef/ef-pinned.yaml,并在第一个补丁的 match.proxy.proxyVersion 中加入正则表达式 ^1\.24.*(其余保持不变)。用 istioctl validate 确认它能通过;为了弄清这个实验中的代理版本,请在 /root/ist2-ef 中用 istioctl kube-inject 对 /opt/lab/fixtures/istio/inject-target.yaml 进行注入,并保存为 /root/ist2-ef/inject.yaml(要把三个注入配置文件 /opt/istio/inject-config.yaml、mesh-config.yaml、values-config.yaml 全部传入)。在 /root/ist2-ef/06-version.txt 中写四行——regex=(原样写下 ef-pinned.yaml 中的正则表达式)、proxy_version=(istio-proxy 镜像的标签)、matches_proxy=(该版本匹配正则表达式则写 yes,否则写 no)、matches_1_25_0=(假设的版本 1.25.0 匹配则写 yes,否则写 no)。
代理连接时会报告自己的版本(ISTIO_VERSION 元数据),istiod 把它与这个正则表达式比对,只有匹配时才会附加补丁。升级之后,如果 Envoy 的过滤器名称或配置形态发生变化,旧补丁可能会弄坏新代理;而限定了版本之后,新代理上根本不会附加补丁。点在正则表达式中表示任意字符,所以用 \. 转义,并用 ^ 固定开头。判别可以用 grep -E 来试。注入产物末尾有空文档,所以 yq 请用 select(.kind=="Deployment") 过滤。
用不带选择器的 EnvoyFilter 对整个命名空间设置请求限制
在 /root/ist2-ef/ef-ratelimit.yaml 中编写 EnvoyFilter——名称 inbound-ratelimit,命名空间 default,不使用 workloadSelector,补丁形态与 ef.yaml 相同(HTTP_FILTER、SIDECAR_INBOUND、router 之前 INSERT_BEFORE),值的名称为 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: 1、tokens_per_fill: 1、fill_interval: 300s,filter_enabled 和 filter_enforced 的 default_value 都是 numerator: 100、denominator: HUNDRED。用 istioctl validate 确认没有关于值的警告之后,把这个值放在 router 之前的 Envoy 配置写入 /root/ist2-ef/envoy-rl.yaml(管理端口、监听器、集群与第 3 步相同)。启动并发送三次 curl localhost:10089/reviews 之后,在 /root/ist2-ef/07-ratelimit.txt 中写四行——codes=(三个响应状态码,用逗号分隔)、rate_limited=(统计 http_local_rate_limit.rate_limited 的值)、scope=(这个 EnvoyFilter 作用的范围:workload、namespace、mesh 之一)、mesh_wide_namespace=(如果把同一个文件放进去,就会作用于整个网格的命名空间,取自 /opt/istio/mesh-config.yaml)。
如果有 workloadSelector,只会附加到标签匹配的工作负载;没有的话,会附加到该命名空间的所有工作负载;如果放在网格配置的根命名空间,则会附加到整个网格。根命名空间的先应用,工作负载命名空间的后应用。请不要在请求限制的值中放入 runtime_key——这个版本的 Envoy 会拒绝。如果只有一个令牌,而且补充的间隔很长,就只有第一个请求能通过。请亲自查看被限制的响应的状态码是什么。
整理成使用 EnvoyFilter 之前的检查清单
在 /root/ist2-ef/08-report.md 中写入 before_status=、after_envoy_rc=、badfield_istioctl_rc=、ratelimit_codes= 四行(依次为第 3 步中 fault 返回的状态码、第 4 步中 Envoy 的退出码、第 5 步中 istioctl 的退出码、第 7 步的三个响应状态码),并在下面写至少四行以 - 开头的说明。
数值请从前面步骤的文件中抄录。说明行中写下“合并 EnvoyFilter 之前要确认什么”,这张表就成了评审检查清单——补丁作用的位置、router 的位置、istioctl 发现不了的问题、版本限定、作用范围。