跳到主要内容

读写分离场景下缓存优化最佳实践

在使用 Apache Doris 的存算分离架构时,特别是部署了多个计算组(Compute Group)来实现读写分离的场景下,查询性能高度依赖于 File Cache 的命中率。当只读计算组(Read-Only Compute Group)的缓存未命中(Cache Miss)时,需要从远端对象存储拉取数据,会导致查询延迟(Query Latency)显著增加。

本文档旨在详细阐述如何通过缓存预热及相关配置,有效减少因 Compaction数据导入(Data Ingestion)以及Schema Change等常见场景引起的缓存未命中问题,从而保障只读集群的查询性能稳定性。

核心问题:新数据版本(Rowset)引发的缓存失效

在 Doris 中,无论是后台的 Compaction / Schema Change 还是前台的数据导入,都会生成新的数据文件集合(Rowset)。这些新 Rowset 在负责写入的计算组(Write-Only Compute Group)的节点上,其数据会默认被写入本地的 File Cache 中,因此该计算组的查询性能不受影响。

然而,对于只读计算组而言,当它同步到元数据并感知到这些新 Rowset 的存在时,其本地缓存中并没有这些新数据。此时若有查询需要访问这些新 Rowset,就会触发缓存未命中,导致性能下降。

为了解决这一问题,核心思路是:让数据在被查询之前,提前或智能地加载到只读计算组的缓存中。

一、 缓存预热机制概览

缓存预热(Cache Warm-up)是主动将远端存储中的数据加载到 BE 节点的 File Cache 中的过程。Doris 提供以下三种主要的预热方式:

1. 主动增量预热 (推荐)

这是一种更为智能和自动化的机制。它通过在写入计算组和只读计算组之间建立预热关系,当写入/Compaction 等事件产生新 Rowset 时,会主动通知并触发关联的只读计算组进行异步的缓存预热。

适用场景:

  • 大部分场景。
  • 用户有权限配置预热关系。

[文档链接]:关于如何配置和使用主动增量预热的详细信息,请参考官方文档 FileCache主动增量预热

2. 只读计算组自动预热

这是一种轻量级的自动预热策略。通过在只读计算组的 BE 节点上开启配置,使其在感知到新 Rowset 时,自动触发一个异步的预热任务。

适用场景:

  • 用户无权配置预热关系
  • 用户使用的是非MoW表

核心配置: 在只读计算组的 be.conf 中设置:

enable_warmup_immediately_on_new_rowset = true

二、 优化 Compaction / Schema Change 对查询性能的影响

后台 Compaction 会合并旧的 Rowset 并生成新的 Rowset。如果新 Rowset 未被预热,只读计算组的查询性能会因 Cache Miss 而抖动。以下是两种推荐的解决方案。

方案一:主动增量预热 + 延迟提交(推荐)

该方案可以从根本上避免只读计算组查询到未被缓存的、由 Compaction / Schema Change 产生的新 Rowset。

实现原理:

  1. 首先,配置好写入计算组和只读计算组之间的主动增量预热关系。
  2. 写入计算组的 BE 节点上,开启 Compaction / Schema Change 延迟提交功能。

核心配置 (写入计算组 be.conf):

enable_compaction_delay_commit_for_warm_up = true
  1. 工作流程:
    1. Compaction / Schema Change 任务在写入计算组上完成,并生成了新的 Rowset。
    2. 此时,该 Rowset 不会立刻提交生效(即对只读计算组不可见)。
    3. 系统会触发关联的只读计算组对这个新 Rowset 进行缓存预热。
    4. 待所有关联的只读计算组都完成了预热后,这个新 Rowset 才会被最终提交,并对所有计算组可见。

优势:

  • 无感知切换:对于只读计算组来说,所有可见的 Compaction 后数据均已在缓存中,查询性能不会出现抖动。
  • 高稳定性:是保障读写分离场景下查询性能最稳健的方案。

方案二:只读计算组自动预热 + 查询感知

该方案通过在查询层进行智能选择,尽量跳过尚未预热完成的新 Rowset(对于Unique Key MoW表,考虑到正确性问题,compaction产生的rowset无法跳过)

