从 Google Mesa 到 Apache Doris

Apache Doris 原名是 Palo,由百度于 2017 年开源,Palo 这个词来自 OLAP 的反转,寓意这是一个 OLAP 系统。去年 5 月份 Doris 的核心研发团队出来创业成立了鼎石纵横科技,负责维护开源的 Doris 以及闭源强化版 DorisDB。

初次看到它的时候以为是又一个数据仓库产品,没怎么关注,直到最近才发现和我们熟悉的 Greemplum、Impala 等等有不少区别,其中最有特色的是它的数据模型,借鉴自 Google 2014 年公开在 VLDB 上的 Mesa。本文的前半部分也会聊聊 Doris/Mesa 的数据模型是怎样的。

Mesa:预聚合数据模型

Mesa 是为了解决 Google 广告业务的实时分析需求而诞生的。广告业务的特点是其数据量特别大,每次广告的展示、点击都会产生一条数据,存储这些原始数据不但会消耗大量的存储资源,也会给实时计算聚合结果(比如“某个广告主截止目前已经消费了多少预算”)带来很大困难。

Mesa 为了解决上面提到的两个问题,提出了一个预聚合的存储模型。Mesa 中的所有表都是预聚合表,以下图中的 Table A 为例:其中竖线之前的 Date、PublisherId、Country 三列是 Key 列,表示聚合的维度,语义等同于 Group-By;竖线之后的 Clicks 和 Cost 列是 Value 列,表示被聚合的结果。例如第一行表示 2013-12-31 这一天,ID 为 100 的 Publisher 在 US 一共发生了 10 次点击、价值 32 块钱。

Example of Mesa Tables

你可能已经发现了,上面的 Table A~D 其实表示的是同一批原始数据的在四个不同维度的聚合结果,供不同的业务查询使用。是不是有点像 MOLAP 或者说 Data Cube 的概念?本质上是一样的,都是用预先定义和计算聚合(简称“预聚合”)来加速特定模式的查询。

和很多数仓产品一样,Mesa 只支持按 batch(或 micro-batch)进行更新。更新具有原子性保证,因此不用担心上面各个表的数据不一致。每个更新版本包含这个 batch 内发生的所有变化值(delta)。Mesa 要求所有的 Value 列都需要定义它的聚合函数,因此 delta 就能和之前的数据进一步合并。

Example of Updates in Mesa

Mesa 在后台会异步地对每次导入的 delta 数据做 Compaction。为了让更新和 Compaction 的效率更高,也为了支持一定时间范围内的历史读能力,Mesa 的 Compaction 分为两层,第一层是对近期数据(比如当天的数据)的合并,称为 Cumulatives,第二层是对某个时间点之前(比如今天以前的)的所有历史数据的合并,称为 Base。下面是一个 Compaction 策略的例子:

Example of Compaction Policy

这样的设计让 Mesa 能够快速查询实时聚合结果,而不像传统 Data Cube 那样需要在全量数据上重新 Build。查询聚合结果时,我们选出最小覆盖集(spanning set)进行二次聚合即可,比如上图的例子中,为了查询版本 92 的聚合结果,我们只需要读取 0-60、61-90、91、92 这 4 个文件并合并即可。

个人认为论文主要的贡献就是这个预聚合数据模型的定义和实现,其他特性诸如高可用设计、存储格式、跨 DC 部署架构等,有兴趣的同学请自行读论文。

Doris:混合的数据模型

一言以概之:Apache Doris = 一般 MPP 数仓 + 借鉴自 Mesa 的预聚合模型

Doris 的诞生背景和 Mesa 非常相似,都是来自广告业务的实时报表需求,从这篇文章看来,2012 年百度从 Google 挖来一名高 T,“带来了当时业界最领先的底层报表引擎技术”,带着团队做出了 Palo 也就是今天的 Doris。

Apache Doris Architecture

Doris 的数据模型稍复杂一些,支持以下三种模型的表:

  1. Aggregate 表:需要定义 Key 列和 Value 列,Key 列相同的数据行会自动合并,合并时 Value 列的数据按预先定义好的聚合函数进行聚合
  2. Duplicate 表:不会自动合并 Key 相同的数据,其语义类似于其他数据库中的关系表,Key 表示表的排序键(sort key)而不是唯一键
  3. Uniq 表,Key 列相同时新的行覆盖(Replace)旧的行,本质上是一种特化的 Aggregate 表

