Docker 技术概述

概要

本文从一个概览的角度,介绍下 Docker 技术的核心优势、技术原理和相关的组件。

主要从以下几个方面来了解:

  1. 为什么用 Docker?
    1. 了解 Docker 能干什么
    2. 了解 Docker 和其他方便比有什么优势
  2. Docker 的技术原理
    1. Docker 本身的架构设计
    2. Docker 底层所依赖的关键技术
  3. Docker 集群的简单介绍

为什么使用 Docker

让我们带着几个核心问题出发。问题从动机开始,而不是从“是什么”来开始:

  1. Docker 主要解决了什么问题?
  2. 在有 Docker 之前有其他的解决方案吗?
  3. 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 Architype

架构主要分为三个部分:

  • 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 镜像是通过分层构建的,其中每一层都是只读的。当容器运行时,会在最上层提供了一个可读写入层用来写入数据。如下图所示:

Docker Union FS

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 等统一抽象,提供了更强大的编排能力。

参考资料

作者

KK

发布于

2021-04-02

更新于

2022-06-11

许可协议

评论