Featured image of post 容器优雅关闭

容器优雅关闭

容器关闭时的信号

容器里面有过个进程的时候,init进程收到的是 SIGTERM 信号,其他进程收到的是 SIGKILL 信号。

主要原因是系统内核在 pid Namespace 下 init 进程的子进程时是发送 SIGKILL 信号,所以这情况下子进程无法捕获终止信号。解决方法是使用像 tini 这样的init进程来管理,tini 能够将 SIGTERM 信号转发给子进程,这样子进程就能接收到 SIGTERM 信号了。

但是 tini 默认情况下是无法转发 SIGTERM 给子子进程的,kill_process_group 默认缺省值,当kill_process_group 没有配置或者为0的时候,tini 就不会将 SIGTERM 转发给子子进程,而是收到 SIGKILL。

容器化程度很高,容器里面只有一个进程,就是应用进程,这样就能避免这个问题,不需要专业的 init 进程。

使用 tini 或其他的特殊的 init 进程管理容器内的进程

为什么其他进程收到的SIGKILL

在用户态的 init 进程调用 exit() 退出后,会调用内核态的 do_exit(),do_exit() 调用 exit_notify() 去通知子进程退出。对于容器来说,容器内的所有进程都是在同一个 pid namespace 下的,所以 exit_notify() 调用的是 zap_pid_ns_processes() 来关闭同一命名空间下的进程。zap_pid_ns_processes() 的源码里面给其他进程发送的是 SIGKILL 信号。对于特权信号,进程无法做到优雅退出。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
    /*
         * The last thread in the cgroup-init thread group is terminating.
         * Find remaining pid_ts in the namespace, signal and wait for them
         * to exit.
         *
         * Note:  This signals each threads in the namespace - even those that
         *        belong to the same thread group, To avoid this, we would have
         *        to walk the entire tasklist looking a processes in this
         *        namespace, but that could be unnecessarily expensive if the
         *        pid namespace has just a few processes. Or we need to
         *        maintain a tasklist for each pid namespace.
         *
         */

        rcu_read_lock();
        read_lock(&tasklist_lock);
        nr = 2;
        idr_for_each_entry_continue(&pid_ns->idr, pid, nr) {
                task = pid_task(pid, PIDTYPE_PID);
                if (task && !__fatal_signal_pending(task))
                        group_send_sig_info(SIGKILL, SEND_SIG_PRIV, task, PIDTYPE_MAX);
        }

tini 是怎么做到向其他进程转发 SIGTERM 信号的

如果容器中使用 tini 作为 init 进程,tini 的 sigtimedwait() 函数来检查自己收到的信号,然后调用 kill() 把信号传递给子进程。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
int wait_and_forward_signal(sigset_t const* const parent_sigset_ptr, pid_t const child_pid) {

        siginfo_t sig;

        if (sigtimedwait(parent_sigset_ptr, &sig, &ts) == -1) {
                switch (errno) {

                }
        } else {
                /* There is a signal to handle here */
                switch (sig.si_signo) {
                        case SIGCHLD:
                                /* Special-cased, as we don't forward SIGCHLD. Instead, we'll
                                 * fallthrough to reaping processes.
                                 */
                                PRINT_DEBUG("Received SIGCHLD");
                                break;
                        default:
                                PRINT_DEBUG("Passing signal: '%s'", strsignal(sig.si_signo));
                                /* Forward anything else */
                                if (kill(kill_process_group ? -child_pid : child_pid, sig.si_signo)) {
                                        if (errno == ESRCH) {
                                                PRINT_WARNING("Child was dead when forwarding signal");
                                        } else {
                                                PRINT_FATAL("Unexpected error when forwarding signal: '%s'", strerror(errno));

                                                return 1;
                                        }
                                }
                                break;
                }
        }
        return 0;
}

容器要做到graceful down,需要给所有的进程来处理自己的退出,而不是强制退出。所以需要接收到的是 SIGTERM 信号,而不是 SIGKILL 信号。或者做到极致的容器化:容器内只有一个进程。

init 进程的关键特点

1、能够将 SIGTERM 信号转发给关键的子进程

2、能够处理zombie进程

3、能够接收外界的 SIGTERM 信号而退出,也可以通过注册 SIGTERM handler,也可以像 tini 一样转发 SIGTERM 该子进程,然后收到 SIGCHILD 后自己退出。

Licensed under CC BY-NC-SA 4.0