参考文献

底层实现原理及关键技术

Docker与虚拟机的区别

  • 虚拟机是通过管理系统(Hypervisor)模拟CPU,内存,网络等硬件,然后在这些模拟的硬件上创建客户内核和操作系统.
    • 这样做的好处就是虚拟机有自己的内核和操作系统,并且硬件都是通过虚拟机管理系统模拟出来的,用户程序无法直接使用到主机的操作系统和硬件资源,因此虚拟机也对隔离性和安全性有着更好的保证.
  • Docker容器则是通过Linux内核的Namespace技术实现了文件系统、进程、设备以及网络的隔离,然后再通过CgroupsCPU、内存等资源进行限制,最终实现了容器之间相互不受影响,由于容器的隔离性仅仅依靠内核来提供,因此容器的隔离性也远弱于虚拟机

img

资源限制

1
2
3
--cpus                          限制 CPU 配额
-m, --memory 限制内存配额
--pids-limit 限制容器的 PID 个数
1
docker run -it --cpus=1 -m=2048m --pids-limit=1000 caoboy sh

资源限制cgroups

  • Cgroups(ControlGroups)是Linux下用于对一个或一组进程进行资源控制和监控的机制;

    • /sys/fs/cgroup/
  • 可以对诸如CPU使用时间、内存、磁盘I/O等进程所需的资源进行限制;

  • 不同资源的具体管理工作由相应的Cgroup子系统(Subsystem)来实现;

  • 针对不同类型的资源限制,只要将限制策略在不同的的子系统上进行关联即可;

  • Cgroups在不同的系统资源管理子系统中以层级树(Hierarchy)的方式来组织管理:每个Cgroup都可以包含其他的子Cgroup,因此子Cgroup能使用的资源除了受本Cgroup配置的资源参数限制,还受到父Cgroup设置的资源限制.

cgroups提供的功能

  • 资源限制: 限制资源的使用量,例如我们可以通过限制某个业务的内存上限,从而保护主机其他业务的安全运行.
  • 优先级控制:不同的组可以有不同的资源( CPU 、磁盘 IO 等)使用优先级.
  • 资源审计:计算控制组的资源使用情况.
  • 任务控制:cgroup可以对任务执行挂起、恢复等操作。

cgroups三个核心概念

  • 子系统(subsystem):是一个内核的组件,一个子系统代表一类资源调度控制器.例如内存子系统可以限制内存的使用量,CPU 子系统可以限制 CPU 的使用时间.
  • 控制组(cgroup):表示一组进程和一组带有参数的子系统的关联关系.例如,一个进程使用了 CPU 子系统来限制 CPU 的使用时间,则这个进程和 CPU 子系统的关联关系称为控制组.
  • 层级树(hierarchy):是由一系列的控制组按照树状结构排列组成的.这种排列方式可以使得控制组拥有父子关系,子控制组默认拥有父控制组的属性,也就是子控制组会继承于父控制组.比如,系统中定义了一个控制组 c1,限制了 CPU 可以使用 1 核,然后另外一个控制组 c2 想实现既限制 CPU 使用 1 核,同时限制内存使用 2G,那么 c2 就可以直接继承 c1,无须重复定义 CPU 限制.

容器监控原理

  • Cgroups的工作目录为/sys/fs/cgroup,/sys/fs/cgroup目录下包含了Cgroups的所有内容.Cgroups包含很多子系统,可以用来对不同的资源进行限制.例如对CPU、内存、PID、磁盘IO等资源进行限制和监控
  • 容器的监控原理其实就是定时读取Linux 主机上相关的文件并展示给用户

可配额/可度量

  • cgroups 实现了对资源的配额和度量.
    • blkio:这个子系统设置限制每个块设备的输入输出控制.例如:磁盘,光盘以及 USB 等等;
    • cpu:这个子系统使用调度程序为 Cgroup任务提供 CPU 的访问;
    • cpuacct:产生 Cgroup任务的 CPU 资源报告;
    • cpuset:如果是多核心的CPU,这个子系统会为 Cgroup任务分配单独的 CPU 和内存;
    • devices:允许或拒绝 Cgroup任务对设备的访问;
    • freezer:暂停和恢复 Cgroup任务;
    • memory:设置每个 Cgroup的内存限制以及产生内存资源报告;
    • net_cls:标记每个网络包以供 Cgroup方便使用;
    • ns:名称空间子系统;
    • pid: 进程标识子系统.

