本系列文章将介绍用户从 Spring Cloud,Dubbo 等传统微服务框架迁徙到 Istio 服务网格时的一些教训,以及在应用 Istio 过程中可能遇到的一些常见问题的解决办法。
故障景象
该问题的体现是装置了 sidecar proxy 的利用在启动后的一小段时间内无奈通过网络拜访 pod 内部的其余服务,例如内部的 HTTP,MySQL,Redis 等服务。如果利用没有对依赖服务的异样进行容错解决,该问题还经常会导致利用启动失败。上面咱们以该问题导致的一个典型故障的剖析过程为例对该问题的起因进行阐明。
典型案例:某运维同学反馈:昨天晚上 Istio 环境中利用的心跳检测报 connect reset,而后服务重启了。狐疑是 Istio 环境中网络不稳固导致了服务重启。
故障剖析
依据运维同学的反馈,该 pod 曾多次重启。因而咱们先用 kubectl logs --previous
命令查问 awesome-app 容器最初一次重启前的日志,以从日志中查找其重启的起因。
kubectl logs --previous awesome-app-cd1234567-gzgwg -c awesome-app
从日志中查问到了其重启前最初的错误信息如下:
Logging system failed to initialize using configuration from 'http://log-config-server:12345/******/logback-spring.xml'
java.net.ConnectException: Connection refused (Connection refused)
at java.net.PlainSocketImpl.socketConnect(Native Method)
at java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:350)
at java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:206)
从错误信息能够得悉,利用过程在启动时试图通过 HTTP 协定从配置核心拉取 logback 的配置信息,但该操作因为网络异样失败了,导致利用过程启动失败,最终导致容器重启。
是什么导致了网络异样呢?咱们再用 Kubectl get pod
命令查问 Pod 的运行状态,尝试找到更多的线索:
kubectl get pod awesome-app-cd1234567-gzgwg -oyaml
命令输入的 pod 具体内容如下,该 yaml 片段省略了其余无关的细节,只显示了 lastState 和 state 局部的容器状态信息。
containerStatuses:
- containerID:
lastState:
terminated:
containerID:
exitCode: 1
finishedAt: 2020-09-01T13:16:23Z
reason: Error
startedAt: 2020-09-01T13:16:22Z
name: awesome-app
ready: true
restartCount: 2
state:
running:
startedAt: 2020-09-01T13:16:36Z
- containerID:
lastState: {}
name: istio-proxy
ready: true
restartCount: 0
state:
running:
startedAt: 2020-09-01T13:16:20Z
hostIP: 10.0.6.161
从该输入能够看到 pod 中的利用容器 awesome-app 重启了两次。整顿该 pod 中 awesome-app 利用容器和 istio-proxy sidecar 容器的启动和终止的工夫程序,能够失去上面的工夫线:
- 2020-09-01T13:16:20Z istio-proxy 启动
- 2020-09-01T13:16:22Z awesome-app 上一次启动工夫
- 2020-09-01T13:16:23Z awesome-app 上一次异样退出工夫
- 2020-09-01T13:16:36Z awesome-app 最初一次启动,当前就始终失常运行
能够看到在 istio-proxy 启动 2 秒后,awesome-app 启动,并于 1 秒后异样退出。联合后面的日志信息,咱们晓得这次启动失败的间接起因是利用拜访配置核心失败导致。在 istio-proxy 启动 16 秒后,awesome-app 再次启动,这次启动胜利,之后始终失常运行。
istio-proxy 启动和 awesome-app 上一次异样退出的工夫距离很短,只有 2 秒钟,因而咱们根本能够判断此时 istio-proxy 尚未启动初始化实现,导致 awesome-app 不能通过 istio-proxy 连贯到内部服务,导致其启动失败。待 awesome-app 于 2020-09-01T13:16:36Z 再次启动时,因为 istio-proxy 曾经启动了较长时间,实现了从 pilot 获取动静配置的过程,因而 awesome-app 向 pod 内部的网络拜访就失常了。
如下图所示,Envoy 启动后会通过 xDS 协定向 pilot 申请服务和路由配置信息,Pilot 收到申请后会依据 Envoy 所在的节点(pod 或者 VM)组装配置信息,包含 Listener、Route、Cluster 等,而后再通过 xDS 协定下发给 Envoy。依据 Mesh 的规模和网络状况,该配置下发过程须要数秒到数十秒的工夫。因为初始化容器曾经在 pod 中创立了 Iptables rule 规定,因而这段时间内利用向外发送的网络流量会被重定向到 Envoy,而此时 Envoy 中尚没有对这些网络申请进行解决的监听器和路由规定,无奈对此进行解决,导致网络申请失败。(对于 Envoy sidecar 初始化过程和 Istio 流量治理原理的更多内容,能够参考这篇文章 Istio 流量治理实现机制深度解析)
解决方案
在利用启动命令中判断 Envoy 初始化状态
从后面的剖析能够得悉,该问题的根本原因是因为利用过程对 Envoy sidecar 配置初始化的依赖导致的。因而最间接的解决思路就是:在利用过程启动时判断 Envoy sidecar 的初始化状态,待其初始化实现后再启动利用过程。
Envoy 的健康检查接口 localhost:15020/healthz/ready
会在 xDS 配置初始化实现后才返回 200,否则将返回 503,因而能够依据该接口判断 Envoy 的配置初始化状态,待其实现后再启动利用容器。咱们能够在利用容器的启动命令中退出调用 Envoy 健康检查的脚本,如上面的配置片段所示。在其余利用中应用时,将 start-awesome-app-cmd
改为容器中的利用启动命令即可。
apiVersion: apps/v1
kind: Deployment
metadata:
name: awesome-app-deployment
spec:
selector:
matchLabels:
app: awesome-app
replicas: 1
template:
metadata:
labels:
app: awesome-app
spec:
containers:
- name: awesome-app
image: awesome-app
ports:
- containerPort: 80
command: ["/bin/bash", "-c"]
args: ["while [[ \"$(curl -s -o /dev/null -w ''%{http_code}'' localhost:15020/healthz/ready)\"!='200']]; do echo Waiting for Sidecar;sleep 1; done; echo Sidecar available; start-awesome-app-cmd"]
该流程的执行程序如下:
- Kubernetes 启动 利用容器。
- 利用容器启动脚本中通过
curl get localhost:15020/healthz/ready
查问 Envoy sidcar 状态,因为此时 Envoy sidecar 尚未就绪,因而该脚本会一直重试。 - Kubernetes 启动 Envoy sidecar。
- Envoy sidecar 通过 xDS 连贯 Pilot,进行配置初始化。
- 利用容器启动脚本通过 Envoy sidecar 的健康检查接口判断其初始化曾经实现,启动利用过程。
该计划尽管能够躲避依赖程序的问题,但须要对利用容器的启动脚本进行批改,对 Envoy 的衰弱状态进行判断。更现实的计划应该是利用对 Envoy sidecar 不感知。
通过 pod 容器启动程序进行管制
通过浏览 Kubernetes 源码,咱们能够发现当 pod 中有多个容器时,Kubernetes 会在一个线程中顺次启动这些容器,如上面的代码片段所示:
// Step 7: start containers in podContainerChanges.ContainersToStart.
for _, idx := range podContainerChanges.ContainersToStart {start("container", containerStartSpec(&pod.Spec.Containers[idx]))
}
因而咱们能够在向 pod 中注入 Envoy sidecar 时将 Envoy sidecar 放到利用容器之前,这样 Kubernetes 会先启动 Envoy sidecar,再启动利用容器。然而还有一个问题,Envoy 启动后咱们并不能立刻启动利用容器,还须要期待 xDS 配置初始化实现。这时咱们就能够采纳容器的 postStart lifecycle hook 来达成该目标。Kubernetes 会在启动容器后调用该容器的 postStart hook,postStart hook 会阻塞 pod 中的下一个容器的启动,直到 postStart hook 执行实现。因而如果在 Envoy sidecar 的 postStart hook 中对 Envoy 的配置初始化状态进行判断,待实现初始化后再返回,就能够保障 Kubernetes 在 Envoy sidecar 配置初始化实现后再启动利用容器。该流程的执行程序如下:
- Kubernetes 启动 Envoy sidecar。
- Kubernetes 执行 postStart hook。
- postStart hook 通过 Envoy 健康检查接口判断其配置初始化状态,直到 Envoy 启动实现。
- Kubernetes 启动利用容器。
Istio 曾经在 1.7 中合入了该修复计划,参见 Allow users to delay application start until proxy is ready #24737。
插入 sidecar 后的 pod spec 如上面的 yaml 片段所示。postStart hook 配置的 pilot-agent wait
命令会继续调用 Envoy 的健康检查接口 ‘/healthz/ready’ 查看其状态,直到 Envoy 实现配置初始化。这篇文章 Delaying application start until sidecar is ready 中介绍了更多对于该计划的细节。
apiVersion: v1
kind: Pod
metadata:
name: sidecar-starts-first
spec:
containers:
- name: istio-proxy
image:
lifecycle:
postStart:
exec:
command:
- pilot-agent
- wait
- name: application
image: my-application
该计划在不对利用进行批改的状况下比拟完满地解决了利用容器和 Envoy sidecar 初始化的依赖问题。然而该解决方案对 Kubernetes 有两个隐式依赖条件:Kubernetes 在一个线程中按定义程序顺次启动 pod 中的多个容器,以及前一个容器的 postStart hook 执行结束后再启动下一个容器。这两个前提条件在目前的 Kuberenetes 代码实现中是满足的,但因为这并不是 Kubernetes 的 API 标准,因而该前提在未来 Kubernetes 降级后很可能被突破,导致该问题再次出现。
Kubernetes 反对定义 pod 中容器之间的依赖关系
为了彻底解决该问题,防止 Kubernetes 代码变动后该问题再次出现,更正当的形式应该是由 Kubernetes 反对显式定义 pod 中一个容器的启动依赖于另一个容器的衰弱状态。目前 Kubernetes 中曾经有一个 issue Support startup dependencies between containers on the same Pod #65502 对该问题进行跟踪解决。如果 Kubernetes 反对了该个性,则该流程的执行程序如下:
- Kubernetes 启动 Envoy sidecar 容器。
- Kubernetes 通过 Envoy sidecar 容器的 readiness probe 查看其状态,直到 readiness probe 反馈 Envoy sidecar 曾经 ready,即曾经初始化结束。
- Kubernetes 启动利用容器。
解耦应用服务之间的启动依赖关系
以上几个解决方案的思路都是管制 pod 中容器的启动程序,在 Envoy sidecar 初始化实现后再启动利用容器,以确保利用容器启动时可能通过网络失常拜访其余服务。但这些计划只是『头痛医头,脚痛医脚』, 是治标不治本的办法。因为即便 pod 中对外的网络拜访没有问题,利用容器依赖的其余服务也可能因为尚未启动,或者某些问题而不能在此时失常提供服务。要彻底解决该问题,咱们须要解耦应用服务之间的启动依赖关系,使利用容器的启动不再强依赖其余服务。
在一个微服务零碎中,原单体利用中的各个业务模块被拆分为多个独立过程(服务)。这些服务的启动程序是随机的,并且服务之间通过不牢靠的网络进行通信。微服务多过程部署、跨过程网络通信的特定决定了服务之间的调用出现异常是一个常见的状况。为了应答微服务的该特点,微服务的一个根本的设计准则是 “design for failure”,即须要以优雅的形式应答可能呈现的各种异常情况。当在微服务过程中不能拜访一个依赖的内部服务时,须要通过重试、降级、超时、断路等策略对异样进行容错解决,以尽可能保证系统的失常运行。
Envoy sidecar 初始化期间网络临时不能拜访的状况只是放大了微服务零碎未能正确处理服务依赖的问题,即便解决了 Envoy sidecar 的依赖程序,该问题仍然存在。例如在本案例中,配置核心也是一个独立的微服务,当一个依赖配置核心的微服务启动时,配置核心有可能尚未启动,或者尚未初始化实现。在这种状况下,如果在代码中没有对该异常情况进行解决,也会导致依赖配置核心的微服务启动失败。在一个更为简单的零碎中,多个微服务过程之间可能存在网状依赖关系,如果没有依照 “design for failure” 的准则对微服务进行容错解决,那么只是将整个系统启动起来就将是一个微小的挑战。对于本例而言,能够采纳一个相似这样的简略容错策略:先用一个缺省的 logback 配置启动利用过程,并在启动后对配置核心进行重试,待连贯上配置核心后,再应用配置核心下发的配置对 logback 进行设置。
小结
利用容器对 Envoy Sidecar 启动依赖问题的典型体现是利用容器在刚启动的一小段时间内调用内部服务失败。起因是此时 Envoy sidecar 尚未实现 xDS 配置的初始化,因而不能为利用容器转发网络申请。该调用失败可能导致利用容器不能失常启动。此问题的根本原因是微服务利用中对依赖服务的调用失败没有进行正当的容错解决。对于遗留零碎,为了尽量避免对利用的影响,咱们能够通过在利用启动命令中判断 Envoy 初始化状态的计划,或者降级到 Istio 1.7 来缓解该问题。但为了彻底解决服务依赖导致的谬误,倡议参考 “design for failure” 的设计准则,解耦微服务之间的强依赖关系,在呈现临时不能拜访一个依赖的内部服务的状况时,通过重试、降级、超时、断路等策略进行解决,以尽可能保证系统的失常运行。
参考文档
- App container unable to connect to network before sidecar is fully running #11130(https://github.com/istio/isti…
- Delaying application start until sidecar is ready(https://medium.com/@marko.luk…
- Kubernetes Container Lifecycle Hooks(https://kubernetes.io/docs/co…
- Istio 流量治理实现机制深度解析 (https://zhaohuabing.com/post/…
【腾讯云原生】云说新品、云研新术、云游新活、云赏资讯,扫码关注同名公众号,及时获取更多干货!!