docker中run的示例分析

这篇文章给大家分享的是有关docker中run的示例分析的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。

docker源码相关

通过在/components/cli/command/commands.go里,抽象出各种命令的初始化操作。

使用第三方库"github.com/spf13/cobra"

  1. docker run 初始化命令行终端解析参数,最终生成 APIclient发出REQUEST请求给docker daemon.

  • docker daemon的初始化,设置了server的监听地址,初始化化routerSwapper, registryService 以及layStore、imageStore、volumeStore等各种存储 。

  • docker run的命令解析为 docker container create 和 container start 两次请求:

    • 其中container create 不涉及底层containerd的调用,首先将host.config 、networkingConfig和AdjustCPUShares等组装成一个客户端请求,发送到docker daemon注册该容器。该请求会完成拉取image, 以及初始化 baseContainer的RWlayer, config文件等,之后daemon就可以通过containerid来使用该容器。

    • container start 命令的核心是调用了daemon的containerStart(),它会完成

    • 调用containerd进行create容器,调用libcontainerd模块 clnt *client 的初始化,

    1. 设置容器文件系统,挂载点: /var/lib/docker/overlay/{container.RWLayer.mountID}/merged

    2. 设置容器的网络模式,调用libnetwork ,CNM模型(sandbox, endpoint,network)

    3. 创建/proc /dev等spec文件,对容器所特有的属性进行设置,

    4. 调用containerd进行create容器

    container.create
    1)获取libcontainerd模块中的containers
    
    2)获取gid和uid
    
    3)创建state目录,配置文件路径。
    
    4)创建一个containercommon对象,创建容器目录,以及配置文件路径,根据spec创建配置文件。
    container.start
    1) 读取spec对象的配置文件
    
    2) 创建一个fifo的pipe
    
    3) 定义containerd的请求对象,grpc调用containerd模块。
    
    ctr.client.remote.apiClient.CreateContainer(context.Background(), r)
    
    4)启动成功后,更新容器状态。
    daemon 启动libcontainerd ,作为grpc的server。
    1. cmd/dockerd/daemon.go 中存在libcontainerd初始化的流程。

      包括启动grpc服务器,对套接字进行监听。

      通过grpc.dail 与grpc server建立连接conn, 根据该链接建立apiclient对象,发送json请求。

    2. runContainerdDaemon

      通过docker-containerd二进制与grpc server进行通信,

      docker-containerd -l unix:///var/run/docker/libcontainerd/docker-containerd.sock --metrics-interval=0 --start-timeout 2m --state-dir /var/run/docker/libcontainerd/containerd --shim docker-containerd-shim --runtime docker-runc

      执行结果的输入输出流重定向到docker daemon。

       runc把state.json文件保存在容器运行时的状态信息,默认存放在/run/runc/{containerID}/state.json。


    containerd源码相关

    type Supervisor struct {
    	// stateDir is the directory on the system to store container runtime state information.
    	stateDir string
    	// name of the OCI compatible runtime used to execute containers
    	runtime     string
    	runtimeArgs []string
    	shim        string
    	containers  map[string]*containerInfo
    	startTasks  chan *startTask //这是containerd到runc的桥梁,由func (w *worker) Start()消费
    	// we need a lock around the subscribers map only because additions and deletions from
    	// the map are via the API so we cannot really control the concurrency
    	subscriberLock sync.RWMutex
    	subscribers    map[chan Event]struct{}
    	machine        Machine
    	tasks          chan Task //所有来自于docker-daemon的request都会转化为event存放到这,由func (s *Supervisor) Start()消费
    	monitor        *Monitor
    	eventLog       []Event
    	eventLock      sync.Mutex
    	timeout        time.Duration
    }
    type startTask struct {
    	Container      runtime.Container
    	CheckpointPath string
    	Stdin          string
    	Stdout         string
    	Stderr         string
    	Err            chan error
    	StartResponse  chan StartResponse
    }

    我们知道containerd作为docker daemon的grpc server端,通过接收 apiclient request转化成对应的events,在不同的子系统distribution , bundles , runtime 中进行数据的流转,包括镜像上传下载,镜像打包和解压,运行时的创建销毁等。

    其中containerd 核心组件包括 supervisor 和executor, 数据流如下:

    docker-daemon
    --->tasks chan Task
     --->func (s *Supervisor) Start()消费
       --->存放到startTasks  chan *startTask
          -->func (w *worker) Start()消费

    containerd的初始化

    docker-containerd初始化包括 新建Supervisor对象:

    1. 该对象会启动10个worker,负责处理创建新容器的任务(task)。

    2. supervisor的初始化,包括startTask chan初始化,启动监控容器进程的monitor

    3. 一个worker包含一个supervisor和sync.waitgroup,wg用于实现容器启动。

    4. supervisor的start,消费tasks,把task中的container数据组装成runtime.container, 封装到type startTask struct,发送到startTask chan队列。

    5. 启动grpc server(startServer),用来接收dockerd的request请求。

    func daemon(context *cli.Context) error {
    	s := make(chan os.Signal, 2048)
    	signal.Notify(s, syscall.SIGTERM, syscall.SIGINT)
    	/*
    		新建一个supervisor,这个是containerd的核心部件
    			==>/supervisor/supervisor.go
    				==>func New
    	*/
    	sv, err := supervisor.New(
    		context.String("state-dir"),
    		context.String("runtime"),
    		context.String("shim"),
    		context.StringSlice("runtime-args"),
    		context.Duration("start-timeout"),
    		context.Int("retain-count"))
    	if err != nil {
    		return err
    	}
    	wg := &sync.WaitGroup{}
    	/*
    		supervisor 启动10个worker
    			==>/supervisor/worker.go
    	*/
    	for i := 0; i < 10; i++ {
    		wg.Add(1)
    		w := supervisor.NewWorker(sv, wg)
    		go w.Start()
    	}
    	//启动supervisor
    	if err := sv.Start(); err != nil {
    		return err
    	}
        // Split the listen string of the form proto://addr
    	/*
    		根据参数获取监听器
    		listenSpec的值为 unix:///var/run/docker/libcontainerd/docker-containerd.sock
    	*/
    	listenSpec := context.String("listen")
    	listenParts := strings.SplitN(listenSpec, "://", 2)
    	if len(listenParts) != 2 {
    		return fmt.Errorf("bad listen address format %s, expected proto://address", listenSpec)
    	}
    	/*
    		启动grpc server端
    	*/
    	server, err := startServer(listenParts[0], listenParts[1], sv)
    	if err != nil {
    		return err
    	}

    其中startServer负责启动grpc server,监听docker-containerd.sock,声明注册路由handler。

    1. 当CreateContainer handler接收到一个Request之后,会把其转化成type startTask struct,将其转化为一个StartTask 事件,其中存放创建容器的request信息。

    2. 通过s.sv.SendTask(e)将该事件发送给supervosior 主循环。

    // SendTask sends the provided event to the the supervisors main event loop
    /*
    	SendTask将evt Task发送给 the supervisors main event loop
    	所有来自于docker-daemon的request都会转化为event存放到这,生产者
    */
    func (s *Supervisor) SendTask(evt Task) {
    	TasksCounter.Inc(1) //任务数+1
    	s.tasks <- evt
    }
    1. 等待woker.Start()消费处理结果后,将StartResponse返回给docker-daemon。

    supervisor.start

    负责将每一个request转化成特定的task类型,通过一个goroutine遍历task中所有的任务并进行处理。消费tasks,把task中的container数据组装成runtime.container, 封装到type startTask struct,发送到startTask chan队列。

    worker.start

    负责调用containerd-shim, 监控容器中的进程,并把结果返回给StartResponse chan队列。

    其中,

    1. container.Start() 通过containerd-shim 调用runc create {containerID}创建容器。

       process, err := t.Container.Start(t.CheckpointPath, runtime.NewStdio(t.Stdin, t.Stdout, t.Stderr))
      
       其中值得注意的是,container.start 和container.exec均是调用createcmd,exec 命令则是通过process.json中的相关属性来判断是Start()还是Exec(),最后组装成containerd-shim的调用命令。
      
       当具体容器内进程pid生成(由runc生成)后,createCmd会启动一个go routine来等待shim命令的结束。 shim命令一般不会退出。 当shim发生退出时,如果容器内的进程仍在运行,则需要把该进程杀死;如果容器内进程已经不存在,则无需清理工作。


    2. process.Start() 通过调用runc start {containerID}命令启动容器的init进程

    root@idc-gz:/var/run/docker/libcontainerd# tree -L 2 eb347b7e27ecbc01f009971a13cb1b24a89baad795f703053de26d9722129039/
    eb347b7e27ecbc01f009971a13cb1b24a89baad795f703053de26d9722129039/
    ├── 95de4070f528e1d68c80142f679013815a2d1a00da7858c390ad4895b8f8991b-stdin
    ├── 95de4070f528e1d68c80142f679013815a2d1a00da7858c390ad4895b8f8991b-stdout
    ├── config.json
    ├── dc172589265f782a476af1ed302d3178887d078c737ff3d18b930cbc143e5fd5-stdin
    ├── dc172589265f782a476af1ed302d3178887d078c737ff3d18b930cbc143e5fd5-stdout
    ├── ef00cfa54bf014e3f732af3bda1f667c9b0f79c0d865f099b1bee014f0834844-stdin
    ├── ef00cfa54bf014e3f732af3bda1f667c9b0f79c0d865f099b1bee014f0834844-stdout
    ├── init-stdin
    └── init-stdout
    root@idc-gz:/var/run/docker/libcontainerdcontainerd# tree -L 2 eb347b7e27ecbc01f009971a13cb1b24a89baad795f703053de26d9722129039/
    eb347b7e27ecbc01f009971a13cb1b24a89baad795f703053de26d9722129039/
    ├── dc172589265f782a476af1ed302d3178887d078c737ff3d18b930cbc143e5fd5
    │   ├── control
    │   ├── exit
    │   ├── log.json
    │   ├── pid
    │   ├── process.json
    │   ├── shim-log.json
    │   └── starttime
    ├── ef00cfa54bf014e3f732af3bda1f667c9b0f79c0d865f099b1bee014f0834844
    │   ├── control
    │   ├── exit
    │   ├── log.json
    │   ├── pid
    │   ├── process.json
    │   ├── shim-log.json
    │   └── starttime
    ├── init
    │   ├── control
    │   ├── exit
    │   ├── log.json
    │   ├── pid
    │   ├── process.json
    │   ├── shim-log.json
    │   └── starttime
    └── state.json

    runc源码相关

    runc create

    在源码create.go中,首先会加载config.json的配置,然后调用startContainer函数,其流程包括:

    1. createContainer, 生成libcontainer.Container对象,状态处于stopped、destoryed。

    • 调用loadFactory方法, 生成一个libcontainer.Factory对象。

    • 调用factory.Create()方法,生成libcontainer.Container

  • 把libcontainer.Container封装到type runner struct对象中。

    • runner.run负责将config.json设置将来在容器中启动的process,设置iopipe和tty

    • runc create ,调用container.Start(process)

    1. linuxContainer.newParentPorcess组装要执行的parent命令, 组装出来的命令是/proc/self/exe init, 通过匿名管道让runc create 和runc init进行通信。

    2. parent.start()会根据parent的类型来选择对应的start(),自此之后,将进入/proc/self/exe init,也就是runc init

    3. 将容器状态持久化到state.json,此时容器状态为created.

  • runc start,调用container.Run(process)

  • // LinuxFactory implements the default factory interface for linux based systems.
    type LinuxFactory struct {
    	// Root directory for the factory to store state.
    	/*
    		factory 存放数据的根目录  默认是 /run/runc
    		而/run/runc/{containerID} 目录下,会有两个文件:
              一个是管道exec.fifo
    		  一个是state.json
    	*/
    	Root string
    
    	// InitArgs are arguments for calling the init responsibilities for spawning
    	// a container.
    	/*
    		用于设置 init命令 ,固定是 InitArgs:  []string{"/proc/self/exe", "init"},
    	*/
    	InitArgs []string
    
    	// CriuPath is the path to the criu binary used for checkpoint and restore of
    	// containers.
    	// 用于checkpoint and restore
    	CriuPath string
    
    	// Validator provides validation to container configurations.
    	Validator validate.Validator
    
    	// NewCgroupsManager returns an initialized cgroups manager for a single container.
    	// 初始化一个针对单个容器的cgroups manager
    	NewCgroupsManager func(config *configs.Cgroup, paths map[string]string) cgroups.Manager
    }
    
    // 一个容器负责对应一个runner
    type runner struct {
    	enableSubreaper bool
    	shouldDestroy   bool
    	detach          bool
    	listenFDs       []*os.File
    	pidFile         string
    	console         string
    	container       libcontainer.Container
    	create          bool
    }

    runc init

    runc create clone出一个子进程,namespace与父进程隔离,子进程中调用/proc/self/exe init进行初始化。

    runc init的过程如下:

    1. 调用factory.StartInitialization();

      1. 配置容器内部网络,路由,初始化mount namespace, 调用setupRootfs在新的mount namespaces中配置设备、挂载点以及文件系统。

      2. 配置hostname, apparmor,processLabel,sysctl, readyonlyPath, maskPath.

      3. 获取父进程的退出信号,通过管道与父进程同步,先发出procReady再等待procRun

      4. 恢复parent进程的death信号量并检查当前父进程pid是否为我们原来记录的不是的话,kill ourself。

      5. 与父进程之间的同步已经完成,关闭pipe。

      6. "只写" 方式打开fifo管道并写入0,会一直保持阻塞。等待runc start以只读的方式打开FIFO管道,阻塞才会消除。之后本进程才会继续执行。

      7. 调用syscall.Exec,执行用户真正希望执行的命令。用来覆盖掉PID为1的Init进程。至此,在容器内部PID为1的进程才是用户希望一直在前台执行的进程。

      8. init进程通过匿名管理读取父进程的信息,initType以及config信息。

      9. 调用func newContainerInit(),生成一个type linuxStandardInit struct对象

      10. 执行linuxStandardInit.Init(),Init进程会根据config配置初始化seccomp,并调用syscall.Exec执行cmd。

    runc start

    runc start的逻辑比较简单,分为两步:

    1. 从context中获取libcontainer.container对象。

    2. 通过判断container 的状态为created,执行linuxContainer.exec()。

    • 以“只读”的方式打开FIFO管道,读取内容。这同时也恢复之前处于阻塞状态的`runc Init`进程,Init进程会执行最后调用用户期待的cmd部分。

    • 如果读取到的data长度大于0,则读取到Create流程中最后写入的“0”,则删除FIFO管道文件。

    感谢各位的阅读!关于“docker中run的示例分析”这篇文章就分享到这里了,希望以上内容可以对大家有一定的帮助,让大家可以学到更多知识,如果觉得文章不错,可以把它分享出去让更多的人看到吧!