緈鍢嘚禙詬 发表于 2023-2-9 13:29:57

100 行 shell 写个 Docker

作者: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//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:
lrwxrwxrwx 1 root root 0 Jan 17 21:43 mnt -> mnt:
lrwxrwxrwx 1 root root 0 Jan 17 21:43 net -> net:
lrwxrwxrwx 1 root root 0 Jan 17 21:43 pid -> pid:
lrwxrwxrwx 1 root root 0 Jan 17 21:43 user -> user:
lrwxrwxrwx 1 root root 0 Jan 17 21:43 uts -> uts:

#这些 namespace 文件都是链接文件。链接文件的内容的格式为 xxx:。
    其中的 xxx 为 namespace 的类型,inode number 则用来标识一个 namespace,我们也可以把它理解为 namespace 的 ID。
    如果两个进程的某个 namespace 文件指向同一个链接文件,说明其相关资源在同一个 namespace 中。以ipc:例,
    ipc是namespace的类型,4026531839是inode number,如果两个进程的ipc namespace的inode number一样,说明他们属于同一个namespace。
    这条规则对其他类型的namespace也同样适用。

#从上面的输出可以看出,对于每种类型的namespace,进程都会与一个namespace ID关联。

#当一个namespace中的所有进程都退出时,该namespace将会被销毁。在 /proc//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 ]
#       示例:nsenter –t 27668 –u –I /bin/bash
#
#    unshare: 离开当前指定类型的namespace,创建且加入新的namesapce,然后执行参数中执行的命令。
#       命令格式:unshare program
#       示例: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:
ipc:

#查看当前主机名
# 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


# hostname
shell-B

# readlink /proc/$$/ns/uts /proc/$$/ns/ipc
uts:
ipc:

# 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:
ipc:

# 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:
ipc:

#设置新的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 三 、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】 我们会及时删除侵权内容,谢谢合作!
页: [1]
查看完整版本: 100 行 shell 写个 Docker