CPU 子系统

  • cpu.shares:可出让的能获得 CPU 使用时间的相对值.
  • cpu.cfs_period_us:cfs_period_us 用来配置时间周期长度,单位为 us(微秒).
  • cpu.cfs_quota_us:cfs_quota_us 用来配置当前 Cgroup在 cfs_period_us 时间内最多能使用的 CPU
    时间数,单位为 us(微秒).
  • cpu.stat :Cgroup内的进程使用的 CPU 时间统计.
  • nr_periods :经过 cpu.cfs_period_us 的时间周期数量.
  • nr_throttled :在经过的周期内,有多少次因为进程在指定的时间周期内用光了配额时间而受到限制.
  • throttled_time :Cgroup中的进程被限制使用 CPU 的总用时,单位是 ns(纳秒)

cpuacct 子系统

  • 用于统计 Cgroup及其子 Cgroup下进程的 CPU 的使用情况.
    • cpuacct.usage
      • 包含该 Cgroup及其子 Cgroup下进程使用 CPU 的时间,单位是 ns(纳秒).
    • cpuacct.stat
      • 包含该 Cgroup及其子 Cgroup下进程使用的 CPU 时间,以及用户态和内核态的时间.

memory 子系统

  • memory.usage_in_bytes

    • Cgroup下进程使用的内存,包含Cgroup及其子Cgroup下的进程使用的内存
  • memory.max_usage_in_bytes

    • Cgroup下进程使用内存的最大值,包含子Cgroup的内存使用量.
  • memory.limit_in_bytes

    • 设置Cgroup下进程最多能使用的内存.如果设置为-1,表示对该Cgroup的内存使用不做限制.
  • memory.oom_control

    • 设置是否在Cgroup中使用OOM(Out of Memory)Killer,默认为使用.当属于该Cgroup的进程使用的内存超过最大的限定值时,会立刻被OOM Killer处理.

资源隔离Namespace

  • Docker是使用Linux的Namespace技术实现各种资源隔离的

  • Namespace是Linux内核的一个特性,该特性可以实现在同一主机系统中,对进程ID、主机名、用户ID、文件名、网络和进程间通信等资源的隔离.Docker利用Linux内核的Namespace特性,实现了每个容器的资源相互隔离,从而保证容器内部只能访问到自己Namespace的资源.

Namespace名称 作用 内核版本
Mount(mnt) 隔离挂载点 2.4.19
Procecss ID(pid) 隔离进程ID 2.6.24
Network(net) 隔离网络设备,网络协议,网络端口等 2.6.29
Interprocess Communication(ipc) 隔离System V IPC和POSIX Message Queues 2.6.19
UTS Namespace(uts) 隔离主机名和域名 2.6.19
User Namespace(user) 隔离用户和用户组 3.8
Control Group Namespace(cgroup) 隔离Cgroup根目录 4.6
Time Namespace 隔离系统时间 5.6

Namespace的常用操作

查看当前系统的Namespace
1
lsns -t <type>
1
2
3
4
[root@iZuf6ib0sh7w9cc92x0h4qZ ~]# lsns -t ipc
NS TYPE NPROCS PID USER COMMAND
4026531839 ipc 114 1 root /usr/lib/systemd/systemd --system --deserialize 23
4026532211 ipc 18 99877 root nsenter --target 3018847 --mount --uts --ipc --net --pid
查看某进程的Namespace
1
ls -la /proc/<pid>/ns
1
2
3
4
5
6
7
8
9
10
11
12
[root@iZuf6ib0sh7w9cc92x0h4qZ proc]# ls -la /proc/481990/ns/
总用量 0
dr-x--x--x 2 root root 0 4月 30 14:56 .
dr-xr-xr-x 9 root root 0 1月 16 15:26 ..
lrwxrwxrwx 1 root root 0 4月 30 14:59 `Cgroup`-> 'cgroup:[4026531835]'
lrwxrwxrwx 1 root root 0 4月 30 14:57 ipc -> 'ipc:[4026531839]'
lrwxrwxrwx 1 root root 0 4月 30 14:57 mnt -> 'mnt:[4026531840]'
lrwxrwxrwx 1 root root 0 4月 30 14:57 net -> 'net:[4026531992]'
lrwxrwxrwx 1 root root 0 4月 30 14:56 pid -> 'pid:[4026531836]'
lrwxrwxrwx 1 root root 0 4月 30 14:59 pid_for_children -> 'pid:[4026531836]'
lrwxrwxrwx 1 root root 0 4月 30 14:59 user -> 'user:[4026531837]'
lrwxrwxrwx 1 root root 0 4月 30 14:59 uts -> 'uts:[4026531838]'
进入某Namespace运行命令
1
nsenter -t <pid> -n ip addr

