[ docker-ce源码分析系列 ]docker exec残留sh进程的原因

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值
[ docker-ce源码分析系列 ]docker exec残留sh进程的原因_第1张图片


4 总结:

用户通过docker exec命令进入容器创建的sh进程,dockerd在代码层面是没有机会往sh进程发送系统信号(虽然containerd进程已经暴露了往进程发送信号的接口),因此sh进程不执行exit指令,是会残留在操作系统中的。

你可能感兴趣的:(docker,golang,docker,golang)