把 VirtualService 翻译成路由表,验证顺序与重试
目标
按照 VirtualService 转换为 Envoy 路由表的规则,亲手建立路由表,并通过请求确认 Host 头、首个匹配优先、权重和重试。
为什么重要
VirtualService 中的错误,通常不出在某一条规则上,而出在规则之间的顺序和重叠上。静态分析发现不了这类问题,所以必须了解转换规则,并能推演出它在 Envoy 中的行为,才能在变更评审中把它筛出来。
步骤
- 编写
/root/ist2-route/dr.yaml(DestinationRulereviews,host 为reviews.default.svc.cluster.local,子集v1、v2、broken——标签version的值分别与子集同名)和/root/ist2-route/vs.yaml(VirtualServicereviews,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)。 - 服务
reviews位于命名空间default,端口为 9080。把这个 VirtualService 在 sidecar 中生成的路由表的各个名称,分三行写入/root/ist2-route/02-names.txt——route_config=(路由表名称)、virtual_host=(虚拟主机名称)、domains=(该虚拟主机的域名中四个较短的名称,用逗号分隔,顺序不限)。 - 在
/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。 - 向以
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。 - 创建把
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重新启动。 - 把
route.yaml复制为/root/ist2-route/route-split.yaml,只把default路由改成weighted_clusters(v1 为 75,v2 为 25,与 VirtualService 的 default 规则一致)。用--concurrency 1重新启动之后,以 Hostreviews恰好向/发送 40 次请求,并把v1=、v2=、total=三行写入/root/ist2-route/06-split.txt。 - 把
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 集群的两个统计值)。 - 在
/root/ist2-route/08-report.md中写入route_config=、virtual_host=、first_match_wins=(如第 5 步所见,只使用第一个匹配的路由,则写yes)、retry_attempts=(VirtualService 的 attempts 值)四行,并在下面写至少四行以-开头的说明。
参考
- 这个 Pod 中既没有真正的 istiod,也没有真正的 sidecar。所以无法用
istioctl proxy-config查看实际生成的内容,需要了解转换规则,并亲手编写等价的 Envoy 配置来确认行为。同样的规则会原样出现在生产集群的proxy-config输出中。 - 每个请求都要加上
-H 'Host: reviews'。虚拟主机是通过 Host 头来选择的。 - 第 6、7 步要统计分配和重试,请用
--concurrency 1启动 Envoy。统计数据是累积的,所以要在重新启动之后立即统计。 - 启动 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 中必须用引号括起来。
编写含四条路由的 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 时我要确认什么”,这张表在下次变更评审时就派得上用场。