Docker核心原理


| 阅读 |,阅读约 25 分钟
| 复制链接:

Overview

Docker核心原理

内容整理自《docker容器与容器云》第三章,该书豆瓣评分8.2。确实是一本不错的书,不过难度较高,很多内容非常晦涩,涉及很多linux底层知识和docker源码分析,不过关于编排部分,除了k8s,其他的有些过时了(现在k8s已经成为事实编排标准),本文只对docker原理章节做简单的整理,对docker原理有大体的了解。

Docker背后的内核知识

  • Docker的本质是宿主机上的进程
  • 通过namespace实现资源隔离
  • 通过cgroups实现资源限制
  • 通过写时复制(Copy on write, COW)实现文件高效操作

一. namespace资源隔离

linux内核提供了6种namespace的隔离的系统调用

namespace 系统调用参数 隔离内容
UTS CLONE_NEWUTS 主机名与域名
Network CLONE_NEWNET 网络设备、网络栈、端口
Mount CLONE_NEWNS 文件系统
User CLONE_NEWUSER 用户、用户组
PID CLONE_NEWPID 进程号
IPC CLONE_NEWIPC 信号量、消息队列和共享内存

1. 进行namespace api操作的四种方式

  • clone
  • setns
  • unshare
  • 查看/proc/{pid}/ns文件

调用这些api时,需要通过flag指定隔离的是哪种namespace,多种类型使用 | 符号组合

1.1 clone()在创建进程时创建namespace
  • clone()是fork()更通用的实现,flag参数控制使用多少功能
1.2 查看/proc/{pid}/ns文件
  • /proc/{pid}/ns下有六个文件,分别指向6中不同namesapce的namespace号
  • 两个进程的namespace号相同,则说明他们在同一个namespace下
  • docker中通过文件描述符fd加入一个存在的namespace是基本的方式
  • 使用mount –bind /proc/{pid}/ns/uts ~/uts也可以达到效果
1.3 setns()
  • setns用于加入一个已经存在的namespace
  • 函数原型:int setns(int fd, int nstype)
  • 配合execve()函数,可以实现加入namespae后执行用户命令,比如/bin/bash
1.4 unshare()
  • unshare()实现在原先进程上的隔离
  • unshare和clone很像,不过unshare工作在原先的进程,不需要创建新的进程
  • 函数原型:int unshare(int flags)
  • docker的实现中没有使用这个函数

2. 扩展知识-fork系统调用

  • unix系统中fork()函数被调用时,系统会创建新的进程,为其分配资源
  • 资源包括存储数据和代码空间
  • 把原来进程的数据都复制到新进程,只有少量值与原来进程不同
  • fork被调用一次,却在父进程和子进程中各返回一次
  • 父进程中返回创建的子进程的进程id
  • 子进程中返回0
  • 如果出错,返回一个负值
 1#include <stdio.h>
 2#include <unistd.h>
 3
 4//编译:gcc fork_example.c
 5//运行:./a.out
 6//输出:I am parent process.
 7//      I am child process.
 8
 9int main() {
10    int pid, fpid;
11    fpid = fork(); //这个函数执行后,变成父子两个进程执行之后的代码
12    if(fpid < 0) {
13	    printf("fork error.\n");
14    }
15    else if(fpid ==0){
16	    printf("I am child process.\n");
17    }
18    else {
19	    printf("I am parent process.\n");
20    }
21    return 0;
22}

3. UTS namespace

  • unix time sharing namespace.
  • 提供主机名和域名的隔离,这样每个docker容器就拥有独立的主机名和域名
  • docker将不再是宿主机的进程,而是一个独立的节点
  • docker中每个镜像都以服务名来命名hostname,原理就是UTS
 1#include <stdio.h>
 2#include <sys/types.h>
 3#include <sys/wait.h>
 4#include <sched.h>
 5#include <signal.h>
 6#include <unistd.h>
 7
 8#define STACK_SIZE (1024 * 1024)
 9
10static char child_stack[STACK_SIZE];
11
12char* const child_args = {
13    "/bin/bash",
14    NULL
15};
16
17int child_main() {
18    printf("child process.");
19    execv(child_args[0], child_args);
20    return 1;
21}
22
23int main() {
24    printf("strt process:...");
25    // 创建子进程时,指定新的命名空间,同时设置新进程的hostname
26    int child_pid = clone(child_main, child_stack + STACK_SIZE, SIGCHLD | CLONE_NEWNTS, NULL);
27    sethostname("NewNamespace", 12);
28    waitpid(child_pid, NULL, 0);
29    printf("exit.");
30    return 0;
31}

4. IPC Namespace

  • IPC(进程间通信)包括信号量、消息队列、共享内存
  • 申请IPC就申请了一个全局唯一的32位ID
  • ipc隔离的代码只需在前面的代码中添加CLONE_NEWIPC标识
 1#define _GNU_SOURCE
 2
 3#include <sys/types.h>
 4#include <sys/wait.h>
 5#include <stdio.h>
 6#include <sched.h>
 7#include <signal.h>
 8#include <unistd.h>
 9
10# define STACK_SIZE (1024 * 1024)
11
12static char child_stack[STACK_SIZE];
13char* const child_args[] = {
14  "/bin/sh",
15  NULL
16};
17
18int child_main(void* args){
19  printf("in child process now. \n");
20  if(execv(child_args[0], child_args) == -1){
21    perror("error on execve");
22  }
23  return 1;
24}
25
26int main() {
27  printf("start process: \n");
28  int child_pid = clone(child_main, child_stack + STACK_SIZE, SIGCHLD | CLONE_NEWUTS | CLONE_NEWIPC, NULL);
29  waitpid(child_pid, NULL, 0);
30  printf("exit.\n");
31  return 0;
32}

5. PID Namespace

  • 进程号隔离对进程PID重新标号,两个不同namespace下的进程可以有相同的PID
  • 每个PID namespace都有自己的计数程序
  • 内核为所有的PID namespace维护一个树状结构,最顶层为root namespace,系统初始时创建
  • 它创建的新的PID namespace成为child namespace
  • 父节点可以看到子节点的进程,单是子节点看不到父节点的进程
  • pid隔离的代码在前面代码中添加CLONE_NEWPID标识,执行echo $$打印当前进程号验证结论
  • 与其他namspace不同,PID namespace需要做一些额外的任务才能保证进程运行顺利,具体包括以下内容
 1#define _GNU_SOURCE
 2
 3#include <sys/types.h>
 4#include <sys/wait.h>
 5#include <stdio.h>
 6#include <sched.h>
 7#include <signal.h>
 8#include <unistd.h>
 9
10# define STACK_SIZE (1024 * 1024)
11
12static char child_stack[STACK_SIZE];
13char* const child_args[] = {
14  "/bin/sh",
15  NULL
16};
17
18int child_main(void* args){
19  printf("in child process now. \n");
20  if(execv(child_args[0], child_args) == -1){
21    perror("error on execve");
22  }
23  return 1;
24}
25
26int main() {
27  printf("start process: \n");
28  int child_pid = clone(child_main, child_stack + STACK_SIZE,
29    SIGCHLD | CLONE_NEWUTS | CLONE_NEWIPC | CLONE_NEWPID,
30    NULL);
31  waitpid(child_pid, NULL, 0);
32  printf("exit.\n");
33  return 0;
34}
5.1 PID namespace中的init进程
  • pid为1的进程是init进程,地位特殊,作为所有进程的父进程
  • 某些进程因为父进程异常,成为孤儿进程时,将由init进程负责资源的监控和回收
5.2 信号与init进程
  • 内核还为init进程赋予特殊权利–信号屏蔽
  • 该功能是防止init进程被误杀
  • init进程一旦被销毁,同一pid namespace中的其他进程也被销毁
  • docker内的init进程也提供这样的功能,对信号进行捕获,结束信号来临时回收相关资源
5.3 挂载proc文件系统
  • 在新的namespace中使用ps命令查看,看到的还是所有进程。因为相关的/proc系统没有被挂载到新的路径
  • 如果只想看到相关pid namespace的进程,需要单独挂载proc文件系统
  • 不过此时并没有实现mount namespace的隔离,会影响到宿主机的mount系统,退出namespace,宿主机执行ps会报错
  • 要实现完整的docker隔离,需要共同完成多个namespace的隔离
5.4 unshare和setns
  • 通过unshare和setns创建新的namespace时,pid的namespace和其他namespace有不同的地方
  • 这两个函数创建pid namespace时,调用者进程不会进入新的namespace,只有创建的子进程才会进入新的namespace
  • 但是创建其他类型namespace时,会直接进入新的namespace

6. mount namespace

  • 通过隔离文件系统挂载点对隔离文件系统提供支持
  • 它是历史上第一个namespace,所以名称毕竟特殊,是CLONE_NEWNS
  • 可以通过/proc/{pid}/mounts查看所有挂载到当前namespace的文件系统
  • 创建mount namespace时,会复制旧的文件结构给新namespace,新namespace操作不会影响外界。这种组发雅阁限制了隔离,但是有些情况不适用
  • 后来引入挂载传播解决该问题,挂载传播定了了挂载对象的关系,包括:共享关系,从属关系

7. network namespace

  • network namespace提供了网络资源隔离
  • 包括网络设备,ipv4、ipv6协议栈,ip路由、防火墙等
  • 由于一个物理设备最多存在于一个namespace,所以需要通过veth pair,即虚拟网络设备对,联通两个namespace,以达到通信目的

8. user namespace

  • 主要隔离安全相关的标识符和属性,包括用户id,用户组id,root目录等
  • user namespace创建的也是树形结构,和pid namespace非常相似。最上层是root,后续的每个节点都有一个父节点和0个或多个子节点
  • 创建完成后要进行用户绑定操作,绑定文件路径为/proc/{pid}/uid_map, /proc/{pid}/gid_map

runc库中关于namespace的定义

二. cgroups资源限制

  • cgroups不仅可以限制任务的资源使用,包括cpu,内存,io等
  • 还可以为资源设置权重,计算资源使用量,操控进程启停

1. 特点

  • cgroups本质上是内核附加在程序上的一系列钩子(hook),通过运行时对资源的调度触发相应钩子达到资源追踪和限制的目的
  • cgroups api以一个伪文件系统的方式实现,通过操作文件以管理cgroups
  • 管理可以细粒度到进程级别

2. 作用

  • 资源限制
  • 优先级分配
  • 资源统计:统计系统资源使用量
  • 任务控制:任务挂起、恢复等操作

3. 术语表

  • Task:任务,表示系统的一个进程或者线程
  • Cgroup:控制组,资源控制的基本单位,按照某种资源控制标准划分而成的任务组。包含一个或多个子系统。任务可以加入控制组,也可以迁移到其他控制组
  • Subsystem:一个资源调度控制器。cpu子系统可以控制cpu分配时间
  • Hierarchy:层级,由一系列cgroup以树状结构排列而成,每个层级通过绑定子系统进行资源控制

4. 组织结构和基本规则

  • 同一个层级可以附加一个或多个子系统

规则1

  • 一个子系统可以附加到多个层级,当且仅当目标层级只有一个子系统

规则2

  • 一个任务不能存在于一个层级的多个cgroups中,但可以存在于不同层级的多个cgroups中

规则3

  • fork或clone的子任务和原任务在同一个cgroups,但是子任务允许被移动到其他cgroups

规则4

5. 子系统简介

  • 子系统是cgroups的资源控制系统,每种子系统独立的控制一种资源
  • docker使用如下9种子系统:
    • blkio:磁盘、硬盘等设备输入输出限制
    • cpu:控制对cpu的使用
    • cpuacct:自动生成cgroups中任务对cpu资源使用情况的报告
    • cpuset:多处理系统中为任务分配cpu和内存
    • devices:开启或关闭对设备的访问
    • freezer:挂起或恢复任务
    • memory:限定cpu的使用,并自动生成报告
    • perf_even:统一的性能测试
    • net_cls:docker没有直接使用,控制流量
  • linux中:cgroups表现为文件系统,需要挂载(mount)mount)才能看到各类子系统

  • docker中:会在子系统的控制组目录下创建名为docker的控制组,文件内再创建以docker id为名称的容器控制组。docker内所有进程写入目录下task中,资源限额写入cpu.cfs_quota_us文件

  • docker实现cgroups主要由libcontainer完成

6. cgroups实现方式和工作原理

  • cgroups的本质的给任务挂上钩子,当任务运行过程中涉及某种资源时,就会触发钩子上的子系统进行检查并进行控制
6.1 如何判断资源超限以及超限后的措施
  • 不同的子系统,cgroups提供了统一的接口对资源进行控制和统计,但限制的方式不尽相同
  • 比如memory子系统,会将限额保存在描述内存的mm_struct结构体中,申请内存时,会触发cgroups检查
  • 超额后的措施:如果设置了OOM Control,进程会收到OOM信号并结束。否则线程会休眠等待。Docker默认设置了OOM Control
6.2 cgroup与任务的关联关系
  • cgroup与任务是多对多的关系,所以并不直接关联,而是通过中间结构关联起来
  • 任务结构体task_struct中包含指针指向cgroup
  • 子系统中也包含指向任务的指针
  • 同时为了好理解,内核按照虚拟文件系统VFS实现了一套cgroup文件系统,把子系统的实现都封装到文件中
 1# sched.h
 2struct task_struct {
 3    ...
 4    #ifdef CONFIG_CGROUPS
 5    	/* Control Group info protected by css_set_lock: */
 6    	struct css_set __rcu		*cgroups;
 7    	/* cg_list protected by css_set_lock and tsk->alloc_lock: */
 8    	struct list_head		cg_list;
 9    #endif
10    ...
11}
6.3 docker在使用cgroups的注意事项
  • docker需要挂载cgroup文件系统,挂载时指定要绑定的子系统
  • 挂载后就可以像操作文件一样对cgroup进行管理和操作
  • 操作cgroups只能通过文件操作,内核没有提供相关系统调用
  • 要绑定的子系统如果被别的层级绑定,就会报挂载失败的错误
  • 激活的层级无法被再挂载或者删除子系统
6.4 docker下cgroup文件系统说明
  • 目录:/sys/fs/cgroup/{子系统,如cpu}/docker/{container id}
  • tasks: 该cgroup中所有进程或线程id
  • cgroup.procs:该cgroup的线程组id(TGID)
  • notify_on_release:填0或1,默认0.是否在最后一个任务退出时通知release agent
  • release_agent:release_agent执行脚本路径

Docker架构概览

  • docker使用了client-server架构
  • 通过docker client与docker daemon建立通信,将请求发送给后者
  • 通过driver模块实现对docker容器执行环境的创建和管理
  • 通过network模块创建和管理网络环境
  • 通过vloume模块创建数据卷和挂载任务
  • 通过execdriver模块执行用户命令和限制运行资源
  • libcontainer是对namespace和cgroups的二次封装
  • execdriver通过libcontainer实现对容器的具体管理
  • docker1.9版本以后,volume和network生命周期独立于docker,可以单独创建,并按需配置给docker

Docker Daemon

  • docker daemon是docker最核心的后台进程,负责相应client的请求,并转换为对容器的操作
  • 该进程会在后台启动一个API Server,负责接受client请求,并分发调度到具体的函数执行

Docker client

  • docker client是一个泛称,用来向dameon发送请求
  • 既包括命令行工具,也包括遵循了Docker API的客户端

镜像管理

  • 几个模块统称为镜像管理模块
  • 具体包括的模块有:
    • distribution:负责与registry交互,上传下载镜像,以及存储元数据
    • registry:负责registry的身份验证、镜像查找、镜像验证、管理的操作
    • image:负责与镜像元数据有关的存储、查找、索引、tar有关的导入导出
    • reference:负责存储本地所有镜像的repository和tag,并维护与镜像id的映射关系
    • layer:镜像层与容器层元数据有关的增删改查

驱动相关(Driver)

  • docker daemon将请求转成系统调用,进而创建和管理容器。为了将这些系统调用抽象成统一的操作接口方便调用者调用。docker把这些操作封装成相关的驱动。

execdriver

  • 对linux操作系统的namespace,cgroups,selinux等容器运行所需的系统操作进行二次封装
  • execdriver现在最主要的实现,也是现在的默认实现。就是官方提供的libcontainer库

volumedriver

  • vloume数据存储操作的最终执行者,负责volume的增删改查,屏蔽不同驱动实现的区别
  • docker中volumedriver的默认实现的local,默认将文件存储于docker根目录下的volume文件里。其他的实现通过外部插件实现

graphdriver

  • graphdriver是与容器镜像相关操作的最终执行者
  • graphdriver会在docker工作目录下维护一组与镜像层对应的目录,并记下镜像层的关系
  • 对镜像的操作会被映射为对文件目录以及元数据的操作

Network

  • 由libnetwork库维护,它抽象出了一个容器网络模型,并给调用者提供统一的抽象接口

Client和Daemon工作原理

  • docker命令包含两种模式:client模式、daemon模式
  • docker命令如果有daemon子命令,则为daemon模式,启动daemon进程。其他命令则为client模式

Client模式

  • client模式的docker工作流程包括以下流程:

1. 解析flag信息

docker支持大量的参数信息,重要的几个包括:

  • Debug: -D, –debug。日志显示调试级别
  • LogLevel: -l, –log-level。日志基本,默认为info
  • Hosts: -H, –host。client模式该值为要连接的daemon地址,daemon模式该值为监听的地址
  • ProtoAddrParts:协议和host的全路径

2. 创建client实例

  • 调用NewDockerCli创建实例

3. 执行具体的命令

3.1 从命令映射到对应的方法
  • 通过反射机制,从输入命令(比如run)得到匹配的执行方法(比如CmdRun)
3.2 执行对应的方法,发起请求
  • 解析传入的参数,并针对参数进行配置处理
  • 获取与docker daemon通信所需的认证配置信息
  • 给docker daemon发送Post和Get请求
  • 读取来自Docker Daemon的返回结果

Daemon模式

docker运行时,如果指定docker daemon子命令,就运行daemon模式,docker daemon通过一个server模块接受来自client的请求,docker daemon启动和初始化过程包括:

1. API Server的配置和初始化

  • 整理解析各项参数
  • 创建PID文件
  • 加载所需的server辅助配置,包括日志,远程访问,TLS等
  • 通过goroutine启动API Server
  • 创建一个负责处理业务的Daemon对象,作为负责处理用户请求的逻辑实体
  • 对API Server中的路由表进行初始化,用户请求和处理函数做映射
  • 设置一个channel,保证上述goroutine只有在server出错时才会退出
  • 设置捕捉信号,当收到INT,TERM,QUIT信号时,关闭API Server,调用shutdownDaemon关闭daemon
  • APiserver与daemon绑定,接受来自客户端的请求
  • daemon进程向宿主机init进程发送READY=1的信号,表示开始正常工作

2. Daemon对象的创建与初始化过程

2.1 docker容器的配置信息

提供用户自由配置的可选功能,使docker运行更贴近用户期望的场景,配置信息包括:

  • 设置默认的网络最大传输单元
  • 检测网桥配置信息
2.2 检测系统支持及用户权限
  • 操作系统对daemon的支持
  • 用户权限的级别
  • 内核版本与处理器的支持
2.3 配置daemon工作路径
  • 主要是创建daemon运行的工作路径,默认是/var/lib/docker
2.4 配置docker容器所需的文件环境

