本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。
本作品 (李兆龙 博文, 由 李兆龙 创作),由 李兆龙 确认,转载请注明版权。
文章目录
- 引言
- Architectural overview
- Data format and meta-data
- Ensuring fast ingestion
- Multi-tiered caching
- Optimized query processing
- Continuous Availability
- 总结
引言
给我的感觉Db2 Event Store
是一个私有的InfluxDB IOX
,只不过没有InfluxDB IOX功能那么强大而已,其在写入流程,COS的利用,存储格式,以及架构上给我的感觉是后者好像也可以这么实现,只不过后者计算节点可以不挂盘,且控制面更为强大而已。
Db2 Event Store
设计原则如下:
- 尽可能快地写入数据: 最小化完成持久化数据所需同步工作,确保节点故障时的持久性,并将数据提供给查询处理。
- Asynchronously refine and enrich data: 数据被写入后会以异步方式进一步优化数据格式,从而提高查询处理的效率。
- 高度优化查询处理: 利用了高度优化的数据格式Parquet;允许跳过部分数据的元数据;强大的查询优化器和runtime;通过在集群中的所有节点上并行运行以及在每个节点内多线程运行,充分利用所有可用硬件;
- 可用性: 最大限度减小单机故障时对于读写的影响
我认为的Db2 Event Store的优劣势如下:
优势:
- 基于IOT场景完全利用了云上廉价的基础设施,并设计多集缓存消除性能影响。
- 高效的写入流程,来源于创新的数据多级流转(这个思路类似于BytesSeries)。
- 重复使用已经构建和优化了几十年的
Db2 SQL compiler
,optimizer
以及runtime
,极大地减少了开发工作量,并立即提供了丰富的 SQL 支持和出色的运行时查询性能。 - 混合
MPP shared nothing
/shared disk cluster architecture
,这样做的好处是将传统MPP shared nothing
的线性可扩展性与shared everything
的高可用性结合,存算分离,在公有云上售卖模型也清楚,隔离也简单,计费也容易。 - 天然支持高基数,因为不同的数据存储在不同的Parquet文件,index分离,还有
synopsis
文件记录表级别概要信息,允许在共享存储中合并,compact过程不影响查询。
劣势:
- 没有提到控制面,元数据完全基于Zookeeper,风险较高,能力相对较弱,无法适应复杂调度。
- 可用性差,单点故障会导致读短暂不可用。
Architectural overview
DB2混合了 MPP shared nothing / shared disk cluster architecture,这样做的好处是将传统 MPP shared nothing 的线性可扩展性与shared everything的高可用性结合。
如上图所示,Table数据根据用户定义的hash key被划分为micro-partitions
,存储在可靠的共享存储介质上。集群中所有的计算节点都能完全访问整个数据集,从而实现存储与计算的解耦,并使每个节点都能独立扩展。
Zookeeper作为协调节点,在逻辑上将micro-partitions
的归属划分给可用的计算节点。逻辑上每个micro-partitions
在任何给定时间点都由一个计算节点拥有,任何该micro-partitions
的读写请求都通过拥有该micro-partitions
的计算节点,实现集群级别的读写并行化。在处理计算节点故障时,受影响的micro-partitions
会在计算节点上重新分配映射关系,与需要进行数据迁移的模式相比,大大缩短了故障转移时间。
为确保元数据没有单点故障,系统采用了floating catalog
的概念,将目录信息存储在可靠的共享存储中,并通过任意一台计算节点提供服务,一旦发生故障在任何计算服务器上重新启动。
数据首先被写入计算节点上本地存储介质(SSD 或 NVMe),并在提交之前进一步复制和写入至少另外两个计算节点的本地存储。这确保了数据在计算节点发生故障时的可用性。然后数据异步写入共享存储。这种模式可以避免了共享存储介质可能产生的任何额外延迟(尤其是在高延迟云对象存储的情况下),高效的处理各种负载的写入。
该架构通过并行和优化存储技术的结合实现快速摄取。该系统采用headless集群架构,所有计算节点都扮演着head nodes 以及 data nodes(类似multi raft?)。在单独一个计算节点内部通过表级别hash partitioning将写入row映射到特定的micro-partitions
,然后发送给拥有该分区的计算节点。(这里提一个问题,为什么不是每个计算节点都可以做写入呢?原因是为了保证一致性,这里的写入不是基于raft的,而是写三份,如果不存在一个单点写入的话可能存在三副本不一致)
数据格式为Parquet,Parquet利用压缩的 PAX 存储格式,可被 Db2 的 BLU 列式runtime引擎用于分析处理,也可以允许Hive、Spark、Presto等直接查询数据。
云对象存储无法为可修改的数据提供强有力的一致性保证,为了利用对象存储的成本和可扩展性优势,db2利用了一种append-only immutable 存储模型,在这种模型中数据块永远不会被重写。Synopsis metadata
允许在查询处理过程中跳过数据(时间?first/last/min/max/count?),作为写入过程的一部分自动生成,并写入单独的 Parquet 文件,从而允许元数据与表数据分开访问和缓存。
索引利用 LSM 树风格的格式来满足append-only的要求。索引作为数据共享流程的一部分异步生成的,以尽量减少写入处理的延迟。所以异步生成意味着在异步共享处理过程中需要消除重复键,这里其实就是多写处理冲突,以前我做multi region写冲突处理时方案时做过这个,方案很多,LWW是最简单有效的一种。
高效的数据访问是通过多层缓存层实现的,该层将频繁访问的数据、元数据和索引缓存在内存中和计算节点本地存储介质中,以防止云对象存储可能产生的延迟。
Data format and meta-data
数据存储格式主要基于两个方面考虑:
- 考虑到处理分析工作负载时的性能优势,最好使用列式数据存储格式
- 使用开源格式,允许外部系统访问
所以最终选择Parquet作为存储格式。
Db2 Event Store 架构下表的最小粒度是micro-partitions
。Parquet 文件属于micro-partitions
,因此给定的 Parquet 文件正好包含一个micro-partitions
的数据。Parquet 文件一旦写入就不可更改。micro-partitions
的每个 Parquet 文件都分配有一个单调递增的数字,称其tablet identifier
。Db2 Event Store runtime engine 以及 external readers 利用这一点来判断较新的数据。共享存储中 Parquet 文件的元数据由 Zookeeper 维护,比如每个micro-partitions
的最新tablet identifier
。
在 Db2 Event Store,tuples 通过 Tuple Sequence Number (TSN)
识别,TSN 是一个整数,可用于定位表中的tuple。Db2 Event Store 中的 TSN 包括tablet identifier
, the zone
,以及tuple在 Parquet 文件中的偏移量。
另外Db2 Event Store还为Parquet贡献了一项特性 Parquet Modular Encryption
[1], 这个特性对每个模块分别加密,从而保留了加密数据的过滤功能和分析效率。它利用 CPU 硬件支持的 AES GCM 密码[2],在不减慢整体工作流程的情况下执行模块加密操作。AES GCM 还能保护存储数据的完整性,使其不会被恶意篡改文件内容。db2使用其他模块保存密码,Parquet 文件在发送到共享存储前已加密,这样做有两个好处:
- 存储后端无法查看加密密钥和明文数据
- 从共享存储检索加密文件后,DB2 Even Store 会使用 Parquet 模块化加密库验证已处理数据的加密完整性,SSD/NVMe 缓存层也使用 Parquet 加密格式,确保本地持久化文件免受内部人员攻击和硬件故障。
这样的做法可以看出db2的工程实现足够优秀,没有经验的团队不会有这样的决策的,因为很多现有的云系统因为性能的考究不会做全链路CRC,这意味着可能发生内存/CPU错误而导致比特跳变,进而导致数据损坏,如果用户侧不自己校验,很多时候根本无法感知这种问题(以我们团队的经验来看,基本几年就会发生一次)。
Ensuring fast ingestion
关键思路和现有的几个系统一样,减少写入流程上的开销,重操作异步执行,基本写入流程如下:
- 客户端可以连接到任何一个节点(连接的节点称为
ingest coordinator
)来执行写入,如果发生故障,会自动将批量插入重新写到任何其他计算节点。ingest coordinator
将batch中数据哈希分割到对应的micro-partitions
,然后将数据写入其中一个副本。 - 对应副本收到写后,会以log的形式将其放入
Log Zone
,持久化的同时会复制到其他计算节点,以确保数据的持久性和高可用性。没有索引。 Log Zone to the Pre-Shared Zone
:Log Zone
可实现快速写入和持久性,但并不是查询的最佳选择。因此必须尽快将最近写入的数据转换为更便于查询的格式,并使用indexes
和synopsis
来提高提高查询效率,这个过程有几秒的延迟。写入Pre-Shared Zone
的数据较小,因为每次只会把上一次持久化到现在的数据持久化到共享存储,因此对于查询并不理想。这个过程异步维护索引 Parquet文件。Pre-Shared Zone to the Shared Zone
:Pre-Shared Zone
数据量足够时小文件便会合并,在所谓的Shared Zone
生成更大的 Parquet 文件。文件越大代表查询处理效率越高,整体数据压缩效果也越好,因此合并后的文件大小一般在 64MB 左右。这个过程会合并索引。
数据的轮转对用户是透明的,所有这些Zone的数据都是不可变的。关键点是Log Zone
不仅要写入本地存储(以保证持久性),还要直接用于处理查询结果。(log is database)
由于计算节点的micro-partitions
分配是动态的,因此需要将索引数据都持久化到共享存储中,以便将micro-partitions
从一个节点转移到另一个节点,当一个micro-partitions
被重新分配给一个新的计算节点时时,数据库引擎会启动一个后台进程,为新的micro-partitions
leader做缓存预热,从而使索引访问能够尽快恢复到最高性能。
另外还有一个常规优化手段,为了表扫描时跳过不必要的数据,Db2 Event Store 会为每个表创建一个synopsis table
,synopsis table
的每一行都涵盖相应用户表的行范围,并包含该行范围内列的最小值和最大值。数据synopsis
块也采用 Parquet 格式。synopsis
在数据合并到Shared Zone
时填充,因此不包括预共享区和日志区的数据。维护这些区域的数据概要内容需要大量额外成本(对共享存储的额外写入),而且由于这些区域的数据量较小,数据跳过带来的价值不大。
Multi-tiered caching
一个通用的优化思路,即对象存储延迟较高(很好理解,COS本身分为三个模块COS前端,Partition layer,Stream layer,一个上传下载内部系统可能多达数十次网络通信,当然COS存小对象另说),价格低;而高性能存储延迟低,价格高(以腾讯云举例基于SSD的系统价格比COS高几十倍),所以如何利用价格低廉的存储并同时提供最佳性能?Db2 Event Store 选择多级缓存,既能利用内存,又能利用本地存储,使系统免受对象存储的高延迟影响。
Db2 Event Store使用了五种缓存机制:
Multi-layered Caching
: SSD/NVMe,RAM 均用于缓存数据块,索引以及synopsis
。Directed Caching
:缓存访问可根据请求类型只定向到 RAM,或定向到本地 SSD/NVMe 设备(可选择放置在 RAM 缓存中);或者可以完全绕过缓存层直接访问云存储(确定冷数据)。Probabilistic Caching
:利用被访问对象的不同表和索引数据总大小的统计数据,并计算相对于给定查询所使用数据被缓存的概率。常常与Directed Caching
结合使用。Soft & Hard Limits
:内存的限制。Epoch based eviction
:一种巧妙的驱逐方式,我没太看明白。
Optimized query processing
在查询处理方面,Db2 Event Store 利用 Db2 的 BLU MPP 集群查询引擎,将 Db2 基于成本的 SQL 优化器和 Db2 的 BLU 加速列引擎的加速分析处理功能与 Db2 Event Store 数据管理层的快速写入和云本地存储功能相结合。这个确实不可模仿,这就是公司的底蕴吧。
计算引擎的优化我并不熟悉,很多内容没看懂。
不过Db2 Event Store 利用过去 30 年中开发和完善的 Db2 SQL compiler, optimizer 以及 runtime,证明了为公共云重新架构并不需要重写所有组件。也证明了计算引擎就根本不应该自己写,直接用现成的最好(Lindorm就使用了Apache Calcite作为计算引擎,InfluxDB IOX使用了DataFusion)。
Continuous Availability
其实对于文中Continuous Availability
这个词我很疑惑,不知道怎么理解,用高可用性看吧,也不对,因为Db2 Event Store在计算节点宕机期间会影响读,也不可用,所以我暂且称它为基本可用吧。
- 写入:
micro-partitions
的所有副本都能处理写入,因此只要大部分副本仍然可用,当一个副本节点丢失时,摄取处理受到的影响不大,客户端处理这种故障在在不同的节点重新尝试操作就可以。 micro-partitions
上的其他操作对表leader的可用性有很高的依赖性。比如:查询,data movement between zones
,data enrichment
,index optimization processes
。前面提到过data enrichment
和index optimization processes
可以通过foller提供服务,所以调度算法只要让主从不要在一台机器上,后台任务就可以继续。读只能通过系统能够快速识别不可用的leader,并将故障计算节点micro-partitions
等leader转移到其他节点,以继续处理查询。- catalog:当catalog节点宕机时,系统仍能继续处理写入和查询,因为metadata数据是由数据节点本身缓存的,它们只依赖目录节点来执行初始缓存,而目录节点使用共享存储进行持久化,这使它可以故障转移到集群中的任何一台主机上,进而为新上架的节点提供缓存服务。(我们曾经有一次故障导致一个集群1000多个接入机70%不可用)
- Zookeeper :只要大部分 Zookeeper 节点存活,它就能持续可用。(我们曾遭遇过五副本mongo宕机的情况)
可以看到单点故障是影响查询的。
最后利用TSBS评估了 Db2 Event Store 与 Druid和TimecaleDB的读写性能,又是遥遥领先!
总结
参考:
5. https://issues.apache.org/jira/browse/PARQUET-1300
6. https://datatracker.ietf.org/doc/html/rfc5288