Doris前世今生

Doris是由百度自研并开源的OLAP数据库,以易用的特点被业内熟知。Doris支持标准的SQL并且完全兼容MySQL协议,仅需亚秒级响应时间即可返回海量数据下的查询结果。
Doris是一款为数据分析而生的数据库。从诞生之日起,Doris的每一次进步都是为了解决切实的OLAP业务痛点,每一次转变都是在应对不同的业务挑战。Doris的发展历程大致如图1-1所示。

图1-1 Doris的发展历程
01
Doris应需而生
在Doris诞生之前,百度和大多数互联网公司一样,使用MySQL的Sharding为OLAP报表业务提供支持。在早些年,百度的主要收入来源是广告,广告主需要通过报表查看广告的展现效果、点击量、收入等信息,并且根据不同维度分析制定后续广告的投放策略。随着百度本身流量的增加,广告流量也随之增加,MySQL的Sharding方案无法满足业务需求,主要痛点如下。
❑大规模数据导入会导致MySQL的读性能大幅降低,有时还会出现锁表现象,导致查询超时,尤其在频繁导入数据时,问题更为明显。
❑MySQL在数据量达千万级别时性能很差,只能从产品层面来限制用户的查询时间,抑制了用户需求,增加了很多后台取数的需求。
❑MySQL单表存储的数据有限,如果数据量过大,查询就会变慢。而且随着数据量的快速增长,Sharding方案维护成本飙升。
上述痛点也是目前大多数未引入Doris的企业所面临的,特别是在互联网行业,数据量大,且较少采用商业软件,主要以开源MySQL为核心构建报表查询系统,需要将在线分析处理结果进行多次聚合,才能满足报表的查询需求。
在2008年那个时间点,处理数据存储和计算的成熟开源产品很少,HBase的导入性能只有约2000条/s,不能满足业务快速增长需求。另外,业务还在不断增加,数据存储和数据分析的压力越来越大。于是,百度选择了自主研发之路,Doris由此诞生。
在Doris 1版本中,数据仍然通过用户ID进行哈希分布,将同一个用户ID的数据交由一台机器处理,这样大量的Join操作都可以在本地完成。Doris 1架构包含数据存储、元数据管理、数据导入和API网关4个部分,其中数据存储组件负责数据的存储和读写,元数据管理组件负责数据文件的目录管理和表信息管理,数据导入组件负责写入外部导入的数据,API网关负责接收、解析、规划查询请求。
相比于MySQL的Sharding方案,Doris主要在如下几个方面进行了改进。
❑Doris 1的数据模型将数据分为Key列、Value列。比如一条交易数据的Key列包括用户ID、时间、地域、来源等,Value列包括展现次数、点击次数、消费额等。在这样的数据模型下,所有Key列相同的行对应的Value列能够进行聚合,比如数据的时间维度最细粒度为小时,那么同一小时内多次导入的数据能够合并成一条记录,这样对于同样的查询来说,Doris 1需要扫描的数据条目相比MySQL会少很多。
❑Doris 1将MySQL逐条插入的方式改为批量更新,并且通过外围模块将同一批次数据进行排序以及预聚合。这样一个批次中相同Key列的数据能够被预先聚合,另外排序后的数据能起到聚集索引的作用,提升查询性能。
❑Doris 1提供了天表、月表这种类似物化视图的功能。比如用户想将数据按天进行汇聚展现,那么可以通过天表来实现。而相对于小时表,天表数据量会少很多,相应的查询性能也会提升几倍。至此,Doris已经有了聚合模型、物化视图、批量读写3个基本特点了。Doris 2主要将Doris 1进行通用化改造,包括支持自定义表结构等,使Doris能够应用于其他产品,拓展了一些应用场景。
02
Doris架构重组
Doris变化和升级比较大的版本是Doris 3,这也是开发时间跨度比较长的一个版本,重大的改进主要有以下几点。
❑Doris 3在架构上引入了ZooKeeper来存储元数据,解耦组件,实现了系统无单点故障,提高了系统稳定性。在Doris 3的组件中,DT(Data Transfer)模块负责数据导入,DS(Data Searcher)模块负责数据查询,DM(Data Master)模块负责集群元数据管理。数据存储在分布式Key-Value引擎中。数据导入和数据查询时,Doris 3直接读取存储在ZooKeeper中的元数据,不再依赖元数据管理组件提供的服务。
❑Doris 3在数据分布方面引入了分区。数据会按照时间进行分区(比如天分区、月分区)。在同一个分区里,数据会根据用户ID进行分布。这样,同一个用户的数据会落在不同的分区,而在进行数据查询时,多台机器能同时处理一个用户的数据,实现了单用户数据分布式计算。
❑为了实现数据的多维分析,Doris 3采用了MySQL Storage Handler方案。通过这种方案,Doris 3伪装成一个MySQL的存储后端,类似于MyISAM、InnoDB。这样既能利用MySQL对SQL的支持,也能利用Doris 3对大量数据的快速处理;同时又引入了MySQL的BKA(Batched Key Access,批量索引访问)算法和MRR(Multi-Range Read,多范围读取优化)特性,尽量将计算下推给Doris 3来完成,从而减轻MySQL的计算压力,避免了单点故障。
❑Doris 3重构了存储引擎。在多维报表分析场景中,原来底层通用Key-Value系统的弊端也逐渐显露。由于Key-Value系统只能够读取完整的Key和完整的Value,而报表分析系统中的大部分查询并不需要读取所有列,这样会带来不必要的I/O开销。同时,Key-Value系统无法感知数据内容,只能使用通用压缩算法,导致数据压缩效率低,读写性能差。为了在底层存储引擎上有所突破,百度启动了OLAP Engine项目,参考Google Mesa系统重构了存储引擎。
新的存储引擎主要有以下特点。
❑引擎端原生就支持Schema,并且所有的列分为Key列、Value列,能够和上层的业务模型相对应,查询部分列时,无须加载全部列,减少不必要的I/O开销。❑独特的数据模型。Value列支持聚合操作,包括SUM、MIN、MAX等。在Key列相同的情况下,Value列能够按照聚合操作类型完成对应的聚合操作。而引擎存储数据类似于LSM树,这样在后台执行聚合操作时,就能够将相同Key的Value字段按照对应的聚合操作类型进行聚合,无须在外部增加数据合并作业。
❑数据批量导入,批量生效。每个批次的导入都会生成一个增量文件,并且会有一个版本号。在查询任务中,只需要在最开始的时候确定读取哪个文件,这样就会只读取生效版本的数据,不会读取没有生效版本的数据,更不会浪费CPU资源进行版本号的过滤。
❑行列式存储。多行数据存储在一个块(Block)内,块内相同列的数据一同压缩与存放,这样可以根据数据特征,利用不同的压缩算法(比如对时间字段使用RLE算法等)来提高数据压缩效率。另外,Doris 3在日常运维,包括表结构变更、集群扩容、集群缩容等方面,都做了针对性的设计,实现了自动化操作。
03
Doris引擎升级
在完成存储引擎的重构以后,Doris的I/O瓶颈得到突破。随着Doris性能的提升,Doris的查询和应用场景也变得越来越复杂,原本采用的MySQL查询引擎逐渐出现瓶颈。
幸运的是,2013年各种SQL on Hadoop项目正蓬勃发展。经过对比,百度研发团队选取Impala作为后续系统的分布式查询引擎。当时选择Impala的主要原因是C++性能较高,并且跟Doris后端开发语言一致,可以省去一部分数据序列化开销。
基于Impala改进的新产品被命名为Palo。Palo除了增加分布式查询层之外,还将Doris 3的OLAP作为单机存储引擎。由于没有分布式Key-Value系统,最初的Palo版本需要自己完成数据分片管理、副本管理等工作。后来为了增加灵活性,MySQL替换ZooKeeper实现元数据存储,整体架构如图1-2所示。

