kubernetes 在 pod 的生命周期中引入探针机制来判断服务的健康状态。
Liveness 探针顾名思义是用来探测服务的生存状态,若是 Liveness 探针连续失败次数超过设定的阈值,则 kubelet 会 kill 掉该 pod。 Readiness 探针用来判断服务是否准备好接收流量和负载,按照官方文档说明,Readiness 探针连续失败后,将从 service 中摘除该 endpoint,再也不承载流量。但实际上它还有另一个做用,就在更新 deployment 或 replica set 时,Liveness 探针决定了 controller kill 掉旧 pod 的时机,这一点在后面的例子中咱们再细讲。java
在写这篇博客以前,官方文档看了一遍又一遍,也翻了好多第三方的博客,但仍是没用深刻的理解这两个探针的区别。在生产环境中使用的时候,咱们到底该不应使用这两个探针呢,又该怎么搭配使用这个探针呢?golang
本文基于 kubernetes 1.14 编写,对于 kubernetes 1.16 引入的 startup 探针的功能请查看官方文档。web
少啰嗦,直接上例子。docker
首先咱们构建一个 golang webserver,而后咱们对此服务验证 Liveness Probe 和 Readiness Probe 对此服务的影响。
Dockerfile 以下:api
FROM golang:1.10-alpine3.8 AS build-env WORKDIR /go/src/app COPY . . RUN go build -o /app . FROM alpine:3.8 COPY --from=build-env /app /app COPY --from=build-env /go/src/app/docker/entrypoint.sh /entrypoint.sh ENTRYPOINT ["/entrypoint.sh"] CMD ["/app"]
在 entrypoint 中咱们加入几秒钟的 sleep 来模拟一个启动较慢的应用(或者你能够直接起一个 java 服务),来方便咱们观察 Liveness Probe 和 Readiness Probe 的做用。
entrypoint 以下:bash
#!/bin/bash echo "in entrypoint.sh" echo "sleep start" sleep 10 echo "sleep over" exec "$@"
依据此 Dockerfile 构建镜像 example.com/server:latest, 而后建立一个最基本的 deploy网络
和 service ,咱们的试验就开始啦。并发
apiVersion: extensions/v1beta1 kind: Deployment metadata: name: server labels: app: server spec: replicas: 1 selector: matchLabels: app: server template: metadata: labels: app: server spec: containers: - name: app image: example.com/server:latest ports: - name: app-tcp containerPort: 8000 protocol: TCP --- apiVersion: v1 kind: Service metadata: name: server labels: app: server spec: selector: app: server ports: - port: 80 protocol: TCP targetPort: 8000
至此,咱们就在集群中运行起来一个服务,它具备以下特征:app
说干就干。
接下来分别配置几种不一样的探针组合方式来观察其对平常发版(更新 deployment/rs)以及扩容,缩容操做:tcp
而后在发版的同时,用压测工具模拟必定并发请求去访问服务,而后统计不一样场景下的请求成功率。
压测工具选用 hey
,须要注意的是,这里使用压测工具并非为了测试系统的负载能力,而单纯是为了模拟
必定并发量,观察系统行为。
# qps 10, 并发 10 ,持续 20s hey -disable-keepalive -t 1 -z 20s -q 10 -c 10 http://server/ping
不配置探针时,kubelet 和 endpoint controller 都默认认为 pod 只要主进程存在,容器即存活,
不会尝试重启 pod, 也不会主动将 pod 的 ip 从 endpoint controller 摘除。
所以,此种场景下 pod 可以正常启动,但启动后流量即打入到新启动的容器。在咱们的场景下,主进程启动 10 秒后服务才能够处理网络请求,所以每次发版会有 10 秒左右的服务不可用时间。
livenessProbe: httpGet: path: /ping port: 8000 scheme: HTTP initialDelaySeconds: 2 timeoutSeconds: 3 periodSeconds: 5 successThreshold: 1 failureThreshold: 3
咱们配置了初始延迟为 2 秒,超时时间为 3 秒,请求间隔为 5 秒,失败阈值为 3 的 liveness 探针,这意味着什么呢?
从 pod 启动2秒钟开始进行探测,每5秒进行一次,连续失败三次后即断定容器不存活,则会杀掉该容器。
2 + 5*3=17, 17 后若是容器还没启动完成,则会被重启!由于咱们的服务启动 10 秒后才能正确的处理 ping
请求,因此咱们的容器将会不断重启,永远没法正常提供服务!
livenessProbe: httpGet: path: /ping port: 8000 scheme: HTTP initialDelaySeconds: 10 timeoutSeconds: 3 periodSeconds: 5 successThreshold: 1 failureThreshold: 3
什么叫适当呢。其实只要你的服务正常启动时间小于 initialDelaySeconds + failureThreshold*periodSeconds 就好。可是这样算有点麻烦,为了简便起见,这里直接让服务启动时间小于 initialDelaySeconds 便可。
这样配置之后,服务是起来了,但咱们测试后,发现仍是有 10 秒左右的不可用时间!
由于 liveness 只是决定了 kubelet 重启 Pod 的时间,可是对 endpoint controller 什么时候将 pod 的 IP
添加与删除并没有直接影响,所以流量仍是能打到未启动彻底的容器上。不过只是咱们的 Pod 能正常启动了。
readinessProbe: httpGet: path: /ping port: 8000 scheme: HTTP initialDelaySeconds: 5 timeoutSeconds: 3 periodSeconds: 5 successThreshold: 1 failureThreshold: 3
readiness 探针会的功能包含两个:
若是只想实现 1 功能,仅服务启动时检测可用性的话, 可以使用 startupProbe (前提是你的集群版本大于 1.16) 。
配置 readiness 探针后,手动调用时没有发现服务不可用,而后咱们再使用 hey 进行并发测试。
测试后发现,成功率能达到 90% 以上,有了质的飞越。
readinessProbe: httpGet: path: /ping port: 8000 scheme: HTTP initialDelaySeconds: 10 timeoutSeconds: 3 periodSeconds: 5 successThreshold: 1 failureThreshold: 3
只要 Readiness 的 initialDelay 小于服务的正常启动时间(在咱们的例子里也就是 10 秒),该值的大小对
服务自己是没有影响的。
可是若是 initialDelay 大于你的服务启动时间,虽然对服务自己也没有影响,可是将会延长你的发布时间。由于在新启动的 pod ready 以前,旧的 pod 是不会被 kill 掉的,流量也不会切到新起的容器。
有了 readiness 探针后,咱们发版时服务的调用成功率已经达 90% 以上了,但对于一些并发较高的服务,这剩余的 10% 的失败,影响也是很严重的。还有没有办法再进一步提高成功率呢?
分析一下,请求失败,无非是两种可能:
第一种状况已经由 readiness 解了,只能是第二种了。来看一张 《Kubernetes in action》 一书中的图:
如下节选自《Kubernetes in action》 :
当APIserver收到一个中止Pod的请求时,它首先修改了etcd里Pod的状态,并通知关注这个删除事件全部的watcher。这些watcher里包括Kubelet和Endpoint Controller。这两个序列的事件是并行发生的(标记为A和B)在A系列事件里,你会看到在Kubelet收到该Pod要中止的通知之后会尽快开始中止Pod的一系列操做(执行prestop钩子,发送SIGTERM信号,等待一段时间而后若是这个容器没有自动退出的话就强行杀掉这个容器)。若是应用响应了SIGTERM并迅速中止接收请求,那么任未尝试链接它的客户端都会收到一个Connection Refusd的错误。由于APIserver是直接向Kubelet发送的请求,那么从Pod被删除开始计算,Pod用于执行这段逻辑的时间至关短。如今,让咱们一块儿看看另一系列事件都发生了什么——移除这个Pod相关的iptables规则(图中所示事件系列B)。当Endpoints Controller(运行在在Kubernetes控制平面里的Controller Manager里)收到这个Pod被删除的通知,而后它会把这个Pod从全部关联到这个Pod的Service里剔除。它经过向APIserver发送一个REST请求对Endpoint对象进行修改来实现。APIserver而后通知每一个监视Endpoint对象的组件。在这些组件里包含了每一个工做节点上运行的Kubeproxy。这些代理负责更新它所在节点上的iptables规则,这些规则能够用来阻止外面的请求被转发到要中止的Pod上。这里有个很是重要的细节,移除这些iptables规则对于已有的链接不会有任何影响——链接到这个Pod的客户端仍然能够经过已有链接向它发送请求。
这些请求都是并行发生的。更确切地,关停应用所须要的时间要比iptables更新花费所需的时间稍短一些。这是由于iptables修改的事件链看起来稍微长一些(见图2),由于这些事件须要先到达Endpoints Controller,而后它向APIServer发送一个新请求,接着在Proxy最终修改iptables规则以前,APIserver必须通知到每一个KubeProxy。这意味着SIGTERM可能会在全部节点iptables规则更新前发送。
简单来讲,就是 ip 回收要比 kubelet 停掉容器慢一点。解决办法就很简单了,咱们另容器的回收晚于 ip 回收,就不会出现上面的问题了。
lifecycle: preStop: exec: command: - sh - -c - "sleep 5"
具体延迟时间需根据集群状况调整。
添加了上面的延迟退出操做后,再来进行并发测试,能够看到在咱们给定的压力下,请求成功率打到了 100%
经过配置 readiness 探针和程序延迟退出的方式,咱们实现了必定并发下 kubernetes 无感发布,发版时服务请求成功率 100%