这两年聊存算分离的人突然多了起来,技术社区、云厂商发布会、数仓选型讨论里,几乎都能碰到这个词。但说实话,很多人把它当成一个新概念在追,实际上它是被成本和弹性逼出来的一条必经之路。我自己从最早的Hadoop时代一路做过来,经历过磁盘和CPU绑死的物理集群,也做过云上Spark完全对接对象存储的改造,中间踩过的坑、填过的洞都不少。这篇就把我对大数据存算分离的理解、落地方案、性能调优和一些经验教训一次讲清楚,给正准备做架构改造或正在犹豫要不要上存算分离的团队一点参考。
存算分离,说白了就是把原来一台机器上既当计算又当存储的耦合关系拆开。计算资源和存储资源各自独立管理、独立扩缩、独立计费。这个思路不是什么天马行空的新发明,而是当数据量涨到一定规模、成本压力大到藏不住的时候,自然而然会出现的选择。下面我从头把这个事情的来龙去脉捋一遍。
1. 存算分离到底在解什么题?一个老兵的视角
1.1 传统存算一体架构的三重困境
过去十年大家搭大数据平台,基本上都是同一个套路:买一批物理机,每台机器上有CPU、内存,也有本地磁盘,数据通过HDFS切成块放在这些机器的本地盘上。跑作业的时候,调度器会把任务尽量发到数据所在的机器去执行,这就是所谓的“数据本地性”。
这种设计在磁盘便宜、数据量可控的年代确实好使,因为它最大程度回避了网络带宽这个瓶颈。但等到业务数据涨到几百TB甚至PB级,问题就一个接一个冒出来了。
第一重困境是资源错配。计算和存储是1比1绑定的,你要扩存储就必须连着计算一起扩。可实际上,一个集群日常跑批的CPU利用率能到30%就算不错了,大量计算资源就是在那儿闲着待命,但磁盘又快满了,你不得不再买一堆机器只为那点存储空间。这笔账怎么算都不划算。
第二重困境是弹性能力为零。大促期间或者月底报表集中生成,计算负载可能飙到平时的四五倍。存算一体架构下,你没有别的办法,只能按这个峰值去备机器。可是一年365天里,峰值可能就出现几天,剩下三百多天,这些多出来的硬件全部是沉没成本。
第三重困境是数据共享困难。每个集群的数据跟着机器走,多个引擎都想用同一份数据时,要么复制多份,要么来回抽数。搞过数据仓库的人都知道,这种烟囱式的架构维护成本有多高,数据口径还经常对不上。
1.2 存算分离的本质:把绑定的资源解耦
理解了上面的困境,存算分离要解什么题就很清晰了。核心就一件事:让计算和存储各自回到自己最擅长、最经济的位置上去。
计算集群专心做CPU密集型的任务,需要多大就扩多大,跑完就缩;存储则交给对象存储或者独立的分布式文件系统,按容量计费,数据可以长期低成本存放。计算和存储之间通过高速网络连接,而不是靠那台机器自带的硬盘。
我刚接触这个思路时,也怀疑过网络IO的延迟会不会让查询慢到不可接受。但实际做下来,你会发现很多场景里,慢一点点的冷查询换来的是总体成本下降五六成、弹性能力从无到有,这笔交换非常划算。而且近几年网络硬件和协议栈的进步,已经把存储访问的延迟压缩到了非常可接受的水平。
所以我在评估一个团队适不适合上存算分离时,通常先问三个问题:你的数据量是不是还在高速增长?你的计算负载有没有明显的波峰波谷?你有没有多个引擎要共享同一份数据的诉求?如果三个里面中两个,那这条路基本可以确定要走。
2. 三条主流实现路线,各自的门道都在哪里
2.1 路线A:HDFS体系内部改造的“轻量解耦”
第一条路线最保守,尽量不动原有计算栈,只是把存储节点和计算节点拆成不同的机器组。计算节点通过网络挂载存储节点的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.accessKeyId=your_access_key spark.hadoop.fs.oss.accessKeySecret=your_secret_key spark.hadoop.fs.oss.endpoint=oss-cn-hangzhou.aliyuncs.com spark.hadoop.mapreduce.fileoutputcommitter.algorithm.version=2 spark.sql.sources.partitionOverwriteMode=dynamicaccessKeyId和accessKeySecret是认证用的,不用多说。endpoint必须和Bucket的region一致,否则网络绕路严重。fileoutputcommitter.algorithm.version=2可能是大多数人忽略的:它把MapReduce作业提交逻辑改成先写临时目录再原子性提交,避免在对象存储这种不支持文件原地rename的系统中出现半成品文件。这个配置不设好,你会遇到数据写到一半、然后作业失败后留下大量脏文件的诡异现象。
partitionOverwriteMode=dynamic是一个安全性配置。它确保你只覆盖需要更新的动态分区,而不是整个表。存算分离架构下,对象存储的读写是按量计费,误操作整表覆盖的代价会非常难看。
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方言”重要得多。存算分离的样子一直在变,但底层这个思维模式,我认为未来很长时间内都不会过时。
这一路走下来,我最深的体会是:技术演进的每一小步,背后都是因为旧架构里有些东西实在撑不住了。存算分离不是谁拍脑袋想出来的名词,它是业界面对数据规模和成本压力时,自然长出来的答案。你要做的不是急着追新,而是判断它能不能接住你眼下的痛。能,就稳扎稳打地干下去。