容器核心技术


文章目录

  • 容器核心技术
  • 前言
  • 一、容器的主要特性
  • 1.隔离性
  • 1)Linux fork函数
  • 2)Linux内核namespace机制
  • a. linux 中 namespace详情
  • b. pid隔离产生的问题
  • c. 解决:文件系统隔离(挂载)问题:1号进程
  • d.网路隔离
  • 3)Cgroup机制
  • a. Cgroup是什么?
  • b. Cgroup主要功能
  • c. Cgroup主要功能相关概念介绍
  • d. subsystem(子系统)
  • e. linux实例
  • f. cgroup文件系统
  • g.linux测试实例(cpu)
  • h.linux测试限制内存使用量
  • 二、容器的框架结构
  • 1) 架构图
  • a.docker client
  • b.docker daemon
  • c.docker server
  • d.engine
  • e.job
  • f.docker registry
  • g.Graph
  • h.driver
  • i.libcontainer
  • j.docker container
  • 2) docker镜像分层特点
  • a.分层图
  • b.容器拷贝技术
  • 3) 架构总结
  • a.CRI规范
  • b.ORI规范
  • 三、(高低级)容器运行时源码分析
  • 1) runc
  • 总结



前言

这周看书发现目录有个关于容器核心技术的篇章。发现对其知识了解较浅,便结合书还有网上找到资料做了下知识梳理和总结。


一、容器的主要特性

容器 foreign layers_子进程


容器 foreign layers_容器_02


虚拟机和容器技术对比,虚拟机需要通过两次系统调用(一次自己的虚拟cpu,一次通过管理程序操作宿主机cpu来执行x86指令,占用系统磁盘空间大。而容器是调用一个内核(宿主机操作系统),产生的隐患就是隔离性的问题。

1.隔离性

1)Linux fork函数

一、fork入门知识

一个进程,包括代码、数据和分配给进程的资源。fork()函数通过系统调用创建一个与原来进程几乎完全相同的进程,也就是两个进程可以做完全相同的事,但如果初始参数或者传入的变量不同,两个进程也可以做不同的事。
一个进程调用fork()函数后,系统先给新的进程分配资源,例如存储数据和代码的空间。然后把原来的进程的所有值都复制到新的新进程中,只有少数值与原来的进程的值不同。相当于克隆了一个自己。
pid_t fpid; //fpid表示fork函数返回的值  
    int count=0;  
    fpid=fork();

在语句fpid=fork()之前,只有一个进程在执行这段代码,但在这条语句之后,就变成两个进程在执行了,这两个进程的几乎完全相同
fork调用的一个奇妙之处就是它仅仅被调用一次,却能够返回两次,它可能有三种不同的返回值:

  1. 在父进程中,fork返回新创建子进程的进程ID;
  2. 在子进程中,fork返回0;
  3. 如果出现错误,fork返回一个负值;

在fork函数执行完毕后,如果创建新进程成功,则出现两个进程,一个是子进程,一个是父进程。在子进程中,fork函数返回0,在父进程中,fork返回新创建子进程的进程ID。我们可以通过fork返回的值来判断当前进程是子进程还是父进程。

2)Linux内核namespace机制

Linux Namspace是一种Linux Kernel提供的资源隔离方案:

  • 系统可以为进程分配不同的Namespace
  • 并保证不用的Namespace资源独立分配、进程彼此隔离,即不同的Namespace下的进程互不干扰。

用户层面上只能看到属于用户自己namespace下的资源,例如使用ps命令只能列出自己namespace下的进程。这样每个namespace看上去就像一个单独的Linux系统。

容器 foreign layers_容器_03


Linux内核里的进程的数据结构:

一个进程可以属于多个namesapce,
既然namespace和进程相关,那么在task_struct结构体中就会包含和namespace相关联的变量。
在task_struct 结构中有一个指向namespace结构体的指针nsproxy。

