容器(Docker) 第一章:动态视图

Author: Lijb
Email: lijb1121@163.com
WeChat: ljb1121

  • 背景
2013~2014 年,以 Cloud Foundry 为代表的 PaaS 项目,逐渐完成了教育用户和开拓市场的艰巨任务,也正是在这个将概念逐渐落地的过程中,应用“打包”困难这个问题,成了整个后端 技术圈子的一块心病。

Docker 项目的出现,则为这个根本性的问题提供了一个近乎完美的解决方案。这正是 Docker 项目刚刚开源不久,就能够带领一家原本默默无闻的 PaaS 创业公司脱颖而出,然后迅速占领了所有云计算领域头条的技术原因。

而在成为了基础设施领域近十年难得一见的技术明星之后,dotCloud 公司则在 2013 年底大胆改名为 Docker 公司。不过,这个在当时就颇具争议的改名举动,也成为了日后容器技术圈风云 变幻的一个关键伏笔。

  • Docker
Docker 项目在短时间内迅速崛起的三个重要原因:

1. Docker 镜像通过技术手段解决了 PaaS 的根本性问题;
2. Docker 容器同开发者之间有着与生俱来的密切关系;
3. PaaS 概念已经深入人心的完美契机。

崭露头角的 Docker 公司,也终于能够以一个更加强硬的姿态来面对这个曾经无比强势,但现在 却完全不知所措的云计算市场。而 2014 年底的 DockerCon 欧洲峰会,则正式拉开了 Docker 公司扩张的序幕。

容器与进程

  • 容器

容器,其实是一种特殊的进程

Docker实际上是在创建容器进程时,指定了这个进程 所需要启用的一组 Namespace 参数。这样,容器就只能“看”到当前 Namespace 所限定的 资源、文件、设备、状态,或者配置。而对于宿主机以及其他不相关的程序,它就完全看不到 了。
所以说,容器,其实是一种特殊的进程而已。
  • 容器化技术的Namespace机制
其实只是 Linux 创建新进程的一个可选参数。在 Linux 系统中创建线程的系统调用是 clone(),比如:
	int pid = clone(main_function, stack_size, SIGCHLD, NULL);  
2. 这个系统调用就会为我们创建一个新的进程,并且返回它的进程号 pid。
3. 而当我们用 clone() 系统调用创建一个新进程时,就可以在参数中指定 CLONE_NEWPID 参数,比如:
		int pid = clone(main_function, stack_size, CLONE_NEWPID | SIGCHLD, NULL);  
4. 新创建的这个进程将会“看到”一个全新的进程空间,在这个进程空间里,它的 PID 是 1。之所以说“看到”,是因为这只是一个“障眼法”,在宿主机真实的进程空间里,这个进程 的 PID 还是真实的数值,比如 100。
5. 我们还可以多次执行上面的 clone() 调用,这样就会创建多个 PID Namespace,而每个 Namespace 里的应用进程,都会认为自己是当前容器里的第 1 号进程,它们既看不到宿主机 里真正的进程空间,也看不到其他 PID Namespace 里的具体情况。

除了刚刚用到的 PID Namespace,Linux 操作系统还提供了 Mount、UTS、IPC、 Network 和 User 这些 Namespace,用来对各种不同的进程上下文进行“障眼法”操作。
比如,Mount Namespace,用于让被隔离进程只看到当前 Namespace 里的挂载点信息; Network Namespace,用于让被隔离进程看到当前 Namespace 里的网络设备和配置。
总结:
Namespace 技术实际上修改了应用进程看待整个计算机“视图”,即它的“视线”被操作系统做了限制,只能“看到”某些指定的内容,但对于宿主机来说,这些被“隔离”了的进程跟其他进程并没有太大区别

容器的隔离与限制

隔离

Namespace

虚拟机和容器的对比
  • 共同点
虚拟机和容器化都可以起到将不同的应用进程相互隔离的作用

img

 1. 虚拟机: Hypervisor是虚拟机主要的部分。它通过硬件虚拟化功能,模拟出了运行一个操作系统需要的各种硬件,比如 CPU、内存、 I/O 设备等等。然后,它在这些虚拟的硬件上安装了一个新的操作系统,即 Guest OS。
		用户的应用进程就可以运行在这个虚拟的机器中,它能看到的自然也只有 Guest OS 的文件和目录,以及这个机器里的虚拟设备。这就是为什么虚拟机也能起到将不同的应用进程相互隔离的作用。

2. 容器: Docker Engine 替换了虚拟机中的 Hypervisor。很多人会把 Docker 项目称为“轻量级”虚拟化技术的原因,实际上就是把虚拟机的概念套在了容 器上。

3. 说明:
		Namespace在使用 Docker 的时候,并没有一个真正的“Docker 容器”运行在宿主机里面。Docker 项目帮助用户启动的,还是原来的应用进程,只不过在创建这些进程时,Docker 为它们加上了各种各样的 Namespace 参数。
		这时,这些进程就会觉得自己是各自 PID Namespace 里的第 1 号进程,只能看到各自 Mount Namespace 里挂载的目录和文件,只能访问到各自 Network Namespace 里的网络设备,就仿佛运行在一个个“容器”里面,与世隔绝。

