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数据存储和分析效率接近原生列存效率。
针对实时数仓数据更新频繁、数据模型简单和分析场景敏捷的特性,Hologres支持高并发实时写入与更新,支持事务隔离与原子性,数据写入即可查。
高吞吐实时写入与更新
Hologres与Flink、Spark等计算框架原生集成,通过内置Connector,支持高通量数据实时写入与更新,支持源表、结果表、维度表多种场景,支持多流合并等复杂操作。
所见即所得的开发
数据实时写入即可查询,支持DB、Schema、Table三级体系,支持视图View,原生支持Update/Delete/Upsert,支持关联、嵌套、窗口等丰富表达能力,原生支持半结构化JSON数据分析,支持MySQL等数据库数据整库一键入库,实时同步。
全链路事件驱动
支持表更新事件的Binlog透出能力,通过Flink消费Hologres Binlog,实现数仓层次间全链路实时开发,满足分层治理的前提下,缩短数据加工端到端延迟。
实时物化视图
支持定义实时物化视图,简化数据加工聚合等开发,数据实时写入,聚合实时更新,完善支持实时加工场景。
支持计算负载、访问权限等细粒度管控要求,提供丰富的监控和告警指标,支持计算资源弹性扩展,支持系统热升级,满足企业级安全可靠的运维需求。
数据安全
支持细粒度访问控制策略,支持BYOK数据存储加密和数据脱敏,支持数据保护伞、IP白名单,支持RAM、STS及独立账号等多种认证体系,通过PCI-DSS安全认证。支持数据备份与恢复。
负载隔离
多个计算实例组成一主多从模式,实例间共享一份存储,计算资源隔离,实现写入和读取隔离,查询和服务隔离,实现故障管理,支持故障节点快速自动恢复。无需本地盘,盘古三副本高可靠冗余存储。
自运维能力
内置查询历史、元仓表等运维诊断信息,用户可以基于查询历史和表的元数据,快速定位系统瓶颈和风险点,提升自运维能力。
兼容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向量检索插件,支持在线实时特征存储、实时召回、向量检索。
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)。用户可以根据业务需求进行弹性扩缩容,轻松应对在线系统不同的流量峰值。
整个架构从上往下分为如下组件。
接入节点(Frontend,FE)
Hologres接入节点,主要用于SQL的认证、解析、优化,一个实例有多个FE接入节点。在生态上兼容Postgres 11,因此用户可以使用Postgres标准语法进行开发,也可以用Postgres兼容的开发工具和BI工具直接连接Hologres。
计算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
轻量级调度。
数据直接存储在Pangu File System。
与MaxCompute在存储层打通,能直接访问MaxCompute存储在盘古的数据,实现高效相互访问。
支持直接访问OSS、DLF数据,类型包含CSV、ORC、Parquet、Hudi、Delta、Meta Data等,加速数据湖探索,也可以将数据回流至OSS,降低存储成本。
Hologres的执行引擎(主要以HQE为主)是自研的执行引擎,通过与大数据领域最新技术结合,实现了对各种查询类型的高性能处理,主要具有如下优势。
分布式执行
执行引擎是一个和存储计算分离架构配合的分布式执行模型。执行计划由异步算子组成的执行图DAG(有向无环图)表示,可以表达各种复杂查询,并且完美适配Hologres的数据存储模型,方便对接查询优化器,利用各种查询优化技术。
全异步执行
端到端的全异步处理框架,可以避免高并发系统的瓶颈,充分利用资源,并且最大可能地避免存储计算分离系统带来的读数据延迟的影响。
向量化和列处理
算子内部处理数据时最大可能地使用向量化执行,与存储引擎深度集成,通过灵活的执行模型,充分利用各种索引,最大化地延迟向量物化和延迟计算,避免不必要的读数据和计算。
自适应增量处理
对常见实时数据应用查询模式进行自适应增量处理。
特定查询深度优化
对一些特定查询模式的独特优化。
当客户端下发一个Query后,在执行引擎中实际上会有多个worker节点,以其中的一个worker节点为例,执行过程如下图所示。
当客户端发起一个SQL后,执行过程如下。
Frontend(FE)节点对SQL进行解析和认证,并分发至执行引擎(Query Engine)的不同执行模块。
执行引擎(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的对比介绍如下。
https://developer.aliyun.com/article/779284?
spm=a2c4g.11186623.0.0.3a0f24d0TwmuJy&groupCode=hologres
3.执行引擎决定正确的执行计划,然后会通过存储引擎(Storage Engine,SE)进行数据获取,最后对每个Shard上的数据进行合并,返回至客户端。
Hologres作为HSAP服务分析一体化的落地最佳实践,其查询引擎是一个完全自研的执行引擎,它的核心设计目标是支持所有类型的分布式分析和服务查询,并做到极致查询性能。为了做到这一点,我们借鉴了各种分布式查询系统,包括分析型数据库,实时数仓等,吸取了各方面的优势从零开始打造出一个全新的执行引擎。
为什么要选择从零开始做一个新的查询引擎?开源的分布式分析查询系统主要有两大类:
Hologres执行引擎从开发到落地实践面临了非常多的挑战,但也给我们提供了机会把这个领域的各种新进展都结合利用起来,并超越已有系统做到对各种查询类型的高性能处理,其背后主要是基于以下特点:
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优化调整。
高并发系统,特别是有大量 I/O 的系统,频繁地等待或者任务切换是常见的系统瓶颈。异步处理是一种已经被证明行之有效的避免这些瓶颈,并把高并发系统性能推到极致的方法。
Hologres 的整个后端,包括执行引擎、存储引擎和其它组件,统一使用 HOS(Hologres Operation System) 组件提供的异步无锁编程框架,能够最大化异步执行的效果。每个 Fragment 的实例使用 HOS 的一个 EC (逻辑调度单位),使得一个 Fragment 里的所有算子和存储引擎可以异步执行并且无锁安全访问绝大多数资源。
算子和 Fragment 都是类似这样的接口:
future<> Open(const SeekParameters& parameters, …)
futureGetNext(…)
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 的查询。
按列处理和向量化执行都是分析查询引擎常用的优化机制,可以大幅度提高数据处理的效率。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 ,带来较大的性能问题。很多查询引擎会在不同的点引入按列存储和按行存储的转换,但是频繁的转换本身会带来不小的开销,而且列转行会造成上述延迟读取列被不必要地读取,还有一些其它的性能问题。
很多实时数据应用经常会对一个查询用不同的时间段反复执行。比如一个监控指标页面打开后,会定期执行 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充分利用存储引擎和计算引擎的深度集成和列式存储大部分数据在只读文件中的特性,在能提供包含最新写入数据的查询结果的同时尽量避免重复计算,对这种类型的查询能够显著提升性能和减少资源使用。
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 申请的专利之一。
Hologres 执行引擎在一个架构里集中了相关分布式查询系统的几乎所有最高效的优化方式(包括各种类型的索引)并作出了特有的改进。通过和存储引擎深度整合,能充分发挥异步模型的优势,并高效利用各种类型的索引来加速查询。所有这些加起来,带来了超越已有系统的性能,并在阿里巴巴双 11 的数据规模下通过了实战的考验,(2020年双11顶住了5.96亿/秒的实时数据洪峰,基于万亿级数据对外提供多维分析和服务,99.99%的查询可以在80ms以内返回结果),对外高并发高性能地提供分布式 HSAP 查询服务。
实时数仓技术不管是面向分析型场景还是服务型场景,所处理的数据量级、场景复杂度都远比传统数据库要高,尤其是互联网、电商等行业,活动促销多,大促和日常所处理的流量完全不一样,这就非常考验系统的资源水平扩展能力。
在传统的分布式系统中,常用的存储计算架构有如下三种:
这种存储计算分离的架构好处在于:
在架构上,Hologres采用的是第3种存储计算分离架构,Hologres的存储使用的是阿里自研的Pangu分布式文件系统(类似HDFS)。用户可以根据业务需求进行弹性扩缩容,轻松应对在线系统不同的流量峰值。
Replication(复制)是实现高可用的必备技术,通过不同形态的Replication设计,快速将数据在节点间、集群间进行复制,实现隔离和SLA保障。
Hologers同时支持了逻辑Replication和物理Replication,下面将会针对Hologres的Replication功能做具体介绍。
1)基于Binlog的逻辑Replication
类似于传统数据库MySQL中的Binlog概念,在Hologres中,Binlog用来记录数据库中表数据的修改记录,比如Insert/Delete/Update的操作,主要应用场景包括:
在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:
• 单实例多副本
Hologres数据分片单元是Shard,Shard可以有多个副本,但是存储只有一份。平时,查询流量可以被各个副本均摊,从而实现高QPS。当某一个副本failover以后,流量可以快速被导到其他副本。并且Shard的故障恢复非常轻量,只需回放部分WAL,没有数据的复制。基于单实例内多副本机制,可以很方便的实现计算的可扩展性,并快速解决物理机单机failover问题。
应用场景:
• 多实例读写分离
和单实例内多副本的Replication相比,跨实例的Replication实现了Meta的物理复制。
Hologres 在V1.1版本,支持了共享存储的多实例部署方案。在该方案中,主实例具备完整能力,数据可读可写,权限、系统参数可配置,而子实例处于只读状态,所有的变更都通过主实例完成,实例之间共享一份数据存储,实例间数据异步实时同步。
应用场景:
• 多实例跨城容灾
多实例非共享存储的Replication,可以理解为传统意义上的灾备功能,支持容灾,异地多活,并实现读写分离和读的高并发,同样也可以基于多个实例实现读的高可用。除此之外,还可以进行版本热升级,存储系统迁移。
应用场景:
分布式环境failover是不可避免的,当failover发生时,需要高效的检测,快速的恢复,这就是调度的范畴。
一个Hologres实例有多个HoloWorker,当某一个HoloWorker发生意外、宕机、failover时,通过Hologres的调度系统,可以快速检测到节点异常,并将异常节点的Service如Frontend、Coordinator、Shard快速调度到另外一个健康的HoloWorker,同时SLB将会将流量导流到新的健康Frontend上。
调度分为计算单元的调度和流量的调度:
1)计算单元的调度
计算单元的调度分为Pod的调度、Pod内子进程调度以及Actor的调度
2)流量的调度
流量的调度又分为外部流量和内部流量的调度。
通过Hologres的调度系统,实现了节点故障、Failover的快速检测以及自动调度恢复能力,满足业务的稳定性需求,提高系统可用性。
随着实时数仓在生产系统越来越广泛的应用,不同的业务也有着不同的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)中的线程级别隔离模型会有如下问题:
理想的方案是能有一种轻量级的调度单元,功能类似于线程,但是创建/销毁和调度/切换的开销要小得多。这样的话:
根据上面的设计理念,Hologres在自研调度系统HOS中,通过一个轻量级调度单元EC来实现。
SchedulingGroup隔离利用了HOS EC调度的能力,同一个Query有多个EC执行,这些EC可以被归类到一个SchedulingGroup,不同的SchedulingGroup可以用公平的策略瓜分时间片。
SchedulingGroup隔离保证了当系统中同时跑一个大Query(分析型)和一个小Query(点查)的时候,小Query不至于因为抢不到CPU被大Query block住。SchedulingGroup隔离本质上是协程级别的隔离,是Hologres的核心竞争力之一。
MaxCompute 交互式分析(Hologres)是阿里云自研开发的HSAP(Hybrid Serving/Analytical Processing)服务/分析一体化系统 ,融合了实时服务和分析大数据的场景,全面兼容PostgreSQL协议并与大数据生态无缝打通。它的出现简化了业务的架构,与此同时为业务提供实时做出决策的能力,让大数据发挥出更大的商业价值。关于架构更详细的介绍,请看文末VLDB论文 。
跟传统的大数据和OLAP系统相比,HSAP系统面临下面的挑战:
基于上诉背景,我们自研了一款存储引擎(Storage Engine),主要负责管理和处理数据, 包括创建,查询,更新,和删除(简称 CRUD)数据的方法。存储引擎的设计和实现提供了HSAP场景所需要的高吞吐,高并发,低延迟,弹性化,可扩展性的能力。根据阿里集团业务和云上客户的需求,我们不断创新和打磨,发展到今天,能支持单表PB级存储,并完美支撑2020年天猫双11核心场景千亿个级别的点查询和千万个级别的实时复杂查询 。
下面,我们将会对Hologres底层的存储引擎做详细的介绍,并介绍存储引擎落地Hologres的具体实现原理和技术亮点。
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字段将会建立位图索引。
每个分片(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操作)访问方法的接口,查询引擎可以通过这些接口访问存储的数据。
下面是存储引擎几个重要的的组件:
WAL 和 WAL Manager
WAL Manager是来管理日志文件的。存储引擎用预写式日志(WAL) 来保证数据的原子性和持久性。当CUD操作发生时,存储引擎先写WAL,再写到对应tablet的MemTable中,等到MemTable积累到一定的规模或者到了一定的时间,就会把这个MemTable切换为不可更改的flushing MemTable, 并新开一个 MemTable接收新的写入请求。 而这个不可更改的flushing MemTable就可以刷磁盘,变成不可更改的文件; 当不可更改的文件生成后,数据就可以算持久化。 当系统发生错误崩溃后,系统重启时会去WAL读日志,恢复还没有持久化的数据。 只有当一个日志文件对应的数据都持久化后,WAL Manager才会把这个日志文件删除。
文件存储
每个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。
Hologres支持两种类型的写入:单分片写入和分布式批量写入。两种类型的写入都是原子的(Atomic Write),即写入或回滚。单分片写入一次更新一个Shard,但是需要支持极高的写入频率。另一方面,分布式批写用于将大量数据作为单个事务写到多个Shard中的场景,并且通常以低得多的频率执行。
存储引擎采取存储计算分离的架构,所有的数据文件存在一个分布式文件系统(DFS, 例如阿里巴巴盘古或者Apache HDFS)的里面。当查询负载变大需要更多的计算资源的时候可以单独扩展计算资源; 当数据量快速增长的时候可以快速单独扩展存储资源。计算节点和存储节点可以独立扩展的架构保证了不需要等待数据的拷贝或者移动就能快速扩展资源; 而且,可以利用DFS存多副本的机制保证数据的高可用性。 这种架构不但极大地简化了运维,而且为系统的稳定性提供了很大的保障。
存储引擎采用了基于事件触发, 非阻塞的纯异步执行架构, 这样能够充分发挥现代CPU多core的处理能力,提高了吞吐量, 支持高并发的写入和查询。这种架构得益于HOS(HoloOS) 框架,HOS在提供高效的异步执行和并发能力的同时,还能自动地做CPU的负载均衡提升系统的利用率。
在HSAP场景下,有两类查询模式,一类是简单的点查询(数据服务Serving类场景),另一类是扫描大量数据的复杂查询(分析Analytical类场景)。 当然,也有很多查询是介于两者之间的。这两种查询模式对数据存储提出了不同的要求。行存能够比较高效地支持点查询,而列存在支持大量扫描的查询上有明显的优势。
为了能够支持各种查询模式,统一的实时存储是非常重要的。存储引擎支持行存和列存的存储格式。根据用户的需求,一个tablet可以是行存的存储格式 (适用于Serving的场景); 也可以是列存的存储格式(适用于Analytical的场景)。 比如,在一个典型HSAP的场景,很多用户会把数据存在列存的存储格式下,便于大规模扫描做分析;与此同时,数据的索引存在行存的存储格式下,便于点查。并通过定义primary key constraint (我们是用行存来实现的)用来防止数据重复·。不管底层用的是行存还是列存,读写的接口是一样的,用户没有感知,只在建表的时候指定即可。
存储引擎采用了snapshot read的语意,读数据时采用读开始时的数据状态,不需要数据锁,读操作不会被写操作block住; 当有新的写操作进来的时候,因为写操作是append-only,所有写操作也不会被读操作block住。这样可以很好的支持HSAP的高并发混合工作负载场景。
存储引擎提供了多种索引类型,用于提升查询的效率。一个表可以支持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