翼度科技»论坛 云主机 LINUX 查看内容

100 行 shell 写个 Docker

5

主题

5

帖子

15

积分

新手上路

Rank: 1

积分
15
作者: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参数。
  1. #查看当前bash进程关联的Namespace
  2. # ls -l /proc/$$/ns
  3. total 0
  4. lrwxrwxrwx 1 root root 0 Jan 17 21:43 ipc -> ipc:[4026531839]
  5. lrwxrwxrwx 1 root root 0 Jan 17 21:43 mnt -> mnt:[4026531840]
  6. lrwxrwxrwx 1 root root 0 Jan 17 21:43 net -> net:[4026531956]
  7. lrwxrwxrwx 1 root root 0 Jan 17 21:43 pid -> pid:[4026531836]
  8. lrwxrwxrwx 1 root root 0 Jan 17 21:43 user -> user:[4026531837]
  9. lrwxrwxrwx 1 root root 0 Jan 17 21:43 uts -> uts:[4026531838]
  10. #这些 namespace 文件都是链接文件。链接文件的内容的格式为 xxx:[inode number]。
  11.     其中的 xxx 为 namespace 的类型,inode number 则用来标识一个 namespace,我们也可以把它理解为 namespace 的 ID。
  12.     如果两个进程的某个 namespace 文件指向同一个链接文件,说明其相关资源在同一个 namespace 中。以ipc:[4026531839]例,
  13.     ipc是namespace的类型,4026531839是inode number,如果两个进程的ipc namespace的inode number一样,说明他们属于同一个namespace。
  14.     这条规则对其他类型的namespace也同样适用。
  15. #从上面的输出可以看出,对于每种类型的namespace,进程都会与一个namespace ID关联。
  16. #当一个namespace中的所有进程都退出时,该namespace将会被销毁。在 /proc/[pid]/ns 里放置这些链接文件的作用就是,一旦这些链接文件被打开,
  17.     只要打开的文件描述符(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相关命令:
  1. #    nsenter: 加入指定进程的指定类型的namespace中,然后执行参数中指定的命令。
  2. #       命令格式:nsenter [options] [program [arguments]]
  3. #       示例:nsenter –t 27668 –u –I /bin/bash
  4. #
  5. #    unshare: 离开当前指定类型的namespace,创建且加入新的namesapce,然后执行参数中执行的命令。
  6. #       命令格式:unshare [options] program [arguments]
  7. #       示例:unshare --fork --pid --mount-proc readlink /proc/self
  8. #
  9. #    ipcmk:创建shared memory segments, message queues, 和semaphore arrays
  10. #       参数-Q:创建message queues
  11. #    ipcs:查看shared memory segments, message queues, 和semaphore arrays的相关信息
  12. #      参数-a:显示全部可显示的信息
  13. #      参数-q:显示活动的消息队列信息
复制代码
下面将以消息队列为例,演示一下隔离效果,为了使演示更直观,我们在创建新的ipc namespace的时候,同时也创建新的uts namespace,然后为新的uts namespace设置新hostname,这样就能通过shell提示符一眼看出这是属于新的namespace的bash。示例中我们用两个shell来展示:
shell A
 
  1. #查看当前shell的uts / ipc namespace number
  2. # readlink /proc/$$/ns/uts /proc/$$/ns/ipc
  3. uts:[4026531838]
  4. ipc:[4026531839]
  5. #查看当前主机名
  6. # hostname
  7. myCentos
  8. #查看ipc message queues,默认情况下没有message queue
  9. # ipcs -q
  10. ------ Message Queues --------
  11. key        msqid      owner      perms      used-bytes   messages   
  12. #创建一个message queue
  13. # ipcmk -Q
  14. Message queue id: 131072
  15. # ipcs -q
  16. ------ Message Queues --------
  17. key        msqid      owner      perms      used-bytes   messages   
  18. 0x82a1d963 131072     root       644        0            0
  19. -----> 切换至shell B执行
  20. ------------------------------------------------------------------
  21. #回到shell A之后我们可以看下hostname、ipc等有没有收到影响
  22. # hostname
  23. myCentos
  24. # ipcs -q
  25. ------ Message Queues --------
  26. key        msqid      owner      perms      used-bytes   messages   
  27. 0x82a1d963 131072     root       644        0            0         
  28. #接下来我们尝试加入shell B中新的Namespace
  29. # nsenter -t 30372 -u -i /bin/bash
  30. [root@shell-B:/root]
  31. # hostname
  32. shell-B
  33. # readlink /proc/$$/ns/uts /proc/$$/ns/ipc
  34. uts:[4026532382]
  35. ipc:[4026532383]
  36. # ipcs -q
  37. ------ Message Queues --------
  38. key        msqid      owner      perms      used-bytes   messages   
  39. #可以看到我们已经成功的加入到了新的Namespace中
复制代码
shell B
  1. #确认当前shell和shell A属于相同Namespace
  2. # readlink /proc/$$/ns/uts /proc/$$/ns/ipc
  3. uts:[4026531838]
  4. ipc:[4026531839]
  5. # ipcs -q
  6. ------ Message Queues --------
  7. key msqid owner perms used-bytes messages
  8. 0x82a1d963 131072 root 644 0 0
  9. #使用unshare创建新的uts和ipc Namespace,并在新的Namespace中启动bash
  10. # unshare -iu /bin/bash
  11. #确认新的bash uts/ipc Namespace Number
  12. # readlink /proc/$$/ns/uts /proc/$$/ns/ipc
  13. uts:[4026532382]
  14. ipc:[4026532383]
  15. #设置新的hostname与shell A做区分
  16. # hostname shell-B
  17. # hostname
  18. shell-B
  19. #查看之前的ipc message queue
  20. # ipcs -q
  21. ------ Message Queues --------
  22. key msqid owner perms used-bytes messages
  23. #查看当前bash进程的PID
  24. # echo $$
  25. 30372
  26. 切换回shell A <-----
复制代码
shell B
  1. 相关命令:
  2.     ip netns: 管理网络namespace
  3.     用法:
  4.        ip netns list
  5.        ip netns add NAME
  6.        ip netns set NAME NETNSID
  7.        ip [-all] netns delete [NAME]
复制代码
三 、Bocker

3.1 功能演示

第二部分中我们对Namespace,cgroup,overlayfs有了一定的了解,接下来我们通过一个脚本来实现个建议的Docker。脚本源自于https://github.com/p8952/bocker,我做了image/pull/存储驱动的部分修改,下面先看下脚本完成后的示例:
3.2 完整脚本

脚本一共用130行代码,完成了上面的功能,也算符合我们此次的标题了。为了大家可以更深入的理解脚本内容,这里就不再对脚本进行拆分讲解,以下是完整脚本。
  1. #创建一对网卡,分别命名为veth0_11/veth1_11
  2. # ip link add veth0_11 type veth peer name veth1_11
  3. #查看已经创建的网卡
  4. #ip a
  5. 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1
  6.     link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
  7.     inet 127.0.0.1/8 scope host lo
  8.        valid_lft forever preferred_lft forever
  9. 2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000
  10.     link/ether 5e:75:97:0d:54:17 brd ff:ff:ff:ff:ff:ff
  11.     inet 192.168.1.1/24 brd 192.168.1.255 scope global eth0
  12.        valid_lft forever preferred_lft forever
  13. 3: br1: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN qlen 1000
  14.     link/ether 00:00:00:00:00:00 brd ff:ff:ff:ff:ff:ff
  15.     inet 172.18.0.1/24 scope global br1
  16.        valid_lft forever preferred_lft forever
  17. 96: veth1_11@veth0_11: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN qlen 1000
  18.     link/ether 5e:75:97:0d:54:0e brd ff:ff:ff:ff:ff:ff
  19. 97: veth0_11@veth1_11: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN qlen 1000
  20.     link/ether a6:c7:1f:79:a6:a6 brd ff:ff:ff:ff:ff:ff
  21. #使用ip netns创建两个net namespace
  22. # ip netns add r1
  23. # ip netns add r2
  24. # ip netns list
  25. r2
  26. r1 (id: 0)
  27. #将两个网卡分别加入到对应的netns中
  28. # ip link set veth0_11 netns r1
  29. # ip link set veth1_11 netns r2
  30. #再次查看网卡,在bash当前的namespace中已经看不到veth0_11和veth1_11了
  31. # ip a
  32. 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1
  33.     link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
  34.     inet 127.0.0.1/8 scope host lo
  35.        valid_lft forever preferred_lft forever
  36. 2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000
  37.     link/ether 5e:75:97:0d:54:17 brd ff:ff:ff:ff:ff:ff
  38.     inet 192.168.1.1/24 brd 192.168.1.255 scope global eth0
  39.        valid_lft forever preferred_lft forever
  40. 3: br1: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN qlen 1000
  41.     link/ether 00:00:00:00:00:00 brd ff:ff:ff:ff:ff:ff
  42.     inet 172.18.0.1/24 scope global br1
  43.        valid_lft forever preferred_lft forever
  44. #接下来我们切换到对应的netns中对网卡进行配置
  45. #通过nsenter --net可以切换到对应的netns中,ip a展示了我们上面加入到r1中的网卡
  46. # nsenter --net=/var/run/netns/r1 /bin/bash
  47. # ip a
  48. 1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN qlen 1
  49.     link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
  50. 97: veth0_11@if96: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN qlen 1000
  51.     link/ether a6:c7:1f:79:a6:a6 brd ff:ff:ff:ff:ff:ff link-netnsid 1
  52. #对网卡配置ip并启动
  53. # ip addr add 172.18.0.11/24 dev veth0_11
  54. # ip link set veth0_11 up
  55. # ip a
  56. 1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN qlen 1
  57.     link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
  58. 97: veth0_11@if96: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state LOWERLAYERDOWN qlen 1000
  59.     link/ether a6:c7:1f:79:a6:a6 brd ff:ff:ff:ff:ff:ff link-netnsid 1
  60.     inet 172.18.0.11/24 scope global veth0_11
  61.        valid_lft forever preferred_lft forever
  62. -----> 切换至shell B执行
  63. ------------------------------------------------------------------
  64. #在r1中ping veth1_11
  65. # ping 172.18.0.12
  66. PING 172.18.0.12 (172.18.0.12) 56(84) bytes of data.
  67. 64 bytes from 172.18.0.12: icmp_seq=1 ttl=64 time=0.033 ms
  68. 64 bytes from 172.18.0.12: icmp_seq=2 ttl=64 time=0.049 ms
  69. ...
  70. #至此我们通过netns完成了创建net Namespace的小实验
复制代码
 
  1. #在shell B中我们同样切换到netns r2中进行配置
  2. #通过nsenter --net可以切换到r2,ip a展示了我们上面加入到r2中的网卡
  3. # nsenter --net=/var/run/netns/r2 /bin/bash
  4. #  ip a
  5. 1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN qlen 1
  6.     link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
  7. 96: veth1_11@if97: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN qlen 1000
  8.     link/ether 5e:75:97:0d:54:0e brd ff:ff:ff:ff:ff:ff link-netnsid 0
  9. #对网卡配置ip并启动
  10. # ip addr add 172.18.0.12/24 dev veth1_11
  11. # ip link set veth1_11 up
  12. # ip a
  13. 1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN qlen 1
  14.     link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
  15. 96: veth1_11@if97: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP qlen 1000
  16.     link/ether 5e:75:97:0d:54:0e brd ff:ff:ff:ff:ff:ff link-netnsid 0
  17.     inet 172.18.0.12/24 scope global veth1_11
  18.        valid_lft forever preferred_lft forever
  19.     inet6 fe80::5c75:97ff:fe0d:540e/64 scope link
  20.        valid_lft forever preferred_lft forever
  21. #尝试ping r1中的网卡
  22. # ping 172.18.0.11
  23. PING 172.18.0.11 (172.18.0.11) 56(84) bytes of data.
  24. 64 bytes from 172.18.0.11: icmp_seq=1 ttl=64 time=0.046 ms
  25. 64 bytes from 172.18.0.11: icmp_seq=2 ttl=64 time=0.040 ms
  26. ...
  27. #可以完成通信
  28. 切换至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

举报 回复 使用道具