京东商城的网站建设,蓝白清爽企业通用wordpress模板,wordpress标签tag,阳江网站目录
容器隔离的实现原理
1. 使用cgroups实现资源隔离
自定义一个cgroup
设置进程的内存使用
启动一个docker 容器#xff0c;观察cgroup的创建情况
2. 使用Namespaces进行资源分区
namespace继承关系引发的问题
3. 结合来使用Namespaces 和chroot
4. 结论
参考文档…目录
容器隔离的实现原理
1. 使用cgroups实现资源隔离
自定义一个cgroup
设置进程的内存使用
启动一个docker 容器观察cgroup的创建情况
2. 使用Namespaces进行资源分区
namespace继承关系引发的问题
3. 结合来使用Namespaces 和chroot
4. 结论
参考文档 容器隔离的实现原理 docker 是一个神奇的工具它能够轻松创建 docker 镜像并快速启动一个完全隔离资源的容器但它是如何实现资源隔离的呢
大致来说docker通过Linux的cgroups 、namespaces、chroot实现容器隔离在这篇文章中我们将通过使用linux基础命令来构造进程隔离就像docker容器中那样 1. 使用cgroups实现资源隔离
在 Linux 中控制组 (cgroups) 是一项内核功能它允许隔离一组进程的资源使用例如 CPU、内存、磁盘 IO、网络等。
Linux 中的每种类型的资源都有一个 cgroup 层次结构。 这些层次结构表示为 /sys/fs/cgroup 中的伪文件系统。
在这个目录下可以看到很多cgroups allenlocalhost:/sys/fs/cgroup$ ls blkio cpu cpu,cpuacct cpuacct cpuset devices freezer hugetlb memory net_cls net_cls,net_prio net_prio perf_event pids rdma systemd unified 正如文件夹名称所固有的那样这些条目中的每一个都负责一种类型的资源。 如果你进一步查看其中一个文件夹可以看到可以各种属性控制文件。 例如让我们看一下内存 cgroup allenlocalhost:/sys/fs/cgroup$ ls memorycgroup.procs memory.soft_limit_in_bytes memory.limit_in_bytes memory.max_usage_in_bytes memory.usage_in_bytes 如上面命令的输出所示每一个文件都控制一个特定的属性。 例如memory.limit_in_bytes 控制此 cgroup 中的进程可以使用的最大内存 cgroup.procs 的文件包含属于该 cgroup 的所有进程的列表。 你可以修改其中一些文件来改变 cgroup 的行为如调整memory.soft_limit_in_bytes使这组cgroup有更大的内存使用率其中一些文件由内核编写用于维护cgroup 当前的状态。 自定义一个cgroup
你可以为特定资源例如内存创建一个属于自己的 cgroup方法是在该资源的 cgroup 目录下创建一个目录。 allenlocalhost:~# mkdir /sys/fs/cgroup/memory/allen Linux kernel会自动在这个目录下创建一些必要的文件 allenlocalhost:~# ls /sys/fs/cgroup/memory/allen cgroup.clone_children memory.kmem.failcnt memory.kmem.tcp.limit_in_bytes memory.max_usage_in_bytes memory.move_charge_at_immigrate memory.stat tasks cgroup.event_control memory.kmem.limit_in_bytes memory.kmem.tcp.max_usage_in_bytes memory.memsw.failcnt memory.numa_stat memory.swappiness cgroup.procs memory.kmem.max_usage_in_bytes memory.kmem.tcp.usage_in_bytes memory.memsw.limit_in_bytes memory.oom_control memory.usage_in_bytes memory.failcnt memory.kmem.slabinfo memory.kmem.usage_in_bytes memory.memsw.max_usage_in_bytes memory.pressure_level memory.use_hierarchy memory.force_empty memory.kmem.tcp.failcnt memory.limit_in_bytes memory.memsw.usage_in_bytes memory.soft_limit_in_bytes notify_on_release 设置进程的内存使用
我们可以改写memory.limit_in_bytes 来限制内存使用大小比如100kbs表示最多使用100kbs echo 100000 /sys/fs/cgroup/memory/sushil/memory.limit_in_bytes 通过将 shell 的 PID 写入 /sys/fs/cgroup/memory/allen/cgroup.procs来将当前 shell 添加到此 cgroup。此操作的意义在于当你用在继续使用bash时属于当前用户的它能够调用的内存资源是100kbs allenlocalhost:/sys/fs/cgroup# ps PID TTY TIME CMD 1973 pts/1 00:00:00 sudo 1974 pts/1 00:00:00 su 1975 pts/1 00:00:00 bash 1983 pts/1 00:00:00 ps allenlocalhost:/sys/fs/cgroup# echo 1975 /sys/fs/cgroup/memory/allen/cgroup.procs 启动一个docker 容器观察cgroup的创建情况
我们启动一个 docker 容器看看它是否创建了一个单独的 cgroup。 allenlocalhost:~$ sudo docker run -d -m 100m nginx
Unable to find image nginx:latest locally
latest: Pulling from library/nginx
c229119241af: Pull complete
2215908dc0a2: Pull complete
08c3cb2073f1: Pull complete
18f38162c0ce: Pull complete
10e2168f148a: Pull complete
c4ffe9532b5f: Pull complete
Digest: sha256:2275af0f20d71b293916f1958f8497f987b8d8fd8113df54635f2a5915002bf1
Status: Downloaded newer image for nginx:latest
17fa32e039f2dcd8ad7dfbb0a9eb192ff01f38f47a8befd7cbf3040ef5a4d20a 在这里我们启动了一个内存限制为 100 MB 的 nginx container 接下来我们将检查这个容器是否有内存 cgroupcgroup 将使用容器 ID 创建 allenlocalhost:~$ lscgroup memory:/ | grep 17fa32e039f2dcd8ad7dfbb0a9eb192ff01f38f47a8befd7cbf3040ef5a4d20a
memory:/docker/17fa32e039f2dcd8ad7dfbb0a9eb192ff01f38f47a8befd7cbf3040ef5a4d20a 确实有创建内存的cgroup 我们检查 memory.limit_in_bytes 文件看看内存限制是多少(该值是通过将 100MB 转换为 KB 来设置的) allenlocalhost:~$ cat /sys/fs/cgroup/memory/docker/17fa32e039f2dcd8ad7dfbb0a9eb192ff01f38f47a8befd7cbf3040ef5a4d20a/memory.limit_in_bytes 104857600 通过以上操作我们可以了解到容器可以通过cgroup 来限制每个容器的资源使用 当你启动容器时运行时会创建一个单独的 cgroup 来设置资源限制。
任何超过 cgroup 限制的行为都会使内核终止该容器来处理并且容器运行时将抛出 OOM 错误。接下来让我们了解一下Linux Namespaces的用法。 2. 使用Namespaces进行资源分区
Namespaces 是 Linux 内核的一项功能它允许为一组进程划分资源。 简单来说如果 cgroups 限制了资源的使用Namespaces 则限制了进程可以看到的资源。 通过将进程放在Namespaces 中你可以限制它可以看到的资源它对内核资源进行分区以便一组进程看到一组资源而另一组进程看到另一组资源。Linux通过将一组资源和进程放在相同Namespaces 来工作比如pid 101 对应资源net101、cpu101、memory101。
以下是不同类型的Namespaces请查看文章下方的参考文档了解更多
Mount (mnt)Process ID (pid)Network (net)Interprocess Communication (ipc)Unix Timesharing System (uts)User ID (user)Control Group (cgroup)
每个进程属于以上7种类型中的其中一种你可以使用 lsns 命令在系统上查看不同的Namespaces 每个进程都与一个命名空间关联并且只能查看或使用与该命名空间关联的资源。 allenlocalhost:~$ lsnsNS TYPE NPROCS PID USER COMMAND
4026531835 cgroup 3 1588 allengcp /lib/systemd/systemd --user
4026531836 pid 3 1588 allengcp /lib/systemd/systemd --user
4026531837 user 3 1588 allengcp /lib/systemd/systemd --user
4026531838 uts 3 1588 allengcp /lib/systemd/systemd --user
4026531839 ipc 3 1588 allengcp /lib/systemd/systemd --user
4026531840 mnt 3 1588 allengcp /lib/systemd/systemd --user
4026531992 net 3 1588 allengcp /lib/systemd/systemd --user namespace继承关系引发的问题
容器使用namespaces 来划分不同的资源。 例如每个容器都有自己的主机名、自己的网络堆栈、自己的一组 cgroup并且只能看到在其中运行的进程。 这是通过为每个容器创建单独的namespaces 来实现的。 namespace的一个特性是子进程可以继承父进程的namespace。
这个继承的特性在容器技术中是要想办法屏蔽的为了帮助读者理解这一点请看下面的一个操作实例。
当你从当前的 shellshl1 启动另一个 shell(shl2)shl2将成为当前 shl1 的子进程并将继承其namespaces 。 我们先执行ps获取当前 shell 进程可以看到的进程列表, 接下来使用 sh命令启动一个新的 shell shl2,并调用ps查看进程表. allenlocalhost:~$ psPID TTY TIME CMD1815 pts/1 00:00:00 bash16787 pts/1 00:00:00 ps
allenlocalhost:~$ sh
$psPID TTY TIME CMD1815 pts/1 00:00:00 bash16788 pts/1 00:00:00 sh16789 pts/1 00:00:00 ps 你可以看到在shl2中可以看到bash 1815这个进程是属于父进程shl1的也就是说子 shell 进程可以看到父进程里所有的进程这一特性对docker 容器来说是不可接受的。 每个容器应该只能看到自己的子进程仅此而已。
我们继续实验使用 unshare 命令在新的 pid 命名空间中启动子进程--fork 参数是为了sh进程继承unshare 进程而不是继承 sudo 进程否则它会继承sudo
补充unshare用于取消共享的命名空间或切断命名空间的继承关系
通过sudo lsns 命令来验证 sh 进程是否在新的 PID 命名空间中运行。 allenlocalhost:~$ sudo unshare --pid --fork sh
# ps
PID TTY TIME CMD
26168 pts/1 00:00:00 sudo
26169 pts/1 00:00:00 unshare
26170 pts/1 00:00:00 sh
26171 pts/1 00:00:00 ps
#sudo lsns
NS TYPE NPROCS PID USER COMMAND
4026532203 mnt 1 200 root /lib/systemd/systemd-udevd
4026532204 uts 1 200 root /lib/systemd/systemd-udevd
4026532251 mnt 1 427 systemd-network /lib/systemd/systemd-networkd
4026532252 mnt 1 432 systemd-resolve /lib/systemd/systemd-resolved
4026532253 mnt 2 1245 _chrony /usr/sbin/chronyd -F -1
4026532314 mnt 1 916 root /lib/systemd/systemd-logind
4026532315 uts 1 916 root /lib/systemd/systemd-logind
4026532326 pid 1 26170 root shallenlocalhost:~$ sudo lsns
NS TYPE NPROCS PID USER COMMAND
4026532203 mnt 1 200 root /lib/systemd/systemd-udevd
4026532204 uts 1 200 root /lib/systemd/systemd-udevd
4026532251 mnt 1 427 systemd-network /lib/systemd/systemd-networkd
4026532252 mnt 1 432 systemd-resolve /lib/systemd/systemd-resolved
4026532253 mnt 2 1245 _chrony /usr/sbin/chronyd -F -1
4026532314 mnt 1 916 root /lib/systemd/systemd-logind
4026532315 uts 1 916 root /lib/systemd/systemd-logind
4026532326 pid 1 26170 root sh很显然新的 shell 进程仍然可以看到系统范围内的进程与其namespace也就是说即使使用unshare隔绝了共享空间但子shell任然可以看到父shell的内容为什么呢
这是由于ps本身的性质所导致的如果你对这个命令底层比较了解的话你会发现它读取的是主机/proc 目录中的文件如果你从子 shell 进程中列出 proc 目录你会发现它与主机的信息是一模一样的
补充/proc 目录是一个位于内存中的伪文件系统。该目录下保存的并不是真正的文件和目录而是一些【运行时】的信息如 CPU 信息、负载信息、系统内存信息、磁盘 IO 信息等。 allenlocalhost:~$ sudo unshare --pid --fork sh # ls /proc 1 115 128 16190 17 20 25 26286 3 321 4 579 79 87 941 buddyinfo diskstats interrupts kmsg misc schedstat sysrq-trigger vmallocinfo ...... 这样就说明需要为我们的子 shell 进程提供一个单独的根目录因为 /proc 文件位于根目录中。 就像容器看不到整个主机文件系统一样我们必须为我们的进程创建一个新根以限制它读取主机的 /proc 文件或任何文件。
这里就需要用到 chroot 这个命令。 chroot 允许更改任何进程的根目录一旦完成进程将失去对新根目录之上任何内容的访问权限因为根目录是任何进程的最顶层目录 命令格式为sudo chroot NEW_ROOT_DIR RUN_COMMAND chroot 运行一条命令并将根设置为新根。 如果未给出 RUN_COMMAND则默认为 ${SHELL}。 allenlocalhost:~$ echo $SHELL /bin/bash allenlocalhost:~$ mkdir new_root allenlocalhost:~$ sudo chroot new_root chroot: failed to run command ‘/bin/bash’: No such file or directory 这里报错了是因为 new_root 目录是空的并且没有 /bin/bash 文件。 新根目录下没有命令和文件。容器可以解决这个问题当你运行容器时新进程获取新根目录docker image中包含新根路径下的内容和执行命令并会对这个新根目录进行填充
下载 new_root 目录下的 alpine linux看看我们的新进程是否可以使用它。 $ mkdir alpine cd alpine $ curl -o alpine.tar.gz http://dl-cdn.alpinelinux.org/alpine/v3.10/releases/x86_64/alpine-minirootfs-3.10.0-x86_64.tar.gz$ tar -xvf alpine.tar.gz 我们现在将 alpine 目录作为根目录。 allenlocalhost:~$ sudo chroot alpine ls
bin dev etc home lib media mnt opt proc root run sbin srv sys tmp usr var 新进程现在可以看到由 alpine linux 实例化的根文件系统。 使用这个新的根创建一个 shell 发出 ps 命令看到一个空列表。 你可以通过 /proc 目录来验证这一点也是空的。 现在我们的新进程有一个独立于主机的 /proc 目录。 现在新进程可以在单独的 PID 命名空间中使用该目录来写入进程信息。 allenlocalhost:~$ sudo chroot alpine sh
/ # ps
PID USER TIME COMMAND
/ #3. 结合来使用Namespaces 和chroot
现在让我们结合使用 unshare 和 chroot 为这个进程提供一个单独的 /proc 目录。 allenlocalhost:~$ sudo unshare --pid --fork chroot alpine sh
/ # ls
bin dev etc home lib media mnt opt proc root run sbin srv sys tmp usr var
/ # ps
PID USER TIME COMMAND
/ #结果你仍然看不到任何进程信息。 那是因为您需要将 /proc 目录挂载为 proc 类型的伪文件系统。 mount -t proc /proc /proc 此命令指示将 /proc 目录挂载到 /proc类型为 proc 完成后再次发出 ps 命令你将开始看到进程信息。 / # mount -t proc /proc /proc
/ # ps
PID USER TIME COMMAND1 root 0:00 sh5 root 0:00 ps可以看到 PID 已在此 shell 中重置这说明这一进程确实位于单独的 PID 命名空间中。
4. 结论
我们来进行一下复盘首先我们了解了cgroups的原理即对资源进行隔离它允许隔离一组进程的资源使用我们将这个概念具体化 假设一台主机的资源CPU、内存、磁盘 IO、网络分别为100%现在我们有4个进程需要均分这些资源显然每个进程将分配到25%的资源。那这里会产生一个问题怎么确保进程间不能互相访问资源呢
于是我们就需要Namespaces 命名空间来隔绝进程间的资源访问 即 cgroups 限制了资源的使用Namespaces 则限制了进程可以看到的资源。 通过将进程放在Namespaces 中你可以限制它可以看到的资源它对内核资源进行分区以便一组进程看到一组资源而另一组进程看到另一组资源。Namespaces无法解决的一个问题是子进程可以继承父进程的namespace也就是说你通过宿主机的docker run命令运行了容器但这个容器是可以看到父进程即宿主机的进程内容的这是非常大的安全隐患于是我们想到使用unshare命令。
Linux unshare 命令可以阻断进程对命名空间的继承关系但是却无法阻断Linux 中文件的存放关系ls /proc就是一个很好的例子这个目录保存了系统【运行时】的状态信息它是位于根下的我们使用命令来查询状态本质上是读取文件目录下的数据信息这里我用指针的概念来阐述一下假设有p1,p2都指向rom1,现在从逻辑上将p2执指向rom2,然而rom1和rom2使用的任然是同一块物理内存所以结果不会改变
于是又引入了 chroot 这个命令。 chroot 允许更改任何进程的根目录一旦完成进程将失去对新根目录之上任何内容的访问权限这样的话我们就可以为rom2开设一块新的地址从而在逻辑与实际意义上都切断联系现在新进程可以在单独的 PID 命名空间中使用该目录来写入进程信息。 参考文档
https://en.wikipedia.org/wiki/Linux_namespaces
https://blog.devgenius.io/basics-of-container-isolation-5eabdb258409