关于运维:100-行-shell-写个-Docker

24次阅读

共计 22128 个字符,预计需要花费 56 分钟才能阅读完成。

作者:vivo 互联网运维团队 - Hou Dengfeng

本文次要介绍应用 shell 实现一个繁难的 Docker。

一、目标

在初接触 Docker 的时候,咱们必须要理解的几个概念就是 Cgroup、Namespace、RootFs,如果自身对虚拟化的倒退没有深刻的理解,那么很难对这几个概念有深刻的了解,本文的目标就是通过在操作系统中以交互式的形式去了解,Cgroup/Namespace/Rootfs 到底实现了什么,能做到哪些事件,而后通过 shell 这种直观的命令行形式把咱们的了解组合起来,去模拟 Docker 实现一个缩减的版本。

二、技术拆解

2.1 Namespace

2.1.1 简介

Linux Namespace 是 Linux 提供的一种内核级别环境隔离的办法。学习过 Linux 的同学应该对 chroot 命令比拟相熟(通过批改根目录把用户限度在一个特定目录下),chroot 提供了一种简略的隔离模式:chroot 外部的文件系统无法访问内部的内容。Linux Namespace 在此基础上,提供了对 UTS、IPC、mount、PID、network、User 等的隔离机制。Namespace 是对全局系统资源的一种封装隔离,使得处于不同 namespace 的过程领有独立的全局系统资源,扭转一个 namespace 中的系统资源只会影响以后 namespace 里的过程,对其余 namespace 中的过程没有影响。

Linux Namespace 有如下品种:

2.1.2 Namespace 相干零碎调用

amespace 相干的零碎调用有 3 个,别离是 clone(),setns(),unshare()。

  • clone: 创立一个新的过程并把这个新过程放到新的 namespace 中
  • setns: 将以后过程退出到已有的 namespace 中
  • unshare: 使以后过程退出指定类型的 namespace,并退出到新创建的 namespace 中

2.1.3 查看过程所属 Namespace

下面的概念都比拟形象,咱们来看看在 Linux 零碎中怎么样去 get namespace。

零碎中的每个过程都有 /proc/[pid]/ns/ 这样一个目录,外面蕴含了这个过程所属 namespace 的信息,外面每个文件的描述符都能够用来作为 setns 函数 (2.1.2) 的 fd 参数。

# 查看以后 bash 过程关联的 Namespace
# ls -l /proc/$$/ns
total 0
lrwxrwxrwx 1 root root 0 Jan 17 21:43 ipc -> ipc:[4026531839]
lrwxrwxrwx 1 root root 0 Jan 17 21:43 mnt -> mnt:[4026531840]
lrwxrwxrwx 1 root root 0 Jan 17 21:43 net -> net:[4026531956]
lrwxrwxrwx 1 root root 0 Jan 17 21:43 pid -> pid:[4026531836]
lrwxrwxrwx 1 root root 0 Jan 17 21:43 user -> user:[4026531837]
lrwxrwxrwx 1 root root 0 Jan 17 21:43 uts -> uts:[4026531838]
 
#这些 namespace 文件都是链接文件。链接文件的内容的格局为 xxx:[inode number]。其中的 xxx 为 namespace 的类型,inode number 则用来标识一个 namespace,咱们也能够把它了解为 namespace 的 ID。如果两个过程的某个 namespace 文件指向同一个链接文件,阐明其相干资源在同一个 namespace 中。以 ipc:[4026531839]例,ipc 是 namespace 的类型,4026531839 是 inode number,如果两个过程的 ipc namespace 的 inode number 一样,阐明他们属于同一个 namespace。这条规定对其余类型的 namespace 也同样实用。#从下面的输入能够看出,对于每种类型的 namespace,过程都会与一个 namespace ID 关联。#当一个 namespace 中的所有过程都退出时,该 namespace 将会被销毁。在 /proc/[pid]/ns 里搁置这些链接文件的作用就是,一旦这些链接文件被关上,只有关上的文件描述符 (fd) 存在,那么就算该 namespace 下的所有过程都完结了,但这个 namespace 也会始终存在,后续的过程还能够再退出进来。

2.1.4 相干命令及操作示例

本节会用 UTS/IPC/NET 3 个 Namespace 作为示例演示如何在 linux 零碎中创立 Namespace,并介绍相干命令。

2.1.4.1 IPC Namespace

IPC namespace 用来隔离 System V IPC objects 和 POSIX message queues。其中 System V IPC objects 蕴含音讯列表 Message queues、信号量 Semaphore sets 和共享内存 Shared memory segments。为了展示辨别 IPC Namespace 咱们这里会应用到 ipc 相干命令:

#    nsenter: 退出指定过程的指定类型的 namespace 中,而后执行参数中指定的命令。#       命令格局:nsenter [options] [program [arguments]]
#       示例:nsenter –t 27668 –u –I /bin/bash
#
#    unshare: 来到以后指定类型的 namespace, 创立且退出新的 namesapce, 而后执行参数中执行的命令。#       命令格局:unshare [options] program [arguments]
#       示例:unshare --fork --pid --mount-proc readlink /proc/self
#
#    ipcmk:创立 shared memory segments, message queues, 和 semaphore arrays
#       参数 -Q:创立 message queues
#    ipcs:查看 shared memory segments, message queues, 和 semaphore arrays 的相干信息
#      参数 -a:显示全副可显示的信息
#      参数 -q:显示流动的音讯队列信息