实现原理:

  1. 只读计算组的 BE 节点上,开启自动预热。

核心配置 (只读计算组 be.conf):

enable_warmup_immediately_on_new_rowset = true
  1. 在查询时,通过 Session 变量或用户属性开启 "预热感知" 的 Rowset 选择策略。

设置查询会话:

SET enable_prefer_cached_rowset = true;

或设置用户属性:

SET property for "jack" enable_prefer_cached_rowset = true;
  1. 工作流程:
    1. 当只读计算组感知到 Compaction 产生的新 Rowset 时,会异步触发预热任务。
    2. 开启 enable_prefer_cached_rowset 后,查询执行器在选择要读取的 Rowset 时,会优先选择那些已经预热完成的版本。
    3. 它会自动忽略那些还在预热中的新 Rowset,前提是这种忽略不影响数据的一致性(即依然可以访问合并前的旧 Rowset)。

优势:

  • 配置相对简单,无需配置跨计算组的预热关系。
  • 能有效降低大部分情况下的性能影响。

注意事项:

此方案是一种“尽力而为”的策略。如果新 Rowset 对应的旧 Rowset 已经被清理,或者查询必须访问最新的数据版本,查询依然需要等待预热完成或直接访问冷数据。

三、 优化数据导入对查询性能的影响

高频的数据导入(如 INSERT INTO, Stream Load)会持续产生新的小文件(Rowset),同样会给只读计算组带来 Cache Miss 问题。如果您的业务可以容忍秒级甚至亚秒级的数据延迟,可以采用以下组合策略,以极小的“新鲜度”代价换取巨大的性能提升。

实现原理: 该策略通过结合自动预热查询时的新鲜度容忍度设置,让查询执行器智能地跳过在指定时间窗口内尚未预热完成的最新数据。

实施步骤:

  1. 开启预热机制
    1. 在只读计算组上开启主动增量预热只读计算组自动预热(enable_warmup_immediately_on_new_rowset=true)。这是让数据能够被异步加载到缓存的前提。
  2. 设置查询新鲜度容忍度
    1. 在只读计算组的查询会话或用户属性中,设置 query_freshness_tolerance_ms 变量。
    2. 设置查询会话:
      -- 设置可以容忍 1000 毫秒(1秒)的数据延迟
      SET query_freshness_tolerance_ms = 1000;
      或设置用户属性:
      SET property for "jack" query_freshness_tolerance_ms = 1000;

工作流程:

  • 当一个查询开始执行时,它会检查需要访问的 Rowset。
  • 如果某个 Rowset 是在最近 1000ms 内生成的,并且尚未预热完成,查询执行器会自动跳过它,转而访问较旧但已缓存的数据。
  • 这样,绝大多数查询都能命中缓存,从而避免了因读取最新写入的冷数据而导致的性能下降。

回退机制:

如果某个 Rowset 的预热过程非常缓慢,超过了 query_freshness_tolerance_ms 设置的时间(例如超过1000ms仍未完成),为了保证数据的最终可见性,查询将不再跳过它,而是会回退到默认行为:直接读取冷数据。

优势:

  • 性能提升显著:对于高吞吐写入场景,能有效消除查询性能毛刺。
  • 灵活性高:用户可以根据业务需求,在数据新鲜度和查询性能之间做出灵活的权衡。

总结与建议

方案适用场景预期效果(各类写操作对cache命中率的影响)
开启主动增量预热+延迟提交+ 配置数据新鲜度容忍时间(可选)适用于查询 latency 要求非常高的场景,需要用户有权限配置预热关系compaction:无
重量级 schema change:无
新写入的数据:取决于新鲜度容忍时间
只读计算组自动预热+优先cache数据 + 配置数据新鲜度容忍时间(可选)用户无权配置预热关系
没有配置新鲜度容忍时间时对于 MOW 主键表无效
compaction:无
重量级 schema change:cache miss
新写入的数据:取决于新鲜度容忍时间

通过合理地运用上述缓存预热策略和相关配置,您可以有效地管理 Apache Doris 在读写分离架构下的缓存行为,最大限度地减少因缓存未命中带来的性能损失,确保只读查询业务的稳定与高效。