Apache Druid 查询缓存完全指南:Per-Segment 与 Whole-Query 缓存原理、配置与调优实践
Apache Druid 查询缓存完全指南Per-Segment 与 Whole-Query 缓存原理、配置与调优实践【免费下载链接】druidApache Druid: a high performance real-time analytics database.项目地址: https://gitcode.com/gh_mirrors/druid6/druid本文系统梳理 Apache Druid 的两类查询缓存——per-segment 缓存与whole-query 缓存它们各自存储什么、默认行为如何、应在哪些服务Historical / Broker / Peon / Indexer上启用、如何通过运行时属性与查询上下文query context精确控制以及哪些场景下缓存几乎无效。读完本文你将能够依据集群规模、数据更新模式与查询形态制定一套可落地的 Druid 缓存策略并能在源码层面理解缓存开关、缓存键生成与失效机制的底层实现。Druid 的查询缓存是提升高并发、混合负载集群吞吐能力的关键手段。它属于查询结果层面的缓存与 Historical 上的数据层面缓存segment 本地存储缓存是两回事二者可叠加使用。若你还不太熟悉 Druid 的整体架构建议先阅读 Druid 架构设计、Segment 存储模型 与 查询执行流程 三篇基础文档再回到本主题。一、两种缓存类型存储什么、缓存在哪里Druid 支持两种查询缓存二者在缓存粒度、默认启用状态和适用服务上截然不同缓存类型存储内容默认状态适用服务Per-segment 缓存单个 segment 上的部分查询结果默认启用Historical默认、Peon / Indexer、小规模集群的 BrokerWhole-query 缓存单个查询的最终完整结果默认关闭仅 Broker无论哪种缓存只要底层数据发生任何变化Druid 都会立即使对应缓存失效以避免返回过期结果。这一点对table数据源尤其重要——它们的底层 segment 高度动态特别是实时real-time数据 segment 持续写入时缓存必须时刻保持与数据一致。关于缓存的物理载体Druid 可以将缓存数据存放在JVM 本地堆内存或外部分布式键值存储如 memcached中默认使用基于 Caffeine 的本地缓存位于 JVM 堆内可直接观测默认最大缓存容量为1 GiB 与 JVM 最大运行时内存 10% 两者的较小值默认无过期时间当堆内缓存增长到上限后最久未使用LRU的 segment 结果会被逐出让位给新结果。具体的缓存存储配置见 Cache configuration。1. Per-segment 缓存按 segment 缓存部分结果Per-segment 缓存是 Druid 最主要的缓存形态它按 segment 粒度缓存部分查询结果并在 Historical 服务上默认启用。它的设计动机很直接Historical 从深度存储拉取到本地segment cache的 segment 通常长期不变对这些不变 segment 维护一个低逐出率的缓存收益极高而实时 segment 则始终在查询时实时计算不占用缓存。Druid 可以把 per-segment 缓存结果与后续形态相近的查询结果进行合并。例如两次查询除了覆盖的时间区间不同其余条件过滤器、聚合等完全一致时后一次查询可以复用前一次已缓存的 segment 结果只计算新增时间区间的数据即可。Per-segment 缓存由useCache与populateCache两个参数控制见下文第四节。它在源码中的默认行为可以在 QueryContexts.java 中找到USE_CACHE_KEY useCache、POPULATE_CACHE_KEY populateCache且硬编码默认值均为trueDEFAULT_USE_CACHE true、DEFAULT_POPULATE_CACHE true见 QueryContexts.java。强烈建议对实时数据使用 per-segment 缓存。典型场景是查询同时覆盖正在从 Kafka 实时到达的数据以及已由 Historical 加载的历史 interval segment。此时 Druid 可以把 Historical segment 的缓存结果与实时流计算的实时结果合并返回。而 whole-query 缓存在此场景下几乎没有价值——实时摄取产生的新数据会不断使整个缓存结果失效。2. Whole-query 缓存按查询缓存最终结果Whole-query 缓存缓存单个查询的完整结果。命中后Broker 无需再向数据服务Historical / 实时任务发起 scatter-gather 并合并各 segment 的部分结果直接返回最终结果。它适合在Broker上启用前提是底层数据在 segment 层面很少被摄取操作失效——典型场景是未使用实时摄取、以批量摄取为主的查询负载。此时 segment 几乎不变per-segment 缓存反而低效每次查询 Druid 仍要为每个 segment 获取结果并到 Broker 合并而 whole-query 缓存直接跳过整条执行链路。二、缓存放哪里按服务选型的决策指南缓存不是每个服务都该无脑开启Druid 对启用位置有明确建议Per-segment 缓存的可用位置Historical默认启用较大的生产集群应启用 Historical 的 segment 级缓存填充。开启后Historical 会先合并自身本地 segment 的结果再返回给 Broker从而显著减轻 Broker 的合并压力。Peon / Indexer 中的摄取任务task较大的生产集群也应在任务执行服务上启用 segment 级缓存填充让任务服务先合并自身产生的本地结果同样可以减轻 Broker 负担。需要特别注意的是任务执行服务只支持本地缓存如caffeine。原因是缓存结果以摄取任务生成的中间部分 segment为粒度存储而这些中间 segment 在任务副本之间可能并不一致因此任务服务会忽略 memcached 等远程缓存类型详见 Indexer caching 中的说明。Broker仅限小集群少于 5 台服务器的小规模生产集群可以在 Broker 上启用 per-segment 缓存。务必避免在大规模集群的 Broker 上启用 per-segment 缓存当 Broker 缓存开启druid.broker.cache.populateCachetrue且查询上下文中populateCache未被显式设为false时各个 Historical不会再自行合并segment 级结果而是把它们原样回传给负责协调的 Broker最终由 Broker 独自完成对所有 segment的大规模合并——这会把合并压力从数据服务转嫁到 Broker适得其反。Whole-query 缓存只在 Broker 上可用这是它唯一的部署位置。三、缓存的性能收益与收益边界缓存带来的核心收益是提升同一系统上的并发能力对于承载并发混合查询负载的集群缓存命中能让查询绕过数据服务的处理线程因此吞吐提升非常可观。但需要注意不要指望缓存改善单条查询或单次页面加载的响应时间。即使冷缓存cache cold状态下单任务的响应时间也应当满足性能目标。查询处理过程中per-segment 缓存会拦截查询并直接把结果发给 Broker从而绕过数据服务处理线程。对于在 Broker 端只需极少处理的查询缓存命中后查询会非常快反之如果 Broker 端的处理本身成为瓶颈开启缓存带来的改善会微乎其微。收益最大的查询类型是topN和时间序列timeseries查询。对于groupBy查询若瓶颈在 Broker 的合并阶段缓存收益较小带 join 或不带 join 的查询情况类似。缓存几乎无效的场景Per-segment 缓存不适用包含子查询sub-query的查询不过子查询的输出本身可以被缓存详见 Query execution 中子查询执行的相关说明带 join 的查询在 Broker 上不支持任何缓存groupBy 查询在 Broker 上不支持 segment 级缓存查询上下文中设置了bySegment的查询在 Broker 上不缓存。Whole-query 缓存不适用涉及inline 数据源inline datasource或lookup 数据源的查询带 join的查询union 数据源的查询。从源码结构看这些限制与缓存键cache key的生成方式密切相关Druid 通过 CacheStrategy.java 中的isCacheable(QueryType, boolean willMergeRunners)判定查询类型是否可缓存并用computeCacheKey(query)生成缓存键。join、sub-query 等查询形态的结果难以用稳定的键描述或复用因此被排除在缓存之外。四、如何配置运行时属性与查询上下文双通道所有查询缓存都由一对参数控制查询与缓存的交互方式useCache指示查询是否读取缓存中的结果populateCache指示查询是否把自身结果写入缓存。“使用”与“填充”的分离设计让你可以在不污染缓存的前提下享受缓存例如对不常见的数据发起查询时可以只读缓存而不写入避免把不太可能被其他查询复用的大报表、超老数据等结果塞进缓存。启用缓存的前提是服务自身的运行时属性runtime.properties中已开启缓存。默认情况下per-segment 缓存已在 Historical 上启用。对单条查询则可以在查询上下文中进一步精确控制。1. 在 Historical 上启用Historical 只支持segment 级缓存默认已启用。通过以下运行时属性控制druid.historical.cache.useCachetrue druid.historical.cache.populateCachetrueHistorical 可用的全部缓存配置项见 Historical caching包括属性取值说明默认值druid.historical.cache.useCachetrue / false是否启用 Historical 上的缓存读取falsedruid.historical.cache.populateCachetrue / false是否填充 Historical 上的缓存falsedruid.historical.cache.unCacheable全部 Druid 查询类型指定不参与缓存的查询类型列表[]druid.historical.cache.maxEntrySize正整数单个缓存条目最大字节数1_000_0002. 在任务执行服务Peon / Indexer上启用Peon 与 Indexer 同样只支持segment 级缓存。通过以下运行时属性控制以 Peon 为例druid.realtime.cache.useCachetrue druid.realtime.cache.populateCachetrue可用的配置项与 Historical 结构一致unCacheable、maxEntrySize等默认值相同见 Peon caching 与 Indexer caching。再次强调任务服务只支持local/caffeine这类本地缓存配置为 memcached 等远程缓存会被直接忽略。这些属性在 CacheConfig.java 中均有对应实现字段例如unCacheable默认空列表、maxEntrySize默认 1_000_000 字节。3. 在 Broker 上启用Broker同时支持segment 级与 whole-query结果级两种缓存。segment 级缓存使用useCache/populateCachedruid.broker.cache.useCachetrue druid.broker.cache.populateCachetruewhole-query 缓存使用结果级参数useResultLevelCache/populateResultLevelCachedruid.broker.cache.useResultLevelCachetrue druid.broker.cache.populateResultLevelCachetrueBroker 的全部缓存配置见 Broker caching属性取值说明默认值druid.broker.cache.useCachetrue / false启用 Broker 上的 segment 级缓存读取falsedruid.broker.cache.populateCachetrue / false填充 Broker 上的 segment 级缓存falsedruid.broker.cache.useResultLevelCachetrue / false启用结果级缓存读取falsedruid.broker.cache.populateResultLevelCachetrue / false填充结果级缓存falsedruid.broker.cache.resultLevelCacheLimit正整数可缓存的最大查询响应大小Integer.MAX_VALUEdruid.broker.cache.unCacheable全部 Druid 查询类型不参与缓存的查询类型列表[]druid.broker.cache.cacheBulkMergeLimit正整数或 0查询涉及 segment 数超过该值时Broker 不再尝试从缓存获取把缓存读取与合并留给 HistoricalInteger.MAX_VALUEdruid.broker.cache.maxEntrySize正整数单个缓存条目最大字节数1_000_000特别提醒即使 Broker 开启了 segment 级缓存groupBy 查询在 Broker 上依然不生效见 Broker caching 中的标注说明。4. 在查询上下文中按查询控制只要服务自身已开启缓存填充你就可以在**单条查询的上下文query context**中覆盖缓存行为。例如通过 HTTP POST API 提交一条 Druid SQL 查询并在 context 中传 JSON 对象{ query : SELECT COUNT(*) FROM data_source WHERE foo bar AND __time TIMESTAMP 2020-01-01 00:00:00, context : { useCache : true, populateCache : false } }上例中用户显式把populateCache设为false目的是避免把超过一年历史的 segment 查询结果写入缓存。相关的 HTTP 接口细节见 Druid SQL client APIs查询上下文其他参数见 Query context。这四个上下文键在源码中的定义位于 QueryContexts.javauseCache、populateCache、populateResultLevelCache、useResultLevelCache其读取逻辑isUseCache()/isPopulateCache()则实现在 QueryContext.java 中允许传入默认值参数——这正是“服务级默认 查询级覆盖”这套优先级链路的实现基础。五、缓存存储引擎选型Caffeine、Memcached 与 Hybrid缓存引擎由druid.cache.type统一控制对所有支持缓存的进程Broker / Historical / MiddleManager / Peon通用且全局生效——因此相同的配置可以直接复用到多个服务例如定义在公共 properties 文件中。可选值local、memcached、hybrid、caffeine默认caffeine。Caffeine 缓存默认基于 Caffeine 的高性能本地缓存要求 JRE 8u60 及以上。核心配置属性说明默认值druid.cache.type设为caffeine或省略caffeinedruid.cache.sizeInBytes堆内缓存最大字节数支持人类可读格式min(1GiB, Runtime.maxMemory / 10)druid.cache.expireAfter访问后缓存条目可被过期的毫秒数无不设时间限制druid.cache.cacheExecutorFactoryCaffeine 维护任务的执行器COMMON_FJP公共 ForkJoinPool默认、SINGLE_THREAD单线程、SAME_THREAD同步执行COMMON_FJPdruid.cache.evictOnClose关闭命名空间如从进程移除 segment时是否立即逐出关联缓存值falseCaffeine 缓存除了常规缓存指标外还会额外上报query/cache/caffeine/*/requests命中与未命中计数、query/cache/caffeine/*/loadTime、query/cache/caffeine/*/evictionBytes被逐出缓存的字节数可用于评估sizeInBytes调优是否合适。Local 缓存已弃用简单的堆内 LRU 缓存自 v0.12.0 起被 Caffeine 取代官方标注 DEPRECATED未来版本可能移除。若启用它务必相应调大 JVM 堆。相关配置druid.cache.sizeInBytes0 表示禁用默认 0、druid.cache.initialSize底层哈希表初始大小默认 500000、druid.cache.logEvictionCount非 0 时每逐出该数量条目打印一次日志默认 0。Memcached 缓存以 memcached 作为缓存后端让所有进程共享同一个缓存。关键配置包括属性说明默认值druid.cache.expirationmemcached 过期时间259200030 天druid.cache.timeout等待 memcached 响应的最大毫秒数500druid.cache.hostsmemcached 主机列表host:port逗号分隔无druid.cache.maxObjectSize单个对象最大字节数5242880050 MiBdruid.cache.memcachedPrefix所有键的前缀druiddruid.cache.numConnectionsmemcached 连接数1druid.cache.protocol通信协议binary或textbinarydruid.cache.locator定位器consistent或array_modconsistentdruid.cache.enableTls是否启用 TLS 连接falsedruid.cache.clientModestatic手动指定节点或dynamicAWS AutoDiscoverystaticdruid.cache.skipTlsHostnameVerification是否跳过 TLS 主机名校验trueHybrid 混合缓存组合任意两种缓存构成L1 / L2 两级缓存典型用法是“本地内存缓存 远程 memcached”。查询先查 L1未命中再查 L2若 L1 未命中而 L2 命中则同时回填 L1。配置通过druid.cache.l1.type/druid.cache.l2.type指定各级缓存类型并用druid.cache.l1.*/druid.cache.l2.*前缀透传对应缓存类型的属性。另有druid.cache.useL2默认trueL1 未命中时是否查询 L2。官方建议在 Historical 上将其设为false——如果 L2 是 memcached 这类远程缓存且 Broker 也共用同一缓存那么查询能到达 Historical 就意味着 Broker 已在同一远程缓存中未命中Historical 再查一次 L2 必然是空操作druid.cache.populateL2默认true是否把结果写入 L2。六、缓存指标观测缓存的实际运行效果命中率、逐出次数等可以通过Druid metrics观测具体指标清单见 Druid metrics。结合上文提到的query/cache/caffeine/*/evictionBytes指标你可以判断当前sizeInBytes下的缓存换手率是否符合预期并据此调整缓存容量与逐出策略。七、缓存策略速查综合以上内容给出面向不同集群形态的缓存策略参考集群 / 负载形态推荐做法大型生产集群≥5 台服务器Historical 与任务执行服务启用 per-segment 缓存populateCachetrue把合并压力留在数据服务侧不要在 Broker 上启用 per-segment 缓存小型生产集群5 台服务器可在 Broker 上启用 per-segment 缓存批量摄取、segment 几乎不变、查询重复度高在 Broker 上启用 whole-query 缓存useResultLevelCache/populateResultLevelCache实时摄取Kafka / Kinesis 等依赖 per-segment 缓存配合 Historical 与实时结果合并whole-query 缓存收益有限大报表、超老数据等低复用查询查询上下文中设populateCachefalse避免污染缓存高并发混合负载优先保证各服务缓存开启缓存直接提升系统并发吞吐如需进一步了解缓存的完整配置细节可继续阅读 Using query caching配置与上下文用法、Query context上下文全参数、Cache configuration缓存引擎与更多选项、Druid 架构各进程职责、Segment 存储数据组织方式与 Query execution查询处理链路。【免费下载链接】druidApache Druid: a high performance real-time analytics database.项目地址: https://gitcode.com/gh_mirrors/druid6/druid创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考