在前边的文章中,我们学习了如何使用初始化容器来初始化主进程运行环境,比如加载配置文件,load数据以及通知外部提供等,随着我们要支持的业务场景越来越复杂,我们可能要在容器启动或者退出的时候,运行额外的处理进程。Kubernetes给容器提供了lifecycle hook机制,让我们在容器启动和终止退出之前,有机会做定制化的初始化和清理工作。具体来说,Kubernetes提供了两种类型的钩子函数(hooks):
- Post-start钩子进程,容器启动后执行。
- Pre-stop钩子进程,容器退出之前执行。
同初始化容器不同的是,钩子进程配置粒度更细,可以针对每个容器来配置,下图向大家展示了钩子进程和容器的生命周期的关系:
如上图所示,请大家务必注意lifecycle hook进程在容器启动和关闭推出执行的时间点。钩子进程和liveness probe类似,可以用来在容器中执行特定的命令,或者从光荣器中给外部的应用程序发送HTTP请求。
注:lifecycle hook钩子进程只支持普通容器,Kubernetes不支持给初始化容器配置lifecycle hook钩子进程,并且lifecycle hook不支持tcoSocket类型,只支持HTTP和exec。
接下来我们来通过实际的例子来看看如何使用者两种类型的钩子进程。post-start lifecycle hook进程在容器被创建后马上触发,因此我们可以使用exec类型的hook来在主进程启动的时候,执行辅助进程;或者通过httpGet来调用应用程序的初始化和预热模块。
对于我们自己负责开发的应用程序,post-start hook提供的功能很容易通过修改源代码来实现,但是如果我们面对的是一个遗留应用,没有源代码在手上,这种情况下,post-start hook就给我们提供一种轻量级的,不需要修改应用程序源代码和容器镜像的扩展方式。
接下来我们部署一个配置了post-start hook的应用,这个应用启动的时候会执行指定命令。为了让接下来的讨论更加有意思,我们的post-start进程要执行的命令必须有点意思,因此决定使用Unix和类Unix系统上一个很古老的工具fortune,这个叫fortune的命令每次执行都会打印一句至理名言,或者谚语。读者使用的是macOS系统的话,可以通过brew install fortune来安装这个工具。笔者的生产工具上运行fortune输出如下:
➜ fortune
`When you say "I wrote a program that crashed Windows", people just stare at
you blankly and say "Hey, I got those with the system, *for free*".'
(By Linus Torvalds) - 圈内微软的朋友不要拉黑啊,我可能只是运气不好碰到这句。
接下来我们的工作就是将这个fortune命令和Nginx web服务器组合,来构造一个在启动的时候,通过fortune输出一句名言,然后通过Nginx提供给客户端访问的简单网站。这个网站的工作原理是,fortune命令将输出的名言警句写到容器的文件系统,然后Nginx从这个文件读取名言并返回给客户端。
Nginx web服务器有现成的容器镜像可以使用,但是不幸的是fortune并没有现成的容器可以使用。因此为了实现我们的名言警句网站,我们就只能自己基于Nginx镜像来构建应用程序了。具体来说,我们基于Nginx镜像构建我们的应用程序镜像的时候,只需要在基础镜像上安装fortune软件包即可。
笔者使用的例子主要为了说明post-start的工作原理,因此我们也就不直接构建新的容器了,取而代之的是在基础容器Nginx启动的时候会同时安装和运行fortune工具,但是在实际的项目中,千万不要这样干。具体来说,我们计划在应用启动的时候,通过post-start钩子进程来安装fortune工具,并运行这个工具来产生网站对外输出的名言警句。如下图YAML文件所示:
如上图所示,我们使用nginx:alpine容器进项来作为POD中运行的主进程,并且为这个主容器进程定义了post-start lifecycle hook进程,当nginx容器启动的时候,执行对应的命名。从配置文件可以看到,Nginx服务器运行在端口号8085上。这里需要大家注意的是,post-start的exec指定的命令和主进程并行运行,从这点可以看出,postStart这个名字非常容易让人产生误解,因为hook指定的命令并不是在容器的主进程启动后才开始执行,而是在容器创建之后,成功启动之前这段时间内执行。
从源代码的角度看,post-start指定的命名和主容器进程几乎是从相同的时间点开始执行,当post-start指定的命令fortune执行完成,命令生成并保存到文件系统的名言警句就可以从Nginx服务器访问了。
好了,废话不多说了,让我们请出kubectl apply命令来直接部署名言警句网站应用吧。在自己的Kubernetes环境上执行kubectl apply -f yunpan-fortune-poststart.yaml,命令执行成功后,验证一下POD已经运行起来。首先通过port-forward来创建客户端的访问代理,接着我们就可以使用curl http://localhost:8085/quote 来访问Nginx提供的名言警句服务,在笔者的macOS上输出如下:
➜ curl http://localhost:1080/quote
Nuclear war can ruin your whole compile.
-- Karl Lehenbauer
虽然我们指定的post-start hook和主容器进程并发执行,但是你必须了解的是,这个hook进程会从两个角度影响主容器进程的运行。首先,即便是容器已经成功启动,如果post-start hook尚未执行成功,那么容器的状态就会是Waiting,并且容器对象的reason字段显示”ContainerCreating“信息,直到post-start hook执行成功。在这之前,如果我们运行kubectl log命令,Kubernetes会拒绝输出任何日志信息,另外kubectl port-forward命令也会拒绝创建本地代理来forward访问流量。读者可以通过如下展示的一个特制的PDO对象来验证post-start由于启动缓慢对主容器进程造成的影响。
感兴趣的同学可以在本地将这个POD部署并运行起来,我们在post-start钩子进程启动后,故意sleep了60秒,同时我们在容器启动后,立即运行kubectl logs yunpan-poststart-slow命令,你就会收到Kubernetes返回的错误信息,告诉我们容器实例尚未启动,这其实和事实不符,你可以通过远程在容器中运行ps命令来验证这一点:kubectl exec yunpan-poststart-slow -- ps x。
其次,post-start hook对主容器进程第二个影响是:如果hook中指定的命令运行后返回了非0的exit code,那么容器实例会被重启。为了验证这一点,我们设计了post-start hook运行失败的例子,请看如下POD的YAML定义:
如上图所示,我们在post-start hook的命令中,故意返回1(非0值),部署这个POD后,如果你通过kubectl get pods -w,你会观察到对应容器启动错误:
yunpan-poststart-fail 0/1 PostStartHookError: command 'sh -c echo 'Emulating a
post-start hook failure'; exit 1' exited with 1:
从上边的错误信息可以看出执行出错的命令和返回码,接着如果我们查看pod的事件列表,我们会看到相同的信息,FailedPostStartHook 警告信息包含了exit code和造成这个exit code命令,如下面的输出所示:
Warning FailedPostStartHook Exec lifecycle hook ([sh -c ...]) for
Container "nginx" in Pod "yunpan-poststart-fail_default(...)" failed - error: command
'...' exited with 1: , message: "Emulating a post-start hook failure\n"
另外上边展示的错误信息也会出现在POD的status字段中,但是容器的状态时刻都在发生变化,这个信息只会保存很短的时间,很多时候看POD的status信息并没有办法回答我们对容器状态的疑问。因此笔者强烈建议大家尽量通过查看pod的事件俩表来分许POD的问题。
本篇文章到这里为止介绍的都是执行指定命令类型的post-start hook应用案例,如我们在文章开头介绍,Kubernets也支持httpGet类型的post-start hook,接下来我们就通过几个例子来看看具体的使用方法。
注:Kubernetes不支持同时配置exec和httGET两种模式的POD定义。
对于电商类的应用来说,会大量使用缓存机制来缩短应用的访问时间,提升吞吐量。而缓存需要预热,要不然大量的真实流量进来会造成缓存穿透到关系型数据库,可能引起系统雪崩。因此我们一般情况下需要对缓存做预热,而post-start hook提供的httpGET模式就很适合在应用启动的时候,通过调用缓存预热接口,来初始化缓存数据。这样当应用启动起来,我们就不用担心巨量接入的流量造成缓存穿透,影响应用程序的整体稳定性。下图是我们特制的一个POD定义,其中指定了httpGET类型的post-start hook:
如上图所示,我们为httpGET类型的post-start hook设置了port和path,除了这两个参数之外,我们还可以指定发送请求的http模式(http或者https),host字段,以及httpHeaders等。需要大家特别注意的是,host字段默认值为PDO的ip地址,千万不要把host指定为localhost,因为localhost指向宿主机,而不是POD,这也是很多不熟悉原理的同学很容易犯的错误。
同命令类型的post-start hook类似,httpGET模式下,post-start hook会在容器被创建后,立即就开始执行。不过在httpGET模式下,发送请求的服务可能还没有启动,因此post-start hook很容易失败,容器进入到无限重启的恶性循环中。
有意思的是,Kubernetes当前并不把httpGET请求返回404当做post-start hook运行失败,因此当我们配置httpGET类型的post-start hook的时候,区别仔细核对,确保URL正确,因为如果你设置错了,可能post-start hook啥都没有干,并且我们还很难发现。
接下来,我们来看看如何在应用结束退出之前运行处理进程。具体来说,Kubernets也允许我们定义pre-stop hook来在容器进程退出之前,执行某个操作,比如进行资源清理等。pre-stop hook在容器结束之前执行。结束一个运行中的进程,通常通过发送TERM信号给进程,以此来通知应用程序,请立即结束运行并退出。由于容器本质上就是一个进程,因此进程终止的方式也适用。
当容器进程被停止挥着重启,Kubernetes会发送TERM信号给主容器进程,但是在发送这个TERM信号之前,Kubernetes会执行pre-stop hook,当然如果我们给容器配置了pre-stop钩子进程。当pre-stop hook配置的进程执行完成,Kubernetes才会继续发送TERM信号给要被停止或者结束的进程。注意,当容器开始退出的时候,liveness probe就不会继续执行了。
pre-stop hook通常被用来实现容器进程的优雅关闭,以及在关闭之前执行一些资源清理工作等。pre-stop hook同post-start hook一样,也提供了命令模式和httpGET模式。我们继续基于前边Nginx的例子来介绍pre-stop hook。我们部署的名言警句网站基于Nginx服务器对外提供http服务,当Nginx进程收到TERM信号后,会立即关闭所有的连接,并开始退出过程。但是这属于有损关闭,因为收到TERM信号的时候,有很多请求处于处理中的状态。
幸运的是,我们可以通过执行nginx -s quit来优雅退出,执行这个命令会导致Nginx服务器停止接受新的http请求,并且等待所有处理中的请求技术后,才开始退出过程。对于运行在Kubernetes上的应用来说,我们可以使用pre-stop hook来执行前边Nginx优雅退出的命令,这样就不会有请求未被处理完,确保数据完整。下图是我们验证pre-stop hook配置的YAML文件:
如上图所示,当容器要退出的时候,会执行nginx -s quit来确保nginx进程优雅关闭,并且这个命令是在nginx进程收到TERM信号之前,确保应用不会有数据的丢失。pre-stop和post-start的区别是,pre-stop hook的执行结果,不会影响容器进程的退出,即便是执行hook后返回码是非0。如果hook执行失败,我们会从pod的事件清单中看到FailedPreStopHook信息,但是我们不会从POD的状态信息中看到任何错误。
注:如果pre-stop hook的成功运行对我们的应用程序正常退出非常重要,那么就确保pre-stop hook成功运行,笔者经历过几个项目,虽然配置了pre-stop hook,但是根本没有人关心运行结果。
好了,这篇文章的内容就真没多了,下篇文章我们总结一下POD的声明周期,从POD的整个lifecyle的角度,来把前边几篇文章的内容做个归纳和总结,敬请期待!