从托管到原生,MPP架构数据仓库的云原生实践
发布时间:2022-01-21 点击数:333
一 前言
Cloud-Hosted:基于市场和业界的云需求,大部分厂商选择了云托管作为演进的第一步。这种模式将不再需要用户线下自建IDC,而是依托于云提供商的标准化资源将数据仓库进行移植并提供高度托管,从而解放了用户对底层硬件的管理成本和灵计划资源的约束。
Cloud-Native:然而随着更多的业务向云上迁移,底层计算和存储一体的资源绑定,导致用户在使用的过程中依然需要考量不必要的资源浪费,如计算资源增加会要求存储关联增加,导致无效成本。用户开始期望云资源能够将数据仓库进行更为细粒度的资源拆解,即对计算,存储的能力进行解耦并拆分成可售卖单元,以满足业务的资源编排。到这里,云原生的最大化价值才被真正凸显,我们不在着重于打造存算平衡的数据仓库,而是面向用户业务,允许存在大规模的计算或存储倾斜,将业务所需要的资源进行独立部署,并按照最小单位进行售卖。这一刻我们真正的进入了数据仓库云原生时代。
二 ADB PG云原生架构
为了让用户可以快速的适配到云数据仓库,目前我们采用的是云上MPP架构的设计理念,将协调节点和计算节点进行独立部署,但承载于单个ECS上,实现了计算节点存储计算一体的部署设计,该设计由于设计架构和客户侧自建高度适配,可快速并无损的将数仓业务迁移至云上,对于早期的云适配非常友好且满足了资源可平行扩展的主要诉求。
内存:主要负责行存访问加速,并负责文件统计信息的缓存;
本地盘:作为行存的持久化存储,并作为远端共享存储的本地加速器;
远端的共享存储:作为数据的持久化存储。
3 读写流程
- 用户写入数据通过数据攒批直接写入OSS,同时会在本地磁盘上记录一条元数据。这条元数据记录了,文件和数据表的对应关系。元数据使用PG的行存表实现,我们通过file metadata表来保存这个信息。
- 更新或者删除的时候,我们不需要直接修改OSS上面的数据,我们通过标记删除来实现,标记删除的信息也是保存在本地行存表中,我们通过visibility bitmap来存这个信息。标记删除会导致读的性能下降,我们通过后台merge来应用删除信息到文件,减少删除带来的读性能影响。
1. Group flush:一批写入的数据,可以通过group flush写到同一个OSS文件,我们的OSS文件采用了ORC格式,不同bucket写入到对应strip;
2. 流水线异步并行:编码攒批,排序是典型的cpu密集型任务,上传到oss是典型的网络IO密集型任务,我们会把这2种任务类型并行起来,在上传oss的任务作为异步任务执行,同时对下一批数据编码排序,加快写入性能。
- 我们通过读取file metadata表,得到需要扫描的OSS文件。
- 根据OSS文件去读取对应文件。
- 读到的文件通过元数据表visibility bitmap过滤掉已经被删除的数据。
• 通过本地行存表实现事务ACID,支持数据块级别的并发;
• 通过Batch和流水线并行化提高写入吞吐;
• 基于DADI实现内存、本地SSD多级缓存加速访问。
4 可见性表
字段 | 类型 | 说明 |
table_oid | Int32 | 表的oid |
hash_bucket_id | Int16 | hash_bucket的id |
level | Int16 | 逻辑文件所处的merge级别,0表示delta文件 |
physical_file_id | Int64 | 逻辑文件对应的oss物理文件id |
stripe_id | Int64 | 逻辑文件对应的oss物理文件中的stripe id |
Total_count | int32 | 逻辑文件总共具有的行数,包括被删除行数 |
Hash bucket:是为了在扩缩容的时候搬迁数据的时候,能够按照bucket来扫描,查询的时候,也是一个bucket跟着一个bucket;
Level:是merge tree的层次,0层代表实时写入的数据,这部分数据在合并的时候有更高的权重;
Physical file id:是文件对应的id,64字节是因为它不再与segment关联,不再只需要保证segment内table的唯一性,需要全局唯一;
Stripe id:是因为一个oss文件可以包含多个bucket 的文件,以stripe为单位,方便在segment一次写入的多个bucket合并到一个oss文件中。避免oss小文件,导致性能下降,和oss小文件爆炸;
Total count:是文件行数,这也是后台合并的一个权重,越大合并的权重越低 。
字段 | 类型 | 说明 |
physical_file_id | Int64 | 逻辑文件对应的oss物理文件id |
stripe_id | Int32 | 逻辑文件对应的oss物理文件中的stripe id |
start_row | Int32 | delete_bitmap对应的起始行号,每32k行对应一个delete_bitmap |
hash_bucket_id | Int16 | hash_bucket的id |
delete_count | Int32 | 该delete_bitmap总共记录删除了多少行 |
bitmap | bytea | delete_bitmap的具体数值,压缩存储 |
Start_row对应32k对应一个delete bitmap。这个32000 4k,行存使用的32k的page可以保存7条记录。
Delete count是被删除的数量。
我们无需访问oss,可以直接得到需要merge的文件,避免访问oss带来的延迟,另外oss对于访问的吞吐也有限额,避免频繁访问导致触发oss的限流。
5 行列混存
1. 0层实时写入的会做合并,不同bucket的文件会合并成大文件,不同bucket会落到对应的stripe;
2. Merge会跨层把符合merge的文件做多路归并,文件内严格有序,但是文件间大致有序,层数越高,文件越大,文件间的overlap越小。
ORC文件:一个ORC文件中可以包含多个stripe,每一个stripe包含多个row group,每个row group包含固定条记录,这些记录按照列进行独立存储。
Postscript:包括文件的描述信息PostScript、文件meta信息(包括整个文件的统计信息,数据字典等)、所有stripe的信息和文件schema信息。
stripe:stripe是对行的切分,组行形成一个stripe,每次读取文件是以行组为单位的,保存了每一列的索引和数据。它由index data,row data和stripe footer组成。
File footer:保存stripe的位置、每一个列的在该stripe的统计信息以及所有的stream类型和位置。
Index data:保存了row group级别的统计信息。
Data stream:一个stream表示文件中一段有效的数据,包括索引和数据两类。
索引stream保存每一个row group的位置和统计信息,数据stream包括多种类型的数据,具体需要哪几种是由该列类型和编码方式决定,下面以integer和string 2种类型举例说明:
1. 零拷贝:为了把ORC的数据类型转换成PG数据类型,我们对于定长类型的做值拷贝,变长类型直接转换成PG的datum做指针引用。
2. Batch Scan:面向column采用batch scan,替代逐行访问而是先扫完一列,再扫下一列,这样对CPU cache更加友好。
3. 支持Seek read:方便过滤命中情况下的跳转。
6 本地缓存
维度 | RT | Throughput | ||
产品 | DADI | Alluxio-Fuse | DADI | Alluxio-Fuse |
命中内存 | 6~7 us | 408 us | 单线程: 4.0 GB/s四线程: 16.2 GB/s | 2.5 GB/s |
命中磁盘 | 127 us | 435 us | 四线程: 541 MB/s | 0.63 GB/s |
从中看到,DADI相比开源解决方案alluxio在内存命中的场景RT上有数量级的提升,在throughput上也有明显的优势。在命中磁盘的场景,也有明显的性能优势,在部分分析场景下,我们会频繁但是少量读取文件统计信息,这些统计信息我们会缓存在本地,这个优势带来整体性能的较大提升。
Cache Instance:管理本地缓存,缓存文件抽象成虚拟块设备来访问,数据在memory和本次磁盘的冷热以block为单位管理。
1. 短路读,直接读共享内存,避免通过IPC读;
2. 缓存是否命中的数据结构,也是在共享内存里面。通过reference count,结合robust mutex来保证共享内存数据的多线程安全;
3. 磁盘读,100us,+ 27us约等于磁盘读本身rt,IPC走shm通信,没有使用本地socket通信。
4. 极低的资源使用。
内存:DADI Service使用的内存在100~200M,原因在于基于共享内存的IPC实现,hash表等数据结构,避免多进程架构下内存膨胀, 精简的编码方式,1个内存页16k 对应 4byte的管理结构;
CPU:Local DADI Service在磁盘打满的时候单核CPU使用20%左右。CPU的使用在SDK这边,SDK与Local DADI Service通信很少。
1. 缓存优先级
支持统计信息高优先级,常驻内存,索引信息常驻本地磁盘。支持维度表数据高优先级缓存在本地。
2. 细粒度缓存策略
为了避免大表冷数据访问,导致本地热数据被全部替换,大表使用专有缓存区。
3. 文件异步预取
根据查询情况,把解析的数据文件,预先读取到本地,这个过程不影响当前文件的读写,并且是异步的。
7 向量化执行
8 有序感知
1. 消除多余sorting操作。如果data本身有序,且满足排序要求,则不需要加sort操作。
2. 最小化需要排序的列。例如希望对{c1,c2,..cn}排序,如果有谓词c1=5,则order简化成{c2,..cn},避免排序多一个字段。
3. order下推。在初始化阶段,降意向排序操作尽量下推。
1. 首先针对不同算子的有序性需求,例如(join/group by/distinct/order by),建立算子的interesting order(即这个算子期望的有序输入)。
2. 其次在sort scan的过程中所生成的interesting order,会尽可能下推到下层算子中(sort-ahead),以尽早满足order属性要求。
3. 如果一个算子具有多个interesting order,会尝试将他们合并,这样一个排序就可以满足多个order属性的需求。
这里的问题在于sort scan的多路归并需要一条条读取数据,与向量化的batch scan与文件的批量读冲突,我们通过CBO来选主最优的执行计划。
9 细粒度并行
四 性能结果
1 扩缩容性能
计算资源扩容(节点数) | 2->4 | 4->8 | 8->16 | 16->128 |
用时 | <1min | <1min | <1min | <7min |
2 读写性能
写性能测试
|
ADB PG 弹性存储 | ADB PG新版云原生 | ||||
并发数 | 1 | 4 | 8 | 1 | 4 | 8 |
COPY | 48MB/s | 77MB/s | 99MB/s | 45MB/s | 156MB/s | 141MB/s |
-
在单并发下新版本与存储弹性版本的性能差不多,主要在于资源都没有满;
-
在4并发下新版本的吞吐是存储弹性的2倍,原因在于使用lineitem表都定义了sort key,新版本在写入数据无需写WAL日志,另外攒批加上流水线并行相比弹性存储版本先写入,再merge,merge的时候也需要写额外的WAL有一定优势;
- 在8并发下新版本与4并发差不多,主要由于4C 4并发已经把CPU用满,所以再提升并发也没有提升。
读性能测试
全内存:使用的是TPCH sf为10的数据集,会生成10G的测试数据集。
全本地磁盘缓存:使用的是TPCH sf为500的数据集,会生成500GB的测试数据集。
一半缓存,一半OSS:使用的是TPCH sf为2000的数据集,会生成2000GB的测试数据集。(本地磁盘缓存960GB)
测试结果如下(纵轴为RT单位ms)
全内存
-
云原生版本对比老的弹性存储版本均有1倍多的性能提升,原因在于细粒度并行带来的加速效果;
- 对于TPCH这种计算密集型的作业,即使数据一半缓存,一半OSS性能也不错,sf 2000数据量是sf 500的4倍,rt增加到原来的2.8倍,主要原因在于4*4C规格的实例没有到OSS的带宽瓶颈,另外由于本身读取的预取等优化。
五 总结
-
通过存储计算分离,用户可以根据业务负载模型,轻松适配计算密集型或存储密集型,存储并按使用计费,避免存储计算一体僵化而造成的资源浪费;
-
动态的适配业务负载波峰和波谷,云原生MPP架构计算侧使用了shared-nothing架构,支持秒级的弹性伸缩能力,而共享存储使得底层存储独立不受计算的影响。这降低了用户早期的规格选型的门槛,预留了后期根据业务的动态调整灵活性;
- 在存储计算分离基础上,提供了数据共享能力,这真正打破了物理机的边界,让云上的数据真正的流动了起来。例如数据的跨实例实时共享,可支持一存多读的使用模式,打破了传统数仓实例之间数据访问需要先导入,再访问的孤岛,简化操作,提高效率,降低成本。
六 后续计划
1. 能力补齐,这块主要是补齐当前版本的一些限制,例如Primary key,索引,物化视图,补齐写入的能力;
2. 性能持续优化,主要优化缓存没有命中场景;
3. 云原生架构持续升级,这块主要是在当前存储计算分离架构下,进一步提升用户体验;
1. 存算分离往Serverless再进一步,扩缩容无感。会进一步把元数据和状态也从计算节点剥离到服务层,把segment做成无状态的,这样的好处在于扩缩容能做到用户无感,另外一个好处在于segment无状态有利于提高系统高可用能力,当前我们还是通过主备模式提供高可用,当有节点故障的时候,主备切换缓存失效性能会急剧下降,segment无状态后我们会直接将它提出集群,通过“缩容”的方式继续提高服务。
2. 应用跨实例的数据共享。此外对于分析型业务,数据规模大,以TB起步,传统数仓采用烟囱式架构,数据冗余,数据同步代价高的问题,我们希望提供跨实例的数据共享能力,重构数仓架构。
上一篇:指标需求思考 | 如何做好指标类需求建设 下一篇:如何快速构建服务发现的高可用能力