ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

HDFS写读全流程拆解:从block到chunk的底层原理

HDFS写读全流程拆解:从block到chunk的底层原理

很多数据工程师在HDFS上跑了几百个任务,写了几十个TB数据,但被问起"一条数据到底是怎么写进DataNode磁盘的",回答往往止步于一句话:文件被切成block,复制三份,存到不同节点。这个说法没错,但粒度太粗。block只是HDFS在逻辑上对文件的切片,真正参与内存组装、网络传输、磁盘落盘和完整性校验的,是比block小得多的packet和chunk。block是HDFS的"户口本"单位,chunk才是数据的"身份证"单位。这篇文章打算沿着block到chunk这条链路,把HDFS读写全流程完整拆一遍,适合想深入理解分布式文件系统读写机制、排查读写性能或数据损坏问题的开发和运维同学。内容不绕弯,直接按底层原理和实操经验来讲。

1. 先把存储单位这层窗户纸捅破:block、packet、chunk各管什么

1.1 block只是逻辑切片,128MB是怎么来的

HDFS的设计目标之一是让单个文件能跨越整个集群的磁盘,所以文件必须切片。block就是HDFS自己定义的文件切片单位,默认128MB。为什么不设小一点?因为分布式系统的寻址成本太高。block太小,一个TB文件会切出上万个block,NameNode内存里每个block的元数据至少要占一百多字节,文件一多,堆内存立刻吃紧。另一个原因是IO模型:批处理任务读大文件时希望顺序IO命中率高,block太小会让任务调度粒度碎掉,block太大又影响并行度。128MB是两者之间的折中。这个值能通过dfs.blocksize配置调整,但生产环境基本都用默认值。

这里必须澄清一个常见误解:block不是物理磁盘块。文件系统层面的磁盘块通常4KB,而HDFS的block纯粹是NameNode的元数据概念。DataNode磁盘上不会出现一个正好128MB、带block名字的文件,block的内容最终会被拆到更小的传输单位里往下走。

1.2 packet是真正在网络上传的单位

客户端往DataNode写数据时,不可能一次把128MB全扔过去,网络socket接不住,内存也扛不住。实际做法是:DFSClient把block数据切分成一个个packet,默认每个64KB。packet是HDFS流式写入的传输单位,它在客户端内存里组装,在pipeline节点之间转发。一个packet里面有header,有多个chunk的data区域,整体结构紧凑。

打个比方:block像集装箱的标准尺寸,但真正在高速公路上跑的车是packet——一车一车运,而不是把整个集装箱甩上马路。理解了这一层,后面看DataStreamer的发送逻辑就顺了。

1.3 chunk是校验和计算的最小单元

每个packet内部,又被分成若干个chunk,默认每个chunk 512字节。chunk是HDFS做数据完整性校验的最小单元,每512字节数据计算一个4字节CRC32校验和。这里有个值得细品的工程设计:如果校验粒度太大,比如按整个block计算,一旦中间有一小段数据在传输或磁盘上损坏,整个block就废了,只能整个重新复制。校验粒度太小,校验和存储开销和计算开销又会翻倍。512字节配4字节CRC,校验和开销只有数据量的1/128,是个经典折中。

所以整个存储体系的分工是:block负责逻辑分片和副本管理,packet负责传输组装,chunk负责完整性校验。三者层层嵌套:一个128MB的block大约包含2048个64KB的packet,一个64KB的packet大约包含128个512字节的chunk,每个chunk对应一个4字节CRC32。

1.4 磁盘上的真相:每个block对应数据文件和meta文件

当数据真正落到DataNode磁盘上时(默认副本数3),每个副本分别对应两个文件:blk_1073741825这样的数据文件,以及blk_1073741825_1001.meta这样的校验和文件。block ID由NameNode全局分配,DataNode的volume选择策略决定block放进哪个磁盘目录。

meta文件的内容,说穿了就是这份数据文件里每个chunk的CRC32校验和列表,文件头还记录chunk大小和校验算法类型。所以磁盘上数据文件有多大,meta文件差不多就是它的1/128大小。曾经有业务方问我"这些.meta文件能不能删",我的答复很直接:不能。删了meta文件等于丢掉每个chunk的校验依据,以后DataNode读这个block时无法做完整性校验,数据损坏只能靠重读其他副本兜底,跟裸奔没区别。

存储单位默认大小职责对应物理实体
block128MB逻辑分片、副本管理、NameNode寻址blk_xxx数据文件
packet64KB网络传输组装、pipeline转发socket包头+载荷
chunk512字节数据完整性校验对应meta中4字节CRC32

2. 写入一条数据:从租约、DataStreamer到pipeline的完整链路

2.1 写请求的开始:create、租约与租约恢复

客户端调用FileSystem.create(path)时,实际是向NameNode发了一个RPC请求。NameNode会做三件事:检查权限、创建文件INode、为文件分配一个租约。租约是HDFS保证单写者写一致性的机制——同一时刻只允许一个客户端持有文件写租约。很多人忽略租约,直到写任务挂掉之后重试时抛出LeaseExpiredException才想起来有这回事。

如果客户端崩溃,租约会超时过期(默认软限60秒,硬限1小时),NameNode会触发租约恢复,把文件恢复到上一个完整状态。观察日志时,lease recovery通常会伴随一系列block recovery操作,这是HDFS把"没写完的半截block"收尾的核心过程,后面2.5节还会讲到。

2.2 DataStreamer是写路径的调度中心

create成功后,客户端拿到的不是普通输出流,而是一个DFSOutputStream。很多人以为写数据是主线程直接把数据写进socket,其实不是。DFSOutputStream内部有个专门线程叫DataStreamer,它维护一份待发送packet队列。主线程只负责:把用户数据切块、逐chunk计算校验和、组装packet、把packet放入队列;DataStreamer线程再从队列取出packet,通过pipeline发出去。

这个设计为什么好?因为网络写和内存组装是节奏完全不同的两件事。主线程若在write()里同步等待socket发送,一次flush就会阻塞很久。拆成独立线程后,write()只需把数据放进队列,吞吐立刻上来。也就是说,你在代码里写的hdfs.write(bytes)和数据真正落到DataNode磁盘,中间隔了一整条异步流水线。定位写性能问题时,第一步就是看DataStreamer线程是卡在队列消费上,还是卡在网络发送上。

2.3 chunk校验是写入路径的保险丝

chunk校验在写路径上的具体动作是:客户端每收到512字节用户数据,就计算CRC32,把这4字节填进packet中对应chunk的区段。packet的header里还会带offset、seqno等信息,DataNode收到packet后,把每个chunk的校验和逐条记入meta文件。

这里分享一个排查经验:某次离线任务频繁报IOException: Packet checksum mismatch,查到最后是网络交换机丢小包导致的。因为HDFS的packet带校验,数据在传输中只要被改一个bit,到达端校验立刻失败。这个机制正是HDFS敢在网络层不做额外完整性保护、也不怕数据漂移的底气。要注意的是,HDFS保障的是传输和存储完整性,不是业务幂等——应用层经常出现"两边对比少几条"的问题,这类问题不该指望HDFS侧解决。

2.4 三副本pipeline:转发、ACK与背压

当DataStreamer准备写一个block时,会向NameNode申请一组DataNode(默认3个),构成pipeline:DN1 -> DN2 -> DN3。写数据流向是:客户端把packet发给DN1,DN1落盘到自己的数据文件和meta文件后,把同一个packet转发给DN2;DN2同样落盘、转发给DN3。最后一个DN写完后,ACK包逐级返回客户端。

这种链式复制把客户端的写带宽压力降到最低:客户端只发一份数据,后两份副本由DataNode之间自行复制。但它也带来一个问题:pipeline中任一节点慢,全链就堵住,这就是背压。如果DN2磁盘老化导致每次fsync耗时很长,ACK反馈给客户端的时间就会拉长,DataStreamer发送队列开始堆积,最终写入阻塞。之前跑大批量入库任务时,最有效的观测手段就是看DataStreamer线程是否长时间卡在wait/read状态,以及DataNode侧数据落盘的fsync耗时。

2.5 写失败现场:pipeline重建与续传