进程的数据结构
struct task_struct {

/* namespaces */

struct nsproxy *nsproxy;
…….
}

再看一下nsproxy

/* 'count' is the number of tasks holding a reference.

 * The count for each namespace, then, will be the number

 * of nsproxies pointing to it, not the number of tasks.

 * The nsproxy is shared by tasks which share all namespaces.

 * As soon as a single namespace is cloned or unshared, the

 * nsproxy is copied

*/

struct nsproxy {

         atomic_t count;

         struct uts_namespace *uts_ns;

         struct ipc_namespace *ipc_ns;

         struct mnt_namespace *mnt_ns;

         struct pid_namespace *pid_ns_for_children;

         struct net             *net_ns;
         };

Linux对Namespace的操作

Namespace   Constant          Isolates
Cgroup      CLONE_NEWCGROUP   Cgroup root directory
IPC         CLONE_NEWIPC      System V IPC, POSIX message queues
Network     CLONE_NEWNET      Network devices, stacks, ports, etc.
Mount       CLONE_NEWNS       Mount points
PID         CLONE_NEWPID      Process IDs
User        CLONE_NEWUSER     User and group IDs
UTS         CLONE_NEWUTS      Hostname and NIS domain name

以上Namespace分别对进程的 Cgroup root进程间通信网络文件系统挂载点进程ID用户和组主机名域名等进行隔离。

  • clone
    在创建进程的系统调用时,可以通过flags参数指定需要创建的Namespace类型。
    int clone(int (*child_func )(void *), void *child_stack, int flags, void *arg);
  • clone() 函数是 fork() 函数更通用的实现方式,通过调用 clone(),并传入需要隔离资源对应的参数,就可以建立一个容器了。
a. linux 中 namespace详情

进入linux中进入 /proc目录(内存下的伪文件)

容器 foreign layers_容器_04


图示蓝色的数字 为进程pid编号
进入目录/proc/1810/ns

[root@k8s-master 1810]# cd ns
[root@k8s-master ns]# pwd
/proc/1810/ns
[root@k8s-master ns]# ls
cgroup  ipc  mnt  net  pid  pid_for_children  user  uts
[root@k8s-master ns]#

通过数字隔离,如果数字相同,则代表在同一空间下

[root@k8s-master ns]# ll
总用量 0
lrwxrwxrwx 1 gdm gdm 0 9月  24 16:00 cgroup -> 'cgroup:[4026531835]'
lrwxrwxrwx 1 gdm gdm 0 9月  24 16:00 ipc -> 'ipc:[4026531839]'
lrwxrwxrwx 1 gdm gdm 0 9月  24 16:00 mnt -> 'mnt:[4026531840]'
lrwxrwxrwx 1 gdm gdm 0 9月  24 16:00 net -> 'net:[4026531992]'
lrwxrwxrwx 1 gdm gdm 0 9月  24 16:00 pid -> 'pid:[4026531836]'
lrwxrwxrwx 1 gdm gdm 0 9月  24 16:00 pid_for_children -> 'pid:[4026531836]'
lrwxrwxrwx 1 gdm gdm 0 9月  24 16:00 user -> 'user:[4026531837]'
lrwxrwxrwx 1 gdm gdm 0 9月  24 16:00 uts -> 'uts:[4026531838]'

容器 foreign layers_容器 foreign layers_05


使用clone函数做测试,创建子进程

容器 foreign layers_容器 foreign layers_06

容器 foreign layers_子进程_07


父子进程pid编号是连着的。

当使用clone 创建不同的ipc 父子进程会使用不同的列队

容器 foreign layers_容器 foreign layers_08

b. pid隔离产生的问题

每个pid中的namespace第一个进程pid都要等于1

容器 foreign layers_容器_09


容器 foreign layers_容器_10


问题:当clone函数创建新进程时,pid虽然变成了1,但里面的1是宿主机的pid1,本来应该是bash。proc目录没有改变。这样会照成一个问题,不同主机公用相同的内存会影响

