数据存储和查询的一般性问题

前言

现今,在不同的行业和领域中,数据驱动的决策无处不在,存储、查询和分析数据已成为大多数业务场景下的一个必选项。旺盛的需求和积极的投入催动着大数据领域的发展,在支持数据查询和分析时,人们也会尽量根据业务需求的不同而选择与之适应的支持方式,查询分析相关的数据引擎也是层出不穷。

数据引擎的蓬勃发展给了我们很多选择的同时,也给使用者带来了一定的困扰,网上更是有很多“A引擎与B引擎华山论剑”、“C引擎在X业务下的落地”等很多文章。对于新业务的支持,这些对比、思考和实践在一定程度上有些参考价值,但是在剥离了这些纷繁的引擎选择和引擎实现外,有没有一些一般性的问题需要去思考的呢?我在2020年初的时候做了一些思考,总结了三个问题,现在我将这三个问题整理了下,希望能够给大家添加一种思考问题的维度。另外我也梳理出了对应的一些核心选择项,希望能够大概描述清楚,当面临一个业务需求时,我们的决策空间是什么样子的。

囿于本人的认知水平,这些问题更多的是面向业务基础功能、数据可以被查询到等方面的思考,而不是更大更全的将安全、数据链路等各个方面都囊括进去考虑。

问题一:什么是一条记录?

定义。几乎所有的存储引擎中都有一个数据集合的概念,例如很多DB中都有Table这个概念。数据集合中的一条记录,需要给出非常明确的定义。例如,你可以定义数据集合中的每一条记录,都是一条用户在产品页面上的点击事件。避免将集合中的记录定义为模糊的、不对齐的或者是未被抽象好的概念。如果定义集合中的一条记录既可能是一个文档,也有可能是一个操作日志,那么可能就不是一个好的实践。另外一个不太好的例子,是记录被定义为既可能是一个系统的最新状态,也有可能是系统状态变更的记录。

属性。为了丰富记录的定义,我们需要明确记录的属性。每条记录的属性都是一样的吗?这些属性的定义是明确的吗?这些问题的答案会帮助我们决定是否能够用固定的Schema去描述数据集合。固定Schema的好处是会对接口定义、性能优化、数据质量等多方面都有帮助,不好的地方在于不够灵活,Schema演进的时候需要对不同场景设计支持方式。在增加属性、删除属性甚至重命名和重新排列属性的时候,都需要测试查询引擎和存储编解码是否能够很好的支持。

Key。明确一条记录是否存在Key。理论上来说,一条记录是现实世界或系统的一个数据描述,都是可以被识别的,但是在记录产生、加工和存储的地方,是否需要Key的概念,是要结合业务场景一起去看的。有Key的好处是多方面的,有了Key可以允许对记录做更新,可以辅助去保证数据链路的Exactly once,对数据迁移时的一致性保证也有帮助。然而Key的存在也会给系统带来一定的复杂性,首先,这种Key的产生在有些情况下就是一个非常复杂的问题。另外,很多的系统要求相同Key的数据被放在一起,这种给系统的灵活性和可伸缩性带来了一定的挑战。如果业务没有Key,这需要假设数据集合中的所有记录都是追加的,或者是可以按照某种业务逻辑做整体替换。

粒度。对记录的粒度和不同粒度的优缺点,也要有清晰的认知。首先最简单的,每条记录是聚合好的数据,还是原始的数据。如果是聚合好的数据,显然是为了节省存储空间和增加查询效率,在聚合的过程中也损失了一定程度业务可接受的信息量的丢失。除此以外,系统中的记录往往存在包含关系。例如,假设一个用户操作中包含多个系统事件,那么我们到底是将一条记录定义为一个操作,还是定义为一个系统时间呢?如果定义为一个操作,想要包含事件信息的话,事件就是作为一个array的形式被包含在一个操作记录中,这有可能会弱化查询的能力(取决于引擎的支持)。如果定义为一个系统事件,那么和操作相关的属性就会被展开存储成多份,同时要想获取操作数量的话,还需要进行去重操作。

问题二:记录怎么分布?

