|
作者: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 <-----
复制代码shell B
- 相关命令:
- ip netns: 管理网络namespace
- 用法:
- ip netns list
- ip netns add NAME
- ip netns set NAME NETNSID
- ip [-all] netns delete [NAME]
复制代码 三 、Bocker
3.1 功能演示
第二部分中我们对Namespace,cgroup,overlayfs有了一定的了解,接下来我们通过一个脚本来实现个建议的Docker。脚本源自于https://github.com/p8952/bocker,我做了image/pull/存储驱动的部分修改,下面先看下脚本完成后的示例:
3.2 完整脚本
脚本一共用130行代码,完成了上面的功能,也算符合我们此次的标题了。为了大家可以更深入的理解脚本内容,这里就不再对脚本进行拆分讲解,以下是完整脚本。- #创建一对网卡,分别命名为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中我们同样切换到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执行 <-----
复制代码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的核心技术点。在工作中与容器打交道时能有更多的思路去排查、解决问题。
来源:https://www.cnblogs.com/vivotech/p/17090111.html
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作! |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
x
|