189 8069 5689

docker中run的示例分析

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

环江ssl适用于网站、小程序/APP、API接口等需要进行数据传输应用场景,ssl证书未来市场广阔!成为创新互联建站的ssl证书销售渠道,可以享受市场价格4-6折优惠!如果有意向欢迎电话联系或者加微信:028-86922220(备注:SSL证书合作)期待与您的合作!

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等各种存储 。

  2. 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

  2. 把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.

  3. 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的示例分析”这篇文章就分享到这里了,希望以上内容可以对大家有一定的帮助,让大家可以学到更多知识,如果觉得文章不错,可以把它分享出去让更多的人看到吧!


文章标题:docker中run的示例分析
标题网址:http://cdxtjz.com/article/iepdpp.html

其他资讯