hologres基础知识一文全

1 功能特性

1.1多场景查询分析

Hologres支持行存、列存、行列共存等多种存储模式和索引类型,同时满足简单查询、复杂查询、即席查询等多样化的分析查询需求。Hologres使用大规模并行处理架构,分布式处理SQL,提高资源利用率,实现海量数据极速分析。

  • 亚秒级交互式分析
    Hologres采用可扩展的大规模并行处理(MPP)架构全并行计算,通过向量化算子发挥CPU最佳算力,基于AliORC压缩存储,面向SSD存储优化IO吞吐,支持PB级数据 - - 亚秒级交互式分析体验。

  • 在线高性能主键点查
    基于行存表的主键索引和查询引擎的短路径优化,Hologres支持每秒数十万QPS高性能在线点查、前缀扫描,支持高吞吐实时更新,相比开源系统性能提升10倍以上,可用于实时加工链路的维表关联、ID-Mapping等场景。

  • 联邦查询,数据湖加速
    Hologres无缝对接MaxCompute,支持外部表透明加速查询和元数据自动导入,相比原生MaxCompute访问加速5-10倍,支持冷热数据关联分析,同时支持MaxCompute与Hologres之间百万行每秒高速同步,支持OSS数据湖格式读写,简化数据入湖入仓。

  • 半结构数据分析
    原生支持半结构化JSON数据类型,支持JSONB列式存储压缩,支持丰富的JSON相关表达算子,使JSON数据存储和分析效率接近原生列存效率。

1.2 原生实时数仓

针对实时数仓数据更新频繁、数据模型简单和分析场景敏捷的特性,Hologres支持高并发实时写入与更新,支持事务隔离与原子性,数据写入即可查。

  • 高吞吐实时写入与更新
    Hologres与Flink、Spark等计算框架原生集成,通过内置Connector,支持高通量数据实时写入与更新,支持源表、结果表、维度表多种场景,支持多流合并等复杂操作。

  • 所见即所得的开发
    数据实时写入即可查询,支持DB、Schema、Table三级体系,支持视图View,原生支持Update/Delete/Upsert,支持关联、嵌套、窗口等丰富表达能力,原生支持半结构化JSON数据分析,支持MySQL等数据库数据整库一键入库,实时同步。

  • 全链路事件驱动
    支持表更新事件的Binlog透出能力,通过Flink消费Hologres Binlog,实现数仓层次间全链路实时开发,满足分层治理的前提下,缩短数据加工端到端延迟。

  • 实时物化视图
    支持定义实时物化视图,简化数据加工聚合等开发,数据实时写入,聚合实时更新,完善支持实时加工场景。

1.3 企业级运维能力

支持计算负载、访问权限等细粒度管控要求,提供丰富的监控和告警指标,支持计算资源弹性扩展,支持系统热升级,满足企业级安全可靠的运维需求。

  • 数据安全
    支持细粒度访问控制策略,支持BYOK数据存储加密和数据脱敏,支持数据保护伞、IP白名单,支持RAM、STS及独立账号等多种认证体系,通过PCI-DSS安全认证。支持数据备份与恢复。

  • 负载隔离
    多个计算实例组成一主多从模式,实例间共享一份存储,计算资源隔离,实现写入和读取隔离,查询和服务隔离,实现故障管理,支持故障节点快速自动恢复。无需本地盘,盘古三副本高可靠冗余存储。

  • 自运维能力
    内置查询历史、元仓表等运维诊断信息,用户可以基于查询历史和表的元数据,快速定位系统瓶颈和风险点,提升自运维能力。

1.4 生态与可扩展性

兼容PostgreSQL生态,与大数据计算引擎及大数据智能研发平台DataWorks无缝打通。无需额外学习,即可上手开发。

  • 兼容PostgreSQL生态
    Hologres兼容PostgreSQL生态,提供JDBC/ODBC接口,轻松对接第三方ETL和BI工具,包括Quick BI、DataV、Tableau、帆软等。支持GIS空间数据分析,支持Oracle函数扩展包。

  • DataWorks开发集成
    Hologres与DataWorks深度集成,提供图形化、智能化、一站式的数仓搭建和交互式分析服务工具,支持数据资产、数据血缘、数据实时同步、数据服务等企业级能力。

  • Hadoop生态集成
    支持Hive/Spark Connector,通过Hadoop平台加工的数据可以高吞吐导入Hologres,并对外提供服务。支持加速读取外部表OSS-HDFS格式存储,支持Hudi、Delta等存储格式。

  • 达摩院Proxima向量检索
    Hologres与机器学习平台PAI紧密结合,内置达摩院Proxima向量检索插件,支持在线实时特征存储、实时召回、向量检索。

2 hologres 架构

2.1 传统分布式系统

在传统的分布式系统中,常用的存储计算架构有如下三种。
在这里插入图片描述

  • Shared Disk/Storage (共享存储)
    有一个分布式的存储集群,每个计算节点像访问单机数据一样访问这个共享存储上的数据。这种架构的存储层可以比较方便的扩展,但是计算节点需要引入分布式协调机制保证数据同步和一致性,因此计算节点的可扩展性有一个上限。

  • Shared Nothing
    每个计算节点自己挂载存储,一个节点只能处理一个分片的数据,节点之间可以通信,最终有一个汇总节点对数据进行汇总。这种架构能比较方便的扩展,但是它的缺点是节点Failover需要等待数据加载完成之后才能提供服务;并且存储和计算需要同时扩容,不够灵活,扩容后,有漫长的数据Rebalance过程。

  • Storage Disaggregation(存储计算分离架构)
    存储和Shared Storage类似,有一个分布式的共享存储集群,计算层处理数据的模式和Shared Nothing类似,数据是分片的,每个Shard只处理自己所在分片的数据,每个计算节点还可以有本地缓存。

存储计算分离的架构存在以下优势。

  • 一致性问题处理简单:计算层只需要保证同一时刻有一个计算节点写入同一分片的数据。

  • 扩展更灵活:计算和存储可以分开扩展,计算不够扩计算节点,存储不够扩存储节点。这样在大促等场景上会非常灵活。计算资源不够了,马上扩容计算就好了,不需要像Shared Nothing那样做耗时耗力的数据Rebalance;也不会像Shared Nothing那样,出现单机的存储容量瓶颈。

  • 计算节点故障恢复快:计算节点发生Failover之后,数据可以按需从分布式的共享存储异步拉取。因此Failover的速度非常快。

Hologres采用的是第三种存储计算分离架构,Hologres的存储使用的是阿里自研的Pangu分布式文件系统(类似HDFS)。用户可以根据业务需求进行弹性扩缩容,轻松应对在线系统不同的流量峰值。

2.2 hologres 基础架构

在这里插入图片描述
底层用的盘古文件系统,自动3副本。
在这里插入图片描述

Hologres架构非常简单,是存储计算分离的架构,数据全部存在一个分布式文件系统中,系统架构图如下图所示:

  • 服务节点Backend真正去接收数据、存储和查询,并且能够支持数据的计算;
  • 执行引擎Frontend接收路由分发的SQL,然后生成逻辑执行计划,再通过优化器生成分布式的物理执行计划,发布到Backend做分布式的执行;
  • 接入端由LBS做相应的负载均衡任务。
  • 下图中黄色部分均部署在容器中,整个分布式系统可以做到高度容错。
  • 兼容PostgreSQL生态,在上层可以直接对接开源或者商业化的开发/BI工具,开箱即可用。

在这里插入图片描述

2.2.1 计算层

  1. 接入节点(Frontend,FE)
    Hologres接入节点,主要用于SQL的认证、解析、优化,一个实例有多个FE接入节点。在生态上兼容Postgres 11,因此用户可以使用Postgres标准语法进行开发,也可以用Postgres兼容的开发工具和BI工具直接连接Hologres。

  2. 计算HoloWorker
    HoloWorker分为执行引擎、存储引擎、调度等组件,主要负责用户任务的计算、调度。

2.1 其中执行引擎(Query Engine,QE)主要有三个。

  • HQE(Hologres Query Engine)
    Hologres自研执行引擎,采用可扩展的MPP架构全并行计算,向量化算子发挥CPU极致算力,从而实现极致的查询性能。(QE主要由HQE组成)。

  • PQE(Postgres Query Engine)
    用于兼容Postgres提供扩展能力,支持PG生态的各种扩展组件,如PostGIS,UDF(PL/JAVA,PL/SQL,PL/Python)等。部分HQE还没有支持的函数和算子,会通过PQE执行,每个版本都在持续优化中,最终目标是去掉PQE。

  • SQE(Seahawks Query Engine)
    无缝对接MaxCompute(ODPS)的执行引擎,实现对MaxCompute的本地访问,无需迁移和导入数据,就可以高性能和全兼容的访问各种MaxCompute文件格式,以及Hash/Range clustered table等复杂表,实现对PB级离线数据的交互式分析,技术原理请参见Hologres加速查询MaxCompute技术揭秘。

2.2 存储引擎Storage Engine(SE)
主要用于管理和处理数据, 包括创建、查询、更新和删除(简称 CRUD)数据等,关于存储引擎详细的技术原理请参见Hologres存储引擎技术揭秘。

2.3 Cache(缓存)
主要是结果缓存,提高查询性能。

2.4 HOS Scheduler
轻量级调度。

  1. Meta Service
    主要用于管理元数据Meta信息(包括表结构信息以及数据在Storage Engine节点上的分布情况),并将Meta信息提供给FE节点。
  2. Holo Master
    Hologres原生部署在K8s上,当某个Worker出现故障时,由K8s进行快速拉起创建一个新的Worker,保障Worker级别的可用性。在Worker内部,每个组件的可用性则由Holo Master负责,当组件出现状态不正常时,Holo Master则会快速重新拉起组件,从而恢复服务,高可用技术原理请参见Hologres高可用技术揭秘。

2.2.2 存储层

数据直接存储在Pangu File System。

与MaxCompute在存储层打通,能直接访问MaxCompute存储在盘古的数据,实现高效相互访问。

支持直接访问OSS、DLF数据,类型包含CSV、ORC、Parquet、Hudi、Delta、Meta Data等,加速数据湖探索,也可以将数据回流至OSS,降低存储成本。
在这里插入图片描述

