ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

HDFS读写流程深度拆解:从NameNode到DataNode的完整数据之旅

HDFS读写流程深度拆解:从NameNode到DataNode的完整数据之旅 搞大数据的人十个里有九个知道HDFS是Hadoop生态的存储底座但真要拉着你问一句“一条数据写进去再从里面读出来到底经历了什么”不少人会卡壳。你能说出来“先找NameNode、再写DataNode、还有副本复制”但再往下追问“租约什么时候创建、Pipeline怎么建立、校验和怎么算、块位置怎么选”很多人就开始含糊了。这不是理论考试钻牛角尖。HDFS读写流程直接决定了你调优的方向、排查故障的思路、写业务代码时对一致性的预期。文件写一半DataNode挂了会发生什么为什么读取的时候明明有副本还要挑节点为什么小文件会压垮NameNode这些问题追根到底全都落在读写流程里。这篇文章我把HDFS的数据写入流程和数据读取流程完整拆开讲从RPC调用到数据包流动再到节点故障处理逐层剥开配合实操命令和日志验证争取让你看完之后能用自己的话把整个过程讲明白。无论是应付面试、做集群运维还是写MapReduce或Spark任务时排查数据问题这套底层逻辑都用得上。1. HDFS读写流程到底在解决什么问题1.1 一个文件系统为什么要搞这么复杂先想一个问题单机文件系统只有一份数据读写流程就是文件描述符、页缓存、磁盘调度简单直接。HDFS面对的是几十上百台机器一份文件被切成若干个Block每个Block又有多个副本散落在不同节点上。这时候“写一个文件”就不再是单机的事而是要让一堆机器协作完成一件事。协作就得有约定。谁记录文件有哪些Block、Block放在哪谁决定这份数据往哪台机器写写的过程中某个节点挂了怎么办读的时候该找哪台机器要数据这些问题没有一套清晰的流程分布式文件系统就会被并发、故障、网络问题搅成一锅粥。所以HDFS的读写流程本质上是一套“协调协议”。它把NameNode变成唯一的元数据仲裁者把DataNode变成数据的执行者客户端则是发起请求的调度者。读写流程里的每个步骤都在回答上述这些问题中的某一个。1.2 先把四个主角认全理解流程之前必须清楚四个参与者的职责。Client客户端发起读写请求负责拆数据块、建管道、接收数据。不是被动的调用者而是整个流程的驱动力。NameNode元数据节点管理命名空间、文件到Block的映射、Block到DataNode的映射。它不存数据本身只存“数据的户口”。DataNode数据节点真正把数据落盘的机器处理读写请求、复制数据、定期向NameNode上报心跳和块报告。租约Lease不是节点是一个协议机制。保证同一时间只有一个客户端持有文件的写入权避免多个写者把文件写乱。这四个角色搞清楚了读写流程就像一条流水线每个环节是谁负责的都一目了然。2. 数据写入流程全拆解从create到close2.1 第一步客户端和NameNode的“领证”过程当你执行hdfs dfs -put 本地文件 /user/data/xxx或者调用FileSystem.create()写入流程就算正式开始了。第一步客户端会通过RPC协议向NameNode发送一个create请求。NameNode收到请求后做几件事校验权限。文件名是否合法、目录是否存在、当前用户有没有写权限。这一步失败的话你在客户端看到的报错会是Permission denied或者FileNotFoundException而不是真正发起写数据。在命名空间中创建空的文件条目。注意此时文件还只是一个“空洞”没有任何Block与之关联NameNode会把它标记为“正在写入”状态UNDER_CONSTRUCTION。这期间如果NameNode的EditLog记录了这次创建操作那么哪怕后续客户端崩溃元数据也不会丢。返回一个文件系统输出流FSDataOutputStream给客户端。从此刻起客户端手里握着这个文件的“写入许可证”后面要写数据就靠这个流。这里有个容易被忽略的细节NameNode在创建文件条目的同时会为客户端生成一个租约。这个租约默认有一个软超时时间和硬超时时间。客户端必须定期续租告诉NameNode“我还活着、还在写”。如果长时间不续租NameNode会认为客户端已经挂了允许其他客户端接管这个文件。这就是为什么某些异常断开的写操作过一段时间后又可以被重新写入的原因。2.2 第二步申请Block并建立管道文件空着不能算写完接下来客户端开始真正写数据。写入单位是Block默认128MB老版本是64MB可以在dfs.blocksize里配置。客户端不是一股脑把数据全发给NameNode而是每准备写一个Block就向NameNode发起一次addBlock请求。addBlock请求里只带着文件名NameNode要做的是给这个文件分配一个新的Block ID然后根据副本放置策略选出若干DataNode来存放这个Block的副本默认副本数3最后把这些DataNode列表返回给客户端。这段逻辑里最有含金量的是副本放置策略。以默认策略为例第1个副本放在客户端所在节点如果客户端不在集群节点上则随机选一个负载较低的节点。第2个副本放在与第1个副本不同机架的某个节点。第3个副本放在与第2个副本同机架但不同节点的机器上。这个“犬牙交错”的放置方式不是随便定的。把副本分散到不同机架是为了避免某个机架的交换机挂了导致所有副本同时消失前两个副本保持较近的距离是为了均衡写性能和容错成本。很多初学者以为副本随便丢就行实际上机架感知从一开始就影响着读写速度和数据安全性。拿到DataNode列表后客户端开始建管道。比如返回的三个DataNode是DN1、DN2、DN3那么客户端会和DN1建立TCP连接然后DN1主动和DN2建立连接DN2再和DN3建立连接形成一个Client - DN1 - DN2 - DN3的复制管道。这里为什么不用客户端直接把数据复制三份发出去原因很朴素网络开销。如果客户端自己给三个节点同时发数据客户端的上行带宽会被打满而且浪费了DataNode之间的带宽。做成管道之后客户端只发一份数据给DN1DN1边存边转发给DN2DN2边存边转发给DN3数据像接力一样流动集群内部的复制流量被DataNode之间分担效率高得多。2.3 第三步数据包在管道中如何流动管道建好之后客户端把数据切成一个个数据包Packet往管道里灌。这个切割粒度比Block更细Block是逻辑寻址单位Packet才是实际网络传输和写入的单位。每个包内部还有更细的结构。HDFS会把数据包进一步拆成Chunk默认一个Chunk是512字节每若干个Chunk计算出一个校验和Checksum跟着数据包一起写下去。这个校验和用的是CRC32算法写的时候会在数据包末尾附上对应的校验值。也就是说HDFS每一次落盘都不只是落原始数据还顺带落了校验信息。数据包沿管道流动时采取了“两段式”机制客户端先把包发给DN1DN1收到后写入本地磁盘同时把包转发给DN2DN2收到后写入本地磁盘同时转发给DN3DN3写入后不再转发而是向上游返回一个ACK确认。这个ACK要像击鼓传花一样一路传回客户端客户端收到ACK之后才允许继续发送下一个数据包。这就实现了管道内的流控。如果某一个DataNode写得很慢ACK传回来的速度就会变慢客户端发送下一个包的速度也会被迫降下来。这种设计避免了客户端一股脑把数据发出去、后端节点处理不过来导致内存爆掉的情况。这里还有一个容易踩坑的点客户端在发送完一个Block的数据后会发送一个带有EOF标志的包表示“这个Block的数据结束了”。DataNode收到EOF后会做一次收尾处理并返回最终确认。只有所有副本都写成功了客户端才认为这个Block写入完成。2.4 第四步写完后和NameNode的收尾工作当文件最后一个Block写完客户端调用close()或者complete()结束写入。这个调用不简单它也会走RPC找NameNode发起的请求内容是告诉NameNode“文件我已经全部写完了”。NameNode收到complete请求后会做三件事把文件的Block列表信息落进元数据把文件状态从“正在写入”改成“已关闭”CLOSED。这之后文件才允许被其他客户端读取。检查每个Block的副本数量是否达到最小副本数通常是1由dfs.namenode.replication.min控制。如果连最小副本数都没达到NameNode不会批准这次“完成”操作客户端需要等待数据复制的追赶。释放客户端持有的租约。释放之后其他客户端才能对这个文件进行重命名、删除或追加写入操作。注意一个细节close()是同步等待所有数据包都得到ACK之后才会执行吗其实不是。客户端发送完一个Block后会等该Block的ACK但并不需要等所有Block全部写完才发起complete。close()过程中会先调用flush()把缓冲区数据真正推送出去然后等所有已经发送的Block收到ACK之后才会向NameNode提交complete。所以你在客户端看到的“写入完成”和NameNode上的“文件关闭”是有严格先后顺序的。2.5 写入流程里的隐形守护者租约机制租约机制值得单独拿出来讲因为它是写流程里最容易被人忽略、却又容易导致线上问题的部分。每个客户端在创建文件时会被授予一个租约租约有两种超时软超时默认60秒dfs.namenode.lease-recheck-interval等相关参数控制超过软超时没有续租其它客户端可以申请抢租约。硬超时默认1小时dfs.namenode.lease-hard-limit超过硬超时没有续租NameNode直接强制释放该租约。正常情况下客户端写入过程中会不断续租撑死也就几十秒续一次不会触发超时。但要是某个写入进程被kill -9或者客户端机器突然断电租约就没人续了。硬超时一到NameNode强制回收租约这个文件才能被别人继续写或者删除。在实际运维中最典型的租约问题是某个任务写文件写到一半挂掉文件一直卡在“正在写入”状态后面还有别的任务想读取或重写这个文件发现拿不到访问权限或者更恶劣的是一个进程长时间持着租约不释放其他进程尝试写同一个文件时报Lease mismatch错误。这时候需要人工介入用hdfs debug recoverLease -path /path/to/file -retries 3强制恢复租约。我个人的经验是尽量不要在生产环境依赖这种人工恢复命令写任务代码时做好try-finally确保输出流一定关闭如果任务会挂最好让上一层的调度系统自动重启并清理临时文件别等租约超时把任务卡成一堆故障。3. 数据读取流程全拆解怎么把数据“就近”拿回来3.1 打开文件还是先问NameNode读取流程和写入流程有很多对称的地方但细节完全不同。第一步仍然是客户端通过RPC向NameNode发起open请求。NameNode不负责传输数据本身它只负责“指路”它会根据文件名找到对应的文件元数据返回给客户端一个LocatedBlocks结构。这个结构里最核心的信息是文件由哪些Block组成每个Block都有哪些副本每个副本分别在哪台DataNode上。对客户端来说这就相当于拿到了一张“藏宝图”——每个Block在哪些节点上有备份一目了然。拿到这些信息后客户端创建FSDataInputStream注意这把“流”和写入时的流性质完全不同。写入时的流会主动把数据往外推读取时的流更像一个“拉取器”按需向DataNode发起请求把数据一块一块拉回来。3.2 顺序读取与块位置排序的秘密客户端拿到LocatedBlocks之后按顺序开始读第一个Block。这时候有一个大多数资料不会细讲、但对性能影响很大的步骤DataNode列表排序。NameNode返回的副本位置列表不是随机排列的而是按“网络距离”从小到大排好序的。HDFS用机架感知计算网络距离两台机器在同一个节点上距离是0同一个机架不同节点距离是2不同机架距离是4。客户端读取时优先选择距离最近、也就是排序最靠前的那个DataNode。为什么排序这么重要因为读取数据消耗的是网络带宽。如果客户端在北京有一份副本在北京另一份副本在深圳选错节点代价巨大。HDFS的聪明之处在于把选择权交给客户端让客户端根据自己的位置选择就近的DataNode把跨机架流量压到最低。这也是为什么HDFS官方会强调把计算任务调度到存有数据的节点上能让MapReduce和Spark的“数据本地性”发挥到极致。读流程还有一个隐藏机制是“顺序连续读”。当客户端读取第一个Block之后会继续读第二个、第三个。对每一个新的Block都要重新走一遍“挑最近的DataNode”的流程。所以一个文件分散在很多Block时读取时会和不同的DataNode分别建立连接而不是只跟一台机器通信。3.3 边读边校验数据管道的反向版本客户端从选定的DataNode读取数据传输格式和写入时类似但方向反了过来DataNode把存储在磁盘上的原始数据块按Packet读出来发送给客户端。这里最重要的操作是校验和验证。还记得写入时每个Chunk都带CRC32校验值吗读取的时候DataNode在发送数据时会带上同样的校验信息客户端接收到数据后自己再算一遍校验值并和本地的校验信息做比对。如果校验值一致数据认可如果不一致说明数据在传输或存储过程中发生了损坏。校验失败不等于读取直接失败。客户端会尝试从副本列表中的下一个DataNode重新读取这个Block同时会向NameNode上报这个块可能损坏了。如果副本全部尝试完仍然失败客户端才抛出异常。这个机制让HDFS在部分DataNode磁盘损坏、数据块损坏时仍然能稳定读取不会因为一块盘坏了就导致整个文件的读取失败。值得一提的是HDFS还支持“读取Pipeline中的校验和文件”以更快地检测损坏但默认情况下客户端只校验自己收到的数据。真正落地的校验更多依赖DataNode后台的BlockScanner定期扫描块数据主动发现并上报异常副本。3.4 读一半节点挂了怎么办读流程的故障处理比写流程“温和”很多因为副本天然存在读一个DataNode失败换一个读就好。客户端在读第一个Block时连接的DataNode如果发生网络超时或者无响应会从位置列表中选出下一个副本重试。注意这里有一个细节HDFS客户端的读取重试机制不是无限重试的默认每个Block最多重试几次就会抛出异常避免无限等待。超时参数在dfs.client.socket-timeout等配置里控制。线上遇到“读文件慢”的时候往往不是文件本身慢而是某个DataNode不可用客户端反复超时重试才显得慢。另一个容易被忽视的场景是读取过程中NameNode新追加了Block。比如一个文件正在被写入另一个任务对相同文件执行读取。HDFS的读流程会先拿到写开始时已存在的Block列表如果后续又有新Block被写入客户端读到最后发现Block列表对不上会再次向NameNode拉取最新的Block位置信息。这个设计保证了读写并发时读到的是相对一致的数据切片不会一直卡着等写入完成。4. 用命令和日志验证把读写流程落实到工程里4.1 常用命令背后发生的底层动作很多人日常用hdfs dfs -ls、hdfs dfs -put、hdfs dfs -get用得飞起却没想过每个命令底层到底触发了什么流程。其实对照一下会发现非常清晰。hdfs dfs -put完整走了第2章的创建文件、申块、写数据、关闭文件四步流程命令结束时文件一定已关闭、可直接读取。hdfs dfs -appendToFile走的是“打开一个已存在文件”的写流程先申请租约再追加新Block最后关闭。这比新建文件的流程更复杂因为要处理和已有文件的元数据合并。hdfs dfs -cat/-get走读取流程先open拿到LocatedBlocks再从DataNode按块拉取数据。如果读的是远端文件路径客户端会尽量挑选距离当前执行节点最近的数据副本。hdfs dfs -copyFromLocal、-copyToLocal本质是put/get加一层本地文件系统的适配底层读写流程完全一样。理解这些对应关系后排查询问题时思路能清晰不少put慢去查写流程里的Pipeline和DN落盘get慢去查读流程里的DataNode取舍和网络距离append失败去查租约有没有被占用。4.2 用Java API体验一次完整写入和读取纸上得来终觉浅如果你周围有测试环境但没配好先跑一段最小的Java API代码输出关键节点能更直观地看到流程顺序。import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.*; import java.io.OutputStream; public class HdfsReadWriteDemo { public static void main(String[] args) throws Exception { Configuration conf new Configuration(); conf.set(fs.defaultFS, hdfs://10.10.10.10:9000); FileSystem fs FileSystem.get(conf); // 1. 写数据create 对应 RPC 创建文件 Path writePath new Path(/user/test/demo.txt); FSDataOutputStream out fs.create(writePath); out.write(hello hdfs write flow\n.getBytes(UTF-8)); // 2. flush/hsync 会触发数据真正写往 DataNode建议手动调用一次观察效果 out.hflush(); out.close(); // 3. 读数据open 对应 RPC 获取 LocatedBlocks FSDataInputStream in fs.open(writePath); byte[] buf new byte[1024]; int len in.read(buf); System.out.println(read: new String(buf, 0, len, UTF-8)); in.close(); fs.close(); } }这段代码最终跑出来的数据流向和hdfs dfs -put完全一样只是控制粒度更细。你可以分别在create()之后、hflush()之前、close()之后去看NameNode里的文件状态变化非常有感知。要注意Java API里OutputStream.write()只是写到客户端缓冲区真正触发网络和落盘的是flush()、hflush()或者缓冲区满之后自动冲刷。所以如果一段代码调用了write()却没调flush()就退出数据有可能会丢失。4.3 从日志里亲眼看到读写过程启用NameNode和DataNode的调试日志后能看到很多读写流程的痕迹。NameNode侧最值得关注的是审计日志默认路径logs/hadoop-hdfs-namenode-host.log中的审计行。每做一次文件系统操作都会打印一行类似allowedtrue ugihuangp (auth:SIMPLE) ip/10.10.10.11 cmdcreate src/user/test/demo.txt dstnull permhuangp:supergroup:drwxr-xr-x protorpc看到cmdcreate、cmdmkdirs这类记录就知道客户端确实和NameNode打了招呼。DataNode侧写入时日志里会出现创建块、处理写入管道的记录读取时则会出现处理读请求的记录。如果配置了DEBUG级别还会看到更详细的数据包收发日志。19/xx/xx 14:00:01 INFO datanode.DataNode: Receiving block blk_12345 src: /10.10.10.11:34567 dest: /10.10.10.12:50010 19/xx/xx 14:00:02 INFO datanode.DataNode: Received block blk_12345 of size 128MB实操中通过日志时间戳对比可以判断“慢”到底慢在客户端→NameNode的RPC、Client→DN的传输还是DN→DN的转发能快速缩小排查范围。5. 常见问题与排查技巧实录5.1 写入慢、写一半失败怎么办写入慢优先看三个位置客户端到第一个DataNode的带宽、DataNode之间的转发带宽、磁盘落盘速度。很多写入卡顿不是网络问题而是某一台DataNode的磁盘是慢盘导致ACK迟迟不回来拖了整个管道。写入失败要看异常类型最经典的是Block recovery failed。这通常是写入过程中某个DataNode挂掉客户端需要重新向NameNode申请一批新的DataNode重建Pipeline。如果所有DataNode都试过了还是失败说明集群里没有足够的节点满足副本放置策略或者某些节点被排除了dfs.hosts.exclude或节点下线状态。写入时还经常遇到NotEnoughReplicasException意思是NameNode无法按副本数凑齐DataNode。这个错误在节点数少于副本数时必现排查思路是检查dfs.replication、dfs.namenode.replication.min确认集群存活节点数是否足够。5.2 读取慢、读取校验失败怎么查读取慢先看文件块分布。用hdfs fsck /path/to/file -files -blocks -locations列出文件每个Block的分布情况。如果某些Block的所有副本都集中在少数几台机器上那读取时会被迫跨网络拉数据。做一次hdfs balancer或设置dfs.datanode.balance.bandwidthPerSec能改善分布。读取报校验失败比如Checksum mismatch说明某个副本数据已经损坏。做法是找到该Block对应的副本位置把那台DataNode上的坏副本删掉或者用hdfs debug recoverLease等工具触发重新复制。注意损坏的副本如果被NameNode标记为corrupt它会在下一次块报告中被上报然后NameNode自动调度新的复制任务。另一个高频案例读文件时报FileNotFoundException但文件明明存在。这种情况多发生在开启了HA的集群上客户端连接的NameNode发生了主备切换客户端的stale元数据没有刷新。定期同步fs.defaultFS配置并让客户端及时重试一般就能解决。5.3 租约冲突的经典案例这里分享一个自己踩过的真实案例。有一次数据同步任务每天晚上写HDFS同一份表某天突然大面积报Lease mismatch错误。排查后确认是一个前一天的任务进程异常残留一直占着文件的租约导致新任务无法获得写入权限。清理掉残留进程后用hdfs debug recoverLease -path /user/data/tmp_table -retries 1强制拿回文件集群才恢复正常。所以对跑批任务我强烈建议临时目录或者临时文件务必在任务完成后主动deleteOnExit()或者异常清理一旦出现租约冲突优先查是否有僵尸进程而不是直接重启集群。5.4 小文件问题读写流程的终极噩梦如果把读写流程反过来看小文件问题其实是被放大得很明显。集群里每个文件在NameNode内存中都要占一块元数据约150-250字节。一个1GB文件可以被切成一个128MB的Block占一份元数据如果切成一万个100KB的小文件就要占一万份元数据NameNode内存瞬间告急。更麻烦的是读大量小文件时客户端每读一个文件都要和NameNode做一次open交互还要分别和不同DataNode建立连接大量时间花在RPC握手和建连上。这也是为什么现在主流做法都偏向于把“小文件先合并成SequenceFile或ORC再写HDFS”本质上就是让写入流程的Block分配更高效而不是一次写几个KB就建一次Pipeline。6. 一点个人经验和总结收尾把读写流程完全捋清楚之后再回头看HDFS你会觉得它不再是一个“黑盒存储”而是一套逻辑严密的分布式协议。写入时靠租约锁住一致性靠Pipeline提升复制效率靠流水线ACK做背压控制读取时靠元数据定位、靠机架感知选近节点、靠校验和保证数据完整。每一个环节都有自己的取舍逻辑没有哪一步是多余的。在实际工作中我最深的感触是读写流程的知识不是为了应付面试背下来的而是故障排查时下意识判断问题的底子。文件写不进去你能立刻判断是NameNode元数据出问题还是DataNode排布出问题还是租约冲突文件读不出来你能下意识去查块分布、查损坏副本、查网络距离。这种“快准狠”的排查能力来自对流程细节的深度理解而不是对着监控面板瞎猜。最后补充一条小技巧排查HDFS读写性能或故障时不要一头扎进日志先看hdfs dfsadmin -report和hdfs fsck的输出确认底层健康的节点和Block状态再决定要不要深入RPC和网络层。磨刀不误砍柴工这条经验帮我省了很多无意义的时间。
返回列表