Doris查询缓存调优实战:从重复查询烧掉集群到命中率95%
1. 重复查询是怎么把集群资源烧掉的一个常见的性能事故场景接手公司数据平台性能优化之后我做的第一件事不是调参数而是拉审计日志。原因很简单不看实际查询永远不知道集群资源到底被什么吃掉了。当时线上跑的是一套基于 Apache Doris 构建的报表平台服务着业务方二十多张固定看板和若干临时分析查询。看起来规格不低——三个 BE 节点、每个 64G 内存、SSD 存储——但每天早上八点到十点P95 延迟能从 800ms 一路飙到 8 秒以上CPU 经常冲到 90% 以上。第一反应是分桶或者分区分得不够细于是去查了大查询和慢查询日志结果发现所有慢查询都不是什么特别复杂的 SQL只是普通的SELECT SUM(amount) FROM orders WHERE dt BETWEEN ... AND ...之类的分组聚合。真正的问题出在重复次数上——同一张订单表32 亿行数据一条日报 SQL 在早高峰两小时内被执行了 2100 多次。也就是说同样的计算逻辑、同样的数据范围、同样的聚合操作集群在短短两小时内白算了 2100 遍。这种场景在大数据平台里实在太常见了。BI 报表、可视化大屏、数据门户本质上都是同一批固定 SQL 被反复触发。每一次执行Doris 都要重新读取底层数据、重新执行谓词下推、重新做聚合和排序完全不做任何“记忆”。于是 CPU 被无意义的重复计算耗尽查询延迟随之恶化而真正需要资源的临时分析查询反而抢不到资源。1.1 从数据库压力曲线看到的规律慢查询和重复查询强相关把审计日志按 SQL 文本去重之后你会发现一个残酷的事实线上查询的活跃 SQL 面往往很窄但每个 SQL 的执行次数高得惊人。我当时统计过活跃 SQL 中文本完全相同的查询占了总查询次数的 62%这些查询贡献了 70% 以上的累计扫描行数。压力曲线上每一个尖峰几乎都对应着固定报表的刷新时刻。更麻烦的是这类查询很难通过“扩大并发”来解决。你扩的是并发但它消耗的是 CPU 和磁盘 IO每个查询扫一遍几亿行数据并发越高争抢越严重。就算加机器也只是把同样的浪费从一个节点搬到三个节点资源利用效率依旧低下。我当时就想如果能把重复计算的结果直接复用问题就转化成了“查一次缓存”而不是“算一次查询”。这其实就是 Doris 查询缓存机制想解决的核心问题。相比 Presto、Hive 这类引擎Doris 的缓存设计更贴近 OLAP 场景的重复查询特征后面会详细拆解它的两条缓存路径和适用边界。1.2 为什么加机器加索引都治不了重复查询的根先说加索引。索引解决的是“扫描量太大”的问题它能让查询更快地定位到目标数据但聚合计算本身——比如对几亿行做 SUM、AVG、COUNT——依然每次都要跑。对于SELECT dt, SUM(amount) FROM orders GROUP BY dt这类查询索引生效之后还是要把所有涉及行的 amount 取出来做累加CPU 消耗并不会因为索引而消失。再说加机器。这是一个纯线性投入查询次数翻倍CPU 消耗翻倍那就把机器翻倍。短期能扛住但业务是增长的查询频次也是增长的每个月的资源成本都在往上走。而且机器加多了单节点扫描的数据量虽然降了但重复计算的总量并没有减少本质上是用更多硬件去抵消重复劳动。物化视图能解决一部分问题前提是你能提前预测哪些查询需要被加速。报表查询还好说但临时分析和探索性查询没办法提前建模而且物化视图的维护本身也要消耗资源。相比之下查询缓存是一种“事后复用”的思路第一次查询老老实实计算结果存下来之后再遇到一模一样的查询直接返回结果。它不要求你提前预判查询模式对固定报表和高频重复查询几乎是无脑收益。所以在我个人的优化优先级里查询缓存是排在“加机器”和“无脑加索引”之前的。2. 缓存到底缓存了什么SQL缓存与分区缓存的取舍Doris 的查询缓存不是一种笼统的“查询结果缓存”它实际提供了两条路径分别对应两种不同的缓存粒度SQL Cache 和 Partition Cache。理解这两者的区别决定了你在实际业务中能不能吃到缓存的红利。2.1 SQL Cache把整道菜做好的成品直接端上来SQL Cache 的思路最简单——把一条 SQL 的最终结果集缓存起来。当用户执行的 SQL 满足命中条件时Doris 在第一次执行后会把结果存在 BE 节点本地之后遇到完全相同的 SQL并且查询涉及的表分区版本没有变化就直接把缓存结果返回给客户端跳过整个计算过程。用生活里的事打比方SQL Cache 就像你把整道红烧肉做好之后拍张照片放在菜单上顾客点什么你直接端出成品不需要重新下厨。它适合的场景非常明确固定报表、BI 看板、定时任务里那些“SQL 文本不变、数据范围不变、查询时间相近”的重复查询。命中条件上SQL Cache 有几个硬性要求SQL 文本必须是确定性的不能包含rand()、now()、uuid()这类非确定性函数。查询涉及的表分区版本不能有变化只要底层数据发生过导入、插入、更新缓存就自动失效。结果集大小不能超过阈值超大结果集的缓存收益会显著下降Doris 也会在满足一定条件下放弃缓存。对应到代码层面开启方式很简单会话级别设置SET enable_sql_cache true;之后的查询如果满足条件Doris 会自动尝试命中缓存。需要注意的是这个变量在较新版本中可能默认开启但具体默认值随版本迭代会有变化生产环境要结合版本确认。2.2 Partition Cache半成品复用只计算真正变化的那部分Partition Cache 是更精细的一条路径也是 Doris 在 OLAP 引擎里比较有辨识度的设计。它的核心思想是如果查询涉及多个分区而其中一部分分区的数据没有变化那么这些分区可以直接复用上一次的中间计算结果Doris 只需要计算“这次新变化/新增的分区”然后把缓存结果和计算结果合并返回最终结果。继续用做饭打比方SQL Cache 是整道菜做好备着Partition Cache 是提前把食材洗好切好、料汁调好客人点菜的时候只需要炒一下新下单的那部分。对于按天分区、按小时分区的时间序列类数据这个机制格外有用。举一个实际场景。有一张按天分区的订单表平台每天凌晨导入前一天的数据。某个业务方要查“近 30 天销售汇总”这条查询每天都会被触发。没有分区缓存时每次都要扫 30 个分区的数据做聚合有了分区缓存之后之前 29 天的分区结果都还留在缓存里只有新导入的那个分区需要真正计算其余直接拼接。查询耗时理论上可以降到原来的几十分之一。Partition Cache 的开启同样在会话级别SET enable_partition_cache true;但有一个容易被忽略的前提查询必须能够被有效裁剪到分区级别。如果你的表设计没有合理分区或者查询条件不带分区键Partition Cache 很难发挥作用。这也是为什么我接下来一定要讲表的分区设计它直接决定了缓存能不能吃到。2.3 两种缓存的边界一张表看清适用场景把两种缓存放到一张表里对比决策的时候就清晰多了对比维度SQL CachePartition Cache缓存粒度整个查询的最终结果集分区级别的中间结果命中前提SQL 文本一致、分区版本无变化查询可分区裁剪、未变化分区版本不变典型场景固定报表、BI 看板、高频同文本查询近 N 天汇总、区间聚合、增量导入场景失效代价整体重新计算只重新计算变化的分区对表结构要求低普通表即可高必须合理设计分区适合数据形态低频更新、读多写少按时间增量写入、历史分区基本不变从这张表能看出一个关键结论如果你的表没有分区或者分区设计不合理Partition Cache 带来的收益基本为零只能靠 SQL Cache 兜底。而 SQL Cache 的命中又很依赖“SQL 文本不变”这个条件——这恰恰是很多团队忽略的细节后面我会单独讲一次线上排查经历正好卡在这个环节上。3. 开启缓存之前参数、内存与命中规则的三重准备很多人在集群上直接SET enable_sql_cache true;就以为完事了结果一跑发现命中率低得可怜要么缓存频繁失效要么 BE 内存被撑爆。缓存不是开关开启前必须做一套组合准备。3.1 关键参数速查与推荐配置以下几组参数是我在实际项目中验证过、比较有参考价值的配置方向。Doris 版本迭代较快不同版本参数名可能略有差异生产环境要以官方对应版本的文档为准。参数/变量作用推荐设置enable_sql_cache会话级开关开启 SQL Cachetrue高频报表场景enable_partition_cache会话级开关开启分区缓存true分区表增量导入场景cache_medium缓存存储介质SSD 或 HDD优先SSD收益显著高于 HDDcache_ttl缓存过期时间根据数据更新频率设置常见 1-7 天cache_result_max_rows单次缓存结果集行数上限按查询特征调整一般默认即可这里重点说一下cache_medium。缓存可以放在内存也可以放磁盘Doris 会按 LRU 策略淘汰旧数据。我建议优先使用 SSD因为 SQL Cache 存的已经是计算结果读取不需要再做二次聚合SSD 的顺序读性能完全够用而把缓存全放内存虽然更快但会挤压查询自身的 buffer 空间可能导致查询性能不升反降。对于内存比较紧张的 BE 节点用 SSD 做缓存介质是更稳妥的方案。会话级变量可以在 JDBC 连接串或者连接池初始化时统一设置。如果你用的是较新版本还可以在表属性里直接配置缓存策略。配置完成后一定要验证生效——很多团队配置完就忘了查默认参数下缓存可能根本没跑起来。3.2 分区设计是分区缓存的生命线Partition Cache 对分区设计的依赖再怎么强调都不为过。如果你用的是无分区大表或者分区覆盖度很差这个缓存路径就是空的。理想的表结构至少满足三点按时间列分区比如dt字段按天分区。数据写入集中在最新分区历史分区基本不会被修改。查询过滤条件尽量带上分区键让查询能精确裁剪到小范围分区。有团队问过我“Doris 数据量只有几 MB是不是不需要分桶”这个问题的答案是——数据量小确实可以不纠结分桶数量但分区是否合理和分桶是两回事。几 MB 的表你随便怎么分桶影响都不大但如果你要让查询缓存生效分区设计依然值得花十分钟想清楚。因为缓存命中的核心不是数据量大小而是“分区版本是否稳定”这个机制特性。反过来分区粒度也不宜过细。小时分区虽然裁剪更精细但数据导入会频繁改变分区版本导致缓存频繁失效。我的经验是对于日报级别的查询天分区通常是性价比最高的粒度只有在查询对延迟极度敏感且数据导入次数极少的情况下才考虑更细的分区。3.3 缓存内存怎么估算才不会挤掉查询性能缓存不是白嫖的资源它吃的是 BE 节点内存或磁盘。如果你把缓存全塞内存就要做好预算。一个经验性的参考是BE 节点 JVM 之外的内存中查询执行本身需要留足 buffer缓存占用的内存建议控制在节点可用内存的 10%~20% 以内。比如一台 64G BE 节点留给缓存的空间在 6G~12G 左右比较合理具体取决于查询并发和数据集特征。估算方式可以这样粗略推单条查询结果集平均大小 × 预计缓存条目数就是缓存占用的空间。如果发现缓存命中率很高但内存压力也大优先做两件事——一是把cache_medium切换成 SSD 分担内存压力二是检查是否把超大结果集也缓存了。超大结果集的缓存命中率往往不高因为每次数据一更新就要全部重算收益小、成本高。还有一个容易忽略的问题缓存生效依赖 BE 节点的本地存储节点重启后缓存会丢失。所以不要指望缓存能替代物化视图它只是一种短周期的加速手段。集群一旦滚动重启缓存命中率会瞬间归零需要一段时间慢慢恢复这也是平台规划时要注意的运维细节。4. 从命中率 60% 到 95%一次线上缓存调优的完整排查链路配置开好了参数也按文档设置了结果打开监控一看缓存命中率只有堪堪 60%。这在当时的项目里已经算不错了但距离我预期还有差距。更麻烦的是命中率不稳定早高峰有时高有时低完全不知道问题出在哪。这里我把完整的排查过程写出来很多坑是文档里不会写的。4.1 第一层排查SQL 文本不一致是最隐蔽的杀手我先拉了一小时内的审计日志把命中缓存的查询和未命中的查询做了对比。结果发现未命中的 SQL 里有一大批文本上看着相似、实际完全不同的查询。最典型的就是 BI 工具生成的 SQL——它对时间过滤条件做参数化时会生成带具体日期字符串的 SQL每次刷新报表日期变了SQL 文本就变了。比如业务方看的是“昨日销售”但 BI 工具传入的是dt 2025-01-05 AND dt 2025-01-05这种带具体日期的条件。今天看的是 1 月 5 号明天看的是 1 月 6 号SQL 文本完全不同SQL Cache 自然永远命中不上。这类问题的解法不是改 Doris而是改业务查询规范把“相对时间”写进 SQL 里比如dt CURDATE() - INTERVAL 1 DAY AND dt CURDATE()。这样无论哪一天执行SQL 文本都一模一样缓存才能稳定命中。如果你控制不了 SQL 生成端也可以在接入层做一层 SQL 规范化但这需要额外开发成本多数团队不如直接推动查询规范。另外注意now()、CURDATE()这类函数虽然能让 SQL 文本一致但它们本质是非确定性函数——今天执行和明天执行的结果不同Doris 会直接拒绝缓存。这里有一个折中如果查询能接受 TTL 过期可以配合缓存过期时间来处理变化频率但如果你需要的是高实时性查询那缓存本来就不合适。4.2 第二层排查分区版本频繁变化把刚缓存的数据全部失效SQL 文本问题解决之后命中率提升到了 78%但距离 95% 还有距离。继续看监控发现一个规律缓存命中率的下跌时段和 ETL 导入任务的执行时段高度重合。一查才发现这对应着上面说的分区版本变化问题。我们的订单表按天分区但 ETL 任务会在白天对当天分区做多次覆盖写入比如修正数据。每次写入都会更新分区版本而 SQL Cache 的失效逻辑是查询涉及的表的分区版本一旦变化整个查询缓存全部失效。这意味着白天每次修正导入都会把当前所有涉及该分区的缓存结果清掉。更隐蔽的是分区缓存虽然只重新计算变化的分区但如果查询涉及一个频繁变化的分区它的中间结果也会被一再无效化。解决方案有两个方向。一是把 ETL 导入时间集中到凌晨白天尽量避免对同一分区反复写入二是使用分区缓存代替 SQL 缓存这样只有被修改的那个分区需要重算其他历史分区还能复用。我们最终做了组合改进把修正数据的导入窗口固定到凌晨三点到六点同时把高频查询切到 Partition Cache 路径命中率明显回升。4.3 第三层排查结果集过大和非确定性函数是最后的两块短板到这一步命中率在 80%~90% 之间徘徊。继续深挖 Profile发现两类查询始终无法命中一类是结果集特别大的明细查询另一类是带了RAND()、UUID()的抽样查询。前者容易理解——结果集会超过缓存阈值Doris 判断缓存成本太高就不缓存了。后者也很合理——RAND()让每次查询结果都不同缓存没有意义。这两个问题的处理思路完全不同。明细查询我会建议业务方拆小查询范围或者改到专门的明细查询服务不要试图拿缓存硬扛而抽样查询本身就是低确定性查询缓存不该管它直接排除在外。最终我得到的缓存命中率稳定在 95% 左右P95 延迟从优化前的 3.2 秒降到了 210 毫秒早高峰 CPU 使用率下降了近 40%。这个结果说明Doris 查询缓存的优化不只是开个开关那么简单而是要把 SQL 规范化、分区设计、ETL 调度和缓存路径选择组合在一起做。4.4 排查链路小结一份可直接使用的检查清单如果你也遇到“缓存开了但命中率不高”的问题按这个顺序排查最快核对 SQL 文本——通过审计日志查看未命中查询和命中查询的文本差异重点排查带具体日期的参数化查询。核对非确定性函数——搜索所有包含RAND()、NOW()、UUID()等函数的 SQL这些查询天然不适合缓存。核对分区版本变化——观察命中率下跌时段是否与 ETL 导入任务重合修正导入策略或切换缓存路径。核对结果集大小——查看 BE 节点上缓存占用的存储空间确认是否有明细大查询在反复冲刷缓存。核对会话变量——确认连接池或 JDBC 连接串中是否真的设置了enable_sql_cache或enable_partition_cache很多问题是配置没传进去。5. 缓存不是银弹哪些查询别指望缓存以及三层加速的完整架构做了这么多优化之后我得泼一盆冷水查询缓存不是万能的它只对特定形态的查询有显著效果。在实际平台架构里查询缓存只是加速体系中的一层。如果你在规划一个 Doris 集群的查询加速方案建议把缓存放在一个更完整的框架里理解。5.1 不适合 Doris 缓存的几类查询第一类是对实时性要求极高的查询比如实时大屏上精确到秒级的指标。缓存的前提是数据版本稳定数据每秒钟都在写入的查询场景缓存结果刚生成就失效了命中意义不大反而白白消耗存储和淘汰资源。第二类是随机性强的分析查询比如带大量RAND()抽样的探索式分析。这类查询每次执行路径不同结果不同缓存永远无法命中。第三类是超大结果集的导出查询比如一次性拉几千万行明细。这类查询即使缓存下来再次复用的概率也极低还白白占空间应该依赖 Doris 的查询结果导出机制而不是查询缓存。判断一条查询能不能吃缓存我在内部定过一个简单的准则如果一条 SQL 一周内被执行超过 20 次且相邻执行之间表数据没有变化缓存就是高收益的如果一条 SQL 一天只跑一两次或者数据每秒都在变就别在缓存上浪费精力。5.2 三层加速方案查询缓存 物化视图 业务层缓存第一层是 Doris 本身的查询缓存解决“相同 SQL 重复计算”的问题。第二层是物化视图解决“同维度不同聚合口径的重复扫描”问题——比如GROUP BY dt和GROUP BY dt, province是两条不同 SQL但底层都是同一批数据的聚合物化视图可以把中间结果预计算好多条 SQL 共用。第三层是应用层缓存比如在 BI 服务外面套一层 Redis把已经渲染好的报表数据和查询结果缓存起来用户访问时连查询都不发直接返回渲染结果。我在实际项目里最推荐这个组合高频、固定、同文本的 SQL——用 Doris SQL Cache。按天分区、增量写入、历史分区稳定的聚合——用 Doris Partition Cache。多口径、多维度的重复聚合——建物化视图。看板级的最终渲染结果——在应用层加 Redis 缓存。这套方案搭配下来真正打到 Doris 集群上的重复计算量可以降到原来的十分之一以下。要注意的是层与层之间不是替代关系而是互补查询缓存覆盖不了多口径聚合物化视图又不擅长处理“同样一条 SQL 被频繁执行”的细粒度场景各管一段整体效果才最好。5.3 集群部署与运维侧的配套动作查询缓存想要稳定发挥效果集群部署时就要想好配套。首先是 BE 节点的存储介质既然缓存介质支持 SSD部署时优先选择 SSD 节点或者至少把缓存目录放到 SSD 上否则磁盘读延迟依然会拖慢缓存命中后的响应速度。其次是监控指标。Doris 的 Profile 里能看到缓存命中情况但生产环境建议把缓存命中率、缓存占用空间、缓存淘汰次数这三个指标单独接进监控系统每天观察趋势。缓存命中率的突然下跌往往对应着一次新的 ETL 任务上线或者业务方改了查询模板——这些信息对平台稳定性管理非常有用。最后是版本规划。Doris 不同版本的缓存能力差异不小比如早期版本的 Partition Cache 支持度还不够完善SQL Cache 的行为也不完全一致。如果你正在规划 Doris 集群部署尽量选择较新的、缓存能力完善的版本部署前把官方文档里的缓存章节完整读一遍确认参数名和默认行为再动手。5.4 和 Hive、Presto 相比Doris 缓存机制到底强在哪我在之前的项目里同时维护过 Hive 和 Presto 两套查询引擎对这个问题感触很深。Hive 的缓存主要体现在执行计划复用和结果物化上需要人工介入LLAP 组件虽然能在内存里缓存热点数据但配置复杂运维成本不低Presto 更多依赖集群计算能力和查询优化器跨查询的结果缓存能力相对较弱通常要靠外部工具或 Presto 的企业版特性来实现。Doris 把查询缓存做进了引擎自身而且是面向 OLAP 重复查询场景专门设计的。SQL Cache 和 Partition Cache 两级缓存配合起来对固定报表和增量分区数据这类典型的数据平台场景几乎是开箱即用的加速手段。这也是我在多个项目里最终选择以 Doris 作为统一 OLAP 引擎的重要原因——它把很多以前需要外部组件才能解决的问题做成了引擎内置能力运维复杂度显著降低。就我个人这几年的实操体会查询缓存这步棋投入产出比远比加机器高。但千万记住它不是让你把 SQL 写得更随意而是要求你把 SQL 规范做得更细致。所有缓存命中率高的团队背后一定有一个严格的查询规范在支撑。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →