到这篇文章为止,笔者通过前边6篇文章详细的介绍了容器以及组织容器,进行资源调度的POD对象,由于POD和容器是容器化,Kubernetes体系的基石(准确说是必要条件),因此理解好POD和容器对进行后续的学习以及设计基于Kubernetes的部署方案至关重要。
注:必要条件是数学中的一种关系形式。如果没有A,则必然没有B;如果有A而未必有B,则A就是B的必要条件,记作B→A,读作“B含于A”。对于Kubernetes和容器的关系来说,如果没有容器技术的普及,肯定不会有容器编排的痛点,也就不会有Kubernetes的快速崛起和普及,从这个意义上来说,容器是Kubernetes的必要条件,而反过来说的话,Kubernetes平台就是容器的充分条件。
接下来,我们来总结一下前边六篇文章中的重要知识点,以期给读者一个完整的POD和容器生命周期视图。读者需要注意并不是看这篇文章就能达到”深入理解“,因为我在写这篇文章的时候,不会重复已经在前文中讲述过的内容,因此如果你发现读起来很吃力,赶紧去看看前边的几篇文章再回来,效果会更好。
当我们向Kubernetes集群提交了应用部署YAML文件后(YAML文件提交的背后原理,请参考笔者前边的文章),Kubernetes驱动调度器给POD分配一个可用的工作节点,接着工作节点上的Kubelet组件驱动容器运行时,把对应的POD运行起来。POD的生命周期被分成三个阶段,如下图所示:
如上图所示,POD的整个生命周期被分成三个阶段,其中在初始化阶段(春),执行初始化容器,完成环境初始化和数据准备工作;接着是运行阶段,这个阶段容器正常运行,对外提供服务能力,这是容器生命周期的夏天;如果POD实例被删除,容器实例收到关闭信号,容器和POD最终会退出(秋冬)。
注:从春夏秋冬的角度来理解容器的生命周期会更加有代入感,要不然这些知识都知识冰冷的理论概念。而我们的人生又何其相似啊,最珍贵的东西,就是你现在拥有的一切,家庭,孩子,时间和健康,并不是失去的东西。人生不过也就是四季,大自然的四季可以轮回,但是生命不会重来,认真过好每一天,不负韶华,只争朝夕就因人而异了。
在初始化阶段,初始化容器最先运行,并且严格按照POD对象的initContainers域定义的顺序依次执行。从时间顺序的角度看,初始化容器被启动后,做的第一件工作就是从镜像仓库pull容器镜像实例到工作节点上,而容器定义中的imagePullPolicy字段控制镜像的拉取策略,比如是每次都拉取最新的镜像实例,还是只在第一次拉取或者从来都不拉取镜像(最后一种看起来有点违反直觉,实际上在生产环境非常有用,因为我们必须控制生产环境镜像的版本。笔者在编写文章中案例的时候,大部分时间也是关闭自动拉取,来节省带宽)。Kubernetes提供的三种镜像拉取策略详细描述如下:
1,Not specified(未指定策略),如果我们使用了:latest标签那么默认就是Always;其他标签默认的策略是IfNotPresent。
2,Always策略,容器实例每次启动或者重启,都从仓库拉取最新的镜像数据,如果工作节点的本地缓存中已经有指定的版本,虽说不会下载,但是还是会和仓库通信。
3,Never策略,容器镜像已经在工作节点的缓存中,不从仓库下载。设置这种策略的时候,我们必须保证指定的镜像已经在工作节点上,比如另外一个容器已经运行过这个镜像,或者镜像就是在这台工作节点上通过docker build构建的,甚至是管理员手动docker pull过这个镜像。
4,IfNotPresent策略,如果指定的镜像不在工作节点的缓存中,那么就从仓库下载。这个策略确保镜像只有在工作节点上第一次执行的时候,才从仓库下载。
镜像拉取策略不光在容器启动的时候起作用,在容器重启的时候,也会基于配置的拉取策略来确定是否要重新下载镜像数据。为了让大家对这三种镜像拉取策略有直观的感受,看下图:
注:如果容器的imagePullPolicy被设置为Always,但是镜像仓库无法连接,即便是本地有制定版本的镜像数据,容器也无法顺利启动。因此设置为Always本质上给应用增加了外部依赖,如果仓库不可用,应用就无法启动,特别是在需要重启的时候,会造成系统的稳定性问题。
【深入理解运行阶段】
当镜像被成功pull到工作节点上之后,初始化容器就开始运行。初始化容器按照YAML文件中定义的顺序,一个接一个执行。具体来说,第一个初始化容器运行成功后,接着下载第二初始化容器的镜像,然后开始运行,这个过程会循环一致到最后一个初始化容器运行完成。这里需要特别注意的是,如果初始化容器运行的时候出现问题,比如退出后返回码为非0,那么初始化容器就会被重新启动,如下图所示:
如上图所示,初始化容器运行失败(退出码为非0),并且POD的重启策略是Always或者Onfailure,那么运行失败的初始化容器就会被重新启动。如果重启策略是Never,后续初始化容器就没有机会运行了,POD的status会被设置为Init:Error。对于运维和开发人员来说,就需要手动删除这个对象,然后重新创建以达到重新启动应用程序的目的。
初始化容器一般情况下只需要被执行一次,即便是POD中的应用容器实例有意异常被Kubernetes退出并重启,这种情况下,初始化容器也不会再次执行。但是在特殊去下,如果Kubernetes决定要重建POD,那么初始化容器就会被再次执行,这就意味着我们在初始化容器中做的工作,必须考虑幂等性。
【深入理解运行阶段】
当所有的初始化容器都成功运行,POD中定义的主容器进程(应用程序)才开始运行,大家需要注意的是,如果PDO中定义了多个容器(比如笔者前边文章中SpringCloud应用容器和Envoy容器的例子),那么这些容器实例并行启动。理论上,容器实例之间没有相互依赖,但是情况并不总是如此,请继续阅读。
从源代码的角度来看,Kubelet并不是同时启动所有的容器实例,而是基于我们提交的YAML文件中容器的定义顺序来逐个启动容器实例。如果一个容器定义了post-start hook,并且我们知道这个hook进程和定义它的容器是并行运行的,因此如果hook进程执行的过程中出现问题,那么这个hook不光会影响定义它的容器进程,也会影响这个容器后边定义的容器实例的启动。
注:post-start hook以及Kubelet按顺序启动每个容器实例属于具体代码实现层面的原理,Kubernetes在后续的版本中可能优化实现逻辑,因此大家了解即可。作为对比,所有容器实例退出过程是并发执行,长时间运行的pre-stop hook的确会block定义它的容器实例退出过程,但是不会影响其他的容器实例。容器定义的多个pre-stop hook会被同时触发开始执行。
在运行阶段,每个容器都是按:1,拉取镜像;2,容器实例启动;3,当容器退出时,基于配置的重启策略,进行重启;4,一直运行到POD被删除后,容器收到TERM信号,这样的顺序运行,接下来我们详细聊聊这个执行的顺序。
基于配置的imagePullPolicy,应用程序容器启动后会首先会从镜像仓库拉取镜像数据,当镜像数据ready后,容器实例就被创建。如笔者前边的介绍,容器实例并不是被同时启动,并且由于每个容器实例不同,镜像的大小不同,因此pull镜像到本地需要的时间也不同,的确存在某个容器还在下载镜像文件,而POD中其他容器都应成功启动运行的场景。
当容器中的主进程启动成功后,整个容器实例就成功启动了,如果为容器我们定义了post-start hook,那么这些hook会和主容器进程并发执行,并且hook执行成功后,主容器进程才会继续运行,要不然会被block。我们还可以为容器定义startup probe,当startup probe成功后,才会触发liveness probe设定的健康检查。
当startup probe或者liveness probe连续多次探测到容器的健康状态是false,那么会被退出,POD定义的restartPolicy策略决定了容器是否需要重新启动。如果POD的restartPolicy被设置为Never,并且startup hook执行失败,POD的状态会被设置成Completed,即便是post-start hook运行失败,这个非常让人confuse。
容器被退出的时候,pre-stop hook会被触发,因此容器可以实现我们所说的优雅退出模式。当pre-stop hook执行完成,TERM信号会被发送给容器中的主进程,Kubernetes会给主进程一段时间来完成退出工作。这个时间可以通过terminationGracePeriodSeconds来设置,默认情况下是30秒。这个时间的计时从pre-stop hook被调用开始,如果在设置的时间内进程尚未完成退出,Kubernetes会通过KILL信号来强制退出。如下图所示:
容器成功退出后,Kubernetes会基于POD上配置的重启策略,来决定是否要重启容器。如果重启策略是不重启,那么容器就会一直在Terminated的状态,即便是其他的容器都在正常运行,知道POD退出或者运行失败为止。
【深入理解POD的退出阶段】
POD中的容器会一直运行下去,直到我们删除了POD对象。当我们删除了POD对象后,POD中所有的容器就开始执行退出流程,并且状态被设置为Terminating。
容器退出流程本质上和liveness probe连续几次失败退出的流程基本一致,除了POD的deletetion-grace-period这个参数设置了容器有多少时间来完成退出流程。
grace period通过POD的参数metadata.deletionGracePeriodSeconds来设定,当我们删除POD的时候,我们可以指定这个参数的值,来修改默认从spec.terminationGracePeriodSeconds读取到的值。
如下图所示,POD中多有容器实例基本同时触发退出流程,对于每个容器实例,如果定义了pre-stop hook,那么会先调用pre-stop hook,然后发送TERM信号给主容器进程,如果在优雅退出等待时间内进程尚未退出,那么Kubernetes会发送KILL信号来强制退出进程。当POD所有的容器进程都停止运行后,POD对象会被最终删除。
为了让大家对退出机制有直观的了解,我们可以通过yunpan-ssl这个POD来实际观察一下整个退出的过程。如果这个POD没有启动,请通过命令kubectl apply -f yunpan-ssl.yaml来首先启动应用程序。
接着我们在自己本地环境上执行kubectl delete pod yunpan-ssl,你如果计算了从按回车到命令返回之间的消耗的秒数,你会发现删除时间略长(大概在数十秒)。我们来分析一下为啥要这么长时间?由于我们并未给yunpan-ssl中的容器定义pre-stop hook,因此当执行delete命令的时候,这些容器会立即收到TERM信号,而由于我们并未修改terminationGracePeriodSeconds设置,因此这个值默认是30秒,也就是最长要等30秒,Kubernetes才会强制退出。为了让退出的时间更快一下,我们来修改terminationGracePeriodSeconds这个参数,如下图所示:
如上图所示,pod的terminationGracePeriodSeconds字段被设置为5秒,这样当我们创建这个POD并删除时,大概只需要5秒就可以完成POD的删除。
注:其实在实际项目中,修改terminationGracePeriodSeconds参数不太常见,但是笔者到建议如果应用进程退出过程非常重,需要更多时间的话,可以适当增大这个时间配置。另外除了在YAML文件中指定,我们还可以直接在delete命令中修改默认容忍时间:kubectl delete pod yunpan-ssl --grace-period 10,这样的话,terminationGracePeriodSeconds参数会被覆盖为10秒。一个特例是,如果--grace-period被置为0,那么pre-stop hook就不会执行。
好了,我们来总结一下POD生命周期介绍的所有相关内容,下图是笔者总结的关于POD生命周期中初始化阶段的所有内容:
当初始化阶段完成后,应用容器实例便开始运行,如下图所示:
基于上边两个图,我们来总结一下POD和容器的全生命周期内容:
1,POD对象的状态信息包含:pod的phase,conditions以及运行在其中的每个容器的状态status信息。我们可以通过kubectl describe命令来查看POD对象的全量信息,或者通过kubectl get pod xx -o yaml。
2,基于POD的重启策略,运行在POD中容器在失败退出后,可能会被”重启“。实际上,容器没有重启的概念,Kubernetes会销毁老的实例,然后创建一个新的额实例来取代老的实例。
3,如果容器实例频繁重启,Kubernetes后组的启动时间会指数级回退。具体来说,第一次重启不会有任何延迟,第二次延迟10秒,第三次20秒,依次类推。上限是5分钟。如果容器正常运行了10分钟,那么这个回退值会清零。
4,指数级回退策略也被用在容器从仓库pull镜像的场景下。
5,给应用容器配置liveness probe可以在出现异常的时候,重启容器实例,liveness提供httpGET,tcpSocket和exec三种模式。
6,如果应用程序需要比较长的启动时间,我们可以给容器定义startup probe,这样就可以给容器足够的时间来启动,不然会进入无限重启恶性循环中。
7,我们可以为每个容器定义lifecyle hook,Kubernetes提供了两种类型。其中post-start hook在容器启动的时候被触发,而pre-stop hook是在容器关闭推出的时候被触发,lifecycle hook提供httpGet和exec两种模式。
8,如果我们为容器定义了pre-stop hook,那么当容器退出的时候,pre-stop hook会先执行。然后Kubernetes会发送TERM信号给容器的主进程,如果进程在配置的terminationGracePeriodSeconds时间内没有退出,那么Kubernetes会强制KILL掉进程。
9,当我们删除POD对象的时候,所有运行其中的容器开始并行退出。POD对象的deletionGracePeriodSeconds字段提供了容器有多长时间来执行退出,默认情况下这个值读取自termination period字段,但是我们可以在执行delete命令的时候重写。
10,如果应用退出需要较长时间,可能原因是运行在POD中的某个容器没有很好的处理TERM信号,给应用增加处理TERM信号的逻辑,而不缩短termination或者deletion grace period参数。
以上就是笔者在过去的几篇文章中的知识要点,读者可以看看是否和自己理解的一致,如果有任何疑问,可以回到前边文章重新阅读。好了,今天的内容就这么多了。云原生的概念笔者反复强调过多次,特别是云原生和元计算的这两个概念的区别需要大家有清晰的认知,具体来说,云计算是解决我们的应用在哪里运行问题,而云原生是解决如何让我们的应用使用云计算带来的红利,比如说扩展性,灵活性等。而扩展性和灵活性需要应用无状态,但是对于业务系统,不可能无状态,要不然就没有啥用了,因为业务的核心其实就是数据的流转,而数据的变化一定会产生状态。云原生说的应用无状态其实是指将状态进行统一管理,比如配置,数据等。因此云原生引用有状态,只是我们通过将状态代理给数据库以及存储系统来处理而已。存储卷是我们状态持久化的一种方案,因此我们从下篇文章开始介绍数据存储,敬请期待!