笔者在上一篇文章中介绍了如何在POD中部署多个容器实例,特别是我们通过部署边车容器的这种方式,在不修改原始容器代码的情况下,对容器的功能进行了扩充,比如提供HTTPS访问支持的能力等。坦白讲,我们上篇文章中有很多细节信息未给大家做展开介绍,比如容器的状态,Ready具体代表什么意思等,今天我们就来针对容器的生命周期进行一次深入的探索。
当我们将应用部署到Kubernetes平台上之后,我们可以通过kubectl describe命令来返回完整的POD对象。返回的POD对象有个status节点,包含了对象的状态信息,具体来讲,这部分会包含如下的POD对象状态信息:
1,POD对象的IP地址和POD被调度到的工作节点。
2,POD是什么时候启动的。
3,POD的QOS(Quality of service)类型。
4,POD当前处于什么阶段(phase)。
5,POD的当前状态(condition)以及POD中每个容器实例的状态(state)。
我们本篇文章主要分析容器的生命周期,因此上边的这些状态中,我们会讨论phase,condition和state,因为这些状态是构成POD和里边运行容器状态的指示灯。首先我们从POD的生命周期来介绍,如下图所示:
如上图所示,POD的整个生命周期中总共有5种phase阶段,接下来我们来详细介绍一下每种phase:
- Pending阶段,是新建POD的初始情况,直到POD被调度到某个工作节点,并且POD中的容器实例镜像被下载到工作节点的缓存并启动起来,那么POD将会一直处于Pending阶段。
- Running阶段,POD中至少有一个容器实例处于Running状态,那么POD就处于这个阶段。
- Succeeded阶段,当POD中所有的容器实例都成功运行并退出,那么POD就处于这个阶段。
- Failed阶段,当POD中至少有一个容器实例因为某种原因运行失败而出错,那么POD就会被标记为Failed阶段。
- Unkown阶段,当Kublet停止向API Server汇报POD的状态信息时,POD就处于这个阶段,造成这种情况的可能原因包括但不限于:工作节点故障,或者工作节点的网络连接断开等。
通过分析POD所处的阶段能快速获知POD的实际运行情况,接下来让我们把yunpan.yaml这个POD重新启动起来,来实战一下上边介绍的五种阶段。首先我们在自己的集群上重新部署yunpan.yaml这个对象,部署命令是:kubectl apply -f yunpan.yaml,接着我们来通过返回的POD对象信息找到status部分并输出到控制台,通过命令:kubectl get po yunpan -o yaml | grep phase,在笔者的本地集群上,输出如下:
➜ kubernetes kubectl get po yunpan -o yaml | grep phase
phase: Running
由于这个POD中只有一个容器,并且这个容器实例正在健康运行,因此POD所处的阶段是Running,和我们上边分析的结果一致。我们也可以通过kubectl get pod yunpan来返回POD的运行状态信息,但是大家要注意的是,如果POD的状态是健康的,那么返回的Status列就是POD的阶段,但是对于出现运行问题的POD,这个Status列将显示具体发生了什么类型的错误,我们会在后面详细介绍。
【POD的Conditions】
笔者在过往的容器化项目上,发现大家对POD的phase,status和conditions这三个属性会出现混淆,本质上这三个属性描述的都是POD的状态信息。phase可以看成是POD在经历某些调度和操作之后的结果,因此从phase上我们是看不出来POD经历了哪些操作或者异常,而condition字段向我们展示了这个细节信息。POD的conditions告诉我们POD从创建之初,具体经历过哪些阶段,以及如果出现异常,具体是什么原因造成的。
因此conditions通常会有多个值,不像phase,每个POD在特定的时间点,只会有一个phase值。我们先来看看Kubernetes的POD对象提供的四种类型的conditions:
- PodScheduled,表示POD是否被成功的调度到工作节点上。
- Initialized,POD中所有的初始化容器都成功的完成执行。
- ContainersReady,POD中所有容器都已经Ready。
- Ready,POD准备好对外提供服务,所有容器实例都报告状态为Ready。
每个condition类型在POD的声明周期中有满足或者不满足两个状态,如下图所示,在POD刚开始启动的时候,PodScheduled和Initialized这两个condition没有满足,但是很快就会满足,并且会保持到整个POD的生命周期。而Ready和ContainerReady这两个condition会在POD的整个生命周期中发生多次变化。
如上图所示,POD对象有四个condition类型,并且在生命周期会发生变化。其实对于Kubernetes中的node对象(工作节点)来说,它也有自己一套condition类型,MemoryPressure,DiskPressure,PIDPressure和Ready。这两个不同类型对象condition的共性是都有Ready类型,其实大部分Kubernetes对象的condition类型都有Ready,表示事情按预期的在进行这个意思。
接下来我们看看condition在POD对象上的真实输出,笔者在自己本地集群上运行kubectl describe pod yunpan,从输出的超长对象信息中,我们找到了condition部分,如下图所示:
从kubectl describe我们只能看到每个condition是否被满足(true或者false),如果你想知道为什么一个状态是false,可以在返回的对象信息中找.status.conditions字段,如下图所示:
从上图可以看出,每个condition类型都有status字段,表示这个condition具体是true还是false,以及unknow。对于笔者本地的yunpan这个pod来说,所有的condition类型都是true,这就说明pod正在健康运行,对外提供服务。每个condition类型也包含了一个reason字段,提供了更多关于状态变化的信息。
我们从POD对象中也可以看到容器实例的状态,具体来说,state字段反映了容器当前的状态,而lastState字段表示上一个容器的启动实例运行结束时的状态。容器状态部分还包括一个ID字段containerID和镜像imageID字段,这两个字段不言自明。除了这些字段,容器状态部分也有ready和restartCount字段,分别表示容器是否ready,以及重启的次数。对于容器实例的状态来说,最最重要的是state字段,一个容器实例的状态机如下图所示:
如上图所示,容器的状态机中有4中状态,对于每种状态的详细介绍如下:
- Waiting状态,容器实例在等待启动,从reason和message字段可以得到处于这个状态容器实例的更多信息。
- Running状态,容器实例已经被创建,并且里边的进程正在运行。startedAt字段记录了容器被成功启动的时间。
- Terminated状态,容器中的进程已经运行结束,startAt字段和finishedAt字段记录了容器启动的时间和运行结束的时间。exitCode字段记录了主进程退出的状态码。
- Unknow状态,容器的状态无法判断。
我们前边通过kubectl get pods这个命令只能看到POD中有多少容器处于Ready状态,要获取更加详细的信息,我们可以使用kubectl describe,如下图所示,在笔者的本地Kubernetes环境的输出:
到现在为止,我们创建的所有POD都很健康,运行过程中没有出现任何问题,但是这并不是常态,代码会有缺陷,网络会抖动等,都可能造成POD中运行的所有容器实例出现故障,对于生产级别的部署方案,我们必须有机制确保POD以及运行在POD中的容器健康运行,接下来我们来聊聊这个话题。
【保障容器实例健康运行】
当POD被调度到某个工作节点后,运行在工作节点上的Kubelet会负责驱动容器运行时来启动容器实例,并尽自己最大的努力来让容器长时间健康运行,直到运行结束退出。如果容器中的主进程因为某种原因退出结束,那么Kublets会负责重启容器。如果应用在运行过程中出现了错误,Kubernetes会自动重新启动POD,大大减轻了我们为了确保应用健康运行所需要的运维工作。我们来通过一个具体的例子,看看这一切是如何发生的。
在前边的文章中,笔者介绍过如何在一个POD中通过边车模式运行多个容器,我们创建了yunpan-ssl这个POD,这个POD中包含两个容器,我们的SpringCloud应用程序容器负责提供业务服务,而Envoy容器实例主要负责处理HTTPS流量请求。首先在自己的环境中将这个POD启动起来,并通过port-forward创建本地访问服务的代理:kubectl port-forward yunpan-ssl 8085 8443 9901。
接下来,我们要做的是让Envoy容器crash异常退出,同时我们观测Kubernetes是如何处理这种场景。新打开一个终端运行命令kubectl get pods -w,我们就能看到POD状态变化的输出信息,当然我们也需要观察产生的时间,请打开第二个终端,运行命令kubectl get events -w,好了,监控窗口准备好了,接下来我们想办法让Envoy容器退出。
如果你熟悉操作系统原理,应该知道我们可以给进程发送KILL信号,来让进程退出(在macOS上或者Linux上执行kill命令,背后的原理就是给进程发送KILL信号),但是对于Envoy容器进程,因为它是PID为1的进程,而Linux操作系统不允许杀掉PID为1的进程,我们得换个方法。你可能已经想到了,我们可以登录到宿主机上(对于minikube实例,宿主机就是这台叫做minikube的虚拟机,笔者有时候也称之为工作节点,因为minikube是一个单机环境,管理节点和工作节点都运行在这台叫minikube的虚拟机上)。
不过我们有更好的方法,而不需要登录到虚拟机上,就是通过Envoy提供的管理接口,来远程通过API接口停止某个进程的运行。我们可以在命令行窗口执行curl -X POST http://localhost:9901/quitquitquit ,接着我们就可以从POD的状态监控接口看到如下的输出信息:
➜ init-demo-image kubectl get pods -w
NAME READY STATUS RESTARTS AGE
yunpan-ssl 2/2 Running 0 12m
yunpan-ssl 1/2 NotReady 0 13m
yunpan-ssl 2/2 Running 1 13m
从上边的输出可以看到,当我们执行了杀死Envoy容器进程之后,POD的状态马上从Running就变成了NotReady,从输出的Ready列也可以看到,2个容器实例中只有1个处于ready状态。Kubernetes接下来会马上重新启动Envoy容器实例,随着容器实例的状态ready,POD的状态也从NotReady变成Running,并且RESTARTS列的计数加1,记录了容器被重启启动的次数。
注意:如果POD中的多个容器实例中,有任何一个运行失败,其他的容器实例不受影响,会继续运行。
接下来我们看看监控时间窗口的输出信息,如下所示:
➜ kubernetes git:(master) kubectl get events -w
LAST SEEN TYPE REASON OBJECT MESSAGE
0s Normal Pulled pod/yunpan-ssl Container image "qigaopan/yunpan-ssl-proxy:v1.1" already present on machine
0s Normal Created pod/yunpan-ssl Created container envoy
0s Normal Started pod/yunpan-ssl Started container envoy
从输出的事件信息中可以看到,envoy容器实例被重新启动了,你可以通过HTTPS来验证Envoy容器能够正常接收请求并处理。从这个输出中希望大家能看到非常重要的一点,Kubernetes本质上从来都没有重启的概念,而是删除老的容器实例,并且重新创建一个容器实例来取代老的实例,但是为了符合大家的直觉,我们还是叫restarting。
注:容器重新创建之后,进程在运行过程中写到容器文件系统的所有信息都会丢失,为了让这些数据能够在容器被重新创建后可用,我们需要给容器挂载存储卷,笔者会在后续的文章中详细介绍。另外需要注意的是,初始化容器不会因为应用实例容器实例的重启而重新执行,从这点可以看到,初始化容器的生命周期和POD一致。
Kubernetes也并不是在所有情况下都会默认的重启容器实例,我们可以通过restartPolicy这个字段来声明POD的重启策略,因为如果不配置,默认情况下,Kubernetes不管三七二十一,无论是POD正常运行结束还是异常情况,都会重启容器实例,这在某些情况下可能和我们的预期不符。Kubernetes提供了三种重启策略,如下图所示:
如上图所示,Kubernetes提供了三种类型的重启策略,详细介绍如下:
- Always策略,容器任何情况下都会重启,无论exitCode返回码表示运行成功还是失败。如果不在POD的YAML文件中显式的设置,默认的重启策略是Always。
- OnFailure策略,容器只会在返回码未非0的时候重启(对于大部分系统来说,非0都表示运行出错)。
- Never策略,从不重启容器实例,即便是运行出现错误退出。
注:Kubernetes的重启策略是配置在POD级别,这就意味着POD中所有的容器使用相同的重启策略,我们无法为每个容器单独配置重启策略。
如果我们多次调用Envoy管理节点提供的/quitquitquit接口,你会发现每次调用后,容器的重启所需要的时间都会变长,POD的状态处于NotReady或者CrashLoopBackOff状态,背后的原理如下图所示:
如上图所示,第一次容器异常退出后,Kubernetes会立即进行重启,如果容器容器由于某种原因,需要再次重启,那么Kubernetes会等待10秒钟再重启容器实例,而这个等待时间会随着后续的重启被加倍,20s,40s,80s,160s,从160s以后,延迟时间会变成5分钟,我们把每次重启需要等待的时间间隔加倍这种策略叫指数级回退策略(exponential back-off)。在极端情况下,容器的确会被block高达5分钟才能重启。而这个指数级回退策略会在容器成功运行10分钟后被清零。笔者在目前负责的项目上,多次看到容器处于CrashLoopBackOff的状态,这是很多调试阶段应用经常遇到的场景,希望大家通过这里的介绍,能够掌握这个原理。
我们在前边介绍的内容都是Kubernetes在我们的容器异常退出的时候,帮助我们重启容器来提供可用性,但是很多时候容器并没有退出,但是提供的服务却无法访问。举个例子,Java应用程序如果有内存泄漏,会返回内存不足的异常,但是这并不妨碍JVM进程继续运行,理论上Kubernetes应用能检测到这样的异常,并且重启容器实例,但是很显然Kubernetes把这个问题交给了运维和开发人员来解决。
如果我们从程序本身来考虑处理这问题,很明显会是个悖论,因为应用自己都没有内存了,如何检测自己出问题。因此这个问题必须要从外部来解决,而Kubernetes提供了叫Liveness probes的功能,来从外部对应用判活。
【应用程序的健康探测】
我们可以给POD配置liveness probe来定期的检测服务是否正常对外提供访问能力,我们可以在容器级别来设定这个liveness probe,而Kubernetes要做的就是,基于配置的信息,周期性的来访问应用程序,判断应用程序的状态。如果应用程序没有影响,或者返回错误信息,容器实例就会被认为不健康,Kubernetes就会终止容器,并基于配置的重启策略来重启容器实例。这里需要注意的是,Liveness probe只能用在正常应用容器实例上,不能配置在初始化容器实例上。
Kubernets为了提供对大部分应用程序关于判活场景的支持,提供如下三种liveness probe机制:
- HTTP GET probe通过发送发送请求给我们制定的资源地址,包括IP地址和端口号,如果probe收到正常的http响应(比如2xx和3xx),那么probe就认为应用健康;如果probe收到除2xx和3xx之外的http状态码,那么就认为此次probe探测失败。
- TCP Socket probe通过在目标容器制定的端口上建立TCP连接来判断容器的状态。如果TCP连接可以被成功的建立,那么就认为probe成功,如果无法成功建立TCP连接,就认为失败。
- Exec probe通过在目标容器实例上执行命令来判断容器的健康状态。执行如果返回非0返回码,就认为执行失败,如果命令执行超时,也会认为probe执行失败。
Kubernetes除了提供这种liveness probe的机制,还提供了一种叫startup probe的机制,我们会在后续的文章中介绍这种机制。好了,今天的内容就这么多了,笔者会在下篇文章中详细介绍如何配置liveness probe,敬请期待!