搞大数据的人,多少都有过这样的经历:报警群里突然弹出磁盘使用率超过90%,领导连环追问是哪块业务在狂写数据,结果查了半天发现是一堆没压缩的日志文件和几千万个小文件把存储耗干了。HDFS作为整个数据体系的底座,平时看着挺稳,真到存储吃紧的时候,各种问题就全冒出来了。这几年我在生产环境里折腾HDFS的存储优化,踩了不少坑,也沉淀了一些实打实的经验,今天就从数据压缩、小文件治理、副本策略、纠删码、分层存储这几个维度,把HDFS数据存储优化算法这件事掰开揉碎了讲清楚。
这篇内容不是那种教科书式的概念罗列,而是实实在在围绕HDFS存储优化算法落地的完整记录。不管你是刚接触大数据生态的开发,还是要扛集群存储成本的管理者,或者是准备大数据面试的求职者,都可以对照这里面的思路和参数去做选型、调优和排查。搞懂HDFS存储优化的逻辑之后,你面对的可就不只是一个能存文件的分布式系统,而是一个能帮你省下真金白银的存储利器。
1. HDFS存储架构的瓶颈与优化切入点
1.1 存储问题的源头在哪里
先聊一个最基本的问题:HDFS为什么需要做存储优化?很多刚入行的朋友觉得HDFS反正就是往里面怼数据,存满了就加节点,哪有那么复杂。但只要真正跑过生产集群,就会知道存储成本不是线性的,它跟业务数据的增长曲线叠加在一起,会形成一道很陡的悬崖。
这里面的核心原因有三个。第一是副本机制。HDFS默认给每个块存3个副本,不管你的数据是热数据还是冷数据,只要写入就翻三倍占用空间,80TB的业务数据在HDFS里真实占用的却是240TB。第二是小文件问题。大量小于块大小的文件会同时挤占NameNode的内存和DataNode的存储,一个文件在NameNode上占用的元数据内存大概是150字节,看起来不多,但如果你有一亿个小文件,那就是15GB的堆内存被吃掉,直接推高整个集群的元数据服务压力。第三是压缩策略缺失。很多业务方把数据往HDFS一扔就不管了,既不选压缩格式也不调压缩参数,结果就是相同的数据量,存储成本比其他团队高出几倍。
这三个问题几乎贯穿所有HDFS集群的整个生命周期。你去看那些真正做过存储治理的团队,他们的优化思路本质上就是在跟这三个源头做斗争:用算法和策略减少冗余,用合理的格式和布局摊薄开销,用分层的手段让每一类数据住进性价比最高的存储介质里。
1.2 优化算法的整体分类与选型思路
把HDFS存储优化算法归类一下,大致可以分成四条主线。
与数据编码相关的压缩算法。这个层面的核心是在数据落盘之前,用压缩算法把数据体积降下来。常见的压缩算法包括Snappy、LZO、Gzip、BZip2和Zstandard,每一款在压缩速度、压缩比、CPU开销和可切分性上各有取舍。
与元数据治理相关的小文件合并算法。针对大量小文件对NameNode内存的消耗问题,通过合并写入、批量归档等方式把小文件变成大文件,从而降低元数据压力。
与冗余策略相关的副本与纠删码算法。通过调整HDFS的副本放置策略,或引入Erasure Coding(纠删码)机制,用更少的冗余换取相同的容错能力。
与数据生命周期相关的分层存储算法。从HDFS 2.6.0开始支持的异构存储和Storage Policy机制,让数据可以根据冷热程度自动迁移到不同类型的存储介质上。
选型的时候不要单点看某一个方案,要把压缩、合并、副本、分层这四层叠加在一起来考虑整体收益。我在实际项目里见过有的团队拼命压缩,结果因为是热数据频繁读取,CPU一直都在跑解压,查询性能反而下滑;也见过有团队为了省空间把副本全改成1份,结果一个节点坏了数据直接丢。存储优化的本质不是某个单一算法的极致应用,而是组合策略的动态平衡。
2. 数据压缩算法选型与参数调优
2.1 主流压缩算法横向对比与适用场景
压缩算法是HDFS数据存储优化里最直接、见效最快的一层。因为数据在写入HDFS之前,通常都会经过计算引擎(Hive、Spark、Flink)的加工,而在存储格式支持的情况下启用压缩,基本属于“举手之劳”就能省下数倍空间的事情。
目前生产环境里用的比较多的有六种:Gzip、BZip2、LZO、Snappy、Zstandard和Deflate。Gzip的压缩比在应对文本类日志数据时表现很好,能达到5到10倍的压缩率,但压缩和解压速度相对慢,而且不可分割。BZip2的压缩比更高,但CPU开销非常大,在实际的大数据生产链路里用得不多,主要是离线归档场景偶尔用它。LZO的特点是压缩速度极快、支持分割,但压缩比稍逊于Gzip。Snappy是Google开源的,主打高速压缩和解压,Google内部和Hadoop生态都很喜欢它,但它和LZO一样,在纯文本存储时也不支持分割,需要依赖容器格式来支持并行处理。Zstandard是Facebook开源的新一代压缩算法,压缩比接近Gzip,压缩速度接近LZO,是一个非常均衡的后起之秀。
我整理了一张表格,方便大家做选型参考:
| 压缩算法 | 压缩比 | 压缩速度 | 解压速度 | 可分割性 | 典型场景 |
|---|---|---|---|---|---|
| Gzip | 高 | 慢 | 慢 | 不支持 | 归档数据、低并发查询 |
| BZip2 | 最高 | 最慢 | 慢 | 支持 | 极端压缩需求 |
| LZO | 中 | 快 | 快 | 支持(需建索引) | 实时写入与查询并重 |
| Snappy | 中 | 最快 | 最快 | 不支持 | 高吞吐实时链路 |
| Zstandard | 高 | 快 | 快 | 支持(新版本) | 均衡型通用场景 |
这里需要注意,可分割性是一个特别关键的指标。MapReduce和Spark在读取HDFS文件时,会按照InputSplit的机制把数据分给不同的任务并行处理。如果文件本身不可分割,集群里有10个节点也只会有一个任务去读这个文件,并行度直接废掉。所以如果数据格式是纯文本并且没有容器格式的保护,选LZO或者BZip2会更好。但如果使用了Parquet、ORC这类列式存储格式,压缩算法的可分割性就不必担心了,因为列式格式自己会管理内部的Row Group和Stripe切分逻辑。
2.2 不同计算引擎的压缩配置实操
在实际落地的过程中,压缩选型不是改一个参数就完事。以Hive为例,如果你用的存储格式是TextFile,还要额外配置中间结果压缩和最终输出压缩,而如果你已经换成了Parquet或ORC,那么只需要设置对应的压缩编码器即可。这里给出一套通用性比较强的Hive配置组合:
# 启用中间结果压缩,减少Shuffle阶段的数据量 set hive.exec.compress.intermediate=true; set mapreduce.map.output.compress=true; set mapreduce.map.output.compress.codec=org.apache.hadoop.io.compress.SnappyCodec; # 启用最终输出压缩 set hive.exec.compress.output=true; set mapreduce.output.fileoutputformat.compress=true; set mapreduce.output.fileoutputformat.compress.codec=org.apache.hadoop.io.compress.SnappyCodec; set mapreduce.output.fileoutputformat.compress.type=BLOCK;在Spark SQL里配置压缩参数很直接,比如统一使用Zstandard:
spark.sql.parquet.compression.codec=zstd spark.sql.orc.compression.codec=zstd spark.io.compression.codec=zstd spark.shuffle.compression.codec=zstd这里的经验是,如果你的业务是高吞吐的实时链路写入,Snappy是首选,因为它在速度上的表现是目前所有压缩算法里最稳的;而如果是离线批处理场景,每天跑一次,查询频率也比较低,Zstandard的性价比会更高,因为存储成本的节省是持续性的,而CPU开销只发生在写入和读取那一刻。
2.3 压缩率实测与CPU开销的权衡经验
选压缩算法不能只看压缩比一个维度。我有一次在项目里把某张核心业务表的存储格式从Parquet+Snappy改成Parquet+Zstandard,压缩比从3.2提升了到4.8,看起来很划算,结果因为这张表被高频点查,前端业务总在等结果,集群的CPU直接被打满了。后来对比了开启Snappy解压的CPU消耗,发现Zstandard解压虽然也不慢,但在高并发点查场景下仍然比Snappy多消耗了大概15%到20%的CPU资源。
所以这里要记住一条基本准则:压缩比高的算法往往意味着CPU换空间,压缩比低的算法则是空间换时间。你需要判断业务是读多还是写多,是存储瓶颈还是CPU瓶颈。如果你们集群的资源瓶颈明明白白是磁盘和存储成本,那就放胆用Zstandard甚至Gzip;如果业务响应时间敏感、查询并发大,选择Snappy会更稳妥。
另外有个实操小技巧:开启压缩之后,要观察作业的Input和Output字节数变化,确认压缩是否真的生效了。有些作业因为上游数据源本身已经是压缩文件或者使用了不支持压缩的OutputFormat,压缩参数配了也不会生效。你在Hive命令行执行一个查询任务之后,看一眼Map数、读取字节数和写出字节数就能判断个大概,如果输出跟输入一样大,那就要重新检查Codec的配置了。
3. 小文件治理与合并策略实战
3.1 小文件为什么是HDFS的存储杀手
可以这么理解:HDFS跟人一样,塞进一万个米粒大小的积木和塞进一百块整砖,消耗的管理精力完全不一样。NameNode要为每个文件、每个目录和每个块维护元数据,启动的时候还要把这些元数据加载到内存里。在HDFS里,文件的列表、属性、块位置等信息全部存放在NameNode的堆内存中,越多的文件意味着越多的内存占用,也意味着更频繁的GC暂停风险。
更重要的是,小文件还会严重拖垮下游计算任务的效率。MapReduce或者Spark读取HDFS数据时,每个文件至少会生成一个InputSplit,每个Split会启动一个Map任务或者一个Partition处理逻辑。同样1GB数据,如果拆成1000个1MB的小文件,执行引擎就要启动大量短生命周期任务,调度开销、进程启动开销、上下文切换开销交织在一起,任务运行时长可能比数据本身大好几倍。
我接手过的一个数据平台,就是因为上游Flink作业每次Checkpoint都会在HDFS写一个非常小的文件,一年下来生成了几千万个碎文件,NameNode的堆内存一直报警,点开Active NameNode的GC日志一看,Full GC每五分钟就来一次。后来一分析,这完全是数据写入模式导致的存储结构问题,和计算逻辑本身没有关系。
3.2 合并方案的系统化设计与实操
小文件合并不能靠一种手段包打天下,要根据数据产生的方式来做不同的处理。
对于实时写入产生的小文件,最好的做法是在源头控制。如果你用的是Flink写入HDFS,可以调大StreamingFileSink或者BucketingSink的滚动策略参数。比如说把每个文件的滚动大小设置成128MB或者256MB,把滚动时间间隔设置成30分钟,把并行写入的桶数控制在合理的范围内,这样落盘的文件天然就是大文件。
如果你用的是Flume采集日志,可以设置HDFS Sink的hdfs.rollSize、hdfs.rollCount和hdfs.rollInterval参数:
agent.sinks.hdfsSink.hdfs.rollSize = 134217728 agent.sinks.hdfsSink.hdfs.rollCount = 0 agent.sinks.hdfsSink.hdfs.rollInterval = 0这里的意图是把文件大小作为滚动条件,128MB就滚动生成新文件,忽略条数和时间间隔。
对于已经存在的大量历史小文件,跑一个合并程序是最直接的方案。思路很简单:按目录读取所有小文件,把它们合并成1024MB的大文件,写回HDFS,然后删除原来的小文件。这个逻辑用Hive的INSERT OVERWRITE或者Spark的coalesce都可以实现。Hive的示例方式如下:
INSERT OVERWRITE TABLE target_big_table SELECT * FROM source_small_files_table;如果你的表是分区表,可以一个分区一个分区地执行这个逻辑,同时打开Hive的合并参数:
set hive.merge.mapfiles=true; set hive.merge.mapredfiles=true; set hive.merge.size.per.task=268435456; set hive.merge.smallfiles.avgsize=16777216;这些参数的意思是:Map任务输出的小文件如果平均大小小于16MB,就触发合并操作,合并后的每个任务输出控制在256MB。我建议你在做这种批量合并之前,先在测试目录里跑一遍,观察一下作业里的Reduce数量,确认一下合并后的文件数量和大小符合预期,再全量操作。
3.3 NameNode内存压力与元数据优化的配套动作
文件合并之后,元数据总量会降下来,但还有一个配套动作很容易被忽略:HDFS的垃圾回收机制。删除小文件时,文件会先进入回收站(Trash),如果你走的是命令行删除,回收站会保留默认的checkpoint间隔(通常是6小时或1小时),这期间NameNode内存依然还在被占用。所以在大规模清理之后,记得清空回收站:hdfs dfs -expunge,这个命令会把回收站里的文件真正物理删除。
还有一个思路是做归档压缩。如果你有一些业务场景要求保留所有原始文件,但很少访问,可以使用Hadoop的Har归档功能,或者直接把文件打包成SequenceFile再存入HDFS。Har归档可以将一个目录下的大量小文件合并成一个单一的HDFS归档文件,NameNode只维护一个文件对象,而不是几千个。但要注意的是,Har文件不支持追加写入,适合静态历史数据。
如果你需要更高效的小文件合并机制,可以参考Hudi或者Iceberg这类存储层技术。它们天生就设计了Clustering/Compaction机制,能在数据写入后自动合并小文件。这个方案的技术门槛稍微高一些,但效果非常显著,生产环境里已经有越来越多的团队采用这种方式来根治小文件问题。
4. 副本策略与纠删码的空间节省原理
4.1 默认副本策略的优缺点剖析
HDFS默认的副本因子是3,这个设计在早期硬件可靠性不佳的年代是非常合理的,三个副本分布在不同的机架和节点上,任何一个副本损坏都能自动恢复。但从存储成本的角度看,默认策略也有明显的浪费:并不是所有数据都需要三副本。比如临时爬虫数据、测试数据、日志备份这些可再生的数据,完全没必要占三份空间。
调整副本数的门槛其实很低,你可以对某个目录单独设置副本系数。用下面的命令把某个目录的副本数改成2:
hdfs dfs -setrep -R -w 2 /data/temp这里要提醒一下,-R表示递归设置了目录下所有现有文件,-w表示等待副本数达到目标数量后才返回。但要注意,这个命令只对执行之后“已经存在”的文件生效,对于后续新写入的文件,还需要通过HDFS的磁盘配额策略或者业务侧配合来保证新文件的副本数也是2。
把副本数从3改成2,存储成本直接下降33%,如果你管理的是磁盘容量长期告急的集群,这个操作带来的空间释放立竿见影。但风险也同时存在:2副本时,如果一块磁盘坏了,数据就只剩下一个活副本,紧接着再坏一块盘,数据就彻底找不回来了。所以这个操作只适合允许一定丢失风险的数据目录,不适合核心业务元数据。
4.2 机架感知与副本放置策略的优化
副本策略不仅仅是“放几份”的问题,还涉及“放在哪里”的问题。HDFS的默认副本放置策略是第一个副本存放在客户端所在节点,第二个副本放在同机架不同节点,第三个副本放在不同机架节点。这个策略保证了机架级容错,但也吃掉了大量跨机架的带宽。
如果你的集群是多机架部署,建议仔细核对一下net.topology.script.file.name配置,确保HDFS的机架感知是正常开启的。机架感知配置好之后,可以让HDFS在副本放置时先就近分配,减少读写操作的网络开销。考虑到跨机架带宽的稀缺,机架感知配合副本因子3能够在性能和容错之间取得更好的平衡。
另外,如果你用过Ceph或者其他分布式存储系统,会发现它们在数据分布上比HDFS更均匀。HDFS的Heterogeneous Storage策略允许你在Datanode上动态调整存储类型权重,虽然不能做到完全均匀,但可以通过dfs.datanode.available-space-volume-choosing-policy.balanced-space-preference-fraction参数来控制不同容量磁盘的写入偏好,避免出现大磁盘写满、小磁盘闲置的情况。这个参数在生产集群里非常实用,因为服务器扩容时经常会加挂不同容量的盘。
4.3 纠删码的原理与省量计算
纠删码是HDFS存储优化里技术含量较高、节省空间效果最显著的一项能力。HDFS的Erasure Coding是从Hadoop 3.0开始稳定支持的,它用数学算法替代了副本的完整复制,实现了“用冗余校验块保住数据可用性”的效果。
做这件事,需要用一个小例子来理解。传统的三副本方案里,三个1GB的块占3GB空间,任意损坏两个副本都能恢复。而纠删码RS-6-3方案里,把数据切成6个单元,再加上3个校验单元,总共占9个单元的空间,但允许任意3个单元损坏都能用剩下的数据通过数学运算恢复出来。对比相同的数据量:三副本占3倍空间,RS-6-3只占1.5倍空间,节省了50%的存储成本。
下表更直观地展示了纠删码配置和副本方案的对比:
| 容错方案 | 数据块数 | 校验块数 | 容错能力 | 存储开销 |
|---|---|---|---|---|
| 3副本 | 1 | 2 | 任意2个损坏 | 300% |
| RS-6-3 | 6 | 3 | 任意3个损坏 | 150% |
| RS-3-2 | 3 | 2 | 任意2个损坏 | 167% |
| RS-10-4 | 10 | 4 | 任意4个损坏 | 140% |
启用纠删码的实操命令也很直接。假设你有一个/data/ec目录想启用RS-6-3策略:
# 向HDFS添加纠删码策略 hdfs ec -addPolicies -policyFile /tmp/ec-policy.xml # 在目标目录启用纠删码 hdfs ec -enablePolicy -path /data/ec # 设置目录的纠删码策略 hdfs ec -setPolicy -path /data/ec -policyName RS-6-3-1024k这里的1024k表示每个条带单元的大小,生产环境中一般用1024k或者256k。条带单元太小会导致写入时CPU做编解码的频率太高,太大则会降低恢复时的并行度,所以默认值即可。
需要特别强调的是,纠删码不是万能的。它对随机写和删除操作的支持非常差,因为每次修改都要重新计算所有校验块。所以准确性上,纠删码只适合写一次之后就不怎么改动、访问频率也不高的冷数据,比如离线计算产出的结果表、归档日志、历史快照等。对于正在热读的业务数据,仍然保留副本策略更稳妥。
5. 基于数据热度的分层存储设计与落地
5.1 HDFS异构存储类型与存储策略
HDFS的分层存储架构,本质上是在DataNode节点上挂载不同类型的存储介质,然后在目录级别指定存储策略。常见的存储类型有四种:RAM_DISK(内存盘)、SSD(固态硬盘)、DISK(普通机械硬盘)、ARCHIVE(高密度归档存储)。
对应的存储策略主要包括:
| 存储策略 | 数据存放位置 | 适用场景 |
|---|---|---|
| Hot | DISK | 默认,高频读写热数据 |
| Warm | DISK + ARCHIVE | 一段时间内访问频率下降的数据 |
| Cold | ARCHIVE | 长期不访问的备份归档数据 |
| All_SSD | SSD | 对读写延迟极其敏感的数据 |
| One_SSD | SSD + DISK | 热数据在SSD,冷副本在DISK |
| Lazy_Persist | RAM_DISK + DISK | 写入速度要求极高的临时数据 |
设置存储策略的命令是:
hdfs storagepolicies -setStoragePolicy -path /data/app_logs -policy WARM设置完策略之后,HDFS会根据策略自动在不同存储介质之间移动数据的副本。这套机制跟传统存储系统的生命周期管理非常像,HDFS里把底层的数据块调度逻辑已经封装好了,你只需要告诉它哪个目录该用什么策略,系统会定期执行数据块迁移任务。
5.2 冷热数据迁移的完整实战流程
在实际项目中,冷热数据迁移是每一家大数据团队都绕不开的治理动作。我的习惯做法是把整个流程分成三步走。
第一步是定义数据分层标准。可以按数据的最后访问时间(Last Access Time)或者最后修改时间(Last Modified Time)来划分。比如设定90天内没有被读取的数据为冷数据,30天内没有被读取的数据为温数据。这一步要跟业务方对齐清楚,因为有的时候业务方嘴上说某张表不重要不再用了,但真到了迁移的时候又发现下游任务还在读它。
第二步是按照目录设置存储策略。先把热数据目录保持默认Hot策略,然后把温数据目录设置成Warm,冷数据目录设置成Cold。这一步用上面提到的hdfs storagepolicies -setStoragePolicy命令操作即可。如果你还没配置ARCHIVE存储目录,需要在hdfs-site.xml里增加如下配置:
<property> <name>dfs.datanode.data.dir</name> <value>/data/disk1,/data/disk2,/data/archive1</value> </property>然后给ARCHIVE目录打上ARCHIVE存储类型标签,具体做法是在存储目录路径后追加[ARCHIVE]标记,比如/data/archive1[ARCHIVE]。接着重启DataNode进程让配置生效。
第三步是验证迁移结果。存储策略配置好之后,HDFS的后台线程会自动执行块迁移,但这个过程不是实时完成的,通常需要等待一段时间。你可以用hdfs storagepolicies -getStoragePolicy -path /data/app_logs来查看策略,用hdfs fsck -path /data/app_logs -storagepolicies来查看数据块的存储分布情况。
5.3 分层存储的注意事项
在使用分层存储的时候,有一些细节需要特别注意。ARCHIVE介质通常比普通DISK的读写带宽低很多,所以冷数据迁移之后,如果业务侧偶尔还要加载这些数据做分析,启动时间会肉眼可见地变长。建议在迁移之前搞清楚下游任务是否还会读这些数据,如果偶尔会读,可以在查询调度里预留出额外的等待时间。
另外,存到ARCHIVE上的数据如果开启了纠删码,空间节省效果可以进一步叠加,但这个组合对数据恢复速度的影响也是双重的。真正的生产环境里,如果数据在归档层损坏,恢复的速度取决于归档存储介质的读取带宽,如果归档盘很慢,恢复过程会拖得非常长。所以对归档层的数据,建议把纠删码策略调整为恢复速度更快的RS-3-2,而不是RS-10-4。
再补充一点关于内存存储的经验。Lazy_Persist策略可以把数据写入RAM_DISK,再异步刷到磁盘,这对一些高并发小数据量写入场景确实能明显提速,但内存空间是集群中最昂贵的资源,切不可随意开启。我建议只在那些确实有高频实时写入诉求、并且数据量可控的场景下才考虑使用。
6. 常见问题与排查技巧实录
6.1 压缩配置不生效怎么排查
在实际过程中,压缩参数配置不生效是相当常见的问题。当发现HDFS上的文件体积没有变小,首先确认一下使用的存储格式是什么。如果你用的是Parquet或者ORC,压缩信息是写在文件里的,可以通过parquet-tools元信息查看:
parquet-tools meta /data/table_data/part-00000.parquet如果显示的是UNCOMPRESSED,说明压缩没生效,检查作业配置里的spark.sql.parquet.compression.codec是否被后续的其他参数覆盖了。有时候Spark作业里同时设置了spark.sql.parquet.compression.codec和spark.hadoop.parquet.compression.codec,两个参数互相矛盾,后一个会覆盖前一个,结果你以为配了Snappy,实际用的是Gzip。建议只保留一个统一入口的参数,避免信息混乱。
如果你使用的是纯文本格式存储,注意看一下Hive的hive.exec.compress.output是否打开,还要确认OutputFormat是不是HiveSequenceFileOutputFormat或者HiveIgnoreKeyTextOutputFormat。有些自定义OutputFormat不带压缩功能,无论怎么设置全局参数都没用。
6.2 纠删码目录写入报错怎么处理
启用纠删码之后,如果业务系统继续往该目录做随机写或追加写,通常会报类似RemoteException: Erasure coding is not supported for append的错误。这个问题非常典型,因为很多业务代码写HDFS时会默认把文件句柄的追加模式打开。解决方案有两条路:要么调整业务代码,把写模式改成覆盖写;要么把这个目录的纠删码策略撤销,改成副本模式。
撤销纠删码的指令是:
hdfs ec -unsetPolicy -path /data/ec值得说明的是,unsetPolicy不会把已经采用纠删码编码的数据块立刻改回副本模式,它只是取消了后续新写入的默认策略,历史数据块的格式保持不变。如果后面决定把历史数据也转到副本模式,还是需要对数据做一次重写。
6.3 存储策略迁移不生效的原因
有时候设置完Warm或者Cold策略后,等了很久发现数据块的存储分布没有变化,最常见的原因是目标DataNode上根本没有对应的存储介质类型。比如你想做冷数据迁移到ARCHIVE,但DataNode的dfs.datanode.data.dir里根本没有挂载ARCHIVE目录,HDFS在后台扫描存储策略时自然找不到可以迁移的目标。
排查命令是检查DataNode的启动日志,或者在Web UI里查看对应节点的存储容量概况。如果确认缺少ARCHIVE介质,就按前面提到的方法修改dfs.datanode.data.dir配置,增加带[ARCHIVE]标识的路径,重启DataNode。这里还有个细节,块迁移是低优先级任务,只在集群空闲时执行,高峰时段可能一直不触发,所以最好在低峰期观察迁移结果,不要刚配完就急着看效果。
6.4 小文件合并后查询变慢的原因
合并小文件本身是为了降低元数据压力,但有时候合并完发现下游查询反而变慢了。这个现象其实跟并行度有关:小文件多的时候,Map任务也多,虽然调度开销大,但并发天然高;合并成大文件之后,Map任务数量减少,如果文件大小超过了HDFS的块大小,每个块生成一个Map任务,浪费率倒是不高,但如果你在Spark或Hive里限制了并行度,就可能导致单个任务读取的数据量过大,跑得很慢。
解决办法是在执行查询之前把并行度调大,比如Spark SQL里设置spark.sql.shuffle.partitions,Hive里设置mapred.map.tasks。合并文件和调并行度最好一起做,否则存储压力是缓解了,计算效率却可能回落。我是从实际业务里学到的这个教训:不要只为了省空间而合并文件,一定要结合下游任务的并发能力来决定合并后每个文件的理想大小。
最后再分享一个我自己的经验。刚负责集群那阵子,我以为存储优化就是把压缩开到最大、副本改成最低、遇到小文件就狠命合并,后来发现每一次激进的操作都会在其他维度付出代价。现在我养成的习惯是:任何存储优化操作都先限定在一个目录或者一个分区内进行,小范围验证,跑通之后再逐步推广到整个业务域;同时把每个目录的优化策略、生效时间、预期收益和回滚方案记录在集群的运维文档里,方便半年后复盘。HDFS存储优化的坑很多,但只要掌握了底层原理、用好组合策略,你完全可以把它调校成兼顾成本、性能和安全的高效底座。