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

Istio 进阶 — 为什么会那样流

把 VirtualService 翻译成路由表,验证顺序与重试

在 TT Lab 中继续学习

目标

按照 VirtualService 转换为 Envoy 路由表的规则,亲手建立路由表,并通过请求确认 Host 头、首个匹配优先、权重和重试。

为什么重要

VirtualService 中的错误,通常不出在某一条规则上,而出在规则之间的顺序和重叠上。静态分析发现不了这类问题,所以必须了解转换规则,并能推演出它在 Envoy 中的行为,才能在变更评审中把它筛出来。

步骤

  1. 编写 /root/ist2-route/dr.yaml(DestinationRule reviews,host 为 reviews.default.svc.cluster.local,子集 v1、v2、broken——标签 version 的值分别与子集同名)和 /root/ist2-route/vs.yaml(VirtualService reviews,hosts 为 [reviews])。vs.yaml 的 http 按以下顺序共有四条——jason(请求头 end-user 恰好等于 jason 时发往 v2)、api(路径前缀为 /api 时发往 v1)、flaky(路径前缀为 /flaky 时发往 broken,并带有 retries: {attempts: 3, retryOn: 5xx} 和 timeout: 2s)、default(v1 的 weight 为 75,v2 为 25)。把 istioctl analyze --use-kube=false vs.yaml dr.yaml 的输出和退出码保存到 /root/ist2-route/01-analyze.txt(最后一行为 rc=0)。
  2. 服务 reviews 位于命名空间 default,端口为 9080。把这个 VirtualService 在 sidecar 中生成的路由表的各个名称,分三行写入 /root/ist2-route/02-names.txt——route_config=(路由表名称)、virtual_host=(虚拟主机名称)、domains=(该虚拟主机的域名中四个较短的名称,用逗号分隔,顺序不限)。
  3. 在 /root/ist2-route/route.yaml 中编写 Envoy 配置——管理端口 9984,监听器 127.0.0.1:10084,使用第 2 步中的路由表名称、虚拟主机名称和四个 domains。路由为每条命名,并按以下顺序写三条——jason(路径 / 加上请求头 end-user 恰好为 jason → outbound|9080|v2|reviews.default.svc.cluster.local)、api(前缀 /api → outbound|9080|v1|reviews.default.svc.cluster.local)、default(前缀 / → outbound|9080|v1|reviews.default.svc.cluster.local)。集群有两个:v1(127.0.0.1:8105)和 v2(127.0.0.1:8106)。以 ok 模式启动两个上游并启动 Envoy 之后,分别用三种 Host 头请求 /,把 HTTP 状态码按 reviews=、reviews.default.svc.cluster.local=、ratings= 三行写入 /root/ist2-route/03-hosts.txt。
  4. 向以 route.yaml 运行的 Envoy 发送三个 Host 为 reviews 的请求,根据响应正文中的端口判断子集,并写入 /root/ist2-route/04-match.txt——anonymous=(不带请求头,请求 /)、jason=(带 end-user: jason,请求 /)、api_as_jason=(带 end-user: jason,请求 /api/list)。值为 v1 或 v2。
  5. 创建把 default 规则从 vs.yaml 中移到最前面的 /root/ist2-route/vs-bad.yaml,并确认 istioctl analyze --use-kube=false vs-bad.yaml dr.yaml 的退出码。再在 Envoy 中重现同样的错误:用把 default 路由从 route.yaml 中移到最前面的 /root/ist2-route/route-bad.yaml 重新启动 Envoy,并带着 end-user: jason 请求 /。在 /root/ist2-route/05-order.txt 中写入两行:analyze_rc=(vs-bad 分析的退出码)和 jason_after=(在 route-bad 中 jason 被发往的子集)。确认之后,请换回 route.yaml 重新启动。
  6. 把 route.yaml 复制为 /root/ist2-route/route-split.yaml,只把 default 路由改成 weighted_clusters(v1 为 75,v2 为 25,与 VirtualService 的 default 规则一致)。用 --concurrency 1 重新启动之后,以 Host reviews 恰好向 / 发送 40 次请求,并把 v1=、v2=、total= 三行写入 /root/ist2-route/06-split.txt。
  7. 把 route-split.yaml 复制为 /root/ist2-route/route-retry.yaml,并追加两处内容——集群 outbound|9080|broken|reviews.default.svc.cluster.local(127.0.0.1:8114,始终返回 503 的上游),以及位于 default 之前的 flaky 路由(前缀 /flaky → 该集群,timeout: 2s,retry_policy: {retry_on: 5xx, num_retries: 3})。以 fail 模式在 8114 上启动上游,并重新启动 Envoy,然后只请求一次 /flaky。在 /root/ist2-route/07-retry.txt 中写入 status=(HTTP 状态码)、upstream_rq_retry= 和 upstream_rq_total=(broken 集群的两个统计值)。
  8. 在 /root/ist2-route/08-report.md 中写入 route_config=、virtual_host=、first_match_wins=(如第 5 步所见,只使用第一个匹配的路由,则写 yes)、retry_attempts=(VirtualService 的 attempts 值)四行,并在下面写至少四行以 - 开头的说明。

