1 概述:
1.1 环境
版本信息如下: a、操作系统: centos 7.6,amd64 b、服务器docker版本:v18.09.2 c、docker的存储驱动: overlay2
2 现象:
执行 [ docker exec -it 容器ID sh ]命令,用户可在容器中执行shell指令进行各种操作,此时用户直接kill掉docker exec命令,或者直接关闭xshell,则该sh进程依然残留在容器中,这种sh进程会消耗虚拟终端的数量,本质上是消耗文件描述符。如果用户退出时不是通过执行exit指令,这种方式会导致操作系统上的虚拟终端数消耗完毕,需要手动杀死残留在容器中的sh进程或增大内核的虚拟终端最大数量。
查看当前系统正在使用的虚拟终端数量的命令:
sysctl kernel.pty.nr
修改虚拟终端数量最大值的命令:
sysctl -w kernel.pty.max=8192
3 源码简析:
通过docker源码,分析目标进程(sh进程)为什么会残留。杀死进程,肯定是通过发送系统信号TERM或信号KILL,个人估计发送系统信号相关的业务逻辑应该是在错误处理代码块中。
3.1 服务端注册路由initRoutes()
func (r *containerRouter) initRoutes() {
r.routes = []router.Route{
/*
其他方法
*/
// 本方法主要是返回一个exec ID,客户端会再发起这样的请求:/v1.39/exec/{exec ID}/start
router.NewPostRoute("/containers/{name:.*}/exec", r.postContainerExecCreate),
router.NewPostRoute("/exec/{name:.*}/start", r.postContainerExecStart),
}
}
3.2 func (s *containerRouter) postContainerExecStart(…)
本方法的核心方法是s.backend.ContainerExecStart(…)。
func (s *containerRouter) postContainerExecStart(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
/*
其他逻辑
*/
// ContainerExecStart(...)会一直阻塞,直到docker exec命令关闭
if err := s.backend.ContainerExecStart(context.Background(), execName, stdin, stdout, stderr); err != nil {
if execStartCheck.Detach {
return err
}
stdout.Write([]byte(err.Error() + "\r\n"))
logrus.Errorf("Error running exec %s in container: %v", execName, err)
}
return nil
}
3.3 func (d *Daemon) ContainerExecStart(…)
docker exec意外退出(通过直接关闭xshell窗口或者kill命令),或者在容器中执行exit指令,dockerd都不会调用containerd的接口来给目标进程发送系统信号,因此导致目标进程的残留。
func (d *Daemon) ContainerExecStart(ctx context.Context, name string, stdin io.Reader, stdout io.Writer, stderr io.Writer) (err error) {
/*
其他逻辑
*/
// attachErr是一个管道,用于后续select语句
attachErr := ec.StreamConfig.CopyStreams(ctx, &attachConfig)
// d.containerd.Exec()是通过grpc调用containerd进程的接口,以在目标容器中创建进程(例如sh)
// systemPid是容器中新建的进程在root pid namespace中的pid
systemPid, err := d.containerd.Exec(ctx, c.ID, ec.ID, p, cStdin != nil, ec.InitializeStdio)
/*
其他逻辑
*/
// 一直select语句阻塞
// docker exec退出,或者在容器中执行exit指令,管道attachErr都会返回nil值
select {
case <-ctx.Done():
// 本方法的入参ctx对象,来自context.Background(),因此分支是没机会进入。
// 本方法的入参ctx对象,来自context.Background(),因此分支是没机会进入。
// 本方法的入参ctx对象,来自context.Background(),因此分支是没机会进入。
/*
调用d.containerd.SignalProcess(...)方法,往目标进程(例如sh进程)发送TERM或KILL信号
*/
return ctx.Err()
case err := <-attachErr:
// 此分支没有实际的业务操作,只是日志记录。
// 因此目标进程(例如sh进程)不会接收到任何系统信号,残留在了容器中。
if err != nil {
if _, ok := err.(term.EscapeError); !ok {
return errdefs.System(errors.Wrap(err, "exec attach failed"))
}
attributes := map[string]string{
"execID": ec.ID,
}
d.LogContainerEventWithAttributes(c, "exec_detach", attributes)
}
}
return nil
}
docker exec意外退出(通过直接关闭xshell窗口或者kill命令),或者在容器中执行exit指令,管道attachErr都会返回nil值
4 总结:
用户通过docker exec命令进入容器创建的sh进程,dockerd在代码层面是没有机会往sh进程发送系统信号(虽然containerd进程已经暴露了往进程发送信号的接口),因此sh进程不执行exit指令,是会残留在操作系统中的。
|