OceanBase 4.3 正式推出列存功能,打造满足实时分析业务的列存能力。本文将作为《列存能力深入剖析解读》的延伸,进一步探讨列存在 OceanBase 数据库架构中应用和演进,以及未来的发展方向。
1970 年,关系模型之父 Codd 提出关系模型,正式开启了数据库的时代。1979 年,Oracle 发布第一个商业数据库版本,数据库技术开始广泛应用于各行各业。在那个数据量还不是特别大、查询也相对简单的年代,单一的数据库系统能够满足用户的需求。
随着时间的推移,数据量急剧增加,查询也变得越来越复杂。单一的数据库已经无法满足用户的事务处理和分析处理需求。因此,Codd 于 1993 年正式提出 OLAP(联机分析处理)概念,并提出了 12 条准则。从此,OLTP(联机事务处理)和 OLAP 开始分道扬镳,在两个领域出现了许多数据库产品。大约十年后的 2005 年,StoneBraker 提出了第一个基于列存的数据库原型 CStore,证明了列存在分析领域的巨大潜力,自此列存储成为 OLAP 数据库的标准配置。然而,值得注意的是,尽管数据库产品在 OLTP 领域主导地位稳固,但在 OLAP 领域,全球技术代表性产品层出不穷,比如 GreenPlum(2006 年)、SnowFlake(2014 年)、DataBrick(2014 年)、ClickHouse(2016 年)等。
尽管 OLTP 和 OLAP 数据库各自在自己的领域占据主导地位,但用户对 OLTP 和 OLAP 的需求却是同时存在的。为了支持业务,用户通常需要使用两套数据库系统,一套用于 OLTP,一套用于 OLAP,并通过数据同步组件进行 OLTP 到 OLAP 的数据同步。然而,这种方式会带来一系列问题:
首先,不仅数据冗余了一份,系统也冗余了一份。尽管面向 OLAP 的系统可以使用相对廉价的存储,但 CPU 和内存的冗余消耗仍然存在。此外,多了一套系统,也意味着增加了一套系统运维的成本。
其次,OLAP 和 OLTP 系统之间的数据同步总会存在延迟,并且很难保证延迟时间。 一旦 OLAP 系统或数据同步组件出现问题,数据修复可能需要数天时间,这段时间内 OLAP 的业务就一直处于不可服务的状态。
最后,随着互联网技术的发展,对 OLAP 实时性的要求也越来越高。 设想这样一个场景,用户在网上购物下单,线上交易这是再经典不过的 OLTP 场景。而系统希望能够根据用户下单的商品以及其他相关信息,自动推荐出用户可能继续加购的商品以增加成交额,这则是典型的 OLAP 场景。用户在 APP 或者页面上的浏览速度几乎是电光火石之间,等数据同步到 OLAP 系统再进行响应,可能就来不及了。
因此,尽管相比上个世纪,数据库承载的数据量增加了成百上千倍,之前一套简单系统的美好时光还能回来吗?2016 年,Gartner 正式提出了 HTAP(混合事务/分析处理)的概念,成为这个问题最好的答案,即无需增加实体,如果一套系统能够满足大部分的 OLTP 和 OLAP 需求,就不需要搭建两套复杂系统了。
相对来说,行存储更适合 OLTP 类负载,而列存储更适合 OLAP 类负载。一套支持 HTAP 实时分析的数据库通常需要同时支持行存储和列存储。尽管相比分别部署 OLTP 和 OLAP 数据库,使用一套 HTAP 数据库可以解决同步延迟和数据实时性的问题,但数据冗余的问题似乎并没有得到解决。然而,实际上,使用一份数据来实现 HTAP 是可能的,这取决于我们如何看待和使用列存。
列存副本方案是一种较为直接的 HTAP 实现方式,它相当于在单一系统内构建了两套独立的引擎:一套基于行存储的引擎面向 OLTP,另一套基于列存储的引擎面向 OLAP。这种方案对用户屏蔽了数据同步的细节,并能提供无延迟的 OLAP 数据访问。Google F1 Lightning 和 PingCAP TiDB 等业内优秀的数据库都采用了类似的方案。
如图所示,左侧的三个副本(Node 1/2/3)基于行存储引擎提供 OLTP 能力,而右侧的副本 Node 4 则是一个列存储引擎,提供 OLAP 能力。两个引擎之间通过 Raft/CDC 进行日志数据同步。该方案的优点在于可以提供较好的隔离性,OLAP 引擎的数据访问不会影响 OLTP 引擎本身的稳定性。
当然,这种方式也存在一定的弊端,那就是成本高昂,尤其是对大体量的数据场景非常显著。不仅数据本身会额外冗余一份,用于支持列存引擎的 CPU 和内存也需要冗余,而且运维成本也丝毫没有减少。此外,作为独立的列存引擎,一旦出现问题,也需要专门的人员进行处理。
通过列存索引的方式来实现 HTAP,比较典型的代表是 SQL Server。尽管早在 2012 年就推出了 Column Index (列存索引)功能,但当时的版本仅支持只读,无法满足用户的更新需求。直到 2016 年,SQL Server 可更新的列存索引正式发布,这项特性开始为用户提供更加友好的体验。
如图所示,SQL Server 内部也单独开发了一套列存存储引擎,与原有的行存引擎并行工作。SQL 层会统一对接底层的不同引擎,如果表是行存的,则使用行存引擎存储数据;如果表上还构建了额外的列存索引,那么就会对这些列存索引使用列存引擎存储。行存和列存可以同时存在,也可以同时构建多个列存索引。这种方式具有很高的灵活度,可以根据需要只针对特定的列构建列存索引,数据冗余程度也远低于列存副本方案。此外,SQL Server 在执行 SQL 语句时可以同时利用列存和行存的能力,极大地提升了执行效率。
具体到实现层面,SQL Server 的列存存储不会按照主键顺序排序,而是类似于堆表的方式进行组织,将固定数量的行组成一个 Row Group。在每个 Row Group 中,每个列都会单独存储到不同的 Segment 中。Row Group 一旦生成便不再修改,删除操作通过 Delete Bitmap 标记完成,更新操作则通过 Delete + Insert 完成。后续的 Insert 操作会被放入 Delta Store,查询时需要将列存数据、Delete Bitmap、Delete Buffer 和 Delta Store 中的数据进行合并得到最终结果。
SQL Server 的列存方案很好地解决了延迟、实时性以及成本等问题,但对于索引组织表来说,列存索引仍然在很大程度上依赖于行存,主键约束和唯一键约束的维护也需要依靠行存来完成。不仅如此,Delta Store 和 Delete Bitmap 的维护也并非没有代价,列存索引的引入会对行存 OLTP 的性能造成一定影响。
Oracle 的做法是将列存作为缓存实现 HTAP 混合负载。2013 年,Oracle 发布了 12C 版本,并推出了名为 IMC(In-Memory Column Store)的特性。
从严格意义上讲,IMC 更像是基于行存的列存加速缓存,而非完整意义上的列存。Oracle 允许在列、分区、表、表空间等不同粒度上开启 IMC,灵活度很高。如果对某张表的某些列开启了 IMC,Oracle 会将这些列的数据从行存中加载到内存中,并以列存的形式存储。但需要注意的是,数据仍然存储在行存中,列存数据不会直接落盘。后续的增删改等修改操作会通过内部刷新机制更新到列存。在 Oracle 的内存管理中,SGA 中的 Buffer Cache 承担了主要的增删改查等事务操作。如果要开启 IMC,则需要在 Buffer Cache 之外额外分配一块单独的内存区域。
这个做法避免了磁盘数据冗余的代价,也可以向用户提供实时无延迟的 OLAP 能力,并且提供了一定的灵活性,用户可以根据自身需要对列存进行灵活的配置。然而,其问题也很明显,内存的代价并未减少,而且相比于磁盘来说,内存总是更加宝贵,用昂贵的内存来支持 OLAP 能力总体成本较高。此外,OLAP 要处理的数据量通常非常庞大,将所有数据都存储在内存中并不现实。一旦需要访问磁盘,就需要将数据从行存中读出并转换成内存列存。在这种场景下,列存相较于行存可以减少 I/O 代价的优势也就无法体现了。
无论是 SQL Server 还是 Oracle,其底层存储引擎都基于 B-Tree。如果我们将视角拓宽到 LSM-Tree,就会发现列存与 LSM-Tree 才是天作之合,产生更显著的化学反应。LSM-Tree 中,数据被划分为 MemTable 和 SSTable 两个部分。MemTable 驻留在内存中,支持动态修改,天然适合行存;而 SSTable 存储在磁盘上不可修改,非常适合用来做列存。在 OceanBase 中,SSTable 又会被细分为转储 SSTable 和基线 SSTable。通常,转储 SSTable 用于存储最近修改的数据,而基线 SSTable 则用于存储较老的数据。
OLTP 类负载以短事务为主,主要包括插入、小范围更新、删除和查询最近的数据。这类负载涉及的数据大多位于 MemTable 和转储 SSTable 中。因此,OceanBase 针对 MemTable 和转储 SSTable 使用行存存储,并对基线 SSTable 增加 Bloom Filter 过滤以阻断大部分空查,同时使用 cache 缓存部分热点列存数据以加速热查,从而确保 OLTP 类负载的性能。
OLAP 类负载以大查询为主,涉及的数据大多位于基线 SSTable 中。对于基线 SSTable 的数据,OceanBase 直接使用列存。但与 SQL Server 不同的是,OceanBase 的列存数据并非无序存储,而是整体按照主键顺序排列。这样一来,即使在列存中处理少量 OLTP 类请求,需要寻找单独一行数据,OceanBase 也能够通过二分法快速定位到目标数据行。很多用户在 POC 阶段评价,这是可以支持 OLTP 业务的列存。
通过这种方式,OceanBase 可以通过一份数据同时兼顾 OLTP 和 OLAP。考虑到相对于转储 SSTable 来说,基线 SSTable 通常占据了数据量的绝大部分,且列存相较于行存具有更高的数据压缩率,OceanBase 的架构可以将成本降至最低。然而,该架构也面临一些挑战:
首先,如何做好 OLTP 和 OLAP 多工作负载的资源隔离是一项极具挑战性的任务。 在理想的调度机制下,OLAP 负载可以灵活地从 OLTP 负载处获取资源,并在 OLTP 空载的情况下使用大部分系统资源。这理论上是可以实现的,就像现在大多数数据库都可以部署在 Docker 容器中一样,但很少有人会担心容器对系统资源的隔离能力。然而,这对于特别高等级的隔离需求来说可能还不够。
其次,也可能存在部分查询需求,在基线 SSTable 使用行存时会表现得更好。 或者将若干列混合存储在一起,可能会带来更好的查询性能。
基于 LSM-Tree 架构,OceanBase 可以将数据做列存存储,以提供最极限的成本节省。但这并不意味着列存存储是 OceanBase 的唯一选择,它还可以作为缓存、索引和副本,为用户提供更高的灵活性和无限的可能性。
首先,OceanBase 可以将列存视作缓存,在缓存中存储部分区域的列存数据,以加速热点范围的查询。 其次,OceanBase 可以将列存看做索引,在基线 SSTable 中同时存储行存与列存数据,或者做部分列的聚合冗余存储。根据查询需要,查询列存或者行存,或者更合适的列组。 再次,OceanBase 可以将列存视为副本,在主副本中使用行存,在只读副本中使用列存,以提供更高等级的资源隔离。 最后,可能在不远的未来,除了提供以上的灵活度以外,OceanBase 或许还可以让用户摆脱行存和列存这些底层存储方式的限制,忘掉 OLTP 和 OLAP 等形态,让数据库回归到最初的本质。用户把数据和查询给到数据库,数据库把结果给用户,无论列存还是行存,数据库总是按照最适合负载的形式组织数据,以最快的速度返回结果。当用户觉得查询有些慢又不想做调优时,只需加资源即可。
以上是我们对列存能力的理解和整体规划,OceanBase 的列存功能已经在 4.3 版本中正式推出,更多强大功能正在研发中。我们希望这些新特性能尽快与大家见面,让更多用户体验到列存在实时分析领域带来的优势和便捷。