NoSQL Revolution

从本世纪初谷歌的三篇论文发布以来,数据处理领域在大数据的方向上探索了将近二十年的时间。从三篇论文的开源实现 Apache HadoopApache HBase 开始,到打破传统关系型数据库的分布式数据处理系统如雨后春笋般接连诞生,NoSQL 系统回应了移动互联时代的数据爆发式增长的挑战。

诚然,传统的数据库专家对 NoSQL 也有像 MapReduce: A major step backwards 这样的批评,不过 NoSQL 系统本身也在向传统数据处理领域当中被证明有效的特性靠拢,向 Not Only SQL 系统转变。

本文首先从移动互联时代数据增长和数据模型演进带来的实际问题出发,讨论 NoSQL 系统在现在企业数据处理生态当中的定位和价值,然后介绍 NoSQL 系统靠近 Not Only SQL 定位的过程中遇到的硬核诉求,最后分析新时代 NoSQL 的发展方向。

数据量的增长带来的挑战

NoSQL 系统崛起的主要原因就是移动互联时代数据的爆发式增长。

起初,企业经营过程中产生且需要运维的数据并不多,单机数据库应对就绰绰有余。尤其是在摩尔定律尚未失效的硬件主导技术升级的年代,数据量级增长的速度未曾超过硬件升级的速度。关系数据库赢下单机数据库战争以后,几乎每家企业的数据处理生态都被 Oracle 数据库、IBM 的 DB2 数据库和微软的 SQL Server 数据库所占据。

随着移动互联时代的到来,计算机全面进入到民用阶段。几乎人人手持一部甚至多部终端设备,这些设备逐渐占领了每个人生活的绝大部分时间。全域搜索、社交媒体、在线游戏、电商购物、网络直播……提供此类服务的企业所要处理的数据的量级,不再是商业场景下的 B2B 订单、客户关系管理和运维的量级,而是全民参与的 B2C 或 C2C 的用户行为的量级。换句话说,这时企业所要处理的数据,从一部分企业及其行为的量级增长到了全体民众及其行为的量级。

另一方面,硬件的升级也遇到了摩尔定律的瓶颈,硬件的升级不再能够满足用户数量增长的需求。阿里巴巴在 2008 年前后开始的“去 IOE 运动”就是这一趋势的一个注脚。原本,阿里巴巴在应对用户数据快速增长的时候,采取的也是传统的硬件技术升级的手段,采购商业级 Oracle 数据库、特殊定制硬件的 IBM 小型机和 EMC 高级存储设备来支持。然而,一方面受到技术自主可控的驱动,另一方面也是出于企业经营成本控制的要求,阿里巴巴转向了 MySQL 数据库以及后续一系列开源或自研的分布式数据处理系统的解决方案。

当然,单机 MySQL 也无法抗住全网用户每天源源不断产生的行为数据。因此,在阿里巴巴等互联网公司当中就诞生了以分库分表技术为核心的数据库中间件解决方案,即通过分拆业务到不同数据库实例中,同一业务选择分片键分拆到不同数据库实例中,再于业务和数据库实例集群之间设置一个解析查询和转发查询的中间件,来实现以多台廉价计算机和运行其上的 MySQL 数据库,抗下用户行为数据的解决方案。

严格来说,这一方案产生的软件不是 NoSQL 系统。NoSQL 系统的一个重要特征是用户能够像对待单一系统那样与整个 NoSQL 分布式系统交互,而分库分表的数据库中间件往往要求用户知悉底下数据分片的模型,从而针对性的写出不会导致全表扫描的查询。另一方面,即使采用了分库分表的解决方案,系统所能处理的数据量仍然是有限的。目前主流的分库分表方案,最多能够应对 TB 级别的数据。这对于用户账户数据、商户和商品概要数据以及最近一段时间的订单数据或许是足够的,但是对于历史订单数据、商品详情数据、用户历史足迹和社交网络活动记录来说,则远远不够。或者说,即使能够扩容分库分表的数量来支撑更大的数据量级,底下运行的 MySQL 实例产生的开销,也不如 NoSQL 系统底下只是需要一个普通的数据节点更有性价比。

NoSQL 系统当中,除去主打内存缓存的 Redis 以外,诸如 HBase 和支持 Redis 协议的数据持久化 NoSQL 系统 Apache Kvrocks (Incubating) 都能支持 PB 级别甚至以上的数据存储和访问。这得益于从谷歌三篇论文一脉相承的 scale out 策略,藉由简化系统复杂度,以硬件技术的新增长点网络性能抵消单机处理的延时优势。这样,企业当中的数据处理系统可以用增加成本可控的节点数,而非对抗摩尔定律购买价格不支持商用的高端硬件的方式,在延时可接收的范围内应对更大的数据量。