这一步会在工作目录下创建一些重要的目录和文件

  • 创建容器配置文件目录
    • 路径:/var/lib/docker/containers/{container id}
    • 文件包括:
      • config.json
      • hostname
      • hosts
      • resolve
  • 配置graphdriver目录,用于完成docker容器镜像管理所需的底层存储驱动
  • 配置镜像目录:创建image目录,用于存放镜像。/var/lib/docker/images
  • 创建volume驱动目录:/var/lib/docker/volumes,metadata.db用来存储元数据
2.5 创建网络
  • 网络部分单独抽离一个模块称为libnetwork
  • libnetwork通过插件形式为docker服务
  • bridge为默认网络驱动,它无法跨主机通信。overlay网络可以跨主机通信
2.6 初始化execdriver

execdriver是管理docker容器的驱动,创建execdriver时,需要注意以下信息:

  • 驱动类别,配置文件中默认为native,对应libcontainer
  • 用户定义的execdriver选项,即-exec-opt参数值
  • -exc-root,即execdriver运行根目录,默认为/var/lib/docker
  • 系统功能信息,包括内存限制功能,交换器内存限制等

2.7 总结:Daemon进程启动三步

  • 启动一个Api Server,工作在用户通过-H指定的socket上
  • NewDaemon创建daemon对象来保存信息和处理业务逻辑
  • 将API Server与daemon对象绑定起来,用于处理客户端请求

3. daemon如何处理客户端请求

从docker run命令分析处理的流程

3.1 发起请求

  • docker run运行,用户端docker进入client模式
  • 经过初始化,创建出一个client
  • client通过反射找到CmdRun方法
  • client解析用户参数后,发送两个post请求
    • /containers/create? + containerValues 创建容器
    • /containers + createResponse.Id + start 启动容器
  • daemon端负责接收create请求的方法为postContainerCreate

3.2 创建容器

  • 解析用户表单,在daemo中创建container对象
  • container信息作为response返回给客户端

3.3 启动容器

  • 所有请求都通过/api/client/{请求名}.go文件来处理,这里是start.go
  • start.go中执行ContainerStart启动容器

3.4 最后一步

  • 跟操作系统相关的系统调用(namespace, cgroups)交给ExecDriver
  • docker中默认为libcontainer,它封装了namespace,cgroups等对操作系统的调用
  • daemon调用libcontainer需要提供以下三个参数
    • commandv:容器需要的所有配置信息集合
    • pipes:用于将容器stdin、stdout、stderror重定向到daemon
    • startCallback:回调方法

4. libcontainer

  • 容器是一个与宿主机系统共享内核但与其他进程资源隔离的执行环境,docker要实现隔离,需要调用linux的系统函数,该工作交给libcontainer来做。
  • execdriver:拿到daemon传过来的command信息,生成一份专门的容器配置
  • libcontainer:拿到容器配置文件,创建容器

4.1 libcontainer的工作方式

  • 创建容器运行时需要的进程对象,称为Process
  • 设置容器的输出管道
  • 使用工厂类Factory创建逻辑容器Container
  • 执行Container.strt(Process)创建物理容器
  • execdriver执行callback回调
  • 执行Process.wait,一直等到创建完成

libcontainer定义了Process、Container来对应linux中进程和容器的关系

4.2 libcontainer的实现原理

用Factory创建逻辑容器Container
  • 逻辑容器对象包括容器要运行的指令及其参数、namespace、cgroups等信息
  • libcontainer底层需要兼容不同的操作系统,所以用工厂方法来创建
  • factory创建对象具体做的事:
    • 验证容器运行的根目录、容器id、容器配置的合法性
    • 验证容器id与现有的容器不冲突
    • 创建工作目录
    • 返回Container对象
启动逻辑容器
  • 创建管道pipe,用来与容器内进程通信
  • 创建容器内进程启动命令对象cmd,并且cmd对象创建第一个进程:init进程
  • 为cmd对象设置环境变量,指明当前为创建动作(exec执行时是进入操作,不会创建)
  • namespace添加到cmd的Cloneflags中
  • 合并container中容器的配置和Process中entrypoint配置,加入到ParentProcess中
  • ParentProcess负责从物理容器外部处理物理容器启动工作
  • ParentProcess是接口,实现为initProcess,调用它的start方法就可以创建容器
