历史

  1. 基于 Linux 的 NamespaceCgroupUnion FS,对进程进行封装隔离,属于 OS 层的虚拟化技术
  2. Docker 最初的实现是基于 LXC
    • 0.7 以后开始移除 LXC,而使用自研的 Libcontainer
    • 1.11 开始,使用 runCContainerd
  3. Docker 在容器的基础上,进行进一步的封装,极大地简化容器的创建和维护

Docker vs VM

image-20230523210544180

特性 Docker VM
启动 秒级 分钟级
磁盘 MB GB
性能 接近原生 弱于
数量 单机上千个容器 几十个

容器标准

OCI - Open Container Initiative

Key Value
Image Specification 如何打包
Runtime Specification 如何解压应用包并运行
Distribution Specification 如何分发镜像

主要特性

隔离性(Namespace)、可配额(CGroup)、便携性(Union FS)、安全性

Namespace

Linux Namespace 是 Linux Kernel 提供的资源隔离方案
Linux 为进程分配不同的 Namespace(不同 Namespace 下的进程互不干扰

Linux Kernel 中相关的数据结构

image-20230523225636574

Linux 对 Namespace 的操作方法

Linux 上 pid 为 1 号的进程,本身会分配默认的 Namespace

操作方法 描述
clone 创建新进程时,可以指定新建的 Namespace 类型
setns 让调用进程加入某个已经存在的 Namespace – nsenter
unshare 将调用进程移动的 Namespace 下
1
2
3
$ sudo lsns -t pid
NS TYPE NPROCS PID USER COMMAND
4026531836 pid 349 1 root /sbin/init splash

分类

Namespace 类型 隔离资源 描述
IPC System V IPC 和 POSIX 消息队列 容器中的进程交互还是采用 Linux 常见的 IPC
容器的进程间交互实际是宿主上具有相同 pid namespace 中的进程间交互
Network 网络设备、网络协议栈、网络端口等 每个 net namespace 有独立的网络设备
Docker 默认采用 veth 的方式将容器中的虚拟网卡与宿主机上的 docker0 桥接在一起
PID 进程 不同用户的进程通过 pid namesapce 隔离
不同 pid namespace 中可以有相同的 pid
Mount 挂载点 允许不同 namespace 的进程看到不同的文件结构
UTS
UNIX Time-sharing System
主机名 + 域名 允许每个容器拥有独立的 hostnamedomain main
使其在网络上被当作一个独立的节点,而非 host 上的一个进程
USR 用户 + 用户组 允许容器有不同的 usergroup id
在容器内部使用容器内部的用户执行程序,而非 host 上的用户

image-20230523222758697

查看当前系统的 namespace:lsns

1
2
3
4
$ lsns -t pid
NS TYPE NPROCS PID USER COMMAND
4026531836 pid 107 22040 xxxxx /lib/systemd/systemd --user
4026532946 pid 4 2043129 xxxxx java -jar lib/sonar-application-9.2.4.50792.jar

查看进程的 namespace

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ ls -la /proc/2043129/ns
total 0
dr-x--x--x 2 xxxxx xxxxx 0 May 18 13:44 .
dr-xr-xr-x 9 xxxxx xxxxx 0 May 18 13:44 ..
lrwxrwxrwx 1 xxxxx xxxxx 0 May 23 22:45 cgroup -> 'cgroup:[4026533029]'
lrwxrwxrwx 1 xxxxx xxxxx 0 May 23 22:42 ipc -> 'ipc:[4026532945]'
lrwxrwxrwx 1 xxxxx xxxxx 0 May 23 22:43 mnt -> 'mnt:[4026532943]'
lrwxrwxrwx 1 xxxxx xxxxx 0 May 18 13:44 net -> 'net:[4026532947]'
lrwxrwxrwx 1 xxxxx xxxxx 0 May 23 22:43 pid -> 'pid:[4026532946]'
lrwxrwxrwx 1 xxxxx xxxxx 0 May 23 22:45 pid_for_children -> 'pid:[4026532946]'
lrwxrwxrwx 1 xxxxx xxxxx 0 May 23 22:45 time -> 'time:[4026531834]'
lrwxrwxrwx 1 xxxxx xxxxx 0 May 23 22:45 time_for_children -> 'time:[4026531834]'
lrwxrwxrwx 1 xxxxx xxxxx 0 May 23 22:42 user -> 'user:[4026531837]'
lrwxrwxrwx 1 xxxxx xxxxx 0 May 23 22:42 uts -> 'uts:[4026532944]'

进入 namespace 并运行命令:nsenter – 常用

1
2
3
4
5
6
7
8
9
$ sudo nsenter -t 2043129 -n ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default 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
162: eth0@if163: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 02:42:ac:19:00:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet 172.25.0.2/16 brd 172.25.255.255 scope global eth0
valid_lft forever preferred_lft forever

样例 1:Docker 容器

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
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
1654738b86ae sonarqube:community "/opt/sonarqube/bin/…" 5 weeks ago Up 5 days 0.0.0.0:9000->9000/tcp, :::9000->9000/tcp sonar-sonarqube-1

$ lsns -t net
NS TYPE NPROCS PID USER NETNSID NSFS COMMAND
4026531840 net 107 22040 xxxxx unassigned /lib/systemd/systemd --user
4026532947 net 4 2043129 xxxxx 3 /run/docker/netns/f039a5128af9 java -jar lib/sonar-application-9.2.4.50792.jar

$ docker exec 16 ip a
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
162: eth0@if163: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue state UP
link/ether 02:42:ac:19:00:02 brd ff:ff:ff:ff:ff:ff
inet 172.25.0.2/16 brd 172.25.255.255 scope global eth0
valid_lft forever preferred_lft forever

$ docker inspect 16 | grep Pid
"Pid": 2043129,
"PidMode": "",
"PidsLimit": null,

$ sudo nsenter -t 2043129 -n ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default 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
162: eth0@if163: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 02:42:ac:19:00:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet 172.25.0.2/16 brd 172.25.255.255 scope global eth0
valid_lft forever preferred_lft forever

样例 2:在的 network namespace 中运行 sleep,在该 net namespace 所看到的网络配置,与宿主不一致

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ sudo unshare -fn sleep 600

$ ps -ef | grep sleep
root 2162506 2161352 0 22:58 pts/0 00:00:00 sudo unshare -fn sleep 600
root 2162507 2162506 0 22:58 pts/1 00:00:00 sudo unshare -fn sleep 600
root 2162508 2162507 0 22:58 pts/1 00:00:00 unshare -fn sleep 600
root 2162509 2162508 0 22:58 pts/1 00:00:00 sleep 600

$ sudo lsns -t net
NS TYPE NPROCS PID USER NETNSID NSFS COMMAND
4026533030 net 2 2162508 root unassigned unshare -fn sleep 600

$ sudo nsenter -t 2162508 -n ip a
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00

Cgroup

  1. Cgroup(Control Group)是 Linux 对一个或者一组进程进行资源控制和监控的机制
  2. 常见资源:CPU 使用时间内存磁盘 IO
  3. 不同资源的具体管理工作由相应的 Cgroup 子系统来实现
  4. 针对不同类型的资源限制,只需要将限制策略在不同的 Cgroup 子系统上进行关联即可
  5. 采用层级树(可递归嵌套)的方式来组织管理:子 Cgroup 受父 Cgroup 的限制
Cgroup 子系统 资源限额
blkio 限制每个块设备的输入输出控制
CPU 使用调度程序为 cgroup 任务提供 CPU 的访问
cpuacct 产生 cgroup 任务的 CPU 资源报告
cpuset 如果是多核 CPU,为 cgroup 任务分配单独的 CPU 和内存
memory 设置每个 cgroup 的内存限制 + 产生内存资源报告

Cgroup Driver

当 OS 使用 systemd 作为 init system 时,会初始化进程生成一个根 Cgroup 目录结构并作为 Cgroup 管理器
systemd 与 Cgroup 紧密结合,并且为每个 systemd unit 分配 Cgroup

cgroupfs:Docker 默认使用 cgroupfs 作为 Cgroup Driver

systemd 作为 init system 的系统中,默认并存在着两套 Cgroup Driver,容易发生冲突

image-20230524080051674

CPU

相对值
cpu.shares - 可出让的能获得CPU使用时间的相对值,默认为 1024

image-20230524080801421

绝对值
cpu.cfs_period_us / cpu.cfs_quota_us
cfs_period_us - 配置时间周期长度,单位为 us(微秒),默认为 100_000
cfs_quota_us - 配置在 cfs_period_us 时间内最多能使用的 CPU 时间数,单位为 us,默认为 -1(不限制)

image-20230524081122220

Others Desc
cpu.stat Cgroup 内的进程使用的 CPU 时间统计
nr_periods 经过 cfs_period_us 的时间周期数量
nr_throttled 在经过的周期内,有多少次因为进程在指定的时间周期内用完了配额时间而受到了限制
throttled_time Cgroup 中的进程被限制使用 CPU 的总用时,单位为 ns (纳秒)

Linux CFS 调度器:Completely Fair Scheduler ,完全公平调度器

主要思想:维护为任务提供处理器时间的平衡,分配给某个任务的时间事情失衡时,应该给失衡的任务分配处理器时间

通过 vruntime 来实现平衡:vruntime = 实际运行时间 * 1024 / 进程权重
优先级越高,其 vruntime 跑得越慢,处于红黑树(以 vruntime 为顺序)的左侧,进而获得更多的实际运行时间

image-20230524082814753

cpuacct

用于统计 Cgroup 及其子 Cgroup 下进程的 CPU 的使用情况

Key Desc
cpuacct.usage 包含该 Cgroup 及其子 Cgroup 下进程使用 CPU 的时间,单位 ns
cpuacct.stat 包含该 Cgroup 及其子 Cgroup 下进程使用 CPU 的时间,以及用户态和内核态的时间
1
2
3
4
5
6
7
8
9
$ cat /sys/fs/cgroup/cpu.stat
usage_usec 1330383788000
user_usec 852027688000
system_usec 478356100000
nr_periods 0
nr_throttled 0
throttled_usec 0
nr_bursts 0
burst_usec 0

Memory

K8S 使用了 limit_in_bytes,并没有使用 soft_limit_in_bytes

Key Desc
memory.usage_in_bytes Cgroup 下进程使用的内存,包含 Cgroup 及其子 Cgroup 下进程使用的内存
memory.max_usage_in_bytes Cgroup 下进程使用的内存的最大值,包含子 Cgroup 的内存使用量
memory.limit_in_bytes 设置 Cgroup 下进程最多使用的内存,-1 表示不限制
memory.soft_limit_in_bytes 该限制不会阻止进程使用超过限额的内存
只是在系统内存足够时,会优先回收超过限额的内存,使其向限定值靠拢
memory.oom_control 设置是否在 Cgroup 中使用 OOM Killer,默认使用

Union FS

  1. 不同目录挂载到同一个虚拟文件系统下的文件系统
  2. 支持为每一个成员目录设定权限readonlyreadwritewhiteout-able
  3. 文件系统分层,对 readonly 权限的成员目录可以进行逻辑上的修改

容器镜像

image-20230525210045498

Docker 文件系统

  1. bootfs
    • Bootloader - 引导加载 Kernel
    • Kernel 被加载到内存umount bootfs
  2. rootfs
    • 标准目录和文件:/dev/proc/bin/etc
    • 对于不同的 Linux 发行版,bootfs 基本一致,但 rootfs 会有差异

image-20230525211057426

Docker 启动

  1. Linux 启动
    • 在 Linux 启动后,先将 rootfs 设置为 readonly,经过一系列检查后,将其切换为 readwrite
  2. Docker 启动
    • 先将 rootfsreadonly 的方式进行加载并检查
    • 借助 union mount 的方式将一个 readwrite 的文件系统挂载在 readonlyrootfs 之上
    • 如果继续向上叠加文件系统,需要将文件系统设定为 readonly
    • 运行时:一组 readonly + 一个 readwrite

image-20230525211852430

写操作:对容器可写层的操作依赖存储驱动提供的写时复制用时分配机制

  1. 写时复制
    • 一个镜像可以被多个容器使用,在内存和磁盘只有一份拷贝
    • 当需要对镜像的文件进行修改,该文件会从镜像的只读文件系统复制容器的可写层
    • 不同容器对文件的修改,相互独立,互不影响
  2. 用时分配
    • 当一个文件被创建出来后,才会分配空间

容器存储驱动

Driver Docker Containerd
AUFS Ubuntu / Debian NO
OverlayFS YES YES
Device Mapper YES YES
Btrfs Ubuntu / Debian / SLES YES
ZFS YES NO

OverlayFS:也是一种 Union FS,只有两层,lower镜像只读层) 和 upper容器可写层

image-20230525213759498

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ mkdir upper lower merged work

$ echo "from lower" > lower/in_lower.txt
$ echo "from upper" > upper/in_upper.txt
$ echo "from lower" > lower/in_both.txt
$ echo "from upper" > upper/in_both.txt

$ sudo mount -t overlay overlay -o lowerdir=`pwd`/lower,upperdir=`pwd`/upper,workdir=`pwd`/work `pwd`/merged

$ df -h
Filesystem Size Used Avail Use% Mounted on
overlay 9.8G 3.3G 6.0G 36% /home/zhongmingmao/merged

$ cat merged/in_both.txt
from upper
1
2
3
4
5
6
7
8
9
10
11
12
$ docker inspect b109ffda8096
...
"GraphDriver": {
"Data": {
"LowerDir": "/var/lib/docker/overlay2/a78d4910eea485bb84ce2c6549d91dcfb6b8f33478962d7a00350833a74cfc6f-init/diff:/var/lib/docker/overlay2/cf1809d2363ee0477578d32ffe2bc1bfae195c7b125f1b5bfafc42bdd0e3bbe8/diff",
"MergedDir": "/var/lib/docker/overlay2/a78d4910eea485bb84ce2c6549d91dcfb6b8f33478962d7a00350833a74cfc6f/merged",
"UpperDir": "/var/lib/docker/overlay2/a78d4910eea485bb84ce2c6549d91dcfb6b8f33478962d7a00350833a74cfc6f/diff",
"WorkDir": "/var/lib/docker/overlay2/a78d4910eea485bb84ce2c6549d91dcfb6b8f33478962d7a00350833a74cfc6f/work"
},
"Name": "overlay2"
}
...

引擎架构

shim 的父进程是 systemd,因此 Containerd 本身重启,不会影响到正在运行的容器

image-20230525220904107

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ docker inspect --format='{{.State.Pid}}' bc67c9067dff
2230740

$ ps -ef | grep 2230740
root 2230740 2230718 0 22:18 pts/0 00:00:00 nginx: master process nginx -g daemon off;
systemd+ 2230784 2230740 0 22:18 pts/0 00:00:00 nginx: worker process
systemd+ 2230785 2230740 0 22:18 pts/0 00:00:00 nginx: worker process
systemd+ 2230786 2230740 0 22:18 pts/0 00:00:00 nginx: worker process
systemd+ 2230787 2230740 0 22:18 pts/0 00:00:00 nginx: worker process
systemd+ 2230788 2230740 0 22:18 pts/0 00:00:00 nginx: worker process
systemd+ 2230789 2230740 0 22:18 pts/0 00:00:00 nginx: worker process
systemd+ 2230790 2230740 0 22:18 pts/0 00:00:00 nginx: worker process
systemd+ 2230791 2230740 0 22:18 pts/0 00:00:00 nginx: worker process

$ ps -ef | grep 2230718
root 2230718 1 0 22:18 ? 00:00:00 /usr/bin/containerd-shim-runc-v2 -namespace moby -id bc67c9067dff58ca5194c26fca987e9acce0cf81b44820defeacbbff79970664 -address /run/containerd/containerd.sock
root 2230740 2230718 0 22:18 pts/0 00:00:00 nginx: master process nginx -g daemon off;

容器网络

Bridge:Docker 在宿主上创建了 docker0 网桥,通过 veth pair 来连接宿主上的每一个 EndPoint

场景 模式 描述
单机 Null 将容器放入独立的网络空间,但不做任何网络配置
Host 复用主机网络
Container 复用其他容器的网络
Bridge 使用 Linux 网桥iptables 提供容器互联
跨机 Underlay 使用现有底层网络,为每个容器配置可路由的 IP
Overlay 通过网络封包实现

单机

Bridge + NAT

默认模式

MASQUERADE:自动获取网卡IP 地址,然后做 SNAT

image-20230525225314217

  1. 为主机 eth0 分配 IP 192.168.0.101
  2. 启动 docker daemon,查看 iptables - SNAT
    • -A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
  3. 在主机上启动容器:docker run -d --name nginx -p 8080:80 nginx,Docker 会以标准模式配置网络
    • 创建 veth pair
    • veth pair 的一端连接到 docker0 网桥,另一端设置为容器命名空间eth0
    • 为容器命名空间的 eth0 分配 ip
    • 主机上的 iptables - DNAT
      • -A DOCKER ! -i docker0 -p tcp -m tcp --dport 8080 -j DNAT --to-destination 172.17.0.2:80
1
2
3
4
5
6
7
8
9
10
11
12
$ sudo iptables-save -t nat
*nat
:PREROUTING ACCEPT [0:0]
:INPUT ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
:POSTROUTING ACCEPT [0:0]
:DOCKER - [0:0]
-A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER
-A OUTPUT ! -d 127.0.0.0/8 -m addrtype --dst-type LOCAL -j DOCKER
-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
-A DOCKER -i docker0 -j RETURN
COMMIT
1
2
3
4
5
$ docker run -d --name nginx -p 8080:80 nginx
3371dc54c9c79c3601dd61295ed2089de42a22f99a7b08c8b1686d93eca2f465

$ docker inspect --format='{{.NetworkSettings.Networks.bridge.IPAddress}}' 3371dc54c9c7
172.17.0.2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ sudo iptables-save -t nat
*nat
:PREROUTING ACCEPT [0:0]
:INPUT ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
:POSTROUTING ACCEPT [0:0]
:DOCKER - [0:0]
-A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER
-A OUTPUT ! -d 127.0.0.0/8 -m addrtype --dst-type LOCAL -j DOCKER
-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
-A POSTROUTING -s 172.17.0.2/32 -d 172.17.0.2/32 -p tcp -m tcp --dport 80 -j MASQUERADE
-A DOCKER -i docker0 -j RETURN
-A DOCKER ! -i docker0 -p tcp -m tcp --dport 8080 -j DNAT --to-destination 172.17.0.2:80
COMMIT
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
$ brctl show
bridge name bridge id STP enabled interfaces
docker0 8000.024286655820 no veth5624eea

$ ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default 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
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
2: ens160: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
link/ether 00:0c:29:d4:3d:1d brd ff:ff:ff:ff:ff:ff
altname enp2s0
inet 192.168.191.133/24 metric 100 brd 192.168.191.255 scope global dynamic ens160
valid_lft 1258sec preferred_lft 1258sec
inet6 fe80::20c:29ff:fed4:3d1d/64 scope link
valid_lft forever preferred_lft forever
4: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 02:42:86:65:58:20 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
valid_lft forever preferred_lft forever
inet6 fe80::42:86ff:fe65:5820/64 scope link
valid_lft forever preferred_lft forever
14: veth5624eea@if13: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP group default
link/ether a2:5c:e1:91:b7:15 brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet6 fe80::a05c:e1ff:fe91:b715/64 scope link
valid_lft forever preferred_lft forever

$ docker inspect --format='{{.State.Pid}}' 3371dc54c9c7
21184

$ sudo nsenter -t 21184 -n ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default 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
13: eth0@if14: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
valid_lft forever preferred_lft forever

$ sudo nsenter -t 21184 -n ip r
default via 172.17.0.1 dev eth0
172.17.0.0/16 dev eth0 proto kernel scope link src 172.17.0.2

$ curl -v -s -o /dev/null 127.1:8080
* Trying 127.0.0.1:8080...
* Connected to 127.0.0.1 (127.0.0.1) port 8080 (#0)
> GET / HTTP/1.1
> Host: 127.0.0.1:8080
> User-Agent: curl/7.81.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Server: nginx/1.25.0
< Date: Fri, 26 May 2023 13:04:29 GMT
< Content-Type: text/html
< Content-Length: 615
< Last-Modified: Tue, 23 May 2023 15:08:20 GMT
< Connection: keep-alive
< ETag: "646cd6e4-267"
< Accept-Ranges: bytes
<
{ [615 bytes data]
* Connection #0 to host 127.0.0.1 left intact

Null

空实现,容器启动后可以通过命令为容器配置网络

1
2
3
4
5
6
7
8
9
10
11
$ docker run --network=none -d nginx
96c7f5d8306ee0ed9aef3127dfe49cf3dad234d5daf7144cb1805715f318703d

$ docker inspect --format='{{.State.Pid}}' 96c7f5d8306e
22394

$ sudo nsenter -t 22394 -n ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default 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

链接 net namespace

1
2
3
4
5
6
7
8
9
10
11
$ export pid=22394

$ sudo ls -l /proc/$pid/ns/net
lrwxrwxrwx 1 root root 0 May 26 13:16 /proc/22394/ns/net -> 'net:[4026532644]'

$ sudo ip netns list

$ sudo ln -s /proc/$pid/ns/net /var/run/netns/$pid

$ sudo ip netns list
22394

查看主机上的 docker0 网桥

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$ brctl show
bridge name bridge id STP enabled interfaces
docker0 8000.024286655820 no

$ ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default 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
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
2: ens160: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
link/ether 00:0c:29:d4:3d:1d brd ff:ff:ff:ff:ff:ff
altname enp2s0
inet 192.168.191.133/24 metric 100 brd 192.168.191.255 scope global dynamic ens160
valid_lft 1757sec preferred_lft 1757sec
inet6 fe80::20c:29ff:fed4:3d1d/64 scope link
valid_lft forever preferred_lft forever
4: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
link/ether 02:42:86:65:58:20 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
valid_lft forever preferred_lft forever
inet6 fe80::42:86ff:fe65:5820/64 scope link
valid_lft forever preferred_lft forever

创建 veth pair

1
$ sudo ip link add A type veth peer name B
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
$ sudo brctl addif docker0 A
$ sudo ip link set A up

$ ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default 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
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
2: ens160: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
link/ether 00:0c:29:d4:3d:1d brd ff:ff:ff:ff:ff:ff
altname enp2s0
inet 192.168.191.133/24 metric 100 brd 192.168.191.255 scope global dynamic ens160
valid_lft 1617sec preferred_lft 1617sec
inet6 fe80::20c:29ff:fed4:3d1d/64 scope link
valid_lft forever preferred_lft forever
4: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
link/ether 02:42:86:65:58:20 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
valid_lft forever preferred_lft forever
inet6 fe80::42:86ff:fe65:5820/64 scope link
valid_lft forever preferred_lft forever
15: B@A: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000
link/ether b2:b3:84:21:6b:36 brd ff:ff:ff:ff:ff:ff
16: A@B: <NO-CARRIER,BROADCAST,MULTICAST,UP,M-DOWN> mtu 1500 qdisc noqueue state LOWERLAYERDOWN group default qlen 1000
link/ether 42:c0:90:f4:47:97 brd ff:ff:ff:ff:ff:ff

$ brctl show
bridge name bridge id STP enabled interfaces
docker0 8000.024286655820 no A
1
2
3
4
5
6
7
8
9
$ SETIP=172.17.0.10
$ SETMASK=16
$ GATEWAY=172.17.0.1

$ sudo ip link set B netns $pid
$ sudo ip netns exec $pid ip link set dev B name eth0
$ sudo ip netns exec $pid ip link set eth0 up
$ sudo ip netns exec $pid ip addr add $SETIP/$SETMASK dev eth0
$ sudo ip netns exec $pid ip route add default via $GATEWAY
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
$ ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default 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
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
2: ens160: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
link/ether 00:0c:29:d4:3d:1d brd ff:ff:ff:ff:ff:ff
altname enp2s0
inet 192.168.191.133/24 metric 100 brd 192.168.191.255 scope global dynamic ens160
valid_lft 1382sec preferred_lft 1382sec
inet6 fe80::20c:29ff:fed4:3d1d/64 scope link
valid_lft forever preferred_lft forever
4: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
link/ether 02:42:86:65:58:20 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
valid_lft forever preferred_lft forever
inet6 fe80::42:86ff:fe65:5820/64 scope link
valid_lft forever preferred_lft forever
16: A@if15: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
link/ether 42:c0:90:f4:47:97 brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet6 fe80::40c0:90ff:fef4:4797/64 scope link
valid_lft forever preferred_lft forever

$ sudo nsenter -t $pid -n ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default 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
15: eth0@if16: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
link/ether b2:b3:84:21:6b:36 brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet 172.17.0.10/16 scope global eth0
valid_lft forever preferred_lft forever
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ curl -v -s -o /dev/null 172.17.0.10
* Trying 172.17.0.10:80...
* Connected to 172.17.0.10 (172.17.0.10) port 80 (#0)
> GET / HTTP/1.1
> Host: 172.17.0.10
> User-Agent: curl/7.81.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Server: nginx/1.25.0
< Date: Fri, 26 May 2023 13:39:29 GMT
< Content-Type: text/html
< Content-Length: 615
< Last-Modified: Tue, 23 May 2023 15:08:20 GMT
< Connection: keep-alive
< ETag: "646cd6e4-267"
< Accept-Ranges: bytes
<
{ [615 bytes data]
* Connection #0 to host 172.17.0.10 left intact

跨机

Underlay

容器网络依托于主机的物理网络

image-20230527135555514

  1. 采用 Linux 网桥设备(sbrctl),通过物理网络连通容器
  2. 创建新的网桥设备 mydr0
  3. 主机加入到网桥
  4. 主机网卡的地址配置到网桥,并把默认路由规则转移到网桥 mydr0
  5. 启动容器
  6. 创建 veth pair,把一个 peer 添加到网桥 mydr0
  7. 配置容器把 veth 的另一个 peer 分配给容器网卡

Overlay

Docker Overlay 网络驱动原生支持多主机网络,本质是封包解包的过程,有一定的开销

Libnetwork 是一个内置的基于 VXLAN 的网络驱动

image-20230527140555581

Flannel

同一主机的 Pod 可以使用网桥进行通信,不同主机的 Pod 将通过 flanneld 将其流量封装在 UDP 数据包中

image-20230527141402433

Dockerfile

应用进程必须是无状态

Build Context

  1. 运行 docker build 时,当前目录被称为构建上下文
  2. 默认查找当前目录Dockerfile 作为构建输入,docker build –f ./Dockerfile .
  3. 当运行 docker build 时,首先会把 Build Context 传输给 docker daemon
    • 如果将没用的文件包含在 Build Context 中,会导致传输时间过长,需要更多的构建资源,构建出的镜像大等问题
    • 可以通过 .dockerignore 来排除某些文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ docker build -f Dockerfile -t "xxx/yyy/zzz:0.0.1" .
[+] Building 1.5s (10/10) FINISHED
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 759B 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [internal] load metadata for xxx/yyy/base:1.0.0 0.4s
=> [auth] yyy/base:pull token for xxx 0.0s
=> [internal] load build context 1.0s
=> => transferring context: 151.91MB 1.0s
=> [1/4] FROM xxx/yyy/base:1.0.0@sha256:0680efe067d6c17586cb628ea9f6675 0.0s
=> CACHED [2/4] WORKDIR /home/aaa/local 0.0s
=> CACHED [3/4] COPY binaries/promtool-linux64/promtool /home/aaa/local/promtool 0.0s
=> CACHED [4/4] COPY target/zzz-*-exec.jar /home/aaa/local/zzz.jar 0.0s
=> exporting to image 0.0s
=> => exporting layers 0.0s
=> => writing image sha256:77a3fa3f601d7492d3fc86f508159080df81c751b080ff800661fb198c13c099 0.0s
=> => naming to xxx/yyy/zzz:0.0.1 0.0s

Build Cache

构建容器镜像时,Docker 依次读取 Dockerfile 中的指令,并按顺序依次执行构建指令

  1. 通常 Docker 简单判断 Dockerfile 中的指令镜像
  2. 针对 ADDCOPY 指令,Docker 会判断该镜像层每一个文件的内容并生成一个 checksum
  3. 其他指令,Docker 简单比较与现存镜像中指令字符串是否一致
  4. 当某一层 Cache 失效后,后续指令都重新构建镜像
    • 最佳实践:稳定的层位于 Dockerfile 的前面,可以最大化地利用 Cache(构建+拉取

Multi-stage build

有效减少镜像层级COPY --from

1
2
3
4
5
6
7
8
9
10
11
12
13
# stage 1
FROM golang:1.16-alpine AS build
RUN apk add --no-cache git
RUN go get github.com/golang/dep/cmd/dep
COPY Gopkg.lock Gopkg.toml /go/src/project/ WORKDIR /go/src/project/
RUN dep ensure -vendor-only
COPY . /go/src/project/
RUN go build -o /bin/project # only need '/bin/project'

# stage 2
FROM scratch
COPY --from=build /bin/project /bin/project ENTRYPOINT ["/bin/project"]
CMD ["--help"]

常用指令

指令 描述 格式
FROM 推荐 alpine FROM [--platform=<platform>] <image>[@<digest>] [AS <name>]
LABELS 按标签组织项目 LABEL multi.label1="value1" multi.label2="value2"
docker images -f label=multi.label1="value1"
RUN 执行命令 RUN apt-get update && apt-get install
CMD 应用的运行命令 CMD ["executable", "param1", "param2"...]
EXPOSE 发布端口 EXPOSE <port> [<port>/<protocol>...]
ENV 设置环境变量 ENV <key>=<value> ...

ADD

从源地址(文件目录URL)复制文件到目标路径:ADD [--chown=<user>:<group>] <src>... <dest>

  1. 支持 Go 风格的通配符,ADD check* /testdir/
  2. src 如果是文件,则必须包含在 Build Context
  3. src 如果是URL
    • 如果 dest 结尾有 /,那么 dest 是目标文件夹;如果 dest 结尾没有 /,那么 dest 是目标文件名
    • 尽量使用 curl 或者 wget 来替代
  4. src 如果是一个目录,则所有文件都会被复制到 dest
  5. src 如果是一个本地压缩文件,则会同时完成解压操作
  6. dest 如果不存在,则会自动创建目录

COPY

从源地址复制文件到目标路径:COPY [--chown=<user>:<group>] <src>... <dest>

  1. 只支持本地文件的复制,不支持 URL
  2. 不解压文件
  3. 可用于 Multi-stage build,即 COPY --from

语义更清晰,复制本地文件时,优先使用 COPY

ENTRYPOINT

定义可以执行的容器镜像入口命令

  1. docker run
    • 参数追加模式:ENTRYPOINT ["executable", "param1", "param2"]
    • 参数替换模式:ENTRYPOINT command param1 param2
  2. 替换 Dockerfile 中定义的 ENTRYPOINT
    • docker run –entrypoint
  3. 最佳实践:通过 ENTRYPOINT 定义主命令,通过 CMD 定义主要参数
1
2
ENTRYPOINT ["s3cmd"]
CMD ["--help"]

VOLUME

将指定目录定义为外挂存储卷,Dockerfile 中在该指令后对该同一目录的修改都是无效

1
VOLUME ["/data"]

等价于 docker run –v /data

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ docker run -d --name nginx -p 8080:80 -v /data nginx
64e8ba804b7dbe0503e65736e66447b5a30137a744f5daf97f6c7e5a4772758f

$ docker inspect 64e8ba804b7d
...
"Mounts": [
{
"Type": "volume",
"Name": "49231693971eca4c050c3ae564da163418448da945e1a8ccfd11a12c7fb91c26",
"Source": "/var/lib/docker/volumes/49231693971eca4c050c3ae564da163418448da945e1a8ccfd11a12c7fb91c26/_data",
"Destination": "/data",
"Driver": "local",
"Mode": "",
"RW": true,
"Propagation": ""
}
],
...

$ docker volume ls
DRIVER VOLUME NAME
local 49231693971eca4c050c3ae564da163418448da945e1a8ccfd11a12c7fb91c26

USER

切换运行镜像的用户和用户组(容器应用以 non-root 运行,容器内访问会受限

1
USER <user>[:<group>]

WORKDIR

切换工作目录

最佳实践

  1. 不安装无效软件包
  2. 简化同时运行的进程数,在理想情况下,只有 1 个进程
    • 如果运行多进程,选择合理的初始化进程(具备管理子进程的能力)
  3. 镜像层最少化
    • 最新的 Docker 只有 RUNCOPYADD 才会创建新层,其它指令只会创建临时层,并不会增加镜像大小
    • 多条 RUN 指令通过 && 来连接成一条指令集
    • 借助 Multi-stage build
  4. 变更频率低的指令优先构建(位于镜像底层,可以更有效地利用 Build Cache
  5. 每个文件单独复制,确保某个文件变更时,只影响该文件对应的缓存

多进程

选择适当的 init 进程ENTRYPOINT 进程:具备管理子进程的能力)

  1. 需要捕获 SIGTERM 信号并完成子进程优雅终止
  2. 负责清理退出的子进程,避免僵尸进程