kubernetesprobeinpractice0x0实验目的kubernetes在pod的生命周期中引入了probe机制来判断service的健康状态。顾名思义,Liveness探针用于检测服务的存活状态。如果Liveness探测连续失败的次数超过设定的阈值,kubelet将杀死pod。就绪探测用于确定服务是否准备好接收流量和负载。根据官方文档,Readiness探测连续失败后,端点将从服务中移除,不再承载流量。但实际上,它还有另一个作用。在更新部署或副本集时,Liveness探测器确定控制器杀死旧pod的时间。我们将在以下示例中详细说明这一点。在写这篇博客之前,我把官方文档翻来覆去看了一遍,也看了很多第三方的博客,但还是没能深入了解这两种探针的区别。在生产环境中使用时,我们应该使用这两个探针吗,这个探针应该如何搭配使用呢?本文基于kubernetes1.14编写。kubernetes1.16引入的startupprobe的功能,请参考官方文档。0x1准备不啰嗦了,直接上例子。首先我们搭建一个golangwebserver,然后验证LivenessProbe和ReadinessProbe对这个服务的影响。Dockerfile如下:FROMgolang:1.10-alpine3.8ASbuild-envWORKDIR/go/src/appCOPY。.RUNgobuild-o/app.FROMalpine:3.8COPY--from=build-env/app/appCOPY--from=build-env/go/src/app/docker/entrypoint.sh/entrypoint.shENTRYPOINT["/entrypoint.sh"]CMD["/app"]在entrypoint中我们加入几秒sleep来模拟一个启动慢的应用(或者你可以直接启动一个java服务),这样我们可以观察LivenessProbe的效果和就绪探针。入口点如下:#!/bin/bashecho"inentrypoint.sh"echo"sleepstart"sleep10echo"sleepover"exec"$@"根据这个Dockerfile构建镜像example.com/server:latest,并然后创建一个基本的部署和服务,我们的实验开始了。apiVersion:extensions/v1beta1kind:Deploymentmetadata:name:serverlabels:app:serverspec:replicas:1选择器:matchLabels:app:servertemplate:metadata:labels:app:serverspec:containers:-name:appimage:example.com/服务器:最新端口:-名称:app-tcpcontainerPort:8000协议:TCP---apiVersion:v1kind:Servicemetadata:名称:服务器标签:app:serverspec:选择器:app:服务器端口:-端口:80协议:TCPtargetPort:8000至此,我们在集群中启动了一个服务,它有如下特点:启动后有10秒的延时,10秒后提供服务。服务提供/ping接口,访问该接口会返回http状态码200页面服务是无状态的,也设置了任意数量的副本集。0x2动作按照它说的去做。接下来配置几种不同的探针组合,观察其对每日发布(部署/rs更新)、扩容、缩容操作的影响:不配置任何探针,给Liveness探针配置较小的initialDelay值,适当配置initialDelay值使用适当的initialDelay值配置Liveness探测器,并使用较小的initialDelay值配置Readiness探测器。给Liveness探针配置一个合适的initialDelay值,给Readiness探针配置一个大的initialDelay值,然后在发布版本的时候用压测工具模拟必须并发请求访问服务,然后统计不同的请求成功率场景。选用嘿嘿作为压测工具。需要注意的是,这里使用压测工具并不是为了测试系统的负载能力,而是简单的模拟一定的并发量,观察系统行为。#qps10,concurrent10,continuous20shey-disable-keepalive-t1-z20s-q10-c10http://server/ping当没有配置probe和配置probe时,kubelet和endpointcontroller都假设为As只要pod主进程存在,容器就会存活。它不会尝试重启pod,也不会主动从端点控制器中删除pod的ip。所以,在这种场景下,pod是可以正常启动的,但是启动后,流量会进入新启动的容器。在我们的场景中,服务可以在主进程启动后10秒处理网络请求,因此每次发布都会有大约10秒的服务不可用。使用较小的initialDelay值配置Liveness探针livenessProbe:httpGet:path:/pingport:8000scheme:HTTPinitialDelaySeconds:2timeoutSeconds:3periodSeconds:5successThreshold:1failureThreshold:3我们配置初始延迟为2秒,超时为3秒,5秒的请求间隔,失败阈值为3的liveness探测,这意味着什么?Pod启动后2秒开始检测,每5秒检测一次。连续失败3次后,确定容器不存活,将容器kill掉。2+5*3=17,17之后,如果容器还没有启动,就重新启动!因为我们的服务需要10秒才能正确处理ping请求,所以我们的容器将不断重启,永远无法正常服务!使用适当的initialDelay值配置LivenessProbe:httpGet:path:/pingport:8000scheme:HTTPinitialDelaySeconds:10timeoutSeconds:3periodSeconds:5successThreshold:1failureThreshold:3什么是合适的?其实只要你的服务正常启动时间小于initialDelaySeconds+failureThreshold*periodSeconds就可以了。但这有点麻烦。为简单起见,让服务启动时间小于initialDelaySeconds。这样配置之后,服务就起来了,但是我们测试之后,发现还有10秒左右的不可用时间!因为liveness只决定kubelet重启Pod的时间,但是当端点控制器添加和删除pod的IP时并没有直接影响,所以流量仍然可以打到没有完全启动的容器。但只有我们的pod才能正常启动。为Readiness探针配置一个更小的initialDelay值readinessProbe:httpGet:path:/pingport:8000scheme:HTTPinitialDelaySeconds:5timeoutSeconds:3periodSeconds:5successThreshold:1failureThreshold:3readiness探针会议的作用包括两个:周期性检测服务可用性,如果服务不可用,端点控制器将删除端点。只有在就绪探测的连续成功次数超过成功阈值后,该ip才会被添加到端点。如果只想实现1个功能,只在服务启动时检测可用性,可以使用startupProbe(前提是你的集群版本大于1.16)。配置readinessprobe后,手动调用也没有发现服务不可用,然后我们使用hey进行并发测试。测试后发现,成功率可以达到90%以上,这是一个质的飞跃。配置一个更大initialDelay值的Readiness探针readinessProbe:httpGet:path:/pingport:8000scheme:HTTPinitialDelaySeconds:10timeoutSeconds:3periodSeconds:5successThreshold:1failureThreshold:3只要Readiness的initialDelay小于正常服务的启动时间(在我们的例子中,它是10秒),这个值的大小对服务本身没有影响。但是如果initialDelay大于你的服务启动时间,虽然对服务本身没有影响,但是会延长你的释放时间。因为在新启动的pod准备好之前,旧的pod不会被kill掉,流量也不会切到新启动的容器。延迟退出有了就绪探针,我们发布版本时服务调用成功率已经达到90%以上,但是对于一些高并发的服务,剩下的10%的失败影响比较严重。有什么方法可以进一步提高成功率吗?分析,请求失败,有两种可能:流量请求到达了还没有完全启动的容器,此时容器无法处理请求,所以向已经停止服务的容器请求连接拒绝流量由于某种原因,导致连接被拒绝或连接超时的第一种情况已经通过就绪解决了,只有第二种。看书上的一张图《Kubernetes in action》:以下摘自《Kubernetes in action》:当APIserver收到停止Pod的请求时,首先修改etcd中Pod的状态,并通知所有关注的watcher删除事件。这些观察者包括Kubelet和EndpointController。这两个事件序列并行发生(标记为A和B)。在A系列事件中,你会看到Kubelet收到Pod即将停止的通知后,会尽快停止Pod的一系列操作(执行prestophook,发送SIGTERM信号,稍等片刻,然后如果容器没有自动退出,则强行杀死容器)。如果应用程序响应SIGTERM并快速停止接受请求,则任何尝试连接到它的客户端都将收到连接拒绝错误。由于APIserver直接将请求发送给Kubelet,因此从删除Pod开始,Pod执行此逻辑所花费的时间非常短。现在,让我们看一下另一系列事件中发生了什么——删除与该Pod相关的iptables规则(如图中的事件系列B)。当EndpointsController(在Kubernetes控制平面的ControllerManager中运行)收到Pod被删除的通知时,它会从与Pod关联的所有Services中删除该Pod。它通过向APIserver发送REST请求以修改Endpoint对象来实现。然后APIserver通知每个组件监控Endpoint对象。这些组件中包括在每个工作节点上运行的Kubeproxy。这些代理负责更新它们所在节点上的iptables规则,这些规则可以用来防止外部请求被转发到要停止的Pod。这里有一个非常重要的细节,删除这些iptables规则不会对现有连接产生影响——连接到此Pod的客户端仍然可以通过现有连接向它发送请求。这些请求都是并行发生的。更准确地说,关闭应用程序所花的时间比iptables更新所花的时间略少。这是因为iptables修改的事件链看起来有点长(见图2),因为这些事件需要先到达EndpointsController,然后它向APIServer发送新的请求,然后必须先通知APIserverProxy最后修改了每个KubeProxy的iptables规则。这意味着SIGTERM可能会在所有节点iptables规则更新之前发送。简单来说,ip回收比kubelet停止容器慢。解决方法很简单。如果我们回收容器晚于ip,就不会出现上面的问题。lifecycle:preStop:exec:command:-sh--c-"sleep5"具体延迟时间需要根据集群情况调整。添加上面的延迟退出操作后,我们来进行并发测试。可以看到在我们给定的压力下,请求成功率达到了100%。0x3conclusion通过配置readinessprobe和程序的延迟退出,实现了一定的无感并发发布kubernetes,发布时服务请求成功率100%
