文章目录

  • docker核心实现技术
  • 基本架构
  • 服务端
  • 客户端
  • 命名空间
  • 进程命名空间
  • PID Namespace
  • Network Namespace
  • UTS Namespace
  • IPC Namespace
  • Mount namespace
  • User Namespace
  • 控制组(CGroups)
  • cgroups是什么?
  • Cgroups中的三个组件
  • cgroups子系统
  • cpu
  • docker如何使用cgroup
  • 注意
  • PS
  • 联合文件系统
  • docker存储
  • linux网络虚拟化
  • 基本原理
  • 网络创建过程
  • 手动配置网络
  • 启动容器
  • 创建网络命名空间
  • 查看桥接网卡及ip信息
  • 创建veth pair接口
  • B接口放到容器中
  • 参考博客


docker核心实现技术

作为一种容器虚拟化技术,docker深度应用了操作系统的多项底层支持技术

操作系统来看 主要包括命名空间(namespace)、控制组(control group)、联合文件系统(union file system)、和linux网络虚拟化支持

基本架构

docker目前采用了标准的C/S架构,客户端和服务器端既可以运行在一个机器上,也可以运行在不同的机器上,通过socket来进行通信

服务端

docker daemon一般在宿主机后台运行,作为服务端接受来自客户的请求,并处理这些请求(创建、运行、分发)

客户端

docker客户端为用户提供了一系列可执行的命令,用户用这些命令与docker daemon交互

命名空间

命名空间是linux内核的一个强大的特性,为容器虚拟化的实现带来极大的便利,这一机制保证了容器之间彼此互补影响。

Namespace是将内核的全局资源做封装,使得每个Namespace都有一份独立的资源,因此不同的进程在各自的Namespace内对同一种资源的使用不会互相干扰。实际上,Linux内核实现namespace的主要目的就是为了实现轻量级虚拟化(容器)服务。在同一个namespace下的进程可以感知彼此的变化,而对外界的进程一无所知。这样就可以让容器中的进程产生错觉,仿佛自己置身于一个独立的系统环境中,以此达到独立和隔离的目的。

这样的解释可能不清楚,举个例子,执行sethostname这个系统调用时,可以改变系统的主机名,这个主机名就是一个内核的全局资源。内核通过实现UTS Namespace,可以将不同的进程分隔在不同的UTS Namespace中,在某个Namespace修改主机名时,另一个Namespace的主机名还是保持不变。

目前Linux内核总共实现了6种Namespace:

IPC:隔离System V IPC和POSIX消息队列。
Network:隔离网络资源。
Mount:隔离文件系统挂载点。每个容器能看到不同的文件系统层次结构。
PID:隔离进程ID。
UTS:隔离主机名和域名。
User:隔离用户ID和组ID。

2: 通过setns()加入一个已经存在的namespace
在进程都结束的情况下,也可以通过挂载的形式把namespace保留下来,保留namespace的目的自然是为以后有进程加入做准备。通过setns()系统调用,你的进程从原先的namespace加入我们准备好的新namespace,使用方法如下。

int setns(int fd, int nstype);
参数fd表示我们要加入的namespace的文件描述符。上文已经提到,它是一个指向/proc/[pid]/ns目录的文件描述符,可以通过直接打开该目录下的链接或者打开一个挂载了该目录下链接的文件得到。
参数nstype让调用者可以去检查fd指向的namespace类型是否符合我们实际的要求。如果填0表示不检查。
3: 通过unshare()在原先进程上进行namespace隔离
后要提的系统调用是unshare(),它跟clone()很像,不同的是,unshare()运行在原先的进程上,不需要启动一个新进程,使用方法如下。

int unshare(int flags);
调用unshare()的主要作用就是不启动一个新进程就可以起到隔离的效果,相当于跳出原先的namespace进行操作。这样,你就可以在原进程进行一些需要隔离的操作。Linux中自带的unshare命令,就是通过unshare()系统调用实现的。

进程命名空间