文件系统(Union FS)

  • 将不同目录挂载到同一个虚拟文件系统下 (unite several directories into a single virtual filesystem)的文件系统.
  • 支持为每一个成员目录(类似Git Branch)设定 readonly、readwrite 和 whiteout-able 权限.
  • 文件系统分层, 对 readonly 权限的 branch 可以逻辑上进行修改(增量地, 不影响 readonly 部分的).
  • 通常 Union FS 有两个用途, 一方面可以将多个disk挂到同一个目录下, 另一个更常用的就是将一个 readonly 的 branch 和一个 writeable 的 branch 联合在一起

Docker 的文件系统

  • Bootfs(boot file system)
    • Bootloader - 引导加载 kernel,
    • Kernel - 当 kernel 被加载到内存中后 umount bootfs.
  • rootfs (root file system)
    • /dev,/proc,/bin,/etc 等标准目录和文件.
    • 对于不同的 linux 发行版, bootfs 基本是一致的,
      但 rootfs 会有差别.

Docker 启动

  • Linux
    • 在启动后,首先将 rootfs 设置为 readonly, 进行一系列检查, 然后将其切换为 “readwrite”
      供用户使用.
  • Docker启动
    • 初始化时也是将 rootfs 以 readonly 方式加载并检查,然而接下来利用 union mount 的方式
      将一个 readwrite 文件系统挂载在 readonly 的 rootfs 之上;
    • 并且允许再次将下层的 FS(file system) 设定为 readonly 并且向上叠加;
    • 这样一组 readonly 和一个 writeable 的结构构成一个 container 的运行时态, 每一个 FS 被称
      作一个 FS 层.

Docker网络原理Libnetwork

  • CNM (Container Network Model) 是 Docker 发布的容器网络标准,意在规范和指定容器网络发展标准,CNM 抽象了容器的网络接口 ,使得只要满足 CNM 接口的网络方案都可以接入到 Docker 容器网络,更好地满足了用户网络模型多样化的需求.

  • CNM 只是定义了网络标准,对于底层的具体实现并不太关心,这样便解耦了容器和网络,使得容器的网络模型更加灵活.

  • CNM 定义的网络标准包含三个重要元素.

    • 沙箱(Sandbox):沙箱代表了一系列网络堆栈的配置,其中包含路由信息、网络接口等网络资源的管理,沙箱的实现通常是 Linux 的 Net Namespace,但也可以通过其他技术来实现,比如 FreeBSD jail 等.
    • 接入点(Endpoint):接入点将沙箱连接到网络中,代表容器的网络接口,接入点的实现通常是 Linux 的 veth 设备对.
    • 网络(Network):网络是一组可以互相通信的接入点,它将多接入点组成一个子网,并且多个接入点之间可以相互通信.
  • 为了更好地构建容器网络标准,Docker 团队把网络功能从 Docker 中剥离出来,成为独立的项目 libnetwork,它通过插件的形式为 Docker 提供网络功能.Libnetwork 是开源的,使用 Golang 编写,它完全遵循 CNM 网络规范,是 CNM 的官方实现.Libnetwork 的工作流程也是完全围绕 CNM 的三个要素进行的.

  • 注意: 为了让容器与外界网络相连,首先要保证主机能允许转发 IP 数据包,另外需要让 iptables 能指定特定的 IP 链路。通过系统参数 ip_forward 来调节开关

    1
    2
    3
    4
    5
    $ sysctl net.ipv4.conf.all.forwarding
    net.ipv4.conf.all.forwarding = 0
    $ sysctl net.ipv4.conf.all.forwarding=1
    $ sysctl net.ipv4.conf.all.forwarding
    net.ipv4.conf.all.forwarding = 1

Libnetwork工作原理

  • Libnetwork 是 Docker 启动容器时,用来为 Docker 容器提供网络接入功能的插件,它可以让 Docker 容器顺利接入网络,实现主机和容器网络的互通

  • 第一步:Docker 通过调用 libnetwork.New 函数来创建 NetworkController 实例.NetworkController 是一个接口类型,提供了各种接口,代码如下:

    1
    2
    3
    4
    5
    type NetworkController interface {
    // 创建一个新的网络. options 参数用于指定特性类型的网络选项.
    NewNetwork(networkType, name string, id string, options ...NetworkOption) (Network, error)
    // ... 此次省略部分接口
    }
  • 第二步:通过调用 NewNetwork 函数创建指定名称和类型的 Network,其中 Network 也是接口类型,代码如下:

    1
    2
    3
    4
    5
    6
    7
    type Network interface {
    // 为该网络创建一个具有唯一指定名称的接入点(Endpoint)
    CreateEndpoint(name string, options ...EndpointOption) (Endpoint, error)
    // 删除网络
    Delete() error
    // ... 此次省略部分接口
    }
  • 第三步:通过调用 CreateEndpoint 来创建接入点(Endpoint).在 CreateEndpoint 函数中为容器分配了 IP 和网卡接口.其中 Endpoint 也是接口类型,代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // Endpoint 表示网络和沙箱之间的逻辑连接.
    type Endpoint interface {
    // 将沙箱连接到接入点,并将为接入点分配的网络资源填充到沙箱中.
    // the network resources allocated for the endpoint.
    Join(sandbox Sandbox, options ...EndpointOption) error
    // 删除接入点
    Delete(force bool) error
    // ... 此次省略部分接口
    }
  • 第四步:调用 NewSandbox 来创建容器沙箱,主要是初始化 Namespace 相关的资源.

  • 第五步:调用 Endpoint 的 Join 函数将沙箱和网络接入点关联起来,此时容器就加入了 Docker 网络并具备了网络访问能力.

