
这两年聊存算分离的人突然多了起来技术社区、云厂商发布会、数仓选型讨论里几乎都能碰到这个词。但说实话很多人把它当成一个新概念在追实际上它是被成本和弹性逼出来的一条必经之路。我自己从最早的Hadoop时代一路做过来经历过磁盘和CPU绑死的物理集群也做过云上Spark完全对接对象存储的改造中间踩过的坑、填过的洞都不少。这篇就把我对大数据存算分离的理解、落地方案、性能调优和一些经验教训一次讲清楚给正准备做架构改造或正在犹豫要不要上存算分离的团队一点参考。存算分离说白了就是把原来一台机器上既当计算又当存储的耦合关系拆开。计算资源和存储资源各自独立管理、独立扩缩、独立计费。这个思路不是什么天马行空的新发明而是当数据量涨到一定规模、成本压力大到藏不住的时候自然而然会出现的选择。下面我从头把这个事情的来龙去脉捋一遍。1. 存算分离到底在解什么题一个老兵的视角1.1 传统存算一体架构的三重困境过去十年大家搭大数据平台基本上都是同一个套路买一批物理机每台机器上有CPU、内存也有本地磁盘数据通过HDFS切成块放在这些机器的本地盘上。跑作业的时候调度器会把任务尽量发到数据所在的机器去执行这就是所谓的“数据本地性”。这种设计在磁盘便宜、数据量可控的年代确实好使因为它最大程度回避了网络带宽这个瓶颈。但等到业务数据涨到几百TB甚至PB级问题就一个接一个冒出来了。第一重困境是资源错配。计算和存储是1比1绑定的你要扩存储就必须连着计算一起扩。可实际上一个集群日常跑批的CPU利用率能到30%就算不错了大量计算资源就是在那儿闲着待命但磁盘又快满了你不得不再买一堆机器只为那点存储空间。这笔账怎么算都不划算。第二重困境是弹性能力为零。大促期间或者月底报表集中生成计算负载可能飙到平时的四五倍。存算一体架构下你没有别的办法只能按这个峰值去备机器。可是一年365天里峰值可能就出现几天剩下三百多天这些多出来的硬件全部是沉没成本。第三重困境是数据共享困难。每个集群的数据跟着机器走多个引擎都想用同一份数据时要么复制多份要么来回抽数。搞过数据仓库的人都知道这种烟囱式的架构维护成本有多高数据口径还经常对不上。1.2 存算分离的本质把绑定的资源解耦理解了上面的困境存算分离要解什么题就很清晰了。核心就一件事让计算和存储各自回到自己最擅长、最经济的位置上去。计算集群专心做CPU密集型的任务需要多大就扩多大跑完就缩存储则交给对象存储或者独立的分布式文件系统按容量计费数据可以长期低成本存放。计算和存储之间通过高速网络连接而不是靠那台机器自带的硬盘。我刚接触这个思路时也怀疑过网络IO的延迟会不会让查询慢到不可接受。但实际做下来你会发现很多场景里慢一点点的冷查询换来的是总体成本下降五六成、弹性能力从无到有这笔交换非常划算。而且近几年网络硬件和协议栈的进步已经把存储访问的延迟压缩到了非常可接受的水平。所以我在评估一个团队适不适合上存算分离时通常先问三个问题你的数据量是不是还在高速增长你的计算负载有没有明显的波峰波谷你有没有多个引擎要共享同一份数据的诉求如果三个里面中两个那这条路基本可以确定要走。2. 三条主流实现路线各自的门道都在哪里2.1 路线AHDFS体系内部改造的“轻量解耦”第一条路线最保守尽量不动原有计算栈只是把存储节点和计算节点拆成不同的机器组。计算节点通过网络挂载存储节点的HDFS服务任务调度时不再强制要求数据本地。好处是迁移成本极低原有Spark、Hive SQL基本不用改只要在Yarn和HDFS的调度策略上做一些调整把数据本地性关掉或者降级。原来跑在物理集群上的作业几乎可以无缝平移过来。但这条路我是持保留意见的。因为HDFS架构里还有个NameNode负责全局元数据管理高并发下它本身就容易成为单点瓶颈。你做了计算和存储分离元数据查询链路还是原来那套等于说最值钱的那层没有变。数据量继续涨性能天花板很快又压回来。所以这条路线更适合那些暂时不能上云、也扛不住大改造的传统企业作为过渡方案可以作为长期架构我不太建议。2.2 路线B对象存储作为主存储这是我落地最多的方案第二条路是目前业界最主流、也是我倾注精力最多的一条把数据放进对象存储比如阿里云OSS、AWS S3、腾讯云COS、华为OBS这些计算引擎通过标准的Hadoop兼容文件系统接口直接读写。Spark、Presto/Trino、Hive、Flink这些引擎基本都内置了s3a、oss等协议的支持。你写代码几乎不用改把路径前缀从hdfs://换成oss://或者s3a://再加上依赖和认证配置作业就能跑。这条路真正的亮点在于对象存储本身的特性容量近乎无限按存储量计费不需要预置硬件数据跨引擎共享也很容易同一份数据Spark能读Presto能读Flink也能读不用各自维护副本。代价自然也存在。对象存储的读延迟比本地磁盘高一个量级小文件场景尤其尴尬。所以配套的缓存加速层基本是标配不能裸奔。这个我后面专门写一节来讲。2.3 路线C云原生数仓把存算分离变成产品能力第三条路线更省心——直接用云厂商已经做好的产品。Snowflake这类产品从诞生那天起就是存储与计算分离的架构数据放在独立的存储服务上用户使用计算仓库按需开启用完就停计费也完全分开。国内很多团队用的云数仓产品也都是这个路子。优势可以说无脑好用不需要自己搭集群不需要调参数弹性伸缩和计费模型都极其清晰。尤其适合那些团队里缺少资深大数据运维、又想快速获得分析能力的业务部门。但它也有绕不开的局限。一是供应商锁定问题一旦数据量大了想迁走成本非常高。二是个性化调优受限SQL特别复杂或者有特殊的资源隔离需求时产品化的服务往往不能满足你。三是计费看起来便宜实际跑高频查询的时候计算资源的账单可能会让你重新审视性价比。2.4 学我这样选先看团队底子再看业务规模路线选型这事我很少直接给死答案因为我见过太多“别人家的成功案例”在各团队里水土不服。我自己会用一个简单的判断模型。如果你们有完整且成熟的Hadoop运维团队代码存量很大改造窗口又紧先去路线A过渡是理性的如果你们正在上云或者已经在云上数据量和成本敏感度都比较高直接奔着路线B去如果团队只想专注业务分析完全不想碰集群那就安心用路线C。记住一个原则不要为了追概念而做存算分离。如果你的数据量几百GB、每天跑几个批三十分钟能出结果那你压根不需要这个复杂度。技术选型要跟着问题走不是为了好看。3. 实操全记录Spark 对象存储怎么一步步落地3.1 动手前把这几件事先敲定很多团队一上来就想大刀阔斧改配置我建议先冷静下来把迁移前的准备工作做足。我自己的清单是这么列的梳理数据清单哪些表最占空间、哪些表查询频次高、哪些表是冷数据分类列出来。冷热不同迁移策略完全不一样。确认对象存储的Bucket所在地域以及计算集群所在的可用区。这条看起来基础但很多人就是在这上面栽跟头——计算集群在华东存储桶开了华南跨地域访问的延迟和流量费用直接让你怀疑人生。准备存储桶的访问密钥权限范围越小越好只开通目标Bucket和目标的读写路径。确定迁移策略全量迁移还是分区增量迁移建议第一轮先迁移冷数据和超大体积的表这样收益最快踩雷范围也小。最终明确计算引擎的版本组合。这里我强烈建议Spark和Hive的版本统一避免出现不同引擎对同一种数据格式兼容性不一致的问题。3.2 核心配置参数以及每个参数背后的原因用Spark读写OSS最基础的配置我放在下面。注意这些配置换成AWS S3时原理一样只是字段名不同。spark.hadoop.fs.oss.accessKeyIdyour_access_key spark.hadoop.fs.oss.accessKeySecretyour_secret_key spark.hadoop.fs.oss.endpointoss-cn-hangzhou.aliyuncs.com spark.hadoop.mapreduce.fileoutputcommitter.algorithm.version2 spark.sql.sources.partitionOverwriteModedynamicaccessKeyId和accessKeySecret是认证用的不用多说。endpoint必须和Bucket的region一致否则网络绕路严重。fileoutputcommitter.algorithm.version2可能是大多数人忽略的它把MapReduce作业提交逻辑改成先写临时目录再原子性提交避免在对象存储这种不支持文件原地rename的系统中出现半成品文件。这个配置不设好你会遇到数据写到一半、然后作业失败后留下大量脏文件的诡异现象。partitionOverwriteModedynamic是一个安全性配置。它确保你只覆盖需要更新的动态分区而不是整个表。存算分离架构下对象存储的读写是按量计费误操作整表覆盖的代价会非常难看。3.3 存储目录设计决定查询性能的一半对象存储最怕小文件因为它内部是按“对象”管理数据的一个小文件也是一个对象读请求要一个个发。文件数量过多时请求并发和数据拉取都会成为瓶颈。所以目录设计和文件布局必须刻意优化。我自己习惯在写入的时候重分区到合理的并行度然后让每个文件大小尽量接近64MB到128MB。也就是说如果一个分区有1GB数据那就把它塞进8到16个文件里不要让它生成几百个小碎文件。写入代码大致是这个思路INSERT OVERWRITE TABLE target_table PARTITION(dt) SELECT col1, col2, dt FROM source_table DISTRIBUTE BY dt SORT BY col1;DISTRIBUTE BY dt让同一分区的数据进同一个Reducer这样每个分区只会生成少量文件而不是每个Map任务都产出碎片文件。目录结构最好做成标准Hive风格表名/分区字段分区值/数据文件。这种布局对Hive Metastore、Spark、Presto都友好也方便后续做增量更新和分区裁剪。3.4 数据迁移三步走读、写、验迁移工作我强烈建议用Spark批任务或者distcp来做千万不要写shell脚本一行一行地去拷贝文件那样既慢又不安全。我自己设计过一套迁移任务大致逻辑如下先从HDFS读取需要迁移的表按分区字段重分区以Parquet格式写入OSS指定路径然后做三个层面的校验。校验这步太关键了。光看任务跑成功远远不够因为对象存储数据写入成功后如果元数据没注册成功查询照样找不到。我的校验方法是第一层对比行数用Spark分别对源表和目标表执行Count看是否一致第二层抽样对比字段明细选几个核心分区随机抽几条记录比对字段内容第三层对比文件总字节数和文件数量确认没有多写少写。三层都过了才敢在源端做清理动作。3.5 缓存加速层让存算分离不再“慢热”对象存储裸读的延迟确实比本地盘要高这是物理层面的客观事实。所以实践中我几乎都会配一层缓存让热数据靠近计算节点。目前缓存这件事有几种做法。第一种是Alluxio这种分布式缓存系统它在计算集群和对象存储之间架一层把常访问的数据块缓存到计算节点的本地磁盘上。第二种是直接用引擎自带的缓存能力比如Trino/Presto的本地缓存插件实现“缓存到计算节点的本地目录”。第三种就简单粗暴一些在Spark里用DataFrame Cache把复用率高的中间结果物化到内存但这对内存压力比较大。我先说说我更常用的方案。当时做迁移我先在计算节点上挂了本地高速盘然后部署了Alluxio让其作为计算集群的统一数据访问入口底层数据源指向OSS。第一次冷查询时速度稍慢跑完后数据块被缓存到本地后续重复查询同一分区的速度基本可以和HDFS持平。我记录过一组对比数据同样跨最近30天的聚合查询HDFS下大约8分钟OSS冷跑是12分钟OSS 缓存热跑则稳定在7分钟左右甚至略好于原来。这说明合理配置缓存层的存算分离架构完全可以在保持低成本的前提下达到与存算一体差不多的查询体验。关键点在于缓存分区的命中率设计和缓存淘汰策略如果缓存机本身不淘汰旧数据、不控制容量反而是新增的运维负担。4. 从当前瓶颈反推存算分离的四个未来趋势4.1 元数据层成为新架构的“承重墙”数据放到对象存储之后谁来告诉计算引擎“这个目录有哪些文件、每个文件多大、字段类型是什么”答案就是元数据服务。现在的Hive Metastore、Iceberg Catalog、Hudi Catalog本质上都是这套承重墙。未来的趋势一定是元数据从各个引擎里彻底独立出来成为数据平台中的一等公民。不管是批处理、即席查询还是实时流式任务大家访问同一份元数据定义统一、权限统一、版本统一。这样可以省掉大量原来“各写各的元数据、各改各的schema”的麻烦。我自己现在做新项目已经习惯把Iceberg或Hudi作为表格目录层来管理。它们自带ACID、时间旅行等功能配合对象存储能做到存算分离下数据强一致这是传统Hive表不具备的。4.2 缓存与加速层走向智能化和产品化存算分离被质疑最多的问题永远是“慢”。所以未来几年围绕缓存和加速层的创新会非常密集。我判断会向两个方向发展。一是本地缓存与远端存储之间的自动预热和自动驱逐不再靠人工配置哪个分区要缓存而是由系统根据查询频率自动判断。二是文件切分的粒度更细缓存只缓存真正被查询访问的热块而不是整张大表。目前Alluxio、JuiceFS这类开源项目已经在往这个方向走。云厂商也在推出相应的数据加速服务把缓存直接做成平台能力。我给团队的忠告是以后选型不仅要关注内核性能和SQL能力还要重点评估它和对象存储配合时的缓存效率。4.3 计算层真正走向Serverless化存算分离让计算资源可以独立扩缩但是扩大和缩小还需要人来判断、人来执行。真正的未来是把这个过程完全自动化。Spark虽然有动态Executor分配但粒度还是偏粗。未来的调度系统应该能根据当前的队列积压、任务预估运行时间、资源池水位自主决定把计算集群扩容到多少实例或者缩容到多少实例甚至能细化到单个SQL的资源开销。这一步落地之后用户写SQL就不用关心背后有多少台机器了这才是真正意义上Serverless的形态。云厂商的一些查询服务其实已经在用这种模式。大数据运维人员未来的角色会从“每天盯着集群发愁”转变为“写策略、做治理、管质量”。4.4 实时计算链路也加入存算分离大潮提到存算分离大家首先想到的是离线分析因为批处理对延迟容忍度高容易做解耦。但实时计算也在悄悄向这个方向演进。典型场景就是Flink的状态存储。传统上状态放在本地RocksDB里一旦TaskManager重启恢复很痛苦。如果状态存储能设计成与计算分离的形态故障恢复和扩容缩容都会简单得多。另一个场景是Kafka消息数据的长期存储现在也有人尝试把冷数据段转入对象存储用分层存储的方式降低消息集群的成本。如果实时链路也实现了彻底的存算分离那么整套数据架构就会趋于同一形态统一存储、多引擎读取、按需弹性。这个趋势我认为未来五年内会非常明显。5. 高频踩坑实录五个坑和对应的排查思路5.1 小文件问题在对象存储下会被放大前面反复提到小文件问题这里用实例说明。我接手过一个团队迁移时直接用源Hive表的数据文件拷到OSS没做重分区合并结果一个分区下有几千个小文件。跑查询时Spark得发起上万次对象存储读请求直接吃满了请求配额任务卡在读取阶段。排查思路很简单用SQL数一下每个分区的文件数和文件大小然后写一个Compact作业按分区重新写出合理大小的文件。这次问题之后我把“文件数量监控”加到了日常巡检脚本里一旦发现分区文件数超标就预警。5.2 认证配置漂移多引擎权限不一致曾经遇到过很诡异的现象Spark作业能正常读写OSS但Hive SQL一跑就报Permission Denied。查了很久最后发现是Spark和Hive各自读取认证配置的路径不同Spark用的是新配置Hive还在用老的hdfs-site.xml里的设置。解决办法是建立一套统一的认证配置源所有引擎都从这里获取凭证和区域信息或者至少在多个配置文件中严格保持同步。这个坑特别隐蔽因为你单测每个引擎都是通的一旦开始跨引擎协作就出问题。5.3 一慢就甩锅给对象存储先看SQL和统计信息存算分离架构上线后团队里最常见的吐槽就是“查询慢了肯定是存算分离害的”。但很多次排查后都发现真正的元凶是SQL执行计划不好或者表的统计信息过期导致选择了错误的Join策略。我总结出一套排查顺序先看执行计划里有没有倾斜、有没有Broadcast失效再刷新统计信息最后才检查存储层的IO消耗和缓存命中率。把这三步走完基本上能分清楚责任到底在哪一层。糊里糊涂调了半天缓存查出来是SQL写得不好那就太浪费时间了。5.4 动态分区覆盖误伤小心整表被清空这个坑我只遇到过一次但吓得够呛。配置没有配好partitionOverwriteMode时一个只更新某一天分区的任务把整个表的其他分区全部覆盖成空。在对象存储上做这种操作恢复数据的成本极高。之后我的铁律是第一线上一律禁止不带分区条件跑Insert Overwrite第二严格配置动态分区覆盖模式第三写任务前先跑一个“Explain预览”确认要覆盖的分区范围。5.5 跨网络区域的部署贵到你怀疑人生有人为了省一点存储费把对象存储桶开在离计算集群很远的区域。第一次全量查询直接就崩了不仅延迟爆炸流量费用还高得吓人。这块我的建议是计算集群和存储必须同Region而且尽量同一个可用区。如果业务要求跨地域容灾可以配置跨区域复制但读取任务的主链路必须走同区域。这一点看起来基础实际上是最容易在架构初期被忽视、后期最难改的问题。6. 踩过许多坑之后我对存算分离的重新理解6.1 这件事到底适合谁不适合谁我不太喜欢把存算分离描述成“未来所有团队的唯一解”。它适合的情况我已经在前面说得比较清楚了数据量大、负载有波峰波谷、多引擎需要共享数据、成本压力大。如果你的数据量还很小跑批也就几分钟那确实没必要用这个复杂度去换那点成本。但反过来不要让“觉得麻烦”挡住必要性的判断。存算分离确实会增加网络调优、缓存管理、元数据治理的复杂度但你省下来的是实实在在的硬件预算和运维小时数。做架构的人要把这笔账放到五年的时间跨度里去算。6.2 落地心得存储先行计算随需如果只能总结一句话我会说“存储先行计算随需”。先把数据安置在统一、便宜、可靠的地方让计算资源成为可按需接入的能力这比纠结于“用哪家引擎”“用哪种SQL方言”重要得多。存算分离的样子一直在变但底层这个思维模式我认为未来很长时间内都不会过时。这一路走下来我最深的体会是技术演进的每一小步背后都是因为旧架构里有些东西实在撑不住了。存算分离不是谁拍脑袋想出来的名词它是业界面对数据规模和成本压力时自然长出来的答案。你要做的不是急着追新而是判断它能不能接住你眼下的痛。能就稳扎稳打地干下去。