docker源码分析Libcontainer

71次阅读
没有评论

共计 7544 个字符,预计需要花费 19 分钟才能阅读完成。

这篇文章主要讲解了“docker 源码分析 Libcontainer”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着丸趣 TV 小编的思路慢慢深入,一起来研究和学习“docker 源码分析 Libcontainer”吧!

1. Libcontainer 简介 1.1 linux container 相关技术

要了解 Libcontainer 首先要了解 linux container 所用到的一些基本技术。linux container 是一种内核虚拟化技术,可以提供轻量级的虚拟化,以便隔离进程和资源。而这正是 docker 容器技术和核心,docker 正是 linux container 的一种实现。linux container 所用到的基本技术包括 namespace、cgroup、chroot、veth、union FS、iptables 和 netfilter、TC、quota、setrlimit, 下面对这些基本技术做一个简要的概括:

1. Namespace:用来做资源隔离以实现轻量级虚拟化,包括六种 namespace,UTS namespace 提供了主机名和域名之间的隔离;IPC namespace 提供了进程间通信的隔离;Network namespace 提供了网络的隔离包括网络设备、网络栈、端口等;Mount namespace 提供了文件系统的隔离;User namespace 提供了用户权限间的隔离。

2. Cgroups:实现资源限制,可以限制、记录任务组所使用的物理资源。还可用于优先级分配,通过分配的 CPU 时间片数量及磁盘 IO 宽带大小控制任务运行的优先级。用于资源统计,统计系统的资源使用量,如 CPU 使用时长、内存用量等。用于任务控制,可以对任务执行挂起、控制等操作。

3. Chroot:更改 root 目录,用于在 container 里查看到的文件系统。他有三大优点:增加系统的安全性,限制了用户的权利;建立一个与原系统隔离的系统目录,这一点对容器极为重要;切换系统的根目录位置。

4. Veth:把一个从网络用户空间(network namespace)发出的数据包转发到另一个用户空间。即实现容器和宿主机之间的通信。

5. Union FS:叠加的文件系统,其中包括 aufs 一种支持联合挂载的文件系统。

6. Iptables,netfilter:主要用来做 ip 数据包的过滤。

7. TC:主要用来做流量隔离,带宽的限制。

8. Quota:用来做磁盘读写大小的限制,用来限制用户可用空间的大小。

9. Setrlimit:可以限制 container 中打开的进程数,限制打开的文件个数等。

1.2 Libcontainer 简介

  基于上文对 linux container 相关技术,docker 基本是实现了前五个的技术,用 libcontainer 做了一层封装。也就是说 docker 通过 libcontainer 封装了 linux container 的部分技术,这样使得 Docker 具有持续部署与测试、跨云平台支持、环境标准化和版本控制、高资源利用率与隔离、容器跨平台性与镜像、易于理解且易用以及具有应用镜像仓库等优点。Libcontainer 本质上是 Docker 中用于容器管理的包,基于 Go 语言实现,通过管理 namespace、Cgroups、capabilities 以及文件系统等来进行容器控制。Libcontainer 可用于创建容器并对容器进行生命周期管理。提到 Libcontainer 就要提到 execdriver,execdriver 封装了对 namespace、cgroups 等对 OS 资源进行操作的所有方法,而 Libcontainer 是 execdriver 的默认实现。execdriver 通过得到的 command 信息加载生成容器配置 container,然后调用 libcontainer 加载容器配置 container,创建真正的 docker 容器,完成容器的创建并对容器的生命周期进行管理。

2. execdriver 工作流程

  Execdriver 的工作流程如图 2.1 所示:

图 2.1 execdriver 的工作流程

2.1 配置信息简介

Execdriver 首先得到 Docker daemon 提交的 command 信息,提交过来的 command 信息包含 namespace、cgroup 等配置容器所需的重要信息。相对应的 command 结构体源码如图 2.2,其中包含了生成容器所需的基本配置,有 namespace 相关比如 UTS 可提主机名和域名之间的隔离;IPC 提供了进程间通信的隔离;Network 提供了网络的隔离包括网络设备、网络栈、端口等;Mount 提供了文件系统的隔离。Resource 包含了 cgroup 相关的信息,ProcessConfig 表示容器中运行的进程的信息。

