这两周抽空根据《从零开始写docker》简单实现了一下docker的底层,可以参考:Docker。本篇文档总结下自己对于容器的相关理解。通过实践中的容器的特点,来一步步剖析容器的实现。
容器的底层原理:rootfs, namespace,cgroups; 分别解决容器以下几个问题:
namespace: 解决容器如何与外部空间进行隔离。
cgroups:容器内进程使用多少资源(cpu,mem),存储资源的限制底层通过文件系统来保证。
rootfs:容器内部包含哪些文件以及独立的文件系统。
Namespace
linux的namespace中包括6个namespace可使用,
通过启动一个容器来进行分析,docker中使用了哪几个?
docker run -d --name=demo busybox sleep 3600
docker exec -it demo /bin/sh
进入到容器中,发现linux的hostname出现了变化,如下:
root@VM-20-12-ubuntu:/home/ubuntu# docker run -d --name=demo busybox sleep 3600
4da78873ac578e3e649c2865388983ae9828210694b9b54c90bacfa946b9c931
root@VM-20-12-ubuntu:/home/ubuntu# docker exec -it demo /bin/sh
/ # hostname
4da78873ac5
一开始为root+系统名称,在进入到容器内部后,变成了/ ,当执行hostname
后,变成了容器ID。这里引入第一个Namespace: UTC Namepsace
,允许每个Namespace有自己的hostname。
接下来,查看容器中的进程:
/ # ps -ef
PID USER TIME COMMAND
1 root 0:00 sleep 3600
6 root 0:00 /bin/sh
14 root 0:00 ps -ef
可以看到,sleep 3600
变成了init进程。第二个Namespace:PID Namepsace
,用来隔离进程ID。
linux中进程相关的信息都来自于/proc
目录,从上面ps -ef
中,可以分析出/proc
目录下的文件信息也与宿主机存在差异:
/ # ls /proc
1 cpuinfo fs kmsg modules scsi thread-self
17 crypto interrupts kpagecgroup mounts self timer_list
23 devices iomem kpagecount mtrr slabinfo tty
acpi diskstats ioports kpageflags net softirqs uptime
buddyinfo dma irq loadavg pagetypeinfo stat version
bus driver kallsyms locks partitions swaps version_signature
cgroups execdomains kcore mdstat pressure sys vmallocinfo
cmdline fb key-users meminfo sched_debug sysrq-trigger vmstat
consoles filesystems keys misc schedstat sysvipc zoneinfo
这里使用的是Mount Namespace
:允许容器内部的看到的进程挂载点视图的不同。
通过ip a
查看容器内部的网卡信息:
/ # ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000
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
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
30: eth0@if31: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue
link/ether 02:42:ac:11:00:03 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.3/16 brd 172.17.255.255 scope global eth0
valid_lft forever preferred_lft forever
inet6 fe80::42:acff:fe11:3/64 scope link
valid_lft forever preferred_lft forever
容器内部的IP地址已经改为172.17.0.3。这里使用了:Network Namepsace
,用于隔离容器内部与外部的网络设备(后续介绍网络)。
以上比较常用的4个Namespace已经分析出来了,而IPC Namespace
: 用来隔离system V和POSIX message queues。可以通过一下方式:
root@VM-20-12-ubuntu:/home/ubuntu# ipcs -q // 查看
------ Message Queues --------
key msqid owner perms used-bytes messages
root@VM-20-12-ubuntu:/home/ubuntu# ipcmk -Q // 创建message queue
Message queue id: 0
root@VM-20-12-ubuntu:/home/ubuntu# docker exec -it demo /bin/sh
/ # ipcs -q
------ Message Queues --------
key msqid owner perms used-bytes messages
----------------------------------------------------------------------------------------
root@VM-20-12-ubuntu:/home/ubuntu# ipcs -q
------ Message Queues --------
key msqid owner perms used-bytes messages
0x38effd04 0 root 644 0 0
而User Namespace
: 用于隔离用户和用户组ID。在容器外部通过非root用户一个User Namespace,进程在User Namespace
内部有root权限,而在外部没有root权限。
如何查看进程的Namespace?
通过ls -l /proc/$pid/ns
查看该进程的namespace:
root@VM-20-12-ubuntu:/home/ubuntu# ls -l /proc/1/ns
total 0
lrwxrwxrwx 1 root root 0 May 26 21:08 cgroup -> 'cgroup:[4026531835]'
lrwxrwxrwx 1 root root 0 May 26 21:08 ipc -> 'ipc:[4026531839]'
lrwxrwxrwx 1 root root 0 May 26 21:08 mnt -> 'mnt:[4026531840]'
lrwxrwxrwx 1 root root 0 May 26 21:08 net -> 'net:[4026531992]'
lrwxrwxrwx 1 root root 0 May 26 21:08 pid -> 'pid:[4026531836]'
lrwxrwxrwx 1 root root 0 May 26 21:08 pid_for_children -> 'pid:[4026531836]'
lrwxrwxrwx 1 root root 0 May 26 21:08 user -> 'user:[4026531837]'
lrwxrwxrwx 1 root root 0 May 26 21:08 uts -> 'uts:[4026531838]'
root@VM-20-12-ubuntu:/home/ubuntu# docker exec -it demo /bin/sh
/ # ls -l /proc/1/ns
total 0
lrwxrwxrwx 1 root root 0 May 26 13:08 cgroup -> cgroup:[4026532393]
lrwxrwxrwx 1 root root 0 May 26 13:08 ipc -> ipc:[4026532391]
lrwxrwxrwx 1 root root 0 May 26 13:08 mnt -> mnt:[4026532389]
lrwxrwxrwx 1 root root 0 May 26 13:08 net -> net:[4026532395]
lrwxrwxrwx 1 root root 0 May 26 13:08 pid -> pid:[4026532392]
lrwxrwxrwx 1 root root 0 May 26 13:08 pid_for_children -> pid:[4026532392]
lrwxrwxrwx 1 root root 0 May 26 13:08 user -> user:[4026531837]
lrwxrwxrwx 1 root root 0 May 26 13:08 uts -> uts:[4026532390]
这里可以分析出:docker中user namespace
与宿主机的保持一致。
如何在宿主机进入到容器的Namespace?
可以通过pstree -pl
命令查看到对应容器的PID或者通过docker inspect demo --format {{.State.Pid}}
。通过nsenter进入到容器的命令空间:
nsenter -n -m -p -t 3163404 /bin/sh
# 解释如下:
nsenter [options] [program [arguments]]
options:
-t, --target pid:指定被进入命名空间的目标进程的pid
-m, --mount[=file]:进入mount命令空间。如果指定了file,则进入file的命令空间
-u, --uts[=file]:进入uts命令空间。如果指定了file,则进入file的命令空间
-i, --ipc[=file]:进入ipc命令空间。如果指定了file,则进入file的命令空间
-n, --net[=file]:进入net命令空间。如果指定了file,则进入file的命令空间
-p, --pid[=file]:进入pid命令空间。如果指定了file,则进入file的命令空间
-U, --user[=file]:进入user命令空间。如果指定了file,则进入file的命令空间
-G, --setgid gid:设置运行程序的gid
-S, --setuid uid:设置运行程序的uid
-r, --root[=directory]:设置根目录
-w, --wd[=directory]:设置工作目录
什么时候使用?
在需要对容器进行抓包,但是容器内部没有tcpdump命令时,可以通过nsenter -n -t <pid>
,这里只进入了Network Namespace
。
Cgroup
在容器中,为了对容器使用的资源进行限制而使用了linux内核的cgroup技术。Linux Cgroups(Control Groups)提供了对一组进程及将来子进程的资源限制、控制和统计的能力,这些资源包括CPU、内存、存储、网络等。
cgroup采用了树形结构来进行管理,资源的限制是可以进行继承的。这里具体的手动使用可以查看文章顶部给出的链接。这里主要说明以下几个问题:
- 在创建docker时,可以选择cgroup-driver:cgroupfs,systemd。在容器或者k8s中,都推荐使用systemd来管理,systemd封装了cgroup相关的API,另外,linux在启动时默认使用systemd,如果docker使用cgroupfs,而linux使用systemd,那么管理存在2个不同视图,可能会引起未知错误。
- 在使用
kind
安装k8s过程中,需要查看linux使用的cgroups的版本号,在使用kind
安装1.24以上的k8s时,会出现kubelet
无法启动的情况,需要linux使用cgroups v2版本才可以进行安装。
如何查看cgroups版本以及修改cgroups版本?
查看:
mount |grep cgroup
cgroup2 on /sys/fs/cgroup type cgroup2 (rw,nosuid,nodev,noexec,relatime,nsdelegate)
# 查看docker使用的cgroup
root@VM-20-12-ubuntu:/home/ubuntu# docker info |grep Cgroup
Cgroup Driver: systemd
Cgroup Version: 2
修改:
vim /etc/default/grub
GRUB_CMDLINE_LINUX_DEFAULT="systemd.unified_cgroup_hierarchy=yes"
# 更新+重启
update-grub
reboot
- 使用golang时,需要注意程序启动的时候默认设置GOMAXPROCS =个数,而在容器中跑的时候,会读取到宿主机的Cpu个数,从而导致1. runtime findrunnable 时产生的损耗,2. 是线程引起的上下文切换。(可查看blog.csdn.net/kevin_tech/…)解决方式:
import _ "go.uber.org/automaxprocs"
func main() {
// Your application logic here
}
cgroup v1的实现方式:
-
通过
cat /proc/self/mountinfo
查找cgroup的挂载点; -
在挂载点下,分别创建:(举例cpu)
-
创建cpu目录,并创建
cpu.shares
,cpu.cfs_period_us
,cpu.cfs_quota_us
文件 -
将限制写入到对应文件中:
- cpu.shares 控制的是CPU使用的比例而不是绝对值。
- cpu.cfs_period_us & cpu.cfs_quota_us 控制的是CPU使用时间,单位是微秒,比如每1秒钟,这个进程只能使用200ms,相当于只能用20%的CPU。
-
将当前进程pid写入到cpu目录下的tasks文件中。
-
cgroup v2的实现方式:
- 在
/sys/fs/cgroup
创建子目录。 - 在
cpu.max
中写入限制条件。 - 将
pid
写入cgroup.procs
中即可。
rootfs
rootfs 只是一个操作系统所包含的文件、配置和目录,并不包括操作系统内核。docker在此基础上,通过联合文件系统(Union File System)的能力完成镜像层之间的复用。通常情况下docker使用的为overlay2。
可通过以下命令完成:
mount -t overlay overlay -o lowerdir=/root/busybox,upperdir=/root/upper,workdir=/root/work /root/merged
如何快速查看image中各层文件?首先下载镜像使用后:
- 通过docker image查看对应的image id;
- 通过image id查看
/var/lib/docker/image/overlay2/imagedb/content/sha256/<image id> | python3 -m json.tool
。 - 获取到想要查看的layer的 rootfs.diff_ids,从上往下看,就是底层到顶层。
cat /var/lib/docker/image/overlay2/layerdb/sha256/<3 diff ids>/cache-id
获取到overlay2的缓存id。cd /var/lib/docker/overlay2/<cache id>/diff
查看到layer的文件。
容器/k8s的一些技巧
-
docker log以及kubectl logs的底层原理都是通过http 将linux系统上的log文件进行输出,如果加上-f 后,则使用http content-chunk的方式,持续扫描文件,并从服务端推送到客户端。(必须是标准输出)
- docker log存放地址:
/var/lib/docker/container/<container id>/<container-id>-json.log
- pods log存放地址为:
/var/log/pods/<podName>
- docker log存放地址:
-
docker ps以及docker inspect实际上是通过读取
/var/lib/docker/container/<container id>/config.v2.json
来进行展示,所有的容器的相关信息都保存在config.v2.json
当中,所以能够实现stop->start方法。通过获取pid+nsenter实现exec
命令等操作。 -
pod内部的镜像没有tcpdump或其他命令,可以通过:
- 登陆到对应的pod的工作节点。
- docker ps ,
docker inspect <container-id> --format {{.State.Pid}}
nsenter -n -t <pid>
,后续操作。
小结
通过自己参照文档进行从零实现一边docker,可以了解到没有注意到的事。 下一篇继续分析容器中网络的底层原理。
作者:TangLyan
链接:https://juejin.cn/post/7372591578756775951
本文转载于稀土掘金,如有侵权请联系删除