尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

分布式数据立方体:预聚合架构与查询性能优化实践

分布式数据立方体:预聚合架构与查询性能优化实践 1. 为什么“查询快”和“数据大”总是打架数据立方体的价值定位1.1 从一次真实的报表卡顿说起先说一个我印象很深的场景。有一年年底业务方要做全年经营复盘需要在几十亿行明细数据上按“月份×区域×品类×渠道”做汇总一套多层钻取的报表要求每个页面5秒内出数。当时团队第一反应是“上更好的数仓引擎”“加大查询并发”结果压测一跑明细表扫描动不动几十秒加上多维钻取要频繁做Group By数据库直接被打满其他业务线跟着遭殃。后来我们换了一条路不查明细了提前把各种维度组合下的汇总结果算好存起来。这就是数据立方体Data Cube的思路。简单说数据立方体就是一组按维度预先聚合好的结果集用户在界面上点“按月份汇总”实际上命中了一条已经算好的月维汇总而不是现场对几十亿行明细做实时聚合。查询响应从几十秒直接降到几百毫秒这是查明细完全做不到的。1.2 数据立方体的本质拿存储换查询时间用大白话讲数据立方体就是在做“以空间换时间”的生意。传统做法是数据来了就存查询时再现场算。这相当于你每次做饭都从买菜、洗菜开始而数据立方体是提前把菜洗好切好、甚至做成半成品客人点菜时热一下就能上桌。维度越多、数据量越大“现场做饭”的代价越高Cube的价值就越明显。一个标准的数据立方体会对事实表比如订单表按若干维度时间、地区、商品、渠道做组合聚合保存每个维度组合下的度量值比如销售额、订单数、客户数。用户查询任意维度组合的汇总只要预聚合层的组合覆盖到了就直接返回结果。这个“覆盖”设计很关键后面会详细讲。1.3 单机Cube的瓶颈到底是什么数据立方体概念是上世纪90年代OLAP领域提出的早年在单机多维数据库上跑得很成熟。但到了大数据时代问题变了数据规模上了几个量级单机内存根本放不下全量Cube构建任务的计算量也暴涨单机跑一次全量预聚合可能要跑几天查询并发上来了单机服务扛不住所以就有了“数据立方体的分布式实现”这个命题。它不是简单把Cube切成几块扔到多台机器上而是要重新设计分片、构建、路由、一致性、容错整套链路才算真正落地。这也是本文想聊清楚的核心内容。2. 分布式数据立方体的整体架构存储、计算、调度三件事2.1 基本架构分层我基于Kylin、Druid、ClickHouse这类系统的思路结合自己项目的实践把分布式Cube的架构拆成四层层次职责典型组件接入层接收查询请求解析、校验、鉴权查询网关、负载均衡路由层根据查询维度匹配最优Cube分片生成执行计划元数据服务、查询引擎计算层执行Cube构建、聚合计算、增量合并分布式计算框架、调度平台存储层保存预聚合结果、维度字典、元数据列式存储、对象存储、KV存储这里最容易犯的错是把注意力全放在存储层觉得“把Cube数据分到多台机器”就万事大吉。实际上路由层才是分布式Cube的“大脑”。查询进来后到底命中哪个Cube命中哪几个分片合并哪些中间结果都靠它决策。路由决策错了后端存储再快也白搭。2.2 分片与分区Cube粒度的设计分片是分布式Cube最核心的设计决策。我见过一种做法把整个Cube按维度组合拆成多个“小Cube”每个小Cube负责一部分维度组合分散到不同节点。这样设计的问题在于用户查询通常跨多个维度组合比如“按城市看月度销量”可能要同时读好几个小Cube再合并查询链路变长性能反而不如单机。更实用的做法是分两级第一级按常用维度切分比如按时间。月度数据放一个分区季度数据放一个分区。热点数据最近一个月放在高性能存储历史冷数据放成本更低的存储。第二级在分区内部按维度值做Hash分片比如按“区域ID Hash”均匀分散到N个节点。这样每个分片只负责一部分区域的数据查询时通过路由精确命中的分片数可以压缩到很小。2.3 元数据服务与查询路由元数据服务记录每个Cube分片的状态构建到哪个版本、数据区间、维度组合、存储位置、分片键范围。查询路由拿到SQL后先解析出涉及哪些维度再查元数据确定命中的分片集合然后把查询下推给对应节点执行。这里有一个很容易忽略的细节维度组合匹配。数据立方体里不是所有维度组合都提前算了比如“月份×区域×品类”算了“小时×门店×单品”可能因为组合爆炸没算。路由时要做“上卷匹配”把查询转成最接近的预聚合层。比如用户想查“天×城市”汇总但没有这个层只有“月×城市”和“天×省”两层路由器需要判断哪一层上卷后再过滤代价更小。这个决策逻辑写得不好查询性能会剧烈波动。3. 核心构建流程从原始数据到分布式Cube3.1 维度建模与编码构建Cube前先要确定维度和度量。维度是分组字段度量是聚合字段。这里建议遵循一个原则优先复用它不要随意造新维度。扩展维度的代价在后端组合数会翻好几倍。高基数维度比如用户ID、设备ID需要特别注意。几千万个用户ID直接作为维度生成的组合数爆炸构建和存储都撑不住。实践中通常有两种处理维度编码为整数ID用全局字典压缩查询时再解码层级化处理把用户ID上卷到用户分组、城市等较粗粒度细粒度查询落到明细库3.2 分片构建与聚合计算构建过程可以拆成“原始数据扫描→维度分组聚合→写入分片存储”三大步。分布式环境下计算逻辑通常跑在一个批量计算框架上我们用的是Spark也有团队用Flink批模式利用分布式计算框架天然的分区能力做聚合。构建流程我通常建议这样设计按时间分区读取源数据尽量只扫描增量数据对每个分片生成计算任务按分片键做局部聚合汇总局部聚合结果做全局精准聚合将聚合结果写入列式存储更新元数据版本这里最关键的优化点是“局部聚合”。比如按区域Hash分片每个分片内先GROUP BY一次输出该区域的聚合结果跨分片拉平汇总时只需要合并少量结果行而不是全量明细。这个优化在数据量大时能省一个数量级的Shuffle开销。3.3 预聚合的调度与依赖管理预聚合层不是一次构建完就结束了。业务数据天天有增量Cube需要按周期重建或增量更新。所以构建任务天然是一个有依赖关系的调度DAG先做维度字典更新再做基础层聚合最后做上层汇总。之前我们把调度逻辑写死在构建脚本里结果每次加一层维度都要改Shell脚本维护成本很高。后来切换到分布式任务调度平台我们用的是Apache DolphinScheduler把每个构建步骤拆成独立任务通过配置描述依赖关系。这样扩展新Cube时只需要新增任务并配置上下游依赖不需要改代码。这里还有一个容易被忽视的问题任务并发控制。多个构建任务同时写同一个Cube分片时会互相覆盖数据。所以在调度层必须加互斥控制确保同一个分片同一时刻只有一个构建任务在跑。3.4 增量更新与历史数据合并绝大多数团队走的路径是“分区级增量替换”。新到数据按时间落到新分区单独构建这个分区的Cube查询时读取多个分区的结果再合并。这样做的好处是构建速度快但跨分区合并引入了一层额外的聚合开销。有些场景要求更高需要跨多个分区的预聚合结果比如跨年的同环比分析。这种数据可以单独建一个“跨分区Cube”定期由各分区Cube汇总而来。以周或者月为周期重建不要让这个高频执行否则构建队列会变成瓶颈。4. 分布式一致性缓存、锁与数据同步4.1 Cube缓存与分布式锁的搭配我看到很多团队在分布式Cube架构里提到“缓存”第一反应是加一个Redis缓存查询结果。这当然有用但过度依赖查询结果缓存会掩盖路由层的问题。我的经验是缓存要分两层第一层是维度字典缓存把高基数维度的编码表缓存到内存避免每次查询都查全局字典第二层是Cube数据分片缓存把热点分片的聚合结果块缓存到查询节点本地这里就涉及分布式锁。当多个节点同时尝试更新同一个缓存分片时需要确保只有一个节点去后端存储加载数据其他节点等待。我们用Redis分布式锁做过这个互斥控制。具体实现是以CacheKey为锁资源获取锁后先查一次缓存Double Check如果还没有数据再加载后端存储并回填。注意分布式锁的粒度很关键。锁粒度太粗会导致大量请求排队太细则锁竞争开销大失去缓存的意义。按“分片维度组合”而不是全局一个锁做锁粒度实测效果最好。4.2 构建过程中缓存失效的坑构建过程中最容易踩的坑是旧Cube还在服务中新Cube构建完成缓存如何平滑切换我最早的做法是“构建完成后直接清缓存”。结果就是构建完成的那一瞬间大量查询同时落到后端存储后端被打爆。后来改成版本号机制每个Cube分片在元数据中带一个版本号查询时把命中分片的版本号拼进CacheKey。新版Cube构建完成后新的查询自然走新CacheKey旧版本缓存逐步过期由LRU自动淘汰。这个方案的好处是不需要主动清缓存冷热交替自然完成线上完全无感。4.3 数据同步的最终一致性策略分布式环境下一个查询要合并多个分片的数据这就要求各分片之间数据状态是一致的。举个具体例子构建“月×区域”Cube时1号到15号的分片已经使用最新数据16号到30号的分片还是上一版本数据用户查询全月汇总拿到的是新旧混合结果。这个问题没有一个银弹解法。我们现在的策略是“增量分区优先跨分区一致的场景用快照发布”增量查询接受小时间窗差异各分区独立构建跨分区一致查询比如财务对账构建一个快照Cube所有分片构建完成后统一发布发布前查询仍用旧版本在Kylin的实践中也有类似机制叫“可查询时间段”构建完成的分区才允许被查询。这种方式思路一致用元数据标记来限制查询范围。5. 查询性能调优命中率、裁剪与热点处理5.1 查询路由与智能聚合选择查询性能的最大变量是“聚合层级选择”。我刚才说过Cube不会覆盖所有维度组合路由器要决定用哪一层预聚合结果来回答用户的查询。举例说明用户查询是按“月份×品类”看销售额系统里存在“天×品类”层和“月×品类”层。直觉上应该选“月×品类”但是如果这个Cube分片最近没有更新而“天×品类”层更新更频繁路由器可能选择读取最近几天的“天×品类”结果并做一次上卷。这个决策需要结合分片新鲜度、数据量、计算代价综合判断。我们在路由层维护了一张“候选层级评分表”每次查询生成候选集按“数据新鲜度、扫描数据量、聚合计算代价、节点负载”四个因子加权评分选最优。大促等活动期间热点Cube有更高的新鲜度权重确保活动数据第一时间可见。5.2 高基数维度的处理方案高基数维度是Cube查询性能的“隐形杀手”。拿“UV口径的用户ID分析”举例用户ID维度值特别多如果Cube里包含了这个维度每个分片内部的维度组合数量会很大存储膨胀查询扫描的分片变多。实用方案有三条路砍掉高基数维度的细粒度层只保留粗粒度层查询高基数维度时路由到明细库引擎不查Cube用HyperLogLog这类近似算法牺牲少量精度换来数量级的空间压缩我个人的经验是优先做第一和第三的搭配普通查询走Cube粗粒度层高基数精确去重走明细库近似分析走HLL。不要让一个Cube承载所有需求适当拆分才能各司其职。5.3 热点分片与倾斜问题的缓解分布式系统里数据倾斜是个永恒话题。Cube场景的一个典型表现是按时间分区时最近一天分片访问量远高于历史分片按地域分片时北京、上海的分片可能是新疆分片的几十倍。倾斜不解决效果就是集群某些节点忙死其他节点闲死查询响应时好时坏。我们的处理思路对热点分片再做二次分片比如最近一天的数据额外按服务端节点拆4份热点分片冗余多份查询通过一致性哈希分散到不同副本根据历史访问统计动态调整分片数热门分片扩容冷门分片缩容这套机制让我意识到一个道理分布式Cube的均衡不是设计出来的而是调出来的。没有一成不变的完美分片只有持续观测和调整。6. 我在实际项目中踩过的坑与实用经验6.1 Cube膨胀失控第一次上线时我们配置了10个维度全组合的预聚合结果构建完发现存储占用比原始数据还大40倍。后来才意识到维度组合数是2的N次方级别增长10个维度就是1024种组合每种组合都要存一份聚合结果。这就是所谓“Cube膨胀”。优化手段强制设置维度组合黑白名单只保留业务真正会查的几十种组合开启聚合组Kylin的Aggregation Group把维度拆成多个组避免全组合合并低基维度为一列减少行列数现在我们的标准是构建完的Cube存储量控制在原始数据量的3-5倍以内超过就要审视维度配置。6.2 锁超时与构建任务卡死Redis分布式锁用得不好就会遇到一个很经典的问题构建任务处理大型分片耗时太长锁自动过期了另一个任务趁机获取锁开始构建同样的分片两个任务同时写数据错乱。排查过程很曲折。先看日志发现两个构建任务同时写同一个分片以为是调度重复触发后来查了锁的自动过期时间才发现默认10秒超时根本不适用于动辄几分钟的构建任务。修复方案锁的超时时间根据分片大小动态计算留出足够余量用看门狗机制续期任务运行中超时自动续期任务完成后主动释放写入前做版本检查如果分片版本号已变说明有其他人构建过本次写入作废6.3 元数据不一致导致查询漂移有一次线上查询偶发出现“同样的条件结果时好时坏”排查了一大圈最后定位到元数据服务。发现有一个Cube分片的元数据被两个服务实例同时更新一个写“已发布”另一个写“构建中”查询路由不稳定有时命中新数据有时命中旧数据。根因是元数据读写缺乏事务保护。我们加入了乐观锁版本号比较后写入并规定Cube的元数据更新统一走单一写入口服务其他服务只读。这个问题解决的直接教训是分布式系统的元数据宁可慢一点也不要让多个入口同时写。6.4 实践中的几条建议先从最常用的3-5个维度做起跑通全链路后再逐步加维度不要一上来就全量组合查询路由的日志一定要全量保存出问题时通过它回溯每个查询命中的层级和分片调优全靠这份数据监控指标重点盯三个Cube构建时长、查询命中率、分片扫描行数。命中率低于80%就说明预聚合配置有问题构建任务的失败重试要设计好幂等性同分片重复构建不会产生脏数据靠版本号控制分布式数据立方体不是一个装了就能用的组件它需要根据业务查询模式、数据规模、更新频率反复调优。建好之后维护成本不低但相比每次查询都深挖明细层把压力前置到构建阶段换来稳定的查询延迟这笔账是算得过来的。
返回列表