1. 当初我们为什么要折腾分布式文件系统先说个我印象特别深的事早几年在一家做日志分析的公司单机存储扛不住了。当时业务一天产生几十GB的日志一台服务器的磁盘很快就满了买新盘、挂阵列、做RAID折腾一圈发现还是不够用。更麻烦的是数据只存在一台机器上一旦那台机器宕机整条数据分析链路就断了全组人盯着重启那种焦虑现在想起来还手心冒汗。后来我们把目光投向了分布式文件系统。这东西说白了就一句话把一堆普通服务器上的磁盘组织起来对外伪装成一个容量超大、可以随便读写的大文件夹。你不需要关心数据到底存在哪台机器上也不需要关心某台机器是不是挂了系统自己会处理这些脏活累活。很多刚接触大数据的朋友容易把分布式文件系统和“网络共享文件夹”搞混。其实差别很大。网络共享文件夹比如SMB、NFS本质上还是把一台机器的磁盘暴露给其他人用数据依然集中存储性能和容错都受限于那一台机器。而分布式文件系统是真正把数据打散存储在多台机器上同一份数据还会有多个副本上游宕机了下游照样读数据。说到分布式文件系统的代表作HDFSHadoop Distributed File System绝对是绕不开的一个。它算是整个大数据生态的地基Hive、HBase、Spark这些耳熟能详的组件底层数据存储基本都跑在HDFS上。这篇文章我打算从设计者的视角把HDFS这套系统从头到尾掰开揉碎讲一遍顺便把日常用得最多的hdfs dfs命令操作体系也梳理清楚。不管你是刚入门大数据、准备面试还是已经写了几年MapReduce想回头补补底层原理应该都能从里面捞到点东西。我尽量用讲故事的方式把架构、原理、读写流程、容错机制、命令实操串成一条线同时把那些“文档上不写但实际经常踩”的坑也一并交代了。2. 整体架构三个角色一台戏2.1 NameNode整个系统的大脑HDFS采用的是经典的主从架构Master-Slave主节点叫NameNode从节点叫DataNode。NameNode这个名字听起来很技术但它的职责可以概括成三件事管目录、管文件、管数据块的位置。所谓“管目录”就是维护整个文件系统的命名空间也就是你在HDFS上看到的路径结构类似Linux的/目录树。“管文件”是指记录每个文件包含哪些数据块Block以及每个数据块的大小、副本数这些元数据。“管数据块位置”则是维护一个映射表某个数据块究竟存放在哪些DataNode上。画个不太严谨但很好懂的类比NameNode就像一个图书馆的总索引卡。真实的书数据分散存放在各个书库DataNode里但你要找哪本书在哪一层哪一个书架去查总索引卡就行了。只要总索引卡不出错读者永远能快速找到书。这里有一个非常关键的设计点NameNode本身不存储任何真实数据。它只存元数据Metadata。真实数据全部在DataNode上。这么做的好处是NameNode的读写压力很小整个系统的数据IO不会被主节点卡脖子。坏处也很明显下面会专门讲。2.2 DataNode真正背锅干活的角色DataNode是真正存数据的地方。每台DataNode机器上挂着若干块磁盘HDFS会把文件切分成固定大小的数据块然后分散存放在这些磁盘上。DataNode定期向NameNode发送心跳Heartbeat和数据块报告BlockReport。心跳的作用是告诉NameNode“我还活着”数据块报告的作用是告诉NameNode“我手上现在有哪些块”。NameNode就是靠这些信息实时掌握集群的健康状况和数据分布情况的。很多人第一次接触HDFS会好奇为什么要把文件切块直接整个文件存不行吗答案是切块是实现并行读写和容错的前提。一个文件切成多个块存放在多台机器上客户端就能同时从多台机器拉数据速度自然就上去了。某一块所在的机器挂了其他机器上的副本还能顶上数据不会丢。关于副本策略后面有专门小节。2.3 Client用户与系统的桥梁第三个角色是客户端Client。它既是用户执行命令的入口也是整个读写流程的发起者。Client并不神秘你在终端里敲hdfs dfs -ls /时调用的就是HDFS客户端。客户端做的事情主要有三件与NameNode通信获取文件的元数据信息比如文件包含哪些块、块在哪些机器上与DataNode直接通信传输真实数据在本地缓存数据以便进行流水线写入和容错处理。有一点需要特别强调客户端写数据时并不通过NameNode中转。NameNode只负责告诉客户端数据块应该写到哪三台机器真正传输数据的是客户端和DataNode之间直接建立的连接。这个设计属于“控制流与数据流分离”是大数据系统一个非常经典的架构思想。把这三个角色放一起整个HDFS的工作方式就清晰了Client负责发号施令NameNode负责任务调度和元数据管理DataNode负责真实数据的存储与读写。3. 文件读写路径拆解数据到底是怎么流动的3.1 写入流程为什么是“流水线复制”假设你要把一个2GB的日志文件上传到HDFS的/data/目录下默认副本数是3。整个过程大概是这样第一步切分与请求客户端读取本地文件按默认块大小通常是128MB老版本是64MB把文件切成多个块。然后向NameNode发起写入请求注意这一步发的是RPC请求走的是控制通道。第二步分配DataNode列表NameNode收到Client的写块请求后会去元数据里查一下当前集群的存储情况和机架拓扑然后返回一份有序的DataNode列表比如[dn3, dn1, dn5]。这个顺序不是随便排的它遵循一个“网络距离”原则第一个DataNode尽量和Client在同一机架后面的节点依次放到不同机架。目的很纯粹——既能降低跨机架流量又能在机架级故障时保住数据。第三步流水线写入Pipeline WriteClient拿到DataNode列表后会和dn3建立连接同时dn3会和dn1建立连接dn1再和dn5建立连接形成一条复制链。数据以数据包Packet默认64KB为单位在链上依次传输Client → dn3 → dn1 → dn5每个节点收到数据包后先写入本地磁盘然后立刻转发给下一个节点。这种设计叫流水线复制好处是传输路径短、延迟低坏处是如果链路中间某个节点挂了系统需要重新构建流水线。实际实现里还引入了ack确认机制每个数据包传输完成后会沿着反方向返回确认消息确保数据是真的落盘了而不是只进了内存。第四步确认与提交所有数据包写完后DataNode会向NameNode汇报“块已提交”NameNode更新元数据映射表。整个文件的全部块都提交完Client收到“写入成功”的响应上传流程结束。这里有个实操小知识虽然HDFS默认块大小是128MB但如果你存的是大量几十KB的小文件或者反过来存的是超大文件都可以用dfs.blocksize参数调整。调大块大小能减少NameNode元数据量调小则能提升并行度但会增加元数据开销。下面“避坑”那一节我还会细讲。3.2 读取流程就近原则与并行拉取读数据比写数据要简单得多但也有很多细节值得说。第一步元数据查询Client向NameNode发送我要读/data/log.txt的请求。NameNode返回该文件的所有块信息包括每个块在哪些DataNode上有副本。第二步就近选择Client拿到块位置列表后会按照网络距离排序优先选择离自己最近的DataNode。怎么判断最近HDFS内部有一套机架感知Rack Awareness机制会计算Client到目标DataNode的“网络距离”。同一个节点距离为0同一机架距离为2不同机架距离为4数字越小优先级越高。这套机制的实现细节不少但核心思想和我们平时点外卖选最近门店是一样的。第三步并行读取如果一个文件有多个块Client会同时向多个DataNode发起读取请求实现并行IO。这也是为什么HDFS适合处理大文件——块越多能并行读取的通道越多吞吐量就越高。读数据过程中如果某个DataNode突然挂了Client不会傻等它会立刻切换到持有同一副本的另一台DataNode对整个读取过程几乎无感。这就是副本机制给读性能带来的隐形加成。3.3 大文件设计哲学讲完读写流程我想额外提一个设计层面的点HDFS的块大小为什么是128MB而不是像普通文件系统那样4KB或者1MB这个问题的答案直接决定了HDFS适合干什么、不适合干什么。块设计得大意味着一个文件对应的块数量少NameNode需要管理的元数据对象就少能支撑的集群规模就大。假设一个100GB文件如果块大小是4KB需要2600万个块来装NameNode内存吃不消如果块大小是128MB只需要800个块完全没压力。块大客户端寻址次数就少顺序读写性能更好。大数据场景基本都是流式读取顺着从头读到尾很少做随机小范围修改大块配合顺序读几乎是黄金组合。块大Mapper端做数据切片Split时更容易实现“计算跟着数据走”的本地化调度减少网络传输。所以说HDFS的设计理念是为“一次性写入、多次读取、顺序扫描”的大文件场景服务的。如果你拿它当普通文件系统用频繁更新文件内容或者存上亿个几十KB的小文件那体验会非常糟糕。这不是HDFS不行而是你用错了场景。4. 副本放置策略与故障自愈容错设计背后的账本4.1 三个副本怎么放才算聪明默认副本数是3这是大数据领域传了很多年的“黄金配置”。那这3个副本到底放在哪怎么放直接决定了系统的容错上限。HDFS默认的机架感知副本放置策略大概是这样的第一个副本优先放在Client所在的DataNode上如果是外部提交则随机选一个负载较轻的节点。为什么因为写入时第一个节点直接接收数据放本机写速度最快、网络开销最小。第二个副本放在与第一个副本不同机架的某个节点上。这样即使整个机架断电或者交换机故障另一个机架上的副本依然可用。第三个副本放在与第二个副本同一机架的另一个节点上。这样既多了一个副本又不会把所有鸡蛋分散得太远能兼顾网络带宽和读写效率。画成图大概是这样机架A: dn1副本1 机架B: dn3副本2, dn5副本3算一笔账这种放置方式能容忍的目标是“一个机架整体宕机”。因为即使机架A全挂机架B上还有两份副本。反过来如果机架B里的dn3和dn5同时挂了机架A的dn1也还能顶上。所以实际故障容忍上限约等于副本数减13副本能扛住2个节点同时挂但如果整个机架只部署了其中一个副本则需要具体分析。这里我想给一个实际建议如果你在搭建生产集群机架信息topology.script.file.name配置一定要配好。如果不配机架感知HDFS会默认把所有节点放在同一个机架里副本放置策略退化为“随机散放”跨机架容灾就形同虚设。很多小公司集群出事最后查下来就是当初没配机架拓扑。4.2 心跳与故障检测系统怎么知道节点挂了DataNode默认每3秒向NameNode发送一次心跳如果NameNode连续10分钟没有收到某个DataNode的心跳注意是10分钟不是30秒就会判定该节点“已死”然后把这个节点上的所有块标记为“副本数不足”进入待复制状态。这里有个很值得玩味的工程设计为什么心跳超时设置得这么长默认dfs.namenode.heartbeat.recheck-interval是5分钟加上dfs.heartbeat.interval的10次心跳最坏情况下要10多分钟才能感知节点故障因为HDFS的设计哲学是“不轻易开始数据恢复”。如果网络抖动几秒钟就触发大规模副本复制那整个集群会陷入无休止的“假死”和“恢复”循环带宽被复制任务吃光正常业务反而会被拖垮。所以HDFS宁可多等一会儿确认节点真的挂了也不愿贸然行动。这种“保守式故障检测”的思路和大规模分布式系统的其他组件比如很多监控系统不太一样但非常值得学习。4.3 块复制与均衡后台默默干活的任务一旦NameNode确认某个节点挂掉它会启动两个后台任务副本复制任务把副本数低于阈值的块重新复制到其他健康节点恢复全部副本数。这个任务会尽量选择负载低的节点作为复制目标同时限制复制速度避免影响正常读写。块均衡任务Balancer如果集群里有节点磁盘使用率差异过大NameNode会调度均衡任务把数据从高负载节点搬移到低负载节点。这个均衡任务可以手动触发hdfs balancer -threshold 5其中5表示允许的磁盘使用率偏差百分比。实际运维中我见过不少这样的情况集群跑了大半年某台新加的机器磁盘几乎空的老机器却快要满了。原因就是新节点加入集群后默认不会自动转移存量数据必须手动跑Balancer。在规划容量和负载时这一项经常被忽略。5. 日常实操把hdfs dfs命令操作体系一次讲透5.1 命令格式入门既然热词里反复出现“hdfs-命令操作”这一节我就把日常使用频率最高的命令系统梳理一遍。HDFS命令有几种不同的入口常见的有hdfs dfs文件系统操作命令最常用hdfs fs老版本里的等价命令hadoop fs也是文件系统操作命令功能基本一样。现在大部分发行版推荐用hdfs dfs不过hadoop fs在很多老脚本里也还能用。看公司集群的习惯就行。基本格式是hdfs dfs -命令 [参数] 路径比如hdfs dfs -ls /data这条命令会列出/data目录下的所有文件和子目录。和Linux的ls -l很像信息包括权限、所有者、文件大小、副本数、修改时间等。注意HDFS的ls输出里文件大小前面有一列数字代表副本数比如3这是和本地文件系统不太一样的地方。5.2 高频命令速查表我把平时用下来的高频操作按场景分类整理成了一张表方便收藏和查阅场景命令示例说明查看目录hdfs dfs -ls /data列目录内容递归查看hdfs dfs -ls -R /data级联列出所有子目录创建目录hdfs dfs -mkdir -p /data/log/2024-p自动创建父目录上传文件hdfs dfs -put local.txt /data/标准上传上传文件hdfs dfs -copyFromLocal local.txt /data/功能同上语义更明确下载文件hdfs dfs -get /data/file.txt ./下载到本地当前目录下载文件hdfs dfs -copyToLocal /data/file.txt ./同上查看内容hdfs dfs -cat /data/test.txt查看文件内容查看前几行hdfs dfs -tail /data/test.txt查看文件末尾1KB查看块信息hdfs dfs -stat %b /data/test.txt显示块大小等信息删除文件hdfs dfs -rm /data/test.txt删除单个文件递归删除hdfs dfs -rm -r /data/old_dir删除目录及内容移动/改名hdfs dfs -mv /data/a.txt /data/b.txt移动或重命名复制hdfs dfs -cp /data/a.txt /backup/在HDFS内部复制修改权限hdfs dfs -chmod 755 /data/script.sh改权限修改属主hdfs dfs -chown hdfs:hadoop /data/file改owner和group检查目录占用hdfs dfs -du -h /data统计目录下文件大小汇总目录大小hdfs dfs -du -s -h /data只显示总计校验文件hdfs dfs -checksum /data/file查看CRC32校验和设置副本数hdfs dfs -setrep -R -w 3 /data/file修改副本数并等待完成其中-setrep可以说是运维里最有用的命令之一。比如你发现某个重要目录的副本数被误设为1可以用它一键改回3或者临时想把冷数据副本数降为1以节省存储空间也会用到它。5.3 容易踩坑的细节命令语法本身不难真正容易出错的是下面几个细节第一上传时路径是HDFS路径还是本地路径必须分清楚。-put前一个是本地路径后一个是HDFS路径-get正好反过来。我自己早期就吃过亏想下载文件结果写反了参数顺序系统直接报错还得重新敲。**第二-cat对大文件非常不友好。**如果你用hdfs dfs -cat读取一个几个GB的文件终端会瞬间被刷屏而且数据全部经过客户端传输极其耗费带宽。日常排查建议先用-tail看文件末尾或者配合管道只取前几行hdfs dfs -cat /data/huge.txt | head -n 20**第三删除操作默认不进回收站。**除非你在core-site.xml里配置了fs.trash.interval比如设为1440分钟否则hdfs dfs -rm是直接物理删除的没有恢复机会。生产环境建议一定配置回收站配置项可以这样设置property namefs.trash.interval/name value1440/value /property1440表示保留24小时超过时间后才会被真正清空。设置完后用-rm删除的文件会转移到/user/xxx/.Trash/目录下误删还能救回来。**第四-put上传大量小文件时性能极差。**一个Map任务处理一个小文件产生成百上千个小文件就会产生成百上千个Map任务调度开销比数据处理本身还贵。实际生产中如果一定要传大量小文件建议先打包成SequenceFile或者ORC格式再传。这个我在下一节还会细聊。6. 设计之外的真实战场部署、调参与避坑经验6.1 小文件问题HDFS最著名的“阿喀琉斯之踵”HDFS的块设计适合大文件这已经是共识了。但是“小文件问题”到底有多严重很多人其实没有直观感受。一个只有10KB的文件在HDFS上依然会占用一个128MB块的元数据位置。NameNode内存中每个文件/目录大约占用150字节左右的元数据不同版本略有差异而每个数据块大约占用150字节。如果一个集群的NameNode堆内存是64GB大约能管理多少个块呢简单算一下64GB内存留一部分给操作系统和JVM本身实际可用于元数据的大概40GB。40GB除以150字节约等于2.86亿个块。听起来很多但注意NameNode内存中的每个块只代表一份副本对应的条目。一个3副本的文件块在元数据里占的是3份空间。也就是说2.86亿不是能存储的块数而是副本条目的总数真实可存储的块数还要再除以副本数。所以如果生产环境里堆满了100KB级别的小文件NameNode内存很快告急整个集群的读写吞吐都会跟着下降。更重要的是客户端读一个小文件需要先问NameNode拿一次元数据再和DataNode建一次连接上千个小文件的读取就得上千次元数据请求NameNode直接变成性能瓶颈。处理办法我简单列一下文件合并把一段时间内产生的日志文件合并成一个大文件再上传。使用列式存储与文件合并工具比如用Hive里的concatenate命令合并小文件用Spark写入时设置coalesce或repartition控制输出文件数量。归档HDFS自带的har命令可以把多个小文件打包成一个归档文件整体减少NameNode内存占用。6.2 NameNode的单点问题设计上绕不过去的坎前面说了NameNode是整个系统的大脑大脑一旦宕机整个HDFS就不可用了。这正是HDFS最受诟病的设计弱点单点故障Single Point of Failure。在没有HA高可用的年代NameNode挂了只能手动恢复运维要做的第一件事就是去翻fsimage和edits日志把它们合并加载到内存里。这个过程短则几分钟长则半个多小时期间整个集群不可用。现在主流的Hadoop发行版基本都支持NameNode HA部署。核心思路是用两个NameNode组成一主一备共享同一个命名空间状态Active NameNode处理所有客户端请求写操作日志edits。Standby NameNode持续同步Active的日志状态随时准备接管。共享存储或JournalNode两边的桥梁负责把Active写入的edits同步到Standby。如果Active挂了Standby会在很短的时间内切换为Active整个集群基本无感知。这个设计就属于“用工程手段弥补架构短板”的典型做法。如果你是自己在搭建学习环境或小规模集群不建议一上来就搞HA配置复杂度会分散你对核心原理的注意力。先跑通单NameNode把文件读写、命令操作、副本机制摸熟了再上HA会顺畅得多。6.3 数据均衡与容灾规划生产环境里除了读写性能还需要关注两个很实际的点。第一集群扩容之后一定要跑Balancer。前面提过新节点不会自动接收存量数据。实际做法是hdfs balancer -threshold 10这里10表示节点磁盘使用率与集群平均值的偏差超过10%时才进行迁移。跑的时候建议在业务低峰期执行因为均衡任务会占用一定的带宽和磁盘IO。第二冷热数据分层。不是所有数据都需要3份副本。HDFS提供了存储策略Storage Policy可以把数据设置为COLD冷数据用存储效率更高的介质、WARM或者HOT。比如归档日志如果允许“丢了也能重新生成”可以把副本数调低或者把存储策略改掉节省下来的空间非常可观。6.4 性能调优的常用参数调优这件事没有一个万能配方但以下几个参数是我实际调过最多、收益最明显的参数默认值作用与调优建议dfs.blocksize128MB大文件建议调大到256MB或512MB减少NameNode元数据dfs.replication3非关键数据可以调为2或1但注意容错上限同时降低dfs.namenode.handler.count10高并发场景适当调大到100左右增加NameNode处理RPC请求的线程数dfs.datanode.handler.count10DataNode处理数据传输请求的线程数同样可增大dfs.replication.max10调-setrep时的上限io.file.buffer.size4096文件读写缓冲区大小调大到6553664KB能提升吞吐每次修改配置后都要记得hdfs dfsadmin -refreshNodes或者滚动重启相关进程让配置生效。最后分享一个比较隐蔽但很实用的小经验如果你发现某个目录写入速度远低于预期先去查一下该目录下的文件数是不是已经达到了数万个。小文件数量一旦上来格式化扫描、元数据缓存、任务调度都会集体变慢这不是某一个参数能救得回来的最好从源头控制文件数量。分布式文件系统的设计看着门槛很高但拆开来看要解决的问题其实很朴素大文件拆块、分散存储、冗余容错、并行读写。理解这些底层机制之后再去写hdfs dfs命令行你会发现自己不再像背命令而是在跟一台聪明的存储机器对话。你清楚它每一步在做什么为什么这样做也知道什么操作会对它造成压力、哪个环节最可能出问题。这种“看得透”的感觉才是让一个大数据从业者真正安心的东西。