Skip to Content

Signals-in-Container

Posted on

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

这篇博客讲一下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