图1-2 Palo 1.x整体架构
从图1-2可以看出,Palo 1.x的组件非常多,运维难度非常大。于是,在随后的Palo 2版本中,元数据管理、分片管理和元数据副本管理合并到Frontend(Doris前端模块,以下简称为FE)组件,OLAP存储引擎和查询引擎合并到Backend(Doris后端模块,以下简称为BE)组件,减少了Agent组件和uWSGI服务组件。FE负责接收用户的查询请求,对查询进行规划,监督查询执行情况,并将查询结果返给用户。BE负责数据的存储,维护多副本数据,以及具体的查询执行。简化后的Doris架构如图1-3所示。

图1-3 Palo 2系统架构
经过Palo 2版本的改进,Doris的架构变得相当简洁,并且不再需要任何外部依赖。在此之后,虽然Doris经过几次改进,但是整体架构仍然保持Palo 2的架构。Palo 2在2015年左右开始大面积推广使用。
04
Doris拥抱开源
到2017年,Palo 2在百度内部几乎满足了所有的报表统计、多维分析需求,获得了非常高的评价。为了帮助更多的企业能更加高效、方便地完成类似业务,百度选择开源这款产品。Palo于2017年正式在GitHub上开源,并且在2018年接入Apache社区,名字改为Apache Doris,正式成为Apache基金会的孵化项目。
自开源以来,Doris以上手简单、性能卓越、运维高效等特点被越来越多的用户认可和使用。经过三年多的孵化,Doris开发者数量增长了20倍,在GitHub上的Star数量翻了6倍,被百度、美团、京东、小米、网易等头部企业广泛使用,覆盖互联网、金融、电商、教育、文娱等多个行业。鉴于Apache Doris各项指标良好,项目趋于成熟,2022年6月16日,Apache Doris顺利通过ASF项目管理委员会的评估,晋升为Apache基金会顶级项目,也成了Apache基金会的第200个顶级项目。
Doris是一款基于MPP技术的分析型数据库,能够在海量数据场景下提供毫秒级查询服务。Doris脱胎于Apache Impala和Google Mesa系统,并进行了大量改造和优化,最终形成了今天大家看到的架构简洁、性能卓越、功能丰富、简单易用的OLAP数据库。
01
极简架构
从设计上来说,Doris融合了Google Mesa的数据存储模型、Apache的ORCFile存储格式、Apache Impala的查询引擎和MySQL交互协议,是一个拥有先进技术和先进架构的领先设计产品,如图1-4所示。

