精子生于 1995 年,英文 ID jysperm.
Docker 与容器化技术实践
这篇文章由我 7 月末在 Connext 2016 进行的一次技术分享整理而来。
RP 主机
我在高一的时候开始尝试搭建自己的网站,当时市面上的「虚拟主机」基本上只提供 PHP 环境,限制也比较多。于是我在 Linode 以每月 20 美元的价格买了一台 Linux VPS 用来搭建网站,但当时我的零花钱无法负担这个开销,于是尝试性地公开出售服务器资源,为此我编写了一套叫 RootPanel 的虚拟主机管理系统。
和其他虚拟主机不同的是,RP 主机的用户可以有非常大的权限 —— 可以登录 SSH, 运行 Node.js、Python 之类的程序;而 RootPanel 则通过 Web 的界面允许用户使用 MySQL、MongoDB 数据库,并且通过 Nginx 共享 80 端口,RootPanel 会检查用户的请求是否符合权限要求,然后去与 Nginx 这些系统服务交互。
当时 Docker 还没有出现,我用了一些比较「传统」的方式来隔离 RP 主机上用户的权限:
- 文件系统:Unix users(文件权限)、quota-tools(磁盘空间)
- CPU 和内存:自行编写脚本来调整进程优先级(CPU 超限时)和杀进程(内存超限时)
- 进程和网络:因为 Unix 本身的权限,无权向其他进程发送信号
在运行 Web 服务时,后端程序(例如 PHP-FPM、Python 的 uwsgi、Node.js 应用进程)本身由用户运行,以 Unix Socket 的方式提供服务(Unix Socket 会遵守 Unix 的文件权限机制),然后可以在 RootPanel 的 Web 界面上配置从域名到 Unix Socket 的映射(RootPanel 会检查你配置的 Unix Socket 是否在你的 home 目录中等),由 Nginx 完成反向代理,实现共享 80 端口。
Redis 和 Memache 这种轻量级数据库也是由用户自行运行的,通过 Unix Socket 提供服务来避免被其他用户访问到。出于性能考虑,所有用户会共同使用同一个 MySQL 和 MongoDB,用户可以在 RootPanel 的 Web 上创建和管理数据库,RootPanel 会为每个用户分配一个用户名和密码,使用这些数据库本身的用户机制进行权限控制。
当然 RP 主机现在已经被关掉了,详见 RP 主机和 GreenShadow 关闭计划。
「隔离」和「资源控制」
到后来 2014 年初的时候我发现了 Docker, 它是一个基于 Linux 的轻量级虚拟化技术,可以以非常低的成本来创建与主机隔离的、可以独立进行资源控制的「容器」。
在前面 RP 主机的例子中,我们虽然一定程度地解决了这两个问题,但并不完美。隔离方面 RP 主机只做到了权限的隔离,但用户依然可以看到其他用户和它们的进程、网络链接;资源控制方面,CPU 和内存都依赖于脚本进行控制,控制的粒度和准确性显然不如利用内核本身的特性。
Docker 使用 Linux 2.6 提供的 namespaces 特性来隔离容器之间的文件系统(mount namespace)、主机名(UTS namespace)、进程(PID & IPC namespace)、网络(network namespace)、用户(user namespace)。使容器中的进程只能看到与自己有关的系统资源,完全感觉不到主机上其他的容器的存在。
Docker 还使用了 Linux 2.6 提供的 cgroups 特性来统计和限制容器的系统资源,包括 CPU(cpuset & cpu & cpuacct cgroup)、内存(memory cgroup)、磁盘 IO(blkio cgroup)等。在资源控制方面,因为是由内核执行的,因此可以进行非常细粒度的控制,例如在 CPU 上,既可以为容器设置权重,也可以直接设置最大使用率。
联合文件系统
在解决了隔离和资源控制之后,我们可以允许容器自由地修改容器内的文件系统,每个容器可以使用不同的发行版、运行不同版本的系统服务。但为了允许容器去自定义它们的文件系统,我们必须要为每个容器挂载一个单独的根目录,这样将会占用大量的磁盘空间。
为了解决这个问题,Docker 基于「联合文件系统(AUFS、OverlayFS)」实现了一个「镜像」的功能。联合文件系统是一种可以将不同的目录,以分层「叠加」的方式挂载为一个文件系统的文件系统。Docker 会将不同容器间共同的部分作为一个共用的只读层(例如发行版就是一个层),然后为每个容器再叠加一个可写的层,容器对文件系统的修改会写入到这个可写的层,而不是共享的层,在容器运行结束后,这个可写的层也可以固化为一个只读的层,被其他容器复用(这就是 docker build 的过程)。
将应用封装为镜像
Docker 的容器实际上都是从 Docker 镜像创建出来的,可以说镜像是容器的模板,这个概念类似于进程是由可执行文件创建出来的,但镜像不仅仅包含可执行文件,而是包含了一个程序允许所需要的所有环境。
我们可以通过 Dockerfile 来创建镜像,Dockerfile 中包含了若干指令,这些指令会在容器中被执行,而这些指令对文件系统的修改,会作为构建出的镜像中的一个「层」。
Docker 镜像让应用的「交付」变得简单了,在理想的情况下,Dockerfile 中包含了构建应用所需要的运行环境的指令,而镜像则是一次构建的结果,Docker 为镜像提供了二进制级别的兼容性,镜像可以被传输到其他 Linux 主机上直接运行,交付一个应用就像发送一个可执行文件那么简单。基于我们前面提到的联合文件系统,Docker 镜像在传输时只会传输新的层,如果不同的镜像基于同一个基础的镜像(层)来构建,那么并不会产生额外的传输和存储开销。
无状态的容器
在一个服务器端系统中,包含了大量业务逻辑、需要频繁修改的「应用进程」是最不稳定的部分,它可能会出错、会崩溃重启、会占用大量的计算资源,因此我们必须要能够快速地对包含应用的容器进行调整。为了做到这一点,一个得到了广泛认同的实践就是将应用实现为「无状态」的,即不在内存中持久性地保存数据,而是将这些状态存储到专门的数据库(Redis 等)中,这些数据库会有自己的分布式解决方案,而不必我们操心。
这样我们便可以随时停止和启动一个应用容器,而不必担心数据丢失或状态不同步,同时无状态的应用对容器数量也毫不关心,我们可以根据业务的负载情况随时调整容器数量进而增加业务的负载能力,应用也不关心这些进程运行在哪台服务器上(Docker 的镜像为容器提供了一致的运行环境),只要前端的负载均衡(Nginx)可以发现它即可,因此我们还需要一种「服务发现」的机制让负载均衡服务能够感知到新容器的加入和已有容器的退出。
分布式的容器调度
在一个公有云的场景中,我们往往需要管理运行在几十台服务器上的几千个容器,物理设备总是可能出现故障的,随着集群规模的增长,出现故障的频率将会越来越高,我们必须能够自动地发现和恢复这些故障,我们将这种程序称为「集群管理器」,它需要关注的问题包括:
- 容器崩溃:应用进程因错误或内存超限退出,可以简单通过设置 Docker 的重启策略来解决。
- 容器僵死:负载均衡器应该能够将应用容器无响应的情况通知给集群管理器来重启容器。
- 服务器崩溃或失联:集群管理需要将崩溃的服务器上的容器移动到其他的服务器并从负载均衡中移除。
在一些计划中的维护任务也需要保证服务不中断:
- 部署新版本:应逐个启动新版本的容器并加入负载均衡,确认新容器工作正常后再将旧容器从负载均衡中移除并停止。
- 调度到资源充足的服务器:集群管理器应该能够感知到各个服务器的负载情况,将负载较高的服务器的容器移动到负载较低的服务器。
集群管理器需要决定将容器部署到哪台服务器,需要考虑的因素包括:
- 服务器实际负载。
- 有时容器会声明自己需要多少资源,虽然实际并没有占用这么多,但一段时间之后可能会有变化。
- 将容器分散到不同的服务器以应对单个服务器失效。
集群管理器还应该能够应对自身或所依赖的服务失效的情况,通过反复地重试保证实际运行的容器与计划中的一致。
在集群管理器方面社区已经有了很多成熟的解决方案,例如 Docker Swarm、Kubernetes、Marathon,作为私有云来说都基本够用,但在公有云的场景下经常还是需要自己开发一部分功能来和现有系统(例如计费)整合的。
小结
因为随着需要处理的数据量越来越大,我们必须将系统设计成分布式的,这需要我们将计算资源进行一个抽象,不去考虑有关运行环境的细节问题,所以虚拟化是一个大的趋势。基于 Docker 的容器是虚拟化解决方案中的一种,也是目前受到了非常多关注的一种,这大概是因为内核级别的虚拟化有着非常好的性能,同时 Docker 作为一个开源的产品也有着非常活跃的社区。我今天主要介绍的是我在部署环节对 Docker 的实践,但 Docker 的应用并不止于此,在开发和测试环节同样有 Docker 的身影。
参考链接
- http://rpvhost.net/
- https://jysperm.me/2013/05/810/
- https://github.com/ekenberg/quotatool
- https://wiki.archlinux.org/index.php/users_and_groups
- https://www.linux.com/learn/understanding-linux-file-permissions
- http://www.infoq.com/cn/articles/features-and-design-concept-of-distributed-system
- http://dockone.io/article/1138