Docker 技术概述
概要
本文从一个概览的角度,介绍下 Docker 技术的核心优势、技术原理和相关的组件。
主要从以下几个方面来了解:
- 为什么用 Docker?
- 了解 Docker 能干什么
- 了解 Docker 和其他方便比有什么优势
- Docker 的技术原理
- Docker 本身的架构设计
- Docker 底层所依赖的关键技术
- Docker 集群的简单介绍
为什么使用 Docker
让我们带着几个核心问题出发。问题从动机开始,而不是从“是什么”来开始:
- Docker 主要解决了什么问题?
- 在有 Docker 之前有其他的解决方案吗?
- Docker 有什么优势?
解决了什么问题
本质:快速的分发部署应用
在云时代,应用需要方便地在网上传播,就是说需要脱离硬件资源的限制,同时能够容易地获取。
设想我们有一个简单的 Web 服务器应用,它基于 Ngnix、Java、MySQL 来开发实现。现在用户需要把它部署到自己的机器上,那么用户需要安装所需要的各种软件和依赖,并对它们进行配置,然后进行一系列的测试验证确认它们是否能够协同工作。这些步骤中很可能因为本地环境的不同遇到各种奇怪的问题,应用的传播十分复杂。
Docker 使用了一种很聪明的方式,通过镜像来把整个应用打包,将应用和运行平台解耦,再集中地管理和分发镜像。
Docker 的官网有一份简述 Why Docker,总的来说就是:
- 目前的软件开发需要使用各种各样的语言、架构、框架和工具,带来了很大的复杂性,Docker 的目标就是简化和加速这个工作流。主要有以下几个方面:
- 让构建、分享、运行变得简单
- 提高开发运维的整个流程的效率,保证运行的一致性
- 团队合作更加高效
Docker 与虚拟机对比
Docker 之前怎么解决前面的问题?
Docker 是一种容器引擎,容器是一种轻量级的虚拟化技术,在这之前主要的虚拟化技术是虚拟机。
容器和虚拟机有什么区别?
- 容器:容器和宿主系统共用同一个内核空间,利用了内核提供的资源隔离和资源限制的能力,直接使用宿主机的资源。
- 虚拟机:通过 Hypervisor 层来模拟硬件,在它上面安装完整的操作系统,应用再运行在虚拟的 Guest OS 中。
它们的特性对比如下:
特性 | 容器 | 虚拟机 |
---|---|---|
启动时间 | 秒级 | 分钟级 |
镜像存储 | KB~MB | GB~TB |
系统资源 | 0~5% | 5~15% |
系统支持量 | 单机支持上千个容器 | 一般几十个 |
总的来讲,Docker 的主要优点有:
- 轻量:秒级的启动和停止。
- 更高效的虚拟化:占用更少的系统资源。
- 扩展性:运行在各种平台上,本地、物理机、云端,能够很轻松的迁移。
- 更简单的管理和分发:镜像的大小比较小,并且通过增量的方式更新和分发,更加高效。
Docker 技术原理
Docker 架构
Docker 使用了 C/S 架构。Client 通过命令和 deamon 交互,deamon 负责具体的镜像拉取、容器构建运行等。Client 和 deamon 可以在同一个系统中,也可以将 deamon 部署在远端。
架构主要分为三个部分:
- Docker deamon: Docker 服务端进程,又叫
dockerd
。它监听端口的命令,提供 REST API 来管理镜像、容器、网络、volume 等,同时还可以和其他的 docker demon 交互来管理 docker 服务。 - Docker client: Docker 客户端,提供命令来让用户和 deamon 交互。客服端发送用户输入的命令到服务端,deamon 执行这些命令。
- Docker registry: 集中存储、分发 Docker 镜像的服务,Docker deamon 从指定 registry 上拉取镜像。
基本概念
Docker 有三个基本概念:
- Image(镜像):
- 镜像就是一个只读的模板,用来创建容器。它是一个特殊的文件系统,包含了程序运行所需要的库、资源、配置文件、环境变量等。它是静态不可变的,不包含动态产生的任何数据。
- Container(容器):
- 容器就是镜像的一个运行时实例,它以镜像为基础,在上面创建一层容器的储存层,是运行在独立命名空间中的一个进程。它可以被创建、启动、关闭、重启等。
- Repository(仓库):
- Docker Registry 是提供镜像的集中式的存储、分发的服务,它可以包含多个 Repository (仓库),每个仓库有可以有多个 tag (标签),其中每个标签就对应了一个镜像。
总结来说就是,镜像是静态的不可变的,而容器是镜像的运行实例,它们有点像面向对象编程中的类和对象的关系,而仓库就是存储和分发镜像的地方。
底层原理
使用容器的一个核心目的就是,每次容器的运行能够独立并且稳定。即在宿主机上启动多次,或者在不同的宿主机上迁移,运行的效果是一样的。
那怎么样才能让每次的运行的效果是一样的?可以从两个方面来考虑:
- 部署一致性:
- 应用的打包要包含应用的完整依赖,包括文件、配置、依赖库等,即镜像中需要有应用完整的依赖。
- 执行一致性:
- 希望容器每次运行时看到相同的计算机的“视图”。不会因为宿主机上运行着不同的进程,有着不同的文件而受到影响。
- 宿主机上可能同时运行着多个容器,为了保证容器的运行稳定,同时保证宿主机的运行稳定。那么就要限制单个容器使用的资源,如 CPU、内存、网络、硬盘 IO 等,防止一个容器占用资源过多影响到宿主机,从而影响到其他容器。
Docker 主要通过以下底层技术实现了前面的两种一致性:
- 部署一致性:rootfs & Union FS
- 执行一致性:namespace & cgroups
rootfs & Union FS
容器的静态依赖是通过镜像来支持的,镜像中包含了容器所需要的完整依赖。那么一个容器通常会有什么依赖呢?
- 最直观想到的部分,应用程序二进制文件本身和它依赖的语言库、配置等。
- 容易被忽略的部分,应用运行的操作系统,是容器完整的“依赖库”。
典型的 Linux 文件系统由两部分组成:
- bootfs: 操作系统的内核
- rootfs: 操作系统包含的所有配置、文件和目录
容器镜像通过打包一个 rootfs,提供了“打包操作系统的能力”,让操作系统这个最基础的“依赖库”保持稳定,从而保证了容器的部署一致性。
那么容器只需要在 rootfs 基础上,再将应用程序、依赖的语言库、配置等增加上去,就能够保证依赖的稳定性。但是直接在 rootfs 上不停的修改有什么缺点呢?
- 缺少复用性:每次安装一些不同的程序软件,都会打包出一个新的 rootfs,这些 rootfs 之间没有关联,无法复用。(可以把它们想象成两个完全无关的 Git 仓库,即使代码相同也无法 merge)
容器镜像使用了联合文件系统(Union FS)来解决这个问题,它通过分层,将每次修改差量的部分作为新的层存储下来。
Docker 镜像是通过分层构建的,其中每一层都是只读的。当容器运行时,会在最上层提供了一个可读写入层用来写入数据。如下图所示:
namespace & cgroups
为了容器执行的一致性,需要 Linux 的以下两种技术:
- namespace: 为容器提供资源隔离能力,给应用创建了一个“边界”,让应用只能看到 namespace 内的“世界”。
- cgroups: 资源限制的能力,限制容器的边界,让容器占用的资源是可控的。
Namespace 是 Linux 创建进程时的一个可选参数,因此容器其实也只是一个特殊的进程。其中 Linux 主要支持了 6 种 namespace 来进行各种资源隔离,基本上覆盖了进程运行所需要的主要的资源:
Namespace | 系统资源 | 容器中的隔离效果 |
---|---|---|
UTS | 主机信息 | 每个容器可以有自己的主机和域名,在网络上可以被视为独立的节点,而非宿主机的一个进程 |
IPC | 进程间通信 | 同一个 IPC namespace 下的进程才彼此可见 |
PID | 进程 | 每个 PID 命名空间中进程可以有独立的 PID,如每个容器可以有它的 PID 为 1 的 root 进程 |
Mount | 文件系统挂载点 | 每个容器能看到不同的文件系统层次结构 |
Network | 网络 | 每个容器有独立的网络设备,IP 地址、IP 路由表、端口号等,如同一台宿主机上多个容器的应用都能使用 80 端口 |
User | 用户 | 容器能够拥有和宿主机独立的 user 和 group |
但是 namespace 隔离机制也有不足之处,隔离的不够彻底。比如时间就不支持 namespace 隔离,容器修改了宿主机的时间,会影响到宿主机上的其他容器。
cgroups 是 Linux 对进程提供资源限制的手段。其中有几个核心概念:
- subsystem(子系统):代表 Linux 能提供限制的一种资源,如 CPU、内存、硬盘 IO 等。
- cgroup(控制组):一组任务和子系统的关联关系,表示对任务进行怎样的资源限制。是一个中间的结构,与多个进程关联,可以理解成一个进程组,主要是为了方便管理,让一组进程能够受到某几个子系统的限制。
- hierarchy(层级树):一组 cgroup 形成的树结构,子节点会默认的继承父节点的属性。这种继承主要是为了方便使用,如在限制的基础上继续限制。
- task(任务):对应系统中的一个进程,与 cgroup 进行多对多关联。
cgroups 实现的本质是给进程挂上钩子(hook),当进程(task)运行时涉及到某个资源时, 就会触发钩子上附带的 subsystem 进行检测和限制。
Linux 主要提供了以下 subsystem:
- cpu, cpuacct: cpu 主要限制进程 cpu 使用率,cpuacct 统计 cgroups 中进程的 cpu 使用报告。
- cpuset: 指定任务能运行在哪个核上。
- blkio: 限制块设备(磁盘、SSD、USB 等)的 IO 速率
- devices: 限制 cgroup 中任务对设备的访问。
- net_cls: 为 cgroup 中任务的报文加上特殊的标记,这样工具就能针对网络进行配置。
- net_prio: 对网络接口设置优先级。
- memory: 限制 cgroup 的内存使用,并生成任务的使用情况报告。
子系统的隔离也有一些不足之处,比如内核 3.x 中,硬盘通过 Buffer IO 写入时,内核缓冲区没有进行隔离,高 IO 的进程很容易影响到其他进程。内核 4.x 中 cgroups v2 对此作了改进。
扩展问题:
- 容器本质是一个进程,那怎么样支持多进程服务?
- 可以通过启动子进程,使用同样的 namespace。那么父进程就需要是一个启动其他进程的程序或脚本,如 supervisord、runit。
- 为什么最佳实践是容器中只启动一个进程?
- 尽量的使用单进程容器,因为 Docker 就相当于一个守护进程,它能很好的管理进程,如 “docker ps” 了解进程状态等。如果使用 supervisord,相当于你自己维护了子进程的状态,容器并不知道,这无疑增加了容器的维护成本和不稳定性。
- 另外从设计哲学上,容器提供单一的功能有很多优点,参考这里。
Docker 从单机到集群
我们将单体应用拆分为细小的服务,运行在各个容器中。那么如何部署、管理、扩展这么多的容器?
容器编排就是用来管理容器集群的方式,从“容器”变成了“容器云”。
容器编排的工具,最具代表性的有:
- Docker 公司的 Compose + Swarm
- Google 主导的 Kubernetes,现在已成为容器编排的事实标准了。
简单的介绍下它们的区别:
- Docker Compose: 允许你配置和启动多个 Docker 容器,用于在同一主机上管理多个容器。
- Docker Swarm: 用于在多个主机上运行和连接 Docker 容器,并且提供一些扩展管理,如容器崩溃时启动新容器等,即一个容器集群管理和编排的工具。
- Kubernetes: 也是一个容器管理和编排的工具, Kubernetes 从更宏观的高度,用统一的方式定义任务间的关系,用 Pod 和 Service 等统一抽象,提供了更强大的编排能力。
参考资料
- Docker 官方文档:https://docs.docker.com/
- Docker — 从入门到实践:https://yeasy.gitbook.io/docker_practice/
- Docker容器实战(八) - 漫谈 Kubernetes 的本质:https://developer.aliyun.com/article/721206
- 美团容器平台架构及容器技术实践:https://tech.meituan.com/2018/11/15/docker-architecture-and-evolution-practice.html
Docker 技术概述