官方文档对如何选择上述 3 种模型给出的建议如下:

  1. Aggregate 表可以通过预聚合,极大地降低聚合查询时所需扫描的数据量和查询的计算量,非常适合有固定模式的报表类查询场景
  2. Uniq 表针对需要唯一主键约束的场景,可以保证主键唯一性约束。但是无法利用 ROLLUP 等预聚合带来的查询优势
  3. Duplicate 适合任意维度的 Ad-hoc 查询。虽然同样无法利用预聚合的特性,但是不受聚合模型的约束,可以发挥列存模型的优势

这个模型给我一种缝合怪的感觉。Aggregate 模型显然直接对应于 Doris 最初的使用场景(广告数据实时聚合),而另外两个 Uniq 和 Duplicate 模型则是对应于其他数据仓库中的关系表。之所以这么设计,猜测是因为还有很多使用场景无法用预聚合模型表示,例如维表和明细表,它们天然地不包含聚合的语义。

Aggregate 表带来了一些令人困惑的特性。Doris 通过 SQL 接口进行查询,在 SQL 语义中,Aggregate 表也会被视为普通的关系表。继续以上文 Mesa Table A 为例,在 Doris 中,用户执行下面的这些语句是“正确”的:

1
SELECT SUM(Clicks) FROM PublisherClicks GROUP BY `Date`, PublisherId, Country

但是用户也完全可以执行这样的语句:

1
SELECT MAX(Clicks) /* <- 奇怪的语义 */ FROM PublisherClicks GROUP BY `Date`, PublisherId, Country

由于 Clicks 本身已经是加和的结果,对它取 MAX 并没有实际的意义。更糟糕的是,计算 MAX(Clicks) 的代价要比计算 SUM(Clicks) 大的多,为何会这样呢?上一小节说 Mesa 的时候提到,每次导入的 delta 数据不会立即和基线数据合并,而是会先以单独的文件存在,积累一定数量后再做 Compaction。而计算 MAX(Clicks) 时必须基于完全合并(对每一个 Key 计算 SUM(Clicks))的数据,才能正确算出 MAX 的结果,代价很高。

文档中特别以 COUNT(*) 的例子阐述了这个问题,目测不少用户在这里踩过坑。

在 Mesa 中没有这个问题,Mesa 仅仅提供了针对预聚合查询(MOLAP)的特殊查询接口而非 SQL,如果用户需要和其他数据作关联,则需要通过 F1 Query 之类的联合查询引擎。个人觉得 Doris 的 Aggregate 表有点弄巧成拙的意思,语义上比较奇怪。

ROLLUP 与物化视图

Mesa 允许用户指定同一份数据的多种维度的预聚合表,并能保证更新时的原子性。这一套设计同样也被搬到了 Doris 中。不过,在 Mesa 中,这些更新数据是由外部系统构建的,Mesa 本身仅仅提供增量聚合的能力。

而在 Doris 中,用户需要创建一个数据最详尽的 Base 表,然后再在上面创建不同的 ROLLUP,以获得更 high-level 的聚合结果。这个 ROLLUP 的概念和 SQL 中的 ROLLUP 语法没关系,它的语义是以另一个维度对 Base 表进行进一步的聚合,当 Base 表发生更新时 Doris 也会自动地同步更新 ROLLUP 数据。例如,对于上面 Mesa 的例子,我们先定义一个包含所有 Key 列和 Value 列的 Aggregate 表,并在此基础上创建 ROLLUP:

Example of Doris Table and Rollup

创建 ROLLUP 的前提是 Base 表必须是 Aggregate 表,这样 ROLLUP 才知道如何聚合各个 Value 列。ROLLUP 的 Key 列也必须被包含在 Base 表的 Key 列中,但顺序可以和原来不一致。

ROLLUP 的引入是 Doris 的创新点之一,它确实简化了数据导入的流程。在 Mesa 中用户还需要再构建一个 pipeline 生成增量数据,而 Doris 通过引入 ROLLUP “内置” 了这一过程。但是另一方面,ROLLUP 必须基于一个包含所有聚合维度的 Base 表(比如上面的 Base 表包含 Date、Publisher、Advertiser、Country 这些维度),真的有必要这样吗?

为了解开 “ROLLUP 必须基于 Aggregate 表”的这个奇怪限制,Doris 后来又引入了物化视图的概念,允许用户基于明细表(Duplicate 表)定义预聚合。Doris 的物化视图仅支持定义聚合(Group-By),并且对聚合函数也有所限定。Doris 的物化视图和 ROLLUP 一样都是增量更新的,内部很有可能是相同的实现。

1
2
3
4
5
6
-- 创建 ROLLUP 的语法
ALTER TABLE ads ADD ROLLUP `PublisherRollup` (`Date`, PublisherId, Clicks, `Cost`)

-- 创建物化视图的语法
CREATE MATERIALIZED VIEW `PublisherMView` AS
SELECT `Date`, PublisherId, SUM(Clicks), SUM(`Cost`) FROM ads GROUP BY `Date`, PublisherId

个人认为,把预聚合模型抽象为物化视图要比 ROLLUP 更优雅、更符合 SQL 语义,可惜在文档的编排中这个功能似乎只是被当作一个辅助出现的。依我看还不如把 ROLLUP 特性废弃掉算了。

如果用户的查询中用到了预聚合的值,查询优化器可以自动选择 ROLLUP 或物化视图来加速查询,这部分的实现是基于规则(比如最长匹配原则)而非基于代价的,有兴趣的读者可以去看文档

Doris as a MPP Data Warehouse

好了,至此为止,我觉得有意思的部分就讲完了。Doris 的其他部分是一个类似 Impala 或 Greemplum 的中规中矩的数据仓库产品,没有太多亮点。不过既然是 MPP 数仓就意味着它能执行各种各样的 SQL,像 TPC-H、TPC-DS 这样的 Ad-hoc 查询当然也不在话下。

快速地过一遍其他特性:

  • 部署架构:分为 FE(前端)和 BE(后端)两个组件
    • FE 负责接受用户请求、优化、调度查询,由 Java 编写
    • BE 负责存储数据、执行 MPP 计划中的各个片段,类似于 Worker 的角色,由 C++ 编写
    • FE 还内置了 BerkeleyDB 用于保存元数据,并通过多副本保证高可用
  • 分区方式:支持逻辑和物理两层分区
    • 逻辑分区通常是时间日期,方便冷热数据分离,数据仓库标配
    • 物理分区通常是哈希,用于打散数据、均摊负载
  • 存储格式:毫无疑问用的是列存,类似 ORC 格式
    • 通过 sort key 支持点查
    • 多副本保证高可用性
  • 支持向量化(含 SIMD),不支持 JIT
  • 支持 Online Schema Change

后记

翻看几篇 Doris 应用实践(比如这篇这篇这篇),发现几个有意思的共通点:

  1. 业务方普遍选择将明细数据也保存在 Doris 中,而不是仅有聚合数据,其用法更接近于一般数仓而不是 Mesa。
  2. 预聚合模型是对 Ad-hoc 的很好补充,对于实时报表等场景有极大提升。其他的实时数仓产品例如 GP、Impala、ClickHouse、TiDB(雾)是否也可以借鉴一下呢?
  3. Aggregate 表常被用于统计 UV(某 URL 被多少个不同的用户访问过),被聚合的 Value 是用户 ID 的 bitmap,这可能是 Doris 团队自己都没有想到的。

最后发表下我的观点:Doris 借助物化视图等概念将预聚合(MOLAP)能力引入到 ROLAP 体系中,并且通过分层合并做到快速更新、快速查询,是对实时数仓系统的一个很好的增强

References

  1. Apache Doris 官方文档
  2. Mesa: Geo-Replicated, Near Real-Time, Scalable Data Warehousing
  3. Apache Doris (Incubating) 原理与实践
  4. Apache Doris在美团外卖数仓中的应用实践
  5. Apache Doris在京东广告的应用实践
  6. Apache Doris 在 WeLab实时大数据平台的应用实践
  7. Doris简史-为分析而生的11年