图1-4 Doris技术分解
在架构方面,Doris只有两类进程:一类是FE,可以理解为Doris的管理节点,主要负责用户请求的接入、查询计划的解析、元数据的存储和集群管理相关工作;另一类是BE,主要负责数据存储、查询计划执行。这两类进程都可以横向扩展。除此之外,Doris不依赖任何第三方系统(如HDFS、ZooKeeper等)。这种高度集成的架构极大地降低了运维成本。
FE节点包含Leader、Follower和Observer三种角色。默认一个集群只能有一个Leader,可以有多个Follower和Observer。其中,Leader和Follower组成一个Paxos选择组,如果Leader宕机,剩下的Follower会自动选出新的Leader,保证写入高可用。Observer同步Leader的数据,但是不参加选举。如果只部署一个FE,FE默认就是Leader。
FE节点主要包含存储管理模块、状态管理模块、协调模块、元数据模块和元数据缓存模块。存储管理模块负责管理所有的元数据信息,包括表信息、Tablet信息、Tablet的副本信息等,还负责管理用户的权限信息(即用户的认证信息和授权信息)和数据的导入任务等。状态管理模块负责管理所有BE进程的存活状态、查询负载等非持久化信息,并提供发布订阅接口。协调模块负责接收用户发来的请求,然后进行语句解析、生成执行规划,根据当前集群的状态,对执行规划进行调度。元数据模块负责对元数据的读写,只有Leader角色拥有此权限。元数据缓存模块负责同步元数据,以供语句解析、生成执行规划,主要是Follower和Observer角色拥有此权限。
BE节点可以无限扩展,并且所有BE节点的角色都是对等的。在集群足够大的情况下,部分BE节点下线不影响集群提供服务。BE节点主要由存储引擎和查询执行器组成。存储引擎负责管理节点本地的Tablet数据,发送或者接收数据并保存副本,定期合并、更新多个版本的数据,以减少存储占用。存储引擎还负责接收来自查询执行器的数据读取请求和批量数据导入请求。在MPP集群中执行查询时,会分解为一个树状的执行树,由Coordinator来协调执行,树的叶子节点也叫计划片断(PlanFragment),每个PlanFragment分配一个BE节点的查询执行器。
02
使用简单
Doris不仅架构简单,开发和使用也很简单。对于一款OLAP数据库来说,性能不是优劣评估的全部,易用性才是决定是否可持续使用的关键。Doris从设计之初就一直以易用性为出发点。
数据分析全周期一般包含数据建模、数据导入、用户上手分析、持续使用以及维护升级,无不体现Doris的易用性。
在数据建模方面,Doris支持Aggregate、Unique和Duplicate三种模型,可以满足OLAP领域的各种应用场景。同时,相对于MySQL,Doris只增加了一些分布式系统所具有的特性,比如分布键、分桶等。Doris建表语句如图1-5所示。

