1. 项目概述当“每日百亿次”遇上“低延迟”在数据驱动的业务决策中报表查询系统是业务团队的“眼睛”。想象一下一个全球性的电商平台运营团队需要实时查看不同地区、不同品类的销售漏斗转化率风控团队需要毫秒级地筛查异常交易模式产品经理则想随时拉取过去任意时间段的用户行为热力图。这些需求背后是每天海量的、随机的、复杂的查询请求。当这个数字膨胀到“百亿次”级别并且要求每次查询都能在亚秒级甚至毫秒内返回结果时传统的数据库或数仓方案往往会瞬间崩溃。这不仅仅是“大数据”问题更是“高并发、低延迟、高可用”的综合性极端挑战。我参与设计和演进这样一个系统的几年里核心目标始终如一如何在资源成本可控的前提下让系统优雅地扛住每日百亿次的查询洪流并保证绝大多数查询的响应时间都在可接受的极低延迟范围内。这不像是一次性的批处理作业而是一场永不停歇、且流量随时可能波动的“马拉松”。背后涉及的技术栈选择、架构设计、优化技巧每一个决策都直接关系到线上服务的稳定性和业务方的体验。今天我就把这几年趟过的路、踩过的坑以及被证明有效的核心方案梳理出来希望能给面临类似规模挑战的团队一些实在的参考。2. 核心架构设计分层与解耦的艺术面对百亿QPSQueries Per Second这里更准确是每日百亿次折算成QPS也相当可观和低延迟的双重压力一个“大而全”的单体架构是绝对走不通的。我们的核心设计哲学是“分层处理、各司其职”和“查询与计算分离”。2.1 查询入口层智能路由与负载均衡所有查询请求首先到达的是查询入口层。这一层不做实质的数据计算它的核心使命是“快准稳”地分发请求。统一查询网关我们开发了一个轻量的查询网关服务。它对外提供统一的SQL或特定查询语言如Presto SQL、ClickHouse SQL接口对内则负责协议解析、基础语法校验、查询重写例如将一些业务常用但低效的写法优化为标准写法和最重要的——路由决策。基于代价的智能路由这是降低延迟的第一道关键闸门。网关会根据解析后的查询语法树快速预判查询的复杂度、涉及的数据量级和计算类型。例如点查/简单聚合直接路由到专门的键值缓存集群或OLTP数据库从库。这类查询模式固定延迟要求极高毫秒级。中等复杂度的多维分析路由到预计算聚合结果存储层如Apache Druid, ClickHouse的物化视图。这里存储了按常见维度组合预聚合好的结果查询只是简单的查找。复杂的Ad-hoc查询路由到大规模并行计算引擎如Presto, Trino, Spark SQL。这类查询无法预知需要扫描大量原始数据或中间数据。路由决策会结合实时集群负载信息避免将流量打到已经繁忙的节点上实现负载均衡。注意智能路由规则的维护是个持续的过程。需要建立查询日志分析流水线定期分析查询模式的变化发现新的“热点”查询进而决定是否为其创建新的预计算聚合物化视图或者调整路由规则。2.2 数据存储与计算层多模引擎协同这是系统的核心我们采用了“多模数据库/引擎”的混合架构而不是寻找一个“银弹”。Layer 1: 实时缓存与索引存储 (亚毫秒~毫秒级)场景应对最高频的KV查询、简单过滤查询。例如根据订单ID查详情根据用户ID查最近行为。选型Redis Cluster缓存热点数据 Elasticsearch复杂全文检索、多条件过滤。通过监听数据库变更日志如CDC近实时地同步数据到这两个系统。心得Redis的内存成本高我们通过精心设计的数据结构如使用Hash存储对象而非多个String和过期策略来优化。Elasticsearch的索引设计分片、副本、字段类型、是否分词对性能影响巨大需要根据查询模式反推设计。Layer 2: 预计算聚合存储 (毫秒~百毫秒级)场景应对占大头的固定报表、仪表盘查询。这些查询维度相对固定如按天、按地区、按产品线统计UV、GMV。选型Apache Druid或ClickHouse。它们专为快速聚合分析而生。我们会在数据链路中通过流处理如Flink或批处理如Spark任务将原始数据按照各种维度组合提前聚合好并导入这些引擎。实操细节这是降低延迟最有效的手段。关键在于设计“数据立方体”Cube。我们不是盲目预计算所有维度组合那会导致组合爆炸而是基于历史查询分析和业务预测优先计算那些高频和高价值的维度组合。例如[日期 地区 产品类目]是一个高频组合而[日期 用户年龄 支付方式]可能是另一个。Druid的roll-up能力和ClickHouse的AggregatingMergeTree表引擎在这里大放异彩。Layer 3: 原始数据与Ad-hoc计算 (秒级~分钟级)场景应对无法预测的、复杂的探索性分析。业务方可能会关联多张原始大表进行深度挖掘。选型对象存储如S3/OSS计算引擎如Presto/Trino。将最原始的、清洗后的数据以列式格式如Parquet, ORC存储在廉价的对象存储上。当有复杂查询到来时由Presto集群从对象存储中读取数据并进行分布式计算。优势存储计算分离成本极低弹性好。计算资源可以按需扩缩容存储则几乎无限扩展。虽然单次查询延迟较高但保证了系统的终极灵活性是“天花板”一样的存在。2.3 资源调度与隔离层为了让这么多引擎协同工作且互不影响资源隔离至关重要。物理/虚拟集群隔离预计算存储层Druid/ClickHouse和Ad-hoc计算层Presto部署在独立的集群上避免资源争抢。查询队列与限流在每个计算引擎内部我们根据查询类型、用户组或业务优先级设置了不同的查询队列。并配置了并发限流和内存限制防止单个“巨无霸”查询拖垮整个集群。弹性伸缩基于查询负载的监控指标如CPU使用率、队列长度我们为Presto这类无状态计算集群设置了自动伸缩策略。在流量高峰前自动扩容低谷时自动缩容以节约成本。3. 核心技术点深度解析架构搭好了但要达到“低延迟”的目标每一个技术环节都需要深度优化。3.1 数据模型与索引设计优化数据模型是性能的基石尤其是在Druid和ClickHouse这类系统中。维度与指标的精心设计高基数维度谨慎处理像用户ID、设备ID这类取值可能上亿的维度如果直接作为分组维度会导致预计算结果膨胀查询时内存消耗巨大。我们的做法是在预计算层将其转化为低基数维度例如只保留其所属的“用户层级”或“设备类型”或者在Ad-hoc层使用近似计算如HyperLogLog来处理其去重计数。指标预聚合尽可能在数据摄入时完成最细粒度的聚合。例如原始点击流数据在进入Druid/ClickHouse前就按分钟、用户、页面聚合好了点击次数、停留时长总和等指标而不是存储每一条原始点击记录。索引策略主键/排序键在ClickHouse中ORDER BY子句定义的表排序键是生命线。它决定了数据在磁盘上的物理顺序也决定了哪些WHERE条件可以高效过滤。我们的原则是将查询中最常用来过滤和分组的维度按区分度从高到低的顺序放入排序键。例如ORDER BY (日期, 省份, 城市, 产品ID)。跳数索引对于排序键中靠后的列ClickHouse的minmax、set等跳数索引能大幅加速数据跳过。我们会对高基数的、常用于等值过滤的列如订单ID创建set索引。Druid的位图索引Druid为每个维度列自动创建位图索引对AND/OR多维度过滤查询效率极高。优化点在于控制维度基数并利用roll-up减少行数。3.2 查询引擎级优化即使数据模型再好查询写得烂也会功亏一篑。避免“大表扫描”这是Ad-hoc查询延迟高的首要原因。我们通过以下方式缓解分区裁剪确保数据按时间如dt字段分区并且查询条件必须包含时间范围过滤。Presto等引擎可以据此跳过无关分区。谓词下推确保查询引擎能将WHERE条件尽可能地推送到存储层如ORC/Parquet Reader在读取数据时就进行过滤减少IO和内存中的数据量。列式存储的优势只读取查询中涉及的列这是列存Parquet/ORC的天然优势。合理使用近似计算对于百亿级数据精确去重计数COUNT(DISTINCT user_id)是性能杀手。在业务允许的误差范围内如0.5%使用近似算法能带来数量级的性能提升。HyperLogLog (HLL)用于估算基数UV。我们会在数据ETL阶段就计算好用户ID的HLL值并存入结果表查询时直接合并HLL值即可快速得到估算结果。百分位数近似使用approx_percentile函数替代精确的percentile_cont。资源管控与SQL提示在Presto中我们教会业务方使用注解或特定Session属性来为查询设置合理的资源上限例如SET SESSION max_memory_per_node20GB防止查询失控。优化Join策略鼓励使用广播Join当小表足够小时或基于分区的分布式Join避免代价高昂的Shuffle Join。3.3 缓存策略全覆盖缓存是应对高并发、降低延迟的终极武器我们在每一层都设计了缓存。结果缓存网关层缓存对于完全相同的查询SQL文本一致网关可以将其结果特别是聚合结果缓存一段时间如5分钟。这应对了仪表盘定时刷新、多人查看同一报表的场景效果立竿见影。计算引擎缓存Presto支持基于Hive Metastore的元数据缓存和基于Alluxio的分布式数据缓存能显著加速对相同数据块的重复查询。中间数据缓存在流计算生成预聚合数据时中间状态会利用RocksDB等嵌入式KV存储进行缓存和增量聚合避免全量重算。缓存失效与更新这是缓存设计的难点。我们采用“TTL 事件驱动更新”结合的策略。常规数据设置一个合理的TTL如1小时。对于关键核心数据则监听源数据变更消息主动刷新或失效对应的缓存项。4. 性能调优与监控实战系统上线后性能调优是一个持续的过程依赖于完善的监控和剖析工具。4.1 全链路监控体系我们建立了从应用到基础设施的全链路监控。应用层监控查询延迟分布使用直方图统计P99, P95, P50而非平均值。百亿次查询中保证P99延迟达标是关键。查询成功率实时监控失败查询的比例和原因语法错误、超时、内存不足等。慢查询日志自动捕获并记录执行时间超过阈值的查询包括其完整的SQL、执行计划、资源消耗用于后续分析优化。资源层监控计算集群CPU使用率、内存使用/压力、GC情况、节点健康状态。存储集群磁盘IOPS/吞吐量、缓存命中率、压缩率。网络跨可用区或跨数据中心的网络延迟和带宽。4.2 性能问题排查流程当收到延迟报警或业务方反馈查询变慢时我们有一套标准的排查流程定位问题层级首先通过查询网关日志确定慢查询被路由到了哪个引擎Druid, Presto还是缓存。分析查询本身获取慢查询的SQL。检查是否缺少关键过滤条件如时间范围是否对高基数维度进行了DISTINCT是否有多表Join且缺乏有效关联条件。查看执行计划对于Presto/Trino使用EXPLAIN ANALYZE获取详细的分布式执行计划观察哪个Stage耗时最长是扫描数据量过大、网络Shuffle数据过多还是某个计算节点成了瓶颈。对于ClickHouse使用EXPLAIN查看查询执行流水线并配合system.query_log表查看详细的执行指标。检查系统状态查看对应集群在查询时间点的监控指标是否存在资源瓶颈如CPU打满、内存溢出导致频繁Full GC、磁盘IO等待队列过长。优化与验证根据分析结果进行优化如增加缺失的索引、优化SQL写法、调整数据分区然后在测试环境用相同查询验证优化效果。4.3 成本与性能的平衡追求极致性能的代价是高昂的成本。我们始终在两者间寻找平衡点。数据生命周期管理TTL原始明细数据、轻度汇总数据、高度聚合数据设置不同的保留周期。越细粒度的数据保留时间越短通过降冷归档至更廉价的存储如对象存储的归档层来节省成本。计算资源弹性利用云服务的弹性伸缩让计算资源Presto Worker节点与查询负载曲线尽可能匹配避免长期保有大量闲置资源。存储格式优化定期评估和转换数据存储格式。例如将文本格式转为压缩率更高的列式格式如Parquet并使用ZSTD等高压缩比算法节省存储空间和IO带宽间接提升查询速度。5. 踩坑实录与经验总结回顾这几年的历程有几个“坑”印象尤为深刻。坑一过度预计算导致“数据爆炸”。早期为了追求速度我们试图为所有可能的维度组合创建物化视图。结果导致存储成本飙升数据管道任务繁重且业务需求一变大量预计算表就沦为废表。教训预计算必须基于真实的、高频的查询模式采用“按需建设、逐步迭代”的策略。建立查询分析看板让数据说话。坑二忽视“长尾查询”的影响。系统平稳运行一段时间后突然在某个业务高峰出现整体延迟飙升。排查发现是有用户提交了极其复杂的、涉及全表扫描的Ad-hoc查询占用了大量计算资源挤占了常规报表查询的资源。教训必须实施严格的资源隔离和限流策略。为Ad-hoc查询设置独立的、有资源上限的队列并设置查询超时时间必要时“杀”掉失控查询。坑三监控指标不全面。曾有一次线上故障查询成功率下降但CPU、内存监控均显示正常。最后发现是集群中某几块SSD磁盘的IO延迟异常升高而我们的监控没有覆盖到磁盘单实例指标。教训监控要到“毛细血管”级别。除了集群整体指标节点级、甚至磁盘/网卡级的指标同样重要。需要建立完善的健康度评分体系。坑四数据更新与一致性难题。当源数据发生更新或删除时如何同步更新下游的预计算聚合层和缓存层是一个复杂的问题。我们最初采用T1的全量重建但无法满足实时性要求。后来切换到基于CDCChange Data Capture的增量更新但需要处理乱序、重复等数据质量问题。教训实时数仓的更新链路设计需要非常谨慎要在延迟、准确性和复杂度之间做出权衡。引入像Apache Iceberg这样的表格式来支持ACID和增量读取是一个值得探索的方向。构建并维护一个能日扛百亿查询的低延迟系统没有一劳永逸的银弹。它是一套结合了合理架构、精细优化、智能调度和全面监控的“组合拳”。技术选型会随着时间演进比如现在可能考虑Data Lakehouse架构但核心思想不变理解你的数据理解你的查询然后为每一类查询匹配最合适的处理路径并通过缓存和预计算把尽可能多的工作做在查询发生之前。这个过程充满挑战但每当看到业务方能流畅、即时地获取洞察并驱动决策时便觉得这一切的复杂和折腾都是值得的。