前面提到,NoSQL 系统对于用户来说是一个整体,而分库分表在扩缩容时却未必能够像传统数据库使用体验那样流畅。由于分片键与实例数相关,分库分表分出来多少个库表,这个知识会成为整个系统的一个固有限制。如果想要增加数据库实例,这个过程并非简单地上线新实例就可以开始服务,而是需要整个逻辑数据库在新的库表数下重新分片。我在某司操作过 32 库乘以 128 表到 128 库乘以 128 表的迁移过程,这个迁移的数据同步阶段总共花了两天半的时间,在线上几乎没有感知的情况下以深夜一分钟左右的闪断为代价切换成功。然而,这还是建立在公司有足够强的研发实力支持从头开发一套数据中间件以及数据迁移系统的前提下的开销。而无论是哪种典型的 NoSQL 系统,几乎都支持用户无感知的扩容和缩容动作。

分库分表的数据库中间件实质上操作的是底下不同的数据库实例,传统数据库支持的事务一致性、多表联合操作和存储过程等功能,几乎都受限于实际上数据存在于多个数据库实例的物理限制而无法支持。NoSQL 系统可以认为是在这样的 baseline 上,基于整体考虑设计出一个能够最大化数据处理吞吐和尽可能降低数据延迟,并且尽可能使得用户像对待一个统一系统那样操作的解决方案。

对于定位在支持传统数据库的语义和功能,同时又要满足数据增长需求的 NewSQL 系统,这些系统能够处理的数据量级上限,实际上也没有超过分库分表方案 TB 级别的水平。同时,在数据量超过一定水平时,这些系统会面临严重的功能挑战,例如大事务延迟不可接受,选择 TSO 作为中央授时的系统中心节点不堪重负,或者 Aurora 会提示用户关闭 Binary Log 以保证用户读写的时延。相比起 NoSQL 系统所能处理的数据量级,这些 NewSQL 系统还是不太够看。从它们支持的数据库功能来看,往往与传统数据库也有明显的差别,比如存在微妙差异的事务一致性,不支持存储过程,不支持外键,等等。

数据模型贴近业务的价值

NoSQL 系统崛起的另一个主要原因是打破了关系模型对数据处理领域的垄断。

严格来说,在业务逻辑开发这一块,关系模型并没有统治开发者的心智。虽然不少业务逻辑是写在存储过程或者触发器当中的,这些代码自然深深地被搭上了关系模型的印记,但是尤其在互联网业务开发的领域当中,开发人员并非直接面向数据库编程。在开发人员编写的业务代码到底下的数据库系统中间,经常有一层对象关系映射框架(ORM)的存在。

这就是关系数据库始终绕不过的“对象关系阻抗失配”问题。

现代程序设计语言的主流是面向对象的程序设计,即使并非“一切都是对象”的信徒,大部分语言也都支持数据结构的嵌套。而在关系模型当中,所有的数据都以元组的形式存储,想要表达列表或者嵌套数据结构,要么需要冗余数据,要么需要设置多张表并藉由外键关联来查询。

前者不仅会造成空间的浪费,还会在数据结构趋于复杂,尤其是存在 option 和 either 这样的结构的时候,列的碾平生出非常难以查询和写入的表结构。后者更不必说,原本是同一个逻辑对象的数据,如今散落在多张表上,无论是更新时需要注意的级联变更和完整性约束,还是查询时需要依靠 JOIN 来聚合数据,都是非常麻烦的事情。

反观 Redis 的主要特点之一就是支持丰富的数据结构,例如开发者熟悉的 List/Hash/Set/ZSet 以及方便的 HyperLogLog/GeoHash/Bitmap 等等。对于接受经典数据结构培养的研发人员来说,Redis 这种丰富数据结构上手成本很低,开发者对于基本的数据结构都会使用。反观关系数据库的模型,要在其中实现 List Push/Pop 这样的操作还是有些麻烦。

MongoDB 的数据模型文档将支持灵活的数据模型放在了第一位,Apache Cassandra 的数据模型文档则进一步点明了这种数据模型价值观与关系数据库的不同——如果说关系数据库的数据模型是表驱动的,那么 NoSQL 系统的数据模型就是查询驱动的。