linux通过命名空间管理进程号,对于同一进程(即同一个task_struct),在不同的命名空间中,看到的进程号不相同,每个进程命名空间有一套自己的进程号管理方法,进程管理命名空间是一个父子关系的结构,子空间中进程对于父空间是可见的。新的fork出的进程在父命名空间和子命名空间将分别有一个进程号来对应

PID Namespace

PID Namespace用于隔离进程PID号,这样一来,不同的Namespace里的进程PID号就可以是一样的了。

Network Namespace

这个Namespace会对网络相关的系统资源进行隔离,每个Network Namespace都有自己的网络设备、IP地址、路由表、/proc/net目录、端口号等。网络隔离的必要性是很明显的,举一个例子,在没有隔离的情况下,如果两个不同的容器都想运行同一个Web应用,而这个应用又需要使用80端口,那就会有冲突了

UTS Namespace

UTS Namespace用于对主机名和域名进行隔离,也就是uname系统调用使用的结构体struct utsname里的nodename和domainname这两个字段,UTS这个名字也是由此而来的。
那么,为什么要使用UTS Namespace做隔离?这是因为主机名可以用来代替IP地址,因此,也就可以使用主机名在网络上访问某台机器了,如果不做隔离,这个机制在容器里就会出问题。

IPC Namespace

IPC是Inter-Process Communication的简写,也就是进程间通信。Linux提供了很多种进程间通信的机制,IPC Namespace针对的是SystemV IPC和Posix消息队列。这些IPC机制都会用到标识符,例如用标识符来区别不同的消息队列,然后两个进程通过标识符找到对应的消息队列进行通信等。
IPC Namespace能做到的事情是,使相同的标识符在两个Namespace中代表不同的消息队列,这样也就使得两个Namespace中的进程不能通过IPC进程通信了。

Mount namespace

Mount namespace通过隔离文件系统挂载点对隔离文件系统提供支持,它是历史上第一个Linux namespace,所以它的标识位比较特殊,就是CLONE_NEWNS。隔离后,不同mount namespace中的文件结构发生变化也互不影响。你可以通过/proc/[pid]/mounts查看到所有挂载在当前namespace中的文件系统,还可以通过/proc/[pid]/mountstats看到mount namespace中文件设备的统计信息,包括挂载文件的名字、文件系统类型、挂载位置等等。

进程在创建mount namespace时,会把当前的文件结构复制给新的namespace。新namespace中的所有mount操作都只影响自身的文件系统,而对外界不会产生任何影响。这样做非常严格地实现了隔离,但是某些情况可能并不适用。比如父节点namespace中的进程挂载了一张CD-ROM,这时子节点namespace拷贝的目录结构就无法自动挂载上这张CD-ROM,因为这种操作会影响到父节点的文件系统。

User Namespace

User Namespace用来隔离用户和组ID,也就是说一个进程在Namespace里的用户和组ID与它在host里的ID可以不一样,这样说可能读者还不理解有什么实际的用处。User Namespace最有用的地方在于,host的普通用户进程在容器里可以是0号用户,也就是root用户。这样,进程在容器内可以做各种特权操作,但是它的特权被限定在容器内,离开了这个容器它就只有普通用户的权限了。

控制组(CGroups)

cgroups是什么?

Cgroup是control group的简写,属于Linux内核提供的一个特性,用于限制和隔离一组进程对系统资源的使用,也就是做资源QoS,这些资源主要包括CPU、内存、block I/O和网络带宽。Cgroup从2.6.24开始进入内核主线,目前各大发行版都默认打开了Cgroup特性。
Cgroups提供了以下四大功能:

  • 资源限制(Resource Limitation):cgroups可以对进程组使用的资源总额进行限制。如设定应用运行时使用内存的上限,一旦超过这个配额就发出OOM(Out of Memory)。
  • 优先级分配(Prioritization):通过分配的CPU时间片数量及硬盘IO带宽大小,实际上就相当于控制了进程运行的优先级。
  • 资源统计(Accounting): cgroups可以统计系统的资源使用量,如CPU使用时长、内存用量等等,这个功能非常适用于计费。
  • 进程控制(Control):cgroups可以对进程组执行挂起、恢复等操作。

