<audio id="audio" title="26 | 容器化：分布式数据库要不要上云，你想好了吗？" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/1a/a3/1a50ff278ea67c7cd645cf72e39f2da3.mp3"></audio>

你好，我是王磊，你也可以叫我Ivan。

今天，我想和你分享的话题是分布式数据库的容器化部署。当数据库遇到容器，我知道这一定是个很有争议的话题。但是，在容器化技术大规模落地的背景下，这也是一个无法回避的话题。

容器化技术可以将资源虚拟化，从而更灵活快速地调配。容器镜像为应用打包提供了完美的解决方案，也为DevOps理念的落地扫清了技术障碍。可以说，容器已经成为现代软件工程化的基础设施，容器化已经成为一个不可逆的发展趋势。

但是，具体谈到数据库的容器化，我们又有太多的纠结。常见的反对意见就是数据库因为有状态、高I/O消耗和稳定运行等要求，所以不适合容器化部署。

那么，随着技术的快速发展，这些理由是不是还成立呢？为了说清楚这个问题，我们先来介绍一些Kubernetes的基本概念。

## Kubernetes基本概念

<li>
<h3>Container</h3>
</li>

容器化就是将物理机划分为若干容器（Container），应用程序是直接部署在容器上的，并不会感知到物理机的存在。具体来说，这个容器就是Docker，它使用的主要技术包括Cgroup和Namespace。

Cgroup是控制组群（Control Groups）的缩写，用来限制一个进程组能够使用的资源上限，包括CPU、内存、磁盘、网络带宽等，本质上实现了资源的隔离。Namespace修改了进程视图，使当前容器处于一个独立的进程空间，无法看到宿主机上的其他进程，本质上实现了权限上的隔离。这两项其实都是Linux平台上的成熟技术，甚至在Docker出现前已经被用在Cloud Foundary的PaaS平台上。

Docker能够快速崛起的重要原因是，它通过容器镜像可以将文件系统与应用程序一起打包。这个文件系统是指操作系统所包含的文件、配置和目录。这样，就不会出现各种环境参数差异导致的错误，应用的部署变得非常容易，完美解决了应用打包的问题。

容器的本质是进程，但复杂的应用系统往往是一个进程组。如果为每个进程建立一个容器，那么容器间就有非常密切的交互关系，这样管理起来就更加复杂。

那么，有什么简化的办法吗？

Kubernetes给出的答案就是Pod。

<li>
<h3>Pod</h3>
</li>

在 Kubernetes 的设计中，最基本的管理单位是 Pod，而不是容器。

Pod是Kubernetes在容器之上的一层封装，它由运行在同一主机上的一个或者多个容器构成。而且，同一个Pod中的容器可以共享一个Network Namespace和同一组数据卷，从而高效的信息交换。因为具有了这些特性，Pod通常会被类比为物理机。

Pod存在的意义还在于可以屏蔽容器之间可能存在依赖关系，这样做到Pod在拓扑关系上的完全对等，调度起来更加简单。

那么Pod又是如何被管理的呢？这就要说到Kubernetes的整体架构。

<li>
<h3>整体架构</h3>
</li>

总体上，Kubernetes是一个主从结构（Master-Slave）。其中Master是一个控制面板，每个Salve就是物理节点上代理Kubelet，Master与Kubelet通讯完成对物理节点上Pod和Container的管理。外部网络可以直接通过节点上的Proxy，调用容器中的服务，不需要经过Master。

<img src="https://static001.geekbang.org/resource/image/4b/72/4b0b3a0f84c1005c6209e0b419571d72.jpg" alt="">

Master内部又包括API Server、Scheduler、Controller和ETCD四个部分，它们的职责分工和这一讲的内容没有直接关系，为了降低学习难度，这里就不做具体介绍了。如果你想再深入了解Kubernetes相关知识，可以在张磊老师的专栏找到更多资料。

## 有状态服务（StatefulSet）

对于无状态服务的管理，用户可以定义好无状态服务的副本数量，Kubernetes调度器就会在不同节点上启动多个 Pod，实现负载均衡和故障转移。多个副本对应的 Pod 是无差别的，所以在节点出现故障时，可以直接在新节点上启动新的Pod替换已经失效的Pod。整个过程不涉及状态迁移问题，管理起来很简单。