容器 foreign layers_容器_11


容器 foreign layers_容器_12


容器 foreign layers_docker_13

c. 解决:文件系统隔离(挂载)问题:1号进程

默认挂载的选项如下图

shared :共享目录

salve和master:传播方向 单向

private :私有的

unbindable:不可被挂载

容器 foreign layers_容器_14


容器 foreign layers_进程组_15


容器 foreign layers_docker_16


主机默认挂载目录是share形式需要自定义–make-private

容器 foreign layers_进程组_17


容器 foreign layers_子进程_18


容器 foreign layers_进程组_19


容器 foreign layers_子进程_20

如上图,当挂载隔离后,pid变成了1 且进程变为了/bin/bash。但会产生新问题:没有权限执行程序。1号进程的问题

解决:

1.docker run 的时候使用 --privileqed 提权

容器 foreign layers_进程组_21

2.centos sevice启动

容器 foreign layers_docker_22


容器 foreign layers_进程组_23


容器 foreign layers_容器_24

d.网路隔离

模拟物理机和容器之间的网络隔离

容器空间里的ip

1:ip netns exec test_ns ip link list (在test_ns中执行命令)

看到环回接口为DOWN(不能完成7层参考模型的封装到解封装)

容器 foreign layers_子进程_25


2: 执行lo up 进行启动

容器 foreign layers_容器 foreign layers_26


3:在物理机上 创建了两个虚拟网卡,并且两个网卡是相连的

容器 foreign layers_docker_27


容器 foreign layers_进程组_28


4:然后吧veth1 放到test_ns隔离空间中

容器 foreign layers_容器 foreign layers_29


5: 分别给两个虚拟网卡设置ip。隔离环境中的test_ns设置10.1.1.1
物理机设置10.1.1.2

容器 foreign layers_子进程_30

容器 foreign layers_容器_31

3)Cgroup机制

a. Cgroup是什么?

Linux CGroup全称Linux Control Group, 是Linux内核的一个功能,用来限制,控制与分离一个进程组群的资源(如CPU、内存、磁盘输入输出等)。这个项目最早是由Google的工程师在2006年发起(主要是Paul Menage和Rohit Seth),最早的名称为进程容器(process containers)。在2007年时,因为在Linux内核中,容器(container)这个名词太过广泛,为避免混乱,被重命名为cgroup,并且被合并到2.6.24版的内核中去。然后,其它开始了他的发展。

Linux CGroupCgroup 可为系统中所运行任务(进程)的用户定义组群分配资源—比如CPU 时间、系统内存、网络带宽或者这些资源的组合。可以监控配置的 cgroup,拒绝cgroup 访问某些资源,甚至在运行的系统中动态配置cgroup。

b. Cgroup主要功能

(1)限制资源使用,比如内存使用上限以及文件系统的缓存限制。

(2)优先级控制,CPU利用和磁盘IO吞吐。

(3)一些审计或一些统计,主要目的是为了计费。

(4)挂起进程,恢复执行进程。

c. Cgroup主要功能相关概念介绍
  • 任务(task): 在cgroup中,任务就是一个进程。
  • 控制组(control group):
    cgroup的资源控制是以控制组的方式实现,控制组指明了资源的配额限制。进程可以加入到某个控制组,也可以迁移到另一个控制组。
  • 层级(hierarchy): 控制组有层级关系,类似树的结构,子节点的控制组继承父控制组的属性(资源配额、限制等)。
  • 子系统(subsystem):
    一个子系统其实就是一种资源的控制器,比如memory子系统可以控制进程内存的使用。子系统需要加入到某个层级,然后该层级的所有控制组,均受到这个子系统的控制。

