做数据仓库的这些年,我有个比较深的感受:规划容量的时候,永远觉得够用;真正跑起来,才知道什么叫压力。数据仓库这类系统,过去一直被认为是“重资产”,买一批机器、搭一套集群,然后按峰值预估容量,之后相当长一段时间都不动它。但进入大数据环境之后,这个逻辑开始站不住脚了:业务数据不是线性增长,而是动不动翻倍;流量高峰也不是一年几次,而是隔三差五就来一趟;再加上实时数仓、即席查询、数据科学实验都在同一个底层存储上抢资源,“固定容量”的假设几乎失效。于是,数据仓库的弹性扩展从一个加分项,慢慢变成了很多团队的刚需。
这篇文章不打算讲太虚的架构理念,而是把我自己在生产环境里验证过的一整套弹性扩展方案拆开来讲:为什么要弹性、架构上怎么选型、落地上怎么实现,以及扩容缩容过程中那些你一定会踩到的坑。如果你正在做大数据平台建设、数据仓库开发,或者是刚接触数仓的工程师,这套思路可以直接拿去对标自己的集群。
1. 先想清楚:你的数据仓库为什么要弹性扩展
1.1 大数据环境让“固定容量”变成了伪命题
传统数据仓库的设计前提,是数据规模相对可控。一个企业跑个两三年,数据量大概能估出来,采购硬件的时候按五年规划估一轮,基本就稳了。但到了大数据阶段,这个前提就崩了。我见过很多业务,第一天建数仓的时候每天增量才几百GB,半年之后因为新增埋点、日志采集、外部数据接入,日增量直接冲到好几TB。还有那种促销型业务,平时一天几千万条记录,活动当天数据量是平时的八到十倍。如果你按照峰值建集群,平时就是巨大的浪费;按照均值建,峰值一到就全链路瘫痪。更别提现在还要接实时数据、流式写入、机器学习特征工程,数仓里流转的不只是结构化报表数据,还有半结构化的日志、非结构化的画像数据。
举几个具体场景你就理解了。网约车平台每天产生大量订单、轨迹、司机行为数据,晚高峰的实时数据量是凌晨的几十倍,数据仓库既要支撑离线分析,又要给实时大屏和派单模型提供数据;校园大数据平台平时只有日常学习、图书馆、门禁数据,但到了选课、考试、迎新这类节点,数据访问量和计算任务会突然翻几番;电商平台更不用说,大促期间的页面埋点日志、订单流、库存变更流同时暴涨,ETL任务和报表任务全挤在同一段时间内执行。这些场景都指向同一个结论:固定容量模型面对大数据环境,没有任何优势。所以我的第一个观点很直接:大数据环境里的数据仓库,弹性扩展不是可选项,而是必须提前设计的防御性能力。
1.2 业务高峰从来不是意外,而是常态
很多团队是在一次大促或者活动之后,才开始认真考虑弹性扩展的。我们之前也经历过那个场景:业务部门临时提了一个全量数据重算的需求,要把最近三个月的用户行为数据重新加工一遍,而且要求在第二天早上八点前出结果。按当时集群的固定容量,这个任务至少要跑14个小时,但当天晚上还有正常的日报任务在跑,资源一抢起来,全集群的任务都在排队,最后活生生跑到第二天中午。
这背后其实是数据仓库负载的典型潮汐效应。白天即席查询多,报表用户点一点就要出数;凌晨离线批处理多,ETL任务、数仓分层加工任务都在这个窗口跑;月底报表任务扎堆,财务、运营、管理层都在等数据;大促前后全链路都在做重跑和补数。你不可能让集群永远处于峰值状态,但也不能在用户需要的时候拿不出资源。弹性扩展要解决的,就是这种“削峰填谷”的资源调度问题。这不是调度系统的问题,也不是哪段SQL写得烂,本质上是计算资源的供给跟不上业务负载的波动。一个健康的数仓,必须能在高峰时段迅速补充计算资源,在低谷时段把资源安全释放掉。
1.3 弹性扩展的本质:让资源和负载匹配
很多人一听到弹性扩展,脑子里想到的第一件事就是加机器。这个理解对了一半。弹性扩展并不等于无限扩容,它真正要做的是:存储容量按需增长,计算资源动态伸缩,缩容时能安全释放,资源配额能和业务优先级挂钩。换句话说,弹性不只是往大了扩,还包括往小了收。一个集群如果只会在业务压力上来的时候加节点,但低峰期缩不回去,那叫有扩展,不叫有弹性。真正的弹性,是一套能感知负载、自动调整、平滑切换的资源治理机制。
我通常会把弹性拆成四个维度看:存储弹性、计算弹性、元数据弹性和成本弹性。存储弹性说的是容量能随数据增长平滑扩容,不会因为磁盘写满而停止服务;计算弹性说的是任务队列能根据负载动态调整执行资源;元数据弹性说的是当节点和分区数量暴增时,NameNode、Hive Metastore、表格式的元数据服务依然能扛住压力;成本弹性说的是资源可以按周期归还,不为低峰期的闲置机器付账单。搞清楚这几个维度之后你再往下看,架构选型和方案设计就会少走很多弯路。
2. 弹性扩展的架构选型:四条路线如何取舍
2.1 垂直扩展:简单但上限明显
垂直扩展,也就是Scale-Up,给现有节点加CPU、加内存、换更大的硬盘。这种方式最省事,不需要改架构,不需要数据迁移,机器装完重启服务,容量就上去了。我见过不少中小团队,初期为了省事,直接把数据节点的内存从64GB升到256GB,CPU翻倍,靠单机性能硬扛了一段时间,效果确实立竿见影。
但它的限制也很清楚:单个节点能买到的计算密度和存储容量是有天花板的。一台机器装到一两百TB硬盘、上TB内存之后,再想往上加就很困难,成本也会急剧上升。而且很多分布式引擎的瓶颈恰恰不在单机性能,而在集群调度、网络IO和元数据管理。你就算把管理节点升级到六十四核、上TB内存,数据节点不够,任务该排队还是排队。所以垂直扩展通常只适合小规模集群做应急处理,或者对NameNode、ResourceManager这类管理节点做加强。在大数据环境整体容量不足的时候,靠给一台台机器换配件撑不了太久,它不是弹性扩展的主流方案。
2.2 水平扩展:分而治之,但注意数据重分布
水平扩展,也就是Scale-Out,是目前绝大多数大数据平台的选择。Hadoop生态的HDFS、Hive、Spark,MPP类型的ClickHouse、Doris、Greenplum,本质上都建立在更多节点参与计算和存储这个模型之上。新节点加入集群之后,新数据会尽量写入新节点,新的计算任务也能分配到更多资源,整体容量和处理能力跟着线性往上走。扩容的方式也很成熟:HDFS加DataNode、YARN加NodeManager、Spark加Worker,重启或动态刷新配置就能纳入集群。
但水平扩展也藏着一个很多人会忽略的问题:扩容之后,历史数据不会自动均匀分布到新节点上。比如HDFS虽然是条带化的,但块分布有随机性,扩容一段时间后往往会出现老节点磁盘使用率远高于新节点的情况。计算任务是快起来了,但读取历史数据时还是集中在一部分老节点上,IO和网络依然可能成为瓶颈。MPP数据库更麻烦,一张大表的分片位置在扩容后基本不会自动重新分布,如果不做数据重分布,新节点只承担新写入的数据,老节点永远在超负荷运转。所以水平扩展不只是一个加节点的动作,往往要跟着做数据均衡和分区重分布,这个后面第4章我会专门讲。
2.3 存算分离:让计算和存储各自弹性
早期大数据架构里,计算节点和存储节点是绑在一起的。扩容的计算节点自带存储,数据写在本地硬盘上,数据本地性很好。但这也带来一个问题:计算有高峰,存储有高峰,两者不一定同步。比如说,某个集群最近要跑全量重算,CPU爆了,但磁盘容量还有大量空闲;另一个集群则相反,数据天天涨,磁盘快满了,但CPU平时很闲。绑在一起的架构很难处理这种错峰需求。
存算分离的思路,是把存储底座独立出来。计算集群只管算,数据统一放到对象存储或者独立的分布式文件系统上。这样带来的弹性非常大:存储不足就扩存储,计算压力大就拉起更多计算引擎实例,两者互不拖累。而且计算集群很多时候是可以随时销毁重建的,弹性扩缩容的响应速度比传统方案快得多。云上的对象存储几乎是无限容量,配上Spark或Presto引擎,临时拉一个百节点的计算集群只需要几分钟。代价是网络开销。计算节点读数据要跨网络,数据本地性没了,这对一些重IO的查询影响比较明显。解决思路通常是利用分布式缓存、列存格式压碎IO量,或者只在特定场景,比如批量ETL和即席查询,启用存算分离的资源组,没必要把整个集群都改造成存算分离架构。
2.4 湖仓一体:为弹性加上事务和数据管理能力
纯对象存储加上Spark,虽然弹性很好,但离数据仓库还差一步,数据仓库需要事务、一致性、分区管理、增量更新这些能力。这也是湖仓一体近几年成为热点的主要原因。以Iceberg、Hudi、Delta Lake为代表的表格式,本质上是在数据湖存储之上加了一层仓库级的元数据和事务层。它们让数仓表有了ACID能力,支持增量读取、分区演进、时间旅行,同时保留了存储层廉价的弹性。数据文件仍然放在对象存储或HDFS上,但表和分区是虚拟的,扩容时不需要迁移数据,只要新增节点、让计算引擎识别更多候选文件就行。
湖仓一体还解决了弹性扩展里的一个很现实的问题:小文件治理。写一次数据产生一堆小文件,扩多少容都会被元数据瓶颈拖垮;借助这些表格式的Compaction和自动优化能力,可以持续把文件合并到一个相对合理的粒度。现在很多团队把离线数仓、实时数仓、数据湖的技术栈融合在一起,底子就是这套湖仓一体的表格式抽象。四类方案怎么选,我个人的判断标准是这样的:如果团队规模小、业务明确,MPP引擎配合水平扩展就能解决问题;如果业务负载波动大、需要频繁弹性伸缩,存算分离加湖仓一体是长期更稳健的方向;如果只是短期容量危机,垂直扩展先顶上也不是不行,但一定要知道它的天花板。
| 方案 | 弹性能力 | 成本 | 运维复杂度 | 适合场景 |
|---|---|---|---|---|
| 垂直扩展 | 低,单机上限明显 | 中高 | 低 | 小规模集群、管理节点加固 |
| 水平扩展 | 中高,需配合数据均衡 | 中 | 中 | Hadoop/MPP传统数仓扩容 |
| 存算分离 | 高,计算存储独立伸缩 | 中 | 偏高 | 云原生、负载波动大的平台 |
| 湖仓一体 | 高,兼得事务与弹性 | 中 | 高 | 离线实时融合、数据湖治理 |
3. 一套可落地的弹性扩展方案:三层弹性设计
3.1 总体思路:存储、计算、调度分开弹性
完整的信息技术方案落地时,我习惯把弹性扩展拆成三个层面:存储层弹性、计算层弹性、调度层弹性。为什么不搞成一个统一的按需加水方案?因为负载模型不一样。存储层和数据生命周期相关,增长趋势是缓慢但持续的,弹性指标是容量水位;计算层和业务波峰相关,弹性指标是CPU、内存、任务等待时长;调度层负责把计算资源准确分配到不同业务队列,弹性指标是队列的配额使用率。三者如果不分开,就会出现一种尴尬局面:计算资源倒是自动加了,但加的机器没挂到正确的队列,业务任务依旧抢不到资源。
三层的依赖关系也要理清楚。存储层是底座,容量不足会拖垮所有计算;调度层是枢纽,负责把二层资源按优先级交给不同业务;计算层是最终执行者,决定一个任务到底能拿多少并发。我们做弹性方案时,优先保障存储层的安全水位,然后调整调度层的队列配额,最后才考虑动态伸缩计算节点。这个顺序反过来,很容易出现计算资源缩了又扩、扩了又缩的震荡。
3.2 存储层弹性:容量评估与冷热分层
先做一轮容量评估。公式不复杂,核心是:每日新增数据量乘以数据保留周期,乘以副本系数,再乘以1加上冗余率。举个例子。一条在线业务线平均每天新增日志和业务数据2TB,数仓要保留90天用于回溯分析,HDFS默认3副本,再预留20%的空间给临时文件、MapReduce中间结果和系统自身开销。算下来就是2TB乘以90天乘以3副本乘以1.2,约648TB。如果当前集群总容量只有400TB,那存储层就已经是高风险状态了。
容量不紧张不代表可以不做弹性,因为不紧张只是短暂的。更稳妥的做法是给存储分热、温、冷三个层级。热层放最近7到30天的数据,走HDFS或者纯SSD存储,查询频次最高,不压缩或轻压缩;温层放30到90天的数据,保留在HDFS,但用Parquet或ORC列式格式加Zstd压缩,容量占用能下降60%以上;冷层放超过90天的数据,定期归档到对象存储,行式数据全部转列式,同时下架临时副本,只保留最终副本或依赖对象存储的冗余机制。这样做的逻辑很简单:弹性不是为了让所有历史数据都变成热数据,而是让真正常用的数据保持在最快路径上,让不常用的数据尽量少占资源。很多数仓扩容是被历史数据堆积逼出来的,冷热分层之后,你会发现存储扩容的需求其实没那么夸张。对象存储一般自带生命周期规则,可以设定一个策略,把超过N天的文件自动转入低频存储或归档存储,不需要运维每天晚上手动搬数据。
3.3 计算层弹性:动态资源分配是基础
计算层的弹性,我建议优先从动态分配开始,而不是一上来就动集群节点数。大数据生态里最常见的动态资源机制是Spark的Dynamic Allocation。开这个功能之后,Spark会根据Stage实际需要动态申请Executor,任务少了自动释放,任务多了自动补上。配置项可以参考下面这套:
spark.dynamicAllocation.enabled true spark.dynamicAllocation.shuffleTracking.enabled true spark.dynamicAllocation.minExecutors 10 spark.dynamicAllocation.maxExecutors 200 spark.dynamicAllocation.initialExecutors 20 spark.dynamicAllocation.executorAllocationRatio 0.5这里有个心法要说一下:不要把minExecutors设得太高。很多团队为了减少调度开销把最小值设成了50甚至100,结果低峰期资源也占着不放,本质上等于放弃了弹性。最小值应该只满足延迟敏感的核心任务,最大值给到业务峰值所需,中间的差值才是弹性空间。如果计算集群跑在Kubernetes上,弹性就更好做了。可以给Spark、Flink的Pod设置独立的HPA策略,按CPU使用率、内存使用率、任务积压量三个指标联动伸缩。注意,不要只看CPU。数仓任务里有很多IO密集型的Shuffle和读写,CPU可能不高但任务在卡IO,按CPU伸缩常常误判。最好把积压的Spark Task数量或者等待调度的Application数量作为一个关键信号。
Flink实时任务的计算弹性不太一样,不能随便把某个TaskManager从正在运行的作业中抽走,否则状态一致性和Checkpoint机制会被破坏。实时数仓的弹性更多体现在资源隔离上,比如为核心实时链路单独划分Slot资源组,低峰期把多余Slot共享给离线任务,高峰期再收回来。这一点很多团队容易忽略,扩容节点的时候直接把Flink的并行度调大,结果重启作业导致数据延迟,反而得不偿失。
3.4 调度层弹性:让资源跟着业务优先级走
调度层是很多人忽略的一环。我用一个场景来说明它的作用:假设你给某一个核心业务队列分配了200个Container,可同时有小查询集群占用了另外300个Container。大任务确实在跑,但如果资源配额没调好,核心任务照样会被一堆即席查询挤得寸步难行。调度层在设计时至少要做三件事。
第一,把业务队列的最小资源和最大资源分开设置。最小资源保证核心任务在高峰期至少有基本保障,最大资源允许业务波峰时借用其他队列的空闲资源。这比硬性限制更贴合潮汐负载。第二,给调度器配置抢占策略。当高优先级队列长时间拿不到资源时,调度器可以从低优先级队列回收部分容器。把抢占阈值和延迟设好,不要一有资源紧张就立刻抢占,否则会很影响即席查询的稳定性。第三,把调度层和集群自动伸缩联动起来。节点自动伸缩不能孤立于队列配额之外。你新扩出来的节点如果没挂到对应的标签和队列,等于白扩。我会在下一章用一个实际案例说明这三件事怎么联动。
4. 扩容实战记录:从固定集群到弹性资源池
4.1 现状盘点与目标确定
拿我自己经历过的方案来说。当时集群大概是500多个计算节点,底层是HDFS,上层跑Hive、Spark和少量Flink任务,每天处理的数据量在几十TB量级。主要痛点有两个:夜间批处理高峰期任务排队严重,部分任务等待时间接近两个小时;存储水位一直在85%以上,运维经常半夜收到磁盘告警。当时的调度策略也很粗放,所有任务都丢在同一个大队列里,没有按业务优先级隔离,所以一个重任务会把整个集群拖住。
我们规划的弹性目标有三条:存储水位要降到70%以下;核心业务任务高峰期排队时间控制在5分钟以内;非核心任务在低峰期要被压到最少资源,尽量不占额外机器。目标定得很具体,后面做弹性策略的时候就有据可依。这里我建议你也这样做,不要只写“提升资源利用率”这种模糊目标,要有明确的数字指标,比如排队时间、存储水位、节点数量变化范围,这样最后才能验证方案到底有没有效果。
4.2 节点标签、队列映射与自动伸缩脚本
第一步,把所有节点按用途打上标签。比如核心批处理节点的标签是batch-core,测试和数据分析节点的标签是adhoc,Flink实时任务的节点标签是realtime。标签的意义在于,调度器做资源分配时能精确匹配到物理节点。第二步,把队列和标签映射起来。YARN队列和Kubernetes的NodeSelector本质都是一样的思路:同一套业务标签下,队列调度和节点注册才能对齐。如果不做这项映射,新扩容出来的节点没有走到对应队列,任务还是继续在老节点上挤。
第三步,写一个自动伸缩脚本,周期性地检查队列负载指标。我比较推荐用ResourceManager的REST API或者Prometheus指标去做判断,不要自己去解析日志。一个简单的判断逻辑长这样:
# 简化示例:检查队列 runningApps 和 pendingApps queue_usage=$(curl -s "http://rm-host:8088/ws/v1/cluster/scheduler" \ | jq '.scheduler.schedulerInfo.queues.queue[0].resourcesUsed') if [ "$queue_usage" -gt 80 ]; then # 调用云接口或自动化平台扩容 fleetctl scale queue-batch-core --size +20 else # 缩容,但先确认没有 running 的作业 fi这个脚本很粗糙,真正生产环境我建议引入冷却时间。比如连续三次检查都满足扩容条件再触发扩容,扩容后至少等15分钟再允许下一次扩缩容判断,防止业务几分钟内的抖动把自动伸缩带偏。缩容条件要更严格,最好连续一个小时低负载才允许触发,并且要遍历所有正在运行的Application,确保没有核心任务在节点上。
4.3 扩容后必须处理的数据重分布
扩容这个动作并不难,难的是扩容完之后的善后。最突出的两个问题:任务调度有数据本地性问题,历史数据分布不均。HDFS默认会对数据做平衡。扩容后从配置上看磁盘总量上去了,但新节点的磁盘使用率可能只有个位数,老节点已经逼近90%。这时候你要主动触发一次Balancer:
hdfs balancer -threshold 10 -bandwidth 100m-threshold表示允许的节点磁盘使用率偏差,-bandwidth限流带宽。我建议带宽不要开太大,否则平衡数据的流量会影响正在跑的业务。可以在低峰期用慢速跑一整天,比高峰期强开完事要安全得多。对于Hive或Spark数仓表,数据重分布还不只是HDFS层面的事。如果一张事实表按日期分区,历史分区都在老节点上,即使HDFS块均衡了,表的文件布局也不会自动跟着变。实用的做法是,对最热的那几个分区做一次Rewrite,让文件尽可能均匀分布到新节点。或者更务实一点,把新任务的产出路径配置为优先写到新节点,让新数据自然填补新节点的存储和容量。
4.4 资源利用率和任务时延的前后对比
弹性方案上线后,我们跑了一个完整业务周期做对比。存储水位从86%降到了64%;以前夜间批处理高峰期接近40分钟的排队时间被压到了3分钟以内;非核心的测试和分析队列在低峰期会把节点缩到最小规模,整体计算节点数在高峰期比低峰期多了大约120台,但账单上平均资源成本并没有涨太多,因为低峰期真的把机器缩回去了。下面这个表是我当时记录的对比数据,不同业务可能数字不一样,但方向是一致的。
| 指标 | 调整前 | 调整后 |
|---|---|---|
| 存储水位 | 86% | 64% |
| 批处理高峰任务排队时长 | 约40分钟 | 3分钟以内 |
| 核心队列平均资源利用率 | 55% | 82% |
| 非核心队列低峰占用节点数 | 全部在线 | 最小保留集群 |
| 夜间重跑任务成功率 | 82% | 99%以上 |
最大的一个意外收获是,故障排查变简单了。以前集群经常处于所有队列都在抢资源的状态,一个问题出现,很难判断是代码问题还是资源问题。有了弹性和配额边界之后,大部分资源问题会被自动伸缩先消化掉,剩下的基本都是任务本身的逻辑问题。你现在去诊断一个慢SQL,不用再先花半小时排查是不是有人占着队列不干活。
5. 常见问题与排查技巧实录
5.1 扩容后任务反而更慢
最让人沮丧的情况就是扩容之后任务没变快,甚至更慢了。十有八九是数据本地性失效导致的。虽然HDFS块是分布式的,但扩容后新节点大概率没有对应的数据副本,Spark调度Task时如果开启了本地性等待,就会因为等待数据本地性而拖慢整个作业。排查时先看Spark UI里Task的本地性级别,如果大量Task是RACK_LOCAL或者ANY,而不是NODE_LOCAL,那基本就是这个原因。解决思路前面也提到了:跑一次Balancer做数据均衡,同时调整spark.locality.wait的值,对重IO任务可以适当放松等待阈值,别让调度器等太久。
还有一种可能是元数据服务节点成了瓶颈。节点一多,NameNode或Hive Metastore的QPS成倍增长。如果这两个服务出现GC停顿,所有查询都会跟着卡。排查时留意管理节点的CPU和JVM Full GC次数,必要时把元数据服务也做成水平部署。
5.2 队列资源增大了,任务依然排队
任务排队不等于资源不够,还可能是因为调度器配置没生效。YARN的Capacity调度器里,队列的最大资源是静态配置,即使集群有空闲资源,队列配额没放开,任务也进不来。这类问题在弹性方案上线初期特别常见:集群确实扩容了,新节点的资源也确实被ResourceManager感知到了,但你给某个队列配的maximum-capacity还是20%,那这个队列最多只能用到集群容量的20%,再多资源都不属于它。检查时直接看调度器页面的队列资源使用情况,确认maximum-capacity是否合理。
另外,检查队列之间是不是存在资源黑洞。有些队列设置了很大的最大资源,但业务根本用不到,又不允许其他队列抢占,结果这些配额就像被锁死了一样。这时候要给空闲队列配置可以借用和抢占策略,把锁死的资源释放出来。
5.3 弹性缩容导致运行中的任务被强杀
缩容是弹性扩展里最危险的动作。如果直接下线一个节点,上面跑的Container和Task都会被强杀。不论YARN还是Kubernetes,缩容都要走优雅下线流程:先把节点标记为Decommissioning,调度器不再往该节点分配新任务,等已有任务完成或迁移后,再真正移除。排查时,重点看YARN中yarn.resourcemanager.work-preserving-recovery.enable是否开启,开启后即使ResourceManager重启也能恢复正在运行的Application;另外自动缩容脚本里要加一个判断条件,当前节点如果还有Running状态的作业,不允许触发缩容。
缩容还有一个容易被忽视的问题:实时任务。如果Flink任务的生命周期很长,它占用的TaskManager不能随意缩掉。我当时的做法是把Flink节点单独打标签,不参与自动缩容池,只有离线批处理节点和即席查询节点才允许弹性缩容。这个隔离设计很关键,宁可让实时节点在低峰期闲置,也不能让它被缩容脚本误杀。
5.4 小文件数量爆炸,NameNode压力陡增
弹性扩展的理想状态是文件少、块大、扫描快。但如果业务写数据时分区设置不合理,或者Spark任务生成了大量小文件,扩容多少节点都会被元数据瓶颈卡住。HDFS的NameNode在内存里要维护所有文件元数据,几亿个小文件存在时,整个集群的读写性能都会急剧下降。排查时先看两个指标:文件总数和目录总数,再对应看一下新增数据节点上的文件块分布,如果平均每节点文件数远高于正常值,小文件问题基本跑不掉。
解决思路有两个。第一是控制写入侧的文件大小,比如Spark写入时提高分区粒度、或者用coalesce减少输出文件数;第二是定时对分区做小文件合并,Iceberg、Hudi这类表格式有原生的Compaction机制,可以在后台持续把小文件合并成大文件。原则很简单:文件大小接近HDFS块大小,元数据数量才可能稳定。弹性扩展带来的节点越多,小文件问题被放大的概率就越大,所以这件事必须前置治理。
5.5 冷热分层之后,跨层查询明显变慢
冷热分层会引入一个新的问题:查询如果扫描的跨度很大,一部分分区在热层,一部分分区在冷层,查询引擎就得跨存储层拉数据。冷层是对象存储或者压缩比很高的归档格式,IO慢,查询体验自然下降。我见过一个团队把三个月前的数据全挪到归档存储之后,业务方跑历史趋势分析,原来两分钟的SQL变成二十分钟,直接被投诉。我的建议是把冷热分层和查询路由结合起来。比如对常用的最近N天查询,强制走热层;对于历史回溯类的低频查询,允许走冷层,但通过异步的方式返回结果。另外,在冷层上建立物化视图或者预聚合表,也能让历史数据查询不用每次全量扫描。
5.6 自动伸缩策略误判,频繁震荡
自动伸缩最常见的坑就是策略参数设置不合理,导致集群在两个容量之间来回伸缩。比如扩容触发的阈值是80%,但缩容的阈值是20%,中间相差过大,或者冷却时间太短,集群就会反复扩容-缩容,每次缩容都会引发数据重新分布和任务迁移,代价极高。调参的小技巧是:把扩容阈值设置得比缩容阈值迟钝一些。比如扩容连续3次检查都超过75%才触发,缩容连续5次检查都低于30%才触发,冷却时间至少15分钟。任何自动化的伸缩判断,都要先跑一周,观察它和真实业务节奏是否吻合,再把触发参数调稳,别第一天看着效果不错就立刻给所有队列启用。
6. 最后分享几条实操经验
6.1 什么情况下暂时别做弹性
做了这么多次扩容和弹性改造,我最想提醒同行的是:先想清楚你到底需不需要弹性,再动手。如果你的业务负载相对稳定、数据量增长缓慢,那固定容量集群没什么问题,强行搞弹性反而会增加系统复杂度和运维成本。比如一个内部BI平台,每天定时跑几十张报表,数据量一年才涨30%,那这套系统真正需要的是一次性容量升级,而不是一套自动伸缩机制。弹性和自动化一样,是为了解决高频的、动态的变化,如果变化本身不存在,引入弹性就是自讨苦吃。
6.2 落地时值得坚持的三个习惯
如果确定要做,也不要一步到位上云原生和容器,先把Spark动态分配、YARN队列配额、磁盘水位监控这三件事做好,效果立刻就能看到。这三件事做完之后,你已经获得了80%的弹性收益,剩下的20%属于真正的自动伸缩和存算分离改造,可以等业务把价值验证清楚再投入。我个人的习惯顺序是:先做监控和容量预估,再做队列配额治理,然后上动态资源分配,最后才做节点级自动伸缩。
还有一点,弹性扩展不是省钱,而是避免浪费。它的价值在于:当业务说要跑一个全量重算任务时,你不需要连夜采购机器;当业务量下降时,你不必继续为闲置资源买单。这种需要的时候拿得出、不需要的时候放得下的能力,才是数据仓库在大数据环境下比较健康和从容的状态。先做好监控,再谈自动伸缩;先做好队列,再谈多集群;缩容永远比扩容更谨慎。这几点,是我在实践中用真金白银换来的经验,写在这里供你参考。