图1-5 Doris建表语句示意图
在数据导入方面,Doris提供多种数据导入方案(如图1-6所示),同时在数据导入过程中保证原子性。不论使用Broker Load进行批量导入,还是使用INSERT语句进行单条导入,都是一个完整的事务操作。导入事务可以保证一个批次内的数据原子性生效,不会出现部分数据写入的情况。

图1-6 Doris数据导入方案
同时,每一个导入作业都会生成一个Label,这个Label在数据库内用于唯一区分导入作业。Label用于保证对应的导入作业仅能成功导入一次。一个成功导入的Label再次调用时,会被拒绝并报错:Label already used。通过这个机制,数据消费侧可以实现At-Most-Once语义。如果结合上游系统的At-Least-Once语义,我们可以实现端到端数据导入的Exactly-Once语义。数据导入流程如图1-7所示。

图1-7 Doris数据导入流程
在SQL应用方面,Doris支持标准的SQL语言,在方言方面与MySQL兼容。不论简单的单表聚合、排序过滤,还是复杂的多表关联、子查询等,Doris都可以通过SQL语句轻松完成,极大地降低了用户的学习迁移和使用成本。高吞吐的即席查询和库内ETL也是Doris的强项。Doris还支持复杂的SQL语法,包括Grouping Set等高级语法功能,还支持通过UDF或UDAF自定义。在TB级别数据上,Doris可以代替部分Hive等离线系统功能,使得用户在一套数据库中满足所有需求。
在工具方面,Doris在FE节点实现了兼容MySQL协议,方便用户使用标准的MySQL客户端或各种语言的类库进行连接,对各种工具的支持都非常好。在数据库开发方面,Doris支持用户无缝使用DBeaver、DataGrip、Navicat等主流开发工具;在编程应用方面,Doris完全支持MySQL的JDBC和ODBC接口,支持C、Python、Java、Shell等开发语言;在BI应用方面,Doris支持帆软BI、观远BI、永洪BI、Tableau等各种敏捷BI软件;在ETL调度方面,Doris支持Kettle、DolphinScheduler等主流软件。
在集群可靠性方面,Doris使用“内存存储+检查点+镜像日志文件”模式,使用BTBJE(类似于Raft)协议实现元数据的高可用和高可靠。Doris内部自行管理数据的多副本并自动修复,保证数据的高可用、高可靠。在部分服务器宕机情况下,集群依然可以正常运行,数据也不会丢失。Doris部署无外部依赖,只需要部署BE和FE模块即可搭建集群。Doris支持在线更改表(加减列、创建Rollup),不会影响当前服务,不会阻塞读写等操作。
在集群扩缩容方面,Doris基于分布式管理框架,自动管理数据副本的分布、修复和均衡。比如对于副本损坏情况,Doris会自动感知并进行修复。而对于节点扩缩容,Doris会自动进行数据分片均衡,整个过程完全不影响其他服务,无须运维人员进行任何额外的操作。
在集群升级方面,Doris只需要替换二进制程序,滚动重启集群即可。在设计上,Doris完全向前兼容,支持通过灰度发布方式进行新版本的验证和测试。而Doris本身的一些失败重试和故障路由功能也极大地降低了集群升级过程中发生的错误对业务的影响。
03
功能丰富
Doris提供了非常丰富的功能来应对不同的业务场景。下面重点介绍一些Doris特色功能。首先是分区分桶裁剪功能。Doris支持两个层次的数据划分:第一层是分区(Partition),支持Range和List的划分方式;第二层是分桶(Bucket),将数据通过Hash值进行水平划分,数据分片(Tablet)在集群中被均匀打散。Doris数据分布示例如图1-8所示。

图1-8 Doris数据分布示例
利用分桶裁剪功能,Doris可以将查询固定到极少数分片上,从而有效降低单个查询对系统资源的消耗,提升集群整体的并发查询能力。在高并发查询场景中,Doris单节点可以支持每秒上千的查询请求。
其次是合理的缓存功能。Doris支持SQL级别和Partition级别的查询缓存,如图1-9所示。其中,SQL级别的缓存以SQL语句的Hash值为Key,直接缓存SQL查询结果,非常适合更新频率不高,但是查询非常频繁的场景。而Partition级别的缓存会智能地将SQL查询结果中不同分区的数据进行存储,之后的查询中可以利用已缓存分区的数据及新分区实时查询数据得到最终结果,从而降低重复数据查询,减少对系统资源的消耗。

图1-9 高并发查询的数据缓存示意图
再次是支持Bitmap数据类型,利用位图来存储整型数据,并且可以通过位图进行一些集合类操作。Bitmap可以应用于高基数精确去重场景。传统的实时计算去重算法需要在内存中构建Hash表,在基数非常高的情况下会占用大量内存。而使用Bitmap可以将数值类型转换成位图上的0和1,从而极大地降低内存开销,并且对于去重,只需要将多个位图求交集后计算1的个数,从而在有限的内存下快速进行高基数精确去重计算。在用户画像场景中,通过位图的集合运算,我们可以快速获取不同标签组合的人群包。同时,Doris内置了很多Bitmap相关的函数,以计算留存率等,比如通过intersect_count()函数可以方便地计算用户的留存率。
最后是物化视图。物化视图也是Doris的核心特点之一。物化视图是将预先计算(根据定义好的SELECT语句)好的数据集存储在一个对用户透明且有真实数据的视图表格中。物化视图主要是为了满足用户对原始明细数据任意维度分析,快速对固定维度进行分析、查询,在统一视角下对明细、聚合数据进行分析的需求。在Doris中,用户可以使用明细数据模型存储明细数据,之后在明细数据上,选择任意维度和指标创建聚合物化视图,如SUM、MIN、MAX、COUNT等。Doris会保证明细表和物化视图中的数据完全一致。如果导入或删除物理表中的数据,Doris会自动更新,保证原始表和物化视图中的数据一致。同时,物化视图对用户是透明的。Doris会自动根据查询语句,匹配到最合适的物化视图进行查询。通过物化视图功能,Doris支持在一张表中统一明细数据模型和聚合模型,以加速某些固定模式的查询。
Doris还支持基于主键的数据更新。通过Unique模型,用户可以对数据基于主键进行更新。在实现层面,Doris采用Merge-on-Read方式提供更新后的数据。此外,用户还可以使用REPLACE_IF_NOT_NULL聚合方式,实现部分列更新。基于Unique模型,Doris还支持通过Marked Delete和Sequence Column等功能,实现对上游交易数据库数据同步更新,并且保证事务的原子性以及数据同步的顺序性。
04
开源开放
Doris还有一个重要的特点,即完全开源开放。Doris作为Apache基金会的项目,遵守Apache License 2.0。Apache License 2.0作为主流的开源协议,被OSI认定为“受欢迎且被广泛使用的许可证”。
有关Apache License 2.0的具体内容,读者可以在Apache官网查阅,简单来说,包括分发完全自由、允许项目代码被修改、允许作为开源或商业化软件再次发布,一旦授权永久有效,修改代码或衍生代码中需要带有原来代码的协议、专利声明等。这是对任何商业公司和用户都极其友好的协议。
和大多数分析型数据库一样,Doris也是以列存格式存储数据的。数据以列进行连续存储,因为类型相同,因此压缩率极高,节省了磁盘空间。Doris对不同的数据类型还提供了不同的编码方式,如INT类型数据存储会使用BitShuffle编码方式,而字符串类型数据存储会使用字典编码方式。更进一步,Doris还会自动根据列的值的分布情况来切换编码类型,比如对于字符串类型,如果列中的重复值比较多,则不再使用字典编码,而直接切换为Plain Text编码,以避免不必要的空间浪费。
从文件格式看,Doris支持的文件格式和Parquet比较类似。一个数据版本会被分割成最大空间为256MB的Segment,每个Segment对应一个物理文件。Segment通常分为Header、Data Region、Index Region、Footer几个部分。Data Region用于按列存储数据,每一列又被分为多个Page,而Page是Doris的最小数据存储单元,如图1-10所示。

