docker底层原理介绍

namespace和cgroup的简单理解

namespace:类似于编程语言的的命名空间

controll groups : controll (system resource) (for) (process)groups

Linux中的namespace

在Linux系统中,可以同时存在多用户多进程,那么对他们的运行协调管理,通过进程调度和进度管理可以解决,但是,整体资源是有限的,怎么把有限的资源(进程号、网络资源等等)合理分配给各个用户所在的进程?

WU003V.png

Linux Namespaces机制提供一种资源隔离方案。PID,IPC,Network等系统资源不再是全局性的,而是属于某个特定的Namespace。每个namespace下的资源对于其他namespace下的资源都是透明,不可见的。因此在操作系统层面上看,就会出现多个相同pid的进程。系统中可以同时存在两个进程号为0,1,2的进程,由于属于不同的namespace,所以它们之间并不冲突。而在用户层面上只能看到属于用户自己namespace下的资源,例如使用ps命令只能列出自己namespace下的进程。这样每个namespace看上去就像一个单独的Linux系统。

WU07HH.png

​ 命名空间建立系统的不同视图, 对于每一个命名空间,从用户看起来,应该像一台单独的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的镜像层和容器层如图所示:

WUrj81.jpg

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/"

WU4wXd.png

实验过程

查看容器的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找到对应的下一层了,依次类推,我们能知道所有的层级结构;

WaPoh8.png

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