一、概述

fsimage 始终是磁盘上的一个文件,不能实时跟 Namenode 内存中的数据结构保持同步,并且 fsimage 文件一般都很大,如果所有的更新操作都实时地写入 fsimage 文件,则会导致 Namenode 运行得十分缓慢,所以 HDFS 每过一段时间才更新一次 fsimage 文件。

为了避免两次持久化之间数据丢失的问题,又设计了 Edits log 编辑日志文件,HDFS 将新 fsimage 文件和上一个 fsimage 文件中进行的 Namenode 操作记录在 editlog (编辑日志) 文件中,editlog 是一个日志文件,文件中记录的是 HDFS 所有更改操作(文件创建,删除或修改)的日志,文件系统客户端执行的更改操作首先会被记录到 edits 文件中。

1.1. 文件格式

oev 是offline edits viewer 的缩写,该工具不需要hadoop集群处于运行状态。

1
$ hdfs oev -i edits_0000000000000000001-0000000000000044061 -o ~/edits.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<EDITS>
<RECORD>
<OPCODE>OP_UPDATE_BLOCKS</OPCODE>
<DATA>
<TXID>44061</TXID>
<PATH>/kylin4_metadata/kylin4/_sparder_logs/2022-06-09/application_1654693852181_0005/executor-1.log</PATH>
<BLOCK>
<BLOCK_ID>1073745202</BLOCK_ID>
<NUM_BYTES>3830</NUM_BYTES>
<GENSTAMP>4382</GENSTAMP>
</BLOCK>
<RPC_CLIENTID></RPC_CLIENTID>
<RPC_CALLID>-2</RPC_CALLID>
</DATA>
</RECORD>
...
</EDITS>

二、实现

在 HDFS 源码中,使用 FSEditLog 类来管理 editlog 文件。和 fsimage 文件不同,editlog 文件会随着 Namenode 的运行实时更新,所以 FSEditLog 类的实现依赖于底层的输入流和输出流,同时 FSEditLog 类还需要对外提供大量的log方法用于记录命名空间的修改操作。

2.1. transactionId 机制

HDFS的 editlog 文件可以存放在多种容器中,比如文件系统(FileJournalManager类管理)、共享 NFS(BackupJournalManager 类管理)、Bookkeeper( BookkeeperJournalManager 类管理)等。而管理这些不同容器内文件的方法也有很多种,目前 HDFS 采用的是基于 transactionId 的日志管理方法。

TransactionId 与客户端每次发起的 RPC 操作相关,当客户端发起一次 RPC 请求对 Namenode 的命名空间修改后,Namenode 就会在 editlog 中发起一个新的 transaction 用于记录这次操作,每个 transaction 用唯一的 transactionId 标识

2.2. FSEditLog 状态

FSEditLog 类被设计成一个状态机,用内部类 FSEditLog.State 描述。FSEditLog 有以下 5 个状态。

对于非 HA 机制的情况,FSEditLog 开始于 UNINITIALIZED 或者 CLOSED 状态。FSEditLog 初始化完成之后进入 BETWEEN_LOG_SEGMENTS 状态,表示前一个 segment 已经关闭,新的还没开始,日志已经做好准备了。当打开日志服务时,改变 FSEditLog 状态为IN_SEGMENT 状态,表示可以写 editlog 文件了。

对于 HA 机制的情况,FSEditLog 同样应该开始于 UNINITIALIZED 或者 CLOSED 状态,但在完成初始化后并不进入 BETWEEN_LOG_SEGMENTS 状态,而是进入OPEN_FOR_READING 状态

Namenode 启动时都是以 Standby 模式启动的,通过 DFSHAAdmin 发送命令把其中一个 Standby NameNode 转换成 Active Namenode

FSEditLog 的状态转移图如图所示

2.2.1. initJournalsForWrite()

initJournalsForWrite() 方法会将 FSEditLog 从 UNINITIALIZED 状态转换为 BETWEEN_LOG_SEGMENTS 状态。

1
2
3
4
5
6
7
// 在 FSNamesystem.startActiveServices 中被调用,只有 ActiveNameNode 才有写权限
public synchronized void initJournalsForWrite() {
// ...
// 初始化日志系统
initJournals(this.editsDirs);
state = State.BETWEEN_LOG_SEGMENTS;
}

initJournalsForWrite() 方法调用了 initJournals() 方法,initJournals() 方法会根据传入的 dirs 变量(保存的是 editlog 文件的存储位置,都是 URI) 初始化 journalSet 字段 (JournalManager 对象的集合)。初始化之后,FSEditLog 就可以调用 journalSet 对象的方法向多个日志存储位置写 editlog 文件。
JournalManager 类是负责在特定存储目录上持久化 editlog 文件的类,它的 format() 方法负责格式化底层存储,startLogSegment() 方法负责从指定事务 id 开始记录一个操作的段落 finalizeLogSegmemt() 方法负责完成指定事务 id 区间的写操作。

