在蚂蚁集团内部孵化的 HoraeDB(已开源) ,能成功应对当前主流数据库在高基数时序场景下所遭遇的性能挑战。这些挑战包括在数据量极大时性能的严重下降,以及现有数据库分布式方案的不完善或成本问题。为了解决这些问题,HoraeDB 采用了创新的核心设计,提供了一个高效且成本效益高的分布式解决方案,分布式查询性能实现了大约2到4倍的效率提升。详细的解决策略和方法,请参阅文章正文。
作者介绍
温馨提醒:本文约7500字,预计花费12分钟阅读。 背景 一、主流数据库高基数场景下存在哪些核心问题? 1.1 一些相关概念
1.1.1 什么是时序数据
时序数据,简单来说,就是基于时间的一系列数据点的集合。在坐标轴中,我们可以将这些数据点按照时间顺序连成一条线,从而形成一条时间序列。
1.1.1 图 – 两条折线分别代表了两台服务器的负载指标例如图中,这些线条本身并没有太多的描述性,为了区分不同的数据序列,我们通常会使用标签(tag)来标识它们。比如,每条线都有两个标签,分别是 host 和 cluster,通过这些标签,我们可以精确地定位到具体的数据序列。
实际上,时序数据的应用场景是相当广泛的。比如,物联网(IoT)、应用性能管理(APM),以及天气预报、股票市场分析等,这些领域都在广泛地应用时序数据。
1.1.2 什么是时间线
时间线可以被理解为一个标签的组合。在底层存储时,时间线扮演了重要的角色。由于时序数据产生的量通常很大,我们会将具有相同时间线的数据聚集在一起,这样便于进行数据压缩和存储。通过将相同时间线的数据放在一起,我们可以快速检索到一条线的所有数据,这大大提高了数据检索的效率。
1.1.2 图 – 时间线示例图我们可以看到图中左侧展示了带有3个标签的时间线。而右侧则是这些时间线的分布情况。每一个Key代表的是不同的时间线,右侧则展示了每条时间线所对应的数据集。在实际应用中,我们通常会将一个小时内的相同时间线数据汇总到一起,以实现较高的压缩比。
1.1.3 倒排索引
倒排索引是一种高效的检索技术,它允许用户根据输入的条件快速定位到对应的时间线。
例如,如果用户输入了两个标签 metric 和 IP,倒排索引可以帮助我们快速找到所有匹配的时间线。这种技术在搜索引擎中非常常见,而在时序数据库中也有其特定的应用。
为了方便理解倒排索引的逻辑,这里介绍了一个包含两个标签的倒排结构。倒排索引本质上是一个双层映射结构:第一层映射的Key是标签名称,如IP地址或环境名称,对应的value是具体的标签值,例如某服务器的IP。第二层映射则将每个IP关联到一个时间线列表,记录相关事件或数据点。这样的结构允许我们快速定位到特定IP对应的时间线,从而高效地进行数据检索。
1.2 存在的问题
1.2.1 时间线高基数问题
在业界,我们通常所说的“高基数问题”主要指的就是这两个方面的问题:写入性能不佳和查询性能不高效。
在云原生环境中,每一次 Pod 的创建与销毁都可能导致 IP 地址的变化,这会导致时间线的数量级急剧增加,可能达到百万甚至千万级别。这样的高基数不仅会导致倒排索引的体积变得庞大,而且在写入和查询方面都会带来显著的性能问题。写入时,索引的膨胀会降低实时吞吐量;查询时,由于查询可能命中大量时间线,导致需要执行多次 IO 操作,这会严重影响查询效率。
例如上图,用红色线条表示了一个查询所要检索的时间线,图中命中了4条不同的时间线,意味着查询需要对这4条时间线分别执行四次独立的IO操作,如果一个查询命中了数百万条时间线,那么它需要执行的IO操作数量将是巨大的,这样的查询效率无疑是非常低下的。
1.2.2 不完善的分布式方案
除了高基数问题,现有的数据库分布式方案也存在不足。许多时序数据库本质上是单机版,面对大数据量和高负载时,缺乏成熟的分布式解决方案或者需要额外付费购买。
例如,某些著名的数据库系统,其分布式版本是商业化的,需要购买才能使用。而对于像 Prometheus 这样的数据库,尽管提供了分布式方案,但在分布式环境下,数据检索和计算下推的效率并不理想,这限制了查询性能。
因此,HoraeDB 的设计初衷就是为了解决这两个核心问题:高基数下的性能退化和分布式方案的不完善。在接下来的分享中,我将详细介绍 HoraeDB 是如何通过其核心设计来应对这些挑战的。
二、应对上述问题,HoraeDB有哪些核心设计?
2.1 高基数解决方案
在高基数场景下,一个庞大的倒排索引往往会给系统带来巨大的开销。面对这一挑战,我们采取了一种直接而有效的策略:去除倒排索引,并探索其他手段以实现高效的数据检索。值得注意的是,业界已经存在一些采用类似策略的解决方案。
具体来说,我们采用了一种结合列式存储和高效scan、多级剪枝的流程。这种方法的本质在于,既然倒排索引的构建成本如此之高,我们便放弃了传统的倒排索引,转而使用基于概率的索引结构来进行高效的数据过滤。
2.1 图 – 查询定位到所需数据的示意图在这一过程中,我们依赖的是一些如最大值、最小值(max/min)或布隆过滤器(bloomfilter)的索引。这些索引让我们能够快速响应查询请求,通过它们,我们可以迅速定位到所需的数据。
对于时序数据而言,最常见的两个查询条件是数据的起始时间和终止时间。因此,我们对数据进行了基于天的分层排列,通过时间戳,我们可以快速过滤掉不在这个时间范围内的数据。进一步地,根据查询中的其他筛选条件,比如IP地址,结合数据块内记录的最大值和最小值,我们可以更精确地筛选出符合条件的数据。例如,如果查询条件指定了IP地址尾号为1的机器,而我们已经记录了IP地址的最大值和最小值,系统就可以迅速排除不包含该条件的数据块,直接定位到符合条件的数据,从而实现了高效的数据过滤。这种基于列式存储的解决方案,在传统的非关系型数据库中已被广泛采用,而在 HoraeDB 中,我们也采用了类似的策略。
2.2 分布式方案
在设计之初,我们就完全采用了云原生的架构,所有的组件都支持水平扩展。在 HoraeDB Engine 这一主要架构中,它负责处理用户的读写查询操作。这些查询最终会落到底层的存储上,而我们采用的是业界广泛使用的、支持水平扩展的对象存储,如阿里云的 OSS 或 AWS 的 S3。
在分布式集群中,每个 HoraeDB 实例都是独立且独享的,我们采用了 share-nothing 架构,每个实例仅处理它当前负责的数据。当用户的数据量增长时,我们可以动态地增加 HoraeDB 实例,实现水平扩容,来应对数据量的增长。
此外,我们还利用了基于 Raft 协议的 ETCD 来记录表的路由信息,确保数据的高可用性。通过这种方式,HoraeDB 的整个架构,从底层存储到上层的计算节点,都具备了水平扩展的能力,有效解决了分布式存储和计算中的挑战。
三、HoraeDB采用哪些策略优化查询性能?
在我们深入讨论 HoraeDB 的查询优化之前,让我们先来了解 HoraeDB 单机实例的读取路径。每个 HoraeDB 实例都构建在 LSM 系统上,它包含两个主要的内存组件:Memtable,用于承接用户的实时写入;以及 SST ,用于持久化 Memtable 中的数据。当 Memtable 中的数据达到一定阈值后,会 flush 到 SST 中。SST 还负责合并小文件,这一过程称为 compaction,是 LSM 树架构中的典型特性。由于数据同时存在于内存和磁盘中,用户的查询必然涉及这两部分。在后续的分享中,我将重点介绍我们是如何针对这两部分进行优化的。
3.1 优化思路
我们的优化思路可以概括为四个主要环节——
-
首先,由于我们去除了精确的倒排索引,面临的挑战是如何进行快速的数据检索,或者说,如何减少不必要的 IO 操作。这一部分将分为两个子环节:一是针对 Memtable 的优化,二是针对 SSTable 的优化。
-
接下来,我们将采用两种系统优化中常用的手段:增加缓存和提高程序并发性。通过这两种手段,我们可以进一步提升 HoraeDB 单机实例的性能。
-
最后,我将介绍分布式查询的优化。在真实的集群部署中,实例数量可能会非常多,比如在我们的案例中,可能会有上百台机器,设计一个能够实现高效分布式检索的查询引擎,是我们优化工作的重中之重。
3.2 减少 IO
3.2.1 Memtable
在 HoraeDB 的 LSM 系统中,Memtable 是用于承接实时写入的关键组件。由于写入操作相对频繁,Memtable 的设计优先考虑了写入效率,通常采用行存储结构,即数据按行顺序追加到 Memtable 中,以最小化写入成本。
然而,在读取操作中,通常不需要访问行中所有列的数据。用户查询可能只涉及100列中的10列,这就导致了读写模式之间的差异,以及在 Memtable 读取时,频繁地将行存储转换为列存储,这种转换对 CPU 的消耗可能成为系统性能的瓶颈。
为了解决这一问题,我们对 Memtable 进行了优化,实现了 Memtable 的分级。最新的数据段是可写的,采用行存储结构,用于承载最近的写入操作。当这个可读写的数据段达到一定的内存大小时,系统会自动将其转换为列存储格式,形成一个不可变的数据块。这样,只有当查询真正需要时,才进行数据格式的转换。
3.2.1 图2 – 读友好的 Memtable ,CPU 火焰图占比从12% 降到 2%通过这种优化,我们减少了不必要的数据格式转换,直接利用列存储结构进行查询,显著降低了 CPU 的消耗。在一些机器上,我们观察到 CPU 消耗从 12% 降低到了 2% 以内,证明了这种优化的有效性。
3.2.2 SST
SST 面临的问题本质上与 Map 类似,存在 IO 放大的问题,但放大的点有所不同。以一个查询为例,如果查询包含两个筛选条件,比如 IP 和 ENV,SST 中存储了大量数据,我们如何高效地筛选出所需的数据块呢?传统的解决方案依赖于概率性索引结构,如最大值、最小值和布隆过滤器,这些结构对数据的分布有特定要求。如果数据无序,筛选效果将大打折扣,可能导致需要扫描所有 SST,严重放大 IO 操作,进而影响查询性能。
那么,如何提高最大值、最小值和布隆过滤器的筛选效率?我们采取的优化思路是,在 HoraeDB 实例中,我们动态实时统计每张表的查询模式,包括查询频率和查询字段。基于这些统计信息,我们自动对表进行排序。例如,如果用户最常查询某个指标,我们就以该指标为排序键进行排序。这样的排序可以显著提升最大值、最小值和布隆过滤器的优化效果。
以查询尾号为“1”的 IP 为例,如果在 SST 的早期状态下,IP 地址是无序的,那么仅通过最大值和最小值是无法有效过滤数据块的。在下图中,左侧的两个数据块都包含尾号为“1”的 IP,因此无法进行数据库的过滤。
为了解决这个问题,我们在后台动态调整表的排序,比如按照 IP 地址进行排序。排序后,我们可以使用最大值和最小值快速定位到所需数据,而排除其他数据块。这种优化表顺序的方法在业界也是常用的,类似于 Snowflake 中的 Automatic Clustering 技术。这一优化手段在我们早期承接业务时发挥了重要作用。
优化实施前,用户的查询成功率大约只有 60%,即大部分查询因超时而失败。而通过这项特性的上线,用户的查询成功率大幅提升至 90% 以上,意味着大多数查询都能在用户期望的时间内得到及时响应。
3.3 增加缓存
在 HoraeDB 中,缓存是优化读取路径的关键组成部分。通过火焰图分析,我们发现最耗时的步骤是从远端对象存储(如 OSS)拉取数据,这一步骤涉及网络 IO,是明显的性能瓶颈。
数据从远端拉取回来后,接下来的瓶颈是解压操作。为了实现高效的数据存储和压缩比,我们采用了一些 CPU 密集型的解压手段。因此,在查询过程中,解压操作不可避免,且通常是 CPU 密集型的。
为了解决这些问题,我们采取了两个主要的缓存策略:
-
-
本地磁盘缓存:我们首先在系统中增加了一层本地磁盘缓存。根据 LRU(最近最少使用)算法,我们将用户最近查询过的数据缓存到本地磁盘中,从而减少了对远端存储的依赖。这样,后续的相同查询可以直接从本机磁盘中获取数据,大幅提升了数据读取速度。
-
-
-
CPU 解压优化:针对 CPU 在解压数据时的高消耗问题,我们采用了直接的优化思路。我们面临的挑战在于,现有的一些技术栈,如 Apache Arrow 库,将数据的拉取和解压操作混合在一起,这不利于我们插入自定义逻辑。因此,我们的主要工作是适配和修改社区的第三方库,将解压后的数据和相关配置进行缓存。通过这种方式,我们通过 LRU 缓存机制有效解决了 CPU 消耗问题。
通过这些缓存策略的实施,我们显著提高了 HoraeDB 的查询性能,尤其是对于频繁访问的数据,大大减少了延迟,提升了用户体验。
3.4 提高并发
除了缓存优化,我们还面临另一个挑战:冷查询或首次查询的处理。这类查询通常不存在于本地磁盘或内存缓存中,因此我们需要其他策略来提升这类查询的性能。
3.4 图 – 未命中 cache 时(首查),IO 导致的性能问题仍然明显为了解决这个问题,我们采用了提高单个查询并发性的方法。具体来说,我们优化了查询流程,将一次查询操作分配到不同的线程中。对于冷查询,网络 IO 通常是瓶颈,因为需要从远端拉取数据。因此,我们引入了预取机制,通过一个后台线程提前进行数据拉取,同时主线程负责 CPU 密集型的计算工作。这种线程隔离的方法可以避免 CPU 密集型任务影响 IO 密集型任务,从而提高整体查询效率。
此外,我们还实现了对 SST 文件的并发拉取。当系统判断用户需要拉取大量数据(例如 100 M)时,我们会将数据拆分成多个部分,并通过多个后台线程并行拉取。这种方法不仅提高了单个文件的拉取效率,也显著提升了冷查询的处理速度。
通过线程隔离和文件并发拉取这两个策略,我们显著提升了冷查询的处理能力,在线上业务引流过程中,查询性能提高了2到3倍。
3.5 优化分布式查询
上述提到的都是单机版的优化实践,下面重点分享一下真正的难点——分布式查询优化。
为了提升分布式查询性能,我们在 HoraeDB 中引入了分区表的概念,它允许将数据根据特定规则分散存储在多台机器上。目前,我们支持两种分区手段:基于特定标签(如通过哈希)的分区,以及随机(Random)分区策略。
3.5 图 – 一个分区表和其对应的物理子表用户在初次接触随机分区的概念时,可能会感到疑惑:为什么随机分配的方式会比传统的分片方法更有效?实际上,这取决于具体的应用场景。随机分区特别适用于那些没有明显特性的指标,例如用户的行为追踪(trace)数据,这类数据通常不会表现出明显的热点问题。
如果按照某个标签进行分片,可能会在单个机器上产生热点,导致大量的请求集中在这台机器上。这会导致请求被过度拆分,例如,一个包含100行数据的查询可能会被拆分成100个独立的请求,分别路由到100个不同的表中。这种细小的请求碎片化会使得服务器需要处理大量的小请求,这对服务器来说是不利的,因为它降低了处理效率并可能影响性能。
相反,采用随机分区可以很好地避免这种极端情况的发生。通过随机分区,数据的分布更加均匀,避免了热点的产生,从而优化了数据的写入和查询过程。
3.5.1 优化挑战一:单机热点
挑战:
在 HoraeDB 的早期版本中,父表和子表被视为对等的物理资源表。由于 HoraeDB 采用 share-nothing 架构,表只能在特定的实例中打开,这导致了所有表的查询请求都会集中到一个节点上,从而形成了单机热点。即使子表可能分布在多个机器上,请求的入口点仍然成为瓶颈,因为所有读写请求都必须经过同一个节点。
3.5.1 图1 – 父表作为物理表,在固定节点打开造成单机热点解决方案:
为了解决这个问题,我们在第一版的优化中引入了虚拟表的概念。我们将父表升级为虚拟表,这样它就可以在集群中的所有节点上打开,而不再是仅限于一个节点。这种设计允许集群中的任何机器来处理父表的读写请求,从而实现了负载均衡,并消除了单机瓶颈。
3.5.1 图2 – 父表作为虚拟表,在所有节点打开3.5.2 优化挑战二:大量网络IO
挑战:
在分布式系统中,查询引擎必须将查询条件发送到各个子表,并在父节点汇总计算结果。这种做法很容易导致数据量过大,成为瓶颈,尤其是在处理大型表时,容易造成内存溢出(OOM)的情况,影响服务的稳定性。
解决方案:
为了应对这一挑战,我们采用了计算下推的策略。计算下推意味着将计算任务尽可能地在数据所在的位置执行,而不是将所有数据拉回到中心节点进行处理。例如,在执行求和(sum)操作时,如果子表中各有50万条记录,经过计算下推,最终可能只需要返回一条汇总记录。这种方法极大地减少了数据的移动,降低了网络IO,同时也减少了中心节点需要处理的数据量,从而提高了服务的稳定性。
3.5.2 图1 – 采用计算计算下推的策略降低网络IO3.5.3 优化挑战三:SQL 执行过程的优化
挑战:
在深入讨论分布式查询优化之前,我们需要理解 SQL 查询在传统数据库中的执行过程。这一过程大致分为三个阶段:
-
解析阶段:首先,数据库的解析层(Parser)会接收用户的查询请求,并对其进行解析,生成一个抽象语法树(AST)。
-
计划阶段:接着,Planner 模块根据数据库的元数据(Catalog),包括表结构和路由信息,对 AST 进行分析,并生成一个或多个潜在的查询执行计划。
-
执行阶段:最后,选择一个最高效的执行计划,由执行层负责具体数据的检索和获取。
对于分区表而言,查询的执行会涉及多个子表。
解决方案:
为了优化这一过程,我们在查询生成阶段引入了计算下推的概念。这意味着,我们将尽可能多的计算任务下推到子表层面执行。例如,当用户对分区表执行带有聚合函数(如 sum)的查询时,系统会根据表分区的数量生成相应数量的子查询,每个子查询都具备计算能力,减少了数据在父表和子表之间的传输。
此外,我们不仅下推了数据,还包括了 Filter(过滤条件)和各种聚合算子,如 count、max、min、avg 等。这样的优化策略显著减少了父表和子表之间的数据传输量,提升了查询效率。
这种优化思路在业界已被广泛应用,许多知名的数据库系统如 Hbase 和 TiDB 都采用了类似的策略,尽管它们可能使用了不同的术语。在我们的实际生产环境中,这些优化措施显著提升了分布式查询的性能,实现了大约2到4倍的效率提升。
四、应用情况
HoraeDB 起初在蚂蚁集团内部孵化,并广泛应用于我们的主营业务中。它支撑着我们的内部监控平台,同时也服务于流计算任务和投资研究场景,帮助进行资产管理和优化。在金融领域,HoraeDB 结合 RMS 监控系统,为银行业务提供支持,展现出其在金融服务行业中的潜力和价值。
自 HoraeDB 开源以来,我们收到了社区广泛的好评和认可。许多社区用户主动接触我们,并在他们的生产环境中部署使用 HoraeDB。作为一个开源产品,所有相关代码均可在 GitHub 上找到。
https://github.com/apache/incubator-horaedb
Q&A:
!!重要通知!!
15万字稳定性提升经验:《2023下半年最佳实践合集》限量申领!
10万字干货:《数字业务连续性提升最佳实践》免费领取|TakinTalks社区
凭朋友圈转发截图免费课件资料
并免费加入「TakinTalks读者交流群」
添加助理小姐姐
声明:本文由公众号「TakinTalks稳定性社区」联合社区专家共同原创撰写,如需转载,请后台回复“转载”获得授权。
更多故障治理内容
本篇文章来源于微信公众号:TakinTalks稳定性社区
本文来自投稿,不代表TakinTalks稳定性技术交流平台立场,如若转载,请联系原作者。