简介
数据库存储选型经验总结 未读
数据库治理的云原生之道 —— Database Mesh 2.0
数据的组织
从单机到分布式数据库存储系统的演进单机数据库存储,要从内存层和持久化层两个方面来解析。
- 在内存层,仅说关系型数据库,其内存数据结构特点可以总结为:一切都是“树”。我们以最常见的 B+ 树为例,B+ 树具有以下突出的特点:
- In memory 操作效率非常高: B+ 树搜索时间复杂度是 log 级别;并且 B+ 树的叶子节点构成链表,非常有利于在内存中对数据进行 scan 操作。
- 磁盘操作效率高:B+ 树的 Fanout 足够大,树的层级较少,呈矮胖状,可以减少磁盘 IO 数;同时 B+ 树的非叶子节点只存索引数据,叶子节点存实际数据,能大大压缩树高,进一步减少磁盘 IO 数。
- 数据结构高度统一:数据 & 索引都可以直接组织成 B+ 树,因此代码的可维护性、可读性和开发效率都比较好。
- 基于单机的 FS / 块存储去做持久化,我们会遇到哪些问题呢?
- 单机容量瓶颈:在 Database 层的单机服务器上运行着 database 进程,服务器上挂载了大量本地磁盘用作数据持久化。但一台物理服务器能挂载的磁盘容量总是有限的,这就导致了单机的容量瓶颈问题。
- 扩缩容困难:当容量、CPU 或者内存等资源不够时,我们需要进行扩容。在单机时代,扩容意味着将数据从这个磁盘搬迁到另一个磁盘。但不管我们是通过网络还是有线连接手段,都需要花费一定的时间,这可能导致业务较长时间的停写(不可用),因此扩缩容是非常困难的。
- 多份独立数据,成本高:如果我们要在“复制集”之间或者主备机之间去做数据冗余或数据同步,那么每新增一分计算能力(新增一个计算节点),就要新增一分存储冗余,就会导致存储成本提高。
NoSQL
主要的非关系模型有KV模型、宽表模型、文档模型、图模型、时序模型等:
- KV模型:主要特征是将每个数据值与唯一的键关联起来,适用于OLTP场景,例如 Amazon’s DynamoDB;
- 宽表模型:主要特征在于动态列、多版本、TTL,其自动分裂的特性和基于lsm-tree的存储引擎提供了强大的水平伸缩性和写入吞吐量,特别适用于稀疏数据集的海量写入和查询场景。业界主流产品是 BigTable 和 HBase,HBase 是 BigTable 的开源实现;
- 文档模型:主要特征在于提供了最接近于真实的业务对象模型的 JSON/BSON 存储格式,是一种对开发非常友好的存储模型。业界主流产品是 MongoDB,其与 Express、Angular、Node.js 等开源技术栈一起构成了 MEAN Stack;
- 图模型:主要特征在于提供了点和边的存储语义,可以高效执行跨节点和边的网络查询,易于描述实体间的关系。业界主流产品是 Neo4J;
- 时序模型:主要特征在于提供了时间序列数据的建模能力,将时序数据抽象为指标、标签、时间戳三个维度。业界主流产品是 InfluxDB、Prometheus、OpenTSDB 等,InfluxDB 与 Telegraf、Chronograf、Kapacitor 等组件一起组合为 TICK Stack;
NoSQL 本质上舍弃了传统关系型数据库的一些功能,从而可以实现更加灵活的特性。这些丰富的灵活的特性简化了应用程序的数据操作,极大简化了业务逻辑。从应用程序视角,NoSQL 等同于 “Non Relational” 或者 “Non Transactional”,意味着灵活的数据模型和访问接口、可定义的放松的数据一致性,从而提供了关系型数据库以外的另一种选择。且其运行于分布式集群之上,天然具备极好的水平扩展性,能够更好的适用于大数据场景。因此,关系型数据库与 NoSQL 具备非常好的特性互补,应用程序往往搭配使用并形成了标准的数据架构。关系型数据库提供 ACID 能力,保证数据持久性和数据完整性,用于 mission-critical 的关键业务场景。NoSQL 提供水平扩展性和灵活数据模型,用于创新场景和大数据场景。两者间的数据划分,可以由应用程序分别写入,也可以通过 event stream/change stream 的方式从关系型数据库流向 NoSQL。
存储
文件
关系型数据库中往往都包含 Log 数据和 Page 数据。
假定数据库存放数据的文件称为data file,数据库的内容在内存里是有缓存的,称为db buffer。某次操作,我们取了数据库某表格中的数据,这个数据会在内存中缓存一些时间。对这个数据的修改在开始时候也只是修改在内存中的内容。当db buffer已满或者遇到其他的情况,这些数据会写入data file。(为了维护这个db buffer和db file的一致性,引入了checkpoint)
db buffer的存在不仅提高了性能,系统也有机会对db buffer进行一定整理后集中写入,毕竟db数据随机写入的开销比较大。log file一般是追加内容,可以认为是顺序写。
kv
MyRocks: MySQL + RocksDB,是单机 SQL over kv 的典型代表。
- 核心理念:用 RocksDB 替换 InnoDB 。使用 RocksDB 能够有效缓解单机容量瓶颈的问题;
- 特点:一是:数据可压缩比例较高。RocksDB 实现了一种比较优秀的压缩算法,根据实际调研结果显示,在关系型数据库场景,基本上它能实现 2-4 倍的压缩比,能有效缓解单机的容量瓶颈问题。例如,单机原本挂载了 10 块磁盘,只能承载 10 TB 数据,使用 RocksDB 就能在不改变硬件条件下帮助单机承载 20 TB 或 30 TB 等更多的数据;二是,顺序写性能较好,这也是 LSM-Tree 这种数据结构在 HDD 年代出现的核心原因。
- 难点:Compaction 会导致性能抖动,且兼容性一般。众所周知,RocksDB 基于 LSM-Tree 构建,必然会遇到一些典型的 LSM-Tree-based 系统的问题。虽然 RocksDB 对顺序写特别友好,但它一定程度上牺牲了读性能—— RocksDB 在读的过程中会触发 Compaction,可能引发性能抖动,导致前台的写出现卡顿现象;同时,这一类 SQL over kv 解决方案的兼容性能表现较为一般。
计算存储分离
- Amazon Aurora核心理念:计算存储分离,Log is Database。存储层带有特定的数据库计算逻辑,除了具备存储能力之外,还具备 Redo Log 解析、回放生成数据库 Page、维护多版本数据的能力。
- Spanner 系: 计算存储分离,且 Share-Nothing。Spanner 系的数据库系统一般基于分布式 k-v 存储构建,由存储层保证事务特性,计算层做成纯计算的无状态节点。
列式存储
当一行数据有 100 个字段,而我们的分析程序只需要其中 5 个字段的时候,就很尴尬了。因为如果我们顺序解析读取数据,我们就要白白多读 20 倍的数据。那么,能不能跳着只读我们需要的字段呢?当然也是不行的,因为对于硬盘来说,顺序读远远要优于随机读。
列存模型中数据按列而不是按行组织。这意味着,同一列的所有数据值在物理存储上是连续存放的,与行内数据的排列无关。这种存储方式尤其适合于数据分析场景,因为能够高效地处理对特定列的聚合操作和筛选(能够显著减少 I/O 操作)。同类数据存储在一起有利于高效的数据压缩,因为相同类型的数据往往具有较高的相似性和重复性,从而节省存储空间。
不过,这样存储之后
- 数据写入就变得有些麻烦了。原先我们只需要顺序追加写入数据,而现在我们需要向很多个文件去追加写入数据,每次写入都需要更新多个列的存储位置。进而难以不支持复杂的事务处理,如 ACID 特性较弱。
- 查询灵活性受限:虽然对特定列的查询非常高效,但执行涉及多列联合查询或非主键列的查询时,性能可能不如预期,尤其是在需要跨列进行复杂运算时。
那有没有什么好办法呢?对于追加写入的数据,我们可以先写 WAL 日志,然后再把数据更新到内存中,接着再从内存里面,定期导出按列存储的文件到硬盘上。事实上,在一个分布式的环境里,我们的数据其实并不能称之为 100% 的列存储。因为我们在分析数据的时候,可能需要多个列的组合筛选条件。所以,更合理的解决方案是行列混合存储。在单个服务器上,数据是列存储的,但是在全局,数据又根据行进行分区,分配到了不同的服务器节点上。
未来发展
在谈及数据库存储的未来演进时,首先我们可以思考一下哪些因素会触发数据库存储架构的变革和演进?答案可能包含:存储架构自身的革命、数据库理论的突破、或者新硬件冲击引发存储系统架构迭代。基于这三个方向的思考,我们总结了以下几个数据库存储系统的演进趋势:
- 在 HTAP/HSAP 系统中,“实时”是第一关键词。为了支持实时,存储系统可能会发生架构演进和变革,因此我们需要探索:
- 行列存 All-in-one:既要存储行式的数据,又要存储列式的数据。
- 近实时,写时计算:我们需要在存储层实现写时计算的逻辑来支持实时性。
- 在硬件变革趋势上,我们总结了三个变革方向:
- 前几年,我们可能更多关注 SSD、HDD。目前我们处于 SSD 往 persistent memory 转变的风口,那么如何利用 persistent memory 去定制软件架构?
- 计算单元变革:CPU 产品已经从 multi-core 变成了 many-core (从 96c 变成了 192c、384c)。要怎么利用多核的能力?对于非计算密集型的存储系统而言,多余的算力能否用来加速数据库算子?一些无锁的数据结构是不是需要要重新设计?以上都需要我们认真考虑。
- 网络设施变革:例如 RDMA ,以及可编程的 P4 交换机这类全新的一些网络设施,可能会对我们的软件架构特别是分布式存储架构造成较大的冲击。相应地,我们需要在存储侧做出调整。
为什么要用 Tair 来服务低延时场景 - 从购物车升级说起 未读
- 大部分云原生数据库将 SQL 语句解析、物理计划执行、事务处理等都放在一层,统称为计算层。而将事务产生的日志、数据的存储放在共享存储层,统称为存储层。在存储层,数据采用多副本确保数据的可靠性,并通过 Raft 等协议保证数据的一致性。计算节点与存储节点之间采用高速网络互联,并通过 RDMA 协议传输数据,让 I/O 性能不再成为瓶颈。
- 云原生数据库拆解了计算、存储,并利用网络发挥分布式的能力,在这三个层面都充分结合新硬件的特性进行设计。未来的数据库将步入全栈优化时代,从硬件平台优化到架构层优化再到上面的应用层优化。所谓“软件优化三年不如硬件更新一代”,比如算力上,一定是充分利用 CPU 最底层的指令集和最新的加速器。
开发者的“技术无感化”时代,从 Serverless HTAP 数据库开始 “数据库作为一个软件形态本身会消亡,而数据库的平台化、微服务化会取代原来的数据库软件形式”。PS:我只是去调一个服务,接口是SQL,与XXService.QueryXX并没有区别。
- 我们一开始花了很长时间去构建了一个稳定的数据库内核,可以弹性扩展、自动 Failover、ACID Transaction 等非常硬核的基础能力。但这些都是基础能力,这些东西应该隐藏在发动机里。作为一个开车的人,不用关心变速箱里有哪些特性;
- HTAP 能够提供实时的一栈式数据服务。用户不需要关心什么是 OLAP,什么是 OLTP。一套系统可以支撑所有负载,也不用担心 OLAP 负载影响 OLTP 的正常服务;
- 基础设施层面,Serverless 部署的成本变得极低,极致的 Serverless 不用关心任何运维的细节。你可以通过代码和 open API 控制这些集群的起停。真正的按需计费。过去我们其实还是按照服务器、虚拟机这样的资源来去看待一个月多少钱,这个服务能不能粒度更细一些,只收业务流量的钱?尤其是对于偏分析的场景来说,有很多时候我们做大数据分析,比如每天半夜要去跑个报表,可能需要一千个虚拟机算,20 秒钟算完,然后再缩回来。
- 我自己写了一个小程序,在一个全新的环境下,通过代码启动一个 TiDB 的 Serverless Tier 实例。在这个过程里,我只是告诉这个程序,要启动一个集群,这个集群叫什么名字,然后把密码一输,20 秒之后可以直接拿一个 MySQL 客户端连上去了,这个时间未来会进一步缩短。你不用关心它的扩展性,即使上线以后,业务流量变得巨大无比的时候,它也能够很好地扩容上去,没有流量的时候,它还能缩回来。
分布式
数据复制:经过第一部分的讨论,我们在单容器上得到了一个可高效读写的存储引擎,但机器总会故障,如何保证在机器故障的情况下,服务对外提供的读写能力不受到影响?自然就是数据在多个容器上存储多份,待机器故障后,使用其他机器的数据对外提供服务。那如何保证不同机器上有多份数据,且它们是一致的就成为接下来要解决的问题。
- 主从复制:同步复制与异步复制。复制滞后问题;
- 多主节点复制。多数据中心:在每个数据中心都配置主节点,数据中心内部仍是主从复制,跨数据中心则由主节点负责数据中心间的数据交换和更新。多主复制最大的挑战就是解决写冲突,跨地域的 用户1 和 用户2 同时修改了title,主节点1收到主节点2同步过来的请求时发现是冲突的,必需有一种冲突解决方案决定title最终是改为B还是改为C。你可能想不到,处理冲突最理想的策略就是:避免发生冲突 。 应用层保证对特定记录的写请求总是路由到同一个主节点,就不会发生写冲突。比如我们的服务部署了天津/深圳两个地域,两个地域是多主节点复制,则每一个用户只会路由到天津或者深圳其中一个地域,不会存在写冲突;从用户的角度开看,基本等价于主从复制模型。实现收敛于一致的可能方式:
- 每个写请求分配唯一的ID(时间戳/随机数/UUID),每个副本仅保留最高ID作为胜利者写入,其他写入请求则丢弃;
- 为每个副本分配一个序号,序号大的副本写入优先级高于低序号的副本;
- 将冲突的结果都记录下来,依靠应用层(或用户)来决策。如上例中,将B/C都记录下来,让用户决策最终使用哪个标题;
数据分片:基于关键字区间分片;基于键的哈希值分片;分片再平衡;
- 固定数量的分片:最简单的方案:创建远超实际节点数的分片数量,为每个节点分配多个分片;需要迁移时就从现有机器上挑选分片移动到新机器上即可;
- 动态分片:初始仅创建少量分片,当分片的数据增长超过一定阈值时(如10GB),就会拆分成2个分片,每个分片承担一半的数据;反之,分片也会合并;每个分片只会分配给一个机器,但是一个机器可以承载多个分片数据;
- 按节点比例分区 请求路由:分片数据已经就绪,客户端应该把请求发送到哪个机器上呢?尤其是若发生了分片动态再平衡,分片与节点的关系也会随之变化;这本质上是一个服务发现问题;通常有三种方式:
- 节点转发:客户端连接任意节点,若节点恰好该数据则直接返回,若节点没有数据,则将请求转发到合适的节点,并将结果返回给客户端
- 代理层转发;在客户端和数据库之间增加一个代理层(Proxy), Proxy记录分片和节点机器的映射关系,负责请求的转发和响应;Proxy本身不处理请求,只是一个感知分片的负载均衡器。该方案采用较多,一来客户端不需要有复杂的逻辑,Proxy可屏蔽分片/节点的动态变化;再者,处理类似于Redis中的 MGet 等涉及多个键的命令时,Proxy可以完成分发&合并结果的工作;最后,Proxy还可以处理”迁移中”的数据,如一个分片正在从一台机器迁移到另一台机器,命中该分片的请求该如何处理?
- 客户端缓存分区关系。客户端感知分区和节点的分配关系,客户端可直接联系到目标节点,不需要任何Proxy 分片和节点的映射关系如何维护?在使用代理转发的选择下,需要去存储并感知分片和节点IP的映射关系:一般采用独立的协调服务(zookeeper/ETCD)跟踪集群范围内的元数据变化。节点分片的动态再平衡(可能是人工通过控制节点触发,也可能是自动再平衡)会同步写到 Zookeeper中,Proxy通过watch感知到节点变化之后会将后续请求转发到正确的节点;
经过上面所有的讨论,我们可以得到如下这个相对通用的分布式存储架构: