很多刚接触Hadoop的人,看到集群里除了NameNode还有一个SecondaryNameNode,第一反应都会跟我当初一样——以为它就是NameNode的“备胎”,主节点挂了它能顶上。实际上,这个理解差得不是一点半点。SecondaryNameNode在英语里单独拎出来还能叫CheckpointNode,职责也直接写在名字里:它是负责定期对NameNode的元数据做“检查点合并”的辅助进程,而不是一份实时备份。今天我就从NameNode为什么必须持久化聊起,把FSImage、EditLog、检查点流程、参数调优和几个我踩过的坑一次说清楚,希望能帮你在生产环境里少走弯路。
1. 为什么NameNode必须“双重写盘”:FSImage和EditLog的分工
1.1 内存元数据和落盘文件的关系
NameNode在HDFS里管的是文件系统的“索引”,也就是元数据:路径、目录树、文件属性、每个文件包含哪些Block、Block存在哪些DataNode、副本数、权限这些。为了给所有读写请求提供秒速响应,NameNode把这些信息全部放在内存里,一个名字都不落。
但纯放内存显然不靠谱,机器一断电、一宕机,索引全没了。所以NameNode要持久化。持久化不是把整个内存快照实时刷进磁盘,那样成本太高,而是拆成两份文件来记账:
- FSImage:某一个时间点上完整的命名空间快照,相当于给整个文件系统拍了一张照片。
- EditLog:从上次快照之后,所有写操作的流水账,每个文件创建、删除、目录修改、副本变更都会以事务的形式追加进去。
这两份文件配合起来,NameNode才能既保证性能,又保证数据不丢。我的理解是,这就像你每月拍一张全家福(FSImage),平时日常里每花一笔钱都记在流水账上(EditLog)。到月底把流水账和全家福合在一起,生成一张新的全家福,再把旧流水账换掉。
1.2 一次写请求在NameNode内部的落盘顺序
客户端向NameNode发请求创建文件时,NameNode不是先改内存,而是先做“写前日志”。大致顺序是这样:
- 客户端调用create / mkdir / delete / rename 之类操作。
- NameNode把对应元数据变更以事务形式追加到当前活跃的EditLog文件中,并执行
fsync让日志真正落到磁盘。 - 日志落盘成功后,NameNode才在内存里更新元数据树。
- 然后才向客户端返回成功。
为什么要坚持“日志先落盘,内存后更新”?这就是典型的Write-Ahead Log思路。万一写日志之后就宕机,重启时还能靠日志把内存状态重放出来;可如果内存先改了,日志还没写,这步操作就彻底丢了。反过来,日志写成功但内存没改的情况,重启后重放日志会把内存补上。所以日志的可靠性优先级最高。
在NameNode的数据目录dfs.namenode.name.dir对应的current/目录下,你会看到一串文件:
$ ls -lh $dfs_namenode_name_dir/current total 123456 -rw------- 1 hadoop hadoop 12345 2024-01-11 10:00 fsimage_0000000000000000123 -rw------- 1 hadoop hadoop 128 2024-01-11 10:00 fsimage_0000000000000000123.md5 -rw------- 1 hadoop hadoop 34567 2024-01-11 10:05 edits_0000000000000000123-0000000000000000400 -rw------- 1 hadoop hadoop 45678 2024-01-11 10:05 edits_0000000000000000401-0000000000000000780 -rw------- 1 hadoop hadoop 2048 2024-01-11 10:06 edits_inprogress_0000000000000000781文件名的数字是事务ID(Transaction ID,简称txid)。fsimage_...表示检查点完成后某个txid对应的完整快照;edits_...是一段连续的增量日志;后面带edits_inprogress的就是当前正在写入的日志文件。NameNode启动时会从最新FSImage出发,重放之后所有edits文件,把所有操作恢复到内存。看起来很简单,但如果长时间不合并,edits越积越多,启动和故障恢复就会越来越慢,慢到让你怀疑人生。
2. SecondaryNameNode不是热备:检查点机制的前因后果
2.1 合并操作不能放在NameNode本身
edits无限增长总得治理。最直接的办法是定期把FSImage和EditLog“合体”,产出新的FSImage,旧日志就可以扔掉。问题是,这个合并操作如果放在NameNode自己身上,会有几个麻烦:
- 合并要读取完整FSImage到内存,再挨个重放所有edits,内存和CPU瞬间飙升。
- 合并期间如果正是业务高峰期,NameNode的RPC响应会明显变慢。
- 一旦合并到一半出问题,可能影响正在提供服务的元数据进程。
于是Hadoop早期版本引入了一个专门角色,就是SecondaryNameNode。它从NameNode侧拉取FSImage和EditLog,在自己本地完成合并,再把新的FSImage交给NameNode。NameNode只需要在合并完成时换一个文件,大部分压力都在Secondary那边。顺便说一句,SecondaryNameNode的内存需求理论上和NameNode一个级别,因为要把元数据整个加载到自己内存里做重放。
2.2 “Secondary”这个名字害了不少人
我见过不少刚上手Hadoop的同事,默认把SecondaryNameNode当成“第二NameNode”。他们觉得NameNode挂了,启动SecondaryNameNode就能接着服务。真这么干会发现:DataNode根本不认识它,客户端也不认它,进程看起来叫SecondaryNameNode,实际上完全不接受正常的元数据请求。
准确讲,SecondaryNameNode只做检查点,而且检查点有明确间隔。它手里那份“元数据”最多只是上一次检查点时刻的,跟NameNode的最新内存状态必然有一段时间差。这也就决定了:它不能作为高可用方案。生产环境想要NameNode不挂服务,得走真正的HA架构,通常就是多个NameNode共享编辑日志,或布置JournalNode,让Standby节点实时接管。而不是指望SecondaryNameNode。
2.3 检查点机制的价值到底是什么
尽管它不承担高可用,但检查点机制仍然非常关键。设想没有检查点:NameNode跑了好几个月,edits文件上百GB。某天机器硬件故障,重启NameNode时要从最老的FSImage开始,一个事务一个事务地重放,可能要花几小时才能把元数据恢复到内存,业务完全停摆。有了定期检查点,恢复时只需要加载最近一次的新FSImage,再重放检查点之后少量的edits,时间可以从几小时压缩到几分钟。
所以,SecondaryNameNode更像是“元数据的整理工”,而不是“备胎”。
3. 一次检查点的完整时间线:从触发到新FSImage落盘
3.1 触发条件:时间和事务数,谁先到谁触发
SecondaryNameNode不是不停地在合并,而是靠两个条件来触发检查点:
fs.checkpoint.period:默认3600秒,也就是每隔1小时检查一次。fs.checkpoint.size:默认是1000000,表示NameNode的edits事务数达到100万时触发。
两个条件是“或”的关系,哪一个先满足都会发起一次新的检查点。有些Hadoop版本还有一个额外参数叫fs.checkpoint.check.period,默认60秒,用来控制SecondaryNameNode每隔多久去NameNode查询一次最新的事务数。也就是说,不是精确到秒立刻触发,而是最多会延迟一个查询周期。
在实际集群里,我建议还是以时间为主,事务数作为上限保护。比如写入量大的集群,可能不到一小时就攒了100万事务,那检查点就会变成十几分钟一次,合并本身也是比较吃资源的事,太频繁同样不划算。要结合自己集群的写入量去权衡。
3.2 合并的六步流程
一次完整的检查点流程可以拆成这么几步:
| 步骤 | 执行者 | 主要动作 | 产物 |
|---|---|---|---|
| 1 | SecondaryNameNode | 触发后向NameNode发起“滚动编辑日志”请求 | 新edits_inprogress文件 |
| 2 | NameNode | 停止当前edits_inprogress写入,新建一个空的in-progress文件,后续事务写进新文件 | 已完成的旧edits段 |
| 3 | SecondaryNameNode | 从NameNode下载最新FSImage和所有已完成的edits段 | 本地临时目录文件 |
| 4 | SecondaryNameNode | 在本进程内加载FSImage,重放edits,合并成新FSImage | 新FSImage |
| 5 | SecondaryNameNode | 把新FSImage传回NameNode | 新FSImage网络传输 |
| 6 | NameNode | 保存新FSImage,删除旧FSImage和已完成edits段,保留新in-progress | 新FSImage替换完成 |
第2步是整个流程里最容易被忽视的同步点。取名“滚动编辑日志”(roll edit log),是因为NameNode必须先把当前还在写的日志文件“封口”,然后新建一个新的edits_inprogress继续接收请求。SecondaryNameNode只有在旧日志被完整封口之后,才能保证拉下来的edits是完整的,不会出现一边写一边被拷贝的半截文件。
第4步里,SecondaryNameNode会在内存里建一棵和NameNode几乎一样的元数据树。这也是为什么它的内存不能省。如果FSImage比较大,合并期间SecondaryNameNode的压力不小,还伴随大量磁盘IO。所以真正生产环境配置时,最好把SecondaryNameNode部署在一台专门机器上,别和数据节点挤一起。
3.3 合并期间NameNode会不会被卡住
合并的第4步完全在Secondary内存里进行,NameNode自身不参与。从第1步滚动日志之后,NameNode继续处理客户端请求,把所有新事务写到新edits_inprogress里。等第6步替换完成,NameNode把这些新事务保留下来,下次检查点继续合并。所以从架构设计的角度看,检查点合并对NameNode的读写操作是无阻塞的,主要影响是网络带宽和NameNode本地磁盘在传输/替换时会有短暂的IO尖峰。
从运维角度,我需要看一眼NameNode的日志确认检查点是否正常。正常日志一般能看到类似下面的内容:
INFO org.apache.hadoop.hdfs.server.namenode.FSNamesystem: Calling saveNamespace... INFO org.apache.hadoop.hdfs.server.namenode.SecondaryNameNode: Checkpointing... INFO org.apache.hadoop.hdfs.server.namenode.SecondaryNameNode: Uploaded fsimage...如果长时间看不到这类日志,就说明检查点可能没有按预期发生。
4. 生产配置里值得留意的三个参数和一个顺序问题
4.1 到底调哪些参数
通常我只动这三个参数,关系如下:
| 参数 | 默认值 | 作用 |
|---|---|---|
fs.checkpoint.period | 3600 | 距离上次检查点的时间间隔,单位秒 |
fs.checkpoint.size | 1000000 | NameNode累积未合并的事务数阈值 |
dfs.namenode.checkpoint.dir | file:///tmp/hadoop/dfs/namesecondary | SecondaryNameNode保存收下来的FSImage和edits的目录 |
还有一个容易忽略的fs.checkpoint.check.period,默认60秒,负责SecondaryNameNode每隔多久主动查询一次NameNode的最新事务数。如果你把检查点周期调得很短,比如300秒,建议把这个检查周期也调成比如30秒,否则触发精度会受影响。
配置示例:
<configuration> <property> <name>fs.checkpoint.period</name> <value>1800</value> </property> <property> <name>fs.checkpoint.size</name> <value>500000</value> </property> <property> <name>fs.checkpoint.check.period</name> <value>30</value> </property> <property> <name>dfs.namenode.checkpoint.dir</name> <value>file:///data/hdfs/namesecondary</value> </property> </configuration>这里要注意:dfs.namenode.checkpoint.dir不是配在NameNode宿主机上的,而是SecondaryNameNode启动时会读取的路径。如果你在同一个节点部署SecondaryNameNode,路径可以本地磁盘;如果SecondaryNameNode独立节点,也要确保这个目录所在物理磁盘容量足够。它至少需要能放下“一份FSImage + 一份或多份Edits”,并且建议和NameNode的dfs.namenode.name.dir不共用同一块盘。
4.2 调整周期的逻辑
合并太频繁,Secondary和NameNode都会有额外IO开销;合并太少,edits增长过快,恢复时间拉长。这里的平衡点是:先确定一个你能接受的NameNode重启恢复时间,再倒推检查点周期。
假设你的NameNode重启后重放edits的速度是每秒2万事务,你希望恢复上限控制在3分钟以内,那就是约360万事务。那么就可以把fs.checkpoint.size设置在200万到300万之间,同时fs.checkpoint.period保持1小时。如果高峰期每小时新增200万事务,那么实际触发就会靠事务数先触发。用hadoop dfsadmin -report或NameNode Web UI查看Pending DataNode Rebuild类似指标?更直接的,还是查看NameNode日志里每次检查点之间的事务ID差值,估算出单位时间的edits增长速度,然后再决定参数。
4.3 一个容易出错的顺序问题:先启动谁
这里说的顺序问题不是指Hadoop进程启动顺序,而是指“第一次合并”的时机。SecondaryNameNode只有在NameNode已经成功启动并正常进入服务状态后才会开始工作。如果你启动集群时Sequence有点乱,比如先启SecondaryNameNode、后启NameNode,SecondaryNameNode可能启动时报错,连接不上NameNode。遇到这种情况不用慌,等NameNode起来后重启一次SecondaryNameNode即可。
另一个顺序细节是:NameNode在启动过程中本身也会把旧的FSImage加载、重放edits,然后才会对外开放。此时SecondaryNameNode如果也同时发起检查点,可能产生重复合并,但对最终数据一致性影响不大。真正的风险是NameNode在启动还没完成时,SecondaryNameNode就拉取FSImage容易拿到半写状态的文件。所以生产上我会把SecondaryNameNode的自动启动放在NameNode进程起来之后,用一个简单的start脚本顺序控制,这也是很多初始化脚本容易漏的点。
5. 名不副实导致的两个典型事故:以及怎么抢救元数据
5.1 事故一:把SecondaryNameNode当备机直接顶上
某实验环境里有一次NameNode进程起不来了,当时有人直接把SecondaryNameNode的安装目录复制了一份,改了配置打算让它“接管”。结果启动后,DataNode陆续来注册,可进程一直停留在安全模式,客户端连上后文件系统目录基本是空的,而且写操作全失败。最后查清原因:SecondaryNameNode根本没有完整的命名空间,也不会接管状态;它启动后只会把自己当作一个普通NameNode去加载本地旧检查点文件,而不是从原NameNode的edits里追数据。
正确的抢救思路是:先从SecondaryNameNode的checkpoint目录里拿到最新的FSImage,再结合原NameNode数据目录里残留的edits,把它们放到NameNode的dfs.namenode.name.dir下,然后再启动NameNode。启动时NameNode会自动重放edits,把最近检查点之后的数据尽量补回来。虽然会有一段时间窗口的数据丢失,但总比没有强。
具体操作步骤大致是这样的:
# 假设SecondaryNameNode保存路径为/data/hdfs/namesecondary/current # NameNode数据目录为/data/hdfs/nn/current cp /data/hdfs/namesecondary/current/fsimage_* /data/hdfs/nn/current/ cp /data/hdfs/nn/current/edits_inprogress_* /data/hdfs/nn/current/拷贝前最好备份原目录,并且把dfs.namenode.name.dir里的旧文件清理一下,只留下你要恢复用的FSImage和edits。再启动NameNode,日志里会看到“Loading 1 edits file”之类的重放记录。
5.2 事故二:长时间没做检查点,恢复时被edits拖垮
另一类更常见的事故是,集群部署时根本没配SecondaryNameNode,或者配了但没监控。半年之后NameNode元数据盘故障,重启时才发现最近的FSImage是三个月前的,后面几百GB的edits需要全部重放。我遇到过一个小几千文件的测试集群,edits达到几个GB,重放就花了快一小时;生产环境如果文件数量上亿,这个时间可能以小时甚至天计。
抢救的思路还是想办法合并。如果原NameNode磁盘没有彻底损坏,那么重启后别急着让它对外服务,先在安全模式下等待“saveNamespace”成功。这里可以手动执行一次:
$ hdfs dfsadmin -saveNamespace这个命令会强制NameNode自己执行一次命名空间保存,把当前内存状态写成新的FSImage。但前提是NameNode已经成功启动并在安全模式下能完成元数据加载。如果edits损坏,NameNode可能启动失败,那就需要做日志修复,比如用hdfs namenode -recover尝试恢复。但恢复命令通常只用于损坏edits尾部的情况,能救多少算多少。
5.3 所以SecondaryNameNode到底该不该部署
我的看法是:在没上HA的常规Hadoop 2.x/3.x非高可用集群里,还是应该部署SecondaryNameNode。它不是为了“备份”,而是为了让NameNode能在可接受的时间内恢复。它提供的最大保障是:当你系统真的坏了,至少有一个不算太旧的FSImage,加上NameNode本地残留的edits,可以把恢复时间从“小时级”降到“分钟级”。
如果集群已经上了HA(NameNode+Standby+JournalNode),SecondaryNameNode就不再必要了,因为Standby本身会承担检查点合并职责,集群里再放一个SecondaryNameNode反而是浪费资源,还容易跟Standby的行为重叠。多数发行版在HA模式都会默认禁用SecondaryNameNode进程。
6. 排查检查点故障的思路:日志、权限和空间
6.1 先确认SecondaryNameNode是否真的在工作
很多集群配置好SecondaryNameNode就不再管了,等发现问题已经是几个月后。第一件事就是看进程和目录:
$ jps 12345 NameNode 12347 DataNode 12346 SecondaryNameNode如果有SecondaryNameNode进程,再检查它的最近日志输出。以默认日志路径为例:
$ tail -50 logs/hadoop-hadoop-secondarynamenode-*.log能看到“Checkpointing...”和“Uploaded fsimage”就是正常的。如果一天都没看到这类关键词,说明检查点可能没有按预期触发。再检查NameNode的edits文件是持续增长还是比较旧,也能判断问题。
6.2 常见报错和处理手段
我整理了一张排查表,覆盖了大多数情况:
| 现象 | 常见原因 | 处理办法 |
|---|---|---|
日志提示Failed to retrieve fsimage from NameNode | SecondaryNameNode和NameNode网络异常、HTTP端口不通 | telnet <nn-host> 50070测试连通性,检查防火墙和安全组 |
日志提示Checkpoint timed out | NameNode太忙或网络传输慢,导致一次检查点超过超时时间 | 增加传输带宽;错开检查点与业务高峰;调大超时参数 |
| SecondaryNameNode进程跑着跑着就挂了 | dfs.namenode.checkpoint.dir空间不足或内存被系统OOM Kill | 检查磁盘空间;检查SecondaryNameNode JVM内存,必要时配置和NameNode同等大小的堆内存 |
| NameNode侧文件替换失败 | dfs.namenode.name.dir空间不足、目录权限不对 | 清理NameNode磁盘空间,检查current目录写入权限 |
| 检查点正常但edits依旧膨胀 | 事务数触发阈值太高或NameNode写入量过大 | 降低fs.checkpoint.size或缩短fs.checkpoint.period |
6.3 一个真实场景的排查过程
有一次我接到一个报障,说NameNode数据目录里的edits文件已经好几天没有滚动,SecondaryNameNode日志里反复出现:
INFO checkpoint.Checkpointer: Checkpointing... INFO checkpoint.Checkpointer: Retrieving fsimage from NameNode WARN checkpoint.Checkpointer: Exception in doCheckpoint() java.net.SocketTimeoutException: Read timed out先怀疑网络丢包,ping了一下NameNode和SecondaryNameNode之间延迟正常。再看了下NameNode HTTP端口,没问题。最后用df -h一看,SecondaryNameNode的checkpoint.dir所在磁盘已经100%被写满。每次它把FSImage下载下来,磁盘就撑不住,合并写到一半就断开。把磁盘清理出空间后,手动清掉旧文件,再重启SecondaryNameNode,检查点立刻恢复正常。
我的经验是:检查点问题里,磁盘空间是头号杀手,其次才是网络和权限。最好把dfs.namenode.checkpoint.dir和NameNode数据目录、DataNode数据目录放到不同的物理磁盘。否则NameNode大FSImage传输过来时,很容易把原先就占满的磁盘压垮。
6.4 把检查点纳入日常监控
最后说一项常规但重要的工作:把检查点状态做成监控指标。最简单的办法是写一个定时脚本,检查NameNode当前校验目录下最新edits_inprogress文件的事务ID和最新fsimage的事务ID差值。如果差值超过你配置的fs.checkpoint.size好几倍,说明检查点可能长时间没执行,应当立刻报警。
另一个更简单的信号是看SecondaryNameNode进程最近一次fsimage文件的时间戳。超过一个半小时没有生成新的fsimage_*,就说明检查点频率低于预期,该查日志了。把这两样纳入监控,至少能把“NameNode恢复要等半天”这种暗雷提前排除掉。
根据我个人维护多套非HA Hadoop集群的经验,我每次部署完都会先手动观察一轮检查点能否在20分钟内正常完成,再设置自动启动和监控。NameNode和SecondaryNameNode这套机制说复杂也复杂,但核心就一句话:FSImage负责“定妆照”,EditLog负责“账本”,SecondaryNameNode负责定期把两者揉成一张新照片。把它和“高可用备份”区分清楚,生产上就不会犯方向性错误。