逻辑容器创建物理容器

initProcess.start()的具体流程:

  • 调用exec执行initProcess.cmd,创建新的进程dockerinit并设置namespace
  • 将dockerinit的pid加入cgroups中管理,容器的隔离环境初步建立完成
  • 创建容器网络设备,包括veth
  • 通过管道发送容器配置给容器内的dockerinit
  • 通过管道等到dockerinit完成初始化工作
  • dockerinit只有一个函数,执行reexec.init()reexec.init(),由注册的具体实现决定
  • 对于libcontainer来说,是StartInitialization方法,具体包括:(以下方法都是在容器内进行)
    • 创建pip管道的文件描述符
    • 通过管道获取ParentProcess传来的容器配置
    • 从配置文件获取变量信息
    • 如果docker run指定了-pid,-ipc, -uts等参数,dockerinit还要把自己加入namespace
    • 初始化网络设备:修改名称,分配mac地址,添加ip地址,默认网关等
    • 设置路由和RLIMIT参数
    • 创建mount namespace,为挂载文件系统做准备
    • 挂载各类设备文件,比如/proc
    • 写入hostname信息,加载profile信息
    • 比较当前进程父进程id与初始化进程,确保没有出错
    • 最后使用execv执行用户args参数指定的命令
docker daemon与容器之间的通讯
  • 创建容器的进程为父进程,容器进程为子进程,他们如何通讯?
  • 父子通讯的四种方式:
    • signal:信号。—包含的信息有限,不适合
    • 内存轮训:效率低,不适合
    • socket通讯:网络栈是隔离的,无法通讯
    • 管道:基于文件和文件描述符。pipe系统调用参数为两个文件描述符,在一端写入,另一端就可以读取

4.3 使用runC与libcontainer交互

  • linux基金会成立OCI(open container Initiative),旨在指定开放的容器标准,指定容器标准格式(OCF)
  • runC库是实现OCF的一个库,直接对libcontainer进行调用,去除docker中复杂的功能
runC的构建
  • 使用runC需要相关容器配置文件以及rootfs
  • 配置文件可用runc spec生成
  • config.json为容器基础配置文件
  • runtime.json为容器运行时文件
runC的运行
  • delete:删除容器运行时资源
  • enents:展示运行时cpu,io等资源
  • exec:容器中运行新的进程
  • list:展示被runc启动的容器
  • ps:展示运行的容器
  • start:启动容器
  • state:展示容器状态

5. 镜像管理

5.1 什么是docker镜像

  • 是一个只读的docker容器模板,含有启动容器所需的文件系统结构和内容
  • docker镜像的文件内容以及一些运行docker容器的配置文件组成了docker容器的静态文件系统运行环境–rootfs
  • 镜像是容器的静态视角,容器是镜像的运行状态
5.1.1 rootfs
  • rootfs是docker容器在启动时内部进程可见的文件系统,即docker容器的根目录
  • 通常包含一个操作系统运行所需的文件系统,比如/bin /dev /proc /etc等
5.1.2 docker镜像的主要特点
  • 分层:docker镜像采用分层构建,每个镜像由一系列镜像层构成,docker镜像轻量的重要原因
  • 写时复制:写时复制在多个容器之间共享镜像,每个容器启动时不需要单独复制一份镜像文件。而是以只读方式将镜像层挂载到挂载点上,再在上面覆盖一个读写层
  • 内容寻址:计算层内容计算校验和,并生成哈希值,作为镜像层的唯一标识
  • 联合挂载:一个挂载点同时挂载多个文件系统,这种技术称为联合文件系统
5.1.3 docker镜像的存储组织方式

5.2 docker镜像关键概念

  • registry:保存镜像的仓库,repository的集合
  • repository:镜像的集合
  • manifest:docker镜像描述文件,pull或load时被转换为config配置文件
  • image:存储镜像相关的一组元数据信息
  • layer:管理镜像层的中间概念
  • dockerfile:docker镜像的定义文件