传统的数据库开发流程,往往是由 DBA 或架构师定义出一系列的表及表的模式,藉由关系数据库系统支持的特性和约束来保证数据的完整性和一致性,以这些表及表的模式为基础,上线数据处理系统支持业务需求。如果业务迭代需要引入新的字段或者添加新表支持嵌套数据结构,这些改动都需要送交 DBA 和架构师审批,甚至对于核心数据表的改动,还需要送交研发高管审批。这一过程和认识直到今天仍然没有什么大的改变。基本上,关系数据库在企业当中的定位就是持久化数据资产。

然而,移动互联时代业务的需求有着很强的时效性,需求经常变化,为了应对某个活动需要临时增加某个字段,过后即可废弃。这样的使用场景遇上层层审批的变更流程,必然激发出剧烈的矛盾。NoSQL 系统此时就扮演了一个在企业关键数据资产和业务经常变化且时效性强的需求之间的润滑剂。

一方面,核心数据资产例如用户账户数据、用户信息数据、订单交易数据等等,仍然由关系数据库来支撑运转,保证数据的完整性、一致性和足以应对容灾的持久化,并且借助几十年来发展得相当成熟的数据平台生态进行冷数据归档,以及数据订阅、数据同步等等,作为业务系统的核心数据来源支撑。另一方面,NoSQL 系统存储非核心的经营数据或者衍生、冗余数据,用以支持业务高速迭代的需求。查询驱动的含义就在于此:业务查询是什么样的,底下的数据模式就可以是什么样的。例如使用 Redis 存储用户账户与手机号的对应关系,使用 HBase 存储全国地图上的兴趣点以支持基于位置的用户服务,将业务数据导入 ElasticSearch 当中提供搜索功能,使用 Apache Pulsar 接受采点上报数据。

这种职责分层实践在过去十几年当中不断地被传播和应用,证明了 NoSQL 在企业当中足以赢得自己生存的空间。从数据模型的角度看,贴近业务的数据模型天然适合应对业务的经常性迭代。灵活的数据模式能够快速适应数据模型变更的需求;丰富的数据结构符合开发者的心智模型,能够更快的完成业务代码开发;而对于消息队列、倒排索引系统和图数据库,则是各自领域当中最贴合的建模方式。例如 XLab 分析 GitHub 全域开源协同数据的时候,就自然选择了图数据库来分析人与人、人与项目、项目与项目之间形如社交网络的关系和行为数据。

随着 NoSQL 系统逐渐成熟,尤其是在数据一致性和存储可靠性上面的突破,越来越多的企业也结合自身业务的特性,尝试把核心业务及其数据也假设在 NoSQL 系统上。国外基于 MongoDB 发展出一套 MEAN 应用开发栈,就是这一实践的注脚。虽然业务稳定以后,数据模型变更减少,表驱动的关系数据库能够带来多年积累的软件成熟度和生态繁荣度的优势,但是对于创业公司或者新团队新业务来说,采用 NoSQL 来快速启动自己的业务,并且能够灵活地调整数据模型,或许是个更好的选择。

Not Only SQL 的诉求

NoSQL 系统一开始得名就是因为它的设计理念和数据模型都是反(NO)关系数据库(SQL)的。

这种反叛的极致体现在谷歌的三篇论文当中完全无视数据库领域二三十年的积累,以一种非常土味的方式用廉价机器拼凑起来一个分布式存储系统 GFS 和仅仅支持 MapReduce 这样简单算子的计算引擎。Bigtable 作为初代 NoSQL 引擎,不支持跨行跨表事务,不支持严格的表模式,没有关联查询,没有索引,没有存储过程。

这些“离经叛道”的创举自然引来了数据库大佬们的批评,比如本文开篇引用的 MapReduce: A major step backwards 博客文章。这些批评主要就集中在上面提到的这些“不支持”和“没有”上,以及与数据库生态的不兼容。

一开始,尝到了堆砌大量廉价机器就能解决业务问题甜头的开发者和公司对这些批评自然是不屑一顾的。只是随着业务越长越大,复杂性越来越高,人们面临着数据杂乱无章的失序的风险,以及缺乏传统数据库约束和索引带来的性能退化的痛点,逐渐开始认真考虑数据库领域一直以来的研究的价值。

事务

第一个被提出的议题就是事务,或者说其所代表的数据一致性问题。单机数据库能够保证简写为 ACID 的事务一致性,而分布式系统受到 CAP 理论的限制,往往无法实现单机关系数据库能达到的数据一致性。

关于 CAP 理论的理解,在实际业务取舍的过程中,并不是简单的一致性、可用性和分区容忍性三选二,而是在分布式系统本来就需要能够做到分区容忍,以及业务必须保证服务可用的前提下,看看能够做到多少一致性。当然,有些一致性是以服务短暂不可用或者时延升高为代价的,但是业务绝对不会接受服务一直不可用。