但是，作为应用系统的基石，数据库和存储系统都是有状态服务。如果将它们排除在外，那容器化的价值将大打折扣。所以，Kubernetes 在V1.3 版本开始推出PetSet用于管理有状态服务，并在V1.5版本更名为StatefulSet。

通常，我们说数据库是一个有状态的服务，是指服务要依赖持久化数据，也就是存储状态。而对于StatefulSet来说，状态又分为两部分，除了存储状态，还有系统的拓扑状态。

### 拓扑状态

拓扑状态是指集群内节点之间的关系，在Kubernetes中就是Pod之间的关系。

对于应用服务器来说，因为它们互相之间是不感知、不依赖的，所以可以随意创建和销毁。但数据库就不一样了，即使是单体数据库，也会有主从复制的关系，从节点是依赖于主节点的。

而分布式数据库的节点角色更加复杂。除了CockroachDB这样的P2P架构，多数分布式数据库中的节点拓扑关系是不对等的，会明确的划分为计算节点、数据节点、元数据和全局时钟等。

拓扑状态管理的第一步是标识每个Pod。我们已经知道，Pod随时会被销毁、重建，而重建之后IP会发生变化，这将导致无法确定每个Pod的角色。StatefulSet采用记录域名的方式，再通过DNS解析出Pod的IP地址。这样Pod就具有了稳定的网络标识。

同时，为了确保体现Pod之间的依赖关系，StatefulSet还引入顺序的概念，将Pod的拓扑状态，也就是启动顺序，按照 Pod 的“名字 + 编号”的方式固定了下来。

### 存储状态

数据库的核心功能是存储，我们再来看看如何实现存储状态的管理。

早期版本中，提供了两种存储方式，分别是本地临时存储和远程存储。顾名思义，本地临时存储中的数据会随着Pod销毁而被清空，所以无法用做数据库的底层持久存储。

远程存储可以保证数据持久化，所以是一种可选方案。具体来说，就是使用持久化卷（Persistent Volume）作为数据的存储载体。当Pod进行迁移时，对应的PV也会重新挂载，而 PV 的底层实现是分布式文件系统，所以新的Pod仍然能访问之前保存的数据。

<img src="https://static001.geekbang.org/resource/image/2d/d1/2d729737d75e0419e9ec8accc27a6cd1.jpg" alt="">

远程存储虽然可以使用，但是它与分布式数据库的架构设计理念存在很大冲突。我们在课程中介绍过，分布式数据库都是采用本地磁盘存储的模式，并且对存储层做了很多针对性的优化。如果改为远程存储，这些设计将会失效，导致性能上有较大的损失。

事实上，这就是很多人反对数据库容器化部署的最主要的原因，数据密集型应用对I/O资源的消耗巨大，远程存储无法适应这个场景。

针对这个不足，Kubernetes尝试引入本地持久卷（Local Persistent Volume）来解决。简单来说，就是一个Local PV 对应一块本地磁盘。开启Local PV特性后，调度器会先获取所有节点与Local PV对应磁盘的关系，基于这些信息来调度Pod。

<img src="https://static001.geekbang.org/resource/image/21/66/215a6d5cc03f95e7dcaf499e3fc03266.jpg" alt="">

与普通PV不同，当挂载Local PV的节点宕机并无法恢复时，数据可能就会丢失。这和物理机部署方案面临的问题是一样的，而分布式数据库的多副本机制正好弥补了这一点。

不过Local PV推出得较晚，直到V1.10（2018年发布）Kubernetes才相对稳定地支持调度功能，在2019年3月的V1.14中正式发布。

通过对拓扑状态和存储状态的管理，StatefulSet初步解决了有状态服务的管理问题，但其中的一些关键特性（比如Local PV）还不够成熟。另外，由于Kubernetes的抽象程度比较高，所以在真正实现分布式数据库的部署还需要很多复杂的控制。

## Operator

控制逻辑的复杂性是与具体软件产品相关的，比如一个常见的问题就是对于Pod状态的判断。

