在观测与日志管理领域,规模与实时性往往是彼此矛盾的目标:数据越多、结构越复杂,越难在可接受的延迟下提供交互式查询体验。Datadog 为了解决这一矛盾,推出了第三代事件存储 Husky,并在其核心构建了一个面向海量无模式事件的查询引擎,能够实现对每日数千亿乃至累计超过100万亿事件的有效检索。本文将从架构视角出发,系统地剖析 Husky 查询路径、读者服务的关键优化、文本搜索与多层缓存策略、请求路由与租户隔离,以及未来技术演进方向,揭示其如何在大规模环境下保持成本与性能的平衡。 理解 Husky 的数据与查询场景,是认识其设计取舍的前提。Husky 存储的基本单位是事件,每条事件带有时间戳与若干属性。由于事件来源多样,结构千差万别:日志、追踪、网络样本等都可能携带不同字段与不同分辨率的数据。
查询模式也呈现出两类典型特征:一种是"针尖式"搜索,定位单条或少量事件;另一种是分析式查询,对大范围时间窗口做聚合统计和维度拆分。要在同一平台上高效支持这两种工作负载,系统必须做到既能快速跳过无关数据,又能高效扫描必要的信息。 在 Husky 中,查询路径被拆分为查询计划器、查询协调器、元数据服务与读者服务四大模块,各模块均支持多租户并分布部署以保证可用性。查询计划器负责接收用户请求、解析上下文、进行权限校验与初步优化;它把长时间范围的查询切分为多个基于时间的子步,并将这些子步调度到查询协调器。查询协调器从元数据服务获取碎片信息(包括文件路径、版本、行数、时间边界以及用于快速匹配的 zone map 等),基于这些元数据进行初步修剪并将需要检查的碎片派发给读者服务。读者服务可以从本地缓存直接返回结果,必要时则从对象存储(blob 存储)按需读取数据。
读者服务是查询性能的重心。为了在面对数以百万计的碎片和数百PB的数据时保持交互式体验,Husky 在碎片内部采用了行组(row group)拆分与延迟解码机制。碎片被划分为若干行组,每个行组存放少量行并带有列级的元数据,例如每列的最小值与最大值、显式值列表等。执行时,扫描算子并不立即把整行或整列解码,而是返回对行组的惰性引用;真正的数据读取与解码只在上层算子需要访问具体列时触发。这样设计带来的直接收益是两方面:一是基于行组的元数据可以在过滤阶段快速剔除大量无法匹配的行组,显著减少后续计算;二是当查询包含多个谓词时,系统会先用代价模型选择最便宜的谓词优先评估,若早期谓词就将候选集缩小为零,则后续列永远不必解码,从而节省计算和 I/O 成本。 文本检索是日志管理中不可或缺的能力。
Husky 为文本搜索引入了附加的段(segment)文件,与碎片并行存在。每个段保存词项的 posting list,以及用于通配符查询的 n-gram 索引。posting list 使用位集(bitset)表示出现在事件中的行编号,分为针对常见文本字段的位集和跨全部字段的位集两类。为了控制索引体积,n-gram 经过哈希后存储并限制每个段中 n-gram 的最大数量,这会引入可控的误报但不会影响最终的精确匹配判断。文本查询在运行时会把词项转换为位集表达式,与片段内的行集合进行按位运算,从而快速缩减需要扫描的行集合量。 在大规模场景下,缓存策略往往决定了成本与性能的天花板。
Husky 的缓存体系由多层组成,最关键的三层是结果缓存、对象块范围缓存(blob range cache)与谓词缓存。得益于 Husky 数据的不可变性,缓存失效问题被大幅简化:只要碎片元数据没有变化,指向的数据就被视为恒定,这使得跨请求复用变得安全且可靠。结果缓存以碎片级别保存前次查询的返回结果,缓存命中率高达约80%,这与监控面板的自动刷新、报警周期性查询与相似筛选条件的重复出现高度相关。对象块范围缓存用于保存从对象存储读取的字节范围,采用 RocksDB 为后端并结合单次读取去重(singleflight)策略,避免并发请求导致重复读盘或重复网络拉取,命中率大约为70%。当节点的磁盘 I/O 达到瓶颈时,系统会绕过本地缓存直接回落到网络对象存储,以保护整体延迟不被本地磁盘抖动拖垮。 谓词缓存是一个更有技巧性的优化,源自对用户逐步收敛查询习惯的观察。
用户往往从宽泛的筛选开始,逐步加上条件锁定问题。Husky 在每个读者节点对评估代价高且更可能被重用的谓词进行探测性缓存。缓存以位集形式存储片段中满足某个谓词的行集合。由于谓词计算成本随上下文而异(受行组分布与短路评估影响),系统周期性地统计谓词的计算代价并选择最昂贵的一部分进行缓存。尽管谓词缓存的命中率相对较低(仅约3%),但每次命中可以节省大量重复计算,其效率(即节省的工作量与构建缓存成本之比)常常高达十倍以上,因此总体收益显著。 多层次的预剪枝与缓存协同,使得大多数查询在到达实际数据读取阶段前就被大幅缩减。
统计数据显示,在一组代表性查询中,约30%的碎片在元数据服务阶段就被排除,700 个派发到读者的碎片中有约560 个被结果缓存直接命中,另外一些通过列元数据或文本索引被进一步剔除,最终只有极少数碎片需要触发对象存储的读取。整体上只有约3.4%的子查询需要读取真实数据,而触发对象存储读取的更是不到0.5%。这样的裁剪率直接带来成本节省和更稳定的查询延迟。 在资源隔离与路由一致性方面,Husky 也做了周密设计。理想状态下每个租户拥有独占资源能保证最强隔离性,但实际成本不可接受。Husky 采用了混合的 shuffle sharding 方案,将每个租户映射到读者节点池中的一个子集,并在该子集内部基于碎片 ID 做一致性映射以实现节点亲和性。
与固定大小的分片不同,Husky 为每个租户推断可变的分片大小,以匹配其使用模式。这既能提升缓存亲和性,又能通过可调的虚拟节点权重实现对新节点的负载缓慢注入,从而避免冷节点一开始遭遇大量昂贵查询时击穿性能。查询协调器还会周期性地拉取各读者节点的负载指标并在派发时避开过载节点,若主路由节点正忙则选择一致的备选节点以保证稳定性。 为了提升用户感知的交互性,Husky 支持流式返回部分结果。查询被当成作业执行并持续汇报进度,前端可以在不等待全量完成的情况下获取部分聚合或行样本,从而加速探索与迭代。流式返回对缓解长尾延迟尤为重要:即便某些碎片存在较高的响应延迟,大部分已完成的结果也能尽快回传给用户,显著改善体验。
然而流式也带来重试与幂等性挑战,需要额外的检查点与去重机制以避免重复返回或在失败时丢失数据。 在技术路线规划上,Husky 正朝模块化与标准化迈进。借助 Apache Arrow、Parquet、Substrait 和 Apache DataFusion 等行业标准,可以将执行层、存储格式与接口解耦,从而让 Husky 更易于与外部数据系统协同,也便于引入社区成熟的算子与格式优化。这些标准有助于提升可组合性、降低与其他平台的集成成本,并在未来为更多复杂查询与分析能力的迭代提供弹性基础。 总结来看,Husky 的成功并非依赖某一项单独技术,而是在架构层面实现了多种优化的组合与协同。碎片与行组的物理布局、惰性解码与代价驱动的谓词排序、基于位集的文本索引、三层缓存体系与精细的路由隔离构成了一个整体,使得在面对海量无模式事件与高并发查询时仍能保持高命中率、低扫描量与可预测的交互延迟。
未来通过采纳开放标准、重构缓存层与持续优化读者路径,Husky 有望进一步降低单次查询成本并扩展更多新型分析场景。 对于关注观测平台设计、分布式查询与大规模存储系统的工程师与架构师,Husky 的实现提供了丰富的实践经验与可借鉴的工程模式:利用不可变数据的性质简化缓存一致性问题,以代价模型驱动执行顺序以减少解码与 I/O,结合位集索引与分段文件加速文本检索,以及在多租户环境中通过可配置的分片策略取得隔离与资源利用的平衡。随着数据量与查询复杂度持续上升,这些思想将在更多场景中证明其价值,并推动下一代可扩展观测平台向着更高的实时性与更低的单位成本演进。 。