这种情况下首先被提出的解决方案是所谓的 BASE 性质,即基本可用、柔性状态和最终一致,或者我喜欢借用一个说法,叫做啥也不保证。BASE 性质基本已经被扫进历史的垃圾堆里了,不会再有系统标榜自己符合所谓的 BASE 性质。但是它确实提供了数据一致性上的一条基线,即最终一致性。也就是说,对于给定的有限的输入集合,NoSQL 系统当中的数据最终会收敛一个稳定状态,但是这个稳定状态下数据的值是否还有业务意义,不保证。

一般来说,NoSQL 系统在此之上能够做到对自己数据模型下单个数据单元的基本操作是原子性的。比如说,KV NoSQL 系统当中 Put 一个字符串是原子的,不会出现两个 Put 操作的结果是值一部分由第一个操作提供,一部分由第二个操作提供的情况。不过,业务要求显然远远不止这点。对于业务来说,常见的一致性或者叫事务需求,是保证对一行数据的多个操作的原子性,乃至多行数据多个操作的原子性。例如单行数据的 CAS 操作,或者多行数据原子写乃至事务性的读后写的支持。

HBase 和 Bigtable 都支持单行事务,这是因为它们的数据模型里单行数据一定存在单台机器上,保证同一台机器上操作的原子性是比较简单的。大部分系统根据自己物理数据分布的特性,也会向用户保证这类数据存储在同一台机器上的情况下事务能力的支持。

对于跨多台机器的事务支持,则要牵扯到分布式事务的话题。对于 Pulsar 这样数据仅追加的消息系统来说,可以通过批量提交及该操作的幂等性来实现生产消息的事务支持。对于存在删改的系统来说,要么选择放弃隔离性,实现复杂的数据补偿逻辑来支持 Sagas 式的分布式“事务”,要么是采用 Raft 这样的共识算法加上某种形式的两阶段提交算法来支持分布式事务。例如 TiKV 采用了 Raft + Percolator 算法来实现分布式事务,Percolator 本质上还是两阶段提交,但是在生产上会有一系列的优化,并且在某些特定条件满足的情况下可以简化成一阶段提交。

一般来说,启用分布式事务会导致数据吞吐的下降和其他性能影响,因此大部分 NoSQL 系统都提供了用户自己调节数据一致性的选项,来保证只在需要对应级别的数据一致性的情况下,才付出相应的开销。

模式

前文提到,NoSQL 的一个优势是灵活的数据模式能够响应业务的高速迭代。不过,随着业务日渐复杂,开发团队人员更迭,维护 NoSQL 系统上存储的数据的质量就成为了一个难题。

如果所有的数据都是无模式的,或者数据模式没有被良好的记录和检验,那么杂乱无章的数据就可能带来极大的存储空间浪费并阻碍业务开发。

关系数据库和 SQL 当中有专门的数据定义语言(DDL)来描述表模式,通过定义清楚字段的类型和约束来保证数据是结构化的。虽然一旦这种约束过于繁琐和严格,且由于企业流程难于变更时,会影响业务开发的效率,但是清晰的类型约束和唯一性约束是有助于开发人员理解字段的属性和检验业务逻辑正确性的。

这种思路体现在 NoSQL 的演进之路上就是渐进式模式定义。

例如,MongoDB 就支持数据模式校验,Pulsar 也支持定义消息的模式

再以 Cassandra 为例,虽然一开始它对外暴露的是稀疏列簇式大宽表的接口,但是也逐渐地转向建议用户以 CQL 和 Cassandra 交互,同时也保留直接操作底下稀疏列簇式大宽表的手段。

对于现有系统本身不支持数据模式定义的,也有其他系统来支持。例如 Apache Hive 支持为 Hadoop 上的数据定义模式,Apache Phoenix 支持为 HBase 定义数据模式。

索引

对于直截了当的查询来说,NoSQL 的性能优势是明显的。例如 HBase 上已知 rowkey 查询值,这样的操作是系统设计之初就考虑到的情况,属于舒适区。

然而,随着业务发展逐渐复杂,各种新的查询维度也纷至沓来。例如,不再是以 rowkey 查询值,而是以某一列的值为筛选条件来查询匹配的所有行。比如一个用户表,一开始将用户 ID 作为主键存储,现在要根据用户所在地筛选出所有在某地的用户。由于 HBase 没有索引,这种查询只能扫全表后过滤。可想而知,每次查询都需要遍历全表数据,查询的性能肯定好不到哪去。

