【容器】如何理解容器底层基础

这两周抽空根据《从零开始写docker》简单实现了一下docker的底层,可以参考:Docker。本篇文档总结下自己对于容器的相关理解。通过实践中的容器的特点,来一步步剖析容器的实现。
容器的底层原理:rootfs, namespace,cgroups; 分别解决容器以下几个问题:
namespace: 解决容器如何与外部空间进行隔离。
cgroups:容器内进程使用多少资源(cpu,mem),存储资源的限制底层通过文件系统来保证。
rootfs:容器内部包含哪些文件以及独立的文件系统。

Namespace

linux的namespace中包括6个namespace可使用,

image.png


通过启动一个容器来进行分析,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采用了树形结构来进行管理,资源的限制是可以进行继承的。这里具体的手动使用可以查看文章顶部给出的链接。这里主要说明以下几个问题:

  1. 在创建docker时,可以选择cgroup-driver:cgroupfs,systemd。在容器或者k8s中,都推荐使用systemd来管理,systemd封装了cgroup相关的API,另外,linux在启动时默认使用systemd,如果docker使用cgroupfs,而linux使用systemd,那么管理存在2个不同视图,可能会引起未知错误。
  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
  1. 使用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的实现方式:

  1. 通过cat /proc/self/mountinfo 查找cgroup的挂载点;

  2. 在挂载点下,分别创建:(举例cpu)

    1. 创建cpu目录,并创建cpu.shares, cpu.cfs_period_us, cpu.cfs_quota_us文件

    2. 将限制写入到对应文件中:

      1. cpu.shares 控制的是CPU使用的比例而不是绝对值。
      2. cpu.cfs_period_us & cpu.cfs_quota_us 控制的是CPU使用时间,单位是微秒,比如每1秒钟,这个进程只能使用200ms,相当于只能用20%的CPU。
    3. 将当前进程pid写入到cpu目录下的tasks文件中。

cgroup v2的实现方式:

  1. /sys/fs/cgroup创建子目录。
  2. cpu.max中写入限制条件。
  3. 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中各层文件?首先下载镜像使用后:

  1. 通过docker image查看对应的image id;
  2. 通过image id查看/var/lib/docker/image/overlay2/imagedb/content/sha256/<image id> | python3 -m json.tool
  3. 获取到想要查看的layer的 rootfs.diff_ids,从上往下看,就是底层到顶层。
  4. cat /var/lib/docker/image/overlay2/layerdb/sha256/<3 diff ids>/cache-id 获取到overlay2的缓存id。
  5. cd /var/lib/docker/overlay2/<cache id>/diff查看到layer的文件。

容器/k8s的一些技巧

  1. docker log以及kubectl logs的底层原理都是通过http 将linux系统上的log文件进行输出,如果加上-f 后,则使用http content-chunk的方式,持续扫描文件,并从服务端推送到客户端。(必须是标准输出)

    1. docker log存放地址:/var/lib/docker/container/<container id>/<container-id>-json.log
    2. pods log存放地址为:/var/log/pods/<podName>
  2. docker ps以及docker inspect实际上是通过读取/var/lib/docker/container/<container id>/config.v2.json来进行展示,所有的容器的相关信息都保存在config.v2.json当中,所以能够实现stop->start方法。通过获取pid+nsenter实现exec命令等操作。

  3. pod内部的镜像没有tcpdump或其他命令,可以通过:

    1. 登陆到对应的pod的工作节点。
    2. docker ps , docker inspect <container-id> --format {{.State.Pid}}
    3. nsenter -n -t <pid>,后续操作。

小结

通过自己参照文档进行从零实现一边docker,可以了解到没有注意到的事。 下一篇继续分析容器中网络的底层原理。

作者:TangLyan
链接:https://juejin.cn/post/7372591578756775951
本文转载于稀土掘金,如有侵权请联系删除
 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值