上面将以音讯队列为例,演示一下隔离成果,为了使演示更直观,咱们在创立新的 ipc namespace 的时候,同时也创立新的 uts namespace,而后为新的 uts namespace 设置新 hostname,这样就能通过 shell 提示符一眼看出这是属于新的 namespace 的 bash。示例中咱们用两个 shell 来展现:

shell A


#查看以后 shell 的 uts / ipc namespace number
 
# readlink /proc/$$/ns/uts /proc/$$/ns/ipc
uts:[4026531838]
ipc:[4026531839]
 
#查看以后主机名
# hostname
myCentos
 
#查看 ipc message queues, 默认状况下没有 message queue
# ipcs -q
 
------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages   
 
 
#创立一个 message queue
# ipcmk -Q
Message queue id: 131072
# ipcs -q
 
------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages   
0x82a1d963 131072     root       644        0            0 
 
-----> 切换至 shell B 执行
------------------------------------------------------------------
 
#回到 shell A 之后咱们能够看下 hostname、ipc 等有没有收到影响
# hostname
myCentos
 
# ipcs -q
 
------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages   
0x82a1d963 131072     root       644        0            0          
 
#接下来咱们尝试退出 shell B 中新的 Namespace
# nsenter -t 30372 -u -i /bin/bash
 
[root@shell-B:/root]
# hostname
shell-B
 
# readlink /proc/$$/ns/uts /proc/$$/ns/ipc
uts:[4026532382]
ipc:[4026532383]
 
# ipcs -q
 
------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages   
#能够看到咱们曾经胜利的退出到了新的 Namespace 中

shell B


#确认以后 shell 和 shell A 属于雷同 Namespace
 
# readlink /proc/$$/ns/uts /proc/$$/ns/ipc
uts:[4026531838]
ipc:[4026531839]
 
# ipcs -q
 
------ Message Queues --------
key msqid owner perms used-bytes messages
0x82a1d963 131072 root 644 0 0
 
#应用 unshare 创立新的 uts 和 ipc Namespace,并在新的 Namespace 中启动 bash
 
# unshare -iu /bin/bash
 
#确认新的 bash uts/ipc Namespace Number
 
# readlink /proc/$$/ns/uts /proc/$$/ns/ipc
uts:[4026532382]
ipc:[4026532383]
 
#设置新的 hostname 与 shell A 做辨别
 
# hostname shell-B
 
# hostname
shell-B
 
#查看之前的 ipc message queue
 
# ipcs -q
 
------ Message Queues --------
key msqid owner perms used-bytes messages
 
#查看以后 bash 过程的 PID
# echo $$
30372
 
切换回 shell A <-----
2.1.4.2 Net Namespace

Network namespace 用来隔离网络设备, IP 地址, 端口等. 每个 namespace 将会有本人独立的网络栈,路由表,防火墙规定,socket 等。每个新的 network namespace 默认有一个本地环回接口,除了 lo 接口外,所有的其余网络设备(物理 / 虚构网络接口,网桥等)只能属于一个 network namespace。每个 socket 也只能属于一个 network namespace。当新的 network namespace 被创立时,lo 接口默认是敞开的,须要本人手动启动起。标记为 ”local devices” 的设施不能从一个 namespace 挪动到另一个 namespace,比方 loopback, bridge, ppp 等,咱们能够通过 ethtool - k 命令来查看设施的 netns-local 属性。

咱们应用以下命令来创立 net namespace。

相干命令:ip netns: 管理网络 namespace
    用法:
       ip netns list
       ip netns add NAME
       ip netns set NAME NETNSID
       ip [-all] netns delete [NAME]

上面应用 ip netns 来演示创立 net Namespace。

shell A


#创立一对网卡,别离命名为 veth0_11/veth1_11
# ip link add veth0_11 type veth peer name veth1_11
 
#查看曾经创立的网卡
#ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1
    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
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000
    link/ether 5e:75:97:0d:54:17 brd ff:ff:ff:ff:ff:ff
    inet 192.168.1.1/24 brd 192.168.1.255 scope global eth0
       valid_lft forever preferred_lft forever
3: br1: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN qlen 1000
    link/ether 00:00:00:00:00:00 brd ff:ff:ff:ff:ff:ff
    inet 172.18.0.1/24 scope global br1
       valid_lft forever preferred_lft forever
96: veth1_11@veth0_11: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN qlen 1000
    link/ether 5e:75:97:0d:54:0e brd ff:ff:ff:ff:ff:ff
97: veth0_11@veth1_11: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN qlen 1000
    link/ether a6:c7:1f:79:a6:a6 brd ff:ff:ff:ff:ff:ff
 
