一文读懂容器三大核心技术

您所在的位置:网站首页 groups么读 一文读懂容器三大核心技术

一文读懂容器三大核心技术

2024-02-04 20:45| 来源: 网络整理| 查看: 265

本文字数:8814 字

精读时间:18 分钟

也可在 8 分钟内完成速读

容器到底是什么?容器是怎么工作的?容器如何隔离资源?为啥容器启动那么快?...如果你是个好奇宝宝,平时在使用容器的时候内心定会泛起类似疑问。本文将通过讲解其三大核心技术:Linux Namespace,Control Groups(cgroups)和UnionFS (联合文件系统)来解答你心中对容器原理的种种疑问。

Linux Namespace

Linux Namespaces是Linux内核提供的一种资源隔离方案。Namespaces之间的资源相互独立。目前Linux中提供七种namespace。 

参考:http://man7.org/linux/man-pages/man7/namespaces.7.html

NamespaceFlag说明

Cgroup

CLONE_NEWCGROUP隔离cgroup IPCCLONE_NEWIPC隔离进程间通信NetworkCLONE_NEWN隔离网络资源MountCLONE_NEWNS隔离挂载点PIDCLONE_NEWPID隔离进程的IDUserCLONE_NEWUSER隔离用户和用户组的IDUTSCLONE_NEWUTS隔离主机名和域名信息

向clone系统调用传入上述表格中对应的Flag参数,可以为新建的进程创建相应的namespace。也可以使用setns系统调用将进程加入到一个已经存在的namespace中。容器通过namespace技术来实现资源隔离。

namespaces限制容器能看到哪些资源。

示例:linux下通过shell创建一个容器

Talk is cheap, show me the code。

我们直接用一个示例来演示一下namespace隔离资源的效果。在命令行下,我们可以通过unshare命令来启动一个新进程,并为其新建相应的命名空间。在这个示例中,我们将通过unshare为我们的容器创建除cgroup和user之外的所有命名空间,这也是docker run something默认为容器创建的命名空间。本示例依赖docker环境来为我们提供一些配置上的便利。完整的示例script放在这里,方便大家scriptreplay回看过程。

git clone https://github.com/DrmagicE/build-container-in-shell cd ./build-container-in-shell scriptreplay build_container.time build_container.his step1: 准备一个rootfs

首先,我们要为我们的容器准备自己的rootfs,用来为容器进程提供隔离后执行环境的文件系统。这里我们直接导出alpine镜像作为我们的rootfs,选择/root/container目录作为镜像rootfs:

[root@drmagic container]# pwd /root/container [root@drmagic container]# # 修改mount类型为private,确保后续的mount/umount不会在namespace之间传播 [root@drmagic container]# mount --make-rprivate / [root@drmagic container]# CID=$(docker run -d alpine true) [root@drmagic container]# docker export $CID | tar -xf- [root@drmagic container]# ls # rootfs建立好啦 bin dev etc home lib media mnt opt proc root run sbin srv sys tmp usr var step2: 命名空间隔离 [root@drmagic container]# # 使用unshare为新的shell创建命名空间 [root@drmagic container]# unshare --mount --uts --ipc --net --pid --fork /bin/bash [root@drmagic container]# echo $$ # 看看新进程的pid 1 [root@drmagic container]# hostname unshare-bash # 修改一下hostname [root@drmagic container]# exec bash #替换bash,显现hostname修改后的效果 [root@unshare-bash container]# # hostname变化了

通过上面的过程,我们可以看到UTS和PID这两个命名空间的隔离效果。

如果你在这一步使用ps来查看所有的进程,结果可能会令你失望——你仍然会看到系统中的所有进程,就像没有隔离成功一样。但这是正常的,因为ps读取/proc下的信息,此时的/proc还是host的/proc,所以ps还是能看到所有的进程。

step3:隔离挂载信息

[root@unshare-bash container]# mount # 还是能看到host上的mount /dev/vda2 on / type xfs (rw,relatime,attr2,inode64,noquota) devtmpfs on /dev type devtmpfs (rw,nosuid,size=1929332k,nr_inodes=482333,mode=755) tmpfs on /dev/shm type tmpfs (rw,nosuid,nodev) devpts on /dev/pts type devpts (rw,nosuid,noexec,relatime,gid=5,mode=620,ptmxmode=000) mqueue on /dev/mqueue type mqueue (rw,relatime) hugetlbfs on /dev/hugepages type hugetlbfs (rw,relatime) .....

我们发现mount依然能够获取全局挂载信息,难道是mount命名空间隔离没生效?非也,mount命名空间已经生效了。当新建一个mount命名空间时,他会拷贝父进程的挂载点,但对该命名空间挂载点的后续修改将不会影响到其他命名空间。

参考:

http://man7.org/linux/man-pages/man7/mount_namespaces.7.html#DESCRIPTION

命名空间内挂载点的修改不影响其他命名空间有一个前提条件——mount的propagation type要设置为MS_PRIVATE,这也是为什么一开始我们要执行 mount --make-rprivate / 的原因

因此我们看到的mount信息是父进程的一份拷贝,我们重新mount一下/proc,好让ps能正常显示。

[root@unshare-bash ~]# # 重新mount一下/proc [root@unshare-bash ~]# mount -t proc none /proc [root@unshare-bash ~]# ps -ef UID PID PPID C STIME TTY TIME CMD root 1 0 0 21:29 pts/0 00:00:00 bash root 77 1 0 21:47 pts/0 00:00:00 ps -ef [root@unshare-bash ~]# # 啊哈,现在我们的ps正常了!

处理完了/proc的挂载,我们还需要清理旧的挂载点,将他们umount掉,这一步我们需要借助pivot_root(new_root,put_old)来完成。pivot_root将当前mount namespace下的所有进程(线程)的根目录挂载点切换至new_root,并将旧的根目录挂载点放到put_old目录下。使用pivot_root的主要目的是用来umount一些从父进程copy过来的挂载点。

http://man7.org/linux/man-pages/man2/pivot_root.2.html

为了满足pivot_root的一些参数要求,需要额外做一次bind mount:

[root@unshare-bash container]# mount --bind /root/container/ /root/container/ [root@unshare-bash container]# cd /root/container/ [root@unshare-bash container]# mkdir oldroot/ [root@unshare-bash container]# pivot_root . oldroot/ [root@unshare-bash container]# cd / [root@unshare-bash /]# PATH=$PATH:/bin:/sbin [root@unshare-bash /]# mount -t proc none /proc [root@unshare-bash /]# ps -ef PID USER TIME COMMAND 1 root 0:00 bash 70 root 0:00 ps -ef [root@unshare-bash /]# mount # 依旧能看到host上的信息 rootfs on / type rootfs (rw) /dev/vda2 on /oldroot type xfs (rw,relatime,attr2,inode64,noquota) devtmpfs on /oldroot/dev type devtmpfs (rw,nosuid,size=1929332k,nr_inodes=482333,mode=755) tmpfs on /oldroot/dev/shm type tmpfs (rw,nosuid,nodev) .... [root@unshare-bash /]# umount -a # umount全部 umount: can't unmount /: Resource busy umount: can't unmount /oldroot: Resource busy umount: can't unmount /: Resource busy [root@unshare-bash /]# mount -t proc none /proc # 重新mount /proc [root@unshare-bash /]# mount rootfs on / type rootfs (rw) /dev/vda2 on /oldroot type xfs (rw,relatime,attr2,inode64,noquota) cpu,cpuacct lrwxrwxrwx 1 root root 11 11月 11 22:49 cpuacct -> cpu,cpuacct drwxr-xr-x 6 root root 0 11月 11 22:49 cpu,cpuacct drwxr-xr-x 4 root root 0 11月 11 22:49 cpuset drwxr-xr-x 6 root root 0 11月 11 23:40 devices drwxr-xr-x 4 root root 0 11月 11 22:49 freezer drwxr-xr-x 4 root root 0 11月 11 22:49 hugetlb drwxr-xr-x 6 root root 0 11月 11 22:49 memory lrwxrwxrwx 1 root root 16 11月 11 22:49 net_cls -> net_cls,net_prio drwxr-xr-x 4 root root 0 11月 11 22:49 net_cls,net_prio lrwxrwxrwx 1 root root 16 11月 11 22:49 net_prio -> net_cls,net_prio drwxr-xr-x 4 root root 0 11月 11 22:49 perf_event drwxr-xr-x 6 root root 0 11月 11 22:49 pids drwxr-xr-x 6 root root 0 11月 11 22:49 systemd

除了systemd以外, 上述目录中的每一个目录都代表着一个子系统,从上图中可以看出其包含有cpu相关(cpu,cpuacct,cpuset), 内存相关(memory),块设备I/O相关(blkio),网络相关(net_cls,net_prio)等子系统。

cgroups用树形的层级关系来管理各项子系统,每个子系统下都有它们自己的树形结构。树中的节点就是一组进程(或线程),不同子系统的层级关系是相互独立的。例如cpu子系统和memory子系统的层级结构可以是不一样的:

cpu/ ├── batch │   ├── bitcoins │   │   └── 52 // tasks。

如果你使用docker启动一个容器,那么docker会为该容器在每个子系统目录下创建docker/$container_id目录。这样cgroups就能对该容器的资源进行管理和限制了。

memory cgroup

memory cgroup是管理内存的cgroup,其两个主要功能是:

统计当前分组的内存使用情。

限制当前分组的内存用量。

统计

memory cgroup以内存页为单位,追踪统计每个分组的内存使用大小。以docker为例,使用下列命令启动一个nginx容器,并读取容器的内存占用情况:

$ container_id=$(docker run -d nginx)d $ cat /sys/fs/cgroup/memory/docker/$container_id/memory.usage_in_bytes 2666496

由于统计是以页为单位的,所以统计结果只能是页大小的倍数(通常4096)。

限制

memory cgroup可以限制整个分组对内存的使用(默认没有限制)。共有两种限制能力:

硬限制(hard limit)。

软限制(soft limit)。

如果内存超出了硬限制,会触发当前分组的OOM-killer来杀死进程。

如果你不想让进程被杀掉,可以禁用当前分组的OOM -killer:

echo 1 > memory.oom_control

相比硬限制的强硬手段,软限制不会强制kill掉进程,软限制仅会在系统内存不足的时候才会起作用。当出现内存不足时,cgroup会尽最大努力将各分组的内存限制在软限制以下,保证系统的总体可用性。

依旧举docker为例,我们使用下列命令将nginx容器的硬限制和软限制分别设置成100M和50M,可以看到对应cgroup文件的变化:

$ container_id=$(docker run -d -m 100m --memory-reservation 50m nginx) $ cat /sys/fs/cgroup/memory/docker/$container_id/memory.limit_in_bytes 104857600 cpu,cpuacct drwxr-xr-x 6 root root 0 11月 11 22:49 cpu,cpuacct

cpu与cpuacct(CPU Accounting)结合起来的主要功能有:

统计当前分组的CPU使用情况。

限制分组使用CPU的能力(通过影响调度策略)。

统计

统计功能主要由cpuacct提供,例如读取当前分组的cpu总耗时:

$ cat cpuacct.usage 1196687732756025 //单位是ns 限制

通过影响调度器的调度行为,可以限制当前分组对CPU的使用能力,这也是容器限制CPU核数的原理。cpu cgroup可以控制以下两种调度器的调度行为:

Completely Fair Scheduler (CFS) 基于完全公平算法的调度器。

Real-Time scheduler (RT) 基于实时调度算法的调度器。

在绝大部分情况下,我们使用的都是默认的CFS调度器,所以在此也仅讨论对CFS调度器的控制行为。在cpu cgroup的目录下,我们可以看到如下两个文件:

$ cat cpu.cfs_period_us 100000 $ cat cpu.cfs_quota_us -1 cpu.cfs_period_us

表示调度周期,微秒(μs)为单位。表示每隔多长时间执行一次调度,默认为100ms(100000μs)

调度周期越长,cpu执行任务的吞吐量越大,延迟则相应增加。反之, 调度周期越短,则延迟越小,但cpu的吞吐量也随之降低了(因为要耗费大量的时间在“无价值”的进程切换上)。

cpu.cfs_quota_us

表示在一个调度周期时间内(即cpu.cfs_period_us设定的时间),当前组内所有的进程允许在单个CPU上运行的总时长,微秒(μs)为单位。默认为-1,即不限制。假设当前分组要充分利用双核CPU资源,可以设置:

cpu.cfs_quota_us = 200000

cpu.cfs_period_us= 100000

同样的,如果我们要给当前分组设置只允许使用0.5个核,则:

cpu.cfs_quota_us = 50000

cpu.cfs_period_us= 100000

cpu.cfs_quota_us/cpu.cfs_period_us = 分配给当前组的cpu核数

当我们使用docker指定容器核数时,其实就是在调整cpu.cfs_quota_us文件的参数。

cpuset cgroup

cpuset用的比较少,当追求极致性能的时候,可以通过其实现绑核,绑NUMA内存节点等功能:

cpuset.cpus用于标明当前分组可以使用哪些CPU。

cpusems用于标明当前分组可以使用哪些NUMA节点。

NUMA(Non-Uniform Memory Access)架构将CPU模块拆分成多个,组成多个NUMA节点,每个CPU模块由多个CPU(如4个)组成,并且具有独立的本地内存、I/O槽口等。CPU访问本NUMA节点的内存的速度是很快的,相当于内存之上的一层缓存。

NUMA不在本文讨论范围内(我没也搞太清楚),感兴趣的小伙伴请自行查阅相关资料。

例如在我本机查看CPU和NUMA节点信息有:

$ lscpu ... CPU(s): 2 On-line CPU(s) list: 0,1 > merged/test $ cat upper/test # 向merged的写入仅影响upper层 upper write something $ cat lower/test lowerdir

使用docker run创建一个容器后,docker就会为容器mount一个OverlayFS:

$ docker run -itd alpine /bin/sh $ mount | grep overlay2 overlay on /var/lib/docker/overlay2/a2a37f61c515f641dbaee62cf948817696ae838834fd62cf9395483ef19f2f55/merged type overlay (rw,relatime, lowerdir=/var/lib/docker/overlay2/l/RALFTJC6S7NV4INMLE5G2DUYVM: /var/lib/docker/overlay2/l/WQJ3RXIAJMUHQWBH7DMCM56PNK, upperdir=/var/lib/docker/overlay2/a2a37f61c515f641dbaee62cf948817696ae838834fd62cf9395483ef19f2f55/diff, workdir=/var/lib/docker/overlay2/a2a37f61c515f641dbaee62cf948817696ae838834fd62cf9395483ef19f2f55/work)

docker将镜像中的每个layer按顺序添加到lowerdir中,将upperdir设置为容器的可写层。

当我们使用docker pull image的时候,docker就已经将镜像中各只读层的目录创建好了,执行docker run时,基本上只需创建容器的可写层,并将它们挂载成OverlayFS即可。所以就算镜像很大,容器的启动依旧是非常迅速。

当你使用docker pull拉镜像的时候,一定出现过Already exists的标识。

docker pull xxxx ... 68ced04f60ab: Already exists


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3