Cgroups中的三个组件

  • cgroup 控制组 。cgroup 是对进程分组管理的一种机制,一个cgroup包含一组进程,并可以在这个cgroup上增加Linux subsystem的各种参数的配置,将一组进程和一组subsystem的系统参数关联起来。
  • subsystem 子系统。subsystem 是一组资源控制的模块。这块在下面会详细介绍。
  • hierarchy 层级树。hierarchy 的功能是把一组cgroup串成一个树状的结构,一个这样的树便是一个hierarchy,通过这种树状的结构,Cgroups可以做到继承。比如我的系统对一组定时的任务进程通过cgroup1限制了CPU的使用率,然后其中有一个定时dump日志的进程还需要限制磁盘IO,为了避免限制了影响到其他进程,就可以创建cgroup2继承于cgroup1并限制磁盘的IO,这样cgroup2便继承了cgroup1中的CPU的限制,并且又增加了磁盘IO的限制而不影响到cgroup1中的其他进程。

cgroups子系统

![360截图17610612118130146.png](https://img-blog.csdnimg.cn/img_convert/5174ef5b99e96863507978e46f6b0392.png#clientId=u598a17da-f008-4&from=ui&id=u4ee1d01b&margin=[object Object]&name=360截图17610612118130146.png&originHeight=86&originWidth=963&originalType=binary&ratio=1&size=8701&status=done&style=none&taskId=u26810a46-cdba-4445-bf85-d976d4bb81a)
cgroup中实现的子系统及其作用如下:

  • devices:设备权限控制。
  • cpuset:分配指定的CPU和内存节点。
  • cpu:控制CPU占用率。
  • cpuacct:统计CPU使用情况。
  • memory:限制内存的使用上限。
  • freezer:冻结(暂停)Cgroup中的进程。
  • net_cls:配合tc(traffic controller)限制网络带宽。
  • net_prio:设置进程的网络流量优先级。
  • huge_tlb:限制HugeTLB的使用。
  • perf_event:允许Perf工具基于Cgroup分组做性能监测。

每个子系统的目录下有更详细的设置项,例如:

cpu

![image.png](https://img-blog.csdnimg.cn/img_convert/d675fde789cd75b3173e54d761b9efc7.png#clientId=u598a17da-f008-4&from=paste&height=66&id=u993c4e8d&margin=[object Object]&name=image.png&originHeight=132&originWidth=1279&originalType=binary&ratio=1&size=23413&status=done&style=none&taskId=ua2dba099-f473-4e5b-8f71-55db76e31ee&width=639.5)
CPU资源的控制也有两种策略,一种是完全公平调度 (CFS:Completely Fair Scheduler)策略,提供了限额和按比例分配两种方式进行资源控制;另一种是实时调度(Real-Time Scheduler)策略,针对实时进程按周期分配固定的运行时间。配置时间都以微秒(µs)为单位,文件名中用us表示。
cpuset CPU绑定
![image.png](https://img-blog.csdnimg.cn/img_convert/ccb692c285bf06738ffc01fe52becd55.png#clientId=u598a17da-f008-4&from=paste&height=76&id=ucfd014ff&margin=[object Object]&name=image.png&originHeight=151&originWidth=1385&originalType=binary&ratio=1&size=30021&status=done&style=none&taskId=ua4dc30d9-eeb6-41f3-924f-78cf7338372&width=692.5)
除了限制 CPU 的使用量,cgroup 还能把任务绑定到特定的 CPU,让它们只运行在这些 CPU 上,这就是 cpuset 子资源的功能。除了 CPU 之外,还能绑定内存节点(memory node)。
在把任务加入到 cpuset 的 task 文件之前,用户必须设置 cpuset.cpus 和 cpuset.mems 参数。

  • cpuset.cpus:设置 cgroup 中任务能使用的 CPU,格式为逗号(,)隔开的列表,减号(-)可以表示范围。比如,0-2,7 表示 CPU 第 0,1,2,和 7 核。
  • cpuset.mems:设置 cgroup 中任务能使用的内存节点,和 cpuset.cpus 格式一样。

memory
![image.png](https://img-blog.csdnimg.cn/img_convert/36a00408ee97c26f98868bc895f59b87.png#clientId=u598a17da-f008-4&from=paste&height=132&id=u0235ae89&margin=[object Object]&name=image.png&originHeight=263&originWidth=1327&originalType=binary&ratio=1&size=50725&status=done&style=none&taskId=u3caa9f72-299d-4678-829b-5e96cdbc4bc&width=663.5)

  • memory.limit_bytes:强制限制最大内存使用量,单位有k、m、g三种,填-1则代表无限制。
  • memory.soft_limit_bytes:软限制,只有比强制限制设置的值小时才有意义。填写格式同上。当整体内存紧张的情况下,task获取的内存就被限制在软限制额度之内,以保证不会有太多进程因内存挨饿。可以看到,加入了内存的资源限制并不代表没有资源竞争。
  • memory.memsw.limit_bytes:设定最大内存与swap区内存之和的用量限制。填写格式同上。

这里专门讲一下监控和统计相关的参数,比如cadvisor采集的那些参数。

  • memory.usage_bytes:报告该 cgroup中进程使用的当前总内存用量(以字节为单位)。
  • memory.max_usage_bytes:报告该 cgroup 中进程使用的最大内存用量。

docker如何使用cgroup

使用docker run 运行一个容器后执行

docker stats

![image.png](https://img-blog.csdnimg.cn/img_convert/491df1202a2c09594be7579e8ca7f9a7.png#clientId=u598a17da-f008-4&from=paste&height=46&id=ud370b26e&margin=[object Object]&name=image.png&originHeight=92&originWidth=1674&originalType=binary&ratio=1&size=18135&status=done&style=none&taskId=u52fc1b5f-8af9-4812-8462-2ef0909d9e7&width=837)

  • 验证Docker是否为此容器放置了一些cgroup
[root@db2 ~]# find  /sys/fs/cgroup/ -name "f42852fba6d6*"
/sys/fs/cgroup/devices/docker/f42852fba6d68a5b315ff8b0bab6def24d1b04852585bde46062ae89040a89da
/sys/fs/cgroup/pids/docker/f42852fba6d68a5b315ff8b0bab6def24d1b04852585bde46062ae89040a89da
/sys/fs/cgroup/freezer/docker/f42852fba6d68a5b315ff8b0bab6def24d1b04852585bde46062ae89040a89da
/sys/fs/cgroup/hugetlb/docker/f42852fba6d68a5b315ff8b0bab6def24d1b04852585bde46062ae89040a89da
/sys/fs/cgroup/memory/docker/f42852fba6d68a5b315ff8b0bab6def24d1b04852585bde46062ae89040a89da
/sys/fs/cgroup/cpu,cpuacct/docker/f42852fba6d68a5b315ff8b0bab6def24d1b04852585bde46062ae89040a89da
/sys/fs/cgroup/cpuset/docker/f42852fba6d68a5b315ff8b0bab6def24d1b04852585bde46062ae89040a89da
/sys/fs/cgroup/perf_event/docker/f42852fba6d68a5b315ff8b0bab6def24d1b04852585bde46062ae89040a89da
/sys/fs/cgroup/blkio/docker/f42852fba6d68a5b315ff8b0bab6def24d1b04852585bde46062ae89040a89da
/sys/fs/cgroup/net_cls,net_prio/docker/f42852fba6d68a5b315ff8b0bab6def24d1b04852585bde46062ae89040a89da
/sys/fs/cgroup/systemd/docker/f42852fba6d68a5b315ff8b0bab6def24d1b04852585bde46062ae89040a89da

注意


在创建或者启动容器时,为每个容器指定容器的指定资源的限制,例如:-c |–cpu-shares [=0] 参数来调整容器使用CPU的权重,使用-m | --menory [=MEMORY],参数来调整容器使用内存的大小


PS

  1. 一般在安装k8s的过程中经常会遇到如下错误:

create kubelet: misconfiguration: kubelet cgroup driver: “cgroupfs” is different from docker cgroup driver: “systemd”

  1. 其实此处错误信息已经很明白了,就是docker 和kubelet指定的cgroup driver不一样。 docker
    支持systemd和cgroupfs两种驱动方式。通过runc代码可以更加直观了解。
    ![1038851208-5b9762c48c396_fix732.png](https://img-blog.csdnimg.cn/img_convert/ff3e08504a13cda243f84c7e9b53bd6b.png#clientId=u598a17da-f008-4&from=drop&id=u99112820&margin=[object Object]&name=1038851208-5b9762c48c396_fix732.png&originHeight=440&originWidth=732&originalType=binary&ratio=1&size=112634&status=done&style=none&taskId=u053692e9-6154-4597-b260-376b57e3221)
  2. cgroup 只能限制 CPU 的使用,而不能保证CPU的使用。也就是说, 使用
    cpuset-cpus,可以让容器在指定的CPU或者核上运行,但是不能确保它独占这些CPU;cpu-shares
    是个相对值,只有在CPU不够用的时候才其作用。也就是说,当CPU够用的时候,每个容器会分到足够的CPU;不够用的时候,会按照指定的比重在多个容器之间分配CPU。
  3. 对内存来说,cgroups 可以限制容器最多使用的内存。使用 -m 参数可以设置最多可以使用的内存

联合文件系统

联合文件系统,(UnionFS)是一种轻量级的高性能分层文件系统,它支持将文件系统中的修改信息作为一次提交,并层层叠加,同时可以将不同目录的挂载到同一虚拟文件系统下,应用看到的是挂载的最终结构。联合文件系统是实现docker镜像的技术基础,docker镜像可以通过分层来进行继承。

UnionFS是一种为Linux,FreeBSD和NetBSD操作系统设计的把其他文件系统联合到一个联合挂载点的文件系统服务。它使用branch把不同文件系统的文件和目录“透明地”覆盖,形成一个单一一致的文件系统。这些branches或者是read-only或者是read-write的,所以当对这个虚拟后的联合文件系统进行写操作的时候,系统是真正写到了一个新的文件中。看起来这个虚拟后的联合文件系统是可以对任何文件进行操作的,但是其实它并没有改变原来的文件,这是因为unionfs用到了一个重要的资管管理技术叫写时复制。

写时复制(copy-on-write,下文简称CoW),也叫隐式共享,是一种对可修改资源实现高效复制的资源管理技术。它的思想是,如果一个资源是重复的,但没有任何修改,这时候并不需要立即创建一个新的资源;这个资源可以被新旧实例共享。创建新资源发生在第一次写操作,也就是对资源进行修改的时候。通过这种资源共享的方式,可以显著地减少未修改资源复制带来的消耗,但是也会在进行资源修改的时候增减小部分的开销。

docker存储

AUFS,英文全称是Advanced multi-layered unification filesystem, 曾经也叫 Acronym multi-layered unification filesystem,Another multi-layered unification filesystem。AUFS完全重写了早期的UnionFS 1.x,其主要目的是为了可靠性和性能,并且引入了一些新的功能,比如可写分支的负载均衡。AUFS的一些实现已经被纳入UnionFS 2.x版本。

docker 镜像自身就是有多个文件层组成,每一层有唯一的编号。

linux网络虚拟化

docker的本地网络实现其实就是利用linux的网络命令空间和虚拟网络设备(特别是veth pair)。熟悉这两部分的基础概念,有助于理解docker网络实现的过程

基本原理

直观上看,要实现网络通信,机器至少需要一个网络接口(物理接口或者虚拟接口)与外界相通,并且可以收发数据包,如果不同子网之间要进行通信,需要额外的路由机制

docker中的网络接口默认都是虚拟的接口,虚拟接口的最大有事就是转发的效率极高,即发送接口的发送缓存中的数据包将被直接复制到接受接口的接受缓存中,而无需通过外部物理网络设备进行交换。对于本地系统和容器内系统来看,虚拟接口跟一个正常的以太网卡相比并无区别,只是它的速度较快

docker容器网络就很好的的利用了linux虚拟网络技术,在本地主机和容器内分别创建一个虚拟接口,并让他们彼此连通(这样的一对接口叫做veth pair)

网络创建过程

  • 创建一对虚拟接口,分别放到本地主机和新容器的命名空间
  • 本地主机一段的虚拟接口连接到默认的docker0网桥或者指定网桥上,并具有一个以veth开头的唯一名字
  • 容器一端虚拟接口将放到新创建的容器中,并修改名字为eth0,这个接口只在容器的命名空间可见
  • 从网桥可用地址段中获取一个空闲的地址分配给容器的eth0中,并配置默认路由网关为docker0 网卡的内部接口docker0的ip地址

完成这些操作后,容器就可以使用它所能看见的eth0虚拟网卡来连接其他容器和访问外部网络
用户也可以通过docker network命令来手动管理网络

在运行容器时,可以通过–net参数指定容器的网络配置,有5个可选值bridge、none、container、host和用户定义的网络

  • –net=bridge,默认值,在docker网桥docker0上为容器创建新的网络栈
  • –net=none,让docker将新容器放到隔离的网络栈中,但是不进行网络配置,需要用户手动配置
  • –net=container:name_or_ID,让docker将新建的容器进程放到一个已存在容器的网络栈中,新容器进程有自己的文件系统、进程列表和资源限制,但会和已存在的容器共享IP地址和端口等网络资源,两者之间可以通过lo环回接口通信
  • –net=host:告诉docker不要将容器网络放到隔离的命名空间中,即不要容器化容器内的网络,此时容器使用本地主机的网络,它拥有完全的本地主机接口的访问权限。容器进程可以跟主机其他root进程一样打开低范围的端口,可以访问本地网络服务,还可以让容器做一些影响整个主机系统的事情,比如重启主机。因此使用这个选项的时候要非常小心,如果进一步的使用–privileged=true参数,容器甚至会被允许直接配置主机的网络栈。
  • –net=user_defined_network:用户自行用network相关命令创建的一个网络,通过这种方式将容器连接到指定的已创建的网络上去。

手动配置网络

用户使用–net=none后,docker将不对容器网络进行配置

启动容器

[root@db2 ~]# docker run -it --rm --net=none docker_test/wordpress bash 
root@d7eb67fe8f99:/var/www/html#

创建网络命名空间

[root@db2 ~]# docker inspect -f '{{.State.Pid}}' d7eb67fe8f99
12331
[root@db2 ~]# mkdir -p /var/run/netns
[root@db2 ~]# ln -s /proc/12331/ns/net/ /var/run/netns/12331

查看桥接网卡及ip信息

[root@db2 ~]# ifconfig 
docker0: flags=4099<UP,BROADCAST,MULTICAST>  mtu 1500
        inet 172.17.0.1  netmask 255.255.0.0  broadcast 172.17.255.255
        ether 02:42:34:ff:7a:e7  txqueuelen 0  (Ethernet)
        RX packets 4731  bytes 6144820 (5.8 MiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 3649  bytes 2944694 (2.8 MiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

创建veth pair接口

[root@db2 ~]# ip link add A type veth peer name B
[root@db2 ~]# brctl addif docker0 A 
[root@db2 ~]# ip link set A up

B接口放到容器中

[root@db2 ~]# docker inspect -f '{{.State.Pid}}' d7eb67fe8f99
12331
[root@db2 ~]# pid=12331
[root@db2 ~]# ip link set B netns ${pid}
[root@db2 ~]# ip netns exec ${pid} ip limk set dev B name eth0
Object "limk" is unknown, try "ip help".
[root@db2 ~]# ip netns exec ${pid} ip link set dev B name eth0
[root@db2 ~]# ip netns exec ${pid} ip link set eth0 up
[root@db2 ~]# ip netns exec ${pid} ip addr add 172.17.0.1/16 dev eth0
[root@db2 ~]# ip netns exec ${pid} ip route add default via 172.24.63.253

参考博客

https://www.jianshu.com/p/47c4a06a84a4