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

资讯详情

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

存算分离架构下大数据查询性能优化实战:从瓶颈到落地

存算分离架构下大数据查询性能优化实战:从瓶颈到落地 从“大数据”这几个字被反复提起开始打磨数据查询性能就成了团队里最常被丢过来的硬骨头。以前我们处理性能问题思路比较直接堆机器、调参数、优化SQL可这些招数在数据量涨到一定规模之后效果会肉眼可见地变差——资源利用率越来越难看扩容的成本却居高不下。最近两年我所在的项目组全面转向了存算分离架构这个选择让我们的数据查询性能优化走入了一个完全不同的阶段。这篇文章就把我们这一路的思路、实践和踩过的坑整理出来围绕大数据领域的存算分离怎么真正落地、怎么把查询性能从细节里抠出来做一个相对完整的复盘。存算分离不是什么新概念但在大数据领域它正在从“可选项”变成“必选项”。它最核心的变化就是把过去绑定在一起的计算节点和存储节点彻底拆开让计算集群专注算、让存储集群专注存中间通过网络连接。理解这个架构并不难难的是如何在它下面把一个复杂的查询跑得又快又稳因为数据访问路径变了瓶颈也变了我们原来的很多优化经验都要重新打碎重组。1. 存算分离的核心理念为什么非拆不可1.1 从Hadoop时代说起存算一体的历史包袱在讲存算分离之前得先把存算一体的来龙去脉捋清楚。早期的Hadoop生态HDFSHadoop分布式文件系统和计算框架是深度耦合的。数据存储在数据节点的本地磁盘上计算任务调度时会把代码“移动”到数据所在的节点上执行这就是那句著名的“移动计算比移动数据更划算”。这种设计在数据规模还不是特别夸张的年代确实高效因为数据本地性data locality让计算和存储之间的交互尽可能发生在同一台机器内部网络开销被压到了最低。但问题也随之而来。计算和存储的资源需求往往不是同频增长的。有些业务计算密集需要大量CPU和内存去跑复杂任务有些业务存储密集需要非常大的磁盘空间去保存海量冷数据。在存算一体的架构里这两类需求被强行绑在了一起。想扩容计算能力你不得不连带着增加存储容量想扩充存储你又得浪费掉多余的算力。更现实的是计算集群通常是按峰值负载来规划的在业务低谷期大量计算资源闲置但机器还得照常运行、照常耗电、照常占用机柜空间。我经历过的真实场景是有一段时间部门的离线报表计算任务明显变多但存储的增长速度完全跟不上计算的增长。扩计算节点吧HDFS很快也会跟着多出好几TB的副本空间来存储那些根本没人查的日志不扩吧晚上跑批的排队时间越拉越长。这就是存算一体架构最难受的地方——资源的弹性被锁死了。1.2 存算分离的突破点计算与存储各自为政存算分离把这两者拆开的关键其实是通过一个“共享存储层”来实现的。在这个架构下数据统一存放在远端存储系统上比如云上的对象存储S3、OSS、COS这类、或者分布式的共享文件系统JuiceFS、Alluxio、GlusterFS等而计算集群则变成了一组无状态的计算节点任务需要数据时通过网络从共享存储中读取。这样一来计算和存储就有了各自的伸缩空间和生命周期。你的集群想扩容只需要新起计算节点几分钟就能加入集群干活不需要关心数据要不要重新拷贝、副本要不要重新平衡。业务低谷期直接把计算集群缩容甚至缩到零存储里的数据纹丝不动永远不会丢。这一点在做大数据平台的同学看来价值是巨大的——基础设施成本一下子从双份变成了单份弹性也真正被释放了出来。还有一点容易被忽略存算分离对硬件选型也有了更大的自由度。在存算一体的模式下每台机器的配置往往是多方妥协的结果。而分离之后计算节点可以选高性能CPU、大内存、有NVMe本地盘做缓存存储节点则完全可以选低成本的机械盘加高密度存储配置。各取所需性价比自然就上来了。1.3 但“拆开”不等于“性能好”我需要特别提醒一点存算分离架构本身并不会自动让查询变快。恰恰相反如果设计不当、优化不到位它比存算一体更容易出现性能瓶颈。为什么呢因为原本“在同一台机器上读本地文件”变成了“通过网络从远端存储读数据”网络I/O的延迟和带宽一下子成了新的约束。这里的核心矛盾在于存算分离优化查询性能从来没有脱离过“尽量减少数据跨网络传输”这个大原则。所以真正要想明白的问题是什么数据必须拉回计算节点什么数据可以缓存在本地什么数据能在存储侧做一下预处理这些问题解决了存算分离才能又省钱又好用。2. 存算分离架构下的性能链路瓶颈在哪儿2.1 一条查询的完整旅程我们在做性能分析时习惯先把一条查询从头到尾的完整路径画出来。不要小看这一步很多时候性能问题不是某一个环节的问题而是多个环节叠加出来的。一个典型的存算分离架构下查询路径大概是这样的客户端提交SQL → 查询引擎如Presto/Trino、Spark SQL解析生成执行计划 → 任务调度器把分片任务分发到多台计算节点 → 计算节点从元数据服务获取数据文件位置 → 计算节点通过网络从远端存储读取数据块 → 数据进入计算节点内存进行过滤、聚合、Join等计算 → 结果汇合返回客户端。对比存算一体中间多出了“元数据服务”和“网络读取远端存储”两个绕不开的环节。元数据服务负责告诉你数据在哪、该读哪个文件网络读取则是真正花时间的地方。一般来说元数据服务如果选型合理、做好缓存不会成为瓶颈真正的性能大头基本都集中在存储读取这一段。2.2 网络I/O最大的新瓶颈存算分离后网络I/O的质量几乎决定了查询性能的天花板。这一点我们在实践里体会特别深。同样一个TPC-DS测试集在本地HDFS上跑可能只要30秒到了远端对象存储上直接翻到2分钟以上原因就在于几十GB甚至上百GB的中间数据要从远端拉回内存而网络带宽和延迟摆在那里。具体来说网络I/O带来的问题主要有三个层面延迟对象存储的每次请求本身就有一定的网络往返时间大量小文件的随机读会因为请求过多而饱受延迟困扰。带宽当多个查询并发执行集群内所有节点同时从存储读取数据很容易就把网络带宽打满导致整体吞吐量骤降。限流很多云厂商的对象存储有API请求频率的限制比如每秒请求数不能超过多少如果应用层没有做好请求的合并和缓存很容易触发限流导致查询任务直接失败或重试。这三个层面哪一个没处理好查询性能都好不了。所以在选型时我们当时对比过HDFS和对象存储方案最终还是优先选择了有着更好吞吐表现和更低延迟的分布式文件存储方案作为主存储同时保留了对象存储的方案作为冷数据归档。这个妥协很现实性能指标先达标成本第二。2.3 数据本地性的消失与重建原来在Hadoop生态里我们很依赖数据本地性来提升效率任务调度器会尽可能把计算任务调度到数据所在的节点让任务在读本机数据时不需要经过网络。存算分离拔掉了这层地基数据在远端不管你任务调度到哪个节点都要走网络。那本地性是不是就完全没用了不是。我们可以在计算节点上做“本地缓存”把经常读到的数据块复制一份到本地磁盘或内存里。这样虽然数据源头在远端但第二次读同样的数据块时命中的是本地缓存速度几乎可以回到存算一体的水平。这里的关键词是缓存命中率。如果业务以热数据为主比如频繁查询最近一天的数据本地缓存带来的提升会非常显著如果业务是随机扫描大量冷数据缓存就形同虚设那网络带宽就又变回首要瓶颈。所以在架构设计阶段就要想清楚你的业务是热点集中型还是全量扫描型。这直接决定了你在缓存上该投入多少资源。3. 实操要点查询性能优化的四个关键维度3.1 存储侧的优化文件格式与数据组织数据在存储里是什么样子直接影响查询读出多少数据量。我们常说“性能优化从数据建模开始”在存算分离架构下这句话更加成立因为你要尽量避免大范围的数据拉取。我的建议是文件格式尽量用列式存储比如Parquet或者ORC。列式存储的好处在于查询只需要读取涉及的列而不是把整行数据都拉回来。举个例子你有一个100列的表但查询只用到其中5列列式存储能把扫描的数据量降到原来的5%这节省的不仅是存储I/O还有宝贵的网络带宽。在存算分离架构里少传输1MB数据比把传输速度提升1MB/s更值钱。数据组织上分区和分桶要继续做好。分区是按某个字段如日期、地域把数据切成一个个独立的目录查询时通过分区裁剪跳过不必要的目录分桶则是把数据进一步散列到固定数量的桶文件中让Join时可以在桶级别做匹配而不必全表扫描。实操的时候我会建议把分区字段选择和查询条件对齐频繁按哪个字段过滤就按哪个字段分区。尽量减少单个文件的数量和尺寸控制在一个合理的范围比如128MB到512MB之间避免因为小文件过多导致请求频繁增加了网络和元数据的负担。3.2 计算侧的优化引擎参数与执行计划说完存储侧再来看计算引擎侧。查询引擎的选择很重要像Spark适合跑复杂的ETL和批处理任务Presto/Trino更擅长做交互式查询StarRocks、Doris这类MPP数据库则在海量数据下也保持了比较好的实时性能。存算分离架构下我们还要关注引擎和存储的适配能力比如是否支持谓词下推、是否能把过滤条件下推到存储层提前处理减少网络传输量。引擎参数上不同框架有各自的调优空间但有几个大方向是通用的增大并发度让更多计算节点同时并行读取不同数据块充分利用网络带宽和多核CPU。对应到Spark里可以调整分区数Presto里则关注并发分片数量。内存管理给执行器足够的内存做缓存和哈希表减少中间结果的落盘。存算分离下中间结果一旦落盘往往意味着又是一次网络写、网络读代价很大。动态资源申请开启动态资源分配根据任务负载自动调整执行器数量避免算力浪费。执行计划方面建议多用引擎提供的EXPLAIN功能提前找出潜在的高代价操作比如大表Join小表、多次Shuffle、数据倾斜等。这些优化和存算一体是相通的但在存算分离下它们的影响会通过网络被放大所以排查优先级要更高。3.3 缓存策略多级缓存与生命周期管理缓存是存算分离性能优化里最立竿见影的手段之一但也最容易被用砸。我们在实践中构建了三级缓存体系每一级的容量、性能和成本都不一样节点本地内存最快但容量有限适合放最热的数据块和过滤后的中间结果。节点本地SSD容量比内存大不少速度虽然不如内存但远好于网络读适合放热数据和一些体量较大的中间输出。集群内分布式缓存比如Alluxio统一管理多个节点的缓存让数据能被不同查询共享避免重复从存储层拉取。这里有一个经验缓存的淘汰策略和预加载策略一定要结合实际业务来设计不能盲目用默认配置。如果一个业务是每天凌晨定时跑批那跑批开始前可以把这一天要用的数据预热到缓存里如果业务是随机查询那更适合用LRU这类策略自动保留最近访问热度高的数据。还有一点要注意缓存一致性。当底层存储里的数据发生了更新比如新的分区数据写入本地缓存里的旧数据如果不及时失效查询结果就会出现错误。我们当时用了一个比较务实的方案——分区级别的版本号管理每次查询先检查元数据的版本号和缓存中的版本号不一致时就主动失效并回源拉新这样既保证了正确性也没有让每次查询都绕过缓存。3.4 网络与元数据调优同样不可忽视网络层面我们主要做了三件事调整网络参数包括增大TCP缓冲区大小、开启巨帧jumbo frame等让大数据块的传输更高效。合理规划拓扑让计算集群和存储集群在同一个地域甚至同一个可用区减少跨地域的物理距离降低延迟。实时性要求高的业务最好让计算节点和存储节点在同一个VPC内。并发控制控制应用层的读取并发数避免同时发起过多请求打满带宽或触发存储限流。元数据服务的优化也容易忽略。在存算分离架构里元数据比如Hive Metastore、Iceberg的Catalog负责管理表结构、分区、文件位置等信息如果元数据查询慢了整个查询的启动阶段就会被卡住。最基本的做法就是给元数据库做缓存和索引并且尽量将元数据请求合并减少与元数据服务的交互次数。4. 实战复盘一次真实的数据查询性能优化记录4.1 业务背景与初始状态说多了概念和策略聊一个具体的实战案例。我们有一套用户行为分析系统数据存储在远端的JuiceFS上计算引擎用的是Presto主要服务于业务部门的数据看板和即席查询。系统上线一段时间后用户的反馈是看板页面经常加载很慢一些细节报表要等十几秒甚至半分钟还有同事抱怨说凌晨跑的数据没有及时刷新查询出的始终是昨天的旧数据。我们当时先做了个摸底简单聚合查询平均耗时在8秒左右复杂的多表Join查询经常超过30秒很多查询读取的数据量其实很小但耗时却很长。这就是典型的存算分离场景下性能没有调优的表现。4.2 定位问题三个明显的瓶颈排查过程遵循“先看链路再看细节”的思路。我们把一次典型查询的日志梳理出来很明显地看到了三个瓶颈冷读问题严重很多查询读取的数据分布很散每次都要回源到底层存储本地缓存命中率不到30%。特别是看板上的同类型查询明明数据差不多却没有利用上上一次查询的缓存。小文件太多上游写入任务因为设置不当生成了大量小文件有的分区里甚至有几百个不到1MB的小文件。Presto在读取的时候需要为每个文件发起一次RPC请求小文件一多延迟彻底被放大而且因为这些文件太小网络传输的效率也非常低。统计信息缺失Presto的CBO基于成本的优化依赖表的统计信息。如果统计信息不更新Join的顺序和方式就选不好明明应该小表驱动大表实际执行计划却反过来了性能自然差。4.3 优化动作我们从三个方向下手针对这三个问题我们分三步走第一步缓存策略重做。原来用的缓存策略是简单按文件路径做的LRU确实存在命中率低的问题。我们调整了思路把看板常用的几个表和字段单独列出来在业务低峰期和凌晨批次任务结束后做一次缓存预热同时为这些热查询场景启用更精细的延迟加载规则让同一类查询的公共子结果也能被缓存复用。过了两周整体缓存命中率从不到30%提升到了65%左右看板类查询的响应时间明显下降。第二步合并小文件。这个没什么捷径就是每天在批次任务落表之后加一步对小文件进行合并的操作把每个分区控制到几十个左右、每个文件128MB左右。合并完之后查询要发起的RPC请求数量断崖式下降网络请求不再是瓶颈。第三步手动更新统计信息并优化执行计划。我们给Presto配置了定时执行ANALYZE任务的调度保证统计信息不过期。同时针对最常用的两个看板SQL用EXPLAIN人工分析了执行计划发现有一处Join用错了表顺序通过调整SQL写法把过滤条件下推、减少中间结果集后把那一条查询从22秒压到了6秒以下。4.4 前后对比与心得优化做完之后我们把系统里最核心的几个查询重新跑了一遍效果如下表所示查询场景优化前平均耗时优化后平均耗时主要优化手段用户活跃看板单表聚合8秒2秒缓存预热 小文件合并多日留存报表多表Join26秒7秒统计信息更新 SQL调优渠道分析即席查询随机过滤15秒4秒分区裁剪优化 列式存储优化这个结果符合我们的预期。总结下来整个优化过程中最核心的体会是存算分离架构下性能优化更像是一个系统工程不是调整某一个参数就能见效的需要把数据组织、缓存策略、计算引擎、网络链路全都拉通来看。5. 常见问题与排查技巧5.1 查询变慢时的排查顺序在存算分离环境下遇到查询性能下降我建议按以下顺序排查不要一上来就调参数先看查询计划确认是否扫描了过多数据分区裁剪和应用在存储层的过滤条件是否生效。再看缓存命中率如果命中率很低先找原因——是缓存容量不够还是缓存策略不匹配还是数据本身太分散。然后看网络指标检查网络吞吐、延迟和丢包情况确认是否存在带宽打满或限流。最后看存储侧指标确认存储系统的资源使用情况底层存储的队列深度是否过高请求是否有重试。这套排查顺序是符合“从应用层往基础设施层”的递进逻辑的。越靠前的判断成本越低、调整越快如果一开始就扎进底层网络监控里很容易迷失方向。5.2 缓存命中率低的三个常见原因缓存命中率不理想大概率是下面这三个原因之一缓存容量不够缓存空间太小装不下热数据频繁被淘汰。解决办法是合理规划缓存容量或者缩小需要热缓存的数据范围比如提升分区粒度。数据访问太离散查询条件随机性强导致读的数据块非常分散缓存还没来得及积累热度就已经被淘汰。这种情况可以考虑调整分桶规则让经常一起读的数据在物理位置上也更接近。缓存策略和业务不匹配默认的缓存策略适合通用场景但不一定适合你的业务模式。看板类固定报表适合定时预热即席查询则需要更智能的淘汰策略。5.3 网络延迟高或吞吐不足怎么办如果确认瓶颈在网络侧可以从这几个方面着手检查物理距离计算和存储是否跨地域尽量迁移到同一可用区。调整并发策略对大规模并行读取场景做限流和分批控制避免“请求风暴”。启用压缩在CPU资源有富余的情况下开启数据压缩传输比如Snappy、ZSTD用一定的CPU开销换取更低的网络传输量。压缩率通常能达到2~4倍对网络瓶颈有立竿见影的缓解效果。升级网络规格如果条件允许增加网卡带宽或使用RDMA等高性能网络方案这是最直接但成本也最高的办法。5.4 踩过的坑缓存一致性问题最后再强调一个我们踩得最深、恢复成本最高的坑——缓存一致性问题。开始做本地缓存不久我们就遇上了数据更新后查询结果还是旧的的情况。当时排查比较费劲因为不是每次查询都错只是那些缓存尚未失效的节点会查到旧数据。最终方案我在前面提到过就是给每个分区引入版本号每次查询前比对一次分区版本。如果版本没变直接读缓存版本变了才强制回源读取新数据并更新缓存。这个机制虽然多了一次元数据服务的交互但这点开销换来了数据正确性非常值得。6. 场景与展望存算分离还能用在哪儿聊了这么多具体优化回到大数据领域的宏观视角。存算分离的价值不只是“省成本”三个字可以概括的。在实时数仓场景存算分离让实时计算和离线计算可以共享同一份数据数据不再需要复制多份一致性更好存储成本也大幅下降。我们在做实时指标计算时就直接把Kafka流写进来和离线Hive表放在同一个存储层一套数据服务多个引擎效率提升非常明显。在AI和大数据融合的场景存算分离让特征数据、训练数据、模型文件都能沉淀在统一存储层训练集群按需拉起、按需缩容底层数据和计算引擎完全解耦。数据科学家做实验时再也不用为“申请一个带数据的Spark集群”等待半天了。从技术演进角度看下一代湖仓架构比如Iceberg、Hudi、Paimon这类数据湖表格式天生就更适合构建在存算分离的存储之上。它们通过元数据层管理数据的版本和变更对存储底层的依赖降到了极低的程度。可以说存算分离是从传统数仓到现代湖仓升级过程中的必备条件。根据我自己的体会做存算分离的查询性能优化最大的转折点是心态上的不要再把每一台机器当成“数据的主人”它更像是一个“随时可以进退场的计算员工”。数据永远安全地待在存储的机房里计算集群需要数据的那一刻才去取。这个心态转变之后你会发现自己做资源规划、做性能优化、做故障处理时思路都会开阔很多。
返回列表