Kubernetes 判断节点故障依据是每个节点上的 Kubelet 服务上报的节点状态。但是，Kubelet作为一个独立的进程，从理论上说，它能否正常工作与用户应用并没有必然联系。那么就有可能出现，Kubelet无法正常启动，但对应节点上的容器还可以正常运行。

这时，如果不处置原来的Pod，立即在其他节点创建新的Pod，应用系统可能就会出现异常。所以，调度过程还需要结合应用本身的状态来进行，其中的复杂性无法被统一封装，于是也就催生了一系列的Operator。

Operator 的工作原理就是利用了 Kubernetes 的自定义资源，来描述用户期望的部署状态；然后在自定义控制器里，根据自定义 API 对象的变化，来完成具体的部署和运维工作。简单来说，Operator 就是将运维人员对软件操作的知识代码化。

etcd-operator是最早出现的Operator，用于实现etcd的容器部署。Operator封装了有状态服务容器化的操作复杂性，对于应用系统上云的有重要的意义，有的分布式数据库厂商也开发了自己的Operator，比如TiDB和CockroachDB。

## 小结

那么，今天的课程就到这里了，让我们梳理一下这一讲的要点。

1. 容器可以更加精细的控制资源，并通过镜像功能实现应用打包部署，使开发、测试、生产等各个环境完美统一，因此得到了越来越广泛的使用。
1. 容器是单进程模型，为了适应更复杂的应用，Kubernetes又引入了Pod概念，一个Pod包含了同一节点上的多个容器。Kubernetes通过调度管理Pod将复杂的应用系统快速容器化部署，这种技术称为“容器编排”。因为容器的本质是进程，所以Kubernetes也被成为下一代操作系统。
1. Kubernetes早期以支持无状态服务为主，Pod完全对等，调度过程简单。为了适应更复杂的应用部署模式，Kubernetes也在不断完善对有状态服务的支持，使用StatefulSet对象封装了相应的功能，包括拓扑状态管理和存储状态管理。在1.14版本中发布了本地持久化卷特性，增强了存储状态管理能力。
1. 对于分布式系统来说，有状态服务的管理非常复杂，通过简单的Kubelet无法把握各个服务的真实状态，所以就有了Operator这个扩展方式，每个产品厂商可以拓展自定义控制器，进行更有针对性的管理。目前，Operator的接受程度在不断提高，TiDB、CockroachDB都开发了自己的Operator。

今天，容器化对于分布式数据库来说并不是一个默认选项。少数产品如TiDB和CockroachDB开始提供容器化部署方案，这和他们正在开展的云服务商业模式有比较直接的关系。而更多的分布式数据库仍然使用物理机，例如OceanBase和GoldenDB等。没有厂商的支持，依靠用户自身的力量实现容器化，还有比较大的困难。

我认为在未来的一两年里，“数据库是否要容器化部署”仍然是一个有争议的话题。但数据库的容器化趋势应该是确定的，因为Kubernetes提供支持越来越完善，真正大规模落地只是时间早晚的问题。而随着分布式数据库普及，数据库的节点规模必然会快速扩大，运维成本随之提升，这也给数据库容器化带来更强的推动力。

如果你想更积极地尝试数据库容器化，可以把大数据应用作为一个风向标。这是因为大数据应用的可用性要求低于OLTP场景，而集群规模更大，上百甚至上千节点都是正常规模。这意味着，大数据应用的容器化改造要承担的风险更小，但收益更大。

<img src="https://static001.geekbang.org/resource/image/21/b4/21bf3396750b548a66bdbf7f7a18efb4.jpg" alt="">

## 思考题

课程的最后部分是我们今天的思考题。今天的课程中，我们简要介绍了Kubernetes对有状态服务的支持情况，从中不难发现，资源调度是它非常核心的功能。我的问题是，除了Kubernetes你还知道哪些集群资源调度系统？它们在设计上有什么差异吗？

欢迎你在评论区留言和我一起讨论，我会在答疑篇和你继续讨论这个问题。如果你身边的朋友也对数据库的容器化部署这个话题感兴趣，你也可以把今天这一讲分享给他，我们一起讨论。
