Signals-in-Container

可能因为天气变热, 最近几天有些烦躁不安. 一个大脑的我, 果真处理不了多任务.

这篇博客讲一下docker对于容器的信号处理, 包括 docker stop, docker kill, 以及docker挂掉的时候, 容器会产生什么样的反映.

起因

事情的起因是前天晚上公司大楼电力检修, 机房停机了, 第二天上班的时候, 有同事问我, docker stop的时候, 容器没有正常关闭是为什么, 关机是正常关机, docker服务也都是正常停的(service docker stop),但是有一个mysql的容器没有正常停, 里面的数据没有落盘就死了, 导致了数据丢失和脏数据.

我一时半会儿还真没法回答, 之前有看过相关知识, 说docker停止容器会向进程号为1的进程发送SIGTERM,但还真不知道机器停止的时候是怎样一种情况, 毕竟理论是理论, 实践才出真知.

经过

然后就做了这样一个容器实验:

FROM golang:alpine
COPY main.go /
RUN go build -o /bin/sig /main.go
CMD sig
package main

import (
	"log"
	"os"
	"os/signal"
	"syscall"
)

func main() {
	log.Printf("Service started, pid: [ %d ]", os.Getpid())
	defer log.Println("Service stopped")

	sigChan := make(chan os.Signal)

	signal.Ignore()
	signal.Notify(sigChan)

	for sig := range sigChan {
		log.Printf("Got signal [ %s ]\n", sig.String())

		if sig == syscall.SIGUSR2 {
			log.Println("quit")
			return
		}
	}

}
➜  signal vi Dockerfile 
➜  signal docker build -t signal-test .
Sending build context to Docker daemon  3.072kB
Step 1/4 : FROM golang:alpine
 ---> 05fe62871090
Step 2/4 : COPY main.go /
 ---> d6e384ba7dd7
Removing intermediate container f5cad46ca980
Step 3/4 : RUN go build -o /bin/sig /main.go
 ---> Running in 50dc18e872b8
 ---> 9439214de404
Removing intermediate container 50dc18e872b8
Step 4/4 : CMD sig
 ---> Running in a9a2eeee310f
 ---> fe7caf1222ab
Removing intermediate container a9a2eeee310f
Successfully built fe7caf1222ab
Successfully tagged signal-test:latest
➜  signal

跑起来:

➜  signal docker run --rm -ti --name sig signal-test
2018/05/05 14:58:11 Service started, pid: [ 6 ]
2018/05/05 14:58:11 Got signal [ window changed ]
^C2018/05/05 14:58:13 Got signal [ interrupt ]
^C2018/05/05 14:58:14 Got signal [ interrupt ]
^C2018/05/05 14:58:14 Got signal [ interrupt ]
➜  signal 

直接Ctrl+C是不管用的, 停止的话需要kill: docker kill sig

发现了两个问题:

  1. 进程的PID不是1
  2. docker stop sig的时候, 等待N秒后, 没有任何日志输出, 直接退出了

然后就忽然意识到!!! 用CMD启动的时候, 其实是sh调用命令执行的!

➜  signal docker exec sig ps aux
PID   USER     TIME   COMMAND
    1 root       0:00 /bin/sh -c sig
    6 root       0:00 sig
   12 root       0:00 ps aux
➜  signal 

所以, 任何信号都是发给了sh, 而不是sig程序, 而sh又不会将信号传递给sig, 所以就相当于, sh在这里劫持了所有docker daemon发来的signal, 容器内的进程也就不会正常退出了.

说到这里, 其实问题已经得到解决, 更改dockerfile:

FROM golang:alpine
COPY main.go /
RUN go build -o /bin/sig /main.go
# 或者用 `ENTRYPOINT sig` 但是要谨慎
# 需要了解 CMD 和 ENTRYPOINT 的区别
# 避免给自己挖坑 TAT
CMD exec sig

这样就将sig进程号变为了1, 然后向其发送不同的signal测试:

➜  signal docker exec sig ps aux
PID   USER     TIME   COMMAND
    1 root       0:00 sig
   13 root       0:00 ps aux
➜  signal docker kill --signal=INT sig 
sig
➜  signal docker kill --signal=ABRT sig 
sig
➜  signal docker kill --signal=HUP sig 
sig
➜  signal docker kill --signal=USR1 sig 
sig
➜  signal docker kill --signal=USR2 sig
sig
➜  signal 

首先可以看到, 容器内的确是两个进程了, 然后观察一下日志:

➜  signal docker run --rm -ti --name sig signal-test
2018/05/05 15:08:05 Service started, pid: [ 1 ]
2018/05/05 15:08:05 Got signal [ window changed ]
2018/05/05 15:08:33 Got signal [ interrupt ]
2018/05/05 15:08:42 Got signal [ aborted ]
2018/05/05 15:08:47 Got signal [ hangup ]
2018/05/05 15:08:54 Got signal [ user defined signal 1 ]
2018/05/05 15:08:57 Got signal [ user defined signal 2 ]
2018/05/05 15:08:57 quit
2018/05/05 15:08:57 Service stopped
➜  signal 

再来看下docker stop sig:

➜  signal docker logs -f sig && date +%T
2018/05/05 15:31:52 Service started, pid: [ 1 ]
2018/05/05 15:33:05 Got signal [ terminated ]
23:33:15
➜  signal 

可以看到, 在收到sigterm后, 经过了20s, 容器直接停掉了.

在来指定一下参数:

➜  signal docker stop --time=3 sig
sig
➜  signal 
➜  signal docker run -dti --name sig signal-test && docker logs -f sig && date +%T
508576da35186a055a197266a50bff4e511bb63b46692e3a622267c3d37ed16e
2018/05/05 15:35:00 Service started, pid: [ 1 ]
2018/05/05 15:35:16 Got signal [ terminated ]
23:35:19
➜  signal

可以看到, 容器的确在3s之后停止了(也就是被kill掉了).

(刚才也试了一下 docker rm -f sig, 发现等同于docker kill + docker rm)


那么在docker停止的时候会发生什么呢? (终于回到主题了)

➜  signal echo "stopping at" $(date +%T) && sudo service docker stop && echo "stopped at" $(date +%T)
stopping at 23:42:48
stopped at 23:43:00
➜  signal 
➜  signal docker run -dti --name sig signal-test && docker logs -f sig && echo "killed at" $(date +%T)
6b70779245b0e6756eeec9d45160436f1f4bc84652563729c935b89e40e5d87e
2018/05/05 15:42:19 Service started, pid: [ 1 ]
2018/05/05 15:42:48 Got signal [ terminated ]
killed at 23:42:58
➜  signal 

可以验证之前的猜想, docker正常停止的时候, 首先会向容器发送SIGTERM, 等待10s无果后, 直接kill(SIGKILL).

那如果是重启呢?

用虚拟机试一下:

➜  signal sssh root@192.168.57.3
Last login: Sat May  5 12:09:06 2018 from 192.168.57.1
[root@localhost ~]# mkdir signal && cd signal && vi main.go Dockerfile
2 files to edit
[root@localhost signal]# docker build -t sig-img . && docker run -dti --name sig --restart=always sig-img
Sending build context to Docker daemon  3.072kB
Step 1/4 : FROM golang:alpine
 ---> 05fe62871090
Step 2/4 : COPY main.go /
 ---> 7bd40c0c5f8e
Step 3/4 : RUN go build -o /bin/sig /main.go
 ---> Running in fc07155af69a
Removing intermediate container fc07155af69a
 ---> 2f3c9e87653d
Step 4/4 : CMD exec sig
 ---> Running in 43c09f16fc8f
Removing intermediate container 43c09f16fc8f
 ---> b4a3e72e0298
Successfully built b4a3e72e0298
Successfully tagged sig-img:latest
bb9d116d1f86c0ced4fa66a74cc6283c46d2b76cb3df82e851a498b141c3c337
[root@localhost signal]# docker logs sig
2018/05/05 16:21:17 Service started, pid: [ 1 ]
[root@localhost signal]# reboot
Shared connection to 192.168.57.3 closed.
➜  signal 
➜  signal sssh root@192.168.57.3
root@192.168.57.3's password: 
Last login: Sat May  5 12:23:08 2018
[root@localhost ~]# docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS               NAMES
6b1b466ddd23        sig-img             "/bin/sh -c 'exec si…"   2 minutes ago       Up 55 seconds                           sig
[root@localhost ~]# docker logs sig
2018/05/05 16:21:17 Service started, pid: [ 1 ]
2018/05/05 16:22:10 Got signal [ terminated ]
2018/05/05 16:22:48 Service started, pid: [ 1 ]
[root@localhost ~]# 

也可以看到, 机器重启的时候, dockerd首先会向容器发送SIGTERM, 超时kill.

结果

如果想解决docker退出, 容器内进程没有正常退出的话, 有两种解决方案, 一个是更改Dockerfile, 将初始化脚本中, 执行命令的部分改成 exec command, 用这种方式将进程移交出去, 使得应用进程(mysql/nginx)能够接收并处理信号; 另一种方式是直接将容器的ENTRYPOINT改掉, 这两种方式的本质都是一样的, 即转移进程, 让容器内应用直接接收信号.

那么还一种方案是, 在容器内安装supervisord, 让其托管应用, 并通过上面的两种方案, 将supervisiord的进程设置为root进程. 这种方法的场景是, 容器内运行多个业务应用(当然这是不推荐的), 然后supervisord接受到SIGTERM后, 会将信号传递给其下的应用. 这样里面的进程也可以接受到信号并处理了.

题外话

学到了一个新的docker daemon配置项, 可以在docker daemon死掉的时候保持容器正常运行, 这样就不会导致升级/重启docker或者docker异常退出的时候, 所引起的容器重启:

{
  "live-restore": true
}

扩展阅读:

更新

刚认真读了一下扩展阅读的第一篇文章, 发现人家还提到了一种方法:

FROM golang:alpine
COPY main.go /
RUN go build -o /bin/sig /main.go
CMD ["sig"]
➜  signal docker ps 
CONTAINER ID        IMAGE               COMMAND             CREATED              STATUS              PORTS               NAMES
713d16b31033        signal-test         "sig"               About a minute ago   Up About a minute                       sig
➜  signal
➜  signal docker exec sig ps aux                                                                     
PID   USER     TIME   COMMAND
    1 root       0:00 sig
   17 root       0:00 ps aux
➜  signal 

可以看到容器内部的root进程是sig, CMD也是sig, 同样达到了 exec sig 的效果, 可以说是非常简单明了了.

所以在这里又得出一条结论, 使用CMD的时候(推荐使用CMD而不是ENTRYPOINT), 将命令和参数用[]包裹起来, 这样docker执行的时候就会使用你指定的命令. 需要注意的一点是, 被执行的文件需要有可执行权限, chmod +x一下.

comments powered by Disqus