关系数据库当中也有一样的问题,MySQL 每一行的主键是固定的,要么是创建表模式时指定,要么由插入行时自动生成的 rowid 取代。关系数据库当中可以针对某张表创建索引。一方面,唯一键索引可以施加键值唯一的约束;另一方面,创建索引通常会在存储系统当中额外创建出一个从索引列到主键的映射。实际以索引列为过滤条件查询的时候,会先从索引映射当中找到对应主键的集合,然后直接挑选出小部分相关行做后续操作。

基本上现在的 NoSQL 系统都会实现一定的索引机制。例如业内前沿的数据湖存储 Apache Hudi 系统,一开始只是一个按照直觉写出的读取 Hadoop 上的文件,应用对给定记录的变更并写回 Hadoop 的 Apache Spark 程序。但是在后来投入生产之后,越来越多的开发人力加入和生产环境对性能无止境的追求,为 Hudi 添加了基于元数据文件的、基于 HBase 外存的,以及在选择 Apache Flink 处理引擎的情况下基于 Flink 内置 State 存储的多种索引方案。

上一节提到的能为 HBase 定义数据模式的 Phoenix 项目,也支持为 HBase 创建索引。

从这一系列 NoSQL 系统的转变来看,索引确实是其走向 Not Only SQL 的一个性能上的硬核需求。

新时代 NoSQL 的发展方向

NoSQL 系统的范畴非常广,具体到每个细分领域面临的业务环境演化和技术需求都不尽相同。

对于整个 NoSQL 生态发展的角度来说,未来的发展方向是发挥在应对大数据量上无需全面兼容传统数据库约束的优势,直面海量数据和全球分布式系统的挑战,并且结合具体业务领域对数据模型的要求,根据对应假设设计出在不同业务场景下最优化的数据处理系统。

对于具体的 NoSQL 系统来说,我想 KV NoSQL 这个细分领域值得关注。字典映射是构建复杂数据结构的基础构建块,无论是应对什么场景特化的 NoSQL 系统,最终映射到持久化存储的数据结构,几乎都是某种 KV 的形式。如果 KV 引擎能够从现在 RocksDB 占据单机引擎半壁江山的状况,发展到有一个分布式 KV NoSQL 系统能够支持其他特化的 NoSQL 系统基础的存储需求,那么这或许会是分布式数据处理系统下一次革命的开端。

对于有望成为这个方向解决方案的系统,它至少能够可选地支持上面提到的 Not Only SQL 所需要的硬核特性,也就是事务、模式和索引。在此基础上,如果能够在低延迟、可扩展性和稳定性上实现突破,比如引擎的创新带来的性能提升,利用云原生时代的基础设施和硬件的迭代支持全球规模的集群管理,工程打磨实现生产可靠的稳定且方便运维的系统,那么这样的一个软件将是有价值的。

对于现有系统来说,实现这样的转型并不容易,但也绝非不可能。例如 Datastax 公司全力投入支持以 Cassandra 为基础的 Astra DB 在云上的应用,HBase 社群也在投入存储上云的开发。对于新系统来说,历史包袱会轻松一些,能够基于现在的情况做针对性的设计。但是在增量市场逐渐萎缩的环境下,做好现有系统 API 的兼容让就是生产系统采用的一个关键考量了。例如,ScyllaDB 采用了 thread-per-code 的线程模型来试图革新 KV NoSQL 的性能,但是在面向用户的接口上选择兼容 Cassandra 的 API 以帮助存量用户平滑过渡。

进一步地,如果某个 KV NoSQL 系统在 KV 领域打开了局面,那么它就可以借助协议层抽象来支持不同场景的数据存储需求。

例如,TiKV 是一个支持分布式事务的 KV NoSQL 系统,Titan 通过实现 Redis 协议层来支持 redis-cli 对 TiKV 系统的访问;TiDB 可以认为是在 TiKV 之上实现了一个支持 SQL 访问的协议层。

总结一下,新时代 KV NoSQL 的发展方向,一方面是需要支持前面提到的 Not Only SQL 的硬核诉求,并且需要和现存的数据处理生态保持良好的兼容性。另一方面,这些系统可以在低延迟、可扩展性和稳定性等方向上寻求突破。最后,如果某个 KV NoSQL 系统足够成熟,那么它可以借助协议层了解 KV 之上具体场景下的数据结构信息,知道用户想要存的是什么数据,从而在复杂场景下允许用户直观的表达自己的业务数据,同时让数据处理系统理解相应场景的的语义,帮用户做场景优化。