2.3 存储类型

在这里插入图片描述

2.4 索引

在这里插入图片描述
在这里插入图片描述

2.5 执行引擎

2.5.1 执行引擎优势

Hologres的执行引擎(主要以HQE为主)是自研的执行引擎,通过与大数据领域最新技术结合,实现了对各种查询类型的高性能处理,主要具有如下优势。

  1. 分布式执行
    执行引擎是一个和存储计算分离架构配合的分布式执行模型。执行计划由异步算子组成的执行图DAG(有向无环图)表示,可以表达各种复杂查询,并且完美适配Hologres的数据存储模型,方便对接查询优化器,利用各种查询优化技术。

  2. 全异步执行
    端到端的全异步处理框架,可以避免高并发系统的瓶颈,充分利用资源,并且最大可能地避免存储计算分离系统带来的读数据延迟的影响。

  3. 向量化和列处理
    算子内部处理数据时最大可能地使用向量化执行,与存储引擎深度集成,通过灵活的执行模型,充分利用各种索引,最大化地延迟向量物化和延迟计算,避免不必要的读数据和计算。

  4. 自适应增量处理
    对常见实时数据应用查询模式进行自适应增量处理。

  5. 特定查询深度优化
    对一些特定查询模式的独特优化。

2.5.2 Query执行过程

当客户端下发一个Query后,在执行引擎中实际上会有多个worker节点,以其中的一个worker节点为例,执行过程如下图所示。
在这里插入图片描述
当客户端发起一个SQL后,执行过程如下。

  1. Frontend(FE)节点对SQL进行解析和认证,并分发至执行引擎(Query Engine)的不同执行模块。

  2. 执行引擎(Query Engine)会根据SQL的特征走不同的执行路径。

  • 如果是点查/点写的场景,会跳过优化器(Query Optimizer,QO),直接分发至后端获取数据,减少数据传送链路,从而实现更优的性能。整个执行链路也叫Fixed Plan,点查(与HBase的KV查询)、点写场景会直接走Fixed Plan。

  • 如果是OLAP查询和写入场景:首先会由优化器(Query Optimizer,QO)对SQL进行解析,生成执行计划,在执行计划中会预估出算子执行Cost、统计信息、空间裁剪等。QO会通过生成的执行计划,决定使用HQE、PQE、SQE或者Hive QE对算子进行真正的计算。
    HQE、PQE、SQE的对比介绍如下。

    • HQE(Hologres Query Engine)
      Hologres自研执行引擎,采用可扩展的MPP架构全并行计算,向量化算子发挥CPU极致算力,从而实现极致的查询性能。(QE主要由HQE组成)。
    • PQE(Postgres Query Engine)
      用于兼容Postgres提供扩展能力,支持PG生态的各种扩展组件,如PostGIS,UDF(PL/JAVA,PL/SQL,PL/Python)等。部分HQE还没有支持的函数和算子,会通过PQE执行,每个版本都在持续优化中,最终目标是去掉PQE。
    • SQE(Seahawks Query Engine)
      无缝对接MaxCompute(ODPS)的执行引擎,实现对MaxCompute的本地访问,无需迁移和导入数据,就可以高性能和全兼容的访问各种MaxCompute文件格式,以及Hash/Range clustered table等复杂表,实现对PB级离线数据的交互式分析,技术原理请参见Hologres加速查询MaxCompute技术揭秘。

https://developer.aliyun.com/article/779284?
spm=a2c4g.11186623.0.0.3a0f24d0TwmuJy&groupCode=hologres

3.执行引擎决定正确的执行计划,然后会通过存储引擎(Storage Engine,SE)进行数据获取,最后对每个Shard上的数据进行合并,返回至客户端。

3 执行引擎技术原理

Hologres作为HSAP服务分析一体化的落地最佳实践,其查询引擎是一个完全自研的执行引擎,它的核心设计目标是支持所有类型的分布式分析和服务查询,并做到极致查询性能。为了做到这一点,我们借鉴了各种分布式查询系统,包括分析型数据库,实时数仓等,吸取了各方面的优势从零开始打造出一个全新的执行引擎。
为什么要选择从零开始做一个新的查询引擎?开源的分布式分析查询系统主要有两大类:

  • 一类是传统的 Massively Parallel Processing 系统,能够支持通用的 SQL 查询,但是对实时场景支持不够好,性能不够理想。
  • 一类是 Apache Druid 和 ClickHouse这些实时数仓,是专门为实时场景设计和优化的,能够比较好地支持一些常见的单表实时查询,但是复杂查询的性能比较差。
  • 另外大数据生态圈基于 MapReduce 的引擎比较适合批处理 ETL,一般不太适合在线服务和多维分析的场景,性能也差不少。
    Hologres 执行引擎是在一个能支持复杂查询和上述高性能实时服务查询的通用架构,先首先实现了常用的实时数仓场景,深入优化并用内部 Benchmark 验证了性能和稳定性超过包括专用实时数仓的其它竞品之后,再扩展到其它复杂查询的支持。扩展的过程中,在不可避免地系统变得越来越复杂的同时,也用 Benchmark 帮助保持简单实时查询的性能没有回退。如果在已有的查询引擎上做改进,因为很多架构和设计上的选择已经定型,牵一发而动全身,就很难达到这样的效果。

Hologres执行引擎从开发到落地实践面临了非常多的挑战,但也给我们提供了机会把这个领域的各种新进展都结合利用起来,并超越已有系统做到对各种查询类型的高性能处理,其背后主要是基于以下特点:

  • 分布式执行模型:一个和存储计算分离架构配合的分布式执行模型。执行计划由异步算子组成的执行图 DAG(有向无环图) 表示,可以表达各种复杂查询,并且完美适配 Hologres 的数据存储模型,方便对接查询优化器,利用业界各种查询优化技术。
  • 全异步执行:端到端的全异步处理框架,可以避免高并发系统的瓶颈,充分利用资源,并且最大可能地避免存储计算分离系统带来的读数据延迟的影响。
  • 向量化和列处理:算子内部处理数据时最大可能地使用向量化执行,和存储引擎的深度集成,通过灵活的执行模型,充分利用各种索引,并且最大化地延迟向量物化和延迟计算,避免不必要的读数据和计算。
  • 自适应增量处理:对常见实时数据应用查询模式的自适应增量处理。
  • 特定查询深度优化:对一些查询模式的独特优化

3.1 分布式执行模型

Hologres 是能够弹性无限水平扩展数据量和计算能力的系统,需要能够支持高效的分布式查询。
Hologres 查询引擎执行的是由优化器生成的分布式执行计划。执行计划由算子组成。因为 Hologres 的一个表的数据会根据 Distribution Key 分布在多个 Shard 上,每个 Shard 内又可以包含很多 Segment,执行计划也会反映这样的结构,并分布到数据所在的节点去执行。每个Table Shard 会被加载到一个计算节点,数据会被缓存到这个节点的内存和本地存储。因为是存储计算分离的架构,如果一个节点出错,其服务的 Shard 可以被重新加载到任意一个计算节点,只是相当于清空了缓存。

例如一个比较简单的查询。

select key, count(value) as total from table1 group by key order by total desc limit 100。

如果是单机数据库,可以用这样的执行计划。如果数据和计算分布在多个节点上,就需要更复杂的执行计划。
在这里插入图片描述
在分布式表上,为了更高效地执行,尽量减少数据传输,可以把执行计划分为不同片段(Fragment)分布到相应节点执行,并且把一些操作下推来减少 Fragment 输出的数据,可能就变成这样的执行计划:
在这里插入图片描述
根据数据的特性,优化器可能会生成不同的计划。例如在某一个局部聚合并没有显著减少数据量的时候,可以省略这个算子。又例如在 Key 就是 Distribution key 的时候,可以优化为:
在这里插入图片描述
比如以下SQL

select user_name, sum(value) as total from t1 join t2 on t1.user_id = t2.user_id where … group by user_name order by total limit 100

在Hologres中可以是这样的执行计划

在这里插入图片描述
如果 Join key 和 Distribution Key 一致,可以优化为如下执行计划,减少远程数据传输。根据需要的查询合理地设置 Distribution Key,可能显著提高查询性能。
在这里插入图片描述
根据过滤条件和统计信息等等,优化器还可能生成不同的优化执行计划,比如包含动态过滤,局部聚合等等。
这样的分布式执行计划足够通用,可以表达所有的 SQL 查询和一些其它查询。执行计划和大部分 Massively Parallel Processing (MPP) 系统也比较类似,方便借鉴和集成业界的一些适用的优化。稍微独特一些的地方是很多查询计划片段的实例是和 Hologres 的存储结构对齐的,能够进行高效的分区裁剪和文件裁剪。
同时,Hologres 实现了 PostgreSQL 的 Explain 和 Explain Analyze 系列语句,可以展示文本格式的执行计划和相应的执行信息,方便用户自助了解执行计划,并针对性做出SQL优化调整。

3.2 全异步执行

高并发系统,特别是有大量 I/O 的系统,频繁地等待或者任务切换是常见的系统瓶颈。异步处理是一种已经被证明行之有效的避免这些瓶颈,并把高并发系统性能推到极致的方法。
Hologres 的整个后端,包括执行引擎、存储引擎和其它组件,统一使用 HOS(Hologres Operation System) 组件提供的异步无锁编程框架,能够最大化异步执行的效果。每个 Fragment 的实例使用 HOS 的一个 EC (逻辑调度单位),使得一个 Fragment 里的所有算子和存储引擎可以异步执行并且无锁安全访问绝大多数资源。
算子和 Fragment 都是类似这样的接口:

future<> Open(const SeekParameters& parameters, …)
future<RecordBatchPtr, bool> GetNext(…)
future<> Close(…)

除了一般异步处理的好处外,异步算子接口较好地规避了存储计算分离架构下相对较高的读数据延迟对查询性能的影响,并且对分布式查询的执行模型本身也有独特的好处。
DAG 执行引擎一般可以分为拉数据的模性(比如火山模型)和推的模型(比如很多大数据的分阶段执行模型),各有其优缺点。而 Hologres采用的异步的拉模型能够取得两种模型的好处并且避免其缺点(已经申请了专利)。举一个常见的 Hash Join 来说明:

在这里插入图片描述
火山模型可以简单做到先拉完 b 的数据构建 hash table,然后流式处理 a 的数据不用全放在内存里。但是当 a 或者 b 需要读数据的时候,简单的实现需要等待不能把 CPU 打满,需要通过提高 Fragment 的并发数或者引入复杂的 pre-fetch 机制来充分利用资源,而这些又会引入别的性能问题。
推数据的模型,比较容易做到并发读数据请求并在完成的时候触发下游处理,但是上述 Join算子的实现会比较复杂。比如 a 处理完一批数据推到 Join 算子而 b 的 hash table 还没有构建完成,这批数据就需要暂存到内存里或者盘上,或者引入反压机制。在 Fragment 的边界也会有类似问题,造成一些在拉数据模型下不需要的数据缓存。
Hologres 的算子和 Fragment 的异步拉数据模型,可以像火山模型一样简单做到按需从上游获取数据,而同时又可以像推数据模型一样简单做到读数据并发,只要向上游发出多个异步 GetNext,上游处理完成时会自然触发后续处理。异步 GetNext 的数目和时机,可以看做是天然的流控机制,可以有效做到提高 CPU 利用率并且避免不必要的数据暂存。
Hologres 已经用这个异步模型实现了一个完整的查询引擎,可以支持所有 PostgreSQL 的查询。

3.3 列处理和向量化

按列处理和向量化执行都是分析查询引擎常用的优化机制,可以大幅度提高数据处理的效率。Hologres 也不例外,在能使用向量处理的时候尽量使用。

Hologres 在内存里也采用列式存储。在内存里按列存储数据能够使用更多的向量处理。列式组织数据还有一个好处,就是对延迟计算比较友好。比如 select … where a = 1 and b = 2 …,对一批数据(一般对应存储的一个 row group),Hologres的 scan 算子输出的 a 和 b 可以是延迟读取的 a 和 b 的信息,在处理 a = 1 的时候会读取这一批的 a。如果 a=1 对这一批的所有行都不满足,这一批的 b 这一列就根本不会被读取。

但是对某些按行处理的算子,比如 Join,按列存储的数据可能会造成更多的 CPU cache miss ,带来较大的性能问题。很多查询引擎会在不同的点引入按列存储和按行存储的转换,但是频繁的转换本身会带来不小的开销,而且列转行会造成上述延迟读取列被不必要地读取,还有一些其它的性能问题。

3.4 自适应增量处理

很多实时数据应用经常会对一个查询用不同的时间段反复执行。比如一个监控指标页面打开后,会定期执行 select avg(v1) from metrics where d1 = x and d2 = y and ts >= ‘2020-11-11 00:00:00’ and ts < ‘2020-11-11 03:01:05’ and … group by d3 … 这样的查询,下一次会改成 ts < ‘2020-11-11 00:03:10’,再下一次 ts < ‘2020-11-11 00:03:15’。

流计算或者增量计算可以对这种查询进行非常高效的处理。但是对这种用户可以随意生成的交互式查询,通常不可能对所有组合都配置流计算或者增量计算任务。如果每次都简单执行查询,又可能有大量的重复计算造成资源浪费和性能不理想。

Hologres充分利用存储引擎和计算引擎的深度集成和列式存储大部分数据在只读文件中的特性,在能提供包含最新写入数据的查询结果的同时尽量避免重复计算,对这种类型的查询能够显著提升性能和减少资源使用。

3.5 针对特定查询模式的深度优化

Hologres 对一些特定查询模式有独特的优化。这里以Filter Aggregate 优化为例子。

很多数据应用都有开放列的需求,相当于可以动态添加逻辑列而不用改 Table Schema。比如有一列是多值列 tags(Postgres 可以用 Array 类型)里面存了’{c1:v1, c2:u1}’ 这样的多个逻辑列的值。查询的时候,如果使用普通列,一类常见的查询是

– Q1:
select c1, sum(x) from t1 where c1 in (v1, v2, v3) and name = ‘abc’ group by c1

使用开放列后,这样的查询会转变为

– Q2:
select unnest(tags), sum(x) from t1 where name = ‘abc’ and tags && ARRAY[‘c1:v1’, ‘c1:v2’, c1:v3’]
group by unnest(tags)
having unnest(tags) in (‘c1:v1’, ‘c1:v2’, c1:v3’)

这种查询,Hologres 可以利用位图索引快速计算过滤条件得到相关的行,但是之后从多值列里面取出相关数据操作不能使用向量处理,性能不能达到最优。经过调研,可以把查询的执行转换为

Q3:
select ‘c1:v1’, sum(x) from t1 where tags && ARRAY[‘c1:v1’]
UNION ALL
select ‘c1:v2’, sum(x) from t1 where tags && ARRAY[‘c1:v2’]
UNION ALL

这样每个 UNION ALL 分支可以只读取 name 和 tags 的位图索引计算过滤条件,然后用 x 列的数据和过滤条件进行向量计算 SUM_IF 即可得出想要的结果。这样的问题是,每个分支都要过一遍 t1,读取 x 列以及 name 列的位图索引,带来重复计算。最后引入了一个 filter aggregate 的特殊算子来把这类常用查询优化到极致性能,可以只过一遍 t1 并且去掉重复操作,只用向量计算即可得到结果,不需要读取 tags 列的数据。在一个几十 TB的表上实测性能提升 3 倍以上。
类似的优化,Hologres 的执行引擎都会尽量抽象为比较通用的算子,可以适用于更多场景。Filter Aggregate 算子也是 Hologres 申请的专利之一。

3.6 总结

Hologres 执行引擎在一个架构里集中了相关分布式查询系统的几乎所有最高效的优化方式(包括各种类型的索引)并作出了特有的改进。通过和存储引擎深度整合,能充分发挥异步模型的优势,并高效利用各种类型的索引来加速查询。所有这些加起来,带来了超越已有系统的性能,并在阿里巴巴双 11 的数据规模下通过了实战的考验,(2020年双11顶住了5.96亿/秒的实时数据洪峰,基于万亿级数据对外提供多维分析和服务,99.99%的查询可以在80ms以内返回结果),对外高并发高性能地提供分布式 HSAP 查询服务。

4 Hologres高可用架构设计

4.1 计算存储分离架构提高系统扩展灵活性

实时数仓技术不管是面向分析型场景还是服务型场景,所处理的数据量级、场景复杂度都远比传统数据库要高,尤其是互联网、电商等行业,活动促销多,大促和日常所处理的流量完全不一样,这就非常考验系统的资源水平扩展能力。

在传统的分布式系统中,常用的存储计算架构有如下三种:

  1. Shared Disk/Storage (共享存储):有一个分布式的存储集群,每个计算节点像访问单机数据一样访问这个共享存储上的数据;这种架构的存储层可以比较方便的扩展,但是计算节点需要引入分布式协调机制保证数据同步和一致性,因此计算节点的可扩展性有一个上限。
  2. Shared Nothing:每个计算节点自己挂载存储,一个节点只能处理一个分片的数据,节点之间可以通信,最终有一个汇总节点把数据进行汇总。这种架构能比较方便的扩展,但是它的缺点是节点failover需要等待数据加载完成之后才能提供服务;并且存储和计算需要同时扩容,不够灵活。扩容后,有漫长的数据rebalance过程。
  3. Storage Disaggregation(存储计算分离架构):存储和Shared Storage类似,有一个分布式的共享存储集群,计算层处理数据的模式和Shared Nothing类似,数据是分片的,每个shard只处理自己所在分片的数据,每个计算节点还可以有本地缓存。

在这里插入图片描述

这种存储计算分离的架构好处在于:

  • 一致性处理简单:计算层只需要保证同一时刻只有一个计算节点写入同一分片的数据。
  • 扩展性更灵活:计算和存储可以分开扩展,计算不够扩计算节点,存储不够扩存储节点。这样在大促等场景上会非常灵活。计算资源不够了,马上扩容计算就好了,不需要像Shared Nothing那样做耗时耗力的数据rebalance;也不会出现Shared Nothing那样,出现单机的存储容量瓶颈。
  • 计算节点故障恢复快:计算节点发生failover之后,数据可以按需从分布式的共享存储异步拉取。因此,failover的速度非常快。

在架构上,Hologres采用的是第3种存储计算分离架构,Hologres的存储使用的是阿里自研的Pangu分布式文件系统(类似HDFS)。用户可以根据业务需求进行弹性扩缩容,轻松应对在线系统不同的流量峰值。

4.2 多形态Replication解决数据读写分离

Replication(复制)是实现高可用的必备技术,通过不同形态的Replication设计,快速将数据在节点间、集群间进行复制,实现隔离和SLA保障。
Hologers同时支持了逻辑Replication和物理Replication,下面将会针对Hologres的Replication功能做具体介绍。

1)基于Binlog的逻辑Replication
类似于传统数据库MySQL中的Binlog概念,在Hologres中,Binlog用来记录数据库中表数据的修改记录,比如Insert/Delete/Update的操作,主要应用场景包括:

  1. 数据实时复制、同步场景,典型的场景就是把一张Hologres的行存表复制成一张列存表,行存支持点查点写,列存支持多维分析型需求,同步的逻辑通常由Flink支撑。这个是Hologres在V1.1版本之前的一种典型用法。在Hologres 1.1中支持了行列共存表后,可以一张表满足行存和列存两种需求。
  2. 实现事件的全链路驱动,通过Flink消费Hologres Binlog,实现事件驱动的加工开发,完成ODS向DWD,DWD向DWS等的全实时加工作业。

在Hologres中,逻辑Replication依赖Binlog实现,发生变更的表作为Publication发布变更事件,加工逻辑处理后写入Subscription侧。用户可以订阅一个表的Binlog转成Record,写入到另外一张表里,实现逻辑上的复制功能。这种做法可以天然做到不同Workload的隔离,但是它有两个问题:它是一个最终一致性的模型,很难做到强一致;另一个是它消耗了两份资源,使用两份存储,并且写入链路的资源也得有两份。
在这里插入图片描述因此Hologres也实现了物理Replication。

