推算韧性配置的取值
目标
一边亲手编写重试、超时、熔断器(circuit breaker)、故障注入、镜像,一边理解每个值会对彼此施加什么约束。
为什么重要
复制来的弹性配置通常不能工作,更糟的是会放大故障。如果在调用链的每一层都配置重试,三层调用链在最坏情况下会有 9 倍、四层则有 27 倍的请求涌向最内层的服务。对于已经奄奄一息的服务,重试就成了最后一击。
超时也一样。timeout 是包含重试和 backoff 在内的整体预算,perTryTimeout 是每次尝试的上限。如果快速失败,即使整体预算小于各次尝试上限之和,也可以重试。反过来,如果一次尝试用掉了整个预算,下一次尝试可能不会开始。本实验中的 3 秒/1 秒/额外 2 次,是用来编写配置的值,并不保证三次尝试各自恰好执行 1 秒。实际次数必须同时观测失败条件、响应时间和 backoff。外层调用方即使结束了等待,也不能保证内层的业务处理被取消,所以要另行设计去重和取消传播,避免已经处理过的写操作被重新执行。
熔断器的值太大,事实上等于关闭;太小,则平时也会出现 503。所以要以实测的并发度为基准来估算,并用 maxEjectionPercent 设置安全阀。本实验就是把这种感觉落到清单中的训练。
Kubernetes 基本资源要真实 apply,Istio CRD 则写成 /root/ica-resilience/ 下的文件。这是基于 kwok 的设计实验,所以不要把它当作已经验证了真实 Envoy 的 HTTP 响应、重试次数、业务重复处理。
步骤
- 创建命名空间
ica-resilience,并添加标签istio-injection=enabled。 - 在命名空间
ica-resilience中,把 Deploymentinventory以 3 个副本部署。Pod 标签是app=inventory,镜像是nginx:1.27-alpine。 - 在同一个命名空间中创建两个 Service:
inventory和inventory-canary,两者的端口都是8080,端口名称都是http。inventory的 selector 是app=inventory。 - 在
/root/ica-resilience/vs-inventory-retry.yaml中编写 VirtualService。spec.hosts[0]是inventory.ica-resilience.svc.cluster.local,http 规则中要包含timeout: 3s、retries.attempts: 2、retries.perTryTimeout: 1s,retries.retryOn中要包含5xx和connect-failure。 - 在
/root/ica-resilience/vs-inventory-fault.yaml中编写 VirtualService。共有两条规则。第一条规则只适用于请求头x-chaos-test为true的请求,并带有fault.delay.fixedDelay: 3s/fault.delay.percentage.value: 50/fault.abort.httpStatus: 503/fault.abort.percentage.value: 10。第二条规则没有 match 也没有 fault,只做路由。 - 在
/root/ica-resilience/dr-inventory.yaml中编写 DestinationRule。spec.host是inventory.ica-resilience.svc.cluster.local,在trafficPolicy下放置outlierDetection(consecutive5xxErrors 5、interval 10s、baseEjectionTime 30s、maxEjectionPercent 50)和connectionPool(tcp.maxConnections 100、http.http1MaxPendingRequests 50)。 - 在
/root/ica-resilience/vs-inventory-mirror.yaml中编写 VirtualService。向inventory.ica-resilience.svc.cluster.local路由 100%,同时向inventory-canary.ica-resilience.svc.cluster.local镜像 20%,并把整体timeout设为5s。
参考
retryOn是用逗号连接的字符串,写法如5xx,connect-failure,reset。percentage是带有value字段的对象,不能只写一个数字。- 常见错误 1:没有为故障注入规则设置 catch-all,导致没有请求头的普通流量无处可去。
- 常见错误 2:给镜像设置两个 weight。镜像会丢弃响应,所以真正的 route 只有一个。
- 镜像请求同样会引发写数据库这类副作用。要镜像写入路径,目标应用必须以影子模式(shadow mode)运行。
准备实验命名空间
创建命名空间 ica-resilience,并添加标签 istio-injection=enabled。
与前面的实验方式相同。别忘了自动注入标签。
部署有 3 个实例的 Deployment
在命名空间 ica-resilience 中,把 Deployment inventory 以 3 个副本部署。Pod 标签是 app=inventory,镜像是 nginx:1.27-alpine。
要想体会 outlierDetection 的 maxEjectionPercent,必须有多个实例。请等 Pod 变为 Ready 之后再评分。
创建主服务和金丝雀服务
在同一个命名空间中创建两个 Service:inventory 和 inventory-canary,两者的端口都是 8080,端口名称都是 http。inventory 的 selector 是 app=inventory。
把镜像目标放在单独的 Service 中更安全。两个 Service 的端口名称都要让协议一目了然。
协调超时与重试的关系
在 /root/ica-resilience/vs-inventory-retry.yaml 中编写 VirtualService。spec.hosts[0] 是 inventory.ica-resilience.svc.cluster.local,http 规则中要包含 timeout: 3s、retries.attempts: 2、retries.perTryTimeout: 1s,retries.retryOn 中要包含 5xx 和 connect-failure。
timeout 是包含重试在内的整体截止时间。perTryTimeout x (attempts + 1) 超过 timeout 时,重试就来不及执行。retryOn 中也要包含请求到达之前就失败的情况。
用请求头控制的故障注入
在 /root/ica-resilience/vs-inventory-fault.yaml 中编写 VirtualService。共有两条规则。第一条规则只适用于请求头 x-chaos-test 为 true 的请求,并带有 fault.delay.fixedDelay: 3s / fault.delay.percentage.value: 50 / fault.abort.httpStatus: 503 / fault.abort.percentage.value: 10。第二条规则没有 match 也没有 fault,只做路由。
在生产集群中对全部流量注入 fault,不是混沌测试,而是故障。请把匹配规则和普通流量规则分开,fault 只放在匹配到的那一条上。
确定熔断器的上限
在 /root/ica-resilience/dr-inventory.yaml 中编写 DestinationRule。spec.host 是 inventory.ica-resilience.svc.cluster.local,在 trafficPolicy 下放置 outlierDetection(consecutive5xxErrors 5、interval 10s、baseEjectionTime 30s、maxEjectionPercent 50)和 connectionPool(tcp.maxConnections 100、http.http1MaxPendingRequests 50)。
connectionPool 和 outlierDetection 并排放在同一个 DestinationRule 的 trafficPolicy 下。请想一想,实例只有 3 个时,把同时驱逐比例设为 100 会怎样。
放出影子流量
在 /root/ica-resilience/vs-inventory-mirror.yaml 中编写 VirtualService。向 inventory.ica-resilience.svc.cluster.local 路由 100%,同时向 inventory-canary.ica-resilience.svc.cluster.local 镜像 20%,并把整体 timeout 设为 5s。
镜像会丢弃响应,所以真正的 route 只放一个。镜像目标是第 3 步创建的金丝雀服务,比例用 mirror 旁边的单独字段指定。