什么,你还不知道容器的本质?

606 阅读11分钟

我正在参与掘金创作者训练营第6期, 点击了解活动详情

相信在2022年,应该没有开发者会不知道docker吧,只要一提到docker,大家都会第一时间想到,“哦,容器”。是的,在如今docker几乎已经成为了容器的代名词,但是docker其实并不能代表容器,它只是容器技术的一个产品。而容器技术的核心原理并不是由docker公司提出的,它是Linux操作系统本身就支持的,docker公司只是新瓶装旧酒,利用了Linux系统的Namespace技术、CGroups技术、Union FS联合文件系统技术,从而有了现今所看到的容器。接下来本文会一一给大家讲述这三种技术分别是什么,在这些技术中,大家也能发现docker火起来的原因。

容器是什么

先讲结论,容器的本质就是一种特殊的进程。带着这个结论我们可以往下继续追溯,为什么我们会说容器的本质是一种特殊的进程

Namespace技术

先来看个例子

# centos 7.0 执行docker指令
docker run -it busybox /bin/sh

#执行完成后会进入busybox的bash界面
#输入ps指令,你会看到 /bin/sh的进程号是1 
ps

image-20220725231657439

可以看到我们最开始执行的/bin/sh ,是这个容器内的1号进程,而这个容器只有2个进程在运行。看起来就像是这里的ps和/bin/sh 被docker隔离在和宿主机完全不同的地方。

Linux系统中有一种叫做namespace的技术,是Linux内核用来隔离内核资源的方式,官方介绍是namespace是对系统资源的一种封装,可以使得进程看起来拥有独立的资源一样。

通俗一点就是,在创建进程时我们可以通过为其指定namespace的方式,来限制进程所能看到的资源。

这里仅仅体现了进程视图的隔离。在Linux系统中,Linux内核为我们提供了6种不同类型的namespace

Namespace类型隔离资源内核版本
IPCsystem V IPC 和posix消息队列2.6.19
net网络设备、网络协议栈、网络端口等2.6.29
pid进程2.6.14
mnt挂载点2.4.19
uts主机和域名2.6.19
usr用户和用户组3.8

一些实操

dockers提供了inspect命令来帮助我们获取容器/镜像的元数据

我们可以先看看刚刚创建的那个容器在系统中的真实pid

# 先拿到container id
docker ps -ls

image-20220815224258073

#执行inspect
docker inspect containerID

image-20220815224430854

可以看到pid是6015

然后通过lsns -t 命令可以看到当前系统的namespace

#type 表示你要查看的namespace类型
lsns -t <type>

那么我们现在可以查看当前系统中所有的net类型的namespace

lsns -t net

image-20220815224728209

可以看到在PID那一栏,只有2个进程id,一个是1,也就是当前系统默认的net的namespace使用的。另一个进程id是6015,也就是我们刚刚所创建的容器使用的。

由此可见,容器最基本的原理其实就是利用namespace技术,对进程进行了一些资源视图的限制。所以容器的本质就是一个进程

CGroups技术

上面讲完了namespace技术,我们大致对容器的基本实现有了一定的了解,但是有没有想过docker是怎么去控制进程所能使用的资源的量的呢。既然有namesapce技术来控制进程能访问的资源,那么肯定需要一个东西来为进程进行资源限制。否则容器中的1号进程,在宿主机上,虽然进程视图被隔离起来了,但是它还是能和宿主机抢占资源,甚至把所有资源吃完。这显然有一点不合理。所以就有了Linux Cgroups。

Linux Cgroups 是Linux内核中用来为进程设置资源限制的一个重要功能,它能够限制一个进程组能够使用资源的上限,包括CPU、内存、磁盘、网络带宽等

那么CGroups技术又是怎样去控制进程的呢。Linux系统中是以配置文件的方式为我们提供的配置

首先我们可以先查看当前系统的支持的CGroups接口。

mount -t cgroup

image-20220817231604021

可以看到支持很多配置,且目录都是在/sys/fs/cgroup/下

我们可以先看看这个目录下有些啥。

image-20220817233726384

可以看到有cpu、memory等子目录。以cpu为例,进入cpu目录

image-20220817233928297

可以看到有很多文件,还有文件夹。文件就是对于cpu的一些配置项。大概可以进行如下参考

配置名称作用
cpu.shares可出让的能获得CPU使用时间的相对值,越大占比越高
cpu.cfs_preiod_uscfs_preiod_us用来配置时间周期长度,单位为us(微秒),默认10W us
cpu.cfs_quota_us配置当前Cgroup在cfs_period_us时间内最多能使用的CPU时间数
cpu.statCgroup内的进程使用的CPU时间统计
nr_periods经过cpu.cfs_preiod_us的时间周期数量
nr_throttled在经过的周期内,有多少次因为进程在指定的时间周期内用光了配额时间而受到限制
throttled_timeCgroup中的进程被限制CPU的总用时,单位是ns

还能看到docker,kubepods的文件夹,因为我本地安装了docker和k8s,前面说到容器的资源限制技术就是由CGroups技术控制的,具体生效的原理就是,你在这个cpu目录下创建一个子目录,这个子目录中会自动生成对应控制cpu的资源限制文件,而tasks文件记录的就是你要使用当前配置的进程号

小小的验证

我们可以验证一下,同样先创建一个docker容器,

# centos 7.0 执行docker指令
docker run -it busybox /bin/sh
#获取到container id
docker ps -ls
#查看容器元数据
docker inspect containerId

记录下这个容器的id和宿主机上的pid

image-20220817234956005

然后进入/sys/fs/cgroup/cpu 目录,刚刚我们已经看到了有一个docker目录,那么docker是怎么帮我们去控制的呢。

image-20220817235242122

可以看到docker目录下也生成了刚刚说的那些资源限制文件,同时也有一些文件夹,而其中有一个文件夹的名称,竟然和我们刚刚看到的那个容器id是一致的,再进入进去看看

cd 69cf94e0d4b4fab31daaf38260a2190d9f01b7216203a7617f29d9a3f9a776eb

image-20220817235533009

可以看到这个文件夹中也是之前说的那些资源限制文件,前文我们提到了,tasks文件记录的是使用当前文件夹中的配置的进程号。

image-20220817235806024

cat之后发现,输出的进程id就是我们刚刚创建的那个容器在宿主机中的pid。

总结

到这里我们基本上就理解了docker是如何为容器做资源限制的,就是创建进程后,先使用namespace技术进行资源隔离,然后再利用CGroups技术为容器创建资源限制的配置文件,将对应的进程id写入其对应文件夹的tasks文件中就OK了。至于各种资源限制文件填写什么值,用户是可以在执行docker run 时的参数中进行指定。

#
docker run -it --cpu-period=100000 --cpu-quota=20000 ubuntu /bin/bash
#那么你可以看到其配置文件中的cpu.cfs_period_us=100000,cpu.cfs_quota_us=20000

联合文件系统(Union FS)

什么是联合文件系统,联合文件系统又做了些什么事情。这一章节会一一解答。

首先,根据上文我们可以知道,docker创建的容器其实只是一个特殊的进程,其利用了namespace技术和Cgroups技术。namespace技术提供隔离性,Cgroups技术分配资源额度,但是我们好像忽略了一点,namespace技术如果只隔离了进程的可见性,那么文件怎么办,我们在容器为啥能看到文件系统也是被隔离的呢

众所周知,在Linux系统中,一切皆文件,Linux系统有2类文件系统,一类是bootfs,一类是rootfs,bootfs是引导文件系统,在启动时会引导加载内核,当内核程序被加载到内存中后会unmount bootfs。rootfs就是提供/dev、/proc、/bin、/etc等标准目录的文件系统

docker利用了rootfs的概念,为容器提供了一个基础镜像(FROM语句),这个镜像其实就是一个标准的文件系统。

为什么要提到联合文件系统

容器在创建后,使用mount namespace限制进程文件系统的挂载点视图后,其实是不会生效的,因为这个程序的新视图(文件系统视图)并没有挂载到新的目录上去,导致其查看相关系统目录,看到的还是宿主机的文件目录,必须要挂载新的文件系统才能让新进程看到全新的文件系统视图。

联合文件系统,它首先是一个文件系统,联合是它的特点(将多个不同位置的目录联合挂载到同一个目录下)。举个例子,现在有两个目录 A 和 B,它们分别有两个文件

image-20220829203955829.png

使用联合挂载的方式,将这两个目录挂载到一个公共的目录 C

image-20220829204013111.png

这个时候去查看目录C的内容,是能够看到A和B下的文件被合并到一起了

image-20220829204103889.png

到这里我们知道了联合文件系统的能力,那我们为什么要提到他呢,因为docker 的创新点就是利用了这种能力,docker在镜像设计过程中引入了层(layer)的概念,这个概念,解决了业界痛点,让打包和分发更加的方便,层的概念的实现原理的就是联合文件系统

验证

#创建2个容器
docker run -d ubuntu:latest sleep 3600
#查看镜像信息
docker image inspect ubuntu:latest

image-20220829205948822.png

可以看到其联合文件系统的存储驱动是OverlayFS。这也是当今主流的根文件系统

这个文件系统的主要结构分为upper层和lower层。docker就把lower层作为镜像层,upper层代表容器的可写层

image-20220829211055832.png

查看2个容器的信息

image-20220829211718545.png

image-20220829211648856.png

先解释一下各个目录的概念

workdir存放临时文件的目录,如果在容器运行过程中有文件修改就会放在这个目录中
lowerdir底层镜像目录,权限是只读的
upperdir目录中的创建、修改、删除操作都会在这一层反映出来,比如你删除了某个目录,但是其实实际没有删除,只是在upper层做了一个假的删除
merged挂载目录,合并后的目录,也就是用户看到的目录

可以对比两个容器中,lowerdir 几乎是一致的,除了init目录。说明这两个容器用了同一个底层文件系统。Init 层是 Docker 项目单独生成的一个内部层,会帮我屏蔽掉容器中的目录内容,比如/etc。需要这样一层的原因是,这些文件本来属于只读的 Ubuntu 镜像的一部分,但是用户往往需要在启动容器时写入一些指定的值比如 /etc/hostname(--hostname=xxx)等,所以就需要在可读写层对它们进行修改。通过inspec 能查到修改后的内容 就是 HostnamePath, HostsPath key 对应的目录。

总结

通过“分层镜像”的设计,以 Docker 镜像为核心,来自不同公司、不同团队的技术人员被紧密地联系在了一起。而且,由于容器镜像的操作是增量式的,这样每次镜像拉取、推送的内容,比原本多个完整的操作系统的大小要小得多;而共享层的存在,可以使得所有这些容器镜像需要的总空间,也比每个镜像的总和要小。这样就使得基于容器镜像的团队协作,要比基于动则几个 GB 的虚拟机磁盘镜像的协作要敏捷得多。更重要的是,一旦这个镜像被发布,那么你在全世界的任何一个地方下载这个镜像,得到的内容都完全一致,可以完全复现这个镜像制作者当初的完整环境。这,就是容器技术“强一致性”的重要体现。