总体架构
Docker 采用的是 C/S 架构,使用 REST API、UNIX 套接字或网络接口进行通信。一般客户端会和 Docker 服务运行在同一台机子上,像我们平常使用的 docker build、pull、run 等命令就是发送到本地客户端上的,本地客户端再发送给 Docker 服务端。另外,客户端也可以独立部署,像 Docker Compose。
Docker 服务一般是以守护进程的形式运行,它会监听客户端的请求,并且进行容器的构建、运行和分发,下面即 Docker 的总体架构:
Docker 守护进程
:侦听 Docker API 请求并管理 Docker 对象,例如镜像、容器、网络和卷。守护进程还可以与其他守护进程通信以管理 Docker 服务。Docker 客户端
:通过 Docker API 发送命令给 Docker 守护进程(dockerd
),让守护进程执行对应的命令动作,例如发送 docker run 命令。Docker Registry
:存储了 Docker 镜像。像 Docker Hub 就是一个任何人都可以使用的公共注册中心,Docker 会默认地从 Docker Hub 上查找镜像。当然,我们也可以自己搭建个 Docker Registry。
容器的演变
一开始,Docker 是基于 Linux 内核提供的技术进行容器管理的,它将 Linux 复杂的容器管理进行了简化,形成了自己独有的一套命令体系。后来,Docker 将底层技术进行了抽象,定义了一组接口,只要实现了这组接口,那么就可以进行容器的管理,这就是 Libcontainer
。
随着 Docker 的火热,越来越多的公司加入容器技术的开发,在 2015 年谷歌、微软、Docker 等公司成立了 OCI 组织,致力于定制一致的容器标准。在 Libcontainer 的基础上推出了容器引擎: runC
。
可能大家会比较好奇的是 windows 的容器架构又是怎么样的?其实在 windows 上也抽象出来了 CGroup
和 Namespace
,它也是符合 OCI 容器标准的,如下图:
(图片来自 Black Belt 在 DockerCon 的演讲:Docker 与 Windows 容器揭秘)
底层技术
Docker 是用 Go 语言编写的,所以天生就支持这种跨平台的部署。不过主流的服务器都是 Linux 系统,所以我们来看看关于 Linux 的容器底层技术:Namespaces
(资源隔离)、CGroups
(资源限制)、UnionFS
(镜像和容器分层)。
Namespaces(资源隔离)
Namespaces
是 Linux 内核在 2.4.19 版本后陆续引入的概念,它将系统的全局资源通过抽象划分,使得在同一 namespace 中的进程看起来拥有自己的全局资源。当前 Linux 支持以下六种 Namespace
namespace | 隔离的系统资源 |
---|---|
Mount namespaces | 文件系统挂接点 |
IPC namespaces | 特定的进程间通信资源 |
UTS namespaces | nodename 和 domainname |
PID namespaces | 进程 ID |
Network namespaces | 网络相关的系统资源 |
User namespaces | 用户和组 ID 空间 |
我们可以看到有 Network 网络的,也有用户 User 的 隔离。当容器被创建时,会创建上面对应的 Namespace 实例,然后将容器进程划分到此 Namespace 里,以此实现了隔离功能。
CGroups(资源限制)
上面的 Namespace 为我们提供了环境隔离的功能,但这还远远不够,因为各个进程所使用的资源还是没有限制的,比如 CPU、内存等。一旦某个容器超过上限,则有可能会被 kill。因此,对资源的限制使用就很重要了,而 Linux 内核的 CGroups
就提供了此功能。
当我们创建了一个容器时,默认的会在 /sys/fs/cgroup
目录下生成对应的资源使用目录,比如 docker run nginx:test
,则会在 /sys/fs/cgroup/memory/docker/nginx容器ID
目录下有对应的资源描述文件:
使用命令 docker run --memory 1024M nginx:test
时,就可以进行内存资源的限制了。其他资源限制命令也类似。
UnionFS(镜像和容器分层)
Linux 的 UnionFS
(联合文件系统) 技术是用来将不同物理位置的目录合并挂载到同一个目录中。实际上 UnionFS 在不同的系统上有不同的实现,现在主流的是 AUFS、Devicemapper 和 OverlayFS。在 Docker 中最常用的是 AUFS,我们主要来看看 AUFS 的相关知识。
首先,默认情况下 AUFS 有个特点,就是要联合的第一个文件是可读可写的,后面的文件目录则只能只读。例如,我们将 teacher、student 目录联合到 mnt 目录下:
# 将 teacher 和 student 联合到 mnt
sudo mount -t aufs -o dirs=./teacher:./student none ./mnt
├── teacher
│ ├── A
│ └── C
└── student
├── B
└── C
# 查看./mnt
$ tree ./mnt
├── A
├── B
└── C
当我们对 mnt 下的 C 目录修改后,会在 teacher 目录下同步看到修改,但 student 目录就不会被修改了,因为它是只读的。那这样的机制在 Docker 里有什么作用呢?
首先,Docker 将文件系统分为容器层和镜像层,这里的容器层相当于上面的 teacher 目录,镜像层相当于 student 目录。也就是容器层文件是可读可写,而镜像层是只读的。这样的话,有利于多个容器共享一个镜像文件。
而且 Docker 在一开始的时候并不会创建容器层,而是先使用镜像层文件,只有当容器里的文件发生了修改,此时才会真正的创建出可读可写的容器层,以保证不影响镜像层文件。而这种类似写时复制
技术,为系统节省了很多不必要的存储文件。
Docker 的安全
在审查 Docker 的安全性时,主要从下面四个方面考虑:
(一)Namespaces、CGroups 的安全
Docker 容器与 LXC 容器非常相似,它们具有一样的安全特性。Namespaces 提供了第一种也是最直接的隔离形式,使得在容器内运行的进程无法看到在另一个容器或主机系统中运行的进程。每个容器也有属于自己的网络堆栈,这意味着一个容器不能获得对另一个容器的套接字或接口的特权访问。
CGroups 是 Linux 容器的另一个关键组件,能对资源进行核算和限制,提供了许多有效指标,确保每个容器获得公平的资源使用(例如内存、CPU、磁盘 I/O),使得单个容器无法耗尽系统资源。这在多租户平台(例如 PaaS)上尤为重要,能保证用户一致的正常运行性能。
(二)Docker 守护进程的安全性
运行 Docker 守护进程是需要 root 特权的,因此只有受信任的 User 才能运行 Docker 守护进程。但是由于 Docker 是允许主机和容器共享文件夹的,如果我们将系统文件映射到 Docker 容器里,那肯定也是能突破系统防护的。不过,这主要取决于我们关联的主机文件,一般还比较好控制。
Docker 也需要防止某些非法请求创建了破坏性的容器。在 0.5.2 之后为了防止一些恶意用户的跨站脚本攻击,Docker 使用了本地的 UNIX 套接字而不是绑定在 127.0.0.1 上的 TCP 套接字,这样就允许用户进行本地权限检查,以进行安全访问了。
(三)Linux 内核的安全
默认情况下,Docker 启动的是一组功能受限的容器,这使得容器中的“root”比真正的“root”拥有更少的特权,例如:
- 禁止任何挂载操作;
- 禁止访问本地套接字(以防止数据包欺骗);
- 禁止某些文件系统的操作,例更改文件所有者或属性;
- 禁止模块加载;
这使得入侵者设法升级到容器内的 root,也很难以对主机造成严重性的破坏。
(四)其他内核安全特性
- 允许配置只能拉取指定秘钥签名的镜像仓库
- 使用 GRSEC 和 PAX 运行内核,在编译和运行时增加许多安全检查
- 使用具备安全特性的容器模板
- 自定义访问控制策略