作者: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有如下品种:

100 行 shell 写个 Docker

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履行 <-----

示意图

100 行 shell 写个 Docker

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里边的视图展现。

100 行 shell 写个 Docker

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
# 在lowerupper中都创立 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中包括了lowerupper中的文件
#然后我检查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目录中
#下面咱们看下假如删去了lowerupper中都有的文件会怎样
# 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。脚本源自于github.com/p8952/bocke…

100 行 shell 写个 Docker

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的核心技能点。在作业中与容器打交道时能有更多的思路去排查、解决问题。