Namenode 可能将 editlog 文件持久化到不同类型的存储上,也就需要不同类型的 JournalManager 来管理,所以需要定义一个抽象的接口。

JoumalManager 有多个子类,普通的文件系统由 FileJoumalManager 类管理、共享 NFS 由BackupJoumalManager 类管理、Quorum 集群则由 QuorumJoumalManager 类管理。

2.2.2. initSharedJournalsForRead()

initSharedJournalsForRead() 方法是 FSEditLog 的 public 方法,用在 HA 情况下。调用这个方法会将 FSEditLog 从 UNINITIALIZED 状态转换为 OPEN_FOR_READING 状态。与initJounalsForWrite() 方法相同,initSharedJouralsForRead() 方法也调用了 initJournals()方法执行初始化操作,只不过 editlog 文件的存储位置不同,在 HA 的情况下,editlog 文件的存储目录为共享存储目录

2.2.3. openForWrite()

初始化 editlog 文件的输出流,并且打开第一个日志段落(log_segment)

截屏2021-04-29 下午4.29.23
  1. getLastWrittenTxId(): 查找到已经写到 editlog 日志文件中的最新 transactionId(如上图: 返回 31)
  2. journalSet.selectInputStreams(): 参数 segmentTxId,这个参数会作为这次操作的 transactionld,值为editlog 已经记录的最新的 transactionld 加 1(即: 31+1=32)。selectInputStreams() 方法会判断有没有一个以 segmentTxId(32) 开始的日志,如果没有则表示当前 transactionld 的值选择正确,可以
    打开新的 editlog 文件记录以 segmentTxId 开始的日志段落。如果方法找到了包含这个 transactionId 的 editlog 文件,则表示出现了两个日志 transactionld 交叉的情况,抛出异常。
  3. startLogSegment(): 开始记录 transactionld 为 32 的日志段落,新建 edits_inprogress_32 文件。同时将 FSEditlog 的状态转变为 IN_SEGMENT.
0900
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* Initialize the output stream for logging, opening the first
* log segment.
*/
synchronized void openForWrite() throws IOException {
Preconditions.checkState(state == State.BETWEEN_LOG_SEGMENTS,
"Bad state: %s", state);
//拿到输出流的最新事务ID+1
long segmentTxId = getLastWrittenTxId() + 1;
// Safety check: we should never start a segment if there are
// newer txids readable.
List<EditLogInputStream> streams = new ArrayList<EditLogInputStream>();
//生成读取文件的输入流, namenode 获取 FileJournalManager journalNode获取QuorumJournalManager
journalSet.selectInputStreams(streams, segmentTxId, true);
if (!streams.isEmpty()) {
String error = String.format("Cannot start writing at txid %s " +
"when there is a stream available for read: %s",
segmentTxId, streams.get(0));
IOUtils.cleanup(LOG, streams.toArray(new EditLogInputStream[0]));
throw new IllegalStateException(error);
}
/**
* 1:获取日志输出流(namenode写本地 和 远程的journalNode)
* 2:启用双缓冲刷数据
* */
startLogSegment(segmentTxId, true);
assert state == State.IN_SEGMENT : "Bad state: " + state;

详细看这个~《Hadoop源码学习-Namenode 元数据管理-FSEditLog》

三、EditLogOutputStream

FSEditLog 类会调用 FSEditLog.editLogStream 字段的 write() 方法在 editlog 文件中记录一个操作,数据会先被写入到 editlog 文件输出流的缓存中,然后 FSEditLog 类调用 editLogStream.flush() 方法将缓存中的数据同步到磁盘上。

FSEditLog 的 editLogStream 字段是 EditLogOutputStream 类型,EditLogOutputStream 类是一个抽象类,它定义了向持久化存储上写 editlog 文件的相关接口。
目前 Namenode 可以在多种类型的异构存储上保存 editlog 文件,例如普通文件系统、共享 NFS、 Bookkeeper 以及 Quorum 集群等,所以 EditLogOutputStream 定义了多个子类来向不同存储系统上的 editlog 文件中写入数据。

EditLogFileOutputStream 抽象了本地文件系统上 editlog 文件的输出流,BookKeeperEditLogOutputStream 抽象了 BookKeeper 系统上 editlog 文件的输出流,QuorumOutputStream 抽象了 Quorum 集群上 editlog 文件的输出流。同时由于 Namenode 可以同时向多个不同的存储上写入 editlog 文件,所以 EditLogOutputStream 还定义了子类 JournalSetOutputStream 执行聚合的写入操作。

四、总结

FSEditLog 总体结构如下图: