[TOC]

0x00 底层实现

我们以 Docker 基础架构来探究Docke底层的核心技术,简单的包括:

  • Linux 上的命名空间(Namespaces)
  • 控制组(Control groups)
  • Union 文件系统(Union file systems)
  • 容器格式(Container format)
  • 容器网络 (Container network)


基本架构
  • C/S 架构,包括客户端和服务端,既可以运行在一个机器上,也可通过 socket 或者 RESTful API 来进行通信。
  • Docker 守护进程 (Daemon)一般在宿主主机后台运行,作为服务端接受来自客户端的请求,并处理这些请求(创建、运行、分发容器)
  • Docker 客户端则为用户提供一系列可执行命令如docker run / ,用户用这些命令实现跟 Docker 守护进程交互。

WeiyiGeek.Docker基础架构

传统虚拟机特点:
传统的虚拟机通过在宿主主机中运行 hypervisor 来模拟一整套完整的硬件环境提供给虚拟机的操作系统,虚拟机系统看到的环境是可限制的,也是彼此隔离的,实现了对资源最完整的封装但是意味着系统资源的浪费;

操作系统与资源共享
操作系统中包括内核、文件系统、网络、PID、UID、IPC、内存、硬盘、CPU 等等,所有的资源都是应用进程直接共享的

例如,以宿主机和虚拟机系统都为 Linux 系统为例,虚拟机中运行的应用其实可以利用宿主机系统中的运行环境(docker也是基于此)。

实现虚拟化的要求:

  • 实现对内存memory(不可压缩)、CPU(可压缩)、网络IO、硬盘IO、存储空间等的限制外
  • 实现文件系统、网络、PID、UID、IPC等等的相互隔离

Memory

  • OOME 介绍:在linux主机中如果kernel监测到当前宿主机没有充足的内存用于实现系统某些重要的功能,就会抛出OOME异常(Out
    Of Memory Exception)同时kill掉一些进程;
    • 一旦发生OOME任何进程都有可能被杀死,包括docker daemon在内,为此Docker特定调整dockerdaemon的优先级以免被误杀,但是容器的优先级未被调整;
    • 根据各种复杂的算法来算出进程的oom-score数越多就会被kill掉,有些您不想kill掉的容器需要采用oom_odj键来初始化指定即(–oom-kill-disable / –oom-score-adj int)
    • 设置限制容器中内存大小的参数:–memory 与 –memory-swap N ,其中内存又被分为RAM、SWAP;必须先设置前则才能使用–memory-swap
      WeiyiGeek.memory-swap

      WeiyiGeek.memory-swap


CPU

  • CFS scheduler : 完全公平调度系统在docker1.3以前使用,在1.3及以后使用的是realtime实时性的;
    • 普通进程优先级调度是非实时的/内核级进程一般都是实时的;
    • 优先级:CPU密集型、IO密集型
    • 限制CPU的参数:–cpus 核数 / –cpu-shares (需要则按照比例进行分配)

使用docker的stress镜像压测实现资源限制:
我的机器是1核的:所以CPU百分比超过100%,Range of CPUs is from 0.01 to 1.00, as there are only 1 CPUs available.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$docker pull lorel/docker-stress-ng
lorel/docker-stress-ng latest 1ae56ccafe55 3 years ago 8.1MB

$docker run --name stress -it --rm lorel/docker-stress-ng:latest stress -help #查看镜像压力测试帮助
Example: stress-ng --cpu 8 --io 4 --vm 2 --vm-bytes 128M --fork 4 --timeout 10s

#演示1.限制虚拟内最大不能藏256MB
[[email protected] ~]# docker run --name stress -it -m 256m --rm lorel/docker-stress-ng:latest stress --vm 2

$docker stats
CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS
36a41704053f stress 98.57% 140MiB / 256MiB (关键点) 54.69% 0B / 0B 0B / 0B 5

#测试cpu的性能:
$docker run --name stress -it -m 256m --rm lorel/docker-stress-ng:latest stress --cpu 8 --io 4 #使用全部CPU占100%
$docker run --name stress --cpus 0.5 -it -m 256m --rm lorel/docker-stress-ng:latest stress --cpu 8 --io 4 #使用半个CPU 不超过50% 上下误差+/-5
$docker run --name stress --cpuset-cpus 0 -it --rm lorel/docker-stress-ng:latest stress --cpu 8 #使用第
CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS
6d6cc81d6e26 stress 50.99% 15.93MiB / 256MiB 6.22% 0B / 0B 0B / 143kB 13


简述I/O设备

描述:Linux中I/O设备分为以下两类,两种设备本身没有严格限制,但是基于不同的功能进行分类;

  • 字符设备: 提供连续的数据流支持按字节、字符来读写数据,应用程序需要按顺序进行读取,所以通常不支持随机存取; 比如:调制解调器就是典型的字符设备;
  • 块设备: 应用程序可以随机访问设备数据,并且程序自己确定读取数据的位置;比如磁盘就是典型的块设备;

Q: 字符设备与块设备的区别?
答:前者顺序读取写入,后者通过寻址磁盘上的任何位置进行读取写入,注意块设备数据的读写只能以块(通常是512B)的倍数进行,与字符设备另外一个区别就是块设备并不支持基于字符的寻址;

Linux 的设备管理和文件系统是紧密相关的(一切皆文件),以文件的格式存放于/dev/目录之下称为设备文件。
应用程序可以打开、关闭、读写这些设备文件,完成对设备的操作就像操作普通数据文件一样,并且为了便于管理设备系统为各个设备进行编号,每个设备号分为主设备号和次设备号;

对于常用设备Linux有约定俗称的编号,如磁盘的主设备号是3;并且一个字符设备或者块设备都有一个主设备号和次设备号(统称为设备号);

Q: 什么是主设备号和次设备号?
答: 主设备号表示一个特定的驱动程序,用来区分不同种类的设备;
次设备号表示使用该驱动程序的各设备,用来区分同一种类(类型)的多个设备;


1.命名空间

描述:容器(Container)利用Linux中内核(>=2.4.19)命名空间来做权限的隔离控制即将某个特定的全局系统资源通过抽象的方法使得namespace中的进程看起来拥有他们自己的隔离的全局系统资源,并且联合利用 cgroups 来做资源分配限制

Docker 容器和 LXC 容器很相似,命名空间提供了最基础也是最直接的隔离,在容器中运行的进程不会被运行在主机上的进程和其它容器发现和作用。随着 Linux 系统对于命名空间功能的完善实现,程序员已经可以实现上面的所有需求,让某些进程在彼此隔离的命名空间中运行。大家虽然都共用一个内核和某些运行时环境(例如一些系统命令和系统库),但是彼此却看不到都以为系统中只有自己的存在

当使用docker run启动一个容器时候,后台Docker会为容器创建一个独立的命名空间和控制组集合;每个容器都有资源独有网络,意味着他们不能访问其他容器的sockets或接口。但能通过–links来连接 2 个容器时或者–net来自定义Docker容器网络(网桥接口相互通信),容器就可以相互通信了(可以根据配置来限制通信的策略);

命名空间(namespace)在Docker容器具体实现说明如下:

  • (1)先来看一个示例1,当前Linux宿主机终端进程对应的namespace信息:

    1
    2
    3
    4
    5
    6
    7
    8
    $ls -l /proc/$$/ns
    总用量 0
    lrwxrwxrwx. 1 root root 0 7月 4 23:27 ipc -> ipc:[4026531839]
    lrwxrwxrwx. 1 root root 0 7月 4 23:27 mnt -> mnt:[4026531840]
    lrwxrwxrwx. 1 root root 0 7月 4 23:27 net -> net:[4026531956]
    lrwxrwxrwx. 1 root root 0 7月 4 23:27 pid -> pid:[4026531836]
    lrwxrwxrwx. 1 root root 0 7月 4 23:27 user -> user:[4026531837]
    lrwxrwxrwx. 1 root root 0 7月 4 23:27 uts -> uts:[4026531838]
  • (2)Docker容器中Namespace

Namespace 隔离的全局系统资源 容器隔离效果
UTS (UNIX Time-sharing System ) 主机名与域名 每个容器拥有独立的 hostname 和 domain name, 使其在网络上可以被视作一个独立的节点而非主机上的一个进程。
User 用户和用户组 每个容器可以有不同的用户uid和组gid, 可以在容器内用容器内部的用户执行程序而非宿主主机上的用户。
IPC (Inter-Process Communication) 信号量、消息队列(POSIX message queues)以及共享内存 每个容器有其自己的System V IPC 和 POSIX 消息队列系统,从而只有在同一个IPC namespace的进程之间才能互相通信;
注意:容器的进程间交互实际上还是 host 上具有相同 pid 命名空间中的进程间交互。因此需要在 IPC 资源(有唯一的32位id)申请时加入命名空间信息。
PID 进程编号 每个名称空间中pid中的进程可以有其独立的PID,即不同用户的进程通过 pid 命名空间隔离开的,且不同命名空间中可以有相同 pid号; 每个容器可以有其PID为1的root进程,也使得容器可以在不同的host之间进行迁移(因为namespace中的进程ID和host不是强绑定,使得容器中的每个进程有两个PID即容器中PID和host上的PID);
Network 网络设备、网络栈、端口等 每个 net 命名空间有独立的网络设备、IP地址、路由表、/proc/net目录、端口号等等,这也使得一个host上多个容器内的同一个应用都绑定到各自的80端口上,Docker 默认采用 veth 的方式,将容器中的虚拟网卡同 host 上的一 个Docker 网桥 docker0 连接在一起。
Mount (mnt) 文件系统挂载点 类似 chroot将一个进程放到一个特定的目录执行,与 chroot 不同是每个命名空间中的容器在 /proc/mounts 的信息只包含所在命名空间的 mount point,允许不同命名空间的进程看到的文件结构不同,使得进程间文件目录隔离开来。

基础示例1-mnt:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 1.创建一个文件夹,使用tmpfs这种基于内存的文件系统来模拟挂载
mkdir /tmp/mnt_isolation
# 2.使用unshare(内置命令做作用是不启动一个新进程就可以起到隔离效果,相当于跳出原先的namespace)隔离mnt namespace
unshare --mount /bin/bash
# 3.挂载我们的tmpfs
mount -t tmpfs tmpfs /tmp/mnt_isolation
# 4.然后进入/tmp/mnt_isolation创建文件
cd /tmp/mnt_isolation && touch linux-mnt-{1..10}
# 5.新建立一个shell终端发现并没有创建文件;
[[email protected] mnt_isolation]$ ls #终端1
linux-mnt-1 linux-mnt-2 linux-mnt-4 linux-mnt-6 linux-mnt-8
linux-mnt-10 linux-mnt-3 linux-mnt-5 linux-mnt-7 linux-mnt-9

[[email protected] mnt_isolation]$ ls /tmp/mnt_isolation/ #终端2
#输出为空

基础示例2-ipc:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# 1.创建一个消息队列
$ ipcmk --queue
消息队列 id:0

# 2.查看创建的消息队列
$ ipcs
# --------- 消息队列 -----------
# 键 msqid 拥有者 权限 已用字节数 消息
# 0x651dbb47 0 root 644 0 0
# ------------ 共享内存段 --------------
# 键 shmid 拥有者 权限 字节 nattch 状态

# --------- 信号量数组 -----------
# 键 semid 拥有者 权限 nsems

# 3.使用unshare命令进行隔离IPC namespace
$echo $$
78285
$unshare --ipc /bin/bash
$echo $$ #此时已经切换到新的进程
78468
$ps aux | grep -E "78468|78285"
root 78285 0.0 0.1 117460 3980 pts/1 Ss 00:01 0:00 -bash
root 78468 0.0 0.1 117408 3956 pts/1 S 00:04 0:00 /bin/bash

# 4.再次查看消息队列发现为空,所以证实了在隔离的Namespace中创建的内容外部无法看到
ipcs
# --------- 消息队列 -----------
# 键 msqid 拥有者 权限 已用字节数 消息
# ------------ 共享内存段 --------------
# 键 shmid 拥有者 权限 字节 nattch 状态
# --------- 信号量数组 -----------
# 键 semid 拥有者 权限 nsems


2.控制组

描述:控制组(Control Group)是 Linux 内核(2.6.24)的一个特性也是容器机制的另外一个关键组件,主要用来对共享资源(内存、CPU、磁盘 IO 等资源)进行隔离、限制、审计等(用于资源控制)。
简单的说Cgroup是Linux内核的一个功能,它将任意进程进行分组化管理具体资源管理就是通过它来实现的,具体的资源管理功能称为Cgroup子系统或控制器,它可以控制内存的Memory控制器、控制进程调度的CPU控制器并且运行中的内核可以使用Cgroup子系统利用/proc/cgroup来管理;

实现原理:将一组进程放在一个控制组里,通过给这个控制组分配指定的可用资源,达到控制这一组进程可用资源的目的。所以只有能控制分配到容器的资源,才能避免当多个容器同时运行时的对系统资源的竞争
实现角度:Cgroup实现了一个通用的进程分组的架构,而不同资源的具体管理则是由各个Cgroup子系统实现的。
实现方式: cgroupfs Cgroup Driver: cgroupfs 或者 systemd Cgroup Driver: systemd;

发展历史:

  • 在Cgroup出现之前,只能针对一个进程做一些资源的控制,例如通过shed_setaffinity系统调用限定一个进程的CPU亲和性,或者用ulimit限制一个进程的文件上限、限大小等;
  • 在Cgroup出现之后,可以对进程进行人员的分组这是用户自定义的,例如安卓的应用分为前台与后台应用;

注意:不同内核版本Cgroup中实现的子系统有些许不同,当用docker run启动一个容器的时候创建一个独立的名称空间和控制组集合;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 3.10.0-1062.18.1.el7.x86_64
grep cgroup /proc/mounts | awk -F " " '{print $2 " = " $4}'
/sys/fs/cgroup = ro,seclabel,nosuid,nodev,noexec,mode=755
/sys/fs/cgroup/systemd = rw,seclabel,nosuid,nodev,noexec,relatime,xattr,release_agent=/usr/lib/systemd/systemd-cgroups-agent,name=systemd
/sys/fs/cgroup/freezer = rw,seclabel,nosuid,nodev,noexec,relatime,freezer
/sys/fs/cgroup/net_cls,net_prio = rw,seclabel,nosuid,nodev,noexec,relatime,net_prio,net_cls
/sys/fs/cgroup/cpu,cpuacct = rw,seclabel,nosuid,nodev,noexec,relatime,cpuacct,cpu
/sys/fs/cgroup/cpuset = rw,seclabel,nosuid,nodev,noexec,relatime,cpuset
/sys/fs/cgroup/perf_event = rw,seclabel,nosuid,nodev,noexec,relatime,perf_event
/sys/fs/cgroup/hugetlb = rw,seclabel,nosuid,nodev,noexec,relatime,hugetlb
/sys/fs/cgroup/pids = rw,seclabel,nosuid,nodev,noexec,relatime,pids
/sys/fs/cgroup/memory = rw,seclabel,nosuid,nodev,noexec,relatime,memory
/sys/fs/cgroup/blkio = rw,seclabel,nosuid,nodev,noexec,relatime,blkio
/sys/fs/cgroup/devices = rw,seclabel,nosuid,nodev,noexec,relatime,devices

# 5.7.0-1.el7.elrepo.x86_64
grep cgroup /proc/mounts | awk -F " " '{print $2 " = " $4}'
/sys/fs/cgroup = ro,nosuid,nodev,noexec,mode=755
/sys/fs/cgroup/systemd = rw,nosuid,nodev,noexec,relatime,xattr,release_agent=/usr/lib/systemd/systemd-cgroups-agent,name=systemd
/sys/fs/cgroup/cpu,cpuacct = rw,nosuid,nodev,noexec,relatime,cpu,cpuacct
/sys/fs/cgroup/memory = rw,nosuid,nodev,noexec,relatime,memory
/sys/fs/cgroup/devices = rw,nosuid,nodev,noexec,relatime,devices #主机设备读写节点创建控制
/sys/fs/cgroup/blkio = rw,nosuid,nodev,noexec,relatime,blkio
/sys/fs/cgroup/net_cls,net_prio = rw,nosuid,nodev,noexec,relatime,net_cls,net_prio
/sys/fs/cgroup/freezer = rw,nosuid,nodev,noexec,relatime,freezer
/sys/fs/cgroup/rdma = rw,nosuid,nodev,noexec,relatime,rdma # 内核新增
/sys/fs/cgroup/hugetlb = rw,nosuid,nodev,noexec,relatime,hugetlb
/sys/fs/cgroup/perf_event = rw,nosuid,nodev,noexec,relatime,perf_event
/sys/fs/cgroup/cpuset = rw,nosuid,nodev,noexec,relatime,cpuset
/sys/fs/cgroup/pids = rw,nosuid,nodev,noexec,relatime,pids

由上面可知cgroups分为多个子系统,每个系统代表一种设施或者说是资源控制器,用来调度某一类的资源使用(cpu,内存,块设备),在实现上cgroups并没有增加新的系统调用,而是表现为一个cgroup文件系统,可以把一个或者多个子系统挂载到某一个目录之中;

1
2
3
4
5
# 挂载cpu子系统到 /sys/fs/cgroup/cpu
mount -t cgroup -o cpu cpu /sys/fs/cgroup/cpu

# 查看
cgroup on /sys/fs/cgroup/cpuset type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,cpuset)

基础实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
#示例1.分配和限制内存和CPU使用
docker run -itd --cpus="1.5" --cpuset-cpus="0-1" --cpuset-mems="0" busybox sleep 6000 #常规分配指定cpu节点
docker run -itd --memory 512m --memory-swap 1g busybox sleep 6000 #分配指定内存 -m.--memory 512m

#示例2.为容器添加一个主机设备
docker run -itd --name=demo --device=/dev/sda3 busybox sleep 6000
docker exec -it demo ls /dev/sda3

#实例3.CPU调度与资源限制
docker run -itd calico/cni -c 512 --cpuset-cpus="0" # -c cpu权重
docker run -itd busybox -c 1024 --cpuset-cpus="1,2" # 让一个cgroup使用cpu的1/3,同时另外一个cgroup使用该cpu的2/3(根据cgroup组来划分)
docker run -itd calico/cni --memory 512m --memory-swap 1g --cpu-period=1000000 --cpu-quota=950000 # docker 1.12 及其之前版本
docker run -itd calico/cni --memory 512m --memory-swap 1g --cpu-rt-runtime 950000 # 完全公平调度 CFS scheduler


子系统之Devices

描述:其作用于控制Cgroup的进程对那些设备具有访问的权限;
接口如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# 全局
cat /sys/fs/cgroup/devices/devices.list
# a *:* rwm

# docker 应用全局
cat /sys/fs/cgroup/devices/docker/devices.list
# a *:* rwm
ls -l /sys/fs/cgroup/devices/devices.allow
--w-------. 1 root root 0 6月 15 09:46 /sys/fs/cgroup/devices/devices.allow
ls -l /sys/fs/cgroup/devices/devices.deny
--w-------. 1 root root 0 6月 15 09:46 /sys/fs/cgroup/devices/devices.deny

# docker 容器设置
$docker ps
25d2d645bfc9 test1 "top -b -d 2" 2 weeks ago
cat /sys/fs/cgroup/devices/docker/25d2d645bfc9e6530039d6aac890f69dd9af33f8f966adc2d7287b74964678e3/devices.list
c 1:5 rwm
c 1:3 rwm
c 1:9 rwm
c 1:8 rwm #brw-rw----. 1 root disk 8, 0 6月 15 09:46 /dev/sda
c 5:0 rwm
c 5:1 rwm
c *:* m
b *:* m
c 1:7 rwm #lrwxrwxrwx. 1 root root 7 6月 15 09:46 /dev/mapper/centos-app -> ../dm-2
c 136:* rwm
c 5:2 rwm
c 10:200 rwm

# 手动创建Devices
mkdir /sys/fs/cgroup/devices/testgroup #创建一个控制组
ls /sys/fs/cgroup/devices/testgroup #默认情况下创建的控制组可以访问全部设备
# cgroup.clone_children cgroup.procs devices.deny notify_on_release
# cgroup.event_control devices.allow devices.list tasks
cat devices.list
# a *:* rwm
echo 'c 1:3 rwm' > /sys/fs/cgroup/devices/testgroup/devices.deny #对testgroup组禁止字符设备/dev/null 读写以及创建节点
echo $$ > /sys/fs/cgroup/devices/testgroup/tasks #将当前进程号加入任务中
echo "hello world" > /dev/null #由于组设置了对字符设备/dev/null进行写限制,所以下面写请求失败
# -bash: /dev/null: 不允许的操作

# $$ 表示当前bash进程的PID等价于$BASHPID
cat /sys/fs/cgroup/devices/testgroup/tasks
# 123453
echo $BASHPID
# 123453

接口说明:

  • (1) devices.list: 只读文件下载当前允许被访问的设备列表(每个条母有三个域)
    • 类型: a(所有设备),b(块设备),c(字符设备)
    • 设备号: 格式为major:minor设备号
    • 权限: r w m(创建设备节点mknod)
      1
      2
      3
      #示例演示
      a *:* rwm #表示所有设备都可以访问
      c 1:3 r #表示字符设备只有一个读权限
  • (2) devices.allow:只写文件不能读取,允许指定的设备访问权限;
  • (3) devices.deny: 与上周作用相反,禁止指定设备的访问权限;


子系统之cpuset

描述:其作用于分配指定的CPU和内存节点以此来限定进程可以使用的cpu核心和内存节点,现广泛用于KVM场景之中;

基础实例:

1
2
3
4
5
6
7
8
9
10
11
#(1) 创建一个控制组
mkdir /sys/fs/cgroup/cpuset/testgroup
#(2) 限制控制组只能使用内存节点0和cpu0与cpu1(与您cpu核心数有关 `grep "processor" /proc/cpuinfo`)
echo 0 > /sys/fs/cgroup/cpuset/testgroup/cpuset.mems
echo 0,1 > /sys/fs/cgroup/cpuset/testgroup/cpuset.cpus
#(3) 同样将当前进程加入控制组中
echo $$ > /sys/fs/cgroup/cpuset/testgroup/tasks
#(4) 验证配置结果
cat /proc/$$/status | grep "_allowed_list"
Cpus_allowed_list: 0-1
Mems_allowed_list: 0


子系统之cpu

描述:其作用是限制每个进程能够占用CPU多长时间进行设置;比如在机器上运行多个可能会消耗大量的系统资源的进程时候,我们不希望出现某个程序占据所有的系统资源而导致其它程序进程无法执行,从而导致程序假死的状态,此时使用cgroup对CPU的获取量便可以有效的进行控制;
我们在学操作系统原理的时候我们知道操作系统中有多种调度策略,各种调度策略适用于不同的应用场景;

常用的调度程序:

  • (1) 完全公平调度 Completely Fair Scheduler (CFS): 按照比例分配调度程序,可以根据任务优先级/权重或者cgroup获得份额,在任务群组(cgroup)间按比例分配CPU时间(cpu带宽)
  • (2) 实时调度 Real-Time Scheduler (RT):任务调度程序,可以对实时任务使用CPU的时间进行限定

cpu子系统接口一览:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- cpu.shares: 资源组的CPU使用权重它不限制进程使用绝对的CPU而是控制各组之间的配额,默认情况下Docker容器中cpu shares权重都为1024;
echo 512 > /sys/fs/cgroup/cpu/testgroup/cpu.shares
echo 1024 > /sys/fs/cgroup/cpu/testgroup1/cpu.shares

- cpu.cfs_period_us: 完全公平调度策略,用于统计CPU使用的时间周期(时间片份数)它可以设定重新分配cgroup的可用CPU资源的时间间隔,单位为微秒us(值范围:1000us~1s);
- cpu.cfs_quota_us: 完全公平调度策略,用于某周期内占用CPU的时间(指单核的时间,多核着需要设置时累加),此参数设定基于时间片份数某个cgroup中所有任务可运行的时间总量,单位为微秒us(默认值为-1);
#比如:cpu.cfs_period_us设置为1000000而cpu.cfs_quota_us设置为200000表示cgroup控制的任务中在每1秒钟的0.2秒可单独对CPU进行存取;
#比如:cpu.cfs_quota_us设置成为-1则表示cgroup不需要遵循任何CPU时间限制,这也是每个cgroup的默认值;
简单来说:在CFS调度下period设置为1s并且quota设置为0.5s,那么在cgroup进程中最多可以运行0.5s然后被强制休眠只能等待下一秒才能继续运行;
#示例:假设CPU>=4核心表示这个组在一个使用周期(1s)内可以跑满4核资源;
echo 1000000 > /sys/fs/cgroup/cpu/testgroup/cpu.cfs_period_us
echo 1000000 > /sys/fs/cgroup/cpu/testgroup/cpu.cfs_quota_us

- cpu.rt_period_us: 实时调度策略,设置某个时间段中每隔多久cgroup对CPU资源的存储就要重新分配,单位为微秒(注意:只可以用于实时调度的程序)
- cpu.rt_runtime_us: 实时调度策略,设置某个时间段内cgroup中任务对CPU资源的最长连续访问时间,单位为微秒(注意:只可以用于实时调度的程序)
#比如:我们宿主机中的内核参数设置如下,表示实时进程在运行时候并不是完全占用CPU的,这样设置的好处是即不会对实时任务响应时间导致大的影响,同时也解决了实时任务卡住时导致整个系统无响应;
sysctl -a | grep "sched_rt"
kernel.sched_rt_period_us = 1000000
kernel.sched_rt_runtime_us = 950000

FAQ补充:
Q:#RT调度与CFS调度区别?
答:两则强制上限类似,但RT只是限制实时任务(对响应时间高要求的进程)对cpu的存取,这类进程需要在限定的时间内处理并完成用户的请求,因此在限定的时间内所占用的CPU资源不能被其它进程打断或者占用; 如果此时实时进程中出现类似死循环之类的情况下就会导致整个系统无响应,因为实时进程中CPU权限最高并且在未完成任务前不会释放CPU资源;


子系统之cpuacct

描述:CPU统计(CPU accounting)子系统会自动生成报告来显示cgroup任务所使用的cpu资源,其中包括子群组任务:

1
2
3
4
5
6
7
8
9
10
ls /sys/fs/cgroup/cpu,cpuacct | grep "cpuacct"
cpuacct.stat # 报告此cgroup中所有任务(包括层级中的低端任务-下同)中用户(用户模式)和系统(kelnel模式)使用的CPU时间
# user 350140
# system 344690

cpuacct.usage # 报告此cgroup中所有任务使用CPU的总时间
# 13713389123644

cpuacct.usage_percpu #报告cgroup中所有任务每个CPU使用时间(单位纳秒)
# 6911595096356 6801848202718


子系统之memory

描述:其作用是用来限制Cgroup组所能使用的内存,主要接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
ls /sys/fs/cgroup/memory | grep "memory."

memory.kmem.limit_in_bytes #设置内存使用上线,单位可以使用k/K,m/M,g/GD等;
memory.memsw.limit_in_bytes #设置内存加上交换分区的使用总量,防止进程把交换分区耗光;
memory.stat #汇报内存使用信息包括当前资源总量,使用量,换页次数,活动页数量等等;
memory.oom_control #用来决定一个进程在申请内存超限时候,是否会被系统kill掉,采用0或1来开启或者关闭cgroup的OOM killer默认开启(oom_kill_disable 0);
# - 0 如果开启任务进程尝试申请内存超过允许,就会被系统OOM Killer终止;
# - 1 如果关闭任务进程尝试申请内存超过允许,那么它就会被暂停,直到额外的内存被释放;
memory.swappiness #设置内存交换条件,对于使用swap空间有很大的重要性;
memory.failcnt
memory.force_empty
memory.kmem.failcnt
memory.kmem.max_usage_in_bytes
memory.kmem.slabinfo
memory.kmem.tcp.failcnt
memory.kmem.tcp.limit_in_bytes
memory.kmem.tcp.max_usage_in_bytes
memory.kmem.tcp.usage_in_bytes
memory.kmem.usage_in_bytes
memory.limit_in_bytes
memory.max_usage_in_bytes
memory.memsw.failcnt
memory.memsw.max_usage_in_bytes
memory.memsw.usage_in_bytes
memory.move_charge_at_immigrate
memory.numa_stat
memory.pressure_level
memory.soft_limit_in_bytes
memory.usage_in_bytes
memory.use_hierarchy

基础示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#1.创建控制组
mkdir /sys/fs/cgroup/memory/testcagroup

#2.设置控制组使用的内存上限位100M,且内存与swap的总量为200M
echo 100M > /sys/fs/cgroup/memory/testcagroup/memory.limit_in_bytes
echo 200M > /sys/fs/cgroup/memory/testcagroup/memory.memsw.limit_in_bytes

#3.设置控制组中如果有进程尝试申请内存超过允许,就会被系统OOM killer掉
cat /sys/fs/cgroup/memory/testcagroup/memory.oom_control
oom_kill_disable 1
under_oom 0

#4.查看memory.stat因为控制组内没有添加进程,所以stat的信息为空将当前进程加入cgroup组内
$echo $$ > /sys/fs/cgroup/memory/testcagroup/tasks
$cat memory.stat
cache 0
rss 225280
rss_huge 0
mapped_file 0
swap 0
pgpgin 108
pgpgout 53
pgfault 584
pgmajfault 0
inactive_anon 0
active_anon 200704
inactive_file 0
active_file 0
unevictable 0
hierarchical_memory_limit 104857600
hierarchical_memsw_limit 209715200
total_cache 0
total_rss 225280
total_rss_huge 0
total_mapped_file 0
total_swap 0
total_pgpgin 108
total_pgpgout 53
total_pgfault 584
total_pgmajfault 0
total_inactive_anon 0
total_active_anon 200704
total_inactive_file 0
total_active_file 0
total_unevictable 0


子系统之blkio(块 I/O)

描述:其子系统可以控制并监控cgroup中任务对块设备I/O存取,对一些伪文件写入值可以限制存取次数或者带宽,从伪文件中读取值可以获得关于I/O操作系统信息。

块 I/O blkio子系统给出两种方式来控制对I/O的存取:

  • 权重分配: 用于完全公平列队I/O调度程序(Completely Fair Queuing I/O Sheduler), 用此方法可以给指定的cgroup设置权重;意味着每个cgroup都有一个预留的I/O操作设定比例(即权重)
  • I/O 节流上限: 当一个指定设备执行I/O操作时,用此方法可为其操作次数设定上限;意味着一个设备的读或者写的操作次数是可以限定的;

基本示例:

1
2
3
4
5
6
7
8
9
10
11
12
# 默认权重为1000
cat /sys/fs/cgroup/blkio/blkio.weight
1000

# 创建2个控制组并设置权重
mkdir/sys/fs/cgroup/blkio/mygroup{1,2}
echo 500 > /sys/fs/cgroup/blkio/mygroup1/blkio.weight
echo 100 > /sys/fs/cgroup/blkio/mygroup2/blkio.weight

# 此时离cgexec命令在清空缓存后采用dd读写(可以以此区别两个控制组的读写比例在5:1左右)
cgexec -g "blkio:mygroup1" dd bs=1M count=4096 if=file1 of=/dev/null
cgexec -g "blkio:mygroup2" dd bs=1M count=4096 if=file1 of=/dev/null


3.Union 文件系统

联合文件系统(UnionFS)是一种分层、轻量级并且高性能的文件系统也是 Docker 镜像的基础,它支持对文件系统的修改作为一次提交来一层层的叠加,同时可以将不同目录挂载到同一个虚拟文件系统下(unite several directories into a single virtual filesystem)。

Docker 目前支持的联合文件系统包括 OverlayFS, AUFS, Btrfs, VFS, ZFS 和 Device Mapper,在可能的情况下,推荐使用 overlay2 存储驱动它是目前 Docker 默认的存储驱动;

使用的好处:
镜像可以通过分层来进行继承(对于修改采用分层存储),基于基础镜像(没有父镜像),可以制作各种具体的应用镜像。另外不同 Docker 容器就可以共享一些基础的文件系统层,同时再加上自己独有的改动层,大大提高了存储的效率。

Docker 中使用的 AUFS(AnotherUnionFS)就是一种联合文件系统。

  • AUFS 支持为每一个成员目录(类似 Git 的分支)设定只读(readonly)、读写(readwrite)和写出(whiteout-able)权限,
  • 同时 AUFS 里有一个类似分层的概念, 对只读权限的分支可以逻辑上进行增量地修改(不影响只读部分的)。


4.容器格式

最初,Docker 采用了 LXC 中的容器格式。
从 0.7 版本以后开始去除 LXC,转而使用自行开发的 libcontainer,从 1.11 开始,则进一步演进为使用 runC 和 containerd。

