docker底层原理介绍
namespace和cgroup的简单理解
namespace:类似于编程语言的的命名空间
controll groups : controll (system resource) (for) (process)groups
Linux中的namespace
在Linux系统中,可以同时存在多用户多进程,那么对他们的运行协调管理,通过进程调度和进度管理可以解决,但是,整体资源是有限的,怎么把有限的资源(进程号、网络资源等等)合理分配给各个用户所在的进程?
Linux Namespaces机制提供一种资源隔离方案。PID,IPC,Network等系统资源不再是全局性的,而是属于某个特定的Namespace。每个namespace下的资源对于其他namespace下的资源都是透明,不可见的。因此在操作系统层面上看,就会出现多个相同pid的进程。系统中可以同时存在两个进程号为0,1,2的进程,由于属于不同的namespace,所以它们之间并不冲突。而在用户层面上只能看到属于用户自己namespace下的资源,例如使用ps命令只能列出自己namespace下的进程。这样每个namespace看上去就像一个单独的Linux系统。
命名空间建立系统的不同视图, 对于每一个命名空间,从用户看起来,应该像一台单独的Linux计算机一样,有自己的init进程(PID为0),其他进程的PID依次递增,A和B空间都有PID为0的init进程,子容器的进程映射到父容器的进程上,父容器可以知道每一个子容器的运行状态,而子容器与子容器之间是隔离的。
namespace |
引入的相关内核版本 |
被隔离的全局系统资源 |
在容器语境下的隔离效果 |
Mount namespaces |
Linux 2.4.19 |
文件系统挂接点 |
将一个文件系统的顶层目录挂到另一个文件系统的子目录上,使它们成为一个整体,称为挂载。把该子目录称为挂载点。 Mount namespace用来隔离文件系统的挂载点, 使得不同的mount namespace拥有自己独立的挂载点信息,不同的namespace之间不会相互影响,这对于构建用户或者容器自己的文件系统目录非常有用。 |
UTS namespaces |
Linux 2.6.19 |
nodename 和 domainname |
UTS,UNIX Time-sharing System namespace提供了主机名和域名的隔离。能够使得子进程有独立的主机名和域名(hostname),这一特性在Docker容器技术中被用到,使得docker容器在网络上被视作一个独立的节点,而不仅仅是宿主机上的一个进程。 |
IPC namespaces |
Linux 2.6.19 |
特定的进程间通信资源,包括System V IPC 和 POSIX message queues |
IPC全称 Inter-Process Communication,是Unix/Linux下进程间通信的一种方式,IPC有共享内存、信号量、消息队列等方法。所以,为了隔离,我们也需要把IPC给隔离开来,这样,只有在同一个Namespace下的进程才能相互通信。如果你熟悉IPC的原理的话,你会知道,IPC需要有一个全局的ID,即然是全局的,那么就意味着我们的Namespace需要对这个ID隔离,不能让别的Namespace的进程看到。 |
PID namespaces |
Linux 2.6.24 |
进程 ID 数字空间 (process ID number space) |
PID namespaces用来隔离进程的ID空间,使得不同pid namespace里的进程ID可以重复且相互之间不影响。PID namespace可以嵌套,也就是说有父子关系,在当前namespace里面创建的所有新的namespace都是当前namespace的子namespace。父namespace里面可以看到所有子孙后代namespace里的进程信息,而子namespace里看不到祖先或者兄弟namespace里的进程信息。 |
Network namespaces |
始于Linux 2.6.24 完成于 Linux 2.6.29 |
网络相关的系统资源 |
每个容器用有其独立的网络设备,IP 地址,IP 路由表,/proc/net 目录,端口号等等。这也使得一个 host 上多个容器内的同一个应用都绑定到各自容器的 80 端口上。 |
User namespaces |
始于 Linux 2.6.23 完成于 Linux 3.8) |
用户和组 ID 空间 |
User namespace用来隔离user权限相关的Linux资源,包括user IDs and group IDs。这是目前实现的namespace中最复杂的一个,因为user和权限息息相关,而权限又事关容器的安全,所以稍有不慎,就会出安全问题。在不同的user namespace中,同样一个用户的user ID 和group ID可以不一样,换句话说,一个用户可以在父user namespace中是普通用户,在子user namespace中是超级用户 |
linux中的cgroup
(1)有了namespace为什么还要cgroup:
Docker 容器使用 linux namespace 来隔离其运行环境,使得容器中的进程看起来就像一个独立环境中运行一样。但是,光有运行环境隔离还不够,因为这些进程还是可以不受限制地使用系统资源,比如网络、磁盘、CPU以及内存 等。关于其目的,一方面,是为了防止它占用了太多的资源而影响到其它进程;另一方面,在系统资源耗尽的时候,linux 内核会触发 OOM,这会让一些被杀掉的进程成了无辜的替死鬼。因此,为了让容器中的进程更加可控,Docker 使用 Linux cgroups 来限制容器中的进程允许使用的系统资源。
(2)原理
Linux Cgroup 可为系统中所运行任务(进程)的用户定义组群分配资源 — 比如 CPU 时间、系统内存、网络带宽或者这些资源的组合。可以监控管理员配置的 cgroup,拒绝 cgroup 访问某些资源,甚至在运行的系统中动态配置 cgroup。所以,可以将 controll groups 理解为 controller (system resource) (for) (process)groups,也就是是说它以一组进程为目标进行系统资源分配和控制。它主要提供了如下功能:
- Resource limitation: 限制资源使用,比如内存使用上限以及文件系统的缓存限制。
- Prioritization: 优先级控制,比如:CPU利用和磁盘IO吞吐。
- Accounting: 一些审计或一些统计,主要目的是为了计费。
- Controll: 挂起进程,恢复执行进程。
使用 cgroup,系统管理员可更具体地控制对系统资源的分配、优先顺序、拒绝、管理和监控。可更好地根据任务和用户分配硬件资源,提高总体效率。
在实践中,系统管理员一般会利用CGroup做下面这些事:
- 隔离一个进程集合(比如:nginx的所有进程),并限制他们所消费的资源,比如绑定CPU的核。
- 为这组进程分配其足够使用的内存
- 为这组进程分配相应的网络带宽和磁盘存储限制
- 限制访问某些设备(通过设置设备的白名单)
Docker底层存储机制
docker的老的存储引擎AUFS
什么是AUFS
AUFS是一种 Union File System,全称是Advanced Union File System,主要是把不同物理位置的目录合并mount到同一个目录下。
AUFS的读写操作
将多个目录mount成一个后,读写操作如下:
-
默认情况下,最上层的目录是读写层,下面可以有一个或者多个只读层
-
读文件时,打开文档使用O_RDONLY选项,从最上面一个文件开始逐层往下找,打开第一个找到的文件,并读取其中的内容
-
写文件时,打开文件使用O_WRONLY和O_RDWR选项
-
- 如果在最上层找到文件,直接打开文件
- 如果是在只读层中找到,则将该文件复制到最上层,然后在最上层打开这个copy的文件(如果读写的文件比较大,这个过程耗费的时间就会比较久)
-
删除文件,在最上层创建一个whiteout文件,就是在原来文件名字前面加上.wh
Docker使用的存储引擎
Docker默认的存储引擎
AUFS是Docker18.06及更早版本的首选存储驱动程序,之后的docker版本默认存储引擎是Overlay2。需要注意的是:不同的存储引擎是需要文件系统支持的!
三个问题?
- 基于镜像A创建镜像B时是否会拷贝镜像中的所有文件?
- 基于镜像创建容器时是否会拷贝镜像中的所有文件到容层?
- 容器和镜像在结构上有什么区别?
Dockers层的实现
构建docker镜像时一般使用dockerfile的方式(也可以在容器里面修改,然后commit成新的镜像,前者可以看到具体的实现,后者除了作者,没人知道如何实现的,所以一般使用前者的方式),而docker的每行命令,都会生成一个层,即使没做什么事。在写dockerfile时,要避免生成很多层,因为使用存储引擎在进行文件查找时,会遍历层,会消耗性能
docker的镜像层和容器层如图所示:
Image layers是base image层,该层是不可改动的(例子中是ubuntu:15.04镜像),是静态的(理解为是不会发生变化的);
当我们基于image layer运行容器时(比如基于ubuntu:15.04镜像运行容器),Docker会创建一个读写层,也就是图中的Container layer,该读写层位于所有层的最上面。
我们对容器的更改操作都会在该读写层中,当我们执行 docker commit 命令去保存我们的改动,生成新的镜像时,就相当于给该读写层copy了一下,放到了Image layers中了
Docker实现文件的增删改
copy-on-write写时复制策略
我们首先需要了解Docker中使用的copy-on-write策略,该策略不是docker设计出的,而是最初沿用AUFS存储引擎的策略,该部分的内容见Docker的老存储引擎AUFS
allocate-on-demand用时分配
用时分配是应用在原本没有该文件的场景,只有在新写入一个文件时才分配空间,这样可以提高存储资源的利用率。比如:启动一个容器时,并不会为这个容器预分配一些磁盘空间,只有当新文件写入时,才按需分配新空间
如此设计的好处
- 最显而易见的好处是减少了存储空间;镜像被分成了很多静态可读层,而这些层是可以共享的;当基于本地一个已有的镜像去构建一个新的镜像时,只需要添加新的层,原有的层可以直接使用
- 启动容器变得轻量快速,因为启动容器时,只是在已有的层上添加了一层读写层,其他层都是用到的时候才会去搜索,遵循copy-on-write原则
Docker存储引擎之Overlay2的详细过程
这个主要是从知乎的答主上面摘抄的内容, 以供学习;
前面介绍AUFS主要是因为该论文是2017年SEC会议收集的,那时候docker还是使用的AUFS引擎,现在docker已经默认使用Overlay2了,但是对于核心的思想,都是一样的。下面这一部分会以一个具体的例子来介绍docker存储中的一些id的作用,以及image是如何和layer映射到一起的。实验步骤如下:
题主用的jenkins, 我也用jenkins来展开这个实验吧;
Dockers安装部署Jenkins
查看docker的jenkins镜像版本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
bomir@morn:~$ docker search jenkins
NAME DESCRIPTION STARS OFFICIAL AUTOMATED
jenkins Official Jenkins Docker image 5245 [OK]
jenkins/jenkins The leading open source automation server 2599
jenkinsci/blueocean https://jenkins.io/projects/blueocean 628
jenkinsci/jenkins Jenkins Continuous Integration and Delivery … 390
jenkins/jnlp-slave a Jenkins agent which can connect to Jenkins… 146 [OK]
jenkinsci/jnlp-slave A Jenkins slave using JNLP to establish conn… 133 [OK]
jenkinsci/slave Base Jenkins slave docker image 67 [OK]
jenkins/slave base image for a Jenkins Agent, which includ… 47 [OK]
jenkinsci/ssh-slave A Jenkins SSH Slave docker image 44 [OK]
jenkins/ssh-slave A Jenkins slave using SSH to establish conne… 37 [OK]
cloudbees/jenkins-enterprise CloudBees Jenkins Enterprise (Rolling releas… 34 [OK]
h1kkan/jenkins-docker 🤖 Extended Jenkins docker image, bundled wi… 29
xmartlabs/jenkins-android Jenkins image for Android development. 28 [OK]
openshift/jenkins-2-centos7 A Centos7 based Jenkins v2.x image for use w… 23
cloudbees/jenkins-operations-center CloudBees Jenkins Operation Center (Rolling … 14 [OK]
jenkins/ssh-agent Docker image for Jenkins agents connected ov… 11
openshift/jenkins-slave-maven-centos7 A Jenkins slave image with OpenJDK 1.8 and M… 11
openshift/jenkins-slave-base-centos7 A Jenkins slave base image. DEPRECATED: see … 7
trion/jenkins-docker-client Jenkins CI server with docker client 6 [OK]
publicisworldwide/jenkins-slave Jenkins Slave based on Oracle Linux 5 [OK]
openshift/jenkins-1-centos7 DEPRECATED: A Centos7 based Jenkins v1.x ima… 4
ansibleplaybookbundle/jenkins-apb An APB which deploys Jenkins CI 1 [OK]
amazeeio/jenkins-slave A jenkins slave that connects to a master vi… 0 [OK]
mashape/jenkins Just a jenkins image with the AWS cli added … 0 [OK]
jameseckersall/jenkins docker-jenkins (based on openshift jenkins 2… 0 [OK]
|
远程拉取镜像
不标注tag代表是获取最新的;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
bomir@morn:~$ docker pull jenkins
Using default tag: latest
latest: Pulling from library/jenkins
55cbf04beb70: Pull complete
1607093a898c: Pull complete
9a8ea045c926: Pull complete
d4eee24d4dac: Pull complete
c58988e753d7: Pull complete
794a04897db9: Pull complete
70fcfa476f73: Pull complete
0539c80a02be: Pull complete
54fefc6dcf80: Pull complete
911bc90e47a8: Pull complete
38430d93efed: Pull complete
7e46ccda148a: Pull complete
c0cbcb5ac747: Pull complete
35ade7a86a8e: Pull complete
aa433a6a56b1: Pull complete
841c1dd38d62: Pull complete
b865dcb08714: Pull complete
5a3779030005: Pull complete
12b47c68955c: Pull complete
1322ea3e7bfd: Pull complete
Digest: sha256:eeb4850eb65f2d92500e421b430ed1ec58a7ac909e91f518926e02473904f668
Status: Downloaded newer image for jenkins:latest
docker.io/library/jenkins:latest
|
创建挂载目录
挂载目录用于映射jenkins的jenkins_home的配置文件等信息;
1
2
3
4
5
6
7
|
# 挂载目录放在home下
bomir@morn:~$ sudo mkdir /home/jenkins
[sudo] password for bomir:
# 该目录需要设置权限, 否则启动容器会报权限错误
bomir@morn:~$ sudo chown -R 1000:1000 /home/jenkins/
|
启动容器
1
2
3
|
# 运行镜像启动命令
docker run -p 8000:8000 -p 50000:50000 -v /home/jenkins:/var/jenkins_home --name jenkins --restart always --privileged=true -u root jenkins
|
-p : 映射端口,宿主机端口:容器端口
-v : 挂载,宿主机目录:容器目录
–name : 自定义容器名
-u : 权限用户名
–privileged : 使用该参数,container内的root拥有真正的root权限,否则,container(容器)内的root只是外部的一个普通用户权限,privileged启动的容器可以看到很多host上的设备,并且可以执行mount,甚至允许你在docker容器内启动docker容器
-p 50000:50000 : 如果您在其他机器上设置了一个或多个基于JNLP的Jenkins代理程序,而这些代理程序又与 jenkinsci/blueocean 容器交互(充当“主”Jenkins服务器,或者简称为“Jenkins主”), 则这是必需的。默认情况下,基于JNLP的Jenkins代理通过TCP端口50000与Jenkins主站进行通信。
首先修改hudson.model.UpdateCenter.xml配置文件
1
2
3
4
5
6
7
|
默认路径
http://updates.jenkins-ci.org/update-center.json
改成路径
http://mirror.xmission.com/jenkins/updates/update-center.json
|
完成后修改 /updates/default.json 配置文件
1
2
3
4
|
默认路径
"connectionCheckUrl":"http://www.google.com/"
改为路径
"connectionCheckUrl":"http://www.baidu.com/"
|
实验过程
查看容器的id
1
2
3
|
bomir@morn:~$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
ba5b4722fb02 jenkins "/bin/tini -- /usr/l…" About an hour ago Up 55 minutes 0.0.0.0:8000->8000/tcp, :::8000->8000/tcp, 0.0.0.0:50000->50000/tcp, :::50000->50000/tcp, 8080/tcp jenkins
|
可以看到容器的id是ba5b4722fb02;
查看镜像的id
1
2
3
4
5
6
7
8
9
10
11
12
|
bomir@morn:~$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
zzyy/centos latest 98330358de77 9 days ago 209MB
nginx latest 4cdc5dd7eaad 2 weeks ago 133MB
tomcat latest 36ef696ea43d 2 weeks ago 667MB
redis 5.0 aec43d10ed3b 3 weeks ago 98.4MB
redis latest 08502081bff6 3 weeks ago 105MB
mysql 5.6 e1aa75e199d7 3 weeks ago 303MB
hello-world latest d1165f221234 4 months ago 13.3kB
centos latest 300e315adb2f 7 months ago 209MB
redis 6.0.4 36304d3b4540 13 months ago 104MB
jenkins latest cd14cecfdb3a 3 years ago 696MB
|
查看镜像文件元数据
image layer
a.到 /var/lib/docker/image/overlay2/imagedb/content/sha256 目录下找到对应镜像文件的元数据(这里是用Step3找到的镜像ID),cat该文件,并找到rootfs部分的diff_ids
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
root@morn:/var/lib/docker/image/overlay2/imagedb/content/sha256# cat cd14cecfdb3a657ba7d05bea026e7ac8b9abafc6e5c66253ab327c7211fa6281 | python3 -m json.tool | grep rootfs -A 50
"rootfs": {
"type": "layers",
"diff_ids": [
"sha256:3b10514a95bec77489a57d6e2fbfddb7ddfdb643907470ce5de0f1b05c603706",
"sha256:719d45669b35360e3ca0d53d159c42ca9985eb925a6b28ac38d0ded39a1314e8",
"sha256:ce6466f43b110e66d7d3a72600262a1f1b015d9a64aad5f133f081868d4825fc",
"sha256:fa0c3f992cbd10a0569ed212414b50f1c35d97521f7e4a9e55a9abcf47ca77e2",
... // 中间省略掉
"sha256:0577e068c587d352efe5dd72477ae6927626840d167fbbf59e17241b8f50a127"
]
}
}
|
这里一共有20层,我将中间的一些层去掉了,但是不妨碍分析。最上面的3b10514a95bec774…是Image的layer层的底层,那既然找到了diff_id,我们再去看下layer层的实现:
b.在/var/lib/docker/image/overlay2/layerdb/sha256目录下去查看layer信息(sha256目录保存的是可读层的信息,mount目录保存的读写层的信息)
1
2
3
4
5
6
7
8
9
10
11
12
13
|
root@morn:/var/lib/docker/image/overlay2/layerdb/sha256# ls -la
total 268
drwxr-xr-x 65 root root 12288 Jul 21 10:30 .
drwx------ 5 root root 4096 Jul 9 21:31 ..
drwx------ 2 root root 4096 Jul 21 10:30 0373335894092868f06432433f14881bd6f09d851931d6e5090601a64e0466f3
drwx------ 2 root root 4096 Jul 20 18:29 0a0f29e43c3a5555d675d253ff51d73e4d238bd558f11ae9b63d2a2a14251b36
drwx------ 2 root root 4096 Jul 21 10:30 0b5e1c633ad7fa60f5185ff00ccbff9af3608ba336dc7c01868f9cd0dd8a7137
drwx------ 2 root root 4096 Jul 9 23:45 2e17f8770b4ad4ce56357edc8dea7329f376803e703201f45141d044634c20e3
drwx------ 2 root root 4096 Jul 9 23:45 33d9996f6196b7459c2fff2af3c499c293df089085845401bec2ebda281b86d8
drwx------ 2 root root 4096 Jul 13 22:07 35266b37f23fb2e4e3973c2e073adc021324b3d2c18ed3909e1bd407886d8e95
drwx------ 2 root root 4096 Jul 21 10:29 3b10514a95bec77489a57d6e2fbfddb7ddfdb643907470ce5de0f1b05c603706
drwx------ 2 root root 4096 Jul 9 23:55
...
|
可以看到, 这里只能找到3b10514a95bec7…这个image的底层, 其他的layer在这个目录下都找不到,这是为什么呢?
这是由于Docker使用了chainID来保存layer,计算公式是:chainID=sha256sum(H(chainID) diffid),其中如果是最底层layer,那么其chainID=diff_id。所以在sha256目录下,我们只会看到Image的最底层layer,而看不到其他的layer。 (sha256下的目录是以chainID命名的,而不是以diff_id命名的,所以需要转换一下)
c.计算3b10514a95bec7…的下一层的chainID
1
2
|
root@morn:/var/lib/docker/image/overlay2/layerdb/sha256# echo -n "sha256:3b10514a95bec77489a57d6e2fbfddb7ddfdb643907470ce5de0f1b05c603706 sha256:719d45669b35360e3ca0d53d159c42ca9985eb925a6b28ac38d0ded39a1314e8" | sha256sum
4a495dbc04bd205c728297a08cf203988e91caeafe4b21fcad94c893a53d96dc
|
可以看到下一层的chainID是 4a495dbc04bd205c728297a08cf203988e91caeafe4b21fcad94c893a53d96dc
, 这样我们能从layerdb/sha256找到对应的下一层了,依次类推,我们能知道所有的层级结构;
d.Image目录下的layerdb目录中保存的是元数据,那么我们需要到overlay2目录中去找到对应的真实目录,那么这个之间的映射关系是怎么得到的呢?
这个时候cache-id就排上用场了,我们可以打印出该chainID的cache-id,并到overlay2目录中去找**(dockr会创建/var/lib/docker/{存储引擎名称}老保存实际的数据)**
1
2
3
4
5
6
7
8
|
# 到最底层3b10514a95bec7...中打印cache-id
root@morn:/var/lib/docker/image/overlay2/layerdb/sha256/3b10514a95bec77489a57d6e2fbfddb7ddfdb643907470ce5de0f1b05c603706# cat cache-id
ba567e2e707034d5443383bc1d8bae7688e1918e1cd66eabfe2213100677d2f4root@morn:/var/lib/docker/image/overlay2/layerdb/sha256/3b10514a95bec77489a57d6e2fbfddb7ddfdb643907470ce5de
#到/var/lib/dockr/overlay2中找到对应的目录
root@morn:/var/lib/docker/overlay2# ls -la | grep ba567e2e
drwx-----x 3 root root 4096 Jul 21 10:29 ba567e2e707034d5443383bc1d8bae7688e1918e1cd66eabfe2213100677d2f4
|
可以看到已经找到对应的目录了,该目录中记录了所有的diff;
e.在Step2中找到的容器ID的元数据其实是保存在/var/lib/docker/image/overlay2/layerdb/mount/目录下
该目录保存的是docker的容器读写层。这其实也对应了docker的存储实现机制,镜像都是只读层,当运行一个容器时,会产生一个容器层,该层即是读写层。
1
2
3
4
|
root@morn:/var/lib/docker/image/overlay2/layerdb/mounts# ls -la | grep ba5b4
drwxr-xr-x 2 root root 4096 Jul 21 10:46 ba5b4722fb0209f5201b0ba83d2dc139be545fd50d45bccb127de5d20a687183
root@morn:/var/lib/docker/image/overlay2/layerdb/mounts# pwd
/var/lib/docker/image/overlay2/layerdb/mounts
|
查看该目录下的文件:
1
2
3
4
5
|
root@morn:/var/lib/docker/image/overlay2/layerdb/mounts/ba5b4722fb0209f5201b0ba83d2dc139be545fd50d45bccb127de5d20a687183# ls -l
total 12
-rw-r--r-- 1 root root 69 Jul 21 10:46 init-id
-rw-r--r-- 1 root root 64 Jul 21 10:46 mount-id
-rw-r--r-- 1 root root 71 Jul 21 10:46 parent
|
init-id 和 mount-id里面的内容基本一样,都是记录该容器层的cache-id,用来映射overlays目录下真实的存储层;
parent文件是记录上一层的layer层,里面记录的chainID;
参考资料
https://cloud.tencent.com/developer/article/1334962
https://zhuanlan.zhihu.com/p/334619552
https://www.jianshu.com/p/3826859a6d6e
https://www.cnblogs.com/nhdlb/p/12576273.html#_label0