我正在参与掘金创作者训练营第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
可以看到我们最开始执行的/bin/sh ,是这个容器内的1号进程,而这个容器只有2个进程在运行。看起来就像是这里的ps和/bin/sh 被docker隔离在和宿主机完全不同的地方。
Linux系统中有一种叫做namespace的技术,是Linux内核用来隔离内核资源的方式,官方介绍是namespace是对系统资源的一种封装,可以使得进程看起来拥有独立的资源一样。
通俗一点就是,在创建进程时我们可以通过为其指定namespace的方式,来限制进程所能看到的资源。
这里仅仅体现了进程视图的隔离。在Linux系统中,Linux内核为我们提供了6种不同类型的namespace
| Namespace类型 | 隔离资源 | 内核版本 |
|---|---|---|
| IPC | system 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
#执行inspect
docker inspect containerID
可以看到pid是6015
然后通过lsns -t 命令可以看到当前系统的namespace
#type 表示你要查看的namespace类型
lsns -t <type>
那么我们现在可以查看当前系统中所有的net类型的namespace
lsns -t net
可以看到在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
可以看到支持很多配置,且目录都是在/sys/fs/cgroup/下
我们可以先看看这个目录下有些啥。
可以看到有cpu、memory等子目录。以cpu为例,进入cpu目录
可以看到有很多文件,还有文件夹。文件就是对于cpu的一些配置项。大概可以进行如下参考
| 配置名称 | 作用 | |
|---|---|---|
| cpu.shares | 可出让的能获得CPU使用时间的相对值,越大占比越高 | |
| cpu.cfs_preiod_us | cfs_preiod_us用来配置时间周期长度,单位为us(微秒),默认10W us | |
| cpu.cfs_quota_us | 配置当前Cgroup在cfs_period_us时间内最多能使用的CPU时间数 | |
| cpu.stat | Cgroup内的进程使用的CPU时间统计 | |
| nr_periods | 经过cpu.cfs_preiod_us的时间周期数量 | |
| nr_throttled | 在经过的周期内,有多少次因为进程在指定的时间周期内用光了配额时间而受到限制 | |
| throttled_time | Cgroup中的进程被限制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
然后进入/sys/fs/cgroup/cpu 目录,刚刚我们已经看到了有一个docker目录,那么docker是怎么帮我们去控制的呢。
可以看到docker目录下也生成了刚刚说的那些资源限制文件,同时也有一些文件夹,而其中有一个文件夹的名称,竟然和我们刚刚看到的那个容器id是一致的,再进入进去看看
cd 69cf94e0d4b4fab31daaf38260a2190d9f01b7216203a7617f29d9a3f9a776eb
可以看到这个文件夹中也是之前说的那些资源限制文件,前文我们提到了,tasks文件记录的是使用当前文件夹中的配置的进程号。
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,它们分别有两个文件
使用联合挂载的方式,将这两个目录挂载到一个公共的目录 C
这个时候去查看目录C的内容,是能够看到A和B下的文件被合并到一起了
到这里我们知道了联合文件系统的能力,那我们为什么要提到他呢,因为docker 的创新点就是利用了这种能力,docker在镜像设计过程中引入了层(layer)的概念,这个概念,解决了业界痛点,让打包和分发更加的方便,层的概念的实现原理的就是联合文件系统
验证
#创建2个容器
docker run -d ubuntu:latest sleep 3600
#查看镜像信息
docker image inspect ubuntu:latest
可以看到其联合文件系统的存储驱动是OverlayFS。这也是当今主流的根文件系统
这个文件系统的主要结构分为upper层和lower层。docker就把lower层作为镜像层,upper层代表容器的可写层
查看2个容器的信息
先解释一下各个目录的概念
| workdir | 存放临时文件的目录,如果在容器运行过程中有文件修改就会放在这个目录中 |
|---|---|
| lowerdir | 底层镜像目录,权限是只读的 |
| upperdir | 目录中的创建、修改、删除操作都会在这一层反映出来,比如你删除了某个目录,但是其实实际没有删除,只是在upper层做了一个假的删除 |
| merged | 挂载目录,合并后的目录,也就是用户看到的目录 |
可以对比两个容器中,lowerdir 几乎是一致的,除了init目录。说明这两个容器用了同一个底层文件系统。Init 层是 Docker 项目单独生成的一个内部层,会帮我屏蔽掉容器中的目录内容,比如/etc。需要这样一层的原因是,这些文件本来属于只读的 Ubuntu 镜像的一部分,但是用户往往需要在启动容器时写入一些指定的值比如 /etc/hostname(--hostname=xxx)等,所以就需要在可读写层对它们进行修改。通过inspec 能查到修改后的内容 就是 HostnamePath, HostsPath key 对应的目录。
总结
通过“分层镜像”的设计,以 Docker 镜像为核心,来自不同公司、不同团队的技术人员被紧密地联系在了一起。而且,由于容器镜像的操作是增量式的,这样每次镜像拉取、推送的内容,比原本多个完整的操作系统的大小要小得多;而共享层的存在,可以使得所有这些容器镜像需要的总空间,也比每个镜像的总和要小。这样就使得基于容器镜像的团队协作,要比基于动则几个 GB 的虚拟机磁盘镜像的协作要敏捷得多。更重要的是,一旦这个镜像被发布,那么你在全世界的任何一个地方下载这个镜像,得到的内容都完全一致,可以完全复现这个镜像制作者当初的完整环境。这,就是容器技术“强一致性”的重要体现。