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

ICA — Istio 认证助理

往哪儿发,以及到了之后怎么处理

在 TT Lab 中继续学习

一句话总结

VirtualService 决定发往哪里,DestinationRule 决定到达之后怎样处理。金丝雀(Canary)发布不起作用的事故,有一半是因为只做了这种分工的一半。

为什么需要它

出发点是想把部署与发布分开。把新版本放上集群(部署)和把用户流量送过去(发布)如果能分别控制,出了问题,只要把权重降为 0,就不用回退镜像。

然而这两件事性质不同。“只让 10% 走 v2”这样的分配是以请求为单位的决定,而“v2 就是带有 version=v2 标签的那些 Pod”这种定义,则是以目的地为单位的事实。Istio 把它们拆成了两个资源。

由此产生了考试中原样会考的不对称。subset 只存在于 DestinationRule 中,weight 只存在于 VirtualService 中。如果没有 DestinationRule,而在 VirtualService 中引用 subset: v2,Envoy 中就不会创建这样的 cluster,请求会收到 503。访问日志的响应标志会记为 UH(no healthy upstream)。

工作原理

路由规则按从上到下评估,第一个匹配者胜出。所以把具体的规则放在上面,把通用规则放在最下面。如果最后没有没有 match 的规则(catch-all),没有命中任何规则的请求,就会带着 NR(no route)标志变成 404 或 503。

匹配条件的结合规则也经常弄错。

# AND: 경로가 /admin 이면서 동시에 헤더도 맞아야 매칭
- match:
    - uri:
        prefix: /admin
      headers:
        x-role:
          exact: admin

# OR: 경로가 /admin 이거나, 헤더가 맞으면 매칭
- match:
    - uri:
        prefix: /admin
    - headers:
        x-role:
          exact: admin

一个 match 块内部的条件是 AND,match 数组的条目之间是 OR。缩进差一格,策略的含义就会颠倒。

复原力配置,与其背固定倍数,不如把时间预算和重试条件分开来读。attempts: 2 不是包含原始请求的两次,而是最多额外重试两次,也就是发往上游的请求最多三次。perTryTimeout 是原始请求和每次重试的时间上限,timeout 是包含重试和等待在内的整个请求的限制。并不是每次尝试都一定要等到上限。

即使整体限制和单次尝试限制都是 1 秒,快速失败也不同。明确写出 retryOn: "503",而真实的上游立即返回 503,就可以在整体 1 秒内执行重试。反过来,如果第一次尝试没有响应就耗尽了整体预算,就没有时间留给下一次尝试。所以,不能仅凭整体限制小于 perTryTimeout × (attempts + 1),就断定不可能重试。如果是想让所有尝试各自都有上限那么长的时间的预算,就不仅要考虑这个乘积,还要考虑尝试之间的 backoff。

实际次数取决于失败条件、发生时间点、backoff 和剩余预算。在 Istio 1.31.0 的另行实测中,同样是 1 秒/1 秒/额外 2 次的配置,快速 503 到达了上游三次,耗时 1.5 秒的响应到达了一次。不要通过最终的 HTTP 状态码去推测次数,要把请求标识符和上游的接收记录对照。代理把连接错误变成 503 的情况,与真实上游的 503 不同,所以仅凭 retryOn: "503" 并不能保证同样的重试。

单次尝试的限制要看正常延迟分布和调用方的剩余预算来定,重试要在确认了业务重复处理的安全性之后再开启。仅凭名字叫 GET,并不能保证实现没有副作用,也没有“对 POST 重试一次左右是安全的”这种规则。即使没有收到响应,服务端也可能已经写入了。在合成订单的实测中,一个成功响应之后留下了三次写入,而对同一个请求键做了原子去重的对照组中只留下一次。不要把单进程内存对照组解读为生产环境用的 exactly-once 保证。

正式定义请在 VirtualService 的 HTTPRetry 中确认。下面写文件的实验练习的是配置结构,本说明中的 HTTP 观测是在另外的真实 sidecar VM 上完成的。

重试要从整条调用链的角度来看。A → B → C 中,如果每一层都有 attempts 2,C 收到的请求在最坏情况下是 3 x 3 = 9 倍。四层则是 27 倍。重试对濒临崩溃的服务来说无异于钉上棺材钉,所以重试只放在调用链的一层(尽量放在最外层)。

熔断器不是分散在两个资源里,而是集中在 DestinationRule 一处。connectionPool 决定“能积压多少”,outlierDetection 决定“要把哪些实例摘出去”。在只有 3 个实例的服务上设置 maxEjectionPercent: 100,三个会同时被驱逐,熔断器就成了全面故障。

同一个 DestinationRule 中还有 localityLbSetting。这是先使用同一区域的 endpoint,该区域挂掉后再转向下一个区域的配置。跨区域的流量会带来费用和延迟,所以规模一大就必须开启。

在此有一个考试和现场都会遇到的陷阱。没有 outlierDetection,故障转移就不会发生。转向下一优先级的条件是“当前优先级的 endpoint 不健康”,而做出这一判定的机制就是 outlierDetection。没有它,Envoy 就没有办法知道同一区域的 endpoint 正在挂掉,会一直往那里发送。

症状之所以让人困惑,是因为平时一切正常。同区域优先路由本身工作良好,延迟降低,费用也降低。然后只在某个区域挂掉的那一天不会转移。如果标签(topology.kubernetes.io/zone)有问题,那么同区域优先路由一开始就不会工作,所以症状不同——平时一切正常、只在故障时不行,几乎总是漏掉了 outlierDetection。

在现场相遇的样子

保留作者家庭实验室(homelab)中的一条教训,原样转述。在用 Cilium 搭建的那个集群里,对同一个 IP、同一个端口的 nginx 应用了 rules.http: [{method: GET}] 策略,结果 GET 是 200,POST 是 403。在 L3/L4 层看不到方法,所以这个区分意味着有人解析了 HTTP。在 Istio 中那个人就是 sidecar Envoy,能够做请求头匹配路由和方法匹配,原因也正是如此。

有意思的是当时 Hubble 日志的样子。请求显示为 DROPPED,响应显示为 FORWARDED。因为代理生成的 403 作为正常响应流了出去。Istio 中也一样,连接建立了却返回 403 与连接本身失败,日志的样子不同。前者是 L7 判定,后者是 L4 问题。分不清这两者,就会对着无关的资源看上好几个小时。

下一项实验要做什么

接下来有两个实验。第一个先真实部署命名空间和 v1/v2 工作负载,再把 DestinationRule 和 VirtualService 清单写成文件,熟悉权重、请求头匹配、URI 重写、规则顺序。第二个处理重试与超时的关系、用请求头控制的故障注入、熔断器、镜像。