docker stop使用姿势普及及误区

一般来说一个容器的生命周期是从run开始到stop结束的。 很多人在容器不用了以后就调用docker stop命令将其关闭。 例如: sudo docker pull centos:centos6 sudo docker run -t -i centos:centos6 /bin/bash 可以用docker ps查到启动成功的container的id为daf3ae88fec2 sudo docker stop daf3ae88fec2 可以直接关闭容器进程 这样看起来没有任何问题,容器进程也相应的退出了。但是真的是这样子的么,查看stop 的usage如下:

   $ sudo docker stophelp
    Usage: docker stop [OPTIONS] CONTAINER [CONTAINER...]
     Stop a running container by sending SIGTERM and then SIGKILL after a grace period
     -t, –time=10      Number of seconds to wait for the container to stop before killing it. Default is 10 seconds.

可以看出当我们执行docker stop时,docker会首先向容器内的当前主程序发送一个SIGTERM信号,用于容器内程序的退出。如果容器在收到SIGTERM后没有马上退出, 那么stop命令会在等待一段时间(默认是10s)后,再向容器发送SIGKILL信号,将容器杀死,变为退出状态。 这里发现一个潜在的坑,即如果SIGTERM没有把容器内的程序杀死的话。过10s以后docker就会把容器直接kill掉,这个时候容器内部的进程很有可能成为一个孤儿进程。 将这一假象用一个多线程程序来试验一下: 1.编写一个多线程的服务:

func main() {
        go sysSignalHandleDemo()

        pid, _, err := syscall.RawSyscall(syscall.SYS_FORK, 0, 0, 0)
        if err != 0 {
                fmt.Printf("err fork process, err: %v\n", err)
                return
        }

        if pid == 0 {
                fmt.Printf("i am in child process, pid = %v\n", syscall.Getpid())
                time.Sleep(time.Hour) // make the child process wait
        }
        fmt.Printf("i am parent process, pid = %v\n", syscall.Getpid())
        fmt.Printf("fork ok, childpid = %v\n", pid)
        time.Sleep(time.Hour) // make the main goroutine wait!
}

func sysSignalHandleDemo() {
        ss := signalSetNew()
        handler := func(s os.Signal, arg interface{}) {
                fmt.Printf("%v: handle signal: %v\n", syscall.Getpid(), s)
                if s == syscall.SIGTERM {
                        fmt.Printf("%v: signal termiate received, app exit normally\n", syscall.Getpid())
                        os.Exit(0)
                }
        }

        ss.register(syscall.SIGINT, handler)
        ss.register(syscall.SIGUSR1, handler)
        ss.register(syscall.SIGUSR2, handler)
        ss.register(syscall.SIGTERM, handler)

        for {
                c := make(chan os.Signal)
                var sigs []os.Signal
                for sig := range ss.m {
                        sigs = append(sigs, sig)
                }
                signal.Notify(c)
                sig := <-c

                err := ss.handle(sig, nil)
                if err != nil {
                        fmt.Printf("%v: unknown signal received: %v, app exit unexpectedly\n", syscall.Getpid(), sig)
                        os.Exit(1)
                }
        }
}

2.写一个DockerFile如下,还是用centos6作为基础镜像,在里面启动上面的多线程的服务。建立~/ImagesFactory目录

  • 将构建好的dockerapp(步骤1的程序)拷贝到~/ImagesFactory目录下
  • 进入~/ImagesFactory目录,创建Dockerfile文件,Dockerfile内容如下:

  • FROM centos:centos6
    COPY ./dockerapp /bin/
    CMD /bin/dockerapp
    

    3.构建镜像

    sudo docker build -t="test:1" ./ 4.启动容器 sudo docker run -d "test:1"

5.通过docker logs查看dockerapp的输出:


$ sudo docker logs 781cecb4b362
i am parent process, pid = 1
fork ok, childpid = 13
i am in child process, pid = 13

可以看出主进程pid为1,子进程pid为13。我们通过stop停止该容器:

$ sudo docker stop 781cecb4b362
781cecb4b362

再次通过docker logs查看:

$ sudo docker logs 781cecb4b362
i am parent process, pid = 1
fork ok, childpid = 13
i am in child process, pid = 13
1: handle signal: terminated
1: signal termiate received, app exit normally

我们可以看到主进程收到了stop发来的SIGTERM并退出,主进程的退出导致容器退出,导致子进程13也无法生存,并且没有优雅退出。 因此对于docker容器内运行的多进程程序,stop命令只会将SIGTERM发送给容器主进程,要想让其他进程也能优雅退出,需要在主进程与 其他进程间建立一种通信机制。在主进程退出前,等待其他子进程退出。待所有其他进程退出后,主进程再退出,容器停止。这样才能保证服务程序的优雅 退出。 正确的使用方式:

1.主进程退出前需要等待其他子进程退出。

2.比较直接的方式:将docker stop的time参数设置长一点,但是也不能完全保证容器内进程完全退出。


网易云容器服务为用户提供了无服务器容器,让企业能够快速部署业务,轻松运维服务。容器服务支持弹性伸缩、垂直扩容、灰度升级、服务发现、服务编排、错误恢复及性能监测等功能。点击免费试用

本文来自网易实践者社区,经作者崔晓晴授权发布。