docker优雅停机原理
Linux的信号
docker环境优雅停机是基于Linux信号实现的,常见的Linux信号包括:
SIGHUP(1):当用户终端连接结束时,系统会像所有运行中的进程发出这个信号;通常在热加载配置文件时候也会使用该信号。wget命令就注册了SIGHUP(1)信号,这样就算你退出了Linux登录,wget也能继续下载文件。同样的,如Docker/Nginx/LVS等服务也会注册SIGHUP(1)信号,实现服务的热加载配置文件功能。
SIGINT(2):程序终止(interrupt)信号,在用户键入INTR字符(通常是Ctrl+C)时发出,用于通知前台进程组终止进程。
SIGQUIT(3):和SIGINT类似,但由QUIT字符(通常是Ctrl+反斜杠)来控制。Nginx就是通过注册这个信号来实现优雅停止服务的。
SIGKILL(9):立刻结束程序。该信号不能被阻塞、处理和忽略,不能在程序中被获取到。
SIGTERM(15):程序结束(Terminate)信号,又叫请求退出信号,与SIGKILL不同的是该信号可以被阻塞和处理,我们可以通过在程序中注册该信号来实现服务的优雅停止。使用kill命令缺省会发出这个信号。
SIGCHLD(17):子进程结束时,一般会向父进程发送这个信号。Nginx是个多进程程序,master进程和worker进程通信就使用的这个信号。
通过Linux的kill命令就看可以向JVM发送信号,使用kill -l
可以查看所有信号列表,然后执行kill -9 <PID>
,就可以模拟SIGKILL信号发送的过程了
docker stop与退出码
docker命令参考如下链接:
docker stop
命令本质上就是利用了Linux信号实现容器停止,当我们用docker stop
命令来停掉容器的时候,docker默认会允许容器中的应用程序有10秒的时间用以终止运行。我们可以通过在执行docker stop --time
或docker stop -t
参数来自定义一个stop时间长度
在docker stop命令执行的时候,会先向容器中PID为1的进程(main process)发送系统信号SIGTERM,然后等待容器中的应用程序终止执行,如果等待时间达到设定的超时时间,如默认的10秒,会继续发送SIGKILL的系统信号强行kill掉进程。在容器中的应用程序,可以选择忽略和不处理SIGTERM信号,不过一旦达到超时时间,程序就会被系统强行kill掉
因为根据信号做了不同的容器响应,在退出后容器上的退出码也会有不同的展示。docker容器的退出码值=128+信号值(Exit Code 1除外),举例如下:
Exit Code 1 :程序错误,pod直接就没起来,例如Dockerfile中引用不存在的文件,或entrypoint.sh中引用了错误的包
Exit Code 137:128+9,即响应SIGKILL信号或
kill -9
的结果,手动docker kill会得到这个退出码,或者pod中limit设置过小,导致pod产生OOMKilled,也会间接产生Exit Code 137退出码Exit Code 139:128+11,即SIGSEGV信号或
kill -11
,对应无效的内存引用,一般是代码问题或docker基础镜像问题Exit Code 143:128+15,即SIGTERM或
kill -15
,意味着pod是优雅停机的,比如执行docker stop
。但是如果服务无法处理SIGTERM信号,也会让docker stop
等待10s无响应后,产生137退出码其他信号:126是权限问题或容器启动时执行设置的bash命令不可执行,比如docker file中的ENTRYPOINT命令或CMD命令;127是shell脚本中的错字或无法识别的语句;255是1在某些场景下转换过来的,和1是一个意思
其他响应信号的docker命令
其他的docker命令,例如docker kill
,则是直接发送SIGKILL信号,不给任何优雅停机的机会;docker rm
用于删除已经停机的容器,如果不停机,则无法删除,但一旦使用docker rm -f
,则会先发送SIGKILL信号,再删除容器
docker daemon命令比较特殊,它接收SIGHUP信号,接收后会重新reload daemon.json配置文件
例如执行以下命令:
root@vm10-1-1-28:~# kill -SIGHUP $(pidof dockerd)
# 或者
root@vm10-1-1-28:~# systemctl reload docker
查看docker daemon的日志可以看到,docker daemon接收这个信号并重新reload daemon.json配置文件
root@vm10-1-1-28:~# journalctl -u docker.service -f
-- Logs begin at Sun 2018-01-07 09:17:01 CST. --
Jan 18 16:20:11 vm10-1-1-28.ksc.com dockerd[26668]: time="2018-01-18T16:20:11.262904839+08:00" level=info msg="Got signal to reload configuration, reloading from: /etc/docker/daemon.json"
Jan 18 16:21:41 vm10-1-1-28.ksc.com systemd[1]: Reloading Docker Application Container Engine.
所以修改完/etc/docker/daemon.json
文件后,可以直接给Docker发送一个SIGHUP信号实现配置文件的reload,而不需要重启docker daemon
而systemctl reload docker 命令通常不会导致机器上的容器重启。这个命令的作用是让 Docker 守护进程重新加载其配置文件,而不会中断正在运行的容器。它和 systemctl restart docker 是不同的,后者会停止并重新启动 Docker 服务,从而导致所有容器重启。
JAVA程序对SIGTERM信号的响应
想要真正优雅退出,需要java服务对SIGTERM信号作出响应
Java 中的 Shutdown Hook 提供了比较好的方案。我们可以通过 Java.Runtime.addShutdownHook(Thread hook)
方法向 JVM 注册关闭钩子,在 JVM 退出之前会自动调用执行钩子方法,做一些结尾操作,从而让进程平滑优雅的退出,保证了业务的完整性。
Spring提供了默认的ShutdownHook实现,可以参考AbstractApplicationContext#registerShutdownHook
public void registerShutdownHook() {
if (this.shutdownHook == null) {
// No shutdown hook registered yet.
this.shutdownHook = new Thread(SHUTDOWN_HOOK_THREAD_NAME) {
@Override
public void run() {
synchronized (startupShutdownMonitor) {
doClose();
}
}
};
Runtime.getRuntime().addShutdownHook(this.shutdownHook);
}
}
该方法设置成员变量this.shutdownHook
,然后在执行该方法的时候调用Runtime.getRuntime().addShutdownHook(this.shutdownHook);
添加进去
那么registerShutdownHook什么时候调用呢?
这个就看使用什么框架了,例如自行搭建基于SpringFramework的e2e框架时,可以使用SpringTestListener的实现:
@Override
public void beforeTestClass(TestContext testContext) throws Exception {
ApplicationContext applicationContext = testContext.getApplicationContext();
SpringTestListener.applicationContext = applicationContext;
……
}
参考如下链接:
再比如华为贡献的CSE框架,则是监听了上下文启动的event,直接就注册进去的:
// org.apache.servicecomb.core.CseApplicationListener#onApplicationEvent
public void onApplicationEvent(ApplicationEvent event) {
if (initEventClass.isInstance(event)) {
if (applicationContext instanceof AbstractApplicationContext) {
((AbstractApplicationContext) applicationContext).registerShutdownHook();
}
……
}
}
然后就是看下spring在ShutdownHook里面都做什么,可以看到AbstractApplicationContext#doClose
protected void doClose() {
// Check whether an actual close attempt is necessary...
if (this.active.get() && this.closed.compareAndSet(false, true)) {
……
// 发布关闭事件,调用方可以自行监听
try {
// Publish shutdown event.
publishEvent(new ContextClosedEvent(this));
}
……
// Stop all Lifecycle beans, to avoid delays during individual destruction.
// 这里停止声明周期bean
if (this.lifecycleProcessor != null) {
try {
this.lifecycleProcessor.onClose();
}
catch (Throwable ex) {
logger.warn("Exception thrown from LifecycleProcessor on context close", ex);
}
}
// Destroy all cached singletons in the context's BeanFactory.
// 这里关闭所有单例bean
destroyBeans();
// Close the state of this context itself.
// 然后关闭bean工厂
closeBeanFactory();
// 抽象方法,给子类自行实现
onClose();
// Reset common introspection caches to avoid class reference leaks.
resetCommonCaches();
// Reset local application listeners to pre-refresh state.
if (this.earlyApplicationListeners != null) {
this.applicationListeners.clear();
this.applicationListeners.addAll(this.earlyApplicationListeners);
}
// Switch to inactive.
this.active.set(false);
}
}
k8s优雅停机
k8s在docker的再上层,优雅停机涉及到pod层面和网络层面
网络层面:
Pod 被删除,状态置为 Terminating。
Endpoint Controller 将该 Pod 的 ip 从 Endpoint 对象中删除。
Kube-proxy 根据 Endpoint 对象的改变更新 iptables/ipvs 规则,不再将流量路由到被删除的 Pod。
如果还有其他 Gateway 依赖 Endpoint 资源变化的,也会改变自己的配置(比如 Nginx Ingress Controller)。
Pod 层面:
Pod 被删除,状态置为 Terminating。
Kubelet 捕获到 ApiServer 中 Pod 状态变化,执行 syncPod 动作。
如果 Pod 配置了 preStop Hook ,将会执行。
kubelet 对 Pod 中各个 container 发送调用 cri 接口中 StopContainer 方法,向 dockerd 发送 stop -t 指令,用 SIGTERM 信号以通知容器内应用进程开始优雅停止。
等待容器内应用进程完全停止,如果容器在 gracePeriod 执行时间内还未完全停止,就发送 SIGKILL 信号强制杀死应用进程(容器运行时处理)。
所有容器进程终止,清理 Pod 资源。
参考文档
https://www.cnblogs.com/zhangmingcheng/p/18254613
https://www.cnblogs.com/luciochn/p/14878160.html
评论区