2)物理Replication
在Hologres中,物理Replication是基于WAL log的复制,我们可以把存储引擎看成是状态机,WAL log是这个状态机的输入。当我们要对某个Shard做Replication的时候,我们会起一个Follower Shard读取当前最新的WAL log进行回放(replay),同时Leader Shard又有新的WAL产生,Follower Shard会从Leader Shard订阅最新的WAL,不断的回放,从而达到和Leader Shard一致的状态。如果需要保证Follower Shard上的可见性,我们可以在读请求中加一个强一致的选项,问一下Follower Shard和Leader Shard之间WAL log的回放差距,等补齐差距后再返回查询结果。

Follower Shard回放WAL log的过程中,对WAL log中指向的数据文件可以进行复制。也可以只进行引用,其中复制的方式称为非共享存储模式,引用的方式称为共享存储模式。

基于此,Hologres实现了3种形态的物理Replication:

  1. 单实例多副本:一个实例内采用Shard级多副本机制,可用来实现跨进程高可用,读写分离,同时因为副本可动态增加,能轻松支持高并发的读。
  2. 多实例读写分离:不同的实例之间共享一份存储,实现计算跨机房高可用,通常用于读写分离场景,并支持高并发的读场景
  3. 多实例容灾:多个实例之间不共享存储,实现计算和存储服务的跨机房高可用,支持读写分离,读的高并发,版本的热升级和存储系统的迁移等功能

• 单实例多副本
Hologres数据分片单元是Shard,Shard可以有多个副本,但是存储只有一份。平时,查询流量可以被各个副本均摊,从而实现高QPS。当某一个副本failover以后,流量可以快速被导到其他副本。并且Shard的故障恢复非常轻量,只需回放部分WAL,没有数据的复制。基于单实例内多副本机制,可以很方便的实现计算的可扩展性,并快速解决物理机单机failover问题。
应用场景:

  1. 单实例内的查询高可用:当一个Shard所在Worker发生故障时,可以通过前端阶段的重试操作,将请求重定向到副本Shard所在Worker,从而应用异常无感知。
  2. 通过负载均摊,实现更高吞吐:同一份数据由多个Shard共同对外提供服务,不同的查询路由到不同的Shard所在节点,从而实现负载在多个Shard间的均衡,QPS可以显著提升,对于每次查询只访问确定Shard的场景(例如点查场景)提升明显。
  3. 机器故障快速Failover:从Hologres V1.1版本开始,采用全新恢复机制,Shard恢复速度在一分钟以内,可用性进一步增强。

在这里插入图片描述
• 多实例读写分离
和单实例内多副本的Replication相比,跨实例的Replication实现了Meta的物理复制。
Hologres 在V1.1版本,支持了共享存储的多实例部署方案。在该方案中,主实例具备完整能力,数据可读可写,权限、系统参数可配置,而子实例处于只读状态,所有的变更都通过主实例完成,实例之间共享一份数据存储,实例间数据异步实时同步。
应用场景:

  1. 读写分离:这个方案实现了完整的读写分离功能,保障不同业务场景的SLA,在高吞吐的数据写入和复杂的架构作业、OLAP、AdHoc查询、线上服务等场景中,负载之间物理上完全隔离,不会因写入产生查询的抖动。
  2. 多类型负载细粒度资源分配:一个主实例可以配置多个只读从实例,实例之间可以根据业务情况配置不同规格,例如使用256Core作为写入和加工实例,512Core作为OLAP只读实例,128Core作为在线Serving实例,32Core作为开发测试实例。

在这里插入图片描述
• 多实例跨城容灾
多实例非共享存储的Replication,可以理解为传统意义上的灾备功能,支持容灾,异地多活,并实现读写分离和读的高并发,同样也可以基于多个实例实现读的高可用。除此之外,还可以进行版本热升级,存储系统迁移。
应用场景:

  1. 灾备:在不同的Region,部署有不同的存储集群(Pangu),数据在同步后会分别保存在不同的存储集群上,当发生某个Region不可用时,异地备份的实例可以继续对外提供服务。
  2. 集群迁移:机房的容量空间总是有限,经常会发生因为不可控原因,需要将实例从某个机房迁移到其他机房,甚至从某个Region迁移到其他Region,用户希望迁移过程尽可能是对业务无感的。因此可以通过Replication机制,实现实例状态在集群间的迁移。
  3. 热升级:热升级过程中,需要业务服务能力不中断,属于高速公路上换发动机的需求,因此需要系统具备某种类似“滚动”升级的能力。通过Replication机制,可以先克隆出一个实例,在新的实例上完成软件版本的升级,再将相关的网络路由等配置接入到新的实例,从而完成无需停机的热升级。
    在这里插入图片描述

4.3 调度系统提高节点failover快速恢复能力

分布式环境failover是不可避免的,当failover发生时,需要高效的检测,快速的恢复,这就是调度的范畴。

一个Hologres实例有多个HoloWorker,当某一个HoloWorker发生意外、宕机、failover时,通过Hologres的调度系统,可以快速检测到节点异常,并将异常节点的Service如Frontend、Coordinator、Shard快速调度到另外一个健康的HoloWorker,同时SLB将会将流量导流到新的健康Frontend上。

调度分为计算单元的调度和流量的调度:
1)计算单元的调度
计算单元的调度分为Pod的调度、Pod内子进程调度以及Actor的调度

  • Pod的调度利用了K8S的能力,Hologres中被K8S调度的单元是HoloWorker;
  • Pod内子进程调度以及Actor的调度是Hologres分布式调度模块HoloFlow提供的能力。
    Hologres中两种类型的计算单元需要被调度,一类是以子进程模式提供Service,例如Frontend;另一类是以Actor模式提供的Service,例如某一个分片的数据服务Shard。HoloFlow提供了这两类服务的健康检测以及调度的能力。

2)流量的调度
流量的调度又分为外部流量和内部流量的调度。

  • 外部流量即入口流量,这部分调度是SLB提供的能力,Hologres会定时监测Frontend的健康状态,一旦某个Frontend不健康了,流量就会从SLB上摘除。
  • 内部流量Hologres提供了内部的健康检测和服务发现机制,例如StoreMaster提供了Shard的健康检测和服务发现机制,一旦某个Shard不健康,Coordinator就会把流量导到这个Shard健康的Replica上。

通过Hologres的调度系统,实现了节点故障、Failover的快速检测以及自动调度恢复能力,满足业务的稳定性需求,提高系统可用性。
在这里插入图片描述

4.4 多层次隔离轻松应对不同业务SLA

随着实时数仓在生产系统越来越广泛的应用,不同的业务也有着不同的SLA诉求,比如双11时,老板和运营对交易数据的查询需求比较高,物流端又希望物流订单能实时高效刷新,开发又希望数据能快速写入,不要影响后面的数据查询和分析…

具体到Hologres,一个实例支持不同的Workload,包括点查点写,批量导入,交互式分析等。那么不同Workload的SLA需要被保障,例如批量导入不能影响交互式分析的延时,交互式分析的请求不能影响实时写入的实效性等;Hologres也支持多租户同时使用,不同租户之间也不能相互影响;

以上描述的场景都是隔离的范畴,相对来说隔离级别越高,成本越大,资源利用率越低。在进程内部实现低成本可用的隔离是一个很有技术挑战的事情。

Hologres实现了多个层次的隔离手段。如下图是上面介绍的Replication(复制)和隔离的关系,复制本质上是在不同的机器/容器中服务同一份数据(或其复本),所以本质上是一种物理隔离。在物理隔离外,Hologres还支持资源组隔离、调度组和(SchedulingGroup)隔离,用户可以在成本和SLA上做tradeoff,满足不同用户对隔离的需求。

在这里插入图片描述
1)物理机和容器隔离
在物理机和容器隔离上,Hologers是通过k8s来部署,利用k8s的Node Selector/Affinity以及Taints/Tolerations等功能,可以比较方便的实现实例和实例间容器的隔离。对于一些对SLA要求非常高的客户,我们还可以对机器单独打标,只允许某一个实例的容器调度到打标的机器上,从而实现机器级别的隔离,防止其他实例的干扰。

2)资源组隔离
在Hologres中,多租户的隔离需求是通过资源组来实现的。Hologres的资源组隔离本质上是线程级别的隔离。实例内的Worker可以按照CPU、内存、IO划分为不同的资源组。不同的用户加入到不同的资源组,限制每个用户使用的资源上限,以保证用户之间的作业互不影响。
例如资源组(1)有50%的资源,资源组(2)有30%的资源,资源组(3)有20%的资源。我们把用户A绑定的资源组(一)上,用户B绑定在资源组(2)上,用户C和D绑定到资源组(3)上。这样用户A,B.C发起的请求就会分别调度到不同的资源组。

通过资源组的隔离,实现实例内的资源隔离。这种隔离的优点是能够在一个实例内实现不同用户的隔离,保证用户间的作业不相互影响。这种隔离是一种软隔离,在隔离效果上是不如基于replication的物理隔离的。所以资源组隔离更适合不同用户的OLAP查询隔离等场景,而基于replication的物理隔离更适合线上服务。
在这里插入图片描述
3)SchedulingGroup隔离
通常来说,2)中的线程级别隔离模型会有如下问题:

  • 在操作系统层面:线程切换是一个不小的开销。为了把因为等待IO而空闲的CPU利用起来,需要把很多CPU浪费在线程切换上。测试发现,严重的时候线程切换能浪费掉一半以上的CPU;
  • 线程的数目很难掌握:不同的query、不同的数据、不同的cache命中率,被IO阻塞的可能性差异会非常大,以至于需要的线程数差别非常大。这种情况下,使用固定线程数目的线程池是很难受的。线程多了会引起多余的切换,加剧切换的开销;线程少了则可能没法把空闲的CPU都利用起来。而相比于线程切换,线程的创建和销毁会带来更大的开销,所以想要通过动态创建线程来保持恰当的线程数,这也是不太可能的;

