docker底层实现

一、docker架构

Docker 采用了 C/S架构,包括客户端和服务端。 Docker daemon 作为服务端接受来自客户的请求,并处理这些请求(创建、运行、分发容器)。 客户端和服务端既可以运行在一个机器上,也可通过 socket 或者 RESTful API 来进行通信。

docker基本机构

Docker daemon 一般在宿主机后台运行,等待接收来自客户端的消息。 Docker 客户端则为用户提供一系列可执行命令,用户用这些命令实现跟 Docker daemon 交互。

Docker提供了工具和平台来管理容器,而Docker Engine则是一个提供了大部分功能组件的CS架构的应用,如架构图所示,Docker Engine负责管理镜像,容器,网络以及数据卷等。

二、Docker容器的底层实现

Docker提供了一个打包和运行应用的隔离环境,称之为容器,docker容器本质上是把系统中为同一个业务目标服务的相关进程合成一组,放在一个叫做namespace的空间中,同一个namespace中的进程能够互相通信,但看不见其他namespace中的进程。每个namespace可以拥有自己独立的主机名、进程ID系统、IPC、网络、文件系统、用户等等资源。Docker的隔离和安全特性允许你在一个主机同时运行多个容器,而且它并不像虚拟机那样重量级,容器都是基于宿主机的内核运行的,它是轻量的,不管你运行的是ubuntu, debian还是其他Linux系统,用的内核都是宿主机内核。

Docker底层基石namespace与cgroup1

容器 = cgroup + namespace + 联合文件系统 + 容器引擎

  • Cgroup: 资源控制
  • namespace: 访问隔离
  • 联合文件系统:文件系统隔离
  • 容器引擎:生命周期控

2.1 Cgroup

控制组(cgroups)是 Linux 内核的一个特性,主要用来对共享资源进行隔离、限制、审计等。只有能控制分配到容器的资源,才能避免当多个容器同时运行时的对系统资源的竞争。

控制组技术最早是由 Google 的程序员 2006 年起提出,Linux 内核自 2.6.24 开始支持。

控制组可以提供对容器的内存、CPU、磁盘 IO 等资源的限制和审计管理。

子系统作用
devices设备权限控制
cpuset分配指定的CPU和内存节点
CPU控制CPU使用率
cpuacct统计CPU使用情况
memory限制内存的使用上限
freezer暂停Cgroup 中的进程
net_cls配合流控限制网络带宽
net_prio设置进程的网络流量优先级
perf_event允许 Perf 工具基于 Cgroup 分组做性能检测
huge_tlb限制 HugeTLB 的使用

在 Cgroup 出现之前,只能对一个进程做资源限制,如 ulimit 限制一个进程的打开文件上限、栈大小。而 Cgroup 可以对进程进行任意分组,如何分组由用户自定义。

子系统介绍

  1. cpuset 子系统
    cpuset 可以为一组进程分配指定的CPU和内存节点。 cpuset 一开始用在高性能计算上,在 NUMA(non-uniform memory access) 架构的服务器上,通过将进程绑定到固定的 CPU 和内存节点上,来避免进程在运行时因跨节点内存访问而导致的性能下降。

cpuset 的主要接口如下:

  • cpuset.cpus: 允许进程使用的CPU列表
  • cpuset.mems: 允许进程使用的内存节点列表
  1. cpu 子系统
    cpu 子系统用于限制进程的 CPU 利用率。具体支持三个功能
    第一,CPU 比重分配。使用 cpu.shares 接口。
    第二,CPU 带宽限制。使用 cpu.cfs_period_us 和 cpu.cfs_quota_us 接口。
    第三, 实时进程的 CPU 带宽限制。使用 cpu_rt_period_us 和 cpu_rt_quota_us 接口。
  1. cpuacct 子系统
    统计各个 Cgroup 的 CPU 使用情况,有如下接口:
  • cpuacct.stat: 报告这个 Cgroup 在用户态和内核态消耗的 CPU 时间,单位是 赫兹。
  • cpuacct.usage: 报告该 Cgroup 消耗的总 CPU 时间。
  • cpuacct.usage_percpu:报告该 Cgroup 在每个 CPU 上的消耗时间。
  1. memory 子系统
    限制 Cgroup 所能使用的内存上限。
  • memory.limit_in_bytes:设定内存上限,单位字节。
    默认情况下,如果使用的内存超过上限,Linux 内核会试图回收内存,如果这样仍无法将内存降到限制的范围内,就会触发 OOM,选择杀死该Cgroup 中的某个进程。
  • memory.memsw,limit_in_bytes: 设定内存加上交换内存区的总量。
  • memory.oom_control: 如果设置为0,那么内存超过上限时,不会杀死进程,而是阻塞等待进程释放内存;同时系统会向用户态发送事件通知。
  • memory.stat: 报告内存使用信息。
  1. blkio
    限制 Cgroup 对 阻塞 IO 的使用。
  • blkio.weight: 设置权值,范围在[100, 1000],属于比重分配,不是绝对带宽。因此只有当不同 Cgroup 争用同一个 阻塞设备时才起作用
  • blkio.weight_device: 对具体设备设置权值。它会覆盖上面的选项值。
  • blkio.throttle.read_bps_device: 对具体的设备,设置每秒读磁盘的带宽上限。
  • blkio.throttle.write_bps_device: 对具体的设备,设置每秒写磁盘的带宽上限。
  • blkio.throttle.read_iops_device: 对具体的设备,设置每秒读磁盘的IOPS带宽上限。
  • blkio.throttle.write_iops_device: 对具体的设备,设置每秒写磁盘的IOPS带宽上限。
  1. devices 子系统
    控制 Cgroup 的进程对哪些设备有访问权限
  • devices.list: 只读文件,显示目前允许被访问的设备列表,文件格式为
    类型[a|b|c] 设备号[major:minor] 权限[r/w/m 的组合]
    a/b/c 表示 所有设备、块设备和字符设备。

  • devices.allow: 只写文件,以上述格式描述允许相应设备的访问列表。

  • devices.deny: 只写文件,以上述格式描述禁止相应设备的访问列表。

2.2 Namespace

Namespace 是将内核的全局资源做封装,使得每个namespace 都有一份独立的资源,因此不同的进程在各自的namespace内对同一种资源的使用互不干扰。
举个例子,执行sethostname这个系统调用会改变主机名,这个主机名就是全局资源,内核通过 UTS Namespace可以将不同的进程分隔在不同的 UTS Namespace 中,在某个 Namespace 修改主机名时,另一个 Namespace 的主机名保持不变。

目前,Linux 内核实现了6种 Namespace。

Namespace作用
IPC隔离 System V IPC 和 POSIX 消息队列
Network隔离网络资源
Mount隔离文件系统挂载点
PID隔离进程ID
UTS隔离主机名和域名
User隔离用户和用户组

与命名空间相关的三个系统调用:
clone创建全新的Namespace,由clone创建的新进程就位于这个新的namespace里。创建时传入 flags参数,可选值有 CLONE_NEWIPC, CLONE_NEWNET, CLONE_NEWNS, CLONE_NEWPID, CLONE_NEWUTS, CLONE_NEWUSER, 分别对应上面六种namespace。

unshare为已有进程创建新的namespace。

setns把某个进程放在已有的某个namespace里。

6种命名空间

  1. UTS namespace
    UTS namespace 对主机名和域名进行隔离。为什么要隔离主机名?因为主机名可以代替IP来访问。如果不隔离,同名访问会出冲突。

  2. IPC namespace
    Linux 提供很多种进程通信机制,IPC namespace 针对 System V 和 POSIX 消息队列,这些 IPC 机制会使用标识符来区别不同的消息队列,然后两个进程通过标识符找到对应的消息队列。
    IPC namespace 使得 相同的标识符在两个 namespace 代表不同的消息队列,因此两个namespace 中的进程不能通过 IPC 来通信。

  3. PID namespace
    隔离进程号,不同namespace 的进程可以使用相同的进程号。
    当创建一个 PID namespace 时,第一个进程的PID 是1,即 init 进程。它负责回收所有孤儿进程的资源,所有发给 init 进程的信号都会被屏蔽。

  4. Mount namespace
    隔离文件挂载点,每个进程能看到的文件系统都记录在/proc/$$/mounts里。在一个 namespace 里挂载、卸载的动作不会影响到其他 namespace。

  5. Network namespace
    隔离网络资源。每个 namespace 都有自己的网络设备、IP、路由表、/proc/net 目录、端口号等。网络隔离可以保证独立使用网络资源,比如开发两个web 应用可以使用80端口。
    新创建的 Network namespace 只有 loopback 一个网络设备,需要手动添加网络设备。

  6. User namespace
    隔离用户和用户组。它的厉害之处在于,可以让宿主机上的一个普通用户在 namespace 里成为 0 号用户,也就是 root 用户。这样普通用户可以在容器内“随心所欲”,但是影响也仅限在容器内。

最后,回到 Docker 上,经过上述讨论,namespace 和 cgroup 的使用很灵活,需要注意的地方也很多。 Docker 通过 Libcontainer 来做这些脏活累活。用户只需要使用 Docker API 就可以优雅地创建一个容器。docker exec 的底层实现就是上面提过的 setns

2.3 联合文件系统

联合文件系统(UnionFS)是一种分层、轻量级并且高性能的文件系统,它支持对文件系统的修改作为一次提交来一层层的叠加,同时可以将不同目录挂载到同一个虚拟文件系统下(unite several directories into a single virtual filesystem)。

联合文件系统是 Docker 镜像的基础。镜像可以通过分层来进行继承,基于基础镜像(没有父镜像),可以制作各种具体的应用镜像。

另外,不同 Docker 容器就可以共享一些基础的文件系统层,同时再加上自己独有的改动层,大大提高了存储的效率。

Docker 中使用的 AUFS(AnotherUnionFS)就是一种联合文件系统。 AUFS 支持为每一个成员目录(类似 Git 的分支)设定只读(readonly)、读写(readwrite)和写出(whiteout-able)权限, 同时 AUFS 里有一个类似分层的概念, 对只读权限的分支可以逻辑上进行增量地修改(不影响只读部分的)。

Docker 目前支持的联合文件系统种类包括 AUFS, btrfs, vfs 和 DeviceMapper。


先来看一下,Linux 操作系统内核启动时,内核会先挂载一个只读的 rootfs,当系统检测其完整性之后,决定是否将其切换到读写模式。
Docker 沿用这种思想,不同的是,挂载rootfs 完毕之后,没有像 Linux 那样将容器的文件系统切换到读写模式,而是利用联合挂载技术,在这个只读的 rootfs 上挂载一个读写的文件系统,挂载后该读写文件系统空空如也。Docker 文件系统简单理解为:只读的 rootfs + 可读写的文件系统。
假设运行了一个 Ubuntu 镜像,其文件系统简略如下

Ubuntu 容器文件视角

参考文献:

https://www.jianshu.com/p/ab423c3db59d

https://www.huaweicloud.com/articles/b9087ca6d7d69fc6a8b8e8310af9f36e.html

https://www.cnblogs.com/mrhelloworld/p/docker2.html

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值