#应用 ip netns 创立两个 net namespace
# ip netns add r1
# ip netns add r2
# ip netns list
r2
r1 (id: 0)
 
#将两个网卡别离退出到对应的 netns 中
# ip link set veth0_11 netns r1
# ip link set veth1_11 netns r2
#再次查看网卡,在 bash 以后的 namespace 中曾经看不到 veth0_11 和 veth1_11 了
# ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1
    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
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000
    link/ether 5e:75:97:0d:54:17 brd ff:ff:ff:ff:ff:ff
    inet 192.168.1.1/24 brd 192.168.1.255 scope global eth0
       valid_lft forever preferred_lft forever
3: br1: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN qlen 1000
    link/ether 00:00:00:00:00:00 brd ff:ff:ff:ff:ff:ff
    inet 172.18.0.1/24 scope global br1
       valid_lft forever preferred_lft forever
 
#接下来咱们切换到对应的 netns 中对网卡进行配置
#通过 nsenter --net 能够切换到对应的 netns 中,ip a 展现了咱们下面退出到 r1 中的网卡
# nsenter --net=/var/run/netns/r1 /bin/bash
# ip a
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
97: veth0_11@if96: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN qlen 1000
    link/ether a6:c7:1f:79:a6:a6 brd ff:ff:ff:ff:ff:ff link-netnsid 1
 
#对网卡配置 ip 并启动
# ip addr add 172.18.0.11/24 dev veth0_11
# ip link set veth0_11 up
# ip a
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
97: veth0_11@if96: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state LOWERLAYERDOWN qlen 1000
    link/ether a6:c7:1f:79:a6:a6 brd ff:ff:ff:ff:ff:ff link-netnsid 1
    inet 172.18.0.11/24 scope global veth0_11
       valid_lft forever preferred_lft forever
 
-----> 切换至 shell B 执行
------------------------------------------------------------------
 
#在 r1 中 ping veth1_11
# ping 172.18.0.12
PING 172.18.0.12 (172.18.0.12) 56(84) bytes of data.
64 bytes from 172.18.0.12: icmp_seq=1 ttl=64 time=0.033 ms
64 bytes from 172.18.0.12: icmp_seq=2 ttl=64 time=0.049 ms
...
#至此咱们通过 netns 实现了创立 net Namespace 的小试验

shell B


#在 shell B 中咱们同样切换到 netns r2 中进行配置
#通过 nsenter --net 能够切换到 r2,ip a 展现了咱们下面退出到 r2 中的网卡
# nsenter --net=/var/run/netns/r2 /bin/bash
#  ip a
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
96: veth1_11@if97: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN qlen 1000
    link/ether 5e:75:97:0d:54:0e brd ff:ff:ff:ff:ff:ff link-netnsid 0
 
#对网卡配置 ip 并启动
# ip addr add 172.18.0.12/24 dev veth1_11
# ip link set veth1_11 up
# ip a
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
96: veth1_11@if97: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP qlen 1000
    link/ether 5e:75:97:0d:54:0e brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 172.18.0.12/24 scope global veth1_11
       valid_lft forever preferred_lft forever
    inet6 fe80::5c75:97ff:fe0d:540e/64 scope link
       valid_lft forever preferred_lft forever
 
#尝试 ping r1 中的网卡
# ping 172.18.0.11
PING 172.18.0.11 (172.18.0.11) 56(84) bytes of data.
64 bytes from 172.18.0.11: icmp_seq=1 ttl=64 time=0.046 ms
64 bytes from 172.18.0.11: icmp_seq=2 ttl=64 time=0.040 ms
...
#能够实现通信
 
切换至 shell A 执行 <-----

示意图

2.2 Cgroup

2.2.1 简介

Cgroup 和 namespace 相似,也是将过程进行分组,但它的目标和 namespace 不一样,namespace 是为了隔离过程组之间的资源,而 cgroup 是为了对一组过程进行对立的资源监控和限度。

Cgroup 作用:

  1. 资源限度(Resource limiting): Cgroups 能够对过程组应用的资源总额进行限度。如对特定的过程进行内存应用下限限度,当超出下限时,会触发 OOM。
  2. 优先级调配(Prioritization): 通过调配的 CPU 工夫片数量及硬盘 IO 带宽大小,实际上就相当于管制了过程运行的优先级。
  3. 资源统计(Accounting): Cgroups 能够统计零碎的资源使用量,如 CPU 应用时长、内存用量等等,这个性能十分实用于计费。
  4. 过程管制(Control):Cgroups 能够对过程组执行挂起、复原等操作。

Cgroups 的组成:

  1. task: 在 Cgroups 中,task 就是零碎的一个过程。
  2. cgroup: Cgroups 中的资源管制都以 cgroup 为单位实现的。cgroup 示意依照某种资源管制规范划分而成的工作组,蕴含一个或多个子系统。一个工作能够退出某个 cgroup,也能够从某个 cgroup 迁徙到另外一个 cgroup。
  3. subsystem: 一个 subsystem 就是一个内核模块,被关联到一颗 cgroup 树之后,就会在树的每个节点(过程组)上做具体的操作。subsystem 常常被称作 ”resource controller”,因为它次要被用来调度或者限度每个过程组的资源,然而这个说法不齐全精确,因为有时咱们将过程分组只是为了做一些监控,察看一下他们的状态,比方 perf_event subsystem。到目前为止,Linux 反对 13 种 subsystem(Cgroup v1),比方限度 CPU 的应用工夫,限度应用的内存,统计 CPU 的应用状况,解冻和复原一组过程等。
  4. hierarchy: 一个 hierarchy 能够了解为一棵 cgroup 树,树的每个节点就是一个过程组,每棵树都会与零到多个 subsystem 关联。在一颗树外面,会蕴含 Linux 零碎中的所有过程,但每个过程只能属于一个节点(过程组)。零碎中能够有很多颗 cgroup 树,每棵树都和不同的 subsystem 关联,一个过程能够属于多颗树,即一个过程能够属于多个过程组,只是这些过程组和不同的 subsystem 关联。如果不思考不与任何 subsystem 关联的状况(systemd 就属于这种状况),Linux 外面最多能够建 13 颗 cgroup 树,每棵树关联一个 subsystem,当然也能够只建一棵树,而后让这棵树关联所有的 subsystem。当一颗 cgroup 树不和任何 subsystem 关联的时候,意味着这棵树只是将过程进行分组,至于要在分组的根底上做些什么,将由应用程序本人决定,systemd 就是一个这样的例子。

2.2.2 查看 Cgroup 信息

查看以后零碎反对的 subsystem

# 通过 /proc/cgroups 查看以后零碎反对哪些 subsystem
# cat /proc/cgroups
#subsys_name    hierarchy       num_cgroups     enabled
cpuset              11              1           1
cpu                 4               67          1
cpuacct             4               67          1
memory              5               69          1
devices             7               62          1
freezer             8               1           1
net_cls             6               1           1
blkio               9               62          1
perf_event          3               1           1
hugetlb             2               1           1
pids                10              62          1
net_prio            6               1           1
 
#字段含意
#subsys_name: subsystem 的名称
#hierarchy:subsystem 所关联到的 cgroup 树的 ID,如果多个 subsystem 关联到同一颗 cgroup 树,那么他们的这个字段将一样,比方这里的 cpu 和 cpuacct 就一样,示意他们绑定到了同一颗树。如果呈现上面的状况,这个字段将为 0:以后 subsystem 没有和任何 cgroup 树绑定
        以后 subsystem 曾经和 cgroup v2 的树绑定
        以后 subsystem 没有被内核开启
#num_cgroups: subsystem 所关联的 cgroup 树中过程组的个数,也即树上节点的个数
#enabled: 1 示意开启,0 示意没有被开启(能够通过设置内核的启动参数“cgroup_disable”来管制 subsystem 的开启).

查看过程所属 cgroup

# 查看以后 shell 过程所属的 cgroup
# cat /proc/$$/cgroup
11:cpuset:/
10:pids:/system.slice/sshd.service
9:blkio:/system.slice/sshd.service
8:freezer:/
7:devices:/system.slice/sshd.service
6:net_prio,net_cls:/
5:memory:/system.slice/sshd.service
4:cpuacct,cpu:/system.slice/sshd.service
3:perf_event:/
2:hugetlb:/
1:name=systemd:/system.slice/sshd.service
 
#字段含意(以冒号分为 3 列):# 1. cgroup 树 ID,对应 /proc/cgroups 中的 hierachy
# 2. cgroup 所绑定的 subsystem, 多个 subsystem 应用逗号分隔。name=systemd 示意没有和任何 subsystem 绑定,只是给他起了个名字叫 systemd。# 3. 过程在 cgroup 树中的门路,即过程所属的 cgroup,这个门路是绝对于挂载点的相对路径。

2.2.3 相干命令

应用 cgroup

cgroup 相干的所有操作都是基于内核中的 cgroup virtual filesystem,应用 cgroup 很简略,挂载这个文件系统就能够了。个别状况下都是挂载到 /sys/fs/cgroup 目录下,当然挂载到其它任何目录都没关系。

查看下以后零碎 cgroup 挂载状况。

# 过滤零碎挂载能够查看 cgroup
# mount |grep cgroup
tmpfs on /sys/fs/cgroup type tmpfs (ro,nosuid,nodev,noexec,mode=755)
cgroup on /sys/fs/cgroup/systemd type cgroup (rw,nosuid,nodev,noexec,relatime,xattr,release_agent=/usr/lib/systemd/systemd-cgroups-agent,name=systemd)
cgroup on /sys/fs/cgroup/hugetlb type cgroup (rw,nosuid,nodev,noexec,relatime,hugetlb)
cgroup on /sys/fs/cgroup/perf_event type cgroup (rw,nosuid,nodev,noexec,relatime,perf_event)
cgroup on /sys/fs/cgroup/cpu,cpuacct type cgroup (rw,nosuid,nodev,noexec,relatime,cpuacct,cpu)
cgroup on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,memory)
cgroup on /sys/fs/cgroup/net_cls,net_prio type cgroup (rw,nosuid,nodev,noexec,relatime,net_prio,net_cls)
cgroup on /sys/fs/cgroup/devices type cgroup (rw,nosuid,nodev,noexec,relatime,devices)
cgroup on /sys/fs/cgroup/freezer type cgroup (rw,nosuid,nodev,noexec,relatime,freezer)
cgroup on /sys/fs/cgroup/blkio type cgroup (rw,nosuid,nodev,noexec,relatime,blkio)
cgroup on /sys/fs/cgroup/pids type cgroup (rw,nosuid,nodev,noexec,relatime,pids)
cgroup on /sys/fs/cgroup/cpuset type cgroup (rw,nosuid,nodev,noexec,relatime,cpuset)
 
#如果零碎中没有挂载 cgroup,能够应用 mount 命令创立 cgroup
#挂载根 cgroup
# mkdir /sys/fs/cgroup
# mount -t tmpfs cgroup_root /sys/fs/cgroup
 
#将 cpuset subsystem 关联到 /sys/fs/cgroup/cpu_memory
# mkdir /sys/fs/cgroup/cpuset
# sudo mount -t cgroup cpuset -o cgroup /sys/fs/cgroup/cpuset/
 
#将 cpu 和 memory subsystem 关联到 /sys/fs/cgroup/cpu_memory
# mkdir /sys/fs/cgroup/cpu_memory
# sudo mount -n -t cgroup -o cpu,memory cgroup /sys/fs/cgroup/cpu_memory

除了 mount 命令之外咱们还能够应用以下命令对 cgroup 进行创立、属性设置等操作,这也是咱们前面脚本中用于创立和治理 cgroup 的命令。

# Centos 操作系统能够通过 yum install cgroup-tools 来装置以下命令
    cgcreate: 在层级中创立新 cgroup。用法: cgcreate [-h] [-t <tuid>:<tgid>] [-a <agid>:<auid>] [-f mode] [-d mode] [-s mode]
                -g <controllers>:<path> [-g ...]
        示例: cgcreate -g *:student -g devices:teacher // 在所有的挂载 hierarchy 中创立 student cgroup, 在 devices   
             hierarchy 挂载点创立 teacher cgroup
         
    cgset: 设置指定 cgroup(s)的参数
        用法: cgset [-r <name=value>] <cgroup_path> ...
        示例: cgset -r cpuset.cpus=0-1 student // 将 student cgroup 的 cpuset 控制器中的 cpus 限度为 0 -1
         
    cgexec: 在指定的 cgroup 中运行工作
        用法: cgexec [-h] [-g <controllers>:<path>] [--sticky] command [arguments]
        示例: cgexec -g cpu,memory:test1 ls -l // 在 cpu 和 memory 控制器下的 test1 cgroup 中运行 ls - l 命令

2.3 Rootfs

2.3.1 简介

Rootfs 是 Docker 容器在启动时外部过程可见的文件系统,即 Docker 容器的根目录。rootfs 通常蕴含一个操作系统运行所需的文件系统,例如可能蕴含典型的类 Unix 操作系统中的目录零碎,如 /dev、/proc、/bin、/etc、/lib、/usr、/tmp 及运行 Docker 容器所需的配置文件、工具等。

就像 Linux 启动会先用只读模式挂载 rootfs,运行完完整性检查之后,再切换成读写模式一样。Docker deamon 为 container 挂载 rootfs 时,也会先挂载为只读模式,然而与 Linux 做法不同的是,在挂载完只读的 rootfs 之后,Docker deamon 会利用联结挂载技术(Union Mount)在已有的 rootfs 上再挂一个读写层。container 在运行过程中文件系统产生的变动只会写到读写层,并通过 whiteout 技术暗藏只读层中的旧版本文件。

Docker 反对不同的存储驱动,包含 aufs、devicemapper、overlay2、zfs 和 vfs 等,目前在 Docker 中,overlay2 取代了 aufs 成为了举荐的存储驱动。

2.3.2 overlayfs

overlayFS 是联结挂载技术的一种实现。除了 overlayFS 以外还有 aufs,VFS,Brtfs,device mapper 等技术。尽管实现细节不同,然而他们做的事件都是雷同的。Linux 内核为 Docker 提供的 overalyFS 驱动有 2 种:overlay2 和 overlay,overlay2 是绝对于 overlay 的一种改良,在 inode 利用率方面比 overlay 更无效。

overlayfs 通过三个目录来实现:lower 目录、upper 目录、以及 work 目录。三种目录合并进去的目录称为 merged 目录。

  • lower:能够是多个,是处于最底层的目录,作为只读层。
  • upper:只有一个,作为读写层。
  • work:为工作根底目录,挂载后内容会被清空,且在应用过程中其内容用户不可见。
  • merged:为最初联结挂载实现给用户出现的对立视图,也就是说 merged 目录外面自身并没有任何实体文件,给咱们展现的只是参加联结挂载的目录外面文件而已,真正的文件还是在 lower 和 upper 中。所以,在 merged 目录下编辑文件,或者间接编辑 lower 或 upper 目录外面的文件都会影响到 merged 外面的视图展现。

2.3.3 文件规定

merged 层目录会显示离它最近层的文件。层级关系中 upperdir 比 lowerdir 更凑近 merged 层,而多个 lowerdir 的状况下,写的越靠前的目录离 merged 层目录越近。雷同文件名的文件会按照层级规定进行“笼罩”。

2.3.4 overlayFS 如何工作

读:

  • 如果文件在容器层(upperdir),间接读取文件;
  • 如果文件不在容器层(upperdir),则从镜像层(lowerdir)读取;

写:

  • ①首次写入:如果在 upperdir 中不存在,overlay 执行 cow 操作,把文件从 lowdir 拷贝到 upperdir,因为 overlayfs 是文件级别的(即便文件只有很少的一点批改,也会产生的 cow 的行为),后续对同一文件的在此写入操作将对曾经复制到容器的文件的正本进行操作。值得注意的是,cow 操作只产生在文件首次写入,当前都是只批改正本。
  • ②删除文件和目录:当文件在容器被删除时,在容器层(upperdir)创立 whiteout 文件,镜像层 (lowerdir) 的文件是不会被删除的,因为他们是只读的,但 whiteout 文件会阻止他们显示。

2.3.5 在零碎里创立 overlayfs

shell


# 创立所需的目录
# mkdir upper lower merged work
# echo "lower" > lower/in_lower.txt
# echo "upper" > upper/in_upper.txt
 
# 在 lower 和 upper 中都创立 in_both 文件
# echo "lower" > lower/in_both.txt
# echo "upper" > upper/in_both.txt
 
#查看下咱们以后的目录及文件构造
# tree .
.
|-- lower
|   |-- in_both.txt
|   `-- in_lower.txt
|-- merged
|-- upper
|   |-- in_both.txt
|   `-- in_upper.txt
`-- work
 
#应用 mount 命令将创立的目录联结挂载起来
# mount -t overlay overlay -o lowerdir=lower,upperdir=upper,workdir=work merged
 
#查看 mount 后果能够看到曾经胜利挂载了
# mount |grep overlay
overlay on /data/overlay_demo/merged type overlay (rw,relatime,lowerdir=lower,upperdir=upper,workdir=work)
 
#此时再查看文件目录构造
# tree .
.
|-- lower
|   |-- in_both.txt
|   `-- in_lower.txt
|-- merged
|   |-- in_both.txt
|   |-- in_lower.txt
|   `-- in_upper.txt
|-- upper
|   |-- in_both.txt
|   `-- in_upper.txt
`-- work
    `-- work
#能够看到 merged 中蕴含了 lower 和 upper 中的文件
#而后我查看 merge 中的 in_both 文件, 验证了下层目录笼罩上层的论断
# cat merged/in_both.txt
upper
 
#下面咱们验证了挂载后 overlayfs 的读,接下来咱们去验证下写
#咱们在 merged 中创立一个新文件,并查看
# touch merged/new_file
# tree .
.
|-- lower
|   |-- in_both.txt
|   `-- in_lower.txt
|-- merged
|   |-- in_both.txt
|   |-- in_lower.txt
|   |-- in_upper.txt
|   `-- new_file
|-- upper
|   |-- in_both.txt
|   |-- in_upper.txt
|   `-- new_file
`-- work
    `-- work
#能够看到新文件理论是放在了 upper 目录中
 
#上面咱们看下如果删除了 lower 和 upper 中都有的文件会怎么
# rm -f merged/in_both.txt
# tree .
.
|-- lower
|   |-- in_both.txt
|   `-- in_lower.txt
|-- merged
|   |-- in_lower.txt
|   |-- in_upper.txt
|   `-- new_file
|-- upper
|   |-- in_both.txt
|   |-- in_upper.txt
|   `-- new_file
`-- work
    `-- work
 
#从文件目录上看只有 merge 中没有了 in_both 文件,然而 upper 中的文件曾经产生了变动
# ll upper/in_both.txt
c--------- 1 root root 0, 0 Jan 21 19:33 upper/in_both.txt
#upper/in_both.txt 曾经变成了一个空的字符文件,且笼罩了 lower 层的内容

三、Bocker

3.1 性能演示

第二局部中咱们对 Namespace,cgroup,overlayfs 有了肯定的理解,接下来咱们通过一个脚本来实现个倡议的 Docker。脚本源自于 https://github.com/p8952/bocker,我做了 image/pull/ 存储驱动的局部批改,上面先看下脚本实现后的示例:

3.2 残缺脚本

脚本一共用 130 行代码,实现了下面的性能,也算合乎咱们此次的题目了。为了大家能够更深刻的了解脚本内容,这里就不再对脚本进行拆分解说,以下是残缺脚本。


#!/usr/bin/env bash
set -o errexit -o nounset -o pipefail; shopt -s nullglob
overlay_path='/var/lib/bocker/overlay' && container_path='/var/lib/bocker/containers' && cgroups='cpu,cpuacct,memory';
[[$# -gt 0]] && while ["${1:0:2}" == '--' ]; do OPTION=${1:2}; [[$OPTION =~ =]] && declare "BOCKER_${OPTION/=*/}=${OPTION/*=/}" || declare "BOCKER_${OPTION}=x"; shift; done
 
function bocker_check() {case ${1:0:3} in
        img) ls "$overlay_path" | grep -qw "$1" && echo 0 || echo 1;;
        ps_) ls "$container_path" | grep -qw "$1" && echo 2 || echo 3;;
    esac
}
 
function bocker_init() { #HELP Create an image from a directory:\nBOCKER init <directory>
    uuid="img_$(shuf -i 42002-42254 -n 1)"
    if [[-d "$1"]]; then
        [["$(bocker_check"$uuid")" == 0 ]] && bocker_run "$@"
        mkdir "$overlay_path/$uuid" > /dev/null
        cp -rf --reflink=auto "$1"/* "$overlay_path/$uuid" > /dev/null
        [[! -f "$overlay_path/$uuid"/img.source]] && echo "$1" > "$overlay_path/$uuid"/img.source
        [[! -d "$overlay_path/$uuid"/proc]] && mkdir "$overlay_path/$uuid"/proc
        echo "Created: $uuid"
    else
        echo "No directory named'$1'exists"
    fi
}
 
function bocker_pull() { #HELP Pull an image from Docker Hub:\nBOCKER pull <name> <tag>
    tmp_uuid="$(uuidgen)" && mkdir /tmp/"$tmp_uuid"
    download-frozen-image-v2 /tmp/"$tmp_uuid" "$1:$2" > /dev/null
    rm -rf /tmp/"$tmp_uuid"/repositories
    for tar in $(jq '.[].Layers[]' --raw-output < /tmp/$tmp_uuid/manifest.json); do
        tar xf /tmp/$tmp_uuid/$tar -C /tmp/$tmp_uuid && rm -rf /tmp/$tmp_uuid/$tar
    done
    for config in $(jq '.[].Config' --raw-output < /tmp/$tmp_uuid/manifest.json); do
        rm -f /tmp/$tmp_uuid/$config
    done
    echo "$1:$2" > /tmp/$tmp_uuid/img.source
    bocker_init /tmp/$tmp_uuid && rm -rf /tmp/$tmp_uuid
}
 
function bocker_rm() { #HELP Delete an image or container:\nBOCKER rm <image_id or container_id>
    [["$(bocker_check"$1")" == 3 ]] && echo "No container named'$1'exists" && exit 1
    [["$(bocker_check"$1")" == 1 ]] && echo "No image named'$1'exists" && exit 1
    if [[-d "$overlay_path/$1"]];then
        rm -rf "$overlay_path/$1" && echo "Removed: $1"
    else
        umount "$container_path/$1"/merged && rm -rf "$container_path/$1" && ip netns del netns_"$1" && ip link del dev veth0_"$1" && echo "Removed: $1"
        cgdelete -g "$cgroups:/$1" &> /dev/null
    fi
     
}
 
function bocker_images() { #HELP List images:\nBOCKER images
    echo -e "IMAGE_ID\t\tSOURCE"
    for img in "$overlay_path"/img_*; do
        img=$(basename "$img")
        echo -e "$img\t\t$(cat"$overlay_path/$img/img.source")"
    done
}
 
function bocker_ps() { #HELP List containers:\nBOCKER ps
    echo -e "CONTAINER_ID\t\tCOMMAND"
    for ps in "$container_path"/ps_*; do
        ps=$(basename "$ps")
        echo -e "$ps\t\t$(cat"$container_path/$ps/$ps.cmd")"
    done
}
 
function bocker_run() { #HELP Create a container:\nBOCKER run <image_id> <command>
    uuid="ps_$(shuf -i 42002-42254 -n 1)"
    [["$(bocker_check"$1")" == 1 ]] && echo "No image named'$1'exists" && exit 1
    [["$(bocker_check"$uuid")" == 2 ]] && echo "UUID conflict, retrying..." && bocker_run "$@" && return
    cmd="${@:2}" && ip="$(echo"${uuid: -3}"| sed's/0//g')" && mac="${uuid: -3:1}:${uuid: -2}"
    ip link add dev veth0_"$uuid" type veth peer name veth1_"$uuid"
    ip link set dev veth0_"$uuid" up
    ip link set veth0_"$uuid" master br1
    ip netns add netns_"$uuid"
    ip link set veth1_"$uuid" netns netns_"$uuid"
    ip netns exec netns_"$uuid" ip link set dev lo up
    ip netns exec netns_"$uuid" ip link set veth1_"$uuid" address 02:42:ac:11:00"$mac"
    ip netns exec netns_"$uuid" ip addr add 172.18.0."$ip"/24 dev veth1_"$uuid"
    ip netns exec netns_"$uuid" ip link set dev veth1_"$uuid" up
    ip netns exec netns_"$uuid" ip route add default via 172.18.0.1
    mkdir -p "$container_path/$uuid"/{lower,upper,work,merged} && cp -rf --reflink=auto "$overlay_path/$1"/* "$container_path/$uuid"/lower > /dev/null && \
    mount -t overlay overlay \
        -o lowerdir="$container_path/$uuid"/lower,upperdir="$container_path/$uuid"/upper,workdir="$container_path/$uuid"/work \
        "$container_path/$uuid"/merged
    echo 'nameserver 114.114.114.114' > "$container_path/$uuid"/merged/etc/resolv.conf
    echo "$cmd" > "$container_path/$uuid/$uuid.cmd"
    cgcreate -g "$cgroups:/$uuid"
    : "${BOCKER_CPU_SHARE:=512}" && cgset -r cpu.shares="$BOCKER_CPU_SHARE" "$uuid"
    : "${BOCKER_MEM_LIMIT:=512}" && cgset -r memory.limit_in_bytes="$((BOCKER_MEM_LIMIT * 1000000))" "$uuid"
    cgexec -g "$cgroups:$uuid" \
        ip netns exec netns_"$uuid" \
        unshare -fmuip --mount-proc \
        chroot "$container_path/$uuid"/merged \
        /bin/sh -c "/bin/mount -t proc proc /proc && $cmd" \
        2>&1 | tee "$container_path/$uuid/$uuid.log" || true
    ip link del dev veth0_"$uuid"
    ip netns del netns_"$uuid"
}
 
function bocker_exec() { #HELP Execute a command in a running container:\nBOCKER exec <container_id> <command>
    [["$(bocker_check"$1")" == 3 ]] && echo "No container named'$1'exists" && exit 1
    cid="$(ps o ppid,pid | grep"^$(ps o pid,cmd | grep -E "^\ *[0-9]+ unshare.*$1" | awk '{print $1}')"| awk'{print $2}')"
    [[! "$cid" =~ ^\ *[0-9]+$ ]] && echo "Container'$1'exists but is not running" && exit 1
    nsenter -t "$cid" -m -u -i -n -p chroot "$container_path/$1"/merged "${@:2}"
}
 
function bocker_logs() { #HELP View logs from a container:\nBOCKER logs <container_id>
    [["$(bocker_check"$1")" == 3 ]] && echo "No container named'$1'exists" && exit 1
    cat "$container_path/$1/$1.log"
}
 
function bocker_commit() { #HELP Commit a container to an image:\nBOCKER commit <container_id> <image_id>
    [["$(bocker_check"$1")" == 3 ]] && echo "No container named'$1'exists" && exit 1
    [["$(bocker_check"$2")" == 0 ]] && echo "Image named'$2'exists" && exit 1
    mkdir "$overlay_path/$2" && cp -rf --reflink=auto "$container_path/$1"/merged/* "$overlay_path/$2" && sed -i "s/:.*$/:$(date +%Y%m%d-%H%M%S)/g" "$overlay_path/$2"/img.source
    echo "Created: $2"
}
 
function bocker_help() { #HELP Display this message:\nBOCKER help
    sed -n "s/^.*#HELP\\s//p;" < "$1" | sed "s/\\\\n/\n\t/g;s/$/\n/;s!BOCKER!${1/!/\\!}!g"
}
 
[[-z "${1-}" ]] && bocker_help "$0" && exit 1
case $1 in
    pull|init|rm|images|ps|run|exec|logs|commit) bocker_"$1" "${@:2}" ;;
    *) bocker_help "$0" ;;
esac
README

Bocker

  • 应用 100 行 bash 实现一个 docker,本脚本是根据 bocker 实现,更换了存储驱动,欠缺了 pull 等性能。

前置条件

为了脚本可能失常运行,机器上须要具备以下组件:

  • overlayfs
  • iproute2
  • iptables
  • libcgroup-tools
  • util-linux >= 2.25.2
  • coreutils >= 7.5

大部分性能在 centos7 上都是满足的,overlayfs 能够通过 modprobe overlay 挂载。

另外你可能还要做以下设置:

  • 创立 bocker 运行目录 /var/lib/bocker/overlay,/var/lib/bocker/containers
  • 创立一个 IP 地址为 172.18.0.1/24 的桥接网卡 br1
  • 确认开启 IP 转发 /proc/sys/net/ipv4/ip_forward = 1
  • 创立 iptables 规定将桥接网络流量转发至物理网卡,示例:iptables -t nat -A POSTROUTING -s 172.18.0.0/24 -o eth0 -j MASQUERADE

实现的性能

  • docker build +
  • docker pull
  • docker images
  • docker ps
  • docker run
  • docker exec
  • docker logs
  • docker commit
  • docker rm / docker rmi
  • Networking
  • Quota Support / CGroups
  • +bocker init 提供了无限的 bocker build 能力

四、总结

到此本文要介绍的内容就完结了,正如开篇咱们提到的,写出最终的脚本实现这样一个小玩意并没有什么实用价值,真正的价值是咱们通过 100 行左右的脚本,以交互式的形式去了解 Docker 的核心技术点。在工作中与容器打交道时能有更多的思路去排查、解决问题。

正文完
 0