对图 2.2 中的部分参数做简要解释,其中:ContainerPid 表示容器中进程的 pid;ID 是容器 ID,代表容器的唯一标识,非常重要;Mount 是 namespace 的一种用于文件系统的隔离;Network 也是 namespace 的一种用于进行网络的隔离;ProcessConfig 描述了容器中运行的进程的信息;Resource 提供了 cgroup 相关的信息,后面会对 Resource 结构体展开做详细的分析;Rootfs 是容器的根目录系统;WorkingDir 顾名思义是容器的工作路径;TmpDir 是用来存储 docker 临时文件的目录;

图 2.2 command 结构体

Cgroups 用于实现资源限制,可以限制、记录任务组所使用的物理资源。cgroups 相关信息包含在 resource 里,resource 包含了对 driver 配置的所有资源的信息,resource 结构体相关定义如图 2.3,其中:memory 表示所使用的存储容量,还定义了 CPU 用量等 cgroup 所需的信息。

图 2.3 resource 结构体

  ProcessConfig 中包含了表示容器中运行的进程的信息,ProcessConfig 结构体相关定义源码如图 2.4,

图 2.4ProcessConfig 结构体

2.2 主要流程分析

  图 2.1 中所示的工作流程相对应的源码在 deamon/execdriver/native/driver.go 的 run 函数中,run 函数部分截图如图 2.5 所示,其中 container, err := d.createContainer(c, hooks) 语句的作用是调用 createContainer 函数创建容器配置。函数传入的参数 c 表示 execdriver.Command,即上文提到的 command 结构体,也就是说 createContainer 函数根据 command 参数创建相关的容器配置。

图 2.5 Run 函数部分函数体

  上文说到 createContainer 函数根据 command 参数创建相关的容器配置,下面我们看一下 createContainer 函数的内部结构,如图 2.6 为 createContainer 函数的部分结构。其中 container = execdriver.InitContainer(c) 可以看到调用 InitContainer 函数通过传入的 execdriver.Command 参数生成容器配置 container。其中一系列的 createXXX() 方法根据 InitContainer 函数得到的 container 填充模板,配置 IPC、Pid、network 等所需字段。其中 createIpc() 表示配置 Ipc 提供提供了进程间通信的隔离;createPid() 表示配置 Pid;createUTS() 表示配置 UTS 提供主机名和域名之间的隔离;createNetwork() 配置 Network 提供了网络的隔离包括网络设备、网络栈、端口等。

图 2.6 createContainer 函数的部分函数体

  由 createContainer 函数的源码的内部结构可以看到在 createContainer 函数中首先调用 InitContainer 函数生成了一个叫做 container 的变量,InitContainer 函数通过传入的 execdriver.Command 参数生成容器配置 container,如图 2.7 是 execdriver.InitContainer 函数的内部结构。在 InitContainer 函数中根据 command 配置 container 的 hostname 主机名、cgroup、devices、rootfs 等信息,最后返回一个容器配置 container,这时候的返回的 container 其实是一个 Config 对象,表示容器配置。后面再由 createContainer 函数中的 createXXX() 方法根据 InitContainer 函数返回的 container 容器配置,配置相应 IPC、Pid、network 等所需字段。

图 2.7 InitContainer 函数的部分函数体

  至此我们已经分析完了 deamon/execdriver/native/driver.go 的 run 函数中 container, err := d.createContainer(c, hooks) 语句,简单的说该语句的结果就是生成了一份 container 容器配置。接下来在 run 函数中 execdriver 调用 libcontainer 加载已经生成好的容器配置 container,创建真正的 Docker 容器。

3. Libcontainer 实现原理

  在 deamon/execdriver/native/driver.go 的 run 函数中,成功生成 container 容器配置以后,工作就交由 libcontainer。libcontainer 的主要工作为:

1. 创建 libcontainer 构建容器所需要使用的进程对象,即 Process。对应源码如图 3.1 所示。Process 指定了容器内进程对象的配置和 IO,其中有指定若干参数,并对参数赋值。Args 表示将要运行的一系列指令;Env 指定该进程对象的环境变量;Cwd 将进程的工作目录改至容器的 rootfs 中;User 将为容器中的正在运行的进程设置 UID 和 GID。

docker 源码分析 Libcontainer

图 3.1 构建 Process

2. 接下来在 run 函数中 err := setupPipes(container, c.ProcessConfig, p, pipes); 语句调用 setupPipes 函数设置容器的输出管道。而 setupPipes 函数即为设置容器输出管道函数,其函数体定义在 deamon/execdriver/native/driver.go 的 setupPipes 函数中。setupPipes 函数主要通过 execdriver.Pipes 配置容器的输出管道,其主要作用是将容器的输出成标准输入、标准输出和标准错误。

3. 使用 Factory 工厂类,用容器 ID 和容器配置 container 创建逻辑容器 Container,在 run 函数中对应的源码为:d.factory.Create(c.ID, container),其中 c 为 execdriver.Command,c.ID 为容器 ID,container 即为之前多次提到的容器配置。在生成逻辑容器的过程中,容器配置 container 的各项会填充到逻辑容器 Container 对像的配置项 config 里。

4. 接下来用启动容器,启动容器对应的语句为 cont.Start(p),其中 cont 为 d.factory.Create(c.ID, container) 函数生成的 Container 逻辑容器,而参数 p 为之前生成的容器所需要使用的进程对象 Process。

5. 下面的代码 p.Wait() 即为 process.Wait(),表示等待之前 Process 的所有工作都完成,直到物理容器创建成功。Processd 的 Wait 函数所对应的源码为图 3.2 所示。

docker 源码分析 Libcontainer

图 3.2 Process 的 Wait() 函数

6. 最后的 cont.Destroy() 表示 Container.Destory(),即在需要的情况下可以销毁容器。

通过上述对 libcontainer 主要工作分析,我们发现 libcontainer 的重点正是 Process、Container、Factory 这 3 个逻辑实体的实现。其中 Factory 用于创建一个逻辑上的容器对象;Container 是包含容器配置信息的逻辑容器;Process 用于物理容器中进程的配置和 IO 管理。下面我们 libcontainer 中这三个逻辑实体进行详细的解析。

3.1 Factory 创建逻辑容器

  Factory 的作用是用给定的容器 ID 创建一个新的容器,并在该容器中启动初始进程。并且接受的容器 ID 为只包含字母、数字、下划线组成的字符创,且长度必须在 1 到 1024 之间。容器 ID 不能与已经存在的容器的 ID 重合,使用同一路径(和文件系统)的 Factory 创建的容器必须有不同的标识。最后用一个正在运行的进程返回一个新的容器。

  在这个过程中可能出现的错误有:IdInUse 表示容器 ID 已经被其他容器占用;InvalidIdFormat 表示容器 ID 的格式不正确;ConfigInvalid 表示配置信息无用;Systemerror 表示系统错误。一但发生错误,那么任何已经创建的容器部分都会被清除,保证了容器创建的原子性,要不全部创建成功,否则全部不创建。

  Factory 对象中包含三个函数,他们分别为:

  1. Create() 函数:其传入参数为一个容器 ID 和一份 Config 类型的配置参数,并且接受的容器 ID 为只包含字母、数字、下划线组成的字符创,且长度必须在 1 到 1024 之间。容器 ID 不能与已经存在的容器的 ID 重合,使用同一路径(和文件系统)的 Factory 创建的容器必须有不同的标识。根据传入的这两个参数创建并返回一个 Container 类,其中包括容器 ID、容器工作目录、容器配置、初始化指令和参数、以及 Cgroup 管理器等信息。在这个函数中 Container 创建完毕。其中可能出现路径不存在、容器已经停止、系统故障等错误。

  2. Load() 函数:传入参数为一个已经被成功 Create 过的容器的容器 ID,返回该容器的信息。如果容器已经 Create 过说明存在 id 目录,则会从 id 目录下直接读取 state.json 来载入容器信息。其中可能出现的错误有管道连接错误和系统故障。

  3. StartInitialization() 函数:是容器初始化函数,是 Libcontainer 在容器重新执行期间会调用的内部 API。

  4. Type() 函数:返回容器管理的类型,比如 lxc 或 libcontainer 等。

  至此,Factory 对象完成了容器的创建和初始化。接下来就了解一下包含包含容器配置信息的逻辑容器 Container。

3.2 逻辑容器 Container

  Container 对象相当于是逻辑容器主要包含了容器配置、控制、状态显示等功能。其中 ID 表示容器的 ID。Status 表示容器内进程的状态,容器的状态包括:Running 表示容器存在并且正在运行;Pausing 表示容器存在并且进程正在被停止;Paused 表示容器存在但是所有的进程都被停止了;Checkpointed 表示容器存在并且容器状态都已保存至磁盘;Destoryed 表示容器不存在。

  Container 对象中具有一系列容器相关的函数操作,其中包括:

  ID():返回容器的 ID,代表容器的唯一标识

  Status():返回容器内进程的状态,可能为运行状态也可能是停止状态。可能抛出的错误为 ContainerDestroyed 表示容器不存在已经被销毁;Systemerror 表示系统错误。

  State():返回容器的状态信息,包括容器 ID、配置信息、初始进程 ID、进程启动时间、cgroup 文件路径、namespace 路径等。可能出现的错误为 Systemerror 即系统错误。

  Config():返回容器的配置信息

  Processes():返回容器的 PID,这个 PID 即为用来调用进程的 namespace。有些 PID 可能不在与容器中的进程相关,除非容器的状态是 PAUSED,这样才能保证每一个 PID 都是有效的。

  Stats():返回容器统计信息,包括 cgroup 中的统计以及网卡设备的统计信息。

  Set():设置容器的资源配置,例如 cgroup 各个子系统的文件路径等。

  Start():在容器内启动一个进程,如果进程启动失败就返回一个错误。可以根据以往的 Process 结构追踪进程的生命周期。其中主要工作有两个:创建 ParentProcess 实例,执行 ParentProcess.start() 来启动物理容器。ParentProcess 是一个接口其具体实现为 initProcess 对象,initProcess 用于创建容器所需的 ParentProcess,为创建物理容器做准备。用逻辑容器 Container 执行 initProcess.start(),真正的物理容器即 Docker 容器就生成了。

  Destory():在结束所有的正在运行的进程以后销毁容器。

3.3  Process 对象

  Process 分为两类,一类是 Process 另外一类是 ParentProcess。Process 用于容器内进程的配置和 IO 的管理,其参数包括:Args 表示将要运行的一系列指令;Env 指定该进程对象的环境变量;Cwd 将进程的工作目录改至容器的 rootfs 中;User 将为容器中的正在运行的进程设置 UID 和 GID;Stdin io.Reader 表示标准输入;Stdout io.Writer 表示标准输出;Stderr io.Writer 表示标准错误;consolePath 表示到容器的控制台的路径;Capabilities 表示容器中进程运行所需的权限;ops 表示 ParentProcess 对象。ParentProcess 负责处理容器启动工作,包含一系列的函数动作:

  pid():返回一个正在运行的进程的 pid,可以通过管道从已启动的容器进程中获得。

  start():开始容器中的执行进程。

  terminate():发送 SIGKILL 信号结束进程。

  StartTime():获取进程启动时间。

  signal():发送信号给进程。

  wait():等待程序执行结束,返回结束的程序状态。

感谢各位的阅读,以上就是“docker 源码分析 Libcontainer”的内容了,经过本文的学习后,相信大家对 docker 源码分析 Libcontainer 这一问题有了更深刻的体会,具体使用情况还需要大家实践验证。这里是丸趣 TV,丸趣 TV 小编将为大家推送更多相关知识点的文章,欢迎关注!

正文完
 
丸趣
版权声明:本站原创文章,由 丸趣 2023-08-16发表,共计7544字。
转载说明:除特殊说明外本站除技术相关以外文章皆由网络搜集发布,转载请注明出处。
评论(没有评论)