Docker 与 LXC(Linux Container)有何不同?
*答:LXC 利用 Linux 上相关技术实现了容器,Docker 则在如下的几个方面进行了改进:

  • 移植性:通过抽象容器配置,容器可以实现从一个平台移植到另一个平台;
  • 镜像系统:基于 AUFS 的镜像系统为容器的分发带来了很多的便利,同时共同的镜像层只需要存储一份,实现高效率的存储;
  • 版本管理:类似于Git的版本管理理念,用户可以更方便的创建、管理镜像文件;
  • 仓库系统:仓库系统大大降低了镜像的分发和管理的成本;
  • 周边工具:各种现有工具(配置管理、云平台)对 Docker 的支持,以及基于 Docker的 PaaS、CI 等系统,让 Docker 的应用更加方便和多样化。


Docker 与 Vagrant 有何不同?
答:两者的定位完全不同。

  • Vagrant 类似 Boot2Docker(一款运行 Docker 的最小内核),是一套虚拟机的管理环境。Vagrant 可以在多种系统上和虚拟机软件中运行,可以在 Windows,Mac 等非 Linux 平台上为 Docker 提供支持,自身具有较好的包装性和移植性。
  • 原生的 Docker 自身只能运行在 Linux 平台上,但启动和运行的性能都比虚拟机要快,往往更适合快速开发和部署应用的场景。

简单说:

  • Docker 不是虚拟机,而是进程隔离,对于资源的消耗很少,但是目前需要 Linux 环境支持。
  • Vagrant 是虚拟机上做的封装,虚拟机本身会消耗资源。
  • 一句话:Vagrant 适合用来管理虚拟机,而 Docker 适合用来管理应用环境。


5.网络实现

描述:Docker 的网络实现其实就是利用了 Linux 上的网络命名空间和虚拟网络设备veth(Vritual Enternet Device),veth主要的目的是为了跨NetWork Namespace之间提供一种类似于Linux进程间通信技术,所以veth总数成对出现进行通信其工作在L2数据链路层,比如veth0与veth1他们分别在不同的Network Namespace,其中一端Veth设备任意一端上RX到的数据都会在另外一端上以Tx的方式发送出去;

前面我们说过Linux下的Docker容器网络通过Network Namespace机制(/proc/net, IP地址, 网卡, 路由)实现隔离网络资源,不同的Network Namespace有各自的网络设备,协议栈,路由器以及防火墙,同一个Namepsace下的进程共享同一个网络视图;所以veth-pair设备接口它在本地主机和容器内分别创建一个虚拟接口(即:在不同的网络命名空间中创建通道),并让它们彼此连通实现网络通信,该设备在转发数据包过程中并不篡改数据包内容;

基本原理:
为了实现Docker网络中各个容器通信,需要veth-pair设备在本地主机和容器内分别创建虚拟接口(Docker 中的网络接口默认都是虚拟的接口), 然后Linux 通过在内核中进行数据复制来实现虚拟接口之间的数据转发,发送接口的发送缓存中的数据包被直接复制到接收接口的接收缓存中,并且对于本地系统和容器内系统看来就像是一个正常的以太网卡,只是它不需要真正同外部网络设备通信,所以速度要快很多则转发效率较高;此外如果不同子网之间要进行通信,还需要路由机制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#1.网桥工具安装brctl
yum install -y bridge-utils

#2.查看网桥连接信息
$brctl show
# bridge name bridge id STP enabled interfaces
# br-fa08aa7db7a6 8000.0242fb5007f1 no veth842b243
# docker0 8000.024245b206ce no veth985cbf0

#3.机器网卡信息
$ip addr | grep "BROADCAST"
# 2: ens192: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
# 3: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
# 5: br-fa08aa7db7a6: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
# 57: [email protected]: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP group default
# 71: [email protected]: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master br-fa08aa7db7a6 state UP group default

当通过docker run创建一个容器的时候便有了单独的网络命名空间,容器网络初始化流程:

  • 1.创建一对虚拟接口,分别放到本地主机和新容器中;
  • 2.本地主机一端桥接到默认的 docker0 或指定网桥上,并具有一个唯一的名字,如 [email protected]
  • 3.容器一端放到新容器中,并修改名字作为 eth0,这个接口只在容器的命名空间可见;
  • 4.从网桥可用地址段中获取一个空闲地址分配给容器的 [email protected] 网卡名称@虚拟接口序号,并配置默认路由到桥接网卡 [email protected](57)
    完成这些之后,容器就可以使用 eth0 虚拟网卡来连接其他容器和其他网络。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    # 简单描述
    ens192|eth0 <-> docker0 <-> [email protected](虚拟网卡) <-> [email protected](容器内部)网卡

    # 容器内部网卡信息
    docker exec -it test1 sh -c ip addr
    # 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1000
    # link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    # inet 127.0.0.1/8 scope host lo
    # valid_lft forever preferred_lft forever
    # 56: [email protected]: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue state UP
    # link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff
    # inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
    # valid_lft forever preferred_lft forever
    WeiyiGeek.虚拟接口原理图

    WeiyiGeek.虚拟接口原理图

Docker 中网络提供了五种模式:

  • Bridge 模式
  • Host 模式
  • Container 模式(即:指定容器之间通信的网络)
  • None 模式
  • Overtlay
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# (1) Docker network 查看
docker network ls
# NETWORK ID NAME DRIVER(驱动模式) SCOPE
# 3448511628ee bridge bridge local
# 5baa8b9f47c3 host host local
# afd58da3d80f none null local
# fa08aa7db7a6 opt_default bridge local

# (2) 网络名称信息详细
docker network inspect $(docker network ls -q)
# [
# {
# "Name": "bridge",
# "Id": "3448511628ee3de59985bfa8251a8731148265417530fba645d1f5cca6464ccf",
# "Created": "2020-07-02T11:21:02.785207578+08:00",
# "Scope": "local",
# "Driver": "bridge",
# "EnableIPv6": false,
# "IPAM": {
# "Driver": "default",
# "Options": null,
# "Config": [
# {
# "Subnet": "172.17.0.0/16",
# "Gateway": "172.17.0.1"
# }
# ]
# },
# "Internal": false,
# "Attachable": false,
# "Ingress": false,
# "ConfigFrom": {
# "Network": ""
# },
# "ConfigOnly": false,
# "Containers": {
# "25d2d645bfc9e6530039d6aac890f69dd9af33f8f966adc2d7287b74964678e3": {
# "Name": "test1",
# "EndpointID": "6c825cb11f084e85afddbc993937e6061b2e36ea7dfaa30792b2ea6d0eb414a1",
# "MacAddress": "02:42:ac:11:00:02",
# "IPv4Address": "172.17.0.2/16",
# "IPv6Address": ""
# }
# },
# "Options": {
# "com.docker.network.bridge.default_bridge": "true",
# "com.docker.network.bridge.enable_icc": "true",
# "com.docker.network.bridge.enable_ip_masquerade": "true",
# "com.docker.network.bridge.host_binding_ipv4": "0.0.0.0",
# "com.docker.network.bridge.name": "docker0",
# "com.docker.network.driver.mtu": "1500"
# },
# "Labels": {}
# }
# ]

Tips:

  • (1)通过 –net 参数来指定容器的网络配置,有4个可选值:
    1
    2
    3
    4
    5
    6
    7
    --net=bridge 这个是默认值,连接到默认的网桥。
    --net=host 告诉Docker不要将容器网络放到隔离的命名空间中,即不要容器化容器内的网络
    - 此时容器使用本地主机的网络,它拥有完全的本地主机接口访问权(不安全),
    - 如果进一步的使用 --privileged=true,容器会被允许直接配置主机的网络堆栈。
    --net=container:NAME_or_ID 让Docker将新建容器的进程放到一个已存在容器的网络栈中,新容器进程有自己的文件系统、进程列表和资源限制,但会和已存在的容器共享 IP 地址和端口等网络资源,两者进程可以直接通过 lo 环回接口通信。
    --net=none 让 Docker 将新容器放到隔离的网络栈中,但是不进行网络配置。之后用户可以自己进行配置。
    - 用户可以使用 ip netns exec 命令来在指定网络命名空间中进行配置,从而配置容器内的网络。


Bridge 模式

描述: 该模式是Docker默认的一种网络通讯模式;

Bridge 网络模式原理:

答:Docker Daemon 首次启动时候,会在其所在的宿主机上创建一个名为Docker0的虚拟网桥,然后利用veth pair技术创建一对虚拟网络接口分别接入到Docker0网桥中和相关容器的Network Namespace之中,即Docker0 <-> veth pair <-> Container[Namespace];
容器在建立之初Docker0网桥会为其设置一个IP地址及其网关(即Docker0接口地址), 在通过iptables控制容器与网络通信以及容器间通信的, 实际上Docker0通过iptables中配置与宿主机上的物理网卡链接,并且符合条件的请求将会通过iptables转发到网桥docker0中,之后再分发给对应的机器;

网络图示:

1
2
3
4
5
6
7
8
9
10
11
----------           ----------   
|Container| |Container|
|[email protected]| |[email protected]| #veth [email protected] 对应宿主机上虚拟网卡号 $ip addr 可查看
---------- ----------
| |
------------------------------------
[email protected] [email protected] #veth pair:@ 后对应着容器内部网络号 $ip addr 可查看
| docker0 (bridge) |
------------------------------------
| ipv4_ip_forward (iptales)
(eth0|ens192)

基础实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# (1) 容器 test1 的网络信息
$docker exec -it test1 ip addr
56: [email protected]: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue state UP
link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
valid_lft forever preferred_lft forever

# (2) 查看iptables中关于test1容器的NAT转发规则
$iptables -t nat -L # 默认四条链+创建的Docker链
Chain PREROUTING (policy ACCEPT)
target prot opt source destination
DOCKER all -- anywhere anywhere ADDRTYPE match dst-type LOCAL #网桥

Chain INPUT (policy ACCEPT)
target prot opt source destination

Chain OUTPUT (policy ACCEPT)
target prot opt source destination
DOCKER all -- anywhere !loopback/8 ADDRTYPE match dst-type LOCAL #网桥

Chain POSTROUTING (policy ACCEPT)
target prot opt source destination
MASQUERADE all -- 172.18.0.0/16 anywhere
MASQUERADE tcp -- 172.18.0.2 172.18.0.2 tcp dpt(destination port):http #运行通过的目标IP协议与应用

Chain DOCKER (2 references)
target prot opt source destination
RETURN all -- anywhere anywhere
RETURN all -- anywhere anywhere
DNAT tcp -- anywhere anywhere tcp dpt:tproxy to:172.18.0.2:80 #将指定容器的IP和端口进行转发
# 上述规则简要说明:
# 从任意源发送当前机器的80端口的TCP转发到容器地址为 172.18.0.2:80 的内部应用上;

# (3) 具体流程
# 访问宿主机的80端口,然后经过iptables的NAT PREROUTING将IP重定向到172.18.0.2, 之后重定向的数据包通过iptables中的FILTER配置,最终在NAT POSTROUTING阶段将IP地址伪装成为127.0.0.1,此时访问宿主机上映射到容器中端口,所有的请求便会转发到容器中;
127.0.0.1:80 -> NAT PREROUTING -> 172.18.0.2:80 -> FILTER FORWARD -> ACCEPT -> NAT POSTROUTING

Bridge网络中容器与宿主机通信示意图:

WeiyiGeek.容器宿主机

WeiyiGeek.容器宿主机

总结说明:

  • 1) Veth-Pair技术在宿主机Docker0网桥上与容器的所属的网络命名空间(Network Namespace)上分别创建一个虚拟网络接口[email protected] <---> [email protected],它保证了无论哪一个接口接收到网络报文,都会无条件的转发到另外一方之上;
  • 2) 默认情况下容器可以访问外部网络(一般都会添加本地系统转发支持)采用是NATP(网络地址端口转换)的方式其包含两种转换方式SNAT(源地址) 和 DNAT(目的地址),容器连接外网是通过源NAT地址转换实现的,但是外部网络却无法访问到容器它也需要通过目的NAT转换(数据包的目的地址)才能与容器进行通信;
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    # (1) 内核转发参数查看
    $sysctl -a | grep "ip_forward"
    net.ipv4.ip_forward = 1
    # PS:手动开启转发(临时生效)
    sysctl -w net.ipv4.ip_forward = 1
    # 启动docker deamon指定以下参数也可以实现本地转发
    dockerd -h | grep "ip_forward"
    --ip-forward Enable net.ipv4.ip_forward (default true)

    # (2) 宿主机以外的世界需要访问容器时候,利用DNAT修改数据包的目的地址,数据包的流向如下
    # 宿主机上的dokcer0网桥采用iptable识别到有请求访问容器IP和端口时,将数据包发送附加到docker0网桥上的[email protected]接口中,该接口由于veth-pair特性会将接收到的数据包无条件的发送给容器内部的 [email protected] ,容器接收到数据包并做出响应;
    |------------------------------------------------|
    数据包(外界) -> Eth0 <-<- DNAT ->-> Docker0 —> [email protected] -> [email protected](容器内部)
    | 宿主机 |
    |------------------------------------------------|

    # (3) 当容器内部需要访问宿主机以外的世界时候,利用SNAT修改数据包的源地址(此时容器对与外部网络是透明的),数据包流向为下所示
    # 流程与上面相似方向相反,只是转发采用SNAT方式;
    数据包[[email protected]](容器) -> [email protected] -> Docker0 <-<- SNAT ->-> Eth0 -> 外界网络
    |--------------------宿主机------------------------|


Host 模式

描述:该网络模式与Bridge桥接的网络模式存在一定的差异,最大差异是没有为容器创建一个隔离的网络环境, 并且因为该host网络模式下的Docker容器与宿主机共享一个网络命名空间(namepace),拥有相同的网络设施并且容器的IP即为宿主机IP能直接与外界进行通信;
由于Doker容器的host网络模式在实现过程之中,由于不需要额外的网桥以及虚拟网卡、故而不涉及docker0网桥以及veth-pair虚拟网卡对;

1
[Docker Container](host 模式) -> 宿主机 (eth0) -> 外界网络

Host网络模式优缺点:

  • 1)优点: 效率高(直接采用宿主机IP与外界通信不经过NATP转换)、端口公用(容器可直接使用宿主机端口)
  • 2)缺点: 安全性差(容器不在拥有隔离的和独立的网络栈)、端口限制(宿主机占用或者Bridge网络模式主机占用的端口不能被使用,即不在拥有容器中全部端口)

基础实例:

1
2
3
4
5
6
7
8
# (1) 创建Host网络模式的同期,可以通过ip addr 命令查看网卡信息,因为直接利用宿主机的网络栈,所有打印出的直接是宿主机的网卡接口信息;
docker run -it --net=host busybox ip addr
# 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000
# 2: ens192: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq qlen 1000
# 3: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue
# 5: br-fa08aa7db7a6: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue
# 57: [email protected]: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue master docker0
# 71: [email protected]: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue master br-fa08aa7db7a6


Container 模式

描述: 该模式通常用于自定义网络栈时候使用,它会重用另外一个容器的网络命名空间(例如Bridge);比如Kubernetes也是使用该模式进行内部分布式应用通信;
实际上,采用Container模式的两个容器共享相同的1个Network Namespace;

Container 网络模式示意图:

1
2
3
4
5
6
7
Docker Container  <->  Docker Container
| [email protected] |
-----------------
|
Docker0 (Bridge) <-> [[email protected]]
| (ipv4.ip_forward)
eth0

基础实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 1.创建一个bridge网络模式的容器
docker run -d -P --net=bridge --name nginx nginx:latest
# 2.查看其容器的ip 信息
docker inspect nginx | grep "IPAddress"
docker inspect nginx | grep "IPAddress"
"SecondaryIPAddresses": null,
"IPAddress": "172.17.0.3",
# 3.构建一个采用Container网络模式的容器
# 创建容器连接到已键容器的网络之中,比如采用container:容器名称进行制定网络;
docker run -d --name busybox --net=container:nginx busybox:latest top
# 13d274350a2855c1295e3c93fd4e6a166b6e34cfc71a91e1396ca8b4a75f67e2

# 4.此时您会发现其IP地址与链接到容器中的网络IP地址一样均为172.17.0.3
$docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
13d274350a28 busybox:latest "top" 3 seconds ago Up 2 seconds busybox
04074e2e85b4 nginx:latest "/docker-entrypoint.…" 4 minutes ago Up 3 minutes 0.0.0.0:32768->80/tcp nginx
$docker exec -it busybox ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000
72: [email protected]: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue
link/ether 02:42:ac:11:00:03 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.3/16 brd 172.17.255.255 scope global eth0
valid_lft forever preferred_lft forever


none 模式

描述: 该模式不为Docker容器提供任何的网络环境可以说该模式只是对容器做了极少的设定,一旦设置none网络模式容器内部就只能使用loopback设备,不会再有其它的网络资源环境;

该模式下方便docker开发者基于此做出其他可能的网络定制开发, 实际上该模式关闭了容器的网络功能,应用场景如下;

  • 容器并不需要网络(例如只需要写磁盘卷的批处理任务)
  • 希望自定义网络

基础实例:

1
2
3
4
5
# 1.创建一个网络模式为none的容器
docker run -itd --net=none --name=busybox-none busybox top
# 2.查看创建的容器的ip地址,只能看见一个回环地址
docker exec -it busybox-none ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000


Overlay 模式

描述:Overlay模式是Docker原生的跨主机网络方案(其他一些方案:Flannel/Weave和Calico K8s已默认使用),而Docker 又通过Libnetwork以及CNM将上述各种方案与docker集成在一起;

Q: 什么是Libnetwork库?

答: 他是Docker容器的网络库,其核心内容是其定义的Container Network Model (CNM)容器网络模型 他对容器网络进行了抽象;

1
2
3
                     | -> Native Drivers(None, Bridge, Overlay, Macvlan) #原生
Docker -> Libnetwork | ->
| -> Remote Drivers(Flannel, Weave, Calico) #第三方网络插件

CNM 由以下三类组件组成:

  • Sandbox: Linux Network Namespace 是基于 Sandbox的标准实现,它是容器的网络栈其囊括Interface, 路由器 和 DNS 设置,也就是说Sandbox将一个容器与另外容器通过Namespace进行隔离,一个容器包含一个Sandbox,它可以包含来自不同的Network的Endpoint,即每个Sandbox可以有多个Endpoint隶属于不同的网络;
  • Endpoint: 将Sandbox接入到Network之中,一个Endpoint只能属于一个网络与一个Sandbox,例如 Veth-pair 的实现;
  • Network: 包含一组Endpoint,同一个Network的Endpoint可以直接通信,其实现可以是Linux bridge vlan等
WeiyiGeek.CNM

WeiyiGeek.CNM

总结:

  • (1) Sandbox 与 Endpoint 是一对多的关系,Endpoint将Sandbox绑定到Network之中,并且同一个Network间的Endpoint可以直接通信;

Overlay网络原理
描述:Docker overlay网络需要一个K-V数据库用于保存状态相关信息(包括CNM网络组件Network,Endpoint以及IP),可选方案有Consul/Etc/Zokeeper等,此处以Consul一种K-V数据库为例进行演示,此处我们并不需要写任何代码只需要按照Consul即可;

基础实践:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# 1.在Docker Deamon其中一个节点中拉取并运行Consul镜像
docker run -d -p 8500:8500 -h consul --name consul-net-overlay progrium/consul -server -bootstrap**docker run -d -p 8500:8500 -h consul --name consul-net-overlay progrium/consul -server -bootstrap
# Status: Downloaded newer image for progrium/consul:latest
# 86d9d93384034b19acad56dfdebed754da060125b534b240fd87f79e18a2bfcd

# 2.然后再各个Docker节点的Deamon上进行配置Consul发现: /etc/systemd/system/docker.service 再其后加上如下参数
--cluster-store=consul://10.10.107.245:8500 --cluster-advertise=ens192:2376
$ systemctl cat docker
# /usr/lib/systemd/system/docker.service
# ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock --exec-opt native.cgroupdriver=systemd --cluster-store=consul://10.10.107.245:8500 (consul运行节点) --cluster-advertise=ens192:2376(当前节点网卡或者IP即Docker Deamon服务)

# 3.systemd守护进程重启与重启docker
[[email protected] ~]$ systemctl daemon-reload
[[email protected] ~]$ systemctl restart docker

# 4.创建overlay 网络与创建bridge网络基本一致,只是在-d参数时候有些许不同
docker network create -d overlay overlay-net
docker network create -d overlay overlay-net-sub --subnet 172.25.0.0/24 --gateway 172.25.0.1


# 5.创建运行容器时候指定-network参数,及时在不同的docker主机下创建的容器只要--network为oveylay-net它们之间都能相互访问;
docker run --network overlay-net busybox sleep 6000
docker run --network overlay-net busybox sleep 6000
$docker ps
# CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
# 430ec4450df4 busybox "sleep 6000" 6 minutes ago Up 6 minutes cranky_curie
# 0902833f9a63 busybox "sleep 6000" 12 minutes ago Up 11 minutes intelligent_chaplygin


# 6.Docker 网络查看
$docker network ls
NETWORK ID NAME DRIVER SCOPE
5de5f196afda bridge bridge local
23d95a1bd969 docker_gwbridge bridge local
3a700c6af892 host host local
93a5b381ef0d none null local
e437f4650c96 overlay-net overlay global
$docker network inspect docker_gwbridge -f "{{.IPAM.Config}}"
# [{172.18.0.0/16 172.18.0.1 map[]}]
$docker network inspect overlay-net -f "{{.IPAM.Config}}"
# [{10.0.0.0/24 10.0.0.1 map[]}]


