全栈工程师开发手册 (作者:栾鹏)
架构系列文章

注意: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> 命令来查看这些事件

Logo

K8S/Kubernetes社区为您提供最前沿的新闻资讯和知识内容

更多推荐