图1-10 Doris文件格式
Index Region负责存储数据的索引。Doris提供了丰富的索引来帮助加速数据的读取和过滤。索引类型大体可分为智能索引和二级索引两种。其中,智能索引是在数据写入时自动生成的,无须用户干预,包括前缀稀疏索引、Min Max索引等。而二级索引是用户可以选择性地在某些列上添加的辅助索引,需要自主选择是否创建,比如Bloom Filter索引、Bitmap索引等。
前缀稀疏索引是建立在排序列结构上的一种索引。存储在文件中的数据是按照排序列有序存储的。基于排序列数据,Doris会每1024行创建一个稀疏索引项,如图1-11所示。索引的Key即当前1024行中第一行的前缀排序列的值。当用户的查询条件包含这些排序列时,Doris可以通过前缀稀疏索引快速定位到起始行。
Min Max索引是建立在Segment和Page上的索引。对于Page级别,Doris都会记录每一列中的最大值和最小值。同样,对于Segment级别,Doris也会记录每一列中的最大值和最小值,如图1-11所示。这样当进行等值或范围查询时,Doris可以通过Min Max索引快速过滤掉不需要读取的行。

图1-11 Doris前缀稀疏索引和Min Max索引示例
当对某一列创建Bloom Filter索引后,Doris会在Page级别创建该列的Bloom Filter数据结构。Bloom Filter索引使用固定空间的位图来快速判断一个值是否存在,非常适合高基数列上的等值查询。
Bitmap索引的Key值是实际的列值,Value值是Key在数据文件中的偏移量。通过Bitmap索引,Doris可以快速定位到列值对应的行号,并且快速取数。该索引比较适合基数较低的列上的等值查询。
除了存储方式和索引结构,Doris在读取逻辑上也有很多优化,比如延迟物化功能会先根据有索引的列,定位一个查询范围,然后根据有过滤条件的列进一步过滤以缩小查询范围,最后读取其他需要读取的列。这种方式可以减少不必要的数据读取,降低查询对I/O的资源消耗。
参考:Doris实时数仓实战:https://weread.qq.com/web/reader/e3432570813ab8029g019a69?




