Docker背后的容器管理——Libcontainer深度解析
1. Libcontainer 特性
1.1 建立文件系统
1.2 资源管理
1.3 可配置的容器安全
1.4 运行时与初始化进程
/etc/hosts
/etc/resolv.conf
/etc/hostname
/etc/localtime
1.5 在运行着的容器中执行新进程
1.6 容器热迁移(Checkpoint & Restore)
服务器需要维护(如系统升级、重启等)时,通过热迁移技术把容器转移到别的服务器继续运行,应用服务信息不会丢失。
对于初始化时间极长的应用程序来说,容器热迁移可以加快启动时间,当应用启动完成后就保存它的检查点状态,下次要重启时直接通过检查点启动即可。
在高性能计算的场景中,容器热迁移可以保证运行了许多天的计算结果不会丢失,只要周期性的进行检查点快照保存就可以了。
收集进程与其子进程构成的树,并冻结所有进程。
收集任务(包括进程和线程)使用的所有资源,并保存。
清理我们收集资源的相关寄生代码,并与进程分离。
读取快照文件并解析出共享的资源,对多个进程共享的资源优先恢复,其他资源则随后需要时恢复。
使用fork恢复整个进程树,注意此时并不恢复线程,在第4步恢复。
恢复所有基础任务(包括进程和线程)资源,除了内存映射、计时器、证书和线程。这一步主要打开文件、准备namespace、创建socket连接等。
恢复进程运行的上下文环境,恢复剩下的其他资源,继续运行进程。
2. nsinit
与Libcontainer的使用
2.1 nsinit
的构建
2.2 nsinit
的使用
config:使用内置的默认参数加上执行命令时用户添加的部分参数,生成一份容器可用的标准配置文件。
exec:启动容器并执行命令。除了一些共有的参数外,还有如下一些独有的参数。
–tty,-t:为容器分配一个终端显示输出内容。
–config:使用配置文件,后跟文件路径。
–id:指定容器ID,默认为
nsinit
。–user,-u:指定用户,默认为“root”.
–cwd:指定当前工作目录。
–env:为进程设置环境变量。
init:这是一个内置的参数,用户并不能直接使用。这个命令是在容器内部执行,为容器进行namespace初始化,并在完成初始化后执行用户指令。所以在代码中,运行
nsinit exec
后,传入到容器中运行的实际上是nsinit init
,把用户指令作为配置项传入。oom:展示容器的内存超限通知。
pause/unpause:暂停/恢复容器中的进程。
stats:显示容器中的统计信息,主要包括cgroup和网络。
state:展示容器状态,就是读取
state.json
文件。checkpoint:保存容器的检查点快照并结束容器进程。需要填
--image-path
参数,后面是检查点保存的快照文件路径。完整的命令示例如下。
3. 配置参数解析
no_pivot_root
:这个参数表示用rootfs
作为文件系统挂载点,不单独设置pivot_root
。parent_death_signal
: 这个参数表示当容器父进程销毁时发送给容器进程的信号。pivot_dir
:在容器root
目录中指定一个目录作为容器文件系统挂载点目录。rootfs
:容器根目录位置。readonlyfs
:设定容器根目录为只读。mounts
:设定额外的挂载,填充的信息包括原路径,容器内目的路径,文件系统类型,挂载标识位,挂载的数据大小和权限,最后设定共享挂载还是非共享挂载(独立于mount_label
的设定起作用)。devices
:设定在容器启动时要创建的设备,填充的信息包括设备类型、容器内设备路径、设备块号(major,minor)、cgroup文件权限、用户编号、用户组编号。mount_label
:设定共享挂载还是非共享挂载。hostname
:设定主机名。namespaces
:设定要加入的namespace,每个不同种类的namespace都可以指定,默认与父进程在同一个namespace中。capabilities
:设定在容器内的进程拥有的capabilities
权限,所有没加入此配置项的capabilities
会被移除,即容器内进程失去该权限。networks
:初始化容器的网络配置,包括类型(loopback、veth)、名称、网桥、物理地址、IPV4地址及网关、IPV6地址及网关、Mtu大小、传输缓冲长度txqueuelen
、Hairpin Mode设置以及宿主机设备名称。routes
:配置路由表。cgroups
:配置cgroups资源限制参数,使用的参数不多,主要包括允许的设备列表、内存、交换区用量、CPU用量、块设备访问优先级、应用启停等。apparmor_profile
:配置用于selinux的apparmor文件。process_label
:同样用于selinux的配置。rlimits
:最大文件打开数量,默认与父进程相同。additional_groups
:设定gid
,添加同一用户下的其他组。uid_mappings
:用于User namespace的uid映射。gid_mappings
:用户User namespace的gid映射。readonly_paths
:在容器内设定只读部分的文件路径。MaskPaths
:配置不使用的设备,通过绑定/dev/null
进行路径掩盖。
4. Libcontainer实现原理
4.1 Factory 对象
Create():通过一个
id
和一份配置参数创建容器,返回一个运行的进程。容器的id
由字母、数字和下划线构成,长度范围为1~1024。容器ID为每个容器独有,不能冲突。创建的最终返回一个Container类,包含这个id
、状态目录(在root目录下创建的以id
命名的文件夹,存state.json
容器状态文件)、容器配置参数、初始化路径和参数,以及管理cgroup的方式(包含直接通过文件操作管理和systemd管理两个选择,默认选cgroup文件系统管理)。Load():当创建的
id
已经存在时,即已经Create
过,存在id
文件目录,就会从id
目录下直接读取state.json
来载入容器。其中的参数在配置参数部分有详细解释。Type():返回容器管理的类型,目前可能返回的有libcontainer和lxc,为未来支持更多容器接口做准备。
StartInitialization():容器内初始化函数。
这部分代码是在容器内部执行的,当容器创建时,如果
New
不加任何参数,默认在容器进程中运行的第一条命令就是nsinit init
。在execdriver
的初始化中,会向reexec
注册初始化器,命名为native
,然后在创建Libcontainer以后把native
作为执行参数传递到容器中执行,这个初始化器创建的Libcontainer就是没有参数的。传入的参数是一个管道文件描述符,为了保证在初始化过程中,父子进程间状态同步和配置信息传递而建立。
不管是纯粹新建的容器还是已经创建的容器执行新的命令,都是从这个入口做初始化。
第一步,通过管道获取配置信息。
第二步,从配置信息中获取环境变量并设置为容器内环境变量。
若是已经存在的容器执行新命令,则只需要配置cgroup、namespace的Capabilities以及AppArmor等信息,最后执行命令。
若是纯粹新建的容器,则还需要初始化网络、路由、namespace、主机名、配置只读路径等等,最后执行命令。
4.2 Container 对象
ID():显示Container的ID,在Factor对象中已经说过,ID很重要,具有唯一性。
Status():返回容器内进程是运行状态还是停止状态。通过执行“SIG=0”的KILL命令对进程是否存在进行检测。
State():返回容器的状态,包括容器ID、配置信息、初始进程ID、进程启动时间、cgroup文件路径、namespace路径。通过调用
Status()
判断进程是否存在。Config():返回容器的配置信息,可在“配置参数解析”部分查看有哪些方面的配置信息。
Processes():返回cgroup文件
cgroup.procs
中的值,在Docker背后的内核知识:cgroups资源限制部分的讲解中我们已经提过,cgroup.procs
文件会罗列所有在该cgroup中的线程组ID(即若有线程创建了子线程,则子线程的PID不包含在内)。由于容器不断在运行,所以返回的结果并不能保证完全存活,除非容器处于“PAUSED”状态。Stats():返回容器的统计信息,包括容器的cgroups中的统计以及网卡设备的统计信息。Cgroups中主要统计了cpu、memory和blkio这三个子系统的统计内容,具体了解可以通过阅读“cgroups资源限制”部分对于这三个子系统统计内容的介绍来了解。网卡设备的统计则通过读取系统中,网络网卡文件的统计信息文件
/sys/class/net/<EthInterface>/statistics
来实现。Set():设置容器cgroup各子系统的文件路径。因为cgroups的配置是进程运行时也会生效的,所以我们可以通过这个方法在容器运行时改变cgroups文件从而改变资源分配。
Start():构建ParentProcess对象,用于处理启动容器进程的所有初始化工作,并作为父进程与新创建的子进程(容器)进行初始化通信。传入的Process对象可以帮助我们追踪进程的生命周期,Process对象将在后文详细介绍。
启动的过程首先会调用
Status()
方法的具体实现得知进程是否存活。创建一个管道(详见Docker初始化通信——管道)为后期父子进程通信做准备。
配置子进程
cmd
命令模板,配置参数的值就是从factory.Create()
传入进来的,包括命令执行的工作目录、命令参数、输入输出、根目录、子进程管道以及KILL
信号的值。根据容器进程是否存在确定是在已有容器中执行命令还是创建新的容器执行命令。若存在,则把配置的命令构建成一个
exec.Cmd
对象、cgroup路径、父子进程管道及配置保留到ParentProcess对象中;若不存在,则创建容器进程及相应namespace,目前对user namespace有了一定的支持,若配置时加入user namespace,会针对配置项进行映射,默认映射到宿主机的root用户,最后同样构建出相应的配置内容保留到ParentProcess对象中。通过在cmd.Env
写入环境变量_LIBCONTAINER_INITTYPE
来告诉容器进程采用的哪种方式启动。执行ParentProcess中构建的
exec.Cmd
内容,即执行ParentProcess.start()
,具体的执行过程在Process部分介绍。最后如果是新建的容器进程,还会执行状态更新函数,把
state.json
的内容刷新。Destroy():首先使用cgroup的freezer子系统暂停所有运行的进程,然后给所有进程发送
SIGKIL
信号(如果没有使用pid namespace
就不对进程处理)。最后把cgroup及其子系统卸载,删除cgroup文件夹。Pause():使用cgroup的freezer子系统暂停所有运行的进程。
Resume():使用cgroup的freezer子系统恢复所有运行的进程。
NotifyOOM():为容器内存使用超界提供只读的通道,通过向
cgroup.event_control
写入eventfd
(用作线程间通信的消息队列)和cgroup.oom_control
(用于决定内存使用超限后的处理方式)来实现。Checkpoint():保存容器进程检查点快照,为容器热迁移做准备。通过使用CRIU的SWRK模式来实现,这种模式是CRIU另外两种模式CLI和RPC的结合体,允许用户需要的时候像使用命令行工具一样运行CRIU,并接受用户远程调用的请求,即传入的热迁移检查点保存请求,传入文件形式以Google的protobuf协议保存。
Restore():恢复检查点快照并运行,完成容器热迁移。同样通过CRIU的SWRK模式实现,恢复的时候可以传入配置文件设置恢复挂载点、网络等配置信息。
TIPs: Docker初始化通信——管道
发送信号通知(signal)
对内存轮询访问(poll memory)
sockets通信(sockets)
文件和文件描述符(files and file-descriptors)
// 需要加入头文件: #include <unistd.h> // 全局变量: int fd[2]; // 在父进程中进行初始化: pipe(fd); // 关闭管道文件描述符 close(checkpoint[1]);
4.3 Process 对象
开始执行进程,首先会运行
C
代码,通过管道获得进程pid,最后等待C
代码执行完毕。通过获得的pid把cmd中的Process替换成新生成的子进程。
把子进程加入cgroup中。
通过管道传配置文件给子进程。
等待初始化完成或出错返回,结束。
pid(): 启动容器进程后通过管道从容器进程中获得,因为容器已经存在,与Docker Deamon在不同的pid namespace中,从进程所在的namespace获得的进程号才有意义。
start(): 初始化容器中的执行进程。在已有容器中执行命令一般由
docker exec
调用,在execdriver包中,执行exec
时会引入nsenter
包,从而调用其中的C语言代码,执行nsexec()
函数,该函数会读取配置文件,使用setns()
加入到相应的namespace,然后通过clone()
在该namespace中生成一个子进程,并把子进程通过管道传递出去,使用setns()
以后并没有进入pid namespace,所以还需要通过加上clone()
系统调用。开始运行进程。
把进程pid加入到cgroup中管理。
初始化容器网络。(本部分内容丰富,将从本系列的后续文章中深入讲解)
通过管道发送配置文件给子进程。
等待初始化完成或出错返回,结束。
pid():启动容器进程后通过
exec.Cmd
自带的pid()
函数即可获得。start():初始化及执行容器命令。
terminate() :发送
SIGKILL
信号结束进程。startTime() :获取进程的启动时间。
signal():发送信号给进程。
wait():等待程序执行结束,返回结束的程序状态。
5. 总结
6. 作者简介
本文系转载,版权归原作者所有,如若侵权请联系我们进行删除!
云掣基于多年在运维领域的丰富时间经验,编写了《云运维服务白皮书》,欢迎大家互相交流学习:
《云运维服务白皮书》下载地址:https://fs80.cn/v2kbbq
想了解更多大数据运维托管服务、数据库运维托管服务、应用系统运维托管服务的的客户,欢迎点击云掣官网沟通咨询:https://yunche.pro/?t=shequ