理想的方案是能有一种轻量级的调度单元,功能类似于线程,但是创建/销毁和调度/切换的开销要小得多。这样的话:

  • 我们可以根据业务逻辑的需要,创建足够多的“线程”去并发使用CPU,而不必担心切换的开销大、或者CPU用不满;
  • 当需要业务逻辑需要使用CPU时,直接根据并发度的需要去创建N个这样的“线程”,用完即销毁。这样就能使业务逻辑灵活控制任务的并行度,不必受制于底层框架;

根据上面的设计理念,Hologres在自研调度系统HOS中,通过一个轻量级调度单元EC来实现。
SchedulingGroup隔离利用了HOS EC调度的能力,同一个Query有多个EC执行,这些EC可以被归类到一个SchedulingGroup,不同的SchedulingGroup可以用公平的策略瓜分时间片。
SchedulingGroup隔离保证了当系统中同时跑一个大Query(分析型)和一个小Query(点查)的时候,小Query不至于因为抢不到CPU被大Query block住。SchedulingGroup隔离本质上是协程级别的隔离,是Hologres的核心竞争力之一。

在这里插入图片描述

5 Hologres存储引擎

5.1背景介绍

MaxCompute 交互式分析(Hologres)是阿里云自研开发的HSAP(Hybrid Serving/Analytical Processing)服务/分析一体化系统 ,融合了实时服务和分析大数据的场景,全面兼容PostgreSQL协议并与大数据生态无缝打通。它的出现简化了业务的架构,与此同时为业务提供实时做出决策的能力,让大数据发挥出更大的商业价值。关于架构更详细的介绍,请看文末VLDB论文 。

跟传统的大数据和OLAP系统相比,HSAP系统面临下面的挑战:

  • 高并发的混合工作负载:HSAP系统需要面对远远超出传统的OLAP系统的并发查询。在实践中,数据服务的并发远远超出OLAP的查询。比如说,我们在现实的应用中见到数据服务需要处理高达每秒钟数千万个查询,这比OLAP查询的并发高出了5个数量级。同时,和OLAP查询相比,数据服务型查询对延迟有着更加苛刻的要求。复杂的混合查询负载对系统的延迟和吞吐有着非常不同的取舍。如何在高效地利用系统的资源同时处理好这些非常不一样的查询,并且保证每个查询的SLO是个巨大的挑战。
  • 高吞吐实时数据导入:在处理高并发的查询负载的同时,HSAP系统还需要处理海量的实时数据导入。从传统的OLTP同步过来的数据只是这其中的一小部分,其他还有大量的数据来自日志等没有强事务语意的系统。实时导入的数据量远远超过了传统的HTAP或者OLAP系统。和传统的OLAP系统的另外一个区别是对数据的实时性有着很高的要求,导入的数据需要在秒级甚至亚秒级可见,这样才能保证我们服务和分析结果的时效性。
  • 弹性和可扩展性:数据导入和查询负载可能会有突发的高峰,这对HSAP系统提出了很高的弹性和可扩展性的要求。在现实的应用中,我们注意到数据导入峰值能达到是平均的2.5倍,查询的峰值可能达到平均的3倍。数据导入和查询的峰值可能不一定同时出现,这也需要系统有根据不同的峰值做迅速调整的能力。

基于上诉背景,我们自研了一款存储引擎(Storage Engine),主要负责管理和处理数据, 包括创建,查询,更新,和删除(简称 CRUD)数据的方法。存储引擎的设计和实现提供了HSAP场景所需要的高吞吐,高并发,低延迟,弹性化,可扩展性的能力。根据阿里集团业务和云上客户的需求,我们不断创新和打磨,发展到今天,能支持单表PB级存储,并完美支撑2020年天猫双11核心场景千亿个级别的点查询和千万个级别的实时复杂查询 。

下面,我们将会对Hologres底层的存储引擎做详细的介绍,并介绍存储引擎落地Hologres的具体实现原理和技术亮点。

5.2 数据模型

Hologres存储引擎的基本抽象是分布式的表,为了让系统可扩展,我们需要把表切分为分片(Shard)。 为了更高效地支持JOIN以及多表更新等场景,用户可能需要把几个相关的表存放在一起,为此Hologres引入了表组(Table Group)的概念。分片策略完全一样的一组表就构成了一个表组,同一个表组的所有表有同样数量的分片。用户可以通过“shard_count"来指定表的分片数,通过“distribution_key"来指定分片列。目前我们只支持Hash的分片方式。

表的数据存储格式分为两类,一类是行存表,一类是列存表,格式可以通过“orientation"来指定。

每张表里的记录都有一定的存储顺序,用户可以通过“clustering_key"来指定。如果没有指定排序列,存储引擎会按照插入的顺序自动排序。选择合适的排序列能够大大优化一些查询的性能。

表还可以支持多种索引,目前我们支持了字典索引和位图索引。用户可以通过“dictionary_encoding_columns"和“bitmap_columns"来指定需要索引的列。

下面是一个示例:
在这里插入图片描述
这个例子建了LINEITEM 和 ORDERS两个表,由于LINEITEM表还指定了主键(PRIMARY KEY),存储引擎会自动建立索引来保证主键的唯一。用户通过指定“colocate_with“把这两个表放到了同一个表组。这个表组被分成24个分片(由shard_count指定)。 LINEITEM将根据L_ORDERKEY的数据值来分片,而ORDERS将根据O_ORDERKEY的数据值来分片。LINEITEM的L_SHIPINSTRUCT以及ORDERS的O_ORDERSTATUS字段将会创建字典。LINEITEM的L_ORDERKEY, L_LINENUMBER, L_SHIPINSTRUCT字段以及ORDERS的O_ORDERKEY,O_CUSTKEY,O_ORDERSTATUS字段将会建立位图索引。

5.3 存储引擎架构

5.3.1 总体架构

在这里插入图片描述
每个分片(Table Group Shard, 简称Shard)构成了一个存储管理和恢复的单元 (Recovery Unit)。上图显示了一个分片的基本架构。一个分片由多个tablet组成,这些tablet会共享一个日志(Write-Ahead Log,WAL)。存储引擎用了Log-Structured Merge (LSM)的技术,所有的新数据都是以append-only的形式插入的。 数据先写到tablet所在的内存表 (MemTable),积累到一定规模后写入到文件中。当一个数据文件关闭后,里面的内容就不会变了。新的数据以及后续的更新都会写到新的文件。 与传统数据库的B±tree数据结构相比,LSM减少了随机IO,大幅的提高了写的性能。

当写操作不断进来,每个tablet里会积累出很多文件。当一个tablet里小文件积累到一定数量时,存储引擎会在后台把小文件合并起来 (Compaction),这样系统就不需要同时打开很多文件,能减少使用系统资源,更重要的是合并后, 文件减少了,提高了读的性能。

在DML的功能上,存储引擎提供了单条或者批量的创建,查询,更新,和删除(CRUD操作)访问方法的接口,查询引擎可以通过这些接口访问存储的数据。

5.3.2 存储引擎组件

下面是存储引擎几个重要的的组件:

  • WAL 和 WAL Manager
    WAL Manager是来管理日志文件的。存储引擎用预写式日志(WAL) 来保证数据的原子性和持久性。当CUD操作发生时,存储引擎先写WAL,再写到对应tablet的MemTable中,等到MemTable积累到一定的规模或者到了一定的时间,就会把这个MemTable切换为不可更改的flushing MemTable, 并新开一个 MemTable接收新的写入请求。 而这个不可更改的flushing MemTable就可以刷磁盘,变成不可更改的文件; 当不可更改的文件生成后,数据就可以算持久化。 当系统发生错误崩溃后,系统重启时会去WAL读日志,恢复还没有持久化的数据。 只有当一个日志文件对应的数据都持久化后,WAL Manager才会把这个日志文件删除。

这里额外多说一点,flink的增量checkpoint就是采用的这种方式做的 statebackend的更新,只不过那边是 memtable sstable ,不过过程都是一样的,预写数据到memtable,然后 用memtable去做数据的刷磁盘。

  • 文件存储
    每个tablet会把数据存在一组文件中,这些文件是存在DFS里 (阿里巴巴盘古或者Apache HDFS )。 行存文件的存储方式是Sorted String Table(SST) 格式。 列存文件支持两种存储格式: 一种是类似PAX的自研格式, 另外一种是改进版的Apache ORC格式 (在AliORC的基础上针对Hologres的场景做了很多优化)。 这两种列存格式都针对文件扫描的场景做了优化。

  • Block Cache (Read Cache)
    为了避免每次读数据都用IO到文件中取,存储引擎通过BlockCache把常用和最近用的数据放在内存中,减少不必要的IO,加快读的性能。在同一个节点内,所有的Shard共享一个Block Cache。 Block Cache有两种淘汰策略: LRU (Least Recently Used,最近最少使用) 和 LFU (Least Frequently Used, 最近不常用)。 顾名思义,LRU算法是首先淘汰最长时间未被使用的Block,而LFU是先淘汰一定时间内被访问次数最少的Block。

5.3.3 读写原理

Hologres支持两种类型的写入:单分片写入和分布式批量写入。两种类型的写入都是原子的(Atomic Write),即写入或回滚。单分片写入一次更新一个Shard,但是需要支持极高的写入频率。另一方面,分布式批写用于将大量数据作为单个事务写到多个Shard中的场景,并且通常以低得多的频率执行。
在这里插入图片描述

  • 单分片写入
    如上图所示,WAL管理器在接收到单分片写请求后,(1)为写请求分配一条Log Sequence Number (LSN),这个LSN是由时间戳和递增的序号组成,并且(2)创建一条新的日志,并在文件系统中的持久化这条日志。这条日志包含了恢复写操作所需的信息。在完全保留这条日志后,才向tablet提交写入。之后,(3)我们会在相应tablet的内存表(MemTable) 中执行这个写操作,并使其对新的读请求可见。值得注意的是,不同tablet上的更新可以并行化。当一个MemTable满了以后,(4)将其刷新到文件系统中,并初始化一个新的MemTable。最后,(5)将多个分片文件在后台异步合并(Compaction)。在合并或MemTable刷新结束时,管理tablet的元数据文件将相应更新。
  • 分布式批量写入
    接收到写入请求的前台节点会将写请求分发到所有相关的分片。这些分片通过两阶段提交机制(Two Phase Commit) 来保证分布式批量写入的写入原子性。
  • 多版本读
    Hologres支持在tablet中多版本读取数据。读请求的一致性是read-your-writes,即客户端始终能看到自己最新提交的写操作。每个读取请求都包含一个读取时间戳,用于构造读的snapshot LSN。如果有一行数据的LSN大于snapshot LSN的记录, 这行数据就会被过滤掉, 因为他是在读的snapshot产生后才被插入到这个tablet的。

5.4 Hologres存储引擎技术亮点

5.4.1 存储计算分离

存储引擎采取存储计算分离的架构,所有的数据文件存在一个分布式文件系统(DFS, 例如阿里巴巴盘古或者Apache HDFS)的里面。当查询负载变大需要更多的计算资源的时候可以单独扩展计算资源; 当数据量快速增长的时候可以快速单独扩展存储资源。计算节点和存储节点可以独立扩展的架构保证了不需要等待数据的拷贝或者移动就能快速扩展资源; 而且,可以利用DFS存多副本的机制保证数据的高可用性。 这种架构不但极大地简化了运维,而且为系统的稳定性提供了很大的保障。

5.4.2 异步执行流程

存储引擎采用了基于事件触发, 非阻塞的纯异步执行架构, 这样能够充分发挥现代CPU多core的处理能力,提高了吞吐量, 支持高并发的写入和查询。这种架构得益于HOS(HoloOS) 框架,HOS在提供高效的异步执行和并发能力的同时,还能自动地做CPU的负载均衡提升系统的利用率。

5.4.3 统一的存储

在HSAP场景下,有两类查询模式,一类是简单的点查询(数据服务Serving类场景),另一类是扫描大量数据的复杂查询(分析Analytical类场景)。 当然,也有很多查询是介于两者之间的。这两种查询模式对数据存储提出了不同的要求。行存能够比较高效地支持点查询,而列存在支持大量扫描的查询上有明显的优势。
为了能够支持各种查询模式,统一的实时存储是非常重要的。存储引擎支持行存和列存的存储格式。根据用户的需求,一个tablet可以是行存的存储格式 (适用于Serving的场景); 也可以是列存的存储格式(适用于Analytical的场景)。 比如,在一个典型HSAP的场景,很多用户会把数据存在列存的存储格式下,便于大规模扫描做分析;与此同时,数据的索引存在行存的存储格式下,便于点查。并通过定义primary key constraint (我们是用行存来实现的)用来防止数据重复·。不管底层用的是行存还是列存,读写的接口是一样的,用户没有感知,只在建表的时候指定即可。

5.4.4 读写隔离

存储引擎采用了snapshot read的语意,读数据时采用读开始时的数据状态,不需要数据锁,读操作不会被写操作block住; 当有新的写操作进来的时候,因为写操作是append-only,所有写操作也不会被读操作block住。这样可以很好的支持HSAP的高并发混合工作负载场景。

5.4.5 丰富的索引

存储引擎提供了多种索引类型,用于提升查询的效率。一个表可以支持clustered index 和 non-clustered index这两类索引。一个表只能有一个clustered index, 它包含表里所有的列。一个表可以有多个non-clustered indices。在non-clustered indexes里,除了排序用的non-clustered index key外,还有用来找到全行数据的Row Identifier (RID)。 如果clustered index存在, 而且是独特的,clustered index key就是RID; 否则存储引擎会产生一个独特的RID。 为了提高查询的效率,在non-clustered index中还可以有其他的列, 这样在某些查询时,扫一个索引就可以拿到所有的列的值了 (covering index)。

在数据文件内部,存储引擎支持了字典和位图索引。字典可以用来提高处理字符串的效率和提高数据的压缩比,位图索引可以帮助高效地过滤掉不需要的记录。

实时数仓选型与建设

在这里插入图片描述
在这里插入图片描述

这里分享新东方的实时数仓建设架构
实时数仓是在离线数仓的基础上,基于Lambda架构构建,离线和实时同时进行建设。有关Lambda的,参阅:Lambda architecture

在这里插入图片描述

6 Hologres技术揭秘: JSON半结构化数据的极致分析性能

6.1 什么是半结构化数据

介绍什么是半结构数据之前,我们首先明确下什么是结构化数据。结构化数据可以理解成在关系型数据库(RDBMS)中的一张表,每张表都有明确严格的结构定义,比如包含哪些列,每列的数据类型是怎样的,存储的数据必须严格遵循表结构的定义。

相对应的,半结构化数据就是非固定结构的、经常变化的,且一般是自描述的,数据的结构和内容混杂在一起,最典型的例子就是JSON格式数据。JSON有标准的格式定义,其主要由对象(Object)和数组构成(Array),对象中存储的是键值对,其中键只能是字符串,值可以是字符串、数组、布尔值、Null值、对象或者数组,数组中可以存放任意多个值。

以下就是一个简单的JSON实例,相信大家都很熟悉:
{“user_name”: “Adam”, “age”: 18, “phone_number”: [123456, 567890]}

Hologres当前正是通过支持JSON数据类型来提供半结构化数据的能力,为了兼容Postgres生态,我们支持Postgres的JSON/JSONB这两种原生类型,其中JSON类型实际以TEXT格式进行存储,而JSONB类型存储的是解析过后的二进制,因为查询时不需要再解析,所以JSONB在处理时会快很多,下文提到的Hologres半结构化数据方案的很多内部优化都是依托JSONB类型完成的。

半结构化数据得益于其本身的易用性以及强大的表达能力,使得半结构化数据的使用场景非常广泛。
对于数仓来说,每当上游的数据格式有变更时,比如变更数据类型、增删字段,数仓中的强Schema格式的表,必须进行相应的表结构演进(Schema Evoluation)来适配上游的数据,比如需要执行DDL进行加列或者删列,甚至中间的实时数据ETL作业也需要进行适配改动并重新上线。

在有频繁Schema Evoluation的场景的时候,如何保证数据的质量是个很大的挑战,同时维护和管理表结构,对于数据开发人员来说也是一项琐碎且麻烦的工作。
而半结构化数据则天然支持Schema Evoluation,上游业务的变更,只需要在JSON列数据中进行增删相应的字段,无需对数仓中的表做任何DDL就能完成,也能对中间的ETL作业做到透明,这样就能大大降低维护和管理表结构的成本。

6.2 传统数仓的半结构化数据解决方案

数仓在处理半结构化数据的时候,衡量一个解决方案好坏的核心考量主要有两点:

  1. 能否保持半结构化数据的易用性和灵活性
  2. 能否实现高效的查询性能
    而传统的解决方案常常是顾此失彼,没法做到“熊掌”与“鱼”的兼得。常见的JSON数据处理方式有2种:
    以下方案都以JSON数据为例,假设我们有如下JSON数据:
    {“user_id”:1001, “user_name”: “Adam”, “gender”: “Male”, “age”: 16}
    {“user_id”:1002, “user_name”: “Bob”, “gender”: “Male”, “age”: 41}
    {“user_id”:1003, “user_name”: “Clair”, “gender”: “Female”, “age”: 21}

方案1: 数仓直接存储原始JSON数据
一种最直观的方案就是将原始JSON数据存成单独的一列,以Hive为例:
在这里插入图片描述
在存储层,这张Hive表的数据也是以一个完整的JSON值作为最小的存储粒度在磁盘上连续存储:在这里插入图片描述
之后使用相关的JSON函数进行查询,比如查询所有年龄大于20的用户数:
SELECT COUNT(1) FROM tbl WHERE cast(get_json_object(json_data, ‘$.age’) as int) > 20;
抽象成下面的流程:
上游直接写入JSON类型到Hologres,中间不经过处理,应用层查询时,再去解析需要的数据。
在这里插入图片描述
这种处理方式:
• 优点是:JSON则天然支持Schema Evoluation,上游业务的变更,只需要在JSON列数据中进行增删相应的字段,无需对数仓中的表做任何DDL就能完成,也能对中间的ETL作业做到透明,最大程度地保留了半结构化数据的易用性和灵活性,能大大降低维护和管理表结构的成本。
• 缺点是:应用端查询时需要选择合适的处理函数和方法,才能解析到需要的数据,开发较为复杂,如果JSON较复杂,同时查询性能会有退化,因为每次JSON列的数据参与计算的时候,都需要对JSON数据完整的解析一遍,比如需要抽取出整个JSON中某个字段,那么查询引擎执行的时候就要读出每一行JSON,解析一遍,取出需要的字段再返回。这中间会涉及大量的IO和计算,而需要的可能只是JSON数据成百上千字段当中的一个字段,这中间的大量IO和计算都是浪费的。

方案2: 加工成宽表
既然JSON查询时的解析开销很大,那就把解析前置在数据加工链路中,于是另外一种做法就是把JSON拍平成了一张宽表:在这里插入图片描述
相应的抽象出来的流程如下:
上游是JSON格式,在导入时,将JSON进行解析,比如常见的通过Flink的JSON_VALUE函数解析,然后打宽成一张大宽表,再写入至Hologres,对于上层应用,直接查询Hologres中已经解析好的列。
在这里插入图片描述
对于这种处理方法:
• 优点是:写入Hologres时,因为是普通列写入,所以写入性能会更好,同时在查询侧,不需要对JSON数据进行解析,查询性能也会更好。
• 缺点是:每当上游的数据格式有变更时,比如变更数据类型、增删字段、执行DDL进行加列或者删列,中间的实时数据ETL作业也需要进行适配改动并重新上线,使用非常不灵活,也会额外增加运维和开发负担。
基于此背景,业界也迫切需要一个既能保持高效的查询性能,又不牺牲使用灵活性的方案,来应对海量半结构化数据的极致分析场景。

6.3 Hologres列式JSON实现方案

为了更好的支持JSON分析场景,Hologres不断迭代技术能力,在早期版本支持了JSON数据格式和相关解析函数,用户可以直接写入JSON类型以及相关的查询解析。同时1.1版本在查询层做了JSON相关的优化,有效的提升JSON数据查询性能,比如支持GIN倒排索引,加速JSON数据的过滤,支持表达式下推等,但整体加速场景有限且使用难度较高,于是1.3版本我们做了大量的存储层优化,通过JSONB列存的方式来实现更好的查询性能。

总体方案介绍

经我们观察,实际用户的非结构化数据,在一段时间周期内,整体数据的结构都是比较稳定的,通常只会有有限个数的确定的字段,区别只是每个字段出现的频率会有所不同,且每个字段的数据类型也是整体稳定的。

基于以上经验,Hologres提供的实现方案的核心要点就是,在导入JSON类型数据至Hologres的时候,引擎自动去抽取JSON数据的结构(字段个数,字段类型等),然后在存储层,将JSON数据转化成强Schema格式的列式存储格式的文件,以此来达到加速查询的效果,同时对外接口上,依旧保持JSON的语义,真正做到了保持JSON易用性的同时,兼顾了OLAP查询性能。
以下图为例,Hologres每张表在同一个Shard上的数据,也是会分文件存储的,而且同一个文件中的数据,通常也是在邻近的时间点写入的,所以在JSON场景下,文件与文件之间可能会有结构的差异,但单个文件内的数据能有比较稳定的结构,从而整体上做到JSON数据结构的稳定演进。
在这里插入图片描述

JSON与JSONB

在详细介绍Hologres JSON列存化实现之前,我们先简单介绍下Postgres中的JSON和JSONB两种数据类型的区别。
JSON和JSONB这两种数据类型在用户接口上没有很大的差异,大部分操作符都是相同,主要区别在于存储格式上的差别:

  1. JSON类型只会校验写入的数据是否符合JSON规范,存储上直接将JSON原文按照TEXT存储,无任何优化
  2. JSONB在JSON的基础上,会对数据进行格式优化,存储的是对原始JSON数据优化过后的二进制格式,其优化包含但不限于:
    a. 去除数据中的冗余空格
    b. 对相同路径下的同名字段去重
    c. 对JSON数据中的字段进行排序,重新排列组织,加速查询能力
    在函数覆盖上,JSON和JSONB这两个类型也有些许差别,比如JSON类型无法直接Cast成INT/Float/Numeric等类型,而JSONB则可以,所以整体语法层面JSONB更完整易用。
    Hologres的JSON列存化方案,当前的实现主要还是基于JSONB这个数据类型,具体原因下文会讲到。

JSON结构抽取

JSON数据的结构抽取,主要做的是确定JSON数据的格式,包括JSON具体有哪些字段,每个字段对应的数据类型,以此作为底层列存文件的实际存储结构。
Hologres数据写入流程整体是个LSM (The Log-Structured Merge-Tree)架构,当数据写入到Hologres的一张表的时候,数据首先会写到内存表(MemTable) 中,当一个MemTable满了以后,将其以异步的方式Flush到文件系统中(下图第4步),并初始化一个新的MemTable,同时后台会有任务,不停将Flush到文件系统的文件做进一步的合并(Compaction,下图第5步)
在这里插入图片描述
而JSON数据的结构抽取,也主要发生在Flush和Compaction两个阶段。
Flush阶段
当MemTable Flush时,我们会遍历一次在MemTable中所有JSON数据,记录下每个JSON中出现过的字段,以及每个字段的数据类型,遍历完成后,就能知道这列JSON数据列存化之后,具体会有哪些列以及每一列的对应类型。
还是以下面的数据为例:
{“user_id”:1001, “user_name”: “Adam”, “gender”: “Male”, “age”: 16}
{“user_id”:1002, “user_name”: “Bob”, “gender”: “Male”, “age”: 41}
{“user_id”:1003, “user_name”: “Clair”, “gender”: “Female”, “age”: 21}
我们就能够抽取出以下JSON格式:

列名数据类型
user_idINT
user_nameTEXT
genderTEXT
ageINT

另外,在遍历JSON的过程中,我们也会进行类型泛化。比如user_id字段某一行数据出现了超过INT类型阈值的值,我们就会把user_id列的类型泛化成BigINT类型来兼容所有数据。
抽取完JSON结构之后,我们就能把MemTable中的数据写到文件系统了,JSON列数据会被拆分写到对应的4列中去。

Compaction阶段

Compaction做的事情就是把多个文件合并成一个更大的文件,这里也涉及到JSON结构的抽取。
在这里插入图片描述

与Flush不同的是,由于Compaction的输入文件已经对JSON列进行了列存化处理,所以我们在大部分情况下并不需要再完整遍历所有文件中的JSON数据去抽取结构,而是可以直接通过文件的Meta信息就能推导出输出文件的JSON格式,只需要对所有文件的输入列取一个并集,并对冲突列的类型进行泛化即可。

在这里插入图片描述
通过上述Flush和Compaction阶段的JSON数据处理,我们就能将数据在存储层列式化,便于后续的查询加速。

6.4 查询自适应改写

上文提到,Hologres虽然底层存储将JSONB数据转成了列式存储,但用户接口还是沿用了原生JSONB的查询接口,而由于底层JSONB数据格式的改变,如果查询引擎还是将列式化后的数据当成JSONB类型,查询势必会失败(数据的实际输入类型和执行计划的预期输入类型不一致),所以这就要求我们的查询引擎有查询自适应改写的能力。
接下来我们以一个简单的SQL为例子讲解查询过程中涉及到的查询自适应改写:
CREATE TABLE TBL(json_data jsonb); --建表DDL
SELECT SUM((json_data->‘quantity’)::BIGINT) FROM TBL;
在Hologres中,对JSONB类型最常用的两个操作符就是->和->>

  1. -> 操作符的含义是,根据操作符后面的路径参数,取出对应的JSONB数据,该操作符的返回数据类型是JSONB
  2. ->> 操作符的含义是,根据操作符后面的路径参数,取出对应的JSONB数据,该操作符的返回数据类型是TEXT
    所以,上面例子的含义就是,读取json_data这一JSONB列中的quantity字段,并转成BIGINT类型后,进行SUM聚合运算。

在这里插入图片描述
所以在物理执行计划中,Scan节点就会有上图中最左边的表达式树,根节点代表将JSONB转换成BIGINT的函数,它的孩子节点表是取出json_data列中的quantity字段。
但实际上底层文件存储的是列存化后的数据,已经没有了json_data这一物理JSON列,所以我们在Scan节点就需要进行自适应的物理执行计划改写:

  1. 第一步就是进行列裁剪,如果我们发现底层文件的Meta信息中含有quantity这一列,我们就可以直接消除->这一表达式计算,得到了上图中间所示的表达式树。当然如果我们发现Meta信息中没有quantity这一列,那我们就可以直接跳过扫描这个文件,返回执行结果,大大提升执行效率。
  2. 第二步就是根据文件Meta信息判断quantity这一列的物理存储类型,当我们发现实际存储类型和要求Cast的类型目标一致时,我们就能进一步改写优化执行计划,省去了Cast的操作,得到了上图中最右所示的表达式树,也就是直接返回物理存储的列数据。另外如果实际存储类型是INT,那么我们就需要将原始的Cast节点替换成INT到BIGINT的Cast操作,来保证结果的正确性。

那为什么不直接让SQL Optimizer把执行计划一开始就改写好呢?

原因在于优化器并不知道JSONB列在存储引擎的真正格式,比如同一列quantity,在文件A中的类型是INT,在文件B中的类型是TEXT,所以对于不同文件的执行计划可能是不同的,SQL Optimizer无法用一个物理执行计划表达所有可能的情况,这就要求执行引擎能够进行自适应的执行计划改写。

  • 脏数据、稀疏数据处理

由于JSON类型的易用性,理论上用户可以写入任意符合JSON格式的数据,这也导致相较于强Schema类型,JSON类型更容易产生脏数据,这就要求Hologres的JSON列式方案要有比较强的鲁棒性,能够容忍脏数据,这里我们主要讨论两类问题::数据类型不一致的问题以及字段名错误导致的数据稀疏问题。

  • 脏数据

首先如何处理不一致的数据类型,假设我们现在有以下JSON数据需要列式存储:
{“user_id”:1001, “user_name”: “Adam”, “gender”: “Male”, “age”: 16}
{“user_id”:1002, “user_name”: “Bob”, “gender”: “Male”, “age”: 41}
{“user_id”:1003, “user_name”: “Claire”, “gender”: “Female”, “age”: “21”}

可以看到age列的前两行数据都是INT类型的,但是到第三行的时候,age列的值就是一个TEXT类型的数据了,这时候我们就会对类型泛化,泛化成我们在上文提到JSONB类型:

列名数据类型
user_idINT
user_nameTEXT
genderTEXT
ageJSONB

我们可以把JSON看做是个递归定义的格式,像16、41、"21"这些age字段的值,本身也是一个JSON值(Object类型),所以我们可以进行这样的类型泛化。这样泛化之后,之后对于age列的查询性能会稍弱于没有脏数据的情况,因为在执行引擎层,无法像上一节提到的,直接略去JSONB的Cast操作,但整体性能还是远好于没有JSON列存化的方案的,因为我们还是只需要读取age这一列数据,可以省去大量的IO和计算操作。

  • 稀疏数据

我们再来看下如何处理稀疏数据,通常稀疏数据产生的原因是上游数据生成的逻辑有问题,生成了大量不重复的字段名,比如以下数据:
{“user_id”:1001, “user_name”: “Adam”, “gender”: “Male”, “age”: 16, “key_1”: “1”}
{“user_id”:1002, “user_name”: “Bob”, “gender”: “Male”, “age”: 41, “key_2”: “2”}
{“user_id”:1003, “user_name”: “Claire”, “gender”: “Female”, “age”: 21, “key_3”: “3”}
可以看到每一行都有一个不一样的字段,且不重复,如果我们选择抽取key_1,key_2,key_3这三列,那这三列的数据就会非常稀疏,也会导致整体文件的列数膨胀的很厉害。

我们选择将这些稀疏数据单独抽取到特殊的一列(holo.remaining),该列的类型也是JSONB,我们会把出现频度低于某个阈值(可配置)的数据,都存放到这个字段中:

列名数据类型
user_idINT
user_nameTEXT
genderTEXT
holo.remainingJSONB

