然而,弹性数据库项目并不是解决京东数据库问题的“银弹”,因此,京东在数据库服务方面仍面临着以下挑战:
>沉默成本:目前数据库服务的所需资源还是要预先申请,而预先申请的资源在一段时间内是没有被充分使用的,这就是沉默成本。
>秒级故障修复以及扩容(快速补从):目前一旦发生故障或者扩容,都是通过拷贝大量数据的方式进行补从完成的,速度比较慢。
>大容量数据存储及容灾:由于单机硬件上限的限制,使得单个实例可以承载的上限是有限的,而且数据量越大,灾难恢复难度越大,对线上影响越大。
为了解决以上的问题,我们必须解决数据库服务的三大问题,即存储之殇、扩容之痛和运维之艰。因此我们在弹性数据库的基础上启动了 disca(disaggregated storage and compute architecture)项目。
注:本文是在假设读者已经了解 kubernetes 和 vitess 的基础上进行描述的。
存储之殇是每个数据产品都会有的,因为我们必须为未来可能产生的数据量提前准备存储资源,这就好比我们要为将来可能要购买的商品提前付款,对企业和组织来说,相当于是增加了运营成本,而且预先购置的资源不能马上使用,不可避免的会产生资源浪费。
弹性数据库虽然实现了流式资源交付,但也只是将资源交付的粒度从原来的 1T 降低到了 64G。实际上,弹性数据库只是降低了数据库服务的成本,并没有完全解决”沉默成本和资源孤岛(资源不能共享)”的问题。
如上图所示,以前每个应用使用的数据库存储资源都需要提前预估好,并且数据库服务是一次性将所有存储资源交付。这是一种非黑即白的交付方式,要么将所有存储资源全部交付,要么一点资源都不交付。
而弹性数据库采用了小粒度流式交付,初始交付一部分存储资源,当磁盘使用率达到 80% 的时候,再进行下一批存储资源的申请和交付。这种方式相当于是小粒度批量交付,无法完全避免存储资源的沉默成本,即提供了一部分存储资源,但是在相当长一段之间内这部分存储资源并没有被完全使用。同时,由于各个数据库实例独享存储,不同实例的磁盘 IO 负载压力不同,导致某些实例 IO 极度紧张,而某些实例 IO 十分空闲。
弹性数据库虽然实现了用户无感知的在线伸缩容,但其仍是传统的数据库服务,在面对实例达到硬件上限或者实例所在的宿主机无法申请到额外硬件资源的情况时,仍然需要迁移扩容。
在满足扩容资源需求的宿主机上申请到容器。
从原来容器拷贝存量备份文件到新容器中,基于存量数据启动 mysql 服务,与主库构建主从关系,然后追加 binlog。
待新从库与主库一致,进行切换。若原来的实例是主库,则先进行主从切换再删除原来数据库实例,否则直接删除原来数据库实例。
从上述过程可以看出,迁移扩容过程复杂,且需要传输大量数据(存量数据传输 + 追加 binlog),耗费时间极长。如果遇到大量数据写入,有可能出现新加的从库一直无法追上主库,导致扩容还未完成,原来的数据库实例硬盘的剩余空间就已经被耗尽,造成数据库宕机,无法提供服务。
根据我们的经验,80% 的扩容和拆分都是由于数据量增大、本地磁盘不足引起的。数据量虽然一直在增大,系统使用的基本都是热数据(近期生成的数据,这部分数据的数据量基本不会有太大变化),对于冷数据(很早的历史数据),访问频度其实不高。因此为了不经常访问或者几乎不访问的数据进行频繁扩容实际上没有太大必要性。若本地磁盘能够足够大,那么这些扩容就可以避免。
弹性数据库虽然具备故障自动恢复和处理,但是数据库与存储的耦合导致数据库实例一旦出现故障,必须添加一个新的数据库实例进行故障恢复。这个过程和前文提到的数据库扩容的最坏场景一样复杂,涉及大量数据传输,且在极端情况下(大量数据写入)会出现新添实例无法追上现存 master 的情况。
为了解决上述三大问题,我们开始研究 disca 项目,通过整合 ChubaoFS、kubernetes、MyRocks 和 Vitess,以极其简单的方式实现了存储计算分离。
ChubaoFS
ChubaoFS 是为大规模容器平台设计的分布式文件系统,可以同时提供对象存储和文件系统协议的存储产品。该产品具备以下特点:
>可扩展性:ChubaoFS 中使用了分布式元数据子系统,以便提供更高的可扩展性。
>多租户:在多租户高并发的条件下,提供了对大文件和小文件的随机 / 顺序读写的支持。
>强一致性复制:根据文件写入方式的不同采用不同的复制协议来保障副本之间的一致性。
>兼容 Posix 接口:简化上层应用的开发,降低新用户的学习难度。同时,ChubaoFS 在实现时放松了对 POSIX 语义的一致性要求来兼顾文件和元文件操作的性能。
>兼容 S3 接口:提供与 Amazon S3 相兼容的对象存储接口,可以和 POSIX 接口同时使用。一份数据,多种接口,根据用户的使用场景可以灵活选择,帮用户应对日益复杂的存储使用场景。
Kubernetes
Kubernetes 是由 google 开源的自动化容器编排、调度和管理系统。具备以下特点:
>可移植: 支持公有云、私有云、混合云、多重云(multi-cloud)。
>可扩展: 模块化、插件化、可挂载、可组合。
>自动化: 自动部署、自动重启、自动复制、自动伸缩 / 扩展。
MyRocks
MyRocks 是 MySQL 基于 RocksDB 实现的存储引擎,更适合运行在 SSD 等高速存储设备上,与 Innodb 相比具备节省存储空间、高效写入性能的优点。
Vitess
Vitess 用于实现 mysql 快速在线弹性扩容的数据库集群系统,具备在线快速扩容、故障自愈、自带内存池和查询缓存的优点。
常规弹性数据库的架构如下图所示:
常规弹性数据库的架构采用本地存储,通过控制台与 kubernetes api server 交互、通过直接创建 pod 的方式进行数据库服务部署。但是这种架构的缺陷就是:
一旦集群扩容或者故障恢复,由于各个数据库实例均采用本地存储,因此需要添加新的数据库实例,并通过网络传输数据文件和 binlog,会产生大量的网络数据传输,最终影响扩容以及故障恢复的时效性和稳定性。
采用本地磁盘导致不同实例的磁盘负载不同(容量和 IO),有的实例存储资源紧缺,有的实例存储资源空闲,存储资源不能共享,出现“资源孤岛”。
Disca 就是为了弥补常规弹性数据库的不足而产生的,其架构如下图所示:
从图中我们可以看出,与常规弹性数据库相比有如下改变:
不再使用宿主机的本地磁盘,而使用 ChubaoFS,这样每个数据库实例与本地磁盘完全解耦。
从原来的 MySQL innodb 引擎转变为 MyRocks 引擎。通过测试以及线上服务经验,我们发现 ChubaoFS 在批量追加写入的场景下表现出来的性能更加优异,而 MyRocks 就是采用 LSM Tree,以批量追加 / 覆盖的方式进行数据写入。两者结合可以发挥最大的性能优势。
抛弃原来采用 Pod 进行数据库实例,集群的编排、扩容和调度工作完全交给控制台的方式,而是充分利用 kubernetes 的能力进行服务编排和调度,通过 statefulset 进行数据库实例管理、编排和调度,通过 pvc 实现数据库实例与数据的粘性,进而保证数据不丢。
透明集成 ChubaoFS
为了实现 ChbuaoFS 对用户的透明,ChubaoFS 就需要作为 Kubernetes 可以自动识别的远程分布式存储服务存在,因此 ChubaoFS 实现了 kuberne 的 CSI(Container Storage Interface), 从而实现与 kubernetes 的无缝集成和基于 POD 的动态挂载。
存储共享
在使用 ChubaoFS 时,申请的每个 Volume 的大小是配额记录信息,不会预先分配资源,也不会以此来限制用户使用的存储上限。比如:ChubaoFS 总体的存储空间是 1TB,有十个数据库实例去申请 ChubaoFS 的空间,每个数据库实例申请的 PV(persistent volumes)的大小都可以是 1TB,只要十个数据库实例实际使用的磁盘之和不超过 1TB 即可。而实际每个用户使用的磁盘空间的大小,由 ChubaoFS 的资源管理节点进行统一监控,一旦总体使用磁盘容量超过总容量的 80%,ChubaoFS 资源管理节点会第一时间监控到,并进行报警。
感谢 ChubaoFS 共享磁盘的设计,disca 可以为每个数据库实例申请比较大的磁盘(默认:2TB),而不用担心浪费或者超过集群总容量。从而可以减少数据库实例因为磁盘空间不足而不得不进行拆分或者扩容的情况。
保证数据粘性
数据库服务不同于应用服务,应用服务本地存储的一般是日志文件,其所属 pod 重启或重新调度后,本地数据可以被删除。而数据库服务在重启或者重新调度后希望原来的数据继续保留和使用,这就是数据粘性:当某个数据库实例所在的 pod 重启或者重新调度,该数据库实例以前的数据不会丢失,并且在数据库重启或者重新调度到另一个 Node 上时,可以重新使用以前的数据。
要实现数据粘性,我们需要使用 kubernetes 中的三种资源:statefulset、pvc(persistent volume claim)和 pv。statefulset 中的每个 pod 都是通过 volumeClaimTemplates 来动态地生成与 pod 一一对应的 pv 和 pvc,并且 statefulset 还保证了每个 pod 的唯一性和顺序性,三种资源的关联关系如下图所示:
所以通过将这三类资源搭配使用可以实现数据粘性,代码片段如下:
一键服务部署
采用 helm 基于 statefulset、pv、pvc 以及 storageclass 完成弹性数据库 On ChubaoFS 的一键化部署,如下图所示:
针对图中的流程,进行详细说明:
每次用户向控制台发起创建 keyspace 请求时,会携带请求参数,这些参数包括:副本数、存储引擎类型、资源类型、资源数等。
控制台首先检查本地是否有对应的 charts 模板,若有,则进行使用请求中携带的参数进行替换;否则从 charts 中心(托管 charts 文件,目前我们是基于 httpd 部署的服务,并且挂载 ChubaoFS 以保证 charts 文件的安全和容灾)下载并缓存到本地,然后执行参数替换。
说明:由于 charts 模板一旦生成,几乎不再变化,因此 charts 中心的 charts 文件由管理员进行不定期更新。
基于参数替换后的 charts 文件在 IDC1 中创建 replica 类型的 statefulset 实例:sts1,该实例中包含两个 mysql 实例:master 和 slave1,并且 slave1 与 master 自动创建主从复制关系。mysql 实例通过 pvc 直接绑定 ChubaoFS 中的 pv 到本地。
说明:vitess 中的数据库节点分为两类:replica 类型和 readonly 类型。其中 replica 类型的节点是指 master 以及与 master 同机房的 slave 节点。readonly 类型节点是指与 master 不在同一个机房的 slave 节点。针对每种类型的节点都有一种 statefulset 与其对应。因此创建一个具备跨机房容灾的数据库集群至少需要为每种类型的 statefulset 资源都创建一个实例。
master 和 slave1 节点一旦服务启动,会自动去 etcd 注册自己的元数据:所属 keyspace、shard、tablet 类型、主从关系等。
基于参数替换后的 charts 文件在另一个机房 IDC2 中创建 readonly 类型的 statefulset 实例:sts2,该实例中包含一个 mysql 实例:slave2,并自动将 salve2 与 master 建立主从复制关系。
slave2 节点一旦启动,会自动去 etcd 注册自己的元数据:所属 keyspace、shard、tablet 类型、主从关系等。
ChubaoFS 保证了数据的永久留存和分布式存储,通过 statefulset 并且配合 MyRocks 的事务提交同步刷盘设置,可以实现每次事务的写入数据都可以在事务提交时写入到分布式存储中,进而使得 MyRocks 可以在任何故障情况下保证数据安全性。
虽然弹性数据库与传统 mysql 数据库相比已经节省了成本,但是在弹性数据库的基础上 disca 还将进一步优化,将成本节省到极致:
传统的数据库架构中,从库的作用是提升读能力和数据容灾。但是在 disca 中使用 ChubaoFS 实现了数据容灾,所以就没有必要再通过从库进行数据容灾,从库的唯一价值只剩下提升读能力。因此对于读流量不高的业务也就没有必要添加从库,仅仅通过单个 master 就可以实现数据容灾,节省了多个从库占用的额外资源。
通过 ChubaoFS 实现存储容量共享,避免存储碎片和资源预购,真正实现了存储资源的流式提供和交付,极大地节省了存储资源成本。
由于 ChubaoFS 的整合 IO 能力,打破了传统方式 IO 壁垒,所有应用都可以充分共享和利用集群的所有 IO 能力,彻底避免了业务 A IO 吃紧,而业务 B IO 空闲的场景,通过 IO 共享,避免了资源浪费。
由于 ChubaoFS 实现了可擦除编码,因此平均数据副本只有 1.5 左右,进一步降低了存储成本。
传统的故障切换分为两种:主库故障切换和从库故障切换,disca 提升了两种故障切换场景的效率。
传统主从架构的主库故障切换分为两步:切主和补从。在切主过程中 disca 发挥的作用不大,但其重新定义了补从过程。
传统补从过程:基于以前静态备份(通常是一天以前)启动一个新的 mysql 实例,并作为 slave 节点加入到集群中,然后应用从静态备份时间以后的所有 binlog,直到追上 master 的状态。这个追加 binlog 的过程极其漫长。
Disca 补从过程:基于 statefulset 进行自动补从。针对宕机的 mysql,master 会自动拉起一个 mysql 实例,mysql 实例会查找自己所属集群的最新 master,然后自己作为从库加入到集群,由于故障 master 通过 pvc 已经与 ChubaoFS 的 PV 建立了存储粘性关系,因此新启动的 mysql 实例会将故障 master 的 pv 继续挂载新的容器上,只需要对宕机恢复期间(通常数秒时间)delay 的 binlog 进行追加即可,极大降低了故障恢复的补从时间。
当一个从库出现故障,传统的处理方式是基于一份静态备份数据新建一个 slave,然后追加从上一次静态备份时间以后所有 binlog。
而 disca 在从库故障后,会自动拉起一个新的 slave 实例,但是不会基于静态备份 + 大量 binlog 追加的模式进行从库数据恢复,而是直接将故障从库的 pv 挂载到新的 slave 实例上,然后追加故障恢复期间的少量 binlog,极大地缩短了从库故障的恢复时间。
在相同配置参数、相同 CPU 和内存资源,不同并发场景和不同网络条件的情况下,我们对 disca 和本地 MyRocks 的读性能进行了对比测试,测试结果如下:
从上图可以看出:
TPS:在并发低于 256 的时候, disca 比 myrocks local 略高,在 256 并发下,discs 开始低于 myrocks local。
TP95: 在不同并发条件下,disca 的 TP95 与 myrocks local 的几乎持平。
因此 disca 在读性能上,相对于本地存储不具备明显优势。
无论是 local 还是 disca 模式,我们测试发现在并发为 512 条件下,双方写入性能最优,因此我们在 512 并发条件下,针对影响写入性能的不同参数组合场景进行了写入性能对比测试,测试结果如下:
从上图可以看出:我们分别对本地 nvme 环境、ChubaoFS-sata 集群环境和 ChubaoFS-ssd 集群环境进行了对比测试,结论如下:
TPS:disca 的平均 TPS 比 Myrocks local 的平均 TPS 高出 19.1%。
TP95: disca 的平均 TP95 是 myrocks local 的平均 TP95 的 44.7%。
Disca 与本地 nvme 环境下 myrocks 相比,在写性能上优势比较明显。
通过集成 MyRocks、ChubaoFS、Vitess 和 Kubernetes,我们探索出了一条更为简单易实现的存储计算道路,而且这种方案对于大部分中小企业来说都可以快速落地实施。相比于参考 aurora 论文去修改 mysql 源码的方式,这种方式节省了大量的研发投入,降低了技术门槛。
通过 disca,我们也得到了切实的好处:大幅降低存储成本、提升写入性能,实现了快速故障处理和数据容灾。
需要注意的是,从最后的测试数据我们可以看到,disca 也不是一枚银弹,它更适合于高并发写入的场景。
吕信,京东零售数据库技术部负责人,先后主导并从事大数据产品以及数据库产品研发,目前专注于京东数据库存储计算分离产品的设计与研发。