Docker 项目称为“轻量级”虚拟化技术,当然该说法不严谨。
  • 不同点
1. “敏捷”和“高性能”是容器相较于虚拟机大的优势,也是它能够在 PaaS 这种更细粒度的资源管理平台上大行其道的重要原因

img

1. Docker Engine并不像 Hypervisor 那样对应用进程的隔离环境负责,也不会创建任何实体的“容器”,正真对隔离环境负责的是宿主机操作系统本身。

2. 所以,在这个对比图里,我们应该把 Docker 画在跟应用同级别并且靠边的位置。这意味着,用 户运行在容器里的应用进程,跟宿主机上的其他进程一样,都由宿主机操作系统统一管理,只不 过这些被隔离的进程拥有额外设置过的 Namespace 参数。而 Docker 项目在这里扮演的角 色,更多的是旁路式的辅助和管理工作。

3. 虚拟化技术作为应用沙盒,就必须要由 Hypervisor 来负责创建虚拟机,这个虚拟机是真实存在的,并且它里面必须运行一个完整的 Guest OS 才能执行用户的应用进程。这就不可避免地带来了额外的资源消耗和占用。

4. 容器只是运行在宿主机上的一种特殊的进程,那么多个容器之间使用的就还是同一个宿主机的操作系统内核。即:共享宿主机内核,因此,容器给应用暴露出来的攻击面是相当大的,应用“越狱”的难度自然也比虚拟机低得多。

5. 在 Linux 内核中,有很多资源和对象是不能被 Namespace 化的,典型的例子就是: 时间。

  • 总结虚拟机和容器化的优缺点
1. 虚拟机可以完美的隔离运行进程;但是对计算资源、网络和磁盘 I/O 的损耗非常大。

2. 容器化隔离不彻底;但是不存在真正的docker容器,所占资源可以忽略不计。

3. 虚拟机不仅有模拟出来的硬件机器充当沙盒,而且每个沙盒里还运行着一个完整的 Guest OS。

限制

Cgroups

1. Linux Cgroups 的全称是 Linux Control Group。它主要的作用,就是限制一个进程组能够使用的资源上限,包括 CPU、内存、磁盘、网络带宽等等。

2. Linux Cgroups 的设计还是比较易用的,简单粗暴地理解就是一个子系统目录加上一组资源限制文件的组合。而对于 Docker 等 Linux 容器项目来说,它们只需要在每个子系统下面,为每个容器创建一个控制组(即创建一个新目录),然后在启动容器进程之后,把这个进程 的 PID 填写到对应控制组的 tasks 文件中就可以了
限制进程资源
  • 在 Linux 中,Cgroups 给用户暴露出来的操作接口是文件系统,即它以文件目录的方式组织 在操作系统的 /sys/fs/cgroup 路径下。在 Ubuntu 16.04 机器里,我可以用 mount 指令把它 们展示出来,这条命令是:
$ mount -t cgroup  cpuset on /sys/fs/cgroup/cpuset type cgroup (rw,nosuid,nodev,noexec,relatime,cpuset) cpu on /sys/fs/cgroup/cpu type cgroup (rw,nosuid,nodev,noexec,relatime,cpu) cpuacct on /sys/fs/cgroup/cpuacct type cgroup (rw,nosuid,nodev,noexec,relatime,cpuacct) blkio on /sys/fs/cgroup/blkio type cgroup (rw,nosuid,nodev,noexec,relatime,blkio) memory on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,memory) 
... 
  • 可以看到,在 /sys/fs/cgroup 下面有很多诸如 cpuset、cpu、 memory 这样的子目录,也叫 子系统。这些都是这台机器当前可以被 Cgroups 进行限制的资源种类。而在子系统对应的资 源种类下,就可以看到该类资源具体可以被限制的方法。比如,对 CPU 子系统来说,我们就 可以看到如下几个配置文件,这个指令是:
$ ls /sys/fs/cgroup/cpu
cgroup.clone_children cpu.cfs_period_us cpu.rt_period_us  cpu.shares notify_on_release cgroup.procs      cpu.cfs_quota_us  cpu.rt_runtime_us cpu.stat  tasks 
  • 可以看到cfs_period 和 cfs_quota 这样的 关键词。这两个参数需要组合使用,可以用来限制进程在长度为 cfs_period 的一段时间内,只 能被分配到总量为 cfs_quota 的 CPU 时间。

  • 创建配置文件

root@ubuntu:/sys/fs/cgroup/cpu$ mkdir container 
root@ubuntu:/sys/fs/cgroup/cpu$ ls container/ 
cgroup.clone_children cpu.cfs_period_us cpu.rt_period_us  cpu.shares notify_on_release cgroup.procs      cpu.cfs_quota_us  cpu.rt_runtime_us cpu.stat  tasks 

这个目录就称为一个“控制组”。操作系统会在新创建的 container 目录下,自 动生成该子系统对应的资源限制文件。

模拟限制过程
  • 执行了一个死循环,可以把计算机的 CPU 吃到 100%,
$ while : ; do : ; done & [1] 226 
  • 用 top 指令来确认一下 CPU 有没有被打满
$ top %Cpu0 :100.0 us, 0.0 sy, 0.0 ni, 0.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st

# 在输出里可以看到,CPU 的使用率已经 100% 了(%Cpu0 :100.0 us)。
  • 通过查看 container 目录下的文件,看到 container 控制组里的 CPU quota 还没有任何限制(即:-1),CPU period 则是默认的 100 ms(100000 us):
$ cat /sys/fs/cgroup/cpu/container/cpu.cfs_quota_us  -1 $ cat /sys/fs/cgroup/cpu/container/cpu.cfs_period_us  100000 
  • 通过修改这些文件的内容来设置限制,比如向 container 组里的 cfs_quota 文件写入 20 ms(20000 us):
$ echo 20000 > /sys/fs/cgroup/cpu/container/cpu.cfs_quota_us

#意味着在每 100 ms 的时间里,被该控制组 限制的进程只能使用 20 ms 的 CPU 时间,也就是说这个进程只能使用到 20% 的 CPU 带宽。
  • 把被限制的进程的 PID 写入 container 组里的 tasks 文件,上面的设置就会对该 进程生效了:
$ echo 226 > /sys/fs/cgroup/cpu/container/tasks  
  • 我们可以用 top 指令查看一下
$ top %Cpu0 : 20.3 us, 0.0 sy, 0.0 ni, 79.7 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st

#可以看到,计算机的 CPU 使用率立刻降到了 20%(%Cpu0 : 20.3 us)

除 CPU 子系统外,Cgroups 的每一项子系统都有其独有的资源限制能力,比如:

blkio,为 块 设 备 设 定 I/O 限制,一般用于磁盘等设备;
cpuset,为进程分配单独的 CPU 核和对应的内存节点; 
memory,为进程设定内存使用的限制

Linux Cgroups 的设计还是比较易用的,简单粗暴地理解呢,它就是一个子系统目录加上一组 资源限制文件的组合。而对于 Docker 等 Linux 容器项目来说,它们只需要在每个子系统下面,为每个容器创建一个控制组(即创建一个新目录),然后在启动容器进程之后,把这个进程 的 PID 填写到对应控制组的 tasks 文件中就可以了。
而至于在这些控制组下面的资源文件里填上什么值,就靠用户执行 docker run 时的参数指定 了,比如这样一条命令:

$ docker run -it --cpu-period=100000 --cpu-quota=20000 ubuntu /bin/bash 

在启动这个容器后,我们可以通过查看 Cgroups 文件系统下,CPU 子系统中,“docker”这 个控制组里的资源限制文件的内容来确认:

$ cat /sys/fs/cgroup/cpu/docker/5d5c9f67d/cpu.cfs_period_us  
100000 

$ cat /sys/fs/cgroup/cpu/docker/5d5c9f67d/cpu.cfs_quota_us  
20000 

#此时就意味着这个 Docker 容器,只能使用到 20% 的 CPU 带宽
  • 小结
1. 通过以上讲述,能够理解,一个正在运行的 Docker 容器,其实就是一个启用了多个 Linux Namespace 的应用进程,而这个进程能够使用的资源量,则受 Cgroups 配置的限制。这也是容器技术中一个非常重要的概念,即:容器是一个“单进程”模型。

2. 由于一个容器的本质就是一个进程,用户的应用进程实际上就是容器里 PID=1 的进程,也是其 他后续创建的所有进程的父进程。这就意味着,在一个容器中,你没办法同时运行两个不同的应 用,除非你能事先找到一个公共的 PID=1 的程序来充当两个不同应用的父进程,这也是为什么 很多人都会用 systemd 或者 supervisord 这样的软件来代替应用本身作为容器的启动进程。
  • 注意事项
跟 Namespace 的情况类似,Cgroups 对资源的限制能力也有很多不完善的地方,被提 及多的自然是 /proc 文件系统的问题。比如:如果在容器里执行 top 指令,就会发现,它显示的信息居然是宿主机的 CPU 和内存数 据,而不是当前容器的数据。造成这个问题的原因就是,/proc 文件系统并不知道用户通过 Cgroups 给这个容器做了什么样 的资源限制,即:/proc 文件系统不了解 Cgroups 限制的存在。在生产环境中,这个问题必须进行修正,否则应用程序在容器里读取到的 CPU 核数、可用内存 等信息都是宿主机上的数据,这会给应用的运行带来非常大的困惑和风险。这也是在企业中,容 器化应用碰到的一个常见问题,也是容器相较于虚拟机另一个不尽如人意的地方。
  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值