docker 基础备忘

Docker 已经不是什么新鲜的事物了,Docker 崛起的核心是因为 Docker 镜像的存在,这个创新使得 Docker 在短短几年内就可以迅速地改变了整个云计算领域的发展历程。Docker 镜像的存在解决了传统 paas 平台对于打包问题的根本难题,使得“压缩包”赋予了一种极其宝贵的能力:本地环境和云端环境的高度一致!

Docker 镜像,其实就是一个压缩包。但是这个压缩包里的内容,比 PaaS 的应用可执行文件 + 启停脚本的组合就要丰富多了。实际上,大多数 Docker 镜像是直接由一个完整操作系统的所有文件和目录构成的,所以这个压缩包里的内容跟你本地开发和测试环境用的操作系统是完全一样的

Docker 使用 Google 公司推出的 Go 语言 进行开发实现,基于 Linux 内核的 cgroup,namespace,以及 OverlayFS 类的 Union FS 等技术,对进程进行封装隔离,属于操作系统层面的虚拟化技术。由于隔离的进程独立于宿主和其它的隔离的进程,因此也称其为容器。最初实现是基于 LXC,从 0.7 版本以后开始去除 LXC,转而使用自行开发的 libcontainer,从 1.11 开始,则进一步演进为使用 runC 和 containerd。

runc 是一个 Linux 命令行工具,用于根据 OCI容器运行时规范 创建和运行容器。
containerd 是一个守护程序,它管理容器生命周期,提供了在一个节点上执行容器和管理镜像的最小功能集。

容器技术与 Docker 架构

容器到底是什么玩意?

容器技术是一种沙盒技术,就是能够像一个“箱子”一样,把你的应用“装”起来的技术。通过这样一种“箱子”,使的应用与应用之间相互不干扰,另外就是被装进“箱子”的应用,也具备了可以被方便地搬来搬去的灵活性。

容器技术的核心功能,就是通过约束和修改进程的动态表现,从而为其创造出一个“边界”。对于 Docker 等大多数 Linux 容器来说,Cgroups 技术是用来制造约束的主要手段,而 Namespace 技术则是用来修改进程视图的主要方法。实际上是在创建容器进程时,指定了这个进程所需要启用的一组 Namespace 参数。这样,容器就只能“看”到当前 Namespace 所限定的资源、文件、设备、状态,或者配置。而对于宿主机以及其他不相关的程序,它就完全看不到了。就是 Linux 容器最基本的实现原理了,所以说,容器,其实是一种特殊的进程而已。

Docker 架构

下图是 Docker 官方提供的 Docker 架构图:

Docker 在容器的基础上,进行了进一步的封装,从文件系统、网络互联到进程隔离等等,极大的简化了容器的创建和维护。使得 Docker 技术比虚拟机技术更为轻便、快捷。

下面的图片比较了 Docker 和传统虚拟化方式的不同之处。传统虚拟机技术是虚拟出一套硬件后,在其上运行一个完整操作系统,在该系统上再运行所需应用进程;而容器内的应用进程直接运行于宿主的内核,容器内没有自己的内核,而且也没有进行硬件虚拟。因此容器要比传统虚拟机更为轻便。

在这个对比图里,我们应该把 Docker 画在跟应用同级别并且靠边的位置。这意味着,用户运行在容器里的应用进程,跟宿主机上的其他进程一样,都由宿主机操作系统统一管理,只不过这些被隔离的进程拥有额外设置过的 Namespace 参数。而 Docker 项目在这里扮演的角色,更多的是旁路式的辅助和管理工作。

隔离与限制

使用虚拟化技术作为应用沙盒,就必须要由 Hypervisor 来负责创建虚拟机,这个虚拟机是真实存在的,并且它里面必须运行一个完整的 Guest OS 才能执行用户的应用进程。这就不可避免地带来了额外的资源消耗和占用。而相比之下,容器化后的用户应用,却依然还是一个宿主机上的普通进程,这就意味着这些因为虚拟化而带来的性能损耗都是不存在的;而另一方面,使用 Namespace 作为隔离手段的容器并不需要单独的 Guest OS,这就使得容器额外的资源占用几乎可以忽略不计。所以说,“敏捷”和“高性能”是容器相较于虚拟机最大的优势,但是万事都有两面,有利就有弊,对于 Docker 这种基于 Linux Namespace 的隔离机制,相比于虚拟化技术最大的不足就是:隔离得不彻底

  • 1、多个容器之间使用的就还是同一个宿主机的操作系统内核。

通过 Mount Namespace 单独挂载其他不同版本的操作系统文件,比如 CentOS 或者 Ubuntu,但这并不能改变共享宿主机内核的事实。这意味着,如果你要在 Windows 宿主机上运行 Linux 容器,或者在低版本的 Linux 宿主机上运行高版本的 Linux 容器,都是行不通的。而相比之下,拥有硬件虚拟化技术和独立 Guest OS 的虚拟机就要方便得多了。最极端的例子是,Microsoft 的云计算平台 Azure,实际上就是运行在 Windows 服务器集群上的,但这并不妨碍你在它上面创建各种 Linux 虚拟机出来

  • 2、在 Linux 内核中,有很多资源和对象是不能被 Namespace 化的,最典型的例子就是:时间。

    你的容器中的程序使用 settimeofday(2) 系统调用修改了时间,整个宿主机的时间都会被随之修改,这显然不符合用户的预期。相比于在虚拟机里面可以随便折腾的自由度,在容器里部署应用的时候,“什么能做,什么不能做”,就是用户必须考虑的一个问题

正是这种隔离不彻底上的问题,使得还需要另外一种技术来保障容器的稳定性,不至于资源都被一个容器全部吃掉,或者因为某个容器的修改导致其他容器也受到影响。这里就需要提到 Cgroups。

Linux Cgroups 的全称是 Linux Control Group,它最主要的作用,就是限制一个进程组能够使用的资源上限,包括 CPU、内存、磁盘、网络带宽等等;Linux Cgroups 的设计是比较易用的,简单理解就是给每一个子系统目录加上一组资源限制文件的组合;对于 Docker 等 Linux 容器项目来说,只需要在每个子系统下面,为每个容器创建一个控制组(即创建一个新目录),然后在启动容器进程之后,把这个进程的 PID 填写到对应控制组的 tasks 文件中就可以了。如:

1
$ docker run -it --cpu-period=100000 --cpu-quota=20000 ubuntu /bin/bash

Docker 中的三个角色

镜像

Docker 镜像是一个特殊的文件系统,除了提供容器运行时所需的程序、库、资源、配置等文件外,还包含了一些为运行时准备的一些配置参数(如匿名卷、环境变量、用户等)。镜像不包含任何动态数据,其内容在构建之后也不会被改变。

由于镜像会包括操作系统完整的 root 文件系统,所以我们一般看到的镜像都是比较大的。Docker 在设计时,其充分利用了 Union FS 的技术,将镜像设计为分层存储的架构模式。所以严格来说,镜像并非是像一个 ISO 那样的打包文件,镜像只是一个虚拟的概念,其实际体现并非由一个文件组成,而是由一组文件系统组成,或者说,由多层文件系统联合组成。

容器

容器的实质是进程,但与直接在宿主执行的进程不同,容器进程运行于属于自己的独立的 namespace 。因此容器可以拥有自己的 root 文件系统、自己的网络配置、自己的进程空间,甚至自己的用户 ID 空间。容器内的进程是运行在一个隔离的环境里,使用起来,就好像是在一个独立于宿主的系统下操作一样。

容器也是分层存储,每一个容器运行时,是以镜像为基础层,在其上创建一个当前容器的存储层,我们可以称这个为容器运行时读写而准备的存储层为 容器存储层。
容器存储层的生存周期和容器一样,容器消亡时,容器存储层也随之消亡,所以按照 Docker 最佳实践的要求,容器不应该向其存储层内写入任何数据,容器存储层要保持无状态化。所有的文件写入操作,都应该使用 数据卷(Volume)、或者 绑定宿主目录,在这些位置的读写会跳过容器存储层,直接对宿主(或网络存储)发生读写,其性能和稳定性更高

仓库

集中的存储、分发镜像的服务:Docker Registry。

参考链接

作者

卫恒

发布于

2020-12-24

更新于

2022-04-21

许可协议

评论