docker/k8s容器优雅退出(python,go,java)
docker kill 直接杀死容器进程docker stop是向容器进程发送SIGTERM信号,本文介绍容器中的进程捕获 SIGTERM 信号,优雅的退出。先来了解一下信号SIGINT程序终止(interrupt)信号, 在用户键入INTR字符(通常是Ctrl-C)时发出,用于通知前台进程组终止进程。SIGQUIT和SIGINT类似, 但由QUIT字符(通常是Ctrl-)来控制. ...
全栈工程师开发手册 (作者:栾鹏)
架构系列文章
注意:docker 正常退出和异常退出都不会自动发起SIGTERM,java正常或异常退出,jvm会发起SIGTERM信号
docker kill 直接杀死容器进程
docker stop是向容器进程发送SIGTERM信号,本文介绍容器中的进程捕获 SIGTERM 信号,优雅的退出。
github地址:https://github.com/626626cdllp/k8s/tree/master/test/docker-signal
先来了解一下信号
SIGINT
程序终止(interrupt)信号, 在用户键入INTR字符(通常是Ctrl-C)时发出,用于通知前台进程组终止进程。
SIGQUIT
和SIGINT类似, 但由QUIT字符(通常是Ctrl-)来控制. 进程在因收到SIGQUIT退出时会产生core文件, 在这个意义上类似于一个程序错误信号。
SIGTERM
程序结束(terminate)信号, 与SIGKILL不同的是该信号可以被阻塞和处理。通常用来要求程序自己正常退出,shell命令kill缺省产生这个信号。如果进程终止不了,我们才会尝试SIGKILL。
SIGSTOP
停止(stopped)进程的执行. 注意它和terminate以及interrupt的区别:该进程还未结束, 只是暂停执行. 本信号不能被阻塞, 处理或忽略.
添加退出处理逻辑
假设源dockerfile中定义的entrypoint为python /app/server.py
现在需要重新写一个脚本entrypoint.sh
#!/bin/sh
# 实际运行的工作程序,将源主程序一般后台运行形势打开
nohup python /app/server.py &
# 中断信号处理函数,关闭python主程序
prog_exit()
{
ps -ef| grep python |grep -v grep |awk '{print $2}'|xargs kill -15
}
# 注册中断处理函数
trap "prog_exit" 15
flag=1
# 前端形式运行睡眠,并交叉是否可以退出(python进程已不存在,也能在python进程自己退出的情况下关闭容器)
while [ $flag -ne 0 ];do
sleep 3;
flag=`ps -ef| grep atm |grep -v grep | wc -l`
done;
然后将dockerfile中的entrypoint改成启动新的脚本
FROM ubuntu:18.04
RUN apt-get update && \
apt-get install -y python3.6 && \
rm -rf /var/lib/apt/lists/*
COPY ./server.py /data/
COPY ./entrypoint.sh /data/
WORKDIR /data/
# 使用sh做优雅退出
ENTRYPOINT ["/data/entrypoint.sh"]
CMD ["bash"]
# 直接向python发起退出命令
# ENTRYPOINT ["python3.6","/data/server.py"]
在业务代码中添加退出处理逻辑(python)
优雅退出的目的一般为处理业务没有处理完的事情。则一般需要在业务代码中处理退出信号。比如在上面的server.py中添加处理逻辑。
第一种方式,
上面我们使用ps -ef| grep python |grep -v grep |awk '{print $2}'|xargs kill -15
命令向python服务发起了kill信号,我们可以在python脚本中也接收这个bash发起的kill信号。
第二种方式,
我们直接在业务代码中接收容器发起的SIGTERM 信号(方法更直接),这样dockerfile中就还是使用源入口点
ENTRYPOINT ["python3.6","/data/server.py"]
比如我们可以这样编写server.py
import signal
import time
is_running = True
def sigint_handler(num, stack):
print(num,stack)
print('receive sigint')
global is_running
is_running = False
def sigterm_handler(num, stack):
print(num,stack)
print('receive sigterm')
global is_running
is_running = False
def main():
signal.signal(signal.SIGINT, sigint_handler)
signal.signal(signal.SIGTERM, sigterm_handler)
signal.siginterrupt(signal.SIGINT, False)
signal.siginterrupt(signal.SIGTERM, False)
while is_running:
print('begin sleep')
# 启动你的业务函数
time.sleep(3)
print("prepare exit")
print("sleep 10")
time.sleep(10)
print("exit")
if __name__ == "__main__":
main()
执行python3.6 server.py将看到这样的日志
begin sleep
begin sleep
begin sleep
15 <frame object at 0x2371b18>
receive sigterm
prepare exit
sleep 10
go语言接收退出信号
关于Go语言对系统Signal的处理,可以参考《Go中的系统Signal处理》一文。
https://tonybai.com/2014/10/09/gracefully-shutdown-app-running-in-docker/
java语言接收退出信号
https://blog.csdn.net/zhangpeterx/article/details/89454050
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
// import com.lmax.disruptor.LifecycleAware;
//要注意让java进程的pid为1,不然docker stop信号接收不到
public class demo {
public static void main(String[] args) {
Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Exited!");
}
}));
System.out.println("begin!");
try{
Thread.sleep(1000*60);
} catch (Exception e) {
e.printStackTrace();
}
}
}
k8s容器生命周期钩子
参考https://kubernetes.io/zh/docs/concepts/containers/container-lifecycle-hooks/
有两个钩子暴露在容器中:
PostStart
这个钩子在创建容器之后立即执行。 但是,不能保证钩子会在容器入口点之前执行。 没有参数传递给处理程序。
PreStop
在容器终止之前是否立即调用此钩子,取决于 API 的请求或者管理事件,类似活动探针故障、资源抢占、资源竞争等等。 如果容器已经完全处于终止或者完成状态,则对 preStop 钩子的调用将失败。 它是阻塞的,同时也是同步的,因此它必须在删除容器的调用之前完成。 没有参数传递给处理程序
k8s示例
# Source: superset/templates/secret.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: python-config
data:
pre_start.py: |-
import time
import os
if __name__=='__main__':
print('pre start process %s.' % os.getpid())
print('end')
time.sleep(10)
print('Process start.')
pre_stop.py: |-
import time
import os
if __name__=='__main__':
print('pre stop process %s.' % os.getpid())
print('end')
print('Process end.')
---
apiVersion: v1
kind: Pod
metadata:
labels:
app: python
name: python-pod
spec:
volumes:
- name: python-configmap
configMap:
name: python-config
items:
- key: pre_stop.py
path: pre_stop.py
- key: pre_start.py
path: pre_start.py
containers:
- command: ['xxxxx','xxxxx']
name: python
workingDir: /
image: xxxxxxxxx
lifecycle:
postStart:
exec:
command: ["/bin/bash","-c","python /pre_start.py >> pre_start.txt"]
preStop:
exec:
command: ["/bin/bash","-c","python /pre_stop.py >> pre_stop.txt"]
volumeMounts:
- name: python-configmap
mountPath: /pre_stop.py
subPath: pre_stop.py
- name: python-configmap
mountPath: /pre_start.py
subPath: pre_start.py
钩子处理程序的实现(可能会重复触发)
容器可以通过实现和注册该钩子的处理程序来访问该钩子。 针对容器,有两种类型的钩子处理程序可供实现:
- Exec - 执行一个特定的命令,例如 pre-stop.sh,在容器的 cgroups 和名称空间中。 命令所消耗的资源根据容器进行计算。
- HTTP - 对容器上的特定端点执行 HTTP 请求
对于PostStart 钩子,容器入口点和钩子异步触发。两个都运行完成,容器才进入running状态。
对于PreStop钩子。如果PreStop钩子在执行过程中挂起,Pod 阶段将保持在 Terminating 状态,并在 Pod 结束的 terminationGracePeriodSeconds 之后被杀死。 也就是说PreStop钩子的最多运行时间为terminationGracePeriodSeconds属性配置的值。
如果 PostStart 或 PreStop 钩子失败,它会杀死容器。
钩子处理程序的日志不会在 Pod 事件中公开。 如果处理程序由于某种原因失败,它将播放一个事件。 对于 PostStart,这是 FailedPostStartHook 事件,对于 PreStop,这是 FailedPreStopHook 事件。 您可以通过运行 kubectl describe pod <pod_name>
命令来查看这些事件
更多推荐
所有评论(0)