当我们向k8s 集群中提交了应用部署YAML文件之后,k8s 驱动调度器给pod分配一个可用的工作节点,接着工作节点的kubelet 组件驱动容器运行时,把对应的pod运行起来,pod 的生命周期被分为三个阶段如下图所示:
如上图所示,pod的整个生命周期被分成三个阶段,其中在初始化阶段(春天),执行初始化容器,完成环境初始化和数据初始化工作;接着是运行阶段,这个阶段容器正常运行,对外提供服务,这个是容器生命周期的夏天;如果pod实例被删除,容器实例收到关闭信号,容器和pod 最终会退出(秋冬)。
在初始化阶段,初始化容器最先运行,并且严格按照pod 对象的init Containers域定义的顺序依次执行。从时间顺序的角度看,初始化容器被启动后,做的第一件工作就是从镜像仓库pull 容器镜像实例到工作节点上,而容器定义中的imagePullPolicy 字段控制镜像的拉取策略,比如是每次都来去最新的镜像实例,还是只在第一次拉去或从来都拉去镜像(最后一种看起来有点违反直觉,实际上在生产环境非常有用,因为我们必须控制生产环境镜像的版本。笔者在编写文章中案例的时候,大部分时间也是关闭自动拉取,来节省带宽)。
1.k8s 提供的三种镜像拉取策略详细描述如下:
1.not specified(未指定策略),如果我们使用了:latest 标签那么默认就是Always;其他标签默认的策略 是IfNotPresent
2.Always策略,容器实例每次启动或者重启,都会从仓库拉取最新的的镜像数据,如果工作节点的本地缓存中已经有指定的版本,虽然说不会下载,但是还是会和仓库镜像通信。
3.Never 策略,容器镜像已经在工作节点的缓存中,不从仓库下载,设置这种策略的时候,我们必须保证指定的镜像已经存在工作节点上,比如另外一个容器已经运行过这个镜像,或者镜像就是在这台节工作节点上,比如另外一个容器已经运行过这个镜像,或者这个镜像就是在这台工作节点上通过docker build 构建的,甚至是管理员手动docker pull 过这个镜像。
4.IfNotPresent策略: 如果指定的经i昂不在工作节点的缓存中,那么就从仓库下载,这个策略确保镜像只有在工作节点上第一次执行的时候,才会从仓库下载。
镜像拉取策略不光在容器启动的时候起作用,在容器重启的时候,也会基于配置的拉取策略来确定是否要重新下载镜像数据,为了让大家对这三种镜像拉取策略有直观的感受,看下图:
注意: 如果容器的imagePullPolicy 被设置为Always ,但是镜像仓库无法连接,即便是本地有指定版本的镜像数据,容器也无法顺利启动。因此设置为Always 本质上给应用增加了 外部依赖,如果仓库不可用,应用就无法启动,特别是在需要重启的时候,会造成系统的稳定性问题。
1.深入理解运行阶段1
当镜像被成功pull 到工作节点上之后,初始化容器就开始运行。初始化容器按照YAML文件中定义的顺序,一个接一个执行,具体来说第一个初始化容器运行成功之后,接着下载第二初始化容器的镜像,然后开始运行,这个过程会循环一直到最后一个初始化容器运行的时候出现问题,比如 退出后返回码未非0,那么初始化容器就会被重新启动,如下图所示:
如上图所示,初始化容器运行失败(退出码为非0),并且pod 的重启策略是Always或者Onfailure,那么运行失败的初始化容器就会被重新 启动,如果重启策略是Never,后续初始化容器就没有机会运行了,Pod的Status就会被设置未Init:Error,对于运维和开发人员来说,就需要手动删除这个对象,然后重新创建以达到重新启动应用程序的目的。
初始化容器一般情况下,只需要被执行一次,即便是Pod中的应用容器实例有异常被k8s 退出并重启,这种情况下,初始化容器也不会再次执行。但是在特殊情况下,如果k8s 决定重建pod ,那么初始化容器就会被再次执行 ,这就意味着我们在初始化容器中做的工作,必须考虑幂等性。
3.深入理解运行阶段2
当所有的初始化容器都成功运行,pod 中定义的主容器进程(应用程序),才开始运行,大家需要注意的是,如果pod中定义了多个容器(比如笔者前面的文章中Spring cloud 应用容器和Envoy容器的例子),那么这些容器实例并行启动。理论上容器实例之间没有相互依赖,但是情况并不总是如此,请继续阅读。
从源代码的角度来看,kubelet 并不是同时启动所有的容器实例 ,而是基于我们提交的YAML 文件中容器的定义顺序来逐个启动容器实例。如果一个容器定义了post-start hook,并且我们知道这个hook 进程和定义它的容器是并行运行的,因此如果hook 进程执行的过程中出现问题,那么这个hook 不光会影响定义它的容器进程,也会影响这个容器后边定义的容器实例 的启动。
注:post-start hook 以及kubelet 按顺序启动每个容器实例属于具体代码实现层面的原理,k8s 在后续的版本中可能优化实现逻辑,因此大家了解 即可。作为对比,所有容器 实例退出过程是并发执行,长时间运行的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 prode(存活探针)设定的健康检查。
当startup prode 或者liveness prode 连续多次探测到容器的健康状态是false ,那么会被退出,pod 定义的restartPolicy策略决定了容器是否 需要重新启动。如果pod 的restartPolicy 被设置为Never, 并且startup hook 执行失败,podd的状态会被设置为completed,即便是post-start hook 运行失败,这个非常让人confuse.
容器退出的时候,pre-stop hook 会被触发,因此容器可以实现我们所说的优雅退出模式,当pre-stop hook 执行完成,TERM 信号会被发送给容器中的主进程,k8s 会给主进程一段时间来完成退出工作。这个时间可以通过terminationGracePeriodSecond来设置,默认情况下是30s,这个时间的计时,会从pre-stop hook被调用开始,如果在设定时间内进程尚未完成退出,k8s 会通过kill 信号来强制退出,如下图所示:
容器成功退出后,k8s 会基于pod 上配置的重启策略,如果重启策略是不重启,那么容器就会一直在terminated 状态,即便是其他容器都在正常运行,直到pod 退出或者运行失败为止。
4.深入理解pod 的退出阶段
pod 中的容器会一直运行下去,直到我们删除了pod 对象,当我们删除pod 对象后,pod 中所有的荣光其就开始执行退出流程,并且状态设置为terminating .
容器退出流程本质上是和liveness prode 连续几次失败退出的流程基本一致,除了pod 的deletetion-grace-peroid 这个参数设置了容器有多少时间来完成退出流程。
grace peroid 通过POD 的参数metadata.deleteGracePeriodSeconds来设定,当我们删除pod 的时候,我们可以指定这个参数的值,来修改默认从spec.terminationGracePeriodSecond读取到的值;
如下图所示,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参数。
以上就是笔者在过去的几篇文章中的知识要点,读者可以看看是否和自己理解的一致,如果有任何疑问,可以回到前边文章重新阅读。好了,今天的内容就这么多了。云原生的概念笔者反复强调过多次,特别是云原生和元计算的这两个概念的区别需要大家有清晰的认知,具体来说,云计算是解决我们的应用在哪里运行问题,而云原生是解决如何让我们的应用使用云计算带来的红利,比如说扩展性,灵活性等。而扩展性和灵活性需要应用无状态,但是对于业务系统,不可能无状态,要不然就没有啥用了,因为业务的核心其实就是数据的流转,而数据的变化一定会产生状态。云原生说的应用无状态其实是指将状态进行统一管理,比如配置,数据等。因此云原生引用有状态,只是我们通过将状态代理给数据库以及存储系统来处理而已。