docker 赖以实现资源隔离与限制的原理

2022-04-03 23:36:59   最后更新: 2022-04-03 23:36:59   访问数量:93




 

此前的一篇文章中,我们介绍了 Docker 的构建和使用:

 

docker 入门 -- 带你全面了解 docker 的概念与使用

 

我们了解到,docker 是一种基于沙盒技术的容器,它实现了运行时环境的封装,从而让我们的集群管理和发布等操作十分便捷。

 

那么,同样是沙盒技术,为什么我们不采用虚拟机技术,而是要使用 docker 呢?这与 docker 的原理是分不开的。本文,我们就来剖析一下 docker 的原理,看看它能够脱颖而出的秘诀是什么。

 

 

 

 

2.1 Linux 的 Namespace 机制

 

Docker 的核心就是对 Linux Namespace 机制的使用。

 

Linux 的 Namespace 机制是在 2002 年 2.4.19 版本内核中首次支持的,这个机制允许在创建进程时开启,从而实现将进程所需的全部资源包装起来,在进程中看起来似乎拥有一套完全独立的全局资源,从而达到进程隔离的目的,这些全局资源包括全新的进程 ID、主机名、用户 ID、文件名与网络访问和进程间通信的名称等各种资源。

 

从 2018 年的 5.6 版本内核开始,扩充到 8 种命名 Namespace:

 

  1. 文件系统 mount(mnt)
  2. 进程 ID(pid)
  3. 网络(net)
  4. 进程间通信(ipc)
  5. 主机名(uts)
  6. 用户名(user)
  7. 控制组(cgroup)
  8. 时间(time)

 

如果进程在创建时被指定了新建某个 Namespace,创建成功的新进程就会在相应的资源上做到与其他进程的隔离。

 

2.2 Linux Namespace 机制的使用

 

那么,我们怎么使用 Linux 的 Namespace 机制呢?

 

linux 提供了三个系统调用来操作 Namespace:

 

  1. clone() -- 通过指定 clone 函数的第三个参数,就可以指定新建的进程要迁移到哪个命名空间中。
  2. setns() -- 加入文件描述符指定的命名空间。
  3. unshare() -- 允许进程或线程取消关联的其他进程或线程的共享部分。

 

2.2.1 新进程的创建 -- clone()

 

我们曾经介绍过 linux 用来创建进程的两个系统调用 fork 与 vfork:

 

创建子进程

 

fork 与 vfork 两个系统调用非常类似,他们的区别在于 vfork 不复制父进程代码到子进程,而是让子进程先在父进程地址空间中运行,直到 exec 或 exit 执行后子进程和父进程才进行分离,而父进程也只有在此刻以后才开始继续运行。

 

而事实上,linux 还有第三个用来创建进程的系统调用,但它一般不怎么被使用,因为它过于复杂,它就是 -- clone():

 

int clone(int (fn)(void ), void *child_stack, int flags, void *arg);

 

 

clone 系统调用除了可以指定子进程的栈空间外,还可以通过许许多多标志来决定子进程的行为。

 

例如,我们可以执行:

 

int pid = clone(main_function, stack_size, CLONE_NEWPID | SIGCHLD, NULL)

 

 

这时,这个新建的进程在 linux 系统中仍然是一个非常大的数字,看起来非常正常,但在这个新的进程中,如果通过 getpid 来获取当前进程 pid,可以发现,返回的竟然是 1。在这个新的进程中,它看不到系统中其他进程。

 

这看起来像是 linux 系统给进程的障眼法,虽然简单,但是有效,这样,被新建出来的进程就被隔离了起来。而 Docker 正是这样被实现的,这也是 Docker 虽然有着类似虚拟机的隔离效果,却不存在虚拟机的性能损耗的秘诀。

 

2.2.2 文件系统的挂载 -- mount()

 

通过为 clone 函数传递 CLONE_NEWPID 标志,可以让新建的进程处于一个新的 Namespace 中,但如果你在新的进程中执行文件系统的调用,你会发现,在这个新进程中,文件系统并没有发生变化,他仍然可以访问外部的所有文件,那么,怎么来实现文件系统的隔离呢?

 

最简单的,我们直接执行 chroot 系统调用,让进程看到的根目录出现在一个不会影响到操作系统其他文件的新路径下不就可以了吗?但如果有多个进程同时启动,每个都想要一个与外界隔离的文件系统呢?总不能让他们都使用同一个路径下的目录吧。这时,就需要 mount 操作:

 

mount("none", "/tmp", "tmpfs", 0, ""); chroot("/tmp")

 

 

通过 mount,在被 Mount Namespace 隔离的新进程中,在 /tmp 下挂载了一个全新的 tmpfs,通过 chroot 调用,新的 tmpfs 成为了新进程的根路径,于是,新的进程从文件系统中被隔离了出来。

 

这便是 Docker 施加在 Linux 系统上的“魔法”。

 

 

在启动 Docker 时,如果传递 -c 或 --cpu-shares 参数,就可以指定限制 Docker 执行时的最高 CPU 占用。内存、io 等资源均可以通过参数限制。这又是怎么实现的呢?这就利用了 Linux 的 CGroup 机制。

 

所谓的 CGroup,就是 control group 的缩写,顾名思义,就是资源控制组,也被称作资源限制子系统。

 

cgroup 结构体可以组织成一颗树的形式,每一棵cgroup 结构体组成的树称之为一个 cgroups 层级结构。

 

在 linux 系统中,/sys/fs/cgroup 目录下,有着注入 cpuset、cpu、memory 等子目录,在这些子目录中,我们就可以通过创建子目录,修改配置文件,实现对某一组进程使用相应的资源的限制。

 

例如我们在 /sys/fs/cgoup/cpu 目录下创建 container 目录,它的下面就会自动出现一系列配置文件。

 

而 cpu.cfs_quota_us 中配置的数字 m 与 cpu.cfs_period_us 中配置的数字 n 就表示在每 n 微秒时间内,最多允许进程占用 CPU m 微妙。

 

在 tasks 文件中写入一个或几个 PID,就可以完成对这些 PID 的资源限制。

 

这就是 docker 中资源限制的原理。内存、IO、带宽等资源的限制也是同理。

 

 

了解了 Docker 的原理,我们明白,为什么 Docker 相比虚拟机拥有着可以比拟宿主机本地程序的性能优势。那么,相比于虚拟机,Docker 就占据绝对优势了吗?

 

并非如此,Docker 的缺点也很明显:

 

  1. 隔离不彻底,与虚拟机完全虚拟出一个操作系统不同,docker 仍然使用的是宿主机的内核,一些对内核的设置指令仍然会影响到宿主机。因此,docker 镜像中的程序并不能像虚拟机中的程序一样肆意妄为。
  2. 无法虚拟内核,虚拟机可以在 linux 系统中虚拟一个 windows 操作系统环境,因为 docker 必须使用宿主机的操作系统内核,所以,这点是 Docker 无法做到的。
  3. 安全性较低,虚拟机中的操作系统虽然有几种方法可以判断出自己是运行在虚拟机中的,但由于有着虚拟机程序的阻隔,除非利用虚拟机的重大漏洞,否则虚拟机中的进程是无法影响到宿主机的,但 docker 中的进程由于实际上和宿主机其他进程是运行在一起的,所以其越狱门槛是低于虚拟机的。

 

 

欢迎关注微信公众号,以技术为主,涉及历史、人文等多领域的学习与感悟,每周三到七篇推文,只有全部原创,只有干货没有鸡汤

 

linux 使用及配置相关






linux      技术贴      docker      虚拟化      沙盒     


京ICP备2021035038号