参考

编写含四条路由的 VirtualService,并通过分析

编写 /root/ist2-route/dr.yaml(DestinationRule reviews,host 为 reviews.default.svc.cluster.local,子集 v1、v2、broken——标签 version 的值分别与子集同名)和 /root/ist2-route/vs.yaml(VirtualService reviews,hosts 为 [reviews])。vs.yaml 的 http 按以下顺序共有四条——jason(请求头 end-user 恰好等于 jason 时发往 v2)、api(路径前缀为 /api 时发往 v1)、flaky(路径前缀为 /flaky 时发往 broken,并带有 retries: {attempts: 3, retryOn: 5xx} 和 timeout: 2s)、default(v1 的 weight 为 75,v2 为 25)。把 istioctl analyze --use-kube=false vs.yaml dr.yaml 的输出和退出码保存到 /root/ist2-route/01-analyze.txt(最后一行为 rc=0)。

VirtualService 决定“发往哪里”,DestinationRule 决定“发送之后如何处理,以及子集的定义”。analyze 会同时读取这两个文件,检查 VS 指向的子集在 DR 中是否存在这类引用关系。如果给每个 http 条目都加上 name,Envoy 一侧的路由也会带上同样的名称,日后便于查找。

推导路由表、虚拟主机和域名的名称

服务 reviews 位于命名空间 default,端口为 9080。把这个 VirtualService 在 sidecar 中生成的路由表的各个名称,分三行写入 /root/ist2-route/02-names.txt——route_config=(路由表名称)、virtual_host=(虚拟主机名称)、domains=(该虚拟主机的域名中四个较短的名称,用逗号分隔,顺序不限)。

在出站一侧,sidecar 为每个端口设置一张路由表。如果有多个服务使用同一个 9080 端口,同一张表中每个服务对应一个虚拟主机,并根据请求的 Host 头来选择。所以表名是端口,虚拟主机名是 FQDN:포트(占位符为端口)。同一命名空间中的应用也会使用 reviews、reviews.default 这样的简称,所以 domains 中也会包含从 FQDN 的末尾逐级截去后得到的名称(可参考官方文档中的 proxy-config 示例)。

按这些名称建立路由表,并观察通过 Host 头选择

在 /root/ist2-route/route.yaml 中编写 Envoy 配置——管理端口 9984,监听器 127.0.0.1:10084,使用第 2 步中的路由表名称、虚拟主机名称和四个 domains。路由为每条命名,并按以下顺序写三条——jason(路径 / 加上请求头 end-user 恰好为 jason → outbound|9080|v2|reviews.default.svc.cluster.local)、api(前缀 /api → outbound|9080|v1|reviews.default.svc.cluster.local)、default(前缀 / → outbound|9080|v1|reviews.default.svc.cluster.local)。集群有两个:v1(127.0.0.1:8105)和 v2(127.0.0.1:8106)。以 ok 模式启动两个上游并启动 Envoy 之后,分别用三种 Host 头请求 /,把 HTTP 状态码按 reviews=、reviews.default.svc.cluster.local=、ratings= 三行写入 /root/ist2-route/03-hosts.txt。

选择路由表的是监听器(端口),在表中选择虚拟主机的则是 Host 头。如果 Host 不在任何一个虚拟主机的 domains 中,就找不到路由而返回 404——这就是 sidecar 遇到不认识的服务名称时的表现。请像 curl -H 'Host: reviews' localhost:10084/ 这样更换请求头反复请求。请求头匹配写在 match.headers 中,格式为 string_match: {exact: …}。

请求头匹配与路径匹配重叠时,谁会获胜

向以 route.yaml 运行的 Envoy 发送三个 Host 为 reviews 的请求,根据响应正文中的端口判断子集,并写入 /root/ist2-route/04-match.txt——anonymous=(不带请求头,请求 /)、jason=(带 end-user: jason,请求 /)、api_as_jason=(带 end-user: jason,请求 /api/list)。值为 v1 或 v2。

Envoy 从上往下查看路由,只使用第一个匹配的路由。api_as_jason 既匹配 jason 规则,也匹配 api 规则,答案取决于哪一条写在前面。正文为 ok:8105 就是 v1,为 ok:8106 就是 v2。

把 catch-all 放在前面——分析毫无动静,流量却走错了

创建把 default 规则从 vs.yaml 中移到最前面的 /root/ist2-route/vs-bad.yaml,并确认 istioctl analyze --use-kube=false vs-bad.yaml dr.yaml 的退出码。再在 Envoy 中重现同样的错误:用把 default 路由从 route.yaml 中移到最前面的 /root/ist2-route/route-bad.yaml 重新启动 Envoy,并带着 end-user: jason 请求 /。在 /root/ist2-route/05-order.txt 中写入两行:analyze_rc=(vs-bad 分析的退出码)和 jason_after=(在 route-bad 中 jason 被发往的子集)。确认之后,请换回 route.yaml 重新启动。

default 没有条件,所以匹配所有请求。如果它在最前面,后面的规则就永远不会被用到。但是 analyze 只检查各条规则引用的对象是否存在,不检查规则之间的遮挡,所以会说一切正常。因此修改 VirtualService 时,“把范围宽的放在后面”必须靠人来遵守。可以像 yq '.spec.http |= [.[3], .[0], .[1], .[2]]' 这样调换列表顺序。

weight 会变成 weighted_clusters——数四十次

把 route.yaml 复制为 /root/ist2-route/route-split.yaml,只把 default 路由改成 weighted_clusters(v1 为 75,v2 为 25,与 VirtualService 的 default 规则一致)。用 --concurrency 1 重新启动之后,以 Host reviews 恰好向 / 发送 40 次请求,并把 v1=、v2=、total= 三行写入 /root/ist2-route/06-split.txt。

权重是每个请求各掷一次骰子,所以四十次请求不会恰好得到 30 比 10。只要总数是 40,并且 v1 更多,就是正确的。在 Istio 中把金丝雀比例设为 1%,却只用几十个请求来确认,v2 可能一次都不会出现,原因也是一样的。

retries 和 timeout 会变成 retry_policy——数一数敲了几次

把 route-split.yaml 复制为 /root/ist2-route/route-retry.yaml,并追加两处内容——集群 outbound|9080|broken|reviews.default.svc.cluster.local(127.0.0.1:8114,始终返回 503 的上游),以及位于 default 之前的 flaky 路由(前缀 /flaky → 该集群,timeout: 2s,retry_policy: {retry_on: 5xx, num_retries: 3})。以 fail 模式在 8114 上启动上游,并重新启动 Envoy,然后只请求一次 /flaky。在 /root/ist2-route/07-retry.txt 中写入 status=(HTTP 状态码)、upstream_rq_retry= 和 upstream_rq_total=(broken 集群的两个统计值)。

VirtualService 的 retries.attempts 对应 Envoy 的 num_retries,retryOn 对应 retry_on,timeout 对应路由的 timeout。重试会在原始请求之外最多再追加 attempts 次,所以上游会被请求 1+attempts 次——这就是对已经出故障的服务,重试会使负载成倍增加的原因。统计数据是累积的,所以重新启动之后只请求一次,数字才干净。请用 /stats?filter= 挑出 broken 集群的两个值。

整理成 VirtualService 转换对照表

在 /root/ist2-route/08-report.md 中写入 route_config=、virtual_host=、first_match_wins=(如第 5 步所见,只使用第一个匹配的路由,则写 yes)、retry_attempts=(VirtualService 的 attempts 值)四行,并在下面写至少四行以 - 开头的说明。

数值请从前面步骤的文件中抄录。说明行中写下“修改 VirtualService 时我要确认什么”,这张表在下次变更评审时就派得上用场。