Docker基础
使用docker和 docker-compose 安装 高可用mysql , 高可用 rocketmq ,等一系列的中间件。
作为大神或者准架构师/架构师,一定要了解一下docker的底层原理。
首先还是简单说明一下docker的简介。
Docker 简介
Docker 是一个开源的应用容器引擎,基于 Go 语言 并遵从 Apache2.0 协议开源。
Docker 可以让开发者打包他们的应用以及依赖包到一个轻量级、可移植的容器中,然后发布到任何流行的 Linux 机器上,也可以实现虚拟化。
容器是完全使用沙箱机制,相互之间不会有任何接口(类似 iPhone 的 app),更重要的是容器性能开销极低。
Docker 从 17.03 版本之后分为 CE(Community Edition: 社区版) 和 EE(Enterprise Edition: 企业版),我们用社区版就可以了
Docker的应用场景
- Web 应用的自动化打包和发布。
- 自动化测试和持续集成、发布。
- 在服务型环境中部署和调整数据库或其他的后台应用。
- 从头编译或者扩展现有的 OpenShift 或 Cloud Foundry 平台来搭建自己的 PaaS 环境。
Docker 架构
Docker 包括三个基本概念:
- 镜像(Image):Docker 镜像(Image),就相当于是一个 root 文件系统。比如官方镜像 ubuntu:16.04 就包含了完整的一套 Ubuntu16.04 最小系统的 root 文件系统。
- 容器(Container):镜像(Image)和容器(Container)的关系,就像是面向对象程序设计中的类和实例一样,镜像是静态的定义,容器是镜像运行时的实体。容器可以被创建、启动、停止、删除、暂停等。
- 仓库(Repository):仓库可看成一个代码控制中心,用来保存镜像。
Docker 使用客户端-服务器 (C/S) 架构模式,使用远程API来管理和创建Docker容器。
Docker 容器通过 Docker 镜像来创建。
概念 | 说明 |
---|---|
Docker 镜像(Images) | Docker 镜像是用于创建 Docker 容器的模板,比如 Ubuntu 系统。 |
Docker 容器(Container) | 容器是独立运行的一个或一组应用,是镜像运行时的实体。 |
Docker 客户端(Client) | Docker 客户端通过命令行或者其他工具使用 Docker SDK (https://docs.docker.com/develop/sdk/) 与 Docker 的守护进程通信。 |
Docker 主机(Host) | 一个物理或者虚拟的机器用于执行 Docker 守护进程和容器。 |
Docker Registry | Docker 仓库用来保存镜像,可以理解为代码控制中的代码仓库。Docker Hub(https://hub.docker.com) 提供了庞大的镜像集合供使用。一个 Docker Registry 中可以包含多个仓库(Repository);每个仓库可以包含多个标签(Tag);每个标签对应一个镜像。通常,一个仓库会包含同一个软件不同版本的镜像,而标签就常用于对应该软件的各个版本。我们可以通过 <仓库名>:<标签> 的格式来指定具体是这个软件哪个版本的镜像。如果不给出标签,将以 latest 作为默认标签。 |
最为常用的几个命令
docker的守护进程查看
systemctl status docker
docker 镜像查看
docker image ls
docker 容器查看
docker ps
Docker Registry配置和查看
cat /etc/docker/daemon.json
配置私有仓库
cat>/etc/docker/daemon.json<<EOF
{
"registry-mirrors":["http://10.24.2.30:5000","https://tnxkcso1.mirrors.aliyuncs.com"],
"insecure-registries":["10.24.2.30:5000"]
}
EOF
Docker 的发展历史
Docker 公司前身是 DotCloud,由 Solomon Hykes 在2010年成立,2013年更名 Docker。同年发布了 Docker-compose 组件提供容器的编排工具。
2014年 Docker 发布1.0版本,2015年Docker 提供 Docker-machine,支持 windows 平台。
在此期间,Docker 项目在开源社区大受追捧,同时也被业界诟病的是 Docker 公司对于 Docker 发展具有绝对的话语权,比如 Docker 公司推行了 libcontainer 难以被社区接受。
为了防止 Docker 这项开源技术被Docker 公司控制,在几个核心贡献的厂商,于是在一同贡献 Docker 代码的公司诸如 Redhat,谷歌的倡导下,成立了 OCI 开源社区。
OCI 开源社区旨在于将 Docker 的发展权利回归社区,当然反过来讲,Docker 公司也希望更多的厂商安心贡献代码到Docker 项目,促进 Docker 项目的发展。
于是通过OCI建立了 runc 项目,替代 libcontainer,这为开发者提供了除 Docker 之外的容器化实现的选择。
OCI 社区提供了 runc 的维护,而 runc 是基于 OCI 规范的运行容器的工具。换句话说,你可以通过 runc,提供自己的容器实现,而不需要依赖 Docker。当然,Docker 的发行版底层也是用的 runc。在 Docker 宿主机上执行 runc,你会发现它的大多数命令和 Docker 命令类似,感兴趣的读者可以自己实践如何用 runc 启动容器。
至2017年,Docker 项目转移到 Moby 项目,基于 Moby 项目,Docker 提供了两种发行版,Docker CE 和 Docker EE, Docker CE 就是目前大家普遍使用的版本,Docker EE 成为付费版本,提供了容器的编排,Service 等概念。Docker 公司承诺 Docker 的发行版会基于 Moby 项目。这样一来,通过 Moby 项目,你也可以自己打造一个定制化的容器引擎,而不会被 Docker 公司绑定。
Docker 与虚拟机有何区别
Docker 的误解:Docker 是轻量级的虚拟机。
很多人将docker理解为, Docker 实现了类似于虚拟化的技术,能够让应用跑在一些轻量级的容器里。这么理解其实是错误的。
到底什么是docker:
Docker是一个Client-Server结构的系统,Docker守护进程运行在主机上, 然后通过Socket连接从客户端访问Docker守护进程。
Docker守护进程从客户端接受命令,并按照命令,管理运行在主机上的容器。
一个docker 容器,是一个运行时环境,可以简单理解为进程运行的集装箱。
如图下所示
docker和kvm都是虚拟化技术,它们的主要差别:
1、Docker有着比虚拟机更少的抽象层
2、docker利用的是宿主机的内核,VM需要的是Guest OS
二者的不同:
- VM(VMware)在宿主机器、宿主机器操作系统的基础上创建虚拟层、虚拟化的操作系统、虚拟化的仓库,然后再安装应用;
- Container(Docker容器),在宿主机器、宿主机器操作系统上创建Docker引擎,在引擎的基础上再安装应用。
所以说,新建一个容器的时候,docker不需要像虚拟机一样重新加载一个操作系统,避免引导。docker是利用宿主机的操作系统,省略了这个复杂的过程,秒级!
虚拟机是加载Guest OS ,这是分钟级别的
与传统VM特性对比:
作为一种轻量级的虚拟化方式,Docker在运行应用上跟传统的虚拟机方式相比具有显著优势:
- Docker 容器很快,启动和停止可以在秒级实现,这相比传统的虚拟机方式要快得多。
- Docker 容器对系统资源需求很少,一台主机上可以同时运行数千个Docker容器。
- Docker 通过类似Git的操作来方便用户获取、分发和更新应用镜像,指令简明,学习成本较低。
- Docker 通过Dockerfile配置文件来支持灵活的自动化创建和部署机制,提高工作效率。
- Docker 容器除了运行其中的应用之外,基本不消耗额外的系统资源,保证应用性能的同时,尽量减小系统开销。
- Docker 利用Linux系统上的多种防护机制实现了严格可靠的隔离。从1.3版本开始,Docker引入了安全选项和镜像签名机制,极大地提高了使用Docker的安全性。
特性 | 容器 | 虚拟机 |
---|---|---|
启动速度 | 秒级 | 分钟级 |
硬盘使用 | 一般为MB | 一般为GB |
性能 | 接近原生 | 弱于原生 |
系统支持量 | 单机支持上千个容器 | 一般几十个 |
Docker 技术的基础:
- namespace,容器隔离的基础,保证A容器看不到B容器. 6个名空间:User,Mnt,Network,UTS,IPC,Pid
- cgroups,容器资源统计和隔离。主要用到的cgroups子系统:cpu,blkio,device,freezer,memory
- unionfs,典型:aufs/overlayfs,分层镜像实现的基础
Linux 命名空间、控制组和 UnionFS 三大技术支撑了目前 Docker 的实现,也是 Docker 能够出现的最重要原因。
UnionFS
UnionFS 其实是一种为 Linux 操作系统设计的用于把多个文件系统『联合』到同一个挂载点的文件系统服务。而 AUFS 即 Advanced UnionFS 其实就是 UnionFS 的升级版,它能够提供更优秀的性能和效率。
AUFS 作为联合文件系统,它能够将不同文件夹中的层联合(Union)到了同一个文件夹中,这些文件夹在 AUFS 中称作分支,整个『联合』的过程被称为联合挂载(Union Mount):
每一个镜像层或者容器层都是 /var/lib/docker/ 目录下的一个子文件夹;在 Docker 中,所有镜像层和容器层的内容都存储在 /var/lib/docker/aufs/diff/ 目录中:
$ ls /var/lib/docker/aufs/diff/00adcccc1a55a36a610a6ebb3e07cc35577f2f5a3b671be3dbc0e74db9ca691c 93604f232a831b22aeb372d5b11af8c8779feb96590a6dc36a80140e38e764d8
00adcccc1a55a36a610a6ebb3e07cc35577f2f5a3b671be3dbc0e74db9ca691c-init 93604f232a831b22aeb372d5b11af8c8779feb96590a6dc36a80140e38e764d8-init
019a8283e2ff6fca8d0a07884c78b41662979f848190f0658813bb6a9a464a90 93b06191602b7934fafc984fbacae02911b579769d0debd89cf2a032e7f35cfa
...
而 /var/lib/docker/aufs/layers/ 中存储着镜像层的元数据,每一个文件都保存着镜像层的元数据,最后的 /var/lib/docker/aufs/mnt/ 包含镜像或者容器层的挂载点,最终会被 Docker 通过联合的方式进行组装。
上面的这张图片非常好的展示了组装的过程,每一个镜像层都是建立在另一个镜像层之上的,同时所有的镜像层都是只读的,只有每个容器最顶层的容器层才可以被用户直接读写,所有的容器都建立在一些底层服务(Kernel)上,包括命名空间、控制组、rootfs 等等,这种容器的组装方式提供了非常大的灵活性,只读的镜像层通过共享也能够减少磁盘的占用。
什么是镜像
那么问题来了,没有操作系统,怎么运行程序?
可以在Docker中创建一个ubuntu的镜像文件,这样就能将ubuntu系统集成到Docker中,运行的应用就都是ubuntu的应用。
实际上 Docker 是使用了很多 Linux 的隔离功能,让容器看起来像一个轻量级虚拟机在独立运行,容器的本质是被限制了的 Namespaces,cgroup,具有逻辑上独立文件系统,网络的一个进程。其底层运用了如下 Linux 的能力:
Namespaces
命名空间(namespaces)是 Linux 为我们提供的用于分离进程树、网络接口、挂载点以及进程间通信等资源的方法。在日常使用 Linux 或者 macOS 时,我们并没有运行多个完全分离的服务器的需要,但是如果我们在服务器上启动了多个服务,这些服务其实会相互影响的,每一个服务都能看到其他服务的进程,也可以访问宿主机器上的任意文件,这是很多时候我们都不愿意看到的,我们更希望运行在同一台机器上的不同服务能做到完全隔离,就像运行在多台不同的机器上一样。
Linux 的命名空间机制提供了以下七种不同的命名空间,包括 CLONE_NEWCGROUP、CLONE_NEWIPC、CLONE_NEWNET、CLONE_NEWNS、CLONE_NEWPID、CLONE_NEWUSER 和 CLONE_NEWUTS,通过这七个选项我们能在创建新的进程时设置新进程应该在哪些资源上与宿主机器进行隔离。
Namespaces
这里提出一个问题,在宿主机上启动两个容器,在这两个容器内都各有一个 PID=1的进程,众所周知,Linux 里 PID 是唯一的,既然 Docker 不是跑在宿主机上的两个虚拟机,那么它是如何实现在宿主机上运行两个相同 PID 的进程呢?
这里就用到了 Linux Namespaces,它其实是 Linux 创建新进程时的一个可选参数,在 Linux 系统中创建进程的系统调用是 clone()方法。
通过调用这个方法,这个进程会获得一个独立的进程空间,它的 pid 是1,并且看不到宿主机上的其他进程,这也就是在容器内执行 PS 命令的结果。
不仅仅是 PID,当你启动启动容器之后,Docker 会为这个容器创建一系列其他 namespaces。
这些 namespaces 提供了不同层面的隔离。容器的运行受到各个层面 namespace 的限制。
Docker Engine 使用了以下 Linux 的隔离技术:
The pid namespace: 管理 PID 命名空间 (PID: Process ID).
The net namespace: 管理网络命名空间(NET: Networking).
The ipc namespace: 管理进程间通信命名空间(IPC: InterProcess Communication).
The mnt namespace: 管理文件系统挂载点命名空间 (MNT: Mount).
The uts namespace: Unix 时间系统隔离. (UTS: Unix Timesharing System).
通过这些技术,运行时的容器得以看到一个和宿主机上其他容器隔离的环境。
进程
进程是 Linux 以及现在操作系统中非常重要的概念,它表示一个正在执行的程序,也是在现代分时系统中的一个任务单元。在每一个 *nix 的操作系统上,我们都能够通过 ps 命令打印出当前操作系统中正在执行的进程,比如在 Ubuntu 上,使用该命令就能得到以下的结果:
当前的 Docker 容器成功将容器内的进程与宿主机器中的进程隔离,如果我们在宿主机器上打印当前的全部进程时,会得到下面三条与 Docker 相关的结果:
UID PID PPID C STIME TTY TIME CMD
root 29407 1 0 Nov16 ? 00:08:38 /usr/bin/dockerd --raw-logs
root 1554 29407 0 Nov19 ? 00:03:28 docker-containerd -l unix:///var/run/docker/libcontainerd/docker-containerd.sock --metrics-interval=0 --start-timeout 2m --state-dir /var/run/docker/libcontainerd/containerd --shim docker-containerd-shim --runtime docker-runc
root 5006 1554 0 08:38 ? 00:00:00 docker-containerd-shim b809a2eb3630e64c581561b08ac46154878ff1c61c6519848b4a29d412215e79 /var/run/docker/libcontainerd/b809a2eb3630e64c581561b08ac46154878ff1c61c6519848b4a29d412215e79 docker-runc
在当前的宿主机器上,可能就存在由上述的不同进程构成的进程树:
这就是在使用 clone(2) 创建新进程时传入 CLONE_NEWPID 实现的,也就是使用 Linux 的命名空间实现进程的隔离,Docker 容器内部的任意进程都对宿主机器的进程一无所知。
containerRouter.postContainersStart
└── daemon.ContainerStart
└── daemon.createSpec
└── setNamespaces
└── setNamespace
Docker 的容器就是使用上述技术实现与宿主机器的进程隔离,当我们每次运行 docker run 或者 docker start 时,都会在下面的方法中创建一个用于设置进程间隔离的 Spec:
func (daemon *Daemon) createSpec(c *container.Container) (*specs.Spec, error) {
s := oci.DefaultSpec()
// ...
if err := setNamespaces(daemon, &s, c); err != nil {
return nil, fmt.Errorf("linux spec namespaces: %v", err)
}
return &s, nil
}
在 setNamespaces 方法中不仅会设置进程相关的命名空间,还会设置与用户、网络、IPC 以及 UTS 相关的命名空间:
func setNamespaces(daemon *Daemon, s *specs.Spec, c *container.Container) error {
// user
// network
// ipc
// uts
// pid
if c.HostConfig.PidMode.IsContainer() {
ns := specs.LinuxNamespace{Type: "pid"}
pc, err := daemon.getPidContainer(c)
if err != nil {
return err
}
ns.Path = fmt.Sprintf("/proc/%d/ns/pid", pc.State.GetPID())
setNamespace(s, ns)
} else if c.HostConfig.PidMode.IsHost() {
oci.RemoveNamespace(s, specs.LinuxNamespaceType("pid"))
} else {
ns := specs.LinuxNamespace{Type: "pid"}
setNamespace(s, ns)
}
return nil
}
所有命名空间相关的设置 Spec 最后都会作为 Create 函数的入参在创建新的容器时进行设置:
daemon.containerd.Create(context.Background(), container.ID, spec, createOptions)
所有与命名空间的相关的设置都是在上述的两个函数中完成的,Docker 通过命名空间成功完成了与宿主机进程和网络的隔离。
网络
如果 Docker 的容器通过 Linux 的命名空间完成了与宿主机进程的网络隔离,但是却有没有办法通过宿主机的网络与整个互联网相连,就会产生很多限制,所以 Docker 虽然可以通过命名空间创建一个隔离的网络环境,但是 Docker 中的服务仍然需要与外界相连才能发挥作用。
这个地址其实也是 Docker 为 Redis 服务分配的 IP 地址,如果我们在当前机器上直接 ping 这个 IP 地址就会发现它是可以访问到的:
$ ping 192.168.0.4
PING 192.168.0.4 (192.168.0.4) 56(84) bytes of data.
64 bytes from 192.168.0.4: icmp_seq=1 ttl=64 time=0.069 ms
64 bytes from 192.168.0.4: icmp_seq=2 ttl=64 time=0.043 ms
^C
--- 192.168.0.4 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 999ms
rtt min/avg/max/mdev = 0.043/0.056/0.069/0.013 ms
从上述的一系列现象,我们就可以推测出 Docker 是如何将容器的内部的端口暴露出来并对数据包进行转发的了;当有 Docker 的容器需要将服务暴露给宿主机器,就会为容器分配一个 IP 地址,同时向 iptables 中追加一条新的规则。
当我们使用 redis-cli 在宿主机器的命令行中访问 127.0.0.1:6379 的地址时,经过 iptables 的 NAT PREROUTING 将 ip 地址定向到了 192.168.0.4,重定向过的数据包就可以通过 iptables 中的 FILTER 配置,最终在 NAT POSTROUTING 阶段将 ip 地址伪装成 127.0.0.1,到这里虽然从外面看起来我们请求的是 127.0.0.1:6379,但是实际上请求的已经是 Docker 容器暴露出的端口了。
$ redis-cli -h 127.0.0.1 -p 6379 ping
PONG
Docker 通过 Linux 的命名空间实现了网络的隔离,又通过 iptables 进行数据包转发,让 Docker 容器能够优雅地为宿主机器或者其他容器提供服务。
Libnetwork
整个网络部分的功能都是通过 Docker 拆分出来的 libnetwork 实现的,它提供了一个连接不同容器的实现,同时也能够为应用给出一个能够提供一致的编程接口和网络层抽象的容器网络模型。
The goal of libnetwork is to deliver a robust Container Network Model that provides a consistent programming interface and the required network abstractions for applications.
libnetwork 中最重要的概念,容器网络模型由以下的几个主要组件组成,分别是 Sandbox、Endpoint 和 Network:
想要获得更多与 libnetwork 或者容器网络模型相关的信息,可以阅读 Design · libnetwork 了解更多信息,当然也可以阅读源代码了解不同 OS 对容器网络模型的不同实现。
挂载点
虽然我们已经通过 Linux 的命名空间解决了进程和网络隔离的问题,在 Docker 进程中我们已经没有办法访问宿主机器上的其他进程并且限制了网络的访问,但是 Docker 容器中的进程仍然能够访问或者修改宿主机器上的其他目录,这是我们不希望看到的。
在新的进程中创建隔离的挂载点命名空间需要在 clone 函数中传入 CLONE_NEWNS,这样子进程就能得到父进程挂载点的拷贝,如果不传入这个参数子进程对文件系统的读写都会同步回父进程以及整个主机的文件系统。
这一部分的内容是作者在 libcontainer 中的 SPEC.md 文件中找到的,其中包含了 Docker 使用的文件系统的说明,对于 Docker 是否真的使用 chroot 来确保当前的进程无法访问宿主机器的目录,作者其实也没有确切的答案,一是 Docker 项目的代码太多庞大,不知道该从何入手,作者尝试通过 Google 查找相关的结果,但是既找到了无人回答的问题,也得到了与 SPEC 中的描述有冲突的答案,如果各位读者有明确的答案可以在博客下面留言,非常感谢。
Chroot
在这里不得不简单介绍一下 chroot(change root),在 Linux 系统中,系统默认的目录就都是以 / 也就是根目录开头的,chroot 的使用能够改变当前的系统根目录结构,通过改变当前系统的根目录,我们能够限制用户的权利,在新的根目录下并不能够访问旧系统根目录的结构个文件,也就建立了一个与原系统完全隔离的目录结构。
与 chroot 的相关内容部分来自《理解 chroot》一文,各位读者可以阅读这篇文章获得更详细的信息。
CGroups
Cgroups
在容器环境里,如果不做限制,运行一个 Java 应用,当应用有 Bug 导致 JVM 进行 Full GC 的时候,会占用大量的内存,也可能导致 CPU 飙升到100%,如何避免由单个容器的问题,导致整个集群不可用?Docker 用到了 Cgroups。Cgroups 是 Control Group 的缩写,由2007年谷歌工程师研发,2008年并入 Linux Kernel 2.6.24,由 C 语言编写。
Docker 底层使用 groups 对进程进行 CPU,Mem,网络等资源的使用限制,从而实现在宿主机上的资源分配,不至于出现一个容器占用所有宿主机的 CPU 或者内存。
具体的实现原理如上图所示,通过使用 cgroups,为不同的进程设定不同的配额,上图中 cgroup1 的进程只能使用60%的 CPU,cgroup2 使用20%的 CPU,同样可以为进程设定容器。所以当你在 Kubernetes 里为某个 pod 声明 CPU 限额时,底层就是调用的 cgroup 的设置。具体实现如下:
进入宿主机 cgroup 目录: cd /sys/fs/cgroup/cpu
为进程创建一个目录,例如 my_container, 然后 cgroup 会自动在这个目录下创建多个默认配置:
在cpu.cfs_quota_us里输入对应的数值,即可实现对 CPU 的配额设置:
echo 60000 > /sys/fs/cgroup/cpu/container/cpu.cfs_ q uota_us,这里的单位是毫秒,意思是每一毫秒内,该进程能够使用60%的 CPU 时间。如何将该配置应用到进程里?
echo 22880 > /sys/fs/cgroup/cpu/container/task s,其中22880是宿主机上的 PID,这时候你通过调用 top 命令,能够看到该进程的 CPU 使用率不会超过60%,这样就能实现对进程的限制,避免之前的问题发生。
我们通过 Linux 的命名空间为新创建的进程隔离了文件系统、网络并与宿主机器之间的进程相互隔离,但是命名空间并不能够为我们提供物理资源上的隔离,比如 CPU 或者内存,如果在同一台机器上运行了多个对彼此以及宿主机器一无所知的『容器』,这些容器却共同占用了宿主机器的物理资源。
每一个 CGroup 都是一组被相同的标准和参数限制的进程,不同的 CGroup 之间是有层级关系的,也就是说它们之间可以从父类继承一些用于限制资源使用的标准和参数。
Linux 的 CGroup 能够为一组进程分配资源,也就是我们在上面提到的 CPU、内存、网络带宽等资源,通过对资源的分配,CGroup 能够提供以下的几种功能:
Linux 使用文件系统来实现 CGroup,我们可以直接使用下面的命令查看当前的 CGroup 中有哪些子系统:
$ lssubsys -m
cpuset /sys/fs/cgroup/cpuset
cpu /sys/fs/cgroup/cpu
cpuacct /sys/fs/cgroup/cpuacct
memory /sys/fs/cgroup/memory
devices /sys/fs/cgroup/devices
freezer /sys/fs/cgroup/freezer
blkio /sys/fs/cgroup/blkio
perf_event /sys/fs/cgroup/perf_event
hugetlb /sys/fs/cgroup/hugetlb
大多数 Linux 的发行版都有着非常相似的子系统,而之所以将上面的 cpuset、cpu 等东西称作子系统,是因为它们能够为对应的控制组分配资源并限制资源的使用。
如果系统管理员想要控制 Docker 某个容器的资源使用率就可以在 docker 这个父控制组下面找到对应的子控制组并且改变它们对应文件的内容,当然我们也可以直接在程序运行时就使用参数,让 Docker 进程去改变相应文件中的内容。
$ docker run -it -d --cpu-quota=50000 busybox
53861305258ecdd7f5d2a3240af694aec9adb91cd4c7e210b757f71153cdd274
$ cd 53861305258ecdd7f5d2a3240af694aec9adb91cd4c7e210b757f71153cdd274/
$ ls
cgroup.clone_children cgroup.event_control cgroup.procs cpu.cfs_period_us cpu.cfs_quota_us cpu.shares cpu.stat notify_on_release tasks
$ cat cpu.cfs_quota_us
50000
当我们使用 Docker 关闭掉正在运行的容器时,Docker 的子控制组对应的文件夹也会被 Docker 进程移除,Docker 在使用 CGroup 时其实也只是做了一些创建文件夹改变文件内容的文件操作,不过 CGroup 的使用也确实解决了我们限制子容器资源占用的问题,系统管理员能够为多个容器合理的分配资源并且不会出现多个容器互相抢占资源的问题。
UnionFS
Linux 的命名空间和控制组分别解决了不同资源隔离的问题,前者解决了进程、网络以及文件系统的隔离,后者实现了 CPU、内存等资源的隔离,但是在 Docker 中还有另一个非常重要的问题需要解决 - 也就是镜像。
镜像到底是什么,它又是如何组成和组织的是作者使用 Docker 以来的一段时间内一直比较让作者感到困惑的问题,我们可以使用 docker run 非常轻松地从远程下载 Docker 的镜像并在本地运行。
Docker 使用了一系列不同的存储驱动管理镜像内的文件系统并运行容器,这些存储驱动与 Docker 卷(volume)有些不同,存储引擎管理着能够在多个容器之间共享的存储。
容器和镜像的区别就在于,所有的镜像都是只读的,而每一个容器其实等于镜像加上一个可读写的层,也就是同一个镜像可以对应多个容器。
dockers=LXC+AUFS
docker为LXC+AUFS组合:
- LXC负责资源管理
- AUFS负责镜像管理;
而LXC包括cgroup,namespace,chroot等组件,并通过cgroup资源管理,那么,从资源管理的角度来看,Docker,Lxc,Cgroup三者的关系是怎样的呢?
cgroup是在底层落实资源管理,LXC在cgroup上面封装了一层,随后,docker有在LXC封装了一层;
Cgroup其实就是linux提供的一种限制,记录,隔离进程组所使用的物理资源管理机制;也就是说,Cgroup是LXC为实现虚拟化所使用资源管理手段,我们可以这样说,底层没有cgroup支持,也就没有lxc,更别说docker的存在了,这是我们需要掌握和理解的,三者之间的关系概念
我们在把重心转移到LXC这个相当于中间件上,上述我们提到LXC是建立在cgroup基础上的,我们可以粗略的认为LXC=Cgroup+namespace+Chroot+veth+用户控制脚本;LXC利用内核的新特性(cgroup)来提供用户空间的对象,用来保证资源的隔离和对应用系统资源的限制;
Docker容器的文件系统最早是建立在Aufs基础上的,Aufs是一种Union FS,简单来说就是支持将不同的目录挂载到同一个虚拟文件系统之下
并实现一种laver的概念,
由于Aufs未能加入到linux内核中,考虑到兼容性的问题,便加入了Devicemapper的支持,Docker目前默认是建立在Devicemapper基础上,
devicemapper用户控件相关部分主要负责配置具体的策略和控制逻辑,比如逻辑设备和哪些物理设备建立映射,怎么建立这些映射关系等,而具体过滤和重定向IO请求的工作有内核中相关代码完成,因此整个device mapper机制由两部分组成--内核空间的device mapper驱动,用户控件的device mapper库以及它提供的dmsetup工具;
参考资料
https://docs.docker.com/storage/storagedriver/aufs-driver/#how-the-aufs-storage-driver-works
https://github.com/opencontainers/runc
http://www.sel.zju.edu.cn/?p=840
https://blog.csdn.net/wangqingjiewa/article/details/85000393