传统进程管理的困境
当我们登录到操作系统后,通过 ps 等命令,能看到各式各样的进程,涵盖系统自带服务与用户应用进程。这些进程具有三个显著特点:相互可见并能通信、共享同一文件系统、共用相同系统资源。
然而,这些特点却引发了诸多问题。高级权限进程可利用相互可见性攻击其他进程;同一文件系统下,进程间数据易被修改、删除,依赖冲突也给运维带来极大压力;系统资源被共享使用,应用间常出现资源抢占,影响其他应用正常运行。例如,当一个资源密集型应用运行时,可能导致其他应用因资源不足而无法提供服务。
为解决这些问题,一种全新的进程运行环境应运而生,那就是容器。
容器:进程的理想家园
隔离与独立
为解决进程使用同一文件系统的问题,Linux 和 Unix 操作系统借助 chroot 系统调用,将子目录转变为根目录,实现视图级别的隔离,使进程拥有独立文件系统,其对文件系统的操作不会影响其他进程。
同时,利用 Namespace 技术,实现进程在资源视图上的隔离,让进程彼此不可见。再通过 Cgroup 限制进程资源使用率,如设置 CPU 和内存使用量,避免进程过度消耗资源。

容器,就是这样一个具备视图隔离、资源可限制、拥有独立文件系统的进程集合。它宛如一个独立的小世界,将系统其他资源隔离开来,拥有自己独特的资源视图。
容器的文件系统
容器拥有独立文件系统,因其使用系统资源,所以在该文件系统内无需内核相关代码或工具,只需具备运行所需的二进制文件、配置文件及依赖即可运行。
镜像:容器的灵魂伴侣
什么是镜像
我们将容器运行时所需的所有文件集合,称作容器镜像。它就像是容器的蓝图,包含了容器运行所需的一切。
镜像的构建
通常,我们采用 Dockerfile 来构建镜像。Dockerfile 提供了便捷的语法糖,能清晰描述构建的每个步骤。每个构建步骤都会对文件系统进行操作,产生的变化称为 changeset。
将构建步骤产生的变化依次作用于空文件夹,就能得到完整镜像。changeset 的分层与复用特性带来诸多优势:提高分发效率,大镜像拆分后可并行下载;减少数据下载量,本地已有部分数据时,只需下载缺失部分;节约磁盘空间,以 golang 镜像基于 alpine 镜像构建为例,复用后可节省大量空间。
以下是一个构建 golang 应用的 Dockerfile 示例:
# 指定构建基于的镜像,体现镜像复用性FROM golang:1.12-alpine# 指定后续构建步骤所在目录,类似 Shell 中的 cd 命令WORKDIR /go/src/app# 将宿主机文件拷贝到容器镜像内COPY . .# 在文件系统内执行相应操作,完成应用构建与安装RUN go get -d -v./...#build the application and install itRUN go install -v./...# 指定使用镜像时的默认程序CMD ["app"]
有了 Dockerfile 后,使用 docker build 命令即可构建应用,构建结果存储在本地。
镜像的存储与使用
构建好的镜像需存储与管理,docker registry(镜像仓库)应运而生,它负责存储所有镜像数据。通过 docker push 可将本地镜像推送到镜像仓库,在生产或测试环境中,就能从仓库下载镜像并运行。
容器的运行之旅
运行容器一般分为三步:
从镜像仓库下载相应镜像。
用 docker images 查看本地镜像列表,从中选择所需镜像。
使用 docker run 运行选中镜像,得到容器,可多次运行同一镜像获得多个容器。镜像如同模板,容器则是具体运行实例,这体现了镜像一次构建、到处运行的特点。

容器的生命周期
运行时生命周期
容器由一组具有隔离特性的进程集合组成。使用 docker run 时,选择镜像提供独立文件系统并指定运行程序(initial 进程)。initial 进程启动,容器随之启动;initial 进程退出,容器也退出,二者生命周期一致。
但容器内不止 initial 进程,它还可产生子进程,或通过 docker exec 进行运维操作,这些都由 initial 进程管理。initial 进程退出时,所有子进程随之退出,防止资源泄漏。
数据持久化
应用程序往往有状态,会产生重要数据。容器退出删除后数据易丢失,为解决此问题,引入数据卷。数据卷是特殊目录,用于容器数据持久化,其生命周期独立于容器。将数据卷挂载到容器内,容器可将数据写入,容器退出数据也不会丢失。
数据卷管理主要有两种方式:
bind 方式:直接将宿主机目录挂载到容器内,简单但依赖宿主机目录,运维成本高。
目录管理交给运行引擎。
容器项目架构
moby 容器引擎架构
moby 是目前最流行的容器管理引擎,moby daemon 负责容器、镜像、网络及 Volume 的管理。其依赖的重要组件是 containerd,containerd 是容器运行时管理引擎,独立于 moby daemon,提供容器和镜像相关管理。
containerd 底层的 containerd shim 模块类似守护进程,设计原因如下:
管理容器生命周期,为不同容器运行时提供灵活插件化管理,shim 针对不同运行时开发,以插件形式管理。
实现动态接管,防止 moby daemon 或 containerd daemon 意外退出时容器无人管理而消失,影响应用运行。
支持 moby 或 containerd 原地升级,不影响业务。

容器 VS VM
差异对比
VM 利用 Hypervisor 虚拟化技术模拟 CPU、内存等硬件资源,在宿主机上建立 Guest OS,每个 Guest OS 有独立内核,如 Ubuntu、CentOS、Windows 等,应用在 Guest OS 下相互独立,隔离效果好。但需占用大量计算资源和磁盘空间,启动慢,如 Windows 安装需 10 - 30G 磁盘空间,Ubuntu 需 5 - 6G。
容器针对进程,无需 Guest OS,仅需独立文件系统提供文件集合,文件隔离为进程级别,启动快,所需磁盘空间小。不过,进程级隔离效果相对 VM 较差。

总体而言,容器和 VM 各有优劣,目前容器技术正朝着强隔离方向发展。
总结
容器作为一个独特的进程集合,拥有独立的视图视角;镜像为容器提供所需的全部文件集合,具备一次构建、到处运行的特性;容器的生命周期与 initial 进程紧密相连;与 VM 相比,容器在某些方面具有优势,同时也在不断发展完善。
随着技术的持续进步,容器技术必将在未来的数字化世界中发挥更为重要的作用,为我们带来更加高效、便捷的应用部署与运行体验。




