扫二维码与项目经理沟通
我们在微信上24小时期待你的声音
解答本文疑问/技术咨询/运营咨询/技术建议/互联网交流
这篇文章给大家分享的是有关docker中run的示例分析的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。
环江ssl适用于网站、小程序/APP、API接口等需要进行数据传输应用场景,ssl证书未来市场广阔!成为创新互联建站的ssl证书销售渠道,可以享受市场价格4-6折优惠!如果有意向欢迎电话联系或者加微信:028-86922220(备注:SSL证书合作)期待与您的合作!
通过在/components/cli/command/commands.go里,抽象出各种命令的初始化操作。
使用第三方库"github.com/spf13/cobra"
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 的初始化,
设置容器文件系统,挂载点: /var/lib/docker/overlay/{container.RWLayer.mountID}/merged
设置容器的网络模式,调用libnetwork ,CNM模型(sandbox, endpoint,network)
创建/proc /dev等spec文件,对容器所特有的属性进行设置,
调用containerd进行create容器
1)获取libcontainerd模块中的containers 2)获取gid和uid 3)创建state目录,配置文件路径。 4)创建一个containercommon对象,创建容器目录,以及配置文件路径,根据spec创建配置文件。
1) 读取spec对象的配置文件 2) 创建一个fifo的pipe 3) 定义containerd的请求对象,grpc调用containerd模块。 ctr.client.remote.apiClient.CreateContainer(context.Background(), r) 4)启动成功后,更新容器状态。
cmd/dockerd/daemon.go 中存在libcontainerd初始化的流程。
包括启动grpc服务器,对套接字进行监听。
通过grpc.dail 与grpc server建立连接conn, 根据该链接建立apiclient对象,发送json请求。
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。
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()消费
docker-containerd初始化包括 新建Supervisor对象:
该对象会启动10个worker,负责处理创建新容器的任务(task)。
supervisor的初始化,包括startTask chan初始化,启动监控容器进程的monitor
一个worker包含一个supervisor和sync.waitgroup,wg用于实现容器启动。
supervisor的start,消费tasks,把task中的container数据组装成runtime.container, 封装到type startTask struct,发送到startTask chan队列。
启动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。
当CreateContainer handler接收到一个Request之后,会把其转化成type startTask struct,将其转化为一个StartTask 事件,其中存放创建容器的request信息。
通过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 }
等待woker.Start()消费处理结果后,将StartResponse返回给docker-daemon。
负责将每一个request转化成特定的task类型,通过一个goroutine遍历task中所有的任务并进行处理。消费tasks,把task中的container数据组装成runtime.container, 封装到type startTask struct,发送到startTask chan队列。
负责调用containerd-shim, 监控容器中的进程,并把结果返回给StartResponse chan队列。
其中,
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发生退出时,如果容器内的进程仍在运行,则需要把该进程杀死;如果容器内进程已经不存在,则无需清理工作。
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
在源码create.go中,首先会加载config.json的配置,然后调用startContainer函数,其流程包括:
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)
linuxContainer.newParentPorcess组装要执行的parent命令, 组装出来的命令是/proc/self/exe init, 通过匿名管道让runc create 和runc init进行通信。
parent.start()会根据parent的类型来选择对应的start(),自此之后,将进入/proc/self/exe init,也就是runc init
将容器状态持久化到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 create clone出一个子进程,namespace与父进程隔离,子进程中调用/proc/self/exe init进行初始化。
runc init的过程如下:
调用factory.StartInitialization();
配置容器内部网络,路由,初始化mount namespace, 调用setupRootfs在新的mount namespaces中配置设备、挂载点以及文件系统。
配置hostname, apparmor,processLabel,sysctl, readyonlyPath, maskPath.
获取父进程的退出信号,通过管道与父进程同步,先发出procReady再等待procRun
恢复parent进程的death信号量并检查当前父进程pid是否为我们原来记录的不是的话,kill ourself。
与父进程之间的同步已经完成,关闭pipe。
"只写" 方式打开fifo管道并写入0,会一直保持阻塞。等待runc start
以只读的方式打开FIFO管道,阻塞才会消除。之后本进程才会继续执行。
调用syscall.Exec,执行用户真正希望执行的命令。用来覆盖掉PID为1的Init进程。至此,在容器内部PID为1的进程才是用户希望一直在前台执行的进程。
init进程通过匿名管理读取父进程的信息,initType以及config信息。
调用func newContainerInit(),生成一个type linuxStandardInit struct对象
执行linuxStandardInit.Init(),Init进程会根据config配置初始化seccomp,并调用syscall.Exec执行cmd。
runc start的逻辑比较简单,分为两步:
从context中获取libcontainer.container对象。
通过判断container 的状态为created,执行linuxContainer.exec()。
以“只读”的方式打开FIFO管道,读取内容。这同时也恢复之前处于阻塞状态的`runc Init`进程,Init进程会执行最后调用用户期待的cmd部分。
如果读取到的data长度大于0,则读取到Create流程中最后写入的“0”,则删除FIFO管道文件。
感谢各位的阅读!关于“docker中run的示例分析”这篇文章就分享到这里了,希望以上内容可以对大家有一定的帮助,让大家可以学到更多知识,如果觉得文章不错,可以把它分享出去让更多的人看到吧!
我们在微信上24小时期待你的声音
解答本文疑问/技术咨询/运营咨询/技术建议/互联网交流