# 7.创建容器的IP地址查看
[[email protected] ~]$docker exec -it 090 ip addr
22: [email protected]: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1450 qdisc noqueue
link/ether 02:42:0a:00:00:02 brd ff:ff:ff:ff:ff:ff
inet 10.0.0.2/24 brd 10.0.0.255 scope global eth0
valid_lft forever preferred_lft forever
25: [email protected]: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue
link/ether 02:42:ac:12:00:02 brd ff:ff:ff:ff:ff:ff
inet 172.18.0.2/16 brd 172.18.255.255 scope global eth1
valid_lft forever preferred_lft forever
[[email protected] ~]$docker exec -it 430 ip addr
27: [email protected]: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1450 qdisc noqueue
link/ether 02:42:0a:00:00:03 brd ff:ff:ff:ff:ff:ff
inet 10.0.0.3/24 brd 10.0.0.255 scope global eth0
valid_lft forever preferred_lft forever
29: [email protected]: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue
link/ether 02:42:ac:12:00:03 brd ff:ff:ff:ff:ff:ff
inet 172.18.0.3/16 brd 172.18.255.255 scope global eth1
valid_lft forever preferred_lft forever

[[email protected] ~]$docker exec -it 090 sh
/ # ip addr
/ # ping 10.0.0.1
PING 10.0.0.1 (10.0.0.1): 56 data bytes
64 bytes from 10.0.0.1: seq=0 ttl=64 time=0.540 ms
/ # ping 10.0.0.2
PING 10.0.0.2 (10.0.0.2): 56 data bytes
64 bytes from 10.0.0.2: seq=0 ttl=64 time=0.055 ms
/ # ping 10.0.0.3 #可以看到是正常通信的
PING 10.0.0.3 (10.0.0.3): 56 data bytes
64 bytes from 10.0.0.3: seq=0 ttl=64 time=0.110 ms

访问Consul查看究竟存在什么东西:http://10.10.107.245:8500/ui/#/dc1/kv/docker/network/v1.0/

WeiyiGeek.Consul-docker-kv-network

WeiyiGeek.Consul-docker-kv-network


总结:
从上面的实际操作中我们可以看见当创建完overlay网络后,查看docker网络会发现网络中多了个 overlay-net (类型为Overlay,Scope为Global)与 docker_gwbridge (类型为Bridge,Scope为local),为什么会多出来一个 docker_gwbridge网络呢?
答:实际上这就是Overlay工作原理之所在;
可以通过yum install -y bridge-utils && brctl show 进行查看得出结果, 当每创建一个网络类型为Overlay的容器,docker_gwbridge 下便会挂载一个veth***,这说明oveylay容器是通过此网桥来进行对外连接的;

1
2
3
4
bridge name           bridge id               STP enabled     interfaces
docker0 8000.024274c0c574 no
docker_gwbridge 8000.02422e73bb62 no vethae1f217
vethec87cb3

简单的说overlay网络数据还是从Bridge网络docker_gwbridge网桥出去的,由于Consul的作用记录leoverlay网络的CNM三大组件的信息,使得其它主机docker知道此网络类型为oveylay,并且可以在该网络下不同主机之间进行相互的通信访问,但是实际上出口还是Docker_gwbridge;


6.存储驱动

描述:Docker最开始采用AUFS作为文件系统,也得益于AUFS分层的概念,实现了多个Container可以共享同一个image;Docker支持的存储驱动类型有overlay2、AUFS、Btrfs、Device mapper、OverlayFS、ZFS五种存储驱动,所有驱动都用到写时复制(CoW)的技术。

[2020年6月19日] - 目前最新版本的 docker 默认优先采用 overlay2 的存储驱动,对于已支持该驱动的 Linux 发行版,不需要任何进行任何额外的配置。devicemapper 存储驱动已经在 docker 18.09 版本中被废弃,docker 官方推荐使用 overlay2 替代devicemapper。

1
2
3
docker info
Server Version: 19.03.3
Storage Driver: overlay2


  • 写时复制(CoW)
    什么是写时复制?
    答:写时复制(CoW)就是copy-on-write,表示只在需要写时才去复制,这个是针对已有文件的修改场景。CoW技术可以让所有的容器共享image的文件系统,所有数据都从image中读取,只有当要对文件进行写操作时,才从image里把要写的文件复制到自己的文件系统进行修改。

所以无论有多少个容器共享同一个image,所做的写操作都是对从image中复制到自己的文件系统中的复本上进行,并不会修改image的源文件,且多个容器操作同一个文件,会在每个容器的文件系统里生成一个复本,每个容器修改的都是自己的复本,相互隔离,相互不影响。使用CoW可以有效的提高磁盘的利用率。

  • 用时分配(allocate-on-demand)
    只有在要新写入一个文件时才分配空间,这样可以提高存储资源的利用率。比如启动一个容器,并不会为这个容器预分配一些磁盘空间,而是当有新文件写入时,才按需分配新空间。
AUFS

什么是AUFS?
答:AUFS(AnotherUnionFS)是一种Union FS,是文件级的存储驱动。
AUFS能透明覆盖一或多个现有文件系统的层状文件系统,把多层合并成文件系统的单层表示。文件系统可以一层一层地叠加修改文件,但是无论底下有多少层都是read only,只有最上层的文件系统是可写的;
当需要修改一个文件时,AUFS创建该文件的一个副本,使用CoW将文件从只读层复制到可写层进行修改,结果也保存在可写层;
在Docker中底下的只读层就是image,可写层就是Container;

WeiyiGeek.AUFS

WeiyiGeek.AUFS


Overlay

描述:Overlay是Linux内核3.18后支持的,它也是一种UnionFS与AUFS不同的是Overlay只有两层:一个upper文件系统和一个lower文件系统,分别代表Docker的镜像层和容器层。
当需要修改一个文件时,使用CoW将文件从只读的lower复制到可写的upper进行修改,结果也保存在upper层。

同样在Docker中底下的只读层就是image,可写层就是Container;

WeiyiGeek.Overlay

WeiyiGeek.Overlay

下图展示了overlayFS的两个特征:上下合并、同名遮盖

WeiyiGeek.overlay-Upper-Lower

WeiyiGeek.overlay-Upper-Lower

采用一个挂载OverlayFS小例子深入理解其存储驱动:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# (1) common.txt分别存放不同的内容
比如lower1下面common.txt内容是lower1,lower2下面common.txt内容是lower2,lower3下面common.txt内容是lower3,upper目录有个和lower2/ower2.sh同名的目录
$tree -L 2
├── lower1
│ ├── common.txt
│ └── ower1.sh
├── lower2
│ ├── common.txt
│ └── ower2.sh
├── lower3
│ ├── common.txt
│ └── ower3.sh
├── merged
├── upper
│ ├── ower2.sh
│ └── up.txt
└── work
└── work

# (2) 我们通过下面的命令进行挂载
* lowerdir: 代表lower层,可以有多个,优先级依次降低,也就是说`lower1 > lower2 > lower3`
* upperdir: 代表upper层,会覆盖lower层
* workdir: 工作目录,用于存放临时文件
* merged: 挂载点,我们看看操作之后的目录

$mount -t overlay overlay -o lowerdir=lower1:lower2:lower3,upperdir=upper,workdir=work merged
# (3) 挂载后的操作目录`merged挂载点`.
├── lower1
│ ├── common.txt
│ └── ower1.sh
├── lower2
│ ├── common.txt
│ └── ower2.sh
├── lower3
│ ├── common.txt
│ └── ower3.sh
├── merged #是不是看出了什么奥秘,很清晰的说明了overlayFS的上面两个特征
│ ├── common.txt
│ ├── ower1.sh
│ ├── ower2.sh
│ ├── ower3.sh
│ └── up.txt
├── upper
│ ├── ower2.sh
│ └── up.txt
└── work
└── work

正如前面介绍的COW(写时复制)在overlayFS中也是只对于只读的lower层的操作,当用到它时候会把它复制到upper层然后再对upper的进行操作,我们演示几个对挂载后的目录的操作,就能很明白这个过程了:

  • 1.删除的文件是upper的,并且这个文件在lower层不存在(up.txt) 直接删除就行了
  • 2.删除的文件来自于lower层而upper层没有对应的文件(ower3.sh), overlayFS通过一种叫whiteout的机制它可以用于屏蔽底层的同名文件,在upper层创建一个主次设备号(mknod c 0 0)都是0的设备,当在merge层去找的时候,overlayFS会自动过滤掉和whiteout文件自身以及和他同名的lower层的文件,从而达到隐藏的目的;
  • 3.删除的是upper覆盖lower的文件(ower2.sh) 依然创建一个whiteout文件;
  • 4.创建一个upper和lower都没有的目录 直接在upper中新增一个;
  • 5.创建一个在lower层已经存在且在upper层有whiteout文件的同名文件并删了whiteout文件,重新创建一个;
  • 6.创建一个lower层存在并且upper层已经有对应whiteout文件的目录,如果这个时候单纯的删除whiteout文件,那么lower层对应目录里面的文件就会显示出来。

overlayFS引入了一种Opaque(不透明的)的属性,通过设置upper层上对应的目录上设置"trusted.overlay.opaque"为y来实现(前提是upper所在的文件系统支持xattr属性),overlayFS在读取上下层存在同名目录的时候,如果upper层的目录被设置了Opaque的属性,他会忽这个目录下层的所有同名目录项来保证新建的是个空目录。

WeiyiGeek.Opaque属性

WeiyiGeek.Opaque属性


映射器(Device mapper)

描述:Device mapper是Linux内核2.6.9后支持的,提供的一种从逻辑设备到物理设备的映射框架机制,在该机制下,用户可以很方便的根据自己的需要制定实现存储资源的管理策略;
注意点:AUFS和OverlayFS都是文件级存储,而Device mapper是块级存储,所有的操作都是直接对块进行操作,而不是文件。

Device mapper驱动会先在块设备上创建一个资源池,然后在资源池上创建一个带有文件系统的基本设备,所有镜像都是这个基本设备的快照,而容器则是镜像的快照。所以在容器里看到文件系统是资源池上基本设备的文件系统的快照,并不有为容器分配空间。当要写入一个新文件时,在容器的镜像内为其分配新的块并写入数据(写时分配)

Device mapper存储驱动默认会创建一个100G的文件包含镜像和容器(其实就是资源存储池)。每一个容器被限制在10G大小的卷内,可以自己配置调整。它会默认会在/var/lib/docker/devicemapper/devicemapper目录下生成data和metadata两个稀疏文件,并将两个文件挂为loop设备作为块设备来使用(默认模式),它使用空闲文件来构建存储池,性能非常低。

注意事项:

  • 1.当要修改已有文件时,再使用CoW为容器快照分配块空间,将要修改的数据复制到在容器快照中新的块里再进行修改,注意挂载后的磁盘UUID变化后则需要手动修改deviceset-metadata来指定BaseDeviceUUID;

    WeiyiGeek.

    WeiyiGeek.

  • 2.在18.09版本之前在Docker初始化时候可以指定--storage-opt dm.loopdatasize=500G --storage-opt dm.loopmetadatasize=4G,将回收环境设备大小设置为500G(数据存储),元数据文件大小为4G(稀疏文件),然后分别附加到回环设备/dev/loop0和/dev/loop1,其次再基于回环块设备创建Thin Pool;

Btrfs

描述:Btrfs被称为下一代写时复制文件系统,并入Linux内核也是文件级级存储,但可以像Device mapper一直接操作底层设备。

Btrfs把文件系统的一部分配置为一个完整的子文件系统,称之为subvolume 。那么采用 subvolume,一个大的文件系统可以被划分为多个子文件系统,这些子文件系统共享底层的设备空间,在需要磁盘空间时便从底层设备中分配,类似应用程序调用 malloc()分配内存一样。为了灵活利用设备空间,Btrfs 将磁盘空间划分为多个chunk 。每个chunk可以使用不同的磁盘空间分配策略。比如某些chunk只存放metadata,某些chunk只存放数据

模型优点:Btrfs支持动态添加设备,用户在系统中增加新的磁盘之后,可以使用Btrfs的命令将该设备添加到文件系统中。

Btrfs把一个大的文件系统当成一个资源池,配置成多个完整的子文件系统,还可以往资源池里加新的子文件系统,而基础镜像则是子文件系统的快照,每个子镜像和容器都有自己的快照,这些快照则都是subvolume的快照。

  • 当写入一个新文件时,为在容器的快照里为其分配一个新的数据块,文件写在这个空间里,这个叫用时分配
  • 当要修改已有文件时,使用CoW复制分配一个新的原始数据和快照,在这个新分配的空间变更数据,变结束再更新相关的数据结构指向新子文件系统和快照,原来的原始数据和快照没有指针指向,被覆盖。
WeiyiGeek.

WeiyiGeek.

ZFS

描述:ZFS 文件系统是一个革命性的全新的文件系统,它从根本上改变了文件系统的管理方式,ZFS 完全抛弃了”卷管理”,不再创建虚拟的卷,而是把所有设备集中到一个存储池中来进行管理,用”存储池”的概念来管理物理存储空间。以前文件系统都是构建在物理设备之上的。为了管理这些物理设备,并为数据提供冗余,”卷管理”的概念提供了一个单设备的映像。而ZFS创建在虚拟的,被称为”zpools”的存储池之上。每个存储池由若干虚拟设备(virtual devices,vdevs)组成。这些虚拟设备可以是原始磁盘,也可能是一个RAID1镜像设备,或是非标准RAID等级的多磁盘组。于是zpool上的文件系统可以使用这些虚拟设备的总存储容量。

  • 当要写一个新文件时使用按需分配,一个新的数据快从zpool里生成,新的数据写入这个块,而这个新空间存于容器(ZFS的克隆)里。
  • 当要修改一个已存在的文件时使用写时复制WoC,分配一个新空间并把原始数据复制到新空间完成修改。
WeiyiGeek.ZFS

WeiyiGeek.ZFS

首先从zpool里分配一个ZFS文件系统给镜像的基础层,而其他镜像层则是这个ZFS文件系统快照的克隆,快照是只读的,而克隆是可写的,当容器启动时则在镜像的最顶层生成一个可写层。如下图所示:

WeiyiGeek.

WeiyiGeek.


存储驱动的对比及适应场景

  • AUFS VS Overlay
    AUFS和Overlay都是联合文件系统,但AUFS有多层,而Overlay只有两层,所以在做写时复制操作时,如果文件比较大且存在比较低的层,则AUSF可能会慢一些。而且Overlay并入了linux kernel mainline,AUFS没有,所以可能会比AUFS快。但Overlay还太年轻,要谨慎在生产使用。而AUFS做为docker的第一个存储驱动,已经有很长的历史,比较的稳定,且在大量的生产中实践过,有较强的社区支持。目前开源的DC/OS指定使用Overlay。

  • Overlay VS Device mapper
    Overlay是文件级存储,Device mapper是块级存储,当文件特别大而修改的内容很小,Overlay不管修改的内容大小都会复制整个文件,对大文件进行修改显示要比小文件要消耗更多的时间,而块级无论是大文件还是小文件都只复制需要修改的块,并不是整个文件,在这种场景下显然device mapper要快一些。因为块级的是直接访问逻辑盘,适合IO密集的场景。而对于程序内部复杂,大并发但少IO的场景,Overlay的性能相对要强一些。

  • Device mapper VS Btrfs Driver VS ZFS
    Device mapper和Btrfs都是直接对块操作,都不支持共享存储,表示当有多个容器读同一个文件时,需要生活多个复本,所以这种存储驱动不适合在高密度容器的PaaS平台上使用。而且在很多容器启停的情况下可能会导致磁盘溢出,造成主机不能工作。Device mapper不建议在生产使用。Btrfs在docker build可以很高效。
    ZFS最初是为拥有大量内存的Salaris服务器设计的,所在在使用时对内存会有影响,适合内存大的环境。ZFS的COW使碎片化问题更加严重,对于顺序写生成的大文件,如果以后随机的对其中的一部分进行了更改,那么这个文件在硬盘上的物理地址就变得不再连续,未来的顺序读会变得性能比较差。ZFS支持多个容器共享一个缓存块,适合PaaS和高密度的用户场景。
WeiyiGeek.

WeiyiGeek.

IO性能对比
测试工具:IOzone(是一个文件系统的benchmark工具,可以测试不同的操作系统中文件系统的读写性能)
测试场景:从4K到1G文件的顺序和随机IO性能
测试方法:基于不同的存储驱动启动容器,在容器内安装IOzone,执行命令./iozone -a -n 4k -g 1g -i 0 -i 1 -i 2 -f /root/test.rar -Rb ./iozone.xls

测试项的定义和解释

  • Write:测试向一个新文件写入的性能。
  • Re-write:测试向一个已存在的文件写入的性能。
  • Read:测试读一个已存在的文件的性能。
  • Re-Read:测试读一个最近读过的文件的性能。
  • Random Read:测试读一个文件中的随机偏移量的性能。
  • Random Write:测试写一个文件中的随机偏移量的性能。

测试数据对比结果:

  • Write

    WeiyiGeek.Write

    WeiyiGeek.Write

  • Re-write

    WeiyiGeek.Re-write

    WeiyiGeek.Re-write

  • Read

    WeiyiGeek.Read

    WeiyiGeek.Read

  • Re-Read

    WeiyiGeek.Re-Read

    WeiyiGeek.Re-Read

  • Random Read

    WeiyiGeek.Random Read

    WeiyiGeek.Random Read

  • Random Write

    WeiyiGeek.Random Write

    WeiyiGeek.Random Write

通过以上的性能数据可以看到:

  • AUFS在读的方面性能相比Overlay要差一些,但在写的方面性能比Overlay要好。
  • device mapper在512M以上文件的读写性能都非常的差,但在512M以下的文件读写性能都比较好。
  • btrfs在512M以上的文件读写性能都非常好,但在512M以下的文件读写性能相比其他的存储驱动都比较差。
  • ZFS整体的读写性能相比其他的存储驱动都要差一些

注意事项:

  • Centos系统上(默认不支持aufs需要查看AUFS是否加入Linux内核),推荐使用overlayfs存储驱动;
  • devicemapper默认会在/var/lib/docker/devicemapper/devicemapper目录下生成data和metadata两个稀疏文件,并将两个文件挂为loop设备作为块设备来使用。
  • Direct和LVM的最大不同是创建DM thin pool的不再是通过losetup挂载的两个稀疏文件,而是两个裸的真正的块设备。由于direct lvm的读写性能表现更加稳定,推荐生产环境上使用direct-lvm模式;


数据共享与持久化

描述:Docker 数据卷是被用于共享和持久化数据的,而且它的声明周期是独立于容器的,所以当容器宕掉或者删除并不会导致数据卷中的数据丢失;

Q: 数据卷的实现原理?
答: 简单的说卷其实就是文件或者目录,通过挂载的方式,由Docker Daemon挂载到容器内部,不属于联合系统;

Docker 提供的几种数据卷的实现方式:

  • 1.数据卷
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    # (1) 宿主机随机目录,指定的目录如果没有不存在将会被创建
    docker run -itd -v /tmp/container/folder --name data-volume busybox top
    docker inspect data-volume -f 'Source:{{(index .Mounts 0).Source}}{{println}}Destination:{{(index .Mounts 0).Destination}}'
    # 即表示容器中的/tmp/container/folder目标被挂载到宿主机的随机目录;
    # Source:/var/lib/docker/volumes/1a035d81e56447cd314faf560754b1808234645785f1b61a2c17199b16a7cf27/_data
    # Destination:/tmp/container/folder
    $docker exec -it data-volume sh
    $ls /tmp/container/folder/ls^C

    # (2) 宿主机指定目录
    docker run -itd -v /tmp/mnt_isolation:/tmp/tar/tmp/container/folder --name data-volume-1 busybox top
    docker inspect data-volume-1 -f 'Source:{{(index .Mounts 0).Source}}{{println}}Destination:{{(index .Mounts 0).Destination}}'
    # 指定了宿主机映射的目录(下面查看源、目的地址的目录)
    # Source:/tmp/mnt_isolation
    # Destination:/tmp/tar/tmp/container/folder
  • 2.数据卷容器:支持多个容器通过某一个容器进行数据工程,PS:k8s是Pod采用的就是这种共享方式;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    # (3) 连接已经存在的数据卷,通过--volumes-from将上面的数据卷data-volume进行挂载起来
    docker run -itd --volumes-from data-volume --name data-volume-2 busybox top
    # 91bfdb9c9ca88571faebf6f2409105b1e55d24866fe30a4ea7c2f750031c8baf
    docker run -itd --volumes-from data-volume --name data-volume-3 busybox top
    # f601713d75277e6255e9a52a30046f1e38c169d7674f3ef2dd287bdb444820d1
    # 进入data-volume-3容器中在其数据卷目录中创建文件
    $docker exec -it data-volume-3 touch /tmp/container/folder/$$.txt
    # 分别查看几个容器共享的数据卷是否一致
    $docker exec -it data-volume-3 ls -alh /tmp/container/folder/
    -rw-r--r-- 1 root root 0 Jul 6 13:50 3758.txt
    $docker exec -it data-volume-2 ls -alh /tmp/container/folder/
    -rw-r--r-- 1 root root 0 Jul 6 13:50 3758.txt
    $docker exec -it data-volume ls -alh /tmp/container/folder/
    -rw-r--r-- 1 root root 0 Jul 6 13:50 3758.txt
    # 删除数据卷容器目录时需要加上-v则删除一个停止的容器并且其数据卷中的内容
    $docker stop data-volume data-volume-2 data-volume-3
    $docker rm -v data-volume data-volume-2 data-volume-3 #需要删除全部的数据卷存储的数据才会全部消失
    data-volume
    #补充:docker volume prune 清除已停止的数据卷
  • 3.数据卷插件
    描述:Docker有多种存储驱动比如aufs、devicemapper等,对于Docker的存储卷一般就是用完将会被丢弃,很难被本机或者其它机器的容器复用,在存储插件出现之前复用的存储卷的唯一方式是在docker run 命令中,通过映射宿主机目录的方式(-v 源宿主机:目标容器),将卷中保存的数据保存在宿主机上,通过Volume Plugin机制能够很方便的整合第三方存储为Docker提供Volume;

Q: 什么是存储插件?
答: 通过使用Docker存储卷插件,为容器提供了持久化卷存储;

Q: 存储插件有哪些?常用的有以下几种插件:

  • Rancher Convoy 其后端支持devicemapper,NFS, EBS 等实现容器跨主机共享数据,并且支持卷的增量备份快照,备份恢复,而且用户也可以方便在不同的宿主机上共享卷,以及卷的迁移,实际实现原理还是将主机目录挂载到容器之中;

Q: 采用哪些方式进行容器数据备份?

  • 1) 通过卷插件,在宿主机上采用手工的方式将分布式系统挂载到本地;
  • 2) 通过容器内部或者外部数据收集程序(fluentd或filebeat)将存储于宿主机上的数据进行实时收集从而减少容器销毁导致的数据丢失带来的损失;

0x01 安全特性

1.Docker服务端防护

Docker 服务的运行目前需要 root 权限,因此其安全性十分关键,由于运行一个容器或应用程序的核心是通过 Docker 服务端。

Docker 允许用户在主机和容器间共享文件夹,同时不需要限制容器的访问权限,这就容易让容器突破资源限制;
例如:恶意用户启动容器的时候将主机的根目录/映射到容器的 /host 目录中,那么容器理论上就可以对主机的文件系统进行任意修改了
因此当提供容器创建服务时(例如通过一个 web 服务器),要更加注意进行参数的安全检查,防止恶意的用户用特定参数来创建一些破坏性的容器。

DOCKER的安全特性:

  • 首先,确保只有可信的用户才可以访问 Docker 服务(理论上由于攻击层出不穷)。
  • 其次, 在容器内不使用 root 权限来运行进程的话。
  • 确保只有可信的网络或 VPN,或证书保护机制(例如受保护的 stunnel 和 ssl 认证)下的访问可以进行。
  • 将容器的 root 用户映射到本地主机上的非 root 用户,减轻容器和主机之间因权限提升而引起的安全问题;
  • 允许 Docker 服务端在非 root 权限下运行,利用安全可靠的子进程来代理执行需要特权权限的操作。这些子进程将只允许在限定范围内进行操作,例如仅仅负责虚拟网络设定或文件系统管理、配置操作等。


2.内核能力机制

Linux 内核一个强大的特性(从Kernel 2.2版本起),可以提供细粒度的权限访问控制操作能力,既可以作用在进程上,也可以作用在文件上。
使用能力机制对加强 Docker 容器的安全有很多好处;

默认情况下,Docker 启动的容器被严格限制只允许使用内核的一部分能力。并且Docker采用白名单机制,禁用必需功能之外的其它权限

例如,一个 Web 服务进程只需要绑定一个低于 1024 的端口的权限,并不需要 root 权限。那么它只需要被授权 net_bind_service 能力即可

为了加强安全,容器可以禁用一些没必要的权限。

  • 完全禁止任何 mount 操作;
  • 禁止直接访问本地主机的套接字;
  • 禁止访问一些文件系统的操作,比如创建新的设备、修改文件属性等;
  • 禁止模块加载。

这样就算攻击者在容器中取得了 root 权限,也不能获得本地主机的较高权限,能进行的破坏也有限。


3.其它安全特性

利用一些现有的安全机制来增强使用 Docker 的安全性,例如 TOMOYO, AppArmor, SELinux, GRSEC 等。
Docker 当前默认只启用了能力机制,用户可以采用多种方案来加强 Docker 主机的安全,例如:

  • 在内核中启用 GRSEC 和 PAX,这将增加很多编译和运行时的安全检查;通过地址随机化避免恶意探测等。并且启用该特性不需要 Docker 进行任何配置。
  • 使用一些有增强安全特性的容器模板,比如带 AppArmor 的模板和 Redhat 带 SELinux 策略的模板。这些模板提供了额外的安全特性。
  • 用户可以自定义访问控制机制来定制安全策略。
    跟其它添加到 Docker 容器的第三方工具一样(比如网络拓扑和文件系统共享),有很多类似的机制,在不改变 Docker 内核情况下就可以加固现有的容器。

0x02 Docker 容器安全

(1)权限管理
Docker运行权限

docker安全配置

1
2
3
4
5
6
7
8
#建议docker账号与组,不采用root运行
#建立 docker 组:
$ sudo groupadd docker
#将当前用户加入 docker 组:
$ sudo usermod -aG docker $USER

#默认情况下,不同容器之间是允许网络互通的。
#如果为了安全考虑,可以在 /etc/docker/daemon.json 文件中配置 {"icc": false} 来禁止它。


容器内部运行权限

Q:在说此项前我们先来了解容器内部应用程序运行用户设置建议采用gosu还是sudo?
答:我们通过实战来确定到底使用哪一个命令较好;

1
2
3
4
5
6
7
8
9
10
11
# gosu
docker run --rm gosu/alpine gosu root ps aux
PID USER TIME COMMAND
1 root 0:00 ps aux #通过gosu启动的是符合我们要求的(PID为1)容器内的唯一进程

# sudo
docker run --rm ubuntu:trusty sudo ps aux
#容器内出现了两个进程,sudo命令会创建第一个进程,然后该进程再创建了ps进程,而且ps进程的PID并不等于1,这是达不到我们要求的,此时在宿主机向该容器发送信号量收到信号量的是sudo进程;
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.0 46012 1772 ? Rs 12:05 0:00 sudo ps aux
root 6 0.0 0.0 15568 1140 ? R 12:05 0:00 ps aux

小结:

  • gosu启动命令时只有一个进程,所以docker容器启动时使用gosu,那么该进程可以做到PID等于1;
  • sudo启动命令时先创建sudo进程,然后该进程作为父进程去创建子进程,1号PID被sudo进程占据;

正题回归

描述:为了安全容器中不要使用root账号(即最小权限),此时就需要一个能够提升自定账号权限的命令 gosu 便应运而生它与 sudo 类似但它比sudo更安全;

方便学习先来看看一个小例子(在镜像中创建非root账号):

首先我们以redis官方镜像的Dockerfile为例,来看看如何创建账号

1
2
3
4
5
6
#1.先添加我们的用户和组,以确保他们的id被一致地分配,不管添加了什么依赖项
RUN groupadd -r redis && useradd -r -g redis redis

#2.可见redis官方镜像使用groupadd和useradd创建了名为redis的组合账号,接下来就是用redis账号来启动服务了,理论上应该是以下套路;
* 用USER redis将账号切换到redis;
* 在docker-entrypoint.sh执行的时候已经是redis身份了,如果遇到权限问题,例如一些文件只有root账号有读、写、执行权限,用sudo xxx命令来执行即可;

但事实并非如此!
在Dockerfile脚本中未发现USER redis命令,意味着执行docker-entrypoint.sh文件的身份是root;

其次在docker-entrypoint.sh中没有发现su - redis命令,也没有sudo命令
这是怎么回事呢?难道容器内的redis服务是用root账号启动的?

1
2
3
4
5
6
7
8
9
#动手实践
docker run --name myredis -idt redis
docker exec -it myredis /bin/bash
apt-get update && apt-get install procps #更新软件源以及PS命令安装
[email protected]:/data# ps -ef
UID PID PPID C STIME TTY TIME CMD
redis 1 0 0 09:22 ? 00:00:01 redis-server *:6379
root 287 0 0 09:36 ? 00:00:00 /bin/bash
root 293 287 0 09:39 ? 00:00:00 ps -ef

上面的结果展示了两个关键信息:
第一,redis服务是redis账号启动的并非root;
第二,redis服务的PID等于(重要),宿主机执行docker stop命令时,该进程可以收到SIGTERM信号量,于是redis应用可以做一些退出前的准备工作,例如保存变量、退出循环等,也就是优雅停机(Gracefully Stopping);

现在我们已经证实了redis服务并非root账号启动,而且该服务进程在容器内还是一号进程,但是我们在Dockerfile和docker-entrypoint.sh脚本中都没有发现切换到redis账号的命令,也没有sudo和su,这是怎么回事呢?

答案:在于redis的docker-entrypoint.sh文件之中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/bin/sh
set -e

# first arg is `-f` or `--some-option`
# or first arg is `something.conf`
if [ "${1#-}" != "$1" ] || [ "${1%.conf}" != "$1" ]; then
set -- redis-server "[email protected]"
fi

# allow the container to be started with `--user` | 首次运行进入
if [ "$1" = 'redis-server' -a "$(id -u)" = '0' ]; then
# 当前文件夹下不属于redis用户的文件,全部授权为redis用户
find . \! -user redis -exec chown redis '{}' +
exec gosu redis "$0" "[email protected]"
fi

exec "[email protected]"

注意上图中的代码分析一下:

  • 1.假设启动容器的命令是docker run --name myredis -idt redis redis-server /usr/local/etc/redis/redis.conf
  • 2.容器启动后会执行docker-entrypoint.sh脚本此时的账号是root;
  • 3.当前账号是root因此进入后会执行第二个if条件中命令,其中位置参数含税表示如下;
    • $0表示当前脚本的名称即docker-entrypoint.sh;
    • [email protected]表示外部传入的所有参数,即redis-server /usr/local/etc/redis/redis.conf;
  • 表示以redis账号的身份执行以下命令
    1
    2
    # gosu redis "$0" "@"
    docker-entrypoint.sh redis-server /usr/local/etc/redis/redis.conf
  • gosu redis "$0" "@"前面加上个exec,表示以gosu redis “$0” “@”这个命令启动的进程替换正在执行的 docker-entrypoint.sh 进程 保证了对应的进程ID为1
  • gosu redis "$0" "@"导致docker-entrypoint.sh再执行一次,但是当前的账号已经不是root了,所以会执行兜底逻辑 exec “[email protected]”;
  • 此时的[email protected]redis-server /usr/local/etc/redis/redis.conf,因此redis服务会启动并且启动服务的用户为redis;

最后我们在 Redis 的Dockerfile 中可以看见安装gosu的一些身影

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# grab gosu for easy step-down from root
# https://github.com/tianon/gosu/releases
ENV GOSU_VERSION 1.10
RUN set -ex; \
\
fetchDeps=" \
ca-certificates \
dirmngr \
gnupg \
wget \
"; \
apt-get update; \
apt-get install -y --no-install-recommends $fetchDeps; \
rm -rf /var/lib/apt/lists/*; \
\
dpkgArch="$(dpkg --print-architecture | awk -F- '{ print $NF }')"; \
wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch"; \
wget -O /usr/local/bin/gosu.asc "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch.asc"; \
export GNUPGHOME="$(mktemp -d)"; \
gpg --batch --keyserver ha.pool.sks-keyservers.net --recv-keys B42F6819007F00F88E364FD4036A9C25BF357DD4; \
gpg --batch --verify /usr/local/bin/gosu.asc /usr/local/bin/gosu; \
gpgconf --kill all; \
rm -r "$GNUPGHOME" /usr/local/bin/gosu.asc; \
chmod +x /usr/local/bin/gosu; \
gosu nobody true; \
\
apt-get purge -y --auto-remove $fetchDeps