版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/feilengcui008/article/details/52758877
本文主要介绍Docker的一些基本概念、Docker的源码分析、Docker的一些issue、Docker周边生态等等。
基本概念
Basics
docker大体包括三大部分,runtime(container)、image(graphdriver)、registry,runtime提供环境的隔离与资源的隔离和限制,image提供layer、image、rootfs的管理、registry负责镜像存储与分发。当然,还有其他一些比如data volume, network等等,总体来说还是分为计算、存储与网络。
computing
- 接口规范
- 命名空间隔离、资源隔离与限制的实现
- 造坑与入坑
network
接口规范与实现
- bridge
- veth pair for two namespace communication
- bridge and veth pair for multi-namespace communication
- do not support multi-host
overlay
- docker overlay netowrk: with swarm mode or with kv etcd/zookeeper/consul -> vxlan
- coreos flannel -> 多种backend,udp/vxlan…
- ovs
- weave -> udp and vxlan,与flannel udp不同的是会将多container的packet一块打包
- 一篇对比
calico
- pure layer 3
- null
- 与世隔绝
- host
- 共享主机net namespace
- bridge
storage
- graphdriver(layers,image and rootfs)
- graph:独立于各个driver,记录image的各层依赖关系(DAG),注意是image不包括运行中的container的layer,当container commit生成image后,会将新layer的依赖关系写入
- device mapper
- snapshot基于block,allocation-on-demand
- 默认基于空洞文件(data and metadata)挂载到回环设备
- aufs
- diff:实际存储各个layer的变更数据
- layers:每个layer依赖的layers,包括正在运行中的container
- mnt:container的实际挂载根目录
- overlayfs
- vfs
- btrfs
- …
- volume
- driver接口
- local driver
- flocker: container和volume管理与迁移
- rancher的convoy:多重volume存储后端的支持device mapper, NFS, EBS…,提供快照、备份、恢复等功能
- 数据卷容器
- driver接口
- registry:与docker registry交互
- 支持basic/token等认证方式
- token可以基于basic/oauth等方式从第三方auth server获取bearer token
- tls通信的支持
- libkv
- 支持consul/etcd/zookeeper
- 分布式存储的支持
security
- docker
- libseccomp限制系统调用(内部使用bpf)
- linux capabilities限制root用户权限范围scope
- user namespace用户和组的映射
- selinux
- apparmor
- …
- image and registry
Other Stuffs
迁移
- CRIU: Checkpoint/Restoreuser In User namespace
- CRAK: Checkpoint/Restart as A Kernel module
开放容器标准
- runtime
- runc
- runv
- rkt(appc)
- libcontainer and runc
- containerd
- docker client and docker daemon
- OCI标准和runC原理解读
- Containerd:一个控制runC的守护进程
- runC:轻量级容器运行环境
- runtime
源码分析
for docker 1.12.*
主要模块
- docker client
- DockerCli => 封装客户端的一些配置
- command => 注册docker client支持的接口
- docker/engine-api/client/[Types|Client|Request|Transport|Cancellable] => 规范访问dockerd apiserver的接口
- docker engine daemon
- DaemonCli
- apiserver => 接受docker client请求,转发到daemon rpc
- daemon => 其他功能比如设置docker根目录、inti process、dockerd运行的user namespace等其他信息
- 包含一个很重要的部分: remote => 通过libcontainerd与containerd的grpc server后端打交道
- cluster => swarm mode相关
- DaemonCli
- containerd
- containerd => grpc server,提供给dockerd操作容器、进程等的接口,提供containerd、containerd-shim、containerd-ctr工具
- libcontainer(runc)
- libcontainer(runc) 提供容器的生命周期相关的接口标准,提供runc工具
- 基本流程:docker client ==http==> dockerd apiserver ====> remote grpc client(libcontainerd) ==grpc==> containerd ==cmd==> containerd-shim ==cmd==> runc exec/create等 ==cmd==> runc init初始化坑内init进程环境,然后execve替换成容器指定的应用程序
详细分析
客户端部分省略,这里主要介绍docker engine daemon(DaemonCli)、containerd以及libcontainer(runc)三大部分。
DaemonCli: 启动docker daemon与containerd daemon的核心对象,包含三大部分,apiserver、Daemon对象和cluster
- apiserver
- middleware
- routers
- 通用模式
- 提供backend具体操作的后端接口(实际全在daemon.Daemon实现,而daemon.Daemon会作为所有router的backend)
- 提供解析请求的routers函数(实际调用backend接口)
- 注册routers
- build => docker build
- container => container创建启停等
- image => 镜像
- network => 网络
- plugin => 插件机制
- swarm => swarm模式相关
- volumn => 数据卷
- system => 系统信息等
- 通用模式
- 我们可以用nc手动测试apiserver,具体实现的接口可以参考标准文档或者api/server下的源码
- 执行命令即可看到json输出(还有个python的客户端lib docker-py)
- echo -e “GET /info HTTP/1.0\r\n” | nc -U /var/run/docker.sock
- echo -e “GET /images/json HTTP/1.0\r\n” | nc -U /var/run/docker.sock
- daemon.Daemon对象
- daemon除了处理engine daemon需要的通用环境(比如storage driver等)外,还包括registry部分和与containerd交互的grpc接口client(libcontainerd.Client/libcontainerd.Remote相关)。在DaemonCli的初始化过程中会由libcontainerd.New创建libcontainerd.remote,启动containerd daemon(grpc server)并且为docker engine daemon注入containerd/types中规范的与containerd daemon通信的grpc接口client
- 以docker pause為例,整個調用鏈條為:
- docker client -> apiserver container router postContainerPause -> daemon.Daemon.ContainerPause(backend) -> backend.containerd.Pause
-> libcontainerd.Client.Pause -> remote.apiClient.UpdateContainer -> containerd.APIClient.UpdateContainer -> grpc.UpdateContainer -> containerd daemon UpdateContainer -> 调用containerd-shim containerid container_path runc -> 调用runc命令
- 说明: containerd是一个从docker daemon中抽出来的项目,提供操作runc的界面(包括一个daemon grpc server、一个ctr客户端工具用grpc.APIClient与grpc server通信、以及containerd-shim负责调用runc命令处理容器生命周期),runc提供的只是一个容器生命周期lib标准和cli工具,而没有daemon。
- docker client -> apiserver container router postContainerPause -> daemon.Daemon.ContainerPause(backend) -> backend.containerd.Pause
- 可以看出,runc(libcontainerd)提供了runtime的lib接口标准,不同os可以实现此接口屏蔽容器的具体实现技术细节;而containerd提供了一个基于libcontainerd接口的server以及cli工具(主要是grpc规范了);而docker daemon(engine)的apiserver提供的是docker client的restful http接口,会通过containerd的grpc Client标准接口与containerd的server通信。我们可以看到”/var/run/docker/libcontainerd/docker-containerd.sock”和”/var/run/docker.sock”,如上面通过nc与docker daemon直接通信,我们也可以使用grpc client与libcontainerd的daemon直接通信
- 综上,不难看出docker提供的几个主要二进制文件是干嘛的了…(docker/dockerd/docker-containerd/docker-containerd-shim/docker-containerd-ctr/docker-runc)
- 用runc直接操作容器: docker-runc list
- 用docker-containerd-ctr 通过docker-containerd grpc Server操作容器: docker-containerd-ctr –address “unix:///var/run/docker/libcontainerd/docker-containerd.sock” containers list
- 用docker通过dockerd、docker-containerd操作容器: docker ps
- 拆分的好处显而易见:标准化、解耦、新特性的实验、换daemon无需停止容器等等
- cluster
- 這一部分與swarm相关,实际上是把swarmkit集成到了docker engine daemon中
- 每次启动docker engine daemon时会检查/var/lib/docker/swarm目录下是否有状态文件,如果有则需要恢复集群,重新启动节点;否则,直接返回,不开启swarm mode
- swarm中的节点有ManagerNode和WorkerNode之分,worker可以被promote成manager,manager也可以被demote回worker。在节点加入集群时可以指定加入的角色是worker还是manager。默认启动一个manager节点
- apiserver
containerd
- 容器元数据、提供管理容器生命周期的grpc server以及ctr 客户端工具,具体的容器的操作是通过containerd-shim调用runc命令,每个容器的init进程在容器外部会有对应的containerd-shim进程。
- 提供了一套任务执行机制,把对容器的生命周期的操作用Task/Worker模型抽象,提供更高的性能
- 从docker engine daemon拆分,使得engine daemon升级时容器不用stop
- 简单流程
- 核心的对象: grpc server、supervisor、worker、task、runtime(處理container和process相關元數據等)等
- 主routine的grpc apiserver等待grpc请求 -> supervisor server handleTask -> 放入supervisor的tasks chan -> worker从tasks chan中取出执行 -> shim -> runc
libcontainer(or runc)
- 未完待续
从containerd到runc到实际的坑内进程起来经过的进程模型(以下起进程都是通过go的cmd)
- containerd的worker启动containerd-shim进程,传递参数shim containerdid containerpath runtime(其中runtime默认为runc),并且给runc传递exec/create的行为参数,起好坑。
- containerd-shim启动runc exec/create进程,等待runc进程的结束,负责容器内的init进程的退出时的清理工作。containerd-shim与containerd daemon进程通信是通过control和exit两个具名管道文件。
- runc exec/create作为父进程负责创建容器内的init进程,并用管道与init进程通信,这个init进程实际上是执行runc init命令,初始化容器环境,然后等待containerd执行runc start的信号,让用户的进程替换容器中的init,在容器中执行起来。
- runc init进程负责初始化整个环境,包括清除所有runc exec/create父进程的环境变量,加载docker engine daemon传下来的docker client和docker image中指定的环境变量,设置namespace等等,然后等在管道的child上,等待runc exec/create父进程发送process的json配置文件,runc init坑内进程拿到这个配置文件,初始化所有的坑内环境,然后等待在exec.fifo具名管道文件上,等待runc start发送信号,然后开始execve用用户的程序替换掉runc init。
相关系统
Docker和Mesos Container建坑流程和进程模型对比
注: P代表进程, L代表线程
Docker
- containerd的worker启动containerd-shim进程,传递参数shim containerdid containerpath runtime(其中runtime默认为runc),并且给runc传递exec/create的行为参数,起好坑。
- containerd-shim启动runc exec/create进程,等待runc进程的结束,负责容器内的init进程的退出时的清理工作。containerd-shim与containerd daemon进程通信是通过control和exit两个具名管道文件。
- runc exec/create作为父进程负责创建容器内的init进程,并用管道与init进程通信,这个init进程实际上是执行runc init命令,初始化容器环境,然后等待containerd执行runc start的信号,让用户的进程替换容器中的init,在容器中执行起来。
- runc init进程负责初始化整个环境,包括清除所有runc exec/create父进程的环境变量,加载docker engine daemon传下来的docker client和docker image中指定的环境变量,设置namespace等等,然后等在管道的child上,等待runc exec/create父进程发送process的json配置文件,runc init坑内进程拿到这个配置文件,初始化所有的坑内环境,然后等待在exec.fifo具名管道文件上,等待runc start发送信号,然后开始execve用用户的程序替换掉runc init。
Mesos Native Linux Container
- 基本模型
- 与docker containerd的主进程和matrix-agent的ContainerManager主线程类似,executor(mesos默认提供Command、Container两种executor)起一进程负责维护containers list的内存状态,并且fork&exec执行容器的启动
- 建坑流程
- Creates a “freezer” cgroup for the container.
- Creates posix “pipe” to enable communication between host (parent process) and container process.
- Spawn child process(container process) using clone system call.
- Moves the new container process to the freezer hierarchy.
- Signals the child process to continue (exec’ing) by writing a character to the write end of the pipe in the parent process.
- 基本模型
Mesos
- todo
Kubernetes
- todo
issues
记录一些Docker相关项目遇到的一些bug或者issue
Docker
- overlayfs在centos6上的bug,某些容器启动时会触发(比如ubuntu)
- 目前解决办法: 升级内核到3.18.*(另外一个trick的办法是先在ubuntu的机器上export然后import,再push到仓库)
- https://github.com/docker/docker/issues/10294#issuecomment-212620653
- https://github.com/docker/docker/issues/10294
- 127.16.0.0网段的路由默认被设置导致创建网络失败 -> route del … -> 但是导致公司内网无法访问机器
- 解决办法
- 手动创建bridge,启动docker时,避开机器路由的默认网段(比如10.,172.,192.*),尤其注意公司机器路由了大网段,docker可能找到一个子网段,还是能成功启动,但是后续创建网络会失败。。。
- 先route del机器的路由网段,docker network create,再设置route(选这种方式绕过…)
- docker default local pool
- 解决办法
- devicemapper storage driver依赖udev,而udev不支持static linked的docker binary,需要dynamic的docker binary
- overlayfs在centos6上的bug,某些容器启动时会触发(比如ubuntu)
Harbor
- harbor的tags和manifests api连接registry泄露fd导致进程超过最大可用fd在新的请求解析dns时失败
- 定位bug应该是每次请求接口时新建client的同时新建了tarnsport对象,从而导致上一次请求的transport的连接无法复用,从而泄露fd。解决办法是使用对每个host的请求使用一个全局的transport和client
- 或者关闭transport的keepalive或者在发请求的时候加req.Close=true,关闭连接重用
- harbor的tags和manifests api连接registry泄露fd导致进程超过最大可用fd在新的请求解析dns时失败
Distribution(Registry v2)
- Distribution的http.Server没有设置ReadTimeout和WriteTimeout,如果客户端与registry通信没有注意连接重用的问题,则可能导致泄露fd
- harbor与distribution通信建立的连接会泄露fd,导致registry和harbor都会由于操作进程限制的fd数量而请求失败
- 通常不可能直接设置registry的timeout,因为对于较大的镜像可能导致下载超时
- Distribution的http.Server没有设置ReadTimeout和WriteTimeout,如果客户端与registry通信没有注意连接重用的问题,则可能导致泄露fd
Mesos
- todo
Kubernetes
- todo