可以认为在remaining列中存储的就是整个JSON数据的一个子集,这一列并上其他列式化的数据,就能构造成原来完整的一个JSON值。
查询remaining列时的性能也会稍弱于查询已经列式化的列,因为存储的是JSONB,会包含所有稀疏字段,所以查询时需要在JSONB数据中搜索指定的字段,这里有额外的开销。但因为这一列中存储的都是稀疏的数据,通常查询命中remaining列的概率也不会很高,所以可以容忍。

  • 嵌套与复杂结构处理

上文中给出的JSON实例都是比较简单的扁平化的数据,但实际上含有嵌套结构的JSON数据也是比较常见的,接下来简单介绍下Hologres是如何处理复杂JSON结构的。

嵌套结构
对于嵌套结构,我们可以把JSON数据看成是一颗树,数据都存在叶子节点中(没有复杂嵌套结构的情况下),比如下面这个JSON数据,就会抽取出右图所示的树形结构:
在这里插入图片描述
因为非叶子节点本身并不存储数据,所以实际上存储的时候就可以把上面的树状结构拍平得到以下表结构,另外我们的元数据会记录节点的深度信息,以此来保证拍平的时候不会出现列名歧义或者冲突的情况。
在这里插入图片描述
复杂嵌套结构
首先我们需要先明确下当前Hologres抽取JSON结构时,只会抽取出以下基本类型:

  1. INT
  2. BIGINT
  3. TEXT
  4. INT[]
  5. BIGINT[]
  6. TEXT[]
  7. JSONB

这里面JSONB类型就是我们尝试类型泛化后仍旧无法抽取成前面6种基本类型时,作为兜底的类型实现,这当中也包括的复杂嵌套结构,比如下面这行JSON数据就会抽取出右图所示的结构,可以看到对于descs这个字段,因为是数组里面嵌套了非基本类型数据,所以这里类型退化成了JSONB类型。

在这里插入图片描述
所以这里要注意的点就是,对于这类退化成JSONB类型的数据,针对这一列的操作的性能会不如那些抽成基本类型数据的列,但整体性能还是会比非列式JSON方案会好很多,因为JSONB列只存储了完整JSON数据的一个子集,查询这一列涉及到的IO和计算都会小很多。

6.5 列式JSON不适用场景

查询带出完整JSON数据
Hologres的列式JSON方案对于大部分使用场景都有比较好的优化效果,主要需要注意的点是,对于查询结果需要带出完整JSON列的场景,性能相较于直接存储原始格式的JSON会有退化,比如以下SQL:

CREATE TABLE TBL(pk int primary key, json_data jsonb); --建表DDL
SELECT json_data FROM TBL WHERE pk = 123;
SELECT * FROM TBL limit 10;

原因在于底层已经将JSON数据转成了列式存储,所以当需要查询出完整JSON数据的时候,就需要将那些已经列式存储的数据重新拼装成原来的JSON格式:

在这里插入图片描述

  • 极稀疏的JSON数据

上文已经提到,当我们列式化JSON数据遇到稀疏的字段时,我们会将这部分字段合并至一个叫做holo.remaining的特殊列中,以此来避免列数膨胀的问题。
所以如果用户的JSON数据,包含的都是稀疏字段,比如极端情况下每个字段都只会出现一次,那么我们的列式化将不会起效,因为所有字段都是稀疏的,那么所有字段都会合并至holo.remaining字段,等于没有进行列式化,这种情况下就不会有查询性能的提升。

6.6 Hologres列式JSON方案收益:降本增效

收益1:存储降本
我们使用了TPCH的数据集来测试Hologres JSON列式方案对于存储空间的优化效果,具体测试对比方案是将TPCH的表都建成一列JSONB的格式,然后对比开启列式方案的效果(几张数据量较小的表略去了):

--存储原始Jsonb数据的表
CREATE TABLE CUSTOMER(data jsonb);
CREATE TABLE LINEITEM(data jsonb);
CREATE TABLE ORDERS(data jsonb);
CREATE TABLE PART(data jsonb);
CREATE TABLE PARTSUPP(data jsonb);

--开启列式Json优化的表
CREATE TABLE CUSTOMER_COLUMNAR(data jsonb);
ALTER TABLE CUSTOMER_COLUMNAR ALTER COLUMN data SET (enable_columnar_type = on);

CREATE TABLE LINEITEM_COLUMNAR(data jsonb);
ALTER TABLE LINEITEM_COLUMNAR ALTER COLUMN data SET (enable_columnar_type = on);

CREATE TABLE ORDERS_COLUMNAR(data jsonb);
ALTER TABLE ORDERS_COLUMNAR ALTER COLUMN data SET (enable_columnar_type = on);

CREATE TABLE PART_COLUMNAR(data jsonb);
ALTER TABLE PART_COLUMNAR ALTER COLUMN data SET (enable_columnar_type = on);

CREATE TABLE PARTSUPP_COLUMNAR(data jsonb);
ALTER TABLE PARTSUPP_COLUMNAR ALTER COLUMN data SET (enable_columnar_type = on);

使用了TPCH 100GB的测试集进行验证,结果如下:
在这里插入图片描述
可以看到,开启列式JSONB优化后,每张表的存储空间都有比较显著的下降,原因在于列式化之后:

  1. 原来JSON数据中的字段名都不会再存储了,而只需要存储每个字段对应的具体值,比如下面是转成JSON后CUSTOMER表的一行数据,数据中的c_name、c_phone、c_acctbal等字符串,列式化后都不需存储

{“c_name”: “Customer#002662050”, “c_phone”: “23-793-162-6786”, “c_acctbal”: 4075.57, “c_address”: “paJBRFkD N368pMSvGsYivWyRAs”, “c_comment”: “ly. fluffily even packages along the blithely even deposits should sleep slyly above the”, “c_custkey”: 2662050, “c_nationkey”: 13, “c_mktsegment”: “BUILDING”}

  1. 列式化后每列的数据类型都是一样的,列式存储能有比较好的数据压缩率
    这里要多说一点的是,在某些数据集上我们也观察到过开启列式优化后实际存储空间没有下降的情况,这种情况通常是由于JSON数据中的字段比较稀疏,列数膨胀比较厉害,且列式化后每一列的类型都是TEXT类型,导致压缩效果不好导致的。所以上述测试只是一个理论值,实际用户的数据各种各样,实际压缩后的存储效果还是要以实际情况为准。

收益2:查询性能提升
得益于底层列式化的存储格式,对于那些能够利用到JSON列裁剪的查询,经我们测试观察,通常性能都会有数倍的提升,甚至在特定场景下能有十倍以上的性能提升。
这里我们使用Github的数据集(见文末SQL和DDL附录)来验证Hologres JSON列式化方案的查询提升,该数据集记录了Github上的各种用户行为日志,包括发起代码评审、评论等等,该数据集是一份JSON格式的数据集。我们选用了2015年的总计172309645行的数据,导入到同一个Hologres实例后,对比了使用原生JSON类型、原生JSONB类型存储和开启列式JSONB优化后的查询性能:
在这里插入图片描述
可以看到,开启列式JSONB优化后的查询性能,相较于原始JSONB格式,有了质的提升。但要注意的是,由于数据集的不同,以及查询模式的不同,性能收益可能会有较大的差异,具体效果还是要以实际情况为准。后续我们也将陆续推出Hologres JSON列式方案在不同场景下的实现案例,以及对应的性能收益。

6.7 总结

Hologres的列式JSON方案,真正做到了在保持JSON易用性和灵活性的同时,兼顾了极致的OLAP查询性能,让用户能够在Hologres上充分挖掘半结构化数据,甚至让Hologres这个一站式实时数仓承担部分数据湖的能力。我们后续也会继续不断优化列式JSON实现,为大家带来更为极致的性能,敬请期待。
附录

  1. Github数据集: https://www.gharchive.org/
  2. 查询性能测试DDL
    CREATE TABLE gh_2015(gh_jsonb jsonb);
    –开启列式优化
    ALTER TABLE gh_2015 ALTER COLUMN gh_jsonb SET (enable_columnar_type = on);
  3. 查询性能测试Query
--Query 1
SELECT COUNT(1) FROM gh_2015 WHERE gh_jsonb->>'type' = 'WatchEvent';

--Query 2
SELECT gh_jsonb->'repo'->>'name', count(1) AS stars FROM gh_2015 WHERE gh_jsonb->>'type' = 'WatchEvent' GROUP BY gh_jsonb->'repo'->>'name' ORDER BY stars DESC LIMIT 50

--Query 3
SELECT to_date((substring((gh_jsonb ->> 'created_at')FROM 1 FOR 8) || '01'), 'YYYY-MM-DD') AS event_month,
       sum(coalesce((gh_jsonb -> 'payload' -> 'issue' ->> 'number'), (gh_jsonb -> 'payload' -> 'pull_request' ->> 'number'),
             (gh_jsonb -> 'payload' ->> 'number'))::int) AS closed
FROM gh_2015
WHERE (gh_jsonb ->> 'type') = 'IssuesEvent'
  AND (gh_jsonb -> 'payload' ->> 'action') = 'closed'
  AND (gh_jsonb -> 'repo' ->> 'id')::bigint = 41986369
GROUP BY 1;

--Query 4
SELECT event_month,
       all_size
FROM
  (SELECT event_month,
          COUNT(*) OVER (PARTITION BY event_month) AS all_size,
                        ROW_NUMBER() OVER (PARTITION BY event_month) AS row_num
   FROM
     (SELECT (gh_jsonb ->> 'type') AS TYPE,
             (gh_jsonb -> 'repo' ->> 'id')::bigint AS repo_id,
             (gh_jsonb -> 'payload' ->> 'action') AS action,
             to_date((substring((gh_jsonb ->> 'created_at')
                                FROM 1
                                FOR 8) || '01'), 'YYYY-MM-DD') AS event_month
      FROM gh_2015) t
   WHERE TYPE = 'PullRequestEvent'
     AND repo_id = 41986369
     AND action = 'opened') sub
WHERE row_num = 1
ORDER BY event_month;

7 Hologres Binlog技术原理揭秘

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Direction_Wind

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值