分布。在上文中,提到数据集合的概念,集合本身是无序的,也不存在分布等描述。在数据写入、读取和运算等相关能力无限的情况下,我们完全可以不在乎记录是如何分布的,因为基于记录和记录的集合已经能够完全去描述所有的查询语义。然而,这个美好的假设显然不成立,另外计算机系统中还存在被广泛应用的局部性原理,因此,我们需要考虑到这些记录是被怎样组织的。

分组。在数据引擎中广泛存在Partition, Shard等相关概念,这里可以统一的称为对数据的分组。最简单的分组策略是不讲任何规则的随机分,这种情况下显然只是为了解决容量相关的瓶颈问题,在查询时起不到任何作用。我们希望分组策略能够减少查询时的读放大,让特定的查询尽可能的只关注特定分组的数据,这意味着分组需要基于特定的逻辑,也就是基于记录的属性去做分组。例如某些数据集可以按照时间分或者按照省份分,这非常取决于业务的频繁查询路径。例如对于时序数据,假设数据总是按照时间切分和查询的,是一个常见的选择。基于业务和记录属性的分组,在很多情况下很自然的需要考虑均衡和倾斜的问题。在有些情况下我们不太在意分组的数量,例如如果按照时间分我们就自然累积就行了。可是如果有些系统按照某些key进行(hash) 分组,如果调整分组的数量,还需要去关注这里的复杂性和调整成本。

排序。大家都知道在一个数组中去搜索一个数字是否存在时,这个数组是否有序对搜索的性能会产生很大的影响。同样在组织数据时,数据在多大程度上是有序的,也会影响到我们查询的效率。从完全无序到几十条记录或者一个大的批次有序,再到整个分组内的记录都是有序的,这里面的成本和收益以及对系统的要求,大家可以做一些深入的思考,不展开聊。除了有序的程度之外,按照什么排序,这也是一个需要和业务和查询逻辑相关的东西。例如在飞机管理系统中,如果每条数据代表这一次乘坐记录,那么依次按照航班号、飞行号和座位号排序或许是一个不错的选择。

问题三:如何去设计辅助存储?

信息冗余。上文提到,记录的集合这个描述已经包含了所有需要查询的信息,同样是因为美好的假设不成立,需要给信息提供一定冗余度,让数据查询的时间和效率尽可能的满足需求。

索引和预计算。索引是一种在数据系统中广泛存在的加速查询手段,索引的数据结构也是多种多样的, bitmap, 各种Tree,postition list等等。我们需要理解每种数据结构的功能边界和性能预期。举例说来,对于整数数据,如果数值较多并且常有按照取值范围的查询,那么对每一个取值建一个bitmap就是一个糟糕的主意。预计算也是一种广泛被用来加速查询的手段,例如对于频繁查询模式,去做预聚合,建cube,甚至提前Join都属于这个范畴。例如虽然乘客乘坐飞机的记录数据非常的全面,但是业务系统经常查的就是从某地出发的乘客数,那么按照时间和出发地点将数据提前聚合一份额外的存储,就是一个值得考虑的选择。

资源、对齐和一致性。什么事情都是有成本的,额外的存储自然会有额外的存储资源消耗,同时往往也需要一定的计算资源去生成存储数据,例如,预聚合所需的计算,索引中的各种Tree的构建等等。除此之外,辅助存储之间,以及辅助存储与原有数据如何做对齐,是需要好好思考的问题。对两个属性分别建的索引,在逻辑运算时需要设计好如何得到一个包含结果的位置列表。对于预聚合好的数据,也需要想清楚和原有的数据是如何对齐到一起的。例如,假设每天的数据集合都生成一份预聚合好的数据,那么就是按照业务逻辑的天进行对齐的。有了冗余之后,自然而然会带来一致性的问题,在做系统设计时,需要考虑清楚是原始数据和辅助数据到底是同步可见的,还是异步可见的。要结合业务去思考,什么样的一致性假设是可以被接受的,能否利用对齐方式和额外的查询条件来确保一致性。

后记

这三个问题大概描述清楚了我们的决策空间,那么,我们如何根据这三个问题去分析各种类型的引擎呢?对于具体的业务场景,我要选择什么样的引擎以及怎样组织我们的数据呢?

我们以后有机会再聊。