Cloud + TiDB 技术解读
作者:邓栓
来源:细说云计算
作为一款定位在 Cloud-native 的数据库,现如今 TiDB 在云整合上已取得了阶段性的进展。日前 Cloud TiDB 产品在 UCloud 平台正式开启公测,TiDB 弹性伸缩的特性在 Cloud 提供的基础设施支持下发挥的淋漓尽致。在感受云数据库魅力的同时,让我们来一探究竟,看一下 TiDB 与 Cloud 背后的技术秘密。
TiDB 的架构
首先还是要先从 TiDB 的架构说起,TiDB 和传统的单机关系型数据库有什么不同?相信长期以来一直关注 TiDB 的同学都比较了解了,但这里还是科普一下。TiDB 作为一个开源的分布式数据库产品,具有多副本强一致性的同时能够根据业务需求非常方便的进行弹性伸缩,并且扩缩容期间对上层业务无感知。TiDB 的主体架构包含三个模块,对应 GitHub 上面 PingCAP 组织下的三个开源项目,TiDB / TiKV / PD:
TiDB 主要是负责 SQL 的解析器和优化器,它相当于计算执行层,同时也负责客户端接入和交互;
TiKV 是一套分布式的 Key-Value 存储引擎,它承担整个数据库的存储层,数据的水平扩展和多副本高可用特性都是在这一层实现;
PD 相当于分布式数据库的大脑,一方面负责收集和维护数据在各个 TiKV 节点的分布情况,另一方面 PD 承担调度器的角色,根据数据分布状况以及各个存储节点的负载来采取合适的调度策略,维持整个系统的平衡与稳定。
上面的这三个模块,每个角色都是一个多节点组成的集群,所以最终 TiDB 的架构看起来是这样的。
由此可见,分布式系统本身的复杂性导致手工部署和运维的成本是比较高的,并且容易出错。传统的自动化部署运维工具如 Puppet / Chef / SaltStack / Ansible 等,由于缺乏状态管理,在节点出现问题时不能及时自动完成故障转移,需要运维人员人工干预。有些则需要写大量的 DSL 甚至与 Shell 脚本一起混合使用,可移植性较差,维护成本比较高。
TiDB 与 Kubernetes 的整合历程
在云时代,容器成为应用分发部署的基本单位,而谷歌基于内部使用数十年的容器编排系统 Borg 经验推出的开源容器编排系统 Kubernetes 成为当前容器编排技术的主流。作为 Cloud Native Database,TiDB 选择拥抱容器技术,并与 Kubernetes 进行深度整合,使其可以非常方便地基于 Kubernetes 完成数据库的自动化管理。
Kubernetes 项目可以说是为 Cloud 而生,利用云平台的 IaaS 层提供的 API 可以很方便的和云进行整合。这样我们要做的事情就很明确了,只要让 TiDB 与 Kubernetes 结合的更好,进而就实现了和各个云平台的整合, 使得 TiDB 在云上的快速部署和高效运维成为现实。
Kubernetes 最早是作为一个纯粹的容器编排系统而诞生的,用户部署好 Kubernetes 集群之后,直接使用其内置的各种功能部署应用服务。由于这个 PaaS 平台使用起来非常便利,吸引了很多用户,不同用户也提出了各种不同的需求,有些特性需求 Kubernetes 直接在其核心代码里面实现了,但是有些特性并不适合合并到主干分支,为满足这类需求,Kubernetes 开放出一些 API 供用户自己扩展,实现自己的需求。当前 Kubernetes 已经发展到 v1.8,其内部的 API 变得越来越开放,使其更像是一个跑在云上的操作系统。用户可以把它当作一套云的 SDK 或 Framework 来使用,而且可以很方便地开发组件来扩展满足自己的业务需求。对有状态服务的支持就是一个很有代表性的例子。
Kubernetes 项目最早期只支持无状态服务 (Stateless Service) 来管理的,无状态服务通过 ReplicationController 定义多个副本,由 Kubernetes 调度器来决定在不同节点上启动多个 Pod,实现负载均衡和故障转移。对于无状态服务,多个副本对应的 Pod 是等价的,所以在节点出现故障时,在新节点上启动一个 Pod 与失效的 Pod 是等价的,不会涉及状态迁移问题,因而管理非常简单。但是对于有状态服务 (Stateful Service),由于需要将数据持久化到磁盘,使得不同 Pod 之间不能再认为成等价,也就不能再像无状态服务那样随意进行调度迁移。Kubernetes v1.3 版本提出 PetSet 的概念用来管理有状态服务并于 v1.5 将其更名为 StatefulSet。StatefulSet 明确定义一组 Pod 中每个的身份,启动和升级都按特定顺序来操作。另外使用持久化卷存储 (PersistentVolume) 来作为存储数据的载体,当节点失效 Pod 需要迁移时,对应的 PV 也会重新挂载,而 PV 的底层依托于分布式文件系统,所以 Pod 仍然能访问到之前的数据。同时 Pod 在发生迁移时,其网络身份例如 IP 地址是会发生变化的,很多分布式系统不能接受这种情况。所以 StatefulSet 在迁移 Pod 时可以通过绑定域名的方式来保证 Pod 在集群中网络身份不发生变化。
然而现实中一些分布式系统更为复杂,StatefulSet 也显得捉襟见肘。举例来说,某些分布式系统的节点在加入集群或下线时还需要做些额外的注册和清理操作,或者滚动升级要考量版本兼容性等。基于这个原因 CoreOS 公司提出了 Operator 概念,并实现了 etcd-operator 和 prometheus-operator 来管理 Etcd 和 Prometheus 这样的复杂分布式系统。用户可以开发自己的 Operator,在 Kubernetes 之上实现自定义的 Controller,将有状态服务的领域特定的运维知识编码进去,从而实现对特定分布式系统的管理。同时 Operator 本身也是跑在 Kubernetes 中的一组 Pod(deployment),对 Kubernetes 系统并无侵入性。
TiDB 系列组件及其作用
针对 TiDB 这种复杂的分布式服务,我们开发了 tidb-operator 等一系列组件,来管理 TiDB 集群实例在 Kubernetes 平台上的创建、销毁、扩缩容、滚动升级和故障转移等运维操作。同时在上层封装一个 tidb-cloud-manager 组件,提供 RESTful 接口,实现与云平台的控制台打通。这样也就实现了一个 DBaaS (数据库即服务)架构的基本形态。
由于 TiDB 对磁盘 I/O 有比较高的要求,通过 PV 挂载网络盘性能上会有明显的性能损耗。另外 TiKV 本身维护了数据多副本,这点和分布式文件系统的多副本是有重复的。所以我们要给 Pod 上挂载本地磁盘,并且在 Kubernetes 上面把 Local PV 管理起来,作为一种特定的资源来维护。Kubernetes 长期以来官方一直没有提供 Local PV 支持,本地存储只支持 hostPath 和 emptyDir 两种方式。其中 hostPath 的生命周期是脱离 Kubernetes 管理的,使用 hostPath 的 Pod 销毁后,里面的数据是不会被自动清理,下次再挂载 Pod 就会造成脏数据。而 emptyDir 更像一个临时磁盘,在 Pod 重建时会被清理重置,不能成为持久化 PV 来使用。为此我们开发了一个 tidb-volume-manager 组件,用于管理 Kubernetes 集群中每台物理主机上的本地磁盘,并且将其暴露成一种特殊的 PV 资源。结合 Operator 在部署 TiDB 节点时会参考 Local PV 资源的情况来选择特定的节点来部署,分配一个空的 Local PV 和 Pod 绑定。而当 Pod 销毁时候会根据具体情况来决定是否结束 Local PV 的生命周期,释放掉的 Local PV 再经历一个 gc 周期后,被 tidb-volume-manager 回收,清理其盘上数据等待再次被分配使用。
将这些组件整合起来,就形成了上图描述了 Cloud TiDB 的总体架构,在 Kubenetes 管理的集群之上通过 tidb-operator 等组件来针对性的调配和使用集群资源,从而实现 TiDB 集群实例的生命周期管理。通过这种方式,来实现 TiDB 分布式数据库和云平台的整合。接下来,我们再针对 Cloud TiDB 的关键特性和实现细节分别进行解读。
自动化运维
数据库产品上云的一个先决条件是能实现自动化的运维管理,否则在云上靠手工运维几乎是不现实的。我们首先用 Kubernetes 将云平台的主机资源管理起来,组成一个大的资源池。然后再通过 tidb-opeartor 及 tidb-cloud-manager 等组件来自动化完成 TiDB 实例的一键部署、扩容缩容、在线滚动升级、自动故障转移等运维操作。
首先拿集群创建来说。前面提到过,TiDB 包含三大核心组件:TiDB / TiKV / PD,每个服务又都是一个多节点的分布式结构。服务和服务之间的启动顺序也存在依赖关系。此外,PD 节点的创建和加入集群方式和 etcd 类似,是需要先创建一个单节点的 initial 集群,后面加入的节点需要用特殊的 join 方式,启动命令上都有差别。有一些操作完成后还需要调用 API 进行通知。Kubernetes 自身提供的 StatefulSet 是很难应付这种复杂的部署,所以需要 tidb-operator 中实现特定的 Controller 来完成这样一系列的操作。并且结合 Kubernetese 强大的调度功能,合理的规划和分配整个集群资源,尽量让新部署的 TiDB 实例节点在集群中均匀分布,最终通过 LB 暴露给对应的租户使用。
在线升级也是类似。由于 TiKV / PD 的 Pod 挂载的是本地存储,并不能像云平台提供的块存储或网络文件系统那样可以随意挂载。如果 TiKV / PD 迁移到其它节点,相当于数据目录也被清空,所以必须保证 TiKV / PD 的 Pod 在升级完成后仍然能够调度在原地,这也是要由 tidb-operator 的 Controller 来保证。TiDB 的数据副本之间由 Raft 算法来保证一致性,因此当集群中某一个节点暂时断开可以不影响整个服务的。所以在集群升级的过程中,必须严格按照服务的依赖关系,再依次对 Pod 进行升级。
当节点出现故障时,同样是由于挂载本地数据盘的原因,也不能像 StatefulSet 那样直接把 Pod 迁移走。当 TiDB Operator 检测到节点失效,首先要等一定的时间确认节点不会再恢复了,开始迁移恢复的操作。首先调度选择一个新节点启动一个 Pod, 然后通知 TiDB 将失效的节点放弃掉,并将新启的 Pod 加入集群。后面会由 TiDB 的 PD 模块来完成数据副本数的恢复,以及数据往新节点上进行搬移,从而重新维持集群内数据平衡。
以上只是列举了 TiDB 几种典型的运维操作流程,实际生产上运维还有很多 case 需要考虑,这些都以程序的方式实现在 tidb-operator 里面。借助 Kubernetes 和 tidb-operator 来代替人工,高效的完成 TiDB 数据库在云平台上的复杂运维管理。
动态扩缩容
弹性水平伸缩是 TiDB 数据库最主要的特性之一。在大数据时代,人们对数据存储的需求在快速膨胀。有时候用户很难预估自己的业务规模的增长速度,如果采用传统的存储方案,可能很快发现存储容量达到了瓶颈,然后不得不停机来做迁移和完成扩容。如果使用 Cloud TiDB 的方案,这个过程就非常简单,只需要在 Cloud 控制台上修改一下 TiDB 的节点数量,很快就能完成扩容操作,期间还不会影响业务的正常服务。
那么在 Cloud 后台,同样借助 Kubernetes 和 tidb-operator 的能力来完成 TiDB 增减节点操作。Kubernetes 本身的运作是基于一种 Reconcile 的机制。简单来说当用户提交一个新的请求,比如期望集群里面跑 5 个 TiKV 节点,而目前正在跑的只有 3 个,那么 Reconcile 机制就会发现这个差异,首先由 Kubernetes 的调度器根据集群整体资源情况,并结合 TiDB 节点分配的亲和性原则和资源隔离原则来分配节点。另外很重要一点就是选择有空闲 Local PV 的机器来创建 Pod 并进行挂载。最终通过 tidb-operator 将 2 个节点加入 TiDB 集群。
对于缩容的过程也是类似。假如数据库存储的总数据量变少,需要减少节点以节省成本。首先用户通过云控制台向后端提交请求,在一个 Reconciling 周期内发现差异,tidb-operator 的 Controller 开始通知 TiDB 集群执行节点下线的操作。安全下线可能是个比较长的过程,因为期间需要由 PD 模块将下线节点的数据搬移到其他节点,期间集群都可以正常服务。当下线完成,这些 TiKV 变成 tombstone 状态。而 tidb-operator 也会通知 Kubernetes 销毁这些 Pod,并且由 tidb-volume-manager 来回收 Local PV。
资源隔离
资源隔离也是云上用户关心的一个问题。尤其是数据库这类应用,不同租户的数据库实例,甚至一个租户的多套数据库实例,都跑在一套大的 Kubernetes 管理的集群上,相互间会不会有资源的争抢问题,某个实例执行高负载的计算任务时,CPU、内存、I/O 等会不会对同台机器上部署的其他实例产生影响。其实容器本身就是资源隔离的一个解决方案,容器的底层是 Linux 内核提供的 cgroups 技术,用于限制容器内的 CPU、内存以及 IO 等资源的使用,并通过 namespace 技术实现隔离。而 Kubernetes 作为容器编排系统,能够根据集群中各个节点的资源状况,选择最优的策略来调度容器。同时 tidb-operator 会根据 TiDB 自身的特性和约束,来综合决策 TiDB 节点的调度分配。举例来说,当一个 Kubernetes 集群横跨多个可用区,用户申请创建一个 TiDB 集群,那么首先根据高可用性原则,将存储节点尽量分配到不同的可用区,并给 TiKV 打上 label。那么同一个可用区内也尽量不把多个 TiKV 部署到相同的物理节点上,以保证集群资源最大化利用。此外,每个 Local PV 也是一块独立的磁盘,每个 TiKV 的 Pod 分别挂载不同的盘,所以 I/O 上也是完全隔离的。Kubernetes 还可以配置 Pod 之间的亲和性(affinity)和反亲和性(anti-affinity),例如 TiKV 和 TiDB 之间我们可以通过亲和性使其调度到网络延时较小的节点之上,提高网络传输效率,TiKV 之间借助反亲和性,使其分散部署到不同的主机、机架和可用区上,降低因硬件或机房故障造成的丢数据的风险。
上面解释了容器层面的隔离,可以看作是物理层面的隔离。那么数据层面的隔离,TiDB 的调度体系也是有所考虑的。比如一个大的 TiDB 集群,节点分布在很多台主机,跨越多个机架、可用区。那么用户可以定义 Namespace,这是一个逻辑概念,不同业务的数据库和表放置在不同的 Namespace。再通过配置 Namespace 和 TiKV 节点以及区域的对应关系,由 PD 模块来进行调度,从而实现不同业务的数据在物理上的隔离。
高可用性
TiDB 作为一个分布式数据库本身就具有高可用性,每个核心组件都可以独立的扩缩容,任意一个模块在部署多份副本时如果有一个挂掉,整体仍然可以正常对外提供服务,这是由 Raft 协议保证的。但是如果对数据库节点的调度不加任何限制,包含一份数据的多个副本的节点可能会被调度到同一台主机。这时如果主机发生故障,就会同时失去多个副本,一个 Raft 分组内在失去多数派节点就会使整个集群处于不可用的状态。因此 tidb-operator 在调度 TiKV 节点时需要避免出现这种情况。
另外 TiDB 支持基于 label 的数据调度的,给不同的 TiKV 实例加上描述物理信息的 label,例如地域(Region)、可用区(AZ)、机架(Rack)、主机(Host),这样 PD 在对数据进行调度时就会参考这些信息更加智能的制定调度策略,尽最大可能保证数据的可用性。例如 PD 会基于 label 信息尽量把相同数据的副本分散调度到不同的主机、机架、可用区、地域上,这样在物理节点挂掉或机架掉电或机房出故障时,其它地方仍然有该数据足够的副本数。借助 tidb-operator 中 controller-manager 组件我们可以自动给 TiKV 实例加上物理拓扑位置标签,充分发挥 PD 对数据的智能调度能力,实现数据层面的高可用性。
同时我们还可以实现实例级别的高可用性,通过 Kubernetes 强大的调度规则和我们扩展的调度器,我们按优先级会尽量选择让 TiKV 部署到不同的主机、机架和可用区上,把因主机、机架、机房出问题造成的影响降到最低,使数据具有最大的高可用性。
另外运行在 Kubernetes 之上我们能实时监测到 TiDB 各组件的运行情况,当出现问题时,我们也能第一时间让 tidb-operator 对集群进行自动修复 (self-healing)。具体表现为 TiDB / TiKV / PD 实例出现故障时,执行安全的下线操作。同时增加新的实例,来保证集群的规模和之前一致。
总结
TiDB 作为一款 Cloud Native Database,通过 tidb-operator 的方式充分发挥 Kubernetes 平台的强大能力,实现云上自动化管理,极大降低人力运维成本。用户可以根据业务需要进行动态扩容缩容,多租户隔离特性让不同租户的实例可以共享计算和存储资源,互不干扰,同时最大程度充分使用云上资源。Raft 算法和 tidb-operator 自动修复能力以及两层调度机制保证了 Cloud TiDB 的高可用性。UCloud 和 PingCAP 公司深度合作,推出 Cloud TiDB 产品现已开启公测,欢迎大家来体验云时代的新一代数据库。