概念间的关系:

  • 子系统可以依附多个层级,当且仅当这些层级没有其他的子系统,比如两个层级同时只有一个cpu子系统,是可以的。
  • 一个层级可以附加多个子系统。
  • 一个任务可以是多个cgroup的成员,但这些cgroup必须位于不同的层级。
  • 子进程自动成为父进程cgroup的成员,可按需求将子进程移到不同的cgroup中。
d. subsystem(子系统)

cgroup是一种对进程资源管理和控制的统一框架,它提供的是一种机制(mechanism),而具体的策略(policy)是通过子系统(subsystem)来完成的,子系统是cgroup对进程组进行资源控制的具体行为。机制和策略是Linux操作系统中一种经典的设计思想,所谓机制就是“我要提供哪种功能”,而策略则是“我要怎样来实现这种功能”。

cgroup中每个子系统都代表一种类型的资源,具体如下:

  1. cpu子系统:该子系统为每个进程组设置一个使用CPU的权重值,以此来管理进程对cpu的访问。
  2. cpuset子系统:对于多核cpu,该子系统可以设置进程组只能在指定的核上运行,并且还可以设置进程组在指定的内存节点上申请内存。
  3. cpuacct子系统:该子系统只用于生成当前进程组内的进程对cpu的使用报告。
  4. memory子系统:该子系统提供了以页面为单位对内存的访问,比如对进程组设置内存使用上限等,同时可以生成内存资源报告
  5. blkio子系统:该子系统用于限制每个块设备的输入输出。首先,与CPU子系统类似,该系统通过为每个进程组设置权重来控制块设备对其的I/O时间;其次,该子系统也可以限制进程组的I/O带宽以及IOPS。
  6. devices子系统:通过该子系统可以限制进程组对设备的访问,即该允许或禁止进程组对某设备的访问。
  7. freezer子系统:该子系统可以使得进程组中的所有进程挂起。
  8. net-cls子系统:该子系统提供对网络带宽的访问限制,比如对发送带宽和接收带宽进程限制。

如果要实现子系统对所属进程组的资源控制,那么就要实现该子系统对应的钩子函数。这个关系与虚拟文件系统类似,VFS提供统一的用户接口,而具体的文件操作则通过文件系统(比如ext3)对钩子函数的实现。具体关系如下图:

容器 foreign layers_容器_32

e. linux实例

/sys 是操作系统的文件映射

[root@k8s-master cgroup]# pwd
/sys/fs/cgroup
[root@k8s-master cgroup]# ll
total 0
dr-xr-xr-x 7 root root  0 Oct 11 04:56 blkio    (块设备,输入输出限制,磁盘、usb等限制)  
lrwxrwxrwx 1 root root 11 Oct 11 04:56 cpu -> cpu,cpuacct    (对cpu使用想限制)
lrwxrwxrwx 1 root root 11 Oct 11 04:56 cpuacct -> cpu,cpuacct
dr-xr-xr-x 7 root root  0 Oct 11 04:56 cpu,cpuacct
dr-xr-xr-x 3 root root  0 Oct 11 04:56 cpuset
dr-xr-xr-x 7 root root  0 Oct 11 04:56 devices    (开启关闭对设备(键盘等)的使用)
dr-xr-xr-x 3 root root  0 Oct 11 04:56 freezer
dr-xr-xr-x 3 root root  0 Oct 11 04:56 hugetlb
dr-xr-xr-x 7 root root  0 Oct 11 04:56 memory
lrwxrwxrwx 1 root root 16 Oct 11 04:56 net_cls -> net_cls,net_prio
dr-xr-xr-x 3 root root  0 Oct 11 04:56 net_cls,net_prio
lrwxrwxrwx 1 root root 16 Oct 11 04:56 net_prio -> net_cls,net_prio
dr-xr-xr-x 3 root root  0 Oct 11 04:56 perf_event
dr-xr-xr-x 7 root root  0 Oct 11 04:56 pids
dr-xr-xr-x 2 root root  0 Oct 11 04:56 rdma
dr-xr-xr-x 7 root root  0 Oct 11 04:56 systemd

[root@k8s-master 1109]# lssubsys -m
cpuset /sys/fs/cgroup/cpuset
cpu,cpuacct /sys/fs/cgroup/cpu,cpuacct
blkio /sys/fs/cgroup/blkio
memory /sys/fs/cgroup/memory
devices /sys/fs/cgroup/devices
freezer /sys/fs/cgroup/freezer
net_cls,net_prio /sys/fs/cgroup/net_cls,net_prio
perf_event /sys/fs/cgroup/perf_event
hugetlb /sys/fs/cgroup/hugetlb
pids /sys/fs/cgroup/pids
rdma /sys/fs/cgroup/rdma

拿cpu举例,进入cpu目录,里面有个tasks目录,tasks目录记录着pid 前面的cpuaxxxx都是对tasks任务的cpu资源限制规则

[root@k8s-master cpu]# pwd
/sys/fs/cgroup/cpu
[root@k8s-master cpu]# ls
cgroup.clone_children  cpuacct.stat       cpuacct.usage_percpu       cpuacct.usage_sys   cpu.cfs_quota_us   cpu.shares  kubepods.slice     release_agent  user.slice
cgroup.procs           cpuacct.usage      cpuacct.usage_percpu_sys   cpuacct.usage_user  cpu.rt_period_us   cpu.stat    machine.slice      system.slice
cgroup.sane_behavior   cpuacct.usage_all  cpuacct.usage_percpu_user  cpu.cfs_period_us   cpu.rt_runtime_us  init.scope  notify_on_release  tasks

容器 foreign layers_进程组_33


子进程自动成为父进程cgroup的成员,可按需求将子进程移到不同的cgroup中。

容器 foreign layers_容器 foreign layers_34

f. cgroup文件系统

cgroup在Linux内核中是以文件系统的形式存在的,不过cgroup对应的这种文件系统与proc文件系统类似,都是只存在于内存中的“虚拟”文件系统。既然如此,就可以通过mount命令创建一个cgroup实例。

g.linux测试实例(cpu)

先将cpu核心设置为1 方便后续测试

容器 foreign layers_docker_35


先创建一个容器 设置其cpu优先级为512

[root@k8s-master ~]# docker run -itd  --name AA --cpu-shares 512 centos:7  /bin/bash
Unable to find image 'centos:7' locally
7: Pulling from library/centos
2d473b07cdd5: Pull complete 
Digest: sha256:9d4bcbbb213dfd745b58be38b13b996ebb5ac315fe75711bd618426a630e0987
Status: Downloaded newer image for centos:7
0239c110a722c3a9037c15315c3c1b63bf09983fa33b3c6d7579cad3092db7fd

可以在/sys/fs/cgroup/cpu/docker/xxxxx/下

cat cpu.shares

可以观察到它的cpu优先级为512

容器 foreign layers_进程组_36

使用stress 启动两个容器,一个容器cpu设置为512,一个设置为1024

[root@k8s-master cpu]# docker run -itd --name AA -c 512 progrium/stress --cpu 1
77cdd4608be8134c55a8fbb1ea44891daff419f723194ce113b937b3207bffad
[root@k8s-master cpu]# 
[root@k8s-master cpu]# docker run -itd --name aa -c 1024 progrium/stress --cpu 1
abdf4cda1266c443f8e619359cb3705c9d5f01d990092fbf7088f4d97b00966e
[root@k8s-master cpu]# top
top - 16:04:22 up  5:58,  1 user,  load average: 8.63, 5.75, 5.57
Tasks: 426 total,   8 running, 418 sleeping,   0 stopped,   0 zombie
%Cpu(s): 75.4 us, 21.9 sy,  0.0 ni,  0.0 id,  0.0 wa,  1.7 hi,  1.0 si,  0.0 st
MiB Mem :   8364.6 total,    119.3 free,   7246.1 used,    999.2 buff/cache
MiB Swap:      0.0 total,      0.0 free,      0.0 used.    716.9 avail Mem 

   PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND                         
 46715 root      20   0    7316     96      0 R  35.4   0.0   0:03.54 stress                          
 46444 root      20   0    7316     96      0 R  17.9   0.0   0:06.80 stress

可观察到优先级高的 竞争cpu资源多

h.linux测试限制内存使用量
[root@k8s-master cpu]# cgcreate -g memory:g2    //创建控制群组g2
[root@k8s-master cpu]# cgget -r memory.limit_in_bytes g2 
//查看默认内存是没有限制的
g2:
memory.limit_in_bytes: 9223372036854771712
#限制内存只有1GB
[root@k8s-master cpu]# cgset -r memory.limit_in_bytes=1073741824 g2  
[root@k8s-master cpu]# cgget -r memory.limit_in_bytes g2
g2:
memory.limit_in_bytes: 1073741824
#执行/tmp/highmemory.sh,进程号是21127 
#vi /tmp/highmem.sh
#/bin/bash
x="a"
while [ True ];do
    x=$x$x
done;

二、容器的框架结构

1) 架构图

容器 foreign layers_子进程_37


容器 foreign layers_docker_38

容器 foreign layers_docker_39

a.docker client

docker client 是docker架构中用户用来和docker daemon建立通信的客户端,用户使用的可执行文件为docker,通过docker命令行工具可以发起众多管理container的请求。

docker client发送容器管理请求后,由docker daemon接受并处理请求,当docker client 接收到返回的请求相应并简单处理后,docker client 一次完整的生命周期就结束了,当需要继续发送容器管理请求时,用户必须再次通过docker可以执行文件创建docker client。

b.docker daemon

docker daemon 是docker架构中一个常驻在后台的系统进程,功能是:接收处理docker client发送的请求。该守护进程在后台启动一个server,server负载接受docker client发送的请求;接受请求后,server通过路由与分发调度,找到相应的handler来执行请求。

docker daemon启动所使用的可执行文件也为docker,与docker client启动所使用的可执行文件docker相同,在docker命令执行时,通过传入的参数来判别docker daemon与docker client。

c.docker server

docker server在docker架构中时专门服务于docker client的server,该server的功能时:接受并调度分发docker client发送的请求,架构图如下:

容器 foreign layers_容器 foreign layers_40


在Docker的启动过程中,通过包gorilla/mux(golang的类库解析),创建了一个mux.Router,提供请求的路由功能。在Golang中,gorilla/mux是一个强大的URL路由器以及调度分发器。该mux.Router中添加了众多的路由项,每一个路由项由HTTP请求方法(PUT、POST、GET或DELETE)、URL、Handler三部分组成。

d.engine

Engine是Docker架构中的运行引擎,同时也Docker运行的核心模块。它扮演Docker container存储仓库的角色,并且通过执行job的方式来操纵管理这些容器。

在Engine数据结构的设计与实现过程中,有一个handler对象。该handler对象存储的都是关于众多特定job的handler处理访问。举例说明,Engine的handler对象中有一项为:{“create”: daemon.ContainerCreate,},则说明当名为”create”的job在运行时,执行的是daemon.ContainerCreate的handler。

e.job

一个Job可以认为是Docker架构中Engine内部最基本的工作执行单元。Docker可以做的每一项工作,都可以抽象为一个job。例如:在容器内部运行一个进程,这是一个job;创建一个新的容器,这是一个job,从Internet上下载一个文档,这是一个job;包括之前在Docker Server部分说过的,创建Server服务于HTTP的API,这也是一个job,等等。

Job的设计者,把Job设计得与Unix进程相仿。比如说:Job有一个名称,有参数,有环境变量,有标准的输入输出,有错误处理,有返回状态等。

f.docker registry

Docker Registry是一个存储容器镜像的仓库。而容器镜像是在容器被创建时,被加载用来初始化容器的文件架构与目录。

在Docker的运行过程中,Docker Daemon会与Docker Registry通信,并实现搜索镜像、下载镜像、上传镜像三个功能,这三个功能对应的job名称分别为”search”,”pull” 与 “push”。

其中,在Docker架构中,Docker可以使用公有的Docker Registry,即大家熟知的Docker Hub,如此一来,Docker获取容器镜像文件时,必须通过互联网访问Docker Hub;同时Docker也允许用户构建本地私有的Docker Registry,这样可以保证容器镜像的获取在内网完成。

g.Graph

Graph在Docker架构中扮演已下载容器镜像的保管者,以及已下载容器镜像之间关系的记录者。一方面,Graph存储着本地具有版本信息的文件系统镜像,另一方面也通过GraphDB记录着所有文件系统镜像彼此之间的关系。Graph的架构如下:

容器 foreign layers_子进程_41


其中,GraphDB是一个构建在SQLite之上的小型图数据库,实现了节点的命名以及节点之间关联关系的记录。它仅仅实现了大多数图数据库所拥有的一个小的子集,但是提供了简单的接口表示节点之间的关系。

同时在Graph的本地目录中,关于每一个的容器镜像,具体存储的信息有:该容器镜像的元数据,容器镜像的大小信息,以及该容器镜像所代表的具体rootfs。

h.driver

Driver是Docker架构中的驱动模块。通过Driver驱动,Docker可以实现对Docker容器执行环境的定制。由于Docker运行的生命周期中,并非用户所有的操作都是针对Docker容器的管理,另外还有关于Docker运行信息的获取,Graph的存储与记录等。因此,为了将Docker容器的管理从Docker Daemon内部业务逻辑中区分开来,设计了Driver层驱动来接管所有这部分请求。
docker version 等

i.libcontainer

libcontainer是Docker架构中一个使用Go语言设计实现的库,设计初衷是希望该库可以不依靠任何依赖,直接访问内核中与容器相关的API。

正是由于libcontainer的存在,Docker可以直接调用libcontainer,而最终操纵容器的namespace、cgroups、apparmor、网络设备以及防火墙规则等。这一系列操作的完成都不需要依赖LXC或者其他包。libcontainer架构如下:

容器 foreign layers_docker_42

j.docker container

Docker container(Docker容器)是Docker架构中服务交付的最终体现形式。

Docker按照用户的需求与指令,订制相应的Docker容器:

用户通过指定容器镜像,使得Docker容器可以自定义rootfs等文件系统; 用户通过指定计算资源的配额,使得Docker容器使用指定的计算资源; 用户通过配置网络及其安全策略,使得Docker容器拥有独立且安全的网络环境; 用户通过指定运行的命令,使得Docker容器执行指定的工作。

容器 foreign layers_容器 foreign layers_43

2) docker镜像分层特点

a.分层图

FROM centos:7
RUN yum install net-tools-y
RUN yum install httpd -y
RUN yum install elink  -y
CMD ["/bin/bash"]

容器 foreign layers_容器 foreign layers_44


分层的好处在于资源共享,比如说有很多的镜像,可以从base镜像构建而来,那么docker host当中只需要在硬盘上保存一份base镜像即可,同时内存当中也只需要加载一份base镜像即可,也就是说我开多少的相同的镜像,内存在上涨值并不明显。

此时会遇到一个问题,多容器共享一份共享,会产生读写问题,比如a容器和b容器共享某镜像,a修改了根目录的文件,b容器会不会产生影响。

b.容器拷贝技术

容器 foreign layers_docker_45


继上产生的问题,对上层容器,容器把镜像实例化后,镜像层的数据以只读的形式挂载到某空间,然后再挂载一个空白空间,让用户写数据,通过union mount 技术将两目录结合,

