规则看似都对,请求却走错地方的原因
一句话总结
VirtualService 的 http[] 会变成 Envoy 路由表的 routes[],连顺序也原样保留。表名是服务端口,虚拟主机名是 FQDN:포트,weight 对应 weighted_clusters,retries 对应 retry_policy。转换如此直白,所以 VirtualService 中的错误会在 Envoy 中原样重现。
为什么需要它
修改 VirtualService 之后,很多人说不清“这个请求为什么会去 v1”。只看 YAML,每条规则似乎都没问题。问题通常出在规则之间的关系:前面范围宽的规则遮住了后面范围窄的规则,或者两条规则都匹配同一个请求,而与本意不符的那一条写在了前面。这类错误 istioctl analyze 发现不了。analyze 只检查每条规则指向的子集和网关是否存在,不会检查规则之间的遮挡。
所以需要了解转换规则。了解规则之后,读 VirtualService 时脑中就能画出 Envoy 的路由表;在生产环境中,则可以把 istioctl proxy-config routes 的输出和 VirtualService 逐行对照。
工作原理
出站一侧的 sidecar 为每个端口设置一张路由表。如果有多个服务使用同一个 9080 端口,这张表中每个服务对应一个虚拟主机,并根据请求的 Host 头来选择。
| VirtualService | Envoy |
|---|---|
| 服务端口 9080 | route_config.name: "9080" |
hosts: [reviews] |
虚拟主机 reviews.default.svc.cluster.local:9080,domains 中有 reviews、reviews.default、reviews.default.svc 和 FQDN |
http[i].match |
routes[i].match(prefix、headers 等) |
http[i].route[].destination.subset |
route.cluster: outbound|9080|v1|… |
weight |
route.weighted_clusters |
retries.attempts / retryOn |
retry_policy.num_retries / retry_on |
timeout |
route.timeout |
如果请求的 Host 不在任何一个虚拟主机的 domains 中,就会返回 404。在表内,从上往下只使用第一个匹配的路由。如果前面有无条件的规则(catch-all),后面的规则就永远不会被用到。
权重是每个请求各掷一次骰子。按 75 比 25 发送四十次,没有恰好得到 30 比 10 是正常的。重试会在原始请求之外最多再追加 attempts 次,因此上游会被请求 1+attempts 次。即使没有在 VirtualService 中写 retries,Istio 也会加入默认重试(2 次,针对连接失败、被拒绝等情况),并同时附上避开同一主机的 previous_hosts 谓词。
在现场相遇的样子
把新规则追加到列表末尾,却毫无效果。原因是末尾已经有一条 catch-all。评审时,要把“新规则是否在 catch-all 之前”作为一条一行的检查项。
故障期间,上游请求量暴增到四倍。如果调用链中的多个环节各自设置了重试,请求量会成倍增加。重试只在调用链的一处设置,并搭配较短的 perTryTimeout。
用过 Envoy 的人会期待 15 秒超时。Envoy 路由的默认超时是 15 秒,但如果 VirtualService 中没有 timeout,Istio 会在路由中写入 0s(即关闭超时)。所以 sidecar 之后的慢调用会一直等下去。如果需要等待的上限,就必须写在 VirtualService 中,写入的值会原样出现在路由的 timeout 中。
看不到 1% 的金丝雀(Canary)。只用几十个请求来确认,v2 可能一次都不会出现。比例要通过统计,并用足够多的请求次数来看。
官方文档:VirtualService · Debugging Envoy and Istiod
下一项实验要做什么
写出 VS 和 DR 并通过分析,然后按规则推导出路由表、虚拟主机和域名,把 Envoy 运行起来。观察由 Host 头选择路由表的过程,以及请求头匹配与路径匹配重叠时先写的那条获胜;确认 catch-all 放在前面的错误既逃过了 analyze,又被 Envoy 原样执行。最后统计权重和重试。