5.3 docker镜像构建操作

  • commit镜像:提交镜像变更的部分
  • build镜像:完成新镜像的构建,路径支持:本地普通文件,压缩文件,git仓库,url地址

5.4 docker镜像的分发方法

  • pull:线上拉取镜像
  • push:推送镜像到线上
  • export:导出容器
  • import:导入容器
  • save:离线导出镜像,保存时每层保存为一个文件,所以多个镜像公共层多时,会公用文件夹,整体大小会缩小很多
  • load:离线导入镜像

6. 存储管理

docker镜像存储时,元数据与镜像完全分离

6.1 repository元数据

  • repository在本地的持久化文件存放于/var/lib/docker/image/xxx/repository.json
  • 存储了所有版本镜像名字,tag和镜像id

6.2 image元数据

  • image元数据包括:镜像架构、操作系统、默认配置、容器id、创建时间,镜像版本等
  • imageStore管理镜像id与元数据的映射关系

6.3 layer元数据

  • docker定义了两种接口:Layer和RWLayer,定义只读层和可读可写层的一些操作
  • 又定义了roLayer和mountedLayer,分别实现上述接口

6.2 存储驱动

为了支持镜像分层和写时复制,docker提供了存储驱动的接口。驱动根据操作系统底层的支持提供了针对某种文件系统的初始化和对镜像的增删改查

存储驱动的功能和管理
  • docker中管理文件系统的驱动为graphdriver,定义了统一的接口对不同的文件系统进行管理
常用的存储驱动
  • aufs
  • device mapper
  • overlay

7. 数据卷

7.1 volume概述

docker镜像分层管理,下层的只读层和上层读写层。这种管理方式提高了镜像构建、存储、分发效率。但是存在一些问题:

  • 不方便在宿主机中访问容器文件
  • 多个容器无法数据共享
  • 删除容器时,产生的数据将会丢失

docker引入数据卷解决以上问题,volume是存在一个或多个容器的特定文件或文件夹,以联合文件系统存在,为数据共享和持久化提供便利

7.2 volume特点

  • 容器创建时就初始化,容器启动后就可以使用
  • 能在不同的容器之间共享和重用
  • 对volume的数据操作会马上生效
  • 对volume的操作不会影响镜像本身
  • 生命周期独立于容器
  • docker提供vloumedriver接口

7.3 使用方式

  • 命令创建
    • docker volume create –name xxx
    • dcoker run -v name:path
  • dockerfile创建:
    • VOLUME path
  • 共享volume:
    • –volumes-from
  • 删除:docker volume rm
  • 备份迁移:存档或者–volumes-from

8. 网络管理

8.1 网络架构

  • CNM:容器网络模型,虚拟化网络标准接口
  • libnetwork作为一个独立的库,docker调用libcontainer提供的api来创建和管理网络
  • CNM主要由沙盒、端点和网络三大组件
    • 沙盒:包含一个容器网络栈的信息,可以对容器的接口、路由、DNS设置进行管理
    • 端点:一个端点可以加入一个沙盒或者一个网络
    • 网络:一组可以直通的端点
  • 内置5种驱动提供不同的网络服务
    • brige:默认设置,容器连接到docker网桥上
    • host:不会创建独立的网络协议栈,容器公用宿主机网络协议
    • overlay:适合大规模场景,使用时需要额外配置存储,daemon启动时需要配置存储地址
    • remote:调用用户自己实现的网络插件
    • null:不对网络进行任何配置,容器网络没有任何网卡、路由信息

8.2 brige驱动实现原理

docker0网桥
  • docker安装后,会多一块docker0的网卡
  • docker0是一个网桥,通过veth pair连接,一端连到eth0,另一端连到容器。容器内的网络通过docker0网桥转发到eth0
  • daemon启动参数设置:
    • –bip=172.17.0.0/16,设置ip地址和子网范围
    • –fixed-cir=xxx,设置子网范围
    • –bridge,指定自己的网桥

参考

  • 《Docker容器与容器云》第三章