在硬盘的一个地方划分一个空间,其实对硬盘照快照就是区分出哪些是快照之前的,哪些是快照之后的数据,照快照的瞬间,硬盘会产生两样操作,无非就在读数据,无非就在写数据。读旧数据从原始数据位置开始读,读新的数据从快照的地方开始读。写数据的话,就是增加新的数据,修改原始数据,删除旧的数据三项操作。

我们主机当中就是把这一瞬间把所有有变化的数据放置到快照里面去,把所有没有变化的数据不动,即就在原始位置,现在我需要增加一个数据,照快照的一瞬间写入硬盘5M数据,那么这5M数据都放到快照里面去,当用户需要读数据的时候,读旧数据从原始数据出,新数据从快照里面出。

  • 改数据的话,把这个文件要改的文件放置到原始数据当中,把改之后的数据放置到快照里面。
  • 删除的话,删除之后不显示在快照里面,没删除的数据在原始数据里面。
  • 逻辑卷快照COW的原理就是,在照快照的一瞬间,产生变化量的数据放在一起。
  • 我们增删改查的数据全部放在容器层内,其他数据放在我们镜像层内。
  • 我们有一个a数据,这个在httpd那层也有一个a数据,那么此时我们怎么读这个数据呢,我们读取数据的方向是从上层往下层的顺序,最终我们看到的是上层数据的a,不会看到下层数据的a。
  • 改数据的话,比如说改b,此时先把b读到我们的容器层内,然后再产生修改,改变的数据不会再写入镜像层。
  • 删除数据的话,也是读到容器层,在容器层内进行删除。
  • 增加数据,也不会写入到镜像层,从而保证我们镜像层的数据永远不会发生变化。注意增加的数据需要持久化。

3) 架构总结

容器 foreign layers_子进程_46


容器 foreign layers_容器_47


容器 foreign layers_docker_48

a.CRI规范

容器 foreign layers_子进程_49


容器 foreign layers_进程组_50


容器 foreign layers_docker_51

b.ORI规范

容器 foreign layers_docker_52


容器 foreign layers_容器 foreign layers_53

三、(高低级)容器运行时源码分析

1) runc

创建一个 OCI Bundle
要使用runc,您必须使用OCI包的格式容器。从现有Docker容器中获取根文件系统。

# create the top most bundle directory
mkdir /mycontainer
cd /mycontainer

# create the rootfs directory
mkdir rootfs

# export busybox via Docker into the rootfs directory
docker export $(docker create busybox) | tar -C rootfs -xvf -

runc提供了一个spec命令来生成可以编辑的基本模板规范。

runc spec
[root@k8s-node1 mycontainer]# ls
config.json  rootfs
[root@k8s-node1 mycontainer]# pwd
/usr/zqa/mycontainer
[root@k8s-node1 mycontainer]#

runc run 启动一个容器

[root@k8s-node1 mycontainer]# runc run simplebusybox
/ # ls
bin   dev   etc   home  proc  root  sys   tmp   usr   var
/ # exit
[root@k8s-node1 mycontainer]# runc run simplebusybox
/ # ls
bin   dev   etc   home  proc  root  sys   tmp   usr   var
/ # ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever
2: tunl0@NONE: <NOARP> mtu 1480 qdisc noop qlen 1000
    link/ipip 0.0.0.0 brd 0.0.0.0
/ #

查看另一个终端,可以查看运行的容器信息

[root@k8s-node1 ~]# runc list
ID              PID         STATUS      BUNDLE                 CREATED                          OWNER
simplebusybox   94828       running     /usr/zqa/mycontainer   2021-11-11T14:05:29.524888919Z   root
[root@k8s-node1 ~]#

总结

提示:这里对文章进行总结:
例如:以上就是今天要讲的内容,本文仅仅简单介绍了pandas的使用,而pandas提供了大量能使我们快速便捷地处理数据的函数和方法。