容器技术原理
容器,其实是一种特殊的进程而已。容器化后的用户应用,却依然还是一个宿主机上的普通进程,这就意味着这些因为虚拟化而带来的性能损耗都是不存在的
Docker 是利用 Linux 的 Namespace 、Cgroups 和联合文件系统三大机制来保证实现的, 所以它的原理是使用 Namespace 做主机名、网络、PID 等资源的隔离,使用 Cgroups 对进程或者进程组做资源(例如:CPU、内存等)的限制,联合文件系统用于镜像构建和容器运行环境.
一个“容器”,实际上是一个由 Linux Namespace、Linux Cgroups 和 rootfs 三种技术构建出来的进程的隔离环境。
一个正在运行的 Linux 容器,其实可以被“一分为二”地看待:
一组联合挂载在 /var/lib/docker/aufs/mnt 上的 rootfs,这一部分我们称为“容器镜像”(Container Image),是容器的静态视图;
一个由 Namespace+Cgroups 构成的隔离环境,这一部分我们称为“容器运行时”(Container Runtime),是容器的动态视图。
rootfs 里面打包的不只是应用,而是整个操作系统的文件和目录,也就意味着,应用和它运行所需要的所有依赖,都被封装在一起。(所有容器共享宿主机内核)
补
Kata Containers 与 gVisor 实现的方式与 Docker 不同
这两种容器实现的本质,都是给进程分配了一个独立的操作系统内核,从而避免了让容器共享宿主机的内核。这样,容器进程能够看到的攻击面,就从整个宿主机内核变成了一个极小的、独立的、以容器为单位的内核,从而有效解决了容器进程发生“逃逸”或者夺取整个宿主机的控制权的问题。
Kata Containers : 使用的是传统的虚拟化技术,通过虚拟硬件模拟出了一台“小虚拟机”,然后在这个小虚拟机里安装了一个裁剪后的 Linux 内核来实现强隔离。
gVisor 直接用 Go 语言“模拟”出了一个运行在用户态的操作系统内核,然后通过这个模拟的内核来代替容器进程向宿主机发起有限的、可控的系统调用。
Namespace
基于 Linux Namespace 的隔离机制相比于虚拟化技术也有很多不足之处,其中最主要的问题就是:隔离得不彻底。
Namespace 是 Linux 内核的一项功能,该功能对内核资源进行隔离,使得容器中的进程都可以在单独的命名空间中运行,并且只可以访问当前容器命名空间的资源。Namespace 可以隔离进程 ID、主机名、用户 ID、文件名、网络访问和进程间通信等相关资源。
Docker 主要用到以下五种命名空间。
pid namespace:用于隔离进程 ID。
net namespace:隔离网络接口,在虚拟的 net namespace 内用户可以拥有自己独立的 IP、路由、端口等。
mnt namespace:文件系统挂载点隔离。
ipc namespace:信号量,消息队列和共享内存的隔离。
uts namespace:主机名和域名的隔离
Cgroups
Cgroups 是一种 Linux 内核功能,可以限制和隔离进程的资源使用情况(CPU、内存、磁盘 I/O、网络等)。在容器的实现中,Cgroups 通常用来限制容器的 CPU 和内存等资源的使用。
cgroups 限制了固定几种资源的使用不会超限,但是它既不能隔离被共享的硬件比如 L3 cache,也不能有效地防止容器逃逸的问题1
2
3
4
5
6
7
8
9
$ docker run -it --cpu-period=100000 --cpu-quota=20000 ubuntu /bin/bash
$ cat /sys/fs/cgroup/cpu/docker/5d5c9f67d/cpu.cfs_period_us
100000 ## CPU period
$ cat /sys/fs/cgroup/cpu/docker/5d5c9f67d/cpu.cfs_quota_us
20000 ## 这就意味着这个 Docker 容器,只能使用到 20% 的 CPU 带宽。
cgroup 可以实现资源的限制,但不能保证资源的使用。
限制 cpu 使用1
2
3
4
5docker run -itd --name docker10 --cpuset-cpus 0,1 --cpu-shares 512 centos /bin/bash
#指定 docker10 只能在 cpu0 和 cpu1 上运行,而且 docker10 的使用 cpu 的份额 512
docker run -itd --name docker20 --cpuset-cpus 0,1 --cpu-shares 1024 centos /bin/bash
#指定 docker20 只能在 cpu0 和 cpu1 上运行,而且 docker20 的使用 cpu 的份额 1024,比 dcker10 多一倍
限制内存使用1
2
3
4docker run -it -m 128m centos
cat /sys/fs/cgroup/memory/memory.limit_in_bytes
134217728
限制硬盘速率1
2
3docker run -it -v /var/www/html/:/var/www/html --device /dev/sda:/dev/sda --device-write-bps /dev/sda:2mb centos /bin/bash
dd if=/dev/sda of=/var/www/html/test.out bs=2M count=50 oflag=direct,nonblock # direct:读写数据采用直接 IO 方式,不走缓存。直接从内存写硬盘上。nonblock:读写数据采用非阻塞 IO 方式,优先写 dd 命令的数据
联合文件系统
联合文件系统,又叫 UnionFS,是一种通过创建文件层进程操作的文件系统,因此,联合文件系统非常轻快。Docker 使用联合文件系统为容器提供构建层,使得容器可以实现写时复制以及镜像的分层构建和存储。常用的联合文件系统有 AUFS、Overlay 和 Devicemapper 等。
Docker 在镜像的设计中,引入了层(layer)的概念。也就是说,用户制作镜像的每一步操作,都会生成一个层,也就是一个增量 rootfs。
容器镜像是只读的,如果要修改必须通过 copy-on-write 机制把文件复制到只读层才行。
Docker 文件系统分层从下到上 : 只读层, init层,读写层
Init 层 : 是 Docker 项目单独生成的一个内部层,专门用来存放 /etc/hosts、/etc/resolv.conf 等信息。这些文件本来属于只读镜像的一部分,但实际写入一些指定的值比如 hostname,所以就需要在可读写层对它们进行修改,可是,这些修改往往只对当前的容器有效,我们并不希望执行 docker commit 时,把这些信息连同可读写层一起提交掉. 所以,Docker 做法是,在修改了这些文件之后,以一个单独的层挂载了出来。而用户执行 docker commit 只会提交可读写层,所以是不包含init 层的内容。
我现在有两个目录 A 和 B,它们分别有两个文件:
1 |
|
使用联合挂载的方式,将这两个目录挂载到一个公共的目录 C 上:
1 |
|
对于 AuFS 来说,它最关键的目录结构在 /var/lib/docker 路径下的 diff 目录:/var/lib/docker/aufs/diff/<layer_id>
Volume
Volume 机制,允许你将宿主机上指定的目录或者文件,挂载到容器里面进行读取和修改操作。
Docker 重要组件
containerd : containerd通过 containerd-shim 启动并管理 runC,可以说containerd真正管理了容器的生命周期。
containerd-shim
ctr
docker
docker-init
docker-proxy
dockerd
runc : 是一个用来运行容器的轻量级工具,是真正用来运行容器的。