pipeline在block写入途中如果某个节点宕机怎么办?举个例子:客户端已经发出packet 1到50,DN1也转发了大部分,此时DN2挂了。客户端的处理方式是:先做一次已确认和未确认packet的对齐,把DN2从pipeline剔除,剩余未写完的packet继续发给DN1,同时NameNode会选一个新节点DN3',DN1把数据补复制给它,最终恢复3副本。HDFS把这个动作叫pipeline recovery。

对运维同学来说,这段逻辑的价值在于:写任务里的IOException不一定是客户端bug。如果日志里出现Recovering pipeline或pipeline closed字样,往往意味着那一瞬间有节点故障或网络抖动,recovery完成后写入会自动继续。应用层此时疯狂重试同一个失败写请求,反而容易把客户端压垮。我的建议是:写数据任务把HDFS的retry策略设宽松些,让HDFS先内部恢复,不要一上来就靠应用层暴力重试。

3. 读取一条数据:定位、就近、校验与降级

3.1 读路径的起点:NameNode返回block位置列表

读文件比写简单,但细节不少。客户端调用open(path)时,NameNode返回文件的block列表,每个block对应一个BlockLocation数组。BlockLocation里最重要的信息是:这个block的副本落在哪些DataNode上,以及每个DataNode的机架信息。客户端读取某段数据时,会先按偏移量定位到目标block。

容易被忽略的一点是:BlockLocation给出的是一组副本候选地址,不是指定读某一个。客户端需要根据自身位置和候选DataNode的位置计算距离,选出最近副本去连接。选择逻辑默认按网络拓扑距离排序:本机优先,然后是同一机架、同一数据中心,最后才是跨数据中心。

3.2 读取的数据单位依然是chunk和packet

虽然读取方向是DataNode往客户端传数据,数据组织仍然沿用写路径的packet和chunk概念。DataNode读取本地block的数据文件,按packet粒度封装后通过socket返回客户端;客户端按packet解析,并逐chunk用CRC32校验和检查。写路径和读路径在数据切分上的默契,让校验逻辑完全复用,这是HDFS设计里很优雅的一点。

读性能上,客户端和DataNode之间距离远时,瓶颈往往不是磁盘顺序读,而是网络带宽。我在跑大表全量扫描时观察到,从不同机架读同一批数据,吞吐量差接近三成。后来把计算任务调度改成与数据所在机架对齐,跨机架流量明显降下来。这就是机架感知在读路径上最实际的价值。

3.3 校验失败时的自动降级链

读取时如果某个chunk校验失败,说明这个副本可能坏了。客户端会先把这块记录为校验失败,并向NameNode上报BlockBad状态;NameNode收到后把损坏副本标记为corrupt,安排从其他副本复制数据复原(详见4.2)。客户端本身则从BlockLocation列表里选下一个可用副本,重新发起读请求。所以只要副本冗余充足,读任务通常不会失败,只会悄悄变慢。

这里提醒一个坑:有些用户把replication临时改成2,再遇到一块坏盘,读任务就开始随机失败。这不是读路径没做降级,而是降级空间被压缩没了。核心数据还是建议保持3副本,除非你明确接受了数据丢失风险。

3.4 短路读:数据明明在本机,何必绕一圈网络

如果客户端进程恰好运行在block副本所在的DataNode上,按正常流程,数据还是会先从DataNode网卡出去,再绕回客户端——同机器绕一圈网络,白白消耗带宽和延迟。为了消除这个开销,HDFS提供Short Circuit Read,短路读:客户端通过Unix Domain Socket直接打开本地副本的数据文件,按偏移量读取,不经过DataNode的socket server。

启用方式是在hdfs-site.xml里配置dfs.client.read.shortcircuit=true,同时设置dfs.domain.socket.path,并确保libhdfs客户端库安装到位。用Spark或MapReduce跑本地任务时,短路读一般能带来10%到30%的读吞吐提升,尤其适合大量小文件或热点数据场景。但如果配置了短路读又没配好权限,客户端会因无法打开socket而回退到正常读路径,日志里出现shortcircuit disable提示。此时性能与普通读一致,属于保护性降级,很多人看到日志误以为集群故障,其实是正常回退。

4. 数据管家视角:副本策略、机架感知、损坏恢复与实操观测

4.1 默认的三副本不是随便放的

HDFS默认三副本放置策略是:第一个副本放在客户端所在的DataNode(客户端不在集群里就随机选一个节点),第二个副本放在与第一个不同机架的节点,第三个副本放在与第二个同机架但不同DataNode的节点。这么放是为了对抗三种故障域:机器故障靠同机不同节点,机架级故障靠跨机架副本,写性能则靠第二个副本合理跨机架换取整体容灾能力。

副本放置还和租约、故障转移联动。高可用模式下NameNode主备切换,block副本状态会经历epoch管理,多副本或少副本的情况由后后台的block replication线程调度补齐。这些机制平时不显眼,但每次节点下线维护时都能看到它们在工作。所以我一直建议,运维状态巡检不要只看CPU和内存,最好定期看一眼副本分布是否均衡。

4.2 从损坏发现到自动重建:校验和修复闭环

当一个DataNode在服务读请求时发现某个block的chunk校验失败,会把这个block上报给NameNode。NameNode把该副本标记为corrupt,然后启动一个复制任务,从其他健康副本把block复制到新节点,恢复副本数。因为校验和按chunk粒度保存,损坏被定位的精度能到512字节级别,读路径可以先发现chunk校验失败提前止损,而不必等整个block读完。

此外,DataNode的BlockScanner会周期性读取本地block并比对meta文件校验和,把静默损坏暴露出来。这就是大型集群普遍依赖HDFS完整性机制、不再另做文件校验的原因。meta文件本身损坏的情况理论上也存在,HDFS把这种情况视为坏盘并触发副本修复。理解这个闭环后,很多"为什么某个block健康副本数是2"的疑问就有答案了。

4.3 用fsck和debug亲眼看到block与chunk

讲了这么多原理,实际操作怎么观察block与chunk?我最常用的命令是这样的:

hdfs fsck /user/data/demo.csv -files -blocks -locations

输出里能看到文件被切成哪些block,block分布在哪些节点,是否健康。如果文件有损坏,fsck会直接列出corrupt block数,这是排查数据问题时最先应该看的结果。

hdfs debug recoverLease -path /user/data/demo.csv -retries 10

这条命令用于租约恢复,比如写任务崩溃后文件处于pending状态,可以通过它人工触发lease recovery。

更底层的观测方式是直接进入DataNode数据目录看blk文件:

ls -l /dfs/data/current/BP-xxx/current/finalized/subdir0/subdir0 | grep blk_

你会看到blk_数字开头的数据文件,和blk_数字_后缀.meta校验文件。两者体积比例大约128比1,正好对应512字节chunk配4字节CRC的设计。这个比例是判断meta文件是否异常丢失的直观依据:如果某个blk旁边没有同名meta文件,请立刻借助副本冗余恢复,不要等它出问题。

4.4 一个绕不开的扩展:EC(纠删码)模式下的block与chunk

写到这里必须提一嘴EC模式。HDFS从3.0开始正式支持纠删码存储,默认策略RS-6-3,用约1.5倍冗余换取与三副本同等的容错能力,存储成本直降一半。EC模式下依然有block概念,但角色变成了block group——一组block拼成一个条带,条带内按数据单元和校验单元布局,取代了副本模式下"每个block三份副本"的模式。因为条带内以单元为单位组织读写,不再走pipeline式全链复制,写路径变成同时写多个数据节点和多个校验节点。

EC对运维的要求也不同:文件一旦EC化,必须保持条带对齐,且不支持追加写。把历史文件从副本模式改成EC,需要新建文件再迁移,不能原地改。生产上我一般建议:热数据、频繁追加的数据保留副本模式;冷数据、写后不动的数据走EC。但无论哪种模式,从block到chunk的完整性校验思想始终一脉相承,只是单元名称和放置维度变了。

最后给一点私人心得:排查HDFS读写变慢或数据异常时,别急着调网络参数、换磁盘,先看block分布和corrupt状态,再顺着chunk校验链路往下查。我见过太多把问题定位到应用层,最后发现是meta文件丢失、副本重建缓慢导致的案例。理解了block到chunk这一层,很多HDFS黑盒问题其实都能用常识推出来。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进