Docker-实现原理
参考文献
底层实现原理及关键技术
Docker
与虚拟机的区别
- 虚拟机是通过管理系统(
Hypervisor
)模拟CPU,内存,网络等硬件,然后在这些模拟的硬件上创建客户内核和操作系统.- 这样做的好处就是虚拟机有自己的内核和操作系统,并且硬件都是通过虚拟机管理系统模拟出来的,用户程序无法直接使用到主机的操作系统和硬件资源,因此虚拟机也对隔离性和安全性有着更好的保证.
Docker
容器则是通过Linux
内核的Namespace
技术实现了文件系统、进程、设备以及网络的隔离,然后再通过Cgroups
对CPU
、内存等资源进行限制,最终实现了容器之间相互不受影响,由于容器的隔离性仅仅依靠内核来提供,因此容器的隔离性也远弱于虚拟机
资源限制
1 | --cpus 限制 CPU 配额 |
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
)来实现; -
针对不同类型的资源限制,只要将限制策略在不同的的子系统上进行关联即可;
-
Cgroup
s在不同的系统资源管理子系统中以层级树(Hierarchy
)的方式来组织管理:每个Cgroup
都可以包含其他的子Cgroup
,因此子Cgroup
能使用的资源除了受本Cgroup
配置的资源参数限制,还受到父Cgroup
设置的资源限制. -
判断linux使用的cgroup版本
- cgroup v1: 如果系统使用 cgroup v1,/sys/fs/cgroup 目录下会有多个子目录,这些子目录的名称对应不同的控制器,如 cpu, memory, blkio 等。
- cgroup v2: 如果系统使用 cgroup v2,/sys/fs/cgroup 目录下通常只有一个子目录,名称为 unified,或者整个 /sys/fs/cgroup 目录下是一个平坦的结构,没有其他子目录,只有一些文件如 cgroup.controllers, cgroup.procs, cgroup.subtree_control 等。
1
mount | grep 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
的所有内容.Cgroup
s包含很多子系统,可以用来对不同的资源进行限制.例如对CPU、内存、PID、磁盘IO等资源进行限制和监控- 容器的监控原理其实就是定时读取Linux 主机上相关的文件并展示给用户
可配额/可度量
cgroups
实现了对资源的配额和度量.blkio
:这个子系统设置限制每个块设备的输入输出控制.例如:磁盘,光盘以及 USB 等等;blkio
属于Cgroup v1
,可以用来限制容器中进程的读写的 IOPS 和吞吐量(Throughput),但是它只能对于 Direct I/O 的读写文件做磁盘限速,对 Buffered I/O 的文件读写,它无法进行磁盘限速.- 这是因为 Buffered I/O 会把数据先写入到内存 Page Cache 中,然后由内核线程把数据写入磁盘,而 Cgroup v1 blkio 的子系统独立于 memory 子系统,无法统计到由 Page Cache 刷入到磁盘的数据量。
- 开启
Cgroup v2
方法: 配置一个 kernel 参数cgroup_no_v1=blkio,memory
,这表示把 Cgroup v1 的 blkio 和 Memory 两个子系统给禁止,这样 Cgroup v2 的 io 和 Memory 这两个子系统就打开了。
cpu
:这个子系统使用调度程序为Cgroup
任务提供 CPU 的访问;cpuacct
:产生Cgroup
任务的 CPU 资源报告;cpuset
:如果是多核心的CPU,这个子系统会为Cgroup
任务分配单独的 CPU 和内存;devices
:允许或拒绝Cgroup
任务对设备的访问;freezer
:暂停和恢复Cgroup
任务;memory
:设置每个Cgroup
的内存限制以及产生内存资源报告;net_cls
:标记每个网络包以供Cgroup
方便使用;ns
:名称空间子系统;pid
: 进程标识子系统.
CPU
子系统
-
CFS
(Completely Fair Scheduler
,即完全公平调度器) -
cpu.shares
:可出让的能获得 CPU 使用时间的相对值. -
cpu.cfs_period_us
:cfs_period_us
用来配置时间周期长度,单位为 us(微秒),- 一般它的值是100000,也就 100ms
-
cpu.cfs_quota_us
:cfs_quota_us
用来配置当前Cgroup
在cfs_period_us
时间内最多能使用的 CPU
时间数,单位为 us(微秒).- 当 cpu.cfs_quota_us的值为 -1 时,表示这个 cgroup 中的任务 不受 CPU 时间配额的限制,也就是说,该 cgroup 可以在 CPU 上使用不受限制的时间,等同于无限制使用。
- 如果用这个值去除以调度周期(也就是 cpu.cfs_period_us),50ms/100ms = 0.5,这样这个控制组被允许使用的 CPU 最大配额就是 0.5 个 CPU。
-
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
- 只读参数,里面的数值的当前控制组里所有进程实际使用的内存总和.数值越接近参数1,OMM的风险越高.
-
memory.max_usage_in_bytes
Cgroup
下进程使用内存的最大值,包含子Cgroup
的内存使用量.
-
memory.limit_in_bytes
- 设置
Cgroup
下进程最多能使用的内存.如果设置为-1,表示对该Cgroup
的内存使用不做限制. - 直接限制控制里所有进程可使用内存的最大值.
- 设置
-
memory.oom_control
- 当控制组中的进程内存使用达到上限值,这个参数能够决定会不会触发
OOM Killer
,默认会触发
- 当控制组中的进程内存使用达到上限值,这个参数能够决定会不会触发
blkio
子系统
-
在
Cgroups v1
里,blkio Cgroup
的虚拟文件系统挂载点一般在/sys/fs/cgroup/blkio/
-
在
blkio Cgroup
中,有四个最主要的参数,它们可以用来限制磁盘 I/O 性能1
2
3
4blkio.throttle.read_iops_device
blkio.throttle.read_bps_device
blkio.throttle.write_iops_device
blkio.throttle.write_bps_device
资源隔离Namespace
-
Docker是使用Linux的
Namespace
技术实现各种资源隔离的 -
Namespace
是Linux内核的一个特性,该特性可以实现在同一主机系统中,对进程ID、主机名、用户ID、文件名、网络和进程间通信等资源的隔离.Docker利用Linux内核的Namespace
特性,实现了每个容器的资源相互隔离,从而保证容器内部只能访问到自己Namespace
的资源. -
这种隔离有两个作用:
-
第一是可以充分地利用系统的资源,也就是说在同一台宿主机上可以运行多个用户的容器
-
第二是保证了安全性,因为不同用户之间不能访问对方的资源
-
1 | man 7 namespaces |
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(time) |
隔离系统时间 | 5.6 |
Network Namespace
Network Namespace
最主要的几部分资源- 第一种,网络设备,这里指的是 lo,eth0 等网络设备。
- 第二种是 IPv4 和 IPv6 协议栈。
- 第三种,IP 路由表
ip route
- 第四种是防火墙规则,iptables 规则
- 最后一种是网络的状态信息,从
/proc/net
和/sys/class/net
里得到
Namespace
的常用操作
查看当前系统的Namespace
1 | lsns -t <type> |
1 | [root@iZuf6ib0sh7w9cc92x0h4qZ ~]# lsns -t ipc |
查看某进程的Namespace
1 | ls -la /proc/<pid>/ns |
1 | [root@iZuf6ib0sh7w9cc92x0h4qZ proc]# ls -la /proc/481990/ns/ |
进入某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
5type NetworkController interface {
// 创建一个新的网络. options 参数用于指定特性类型的网络选项.
NewNetwork(networkType, name string, id string, options ...NetworkOption) (Network, error)
// ... 此次省略部分接口
} -
第二步:通过调用 NewNetwork 函数创建指定名称和类型的 Network,其中 Network 也是接口类型,代码如下:
1
2
3
4
5
6
7type 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
之间可以互相访问.
- veth 是 Linux 中的虚拟设备接口,veth 都是成对出现的,它在容器中,通常充当一个桥梁.veth 可以用来连接虚拟网络设备,例如 veth 可以用来连通两个 Net Namespace,从而使得两个 Net
-
Linux bridge
- Linux bridge 是一个虚拟设备,是用来连接网络的设备,相当于物理网络环境中的交换机.Linux bridge 可以用来转发两个 Net
Namespace
内的流量.
- Linux bridge 是一个虚拟设备,是用来连接网络的设备,相当于物理网络环境中的交换机.Linux bridge 可以用来转发两个 Net
- 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