Libnetwork 常见网络模式

  • null 空网络模式:可以帮助我们构建一个没有网络接入的容器环境,以保障数据安全.
  • bridge 桥接模式:可以打通容器与容器间网络通信的需求.
  • host 主机网络模式:可以让容器内的进程共享主机网络,从而监听或修改主机网络.
  • container 网络模式:可以将两个容器放在同一个网络命名空间内,让两个业务通过 localhost 即可实现访问.
null 空网络模式
  • 应用场景: 有时候,我们需要处理一些保密数据,出于安全考虑,我们需要一个隔离的网络环境执行一些纯计算任务.这时候 null 网络模式就派上用场了,这时候我们的容器就像一个没有联网的电脑,处于一个相对较安全的环境,确保我们的数据不被他人从网络窃取.

  • 使用方式: docker run命令启动时,添加--net=none参数启动一个空网络模式的容器

    1
    docker run --net=none image
bridge 桥接模式
  • Docker 的 bridge 网络是启动容器时默认的网络模式,使用 bridge 网络可以实现容器与容器的互通,可以从一个容器直接通过容器 IP 访问到另外一个容器.同时使用 bridge 网络可以实现主机与容器的互通,我们在容器内启动的业务,可以从主机直接请求.

  • Linux 的 veth 和 bridge 相关的技术,因为 Docker 的 bridge 模式正是由这两种技术实现的.

    • Linux veth

      • veth 是 Linux 中的虚拟设备接口,veth 都是成对出现的,它在容器中,通常充当一个桥梁.veth 可以用来连接虚拟网络设备,例如 veth 可以用来连通两个 Net Namespace,从而使得两个 Net Namespace 之间可以互相访问.
    • Linux bridge

      • Linux bridge 是一个虚拟设备,是用来连接网络的设备,相当于物理网络环境中的交换机.Linux bridge 可以用来转发两个 Net Namespace 内的流量.

    img

    • bridge 就像一台交换机,而 veth 就像一根网线,通过交换机和网线可以把两个不同 Net Namespace 的容器连通,使得它们可以互相通信.
    • Docker 的 bridge 模式也是这种原理.Docker 启动时,libnetwork 会在主机上创建 docker0 网桥,docker0 网桥就相当于图 1 中的交换机,而 Docker 创建出的 brige 模式的容器则都会连接 docker0 上,从而实现网络互通.
    • bridge 桥接模式是 Docker 的默认网络模式,当我们创建容器时不指定任何网络模式,Docker 启动容器默认的网络模式为 bridge.
host主机网络模式
  • 容器内的网络并不是希望永远跟主机是隔离的,有些基础业务需要创建或更新主机的网络配置,我们的程序必须以主机网络模式运行才能够修改主机网络,这时候就需要用到 Docker 的 host 主机网络模式.

  • 使用 host 主机网络模式时:

    • libnetwork 不会为容器创建新的网络配置和 Net Namespace.

    • Docker 容器中的进程直接共享主机的网络配置,可以直接使用主机的网络信息,此时,在容器内监听的端口,也将直接占用到主机的端口.

    • 除了网络共享主机的网络外,其他的包括进程、文件系统、主机名等都是与主机隔离的.

  • 使用方式: docker run命令启动时,添加--net=host参数启动一个空网络模式的容器

    1
    docker run --net=host image
container 网络模式
  • container 网络模式允许一个容器共享另一个容器的网络命名空间.当两个容器需要共享网络,但其他资源仍然需要隔离时就可以使用 container 网络模式.

    • 例如我们开发了一个 http 服务,但又想使用 nginx 的一些特性,让 nginx 代理外部的请求然后转发给自己的业务,这时我们使用 container 网络模式将自己开发的服务和 nginx 服务部署到同一个网络命名空间中.
  • 使用方式

    1
    2
    3
    4
    5
    # 启动第一个容器
    docker run -d --name=cowboy1 cowboy

    # 启动第二个容器,与第一个容器共享网络
    docker run -d --net=container:cowboy1 --name=cowboy2 cowboy