Editlogs文件主要是用来保存存储客户端对hdfs文件系统的修改更新等操作,为什么需要额外的editlogs文件来保存修改操作是因为:如果实时的将内存中的hdfs文件系统元数据信息保存同步到fsimage文件中,将会非常消耗资源且造成NameNode运行缓慢,所有NameNode会将客户端针对命名空间的修改操作先保存在编辑日志editlogs中;然后定期合并fsimage和editlogs文件。
所以在针对editlog文件的记录过程中,需要一种机制来标识每一次的客户端修改请求;在editlogs源码中就是基于transactionid的日志管理方式来进行的;当客户端发起一次rpc请求对NameNode命名空间修改后,NN就会在editlogs中发起一个新的transactionid来记录此次的修改操作;
在Hadoop-2.X的源码中;其对应的实现类为:FSEditLog;其主要的作用为:
- 生成新的transactionid;
- 保存每次的修改请求;
接下来先来看看transactionid是什么,以及在源码中是如何实现的:
当客户端每次发起rpc请求操作对NameNode的命名空间修改后,NameNode就会在editlog文件中发起一个新的transactionid用于记录此次的操作,每个transactionid都会用一个唯一的transactionid标识;其在namenode的元数据文件管理目录下呈现形式如下:
在文件中我们可以看到:
edits_start transactionid-end transactionid:edits文件就是我们所说的editlog文件,该文件保存记录了从start transactionid-end transactionid之间的所有客户端对NameNode命名空间的修改操作记录。比如edits_0000000000000242452-0000000000000242454其就记录了transactionid:edits在242452和242454之间的所有修改操作;
edits_inprogress_start transactionid:保存了当前正在进行处理中的editlogs文件;其会记录从start transactionid开始的所有新的修改操作;直到hdfs flush刷写重置该文件;
fsimage_end transactionid:此处记录了end transactionid操作前的hdfs文件系统的命名空间元数据信息镜像;后续的fsimage和editlog文件合并也就是从此文件开始出发;
seen_txid:这个文件保存了上一个检查点(ckp合并fsimage和editlog文件)时的最新的事务id;
transactionid在源码中的实现也就是一个long型的txid变量,其会在每次记录一个transaction是进行调用,简单的进行++操作;
// a monotonically increasing counter that represents transactionIds.
private long txid = 0;
private long beginTransaction() {
assert Thread.holdsLock(this);
// get a new transactionId
txid++;
//
// record the transactionId when new data was written to the edits log
//
TransactionId id = myTransactionId.get();
id.txid = txid;
return now();
}
在记录修改操作到editlogs文件过程中,其主要日志同步过程为以下流程,其调用源码如下:
- 记录当前的修改操作到FSEditLogOp中;
- 调用FSEditLog.logEdit()的函数方法进行当前操作写入到输出流的缓冲区中;
- 调用FSEditLog.logSync()同步操作;将当前缓冲区中的记录同步持久化存储到editlog磁盘文件中;
/**
* Write an operation to the edit log. Do not sync to persistent
* store yet.
*/
void logEdit(final FSEditLogOp op) {
synchronized (this) {
assert isOpenForWrite() :
"bad state: " + state;
// wait if an automatic sync is scheduled
// 自动同步开启,等待自动同步完成
waitIfAutoSyncScheduled();
// 开启一个新的transactionid
long start = beginTransaction();
op.setTransactionId(txid);
try {
// 使用editlogStream流写入当前的op操作
editLogStream.write(op);
} catch (IOException ex) {
// All journals failed, it is handled in logSync.
}
endTransaction(start);
// check if it is time to schedule an automatic sync
if (!shouldForceSync()) {
return;
}
isAutoSyncScheduled = true;
}
// sync buffered edit log entries to persistent store
// 同步当前写入的操作,持久化到磁盘
logSync();
}
针对实际的hadoop fs -mkdir xxx操作来进行分析;我们主要看一下editLogStream.write(op);写入操作以及最后的logSync()同步操作;在mkdir操作中,其会使用一个FSEditLogOp类的父类MkdirOp类来记录此次的操作;首先其会记录此次操作的操作目录Inode的id,以及对应的hdfs文件路径、权限等等信息;之后便调用logEdit()方法进行操作的记录存储;在editLogStream.write(op)操作中,可以看到其会使用EditLogOutputStream类来进行不同存储文件的写入操作;其写入的存储可以是普通的文件系统、共享NFS以及Quorum集群等等;其子类继承图如下:
我们以最本地文件存储为例进行分析:在EditLogFileOutputStream实现类中;数据首先会被写入到输出流的缓冲区中,当显示调用flush()刷写操作时,数据才会从缓冲区中同步存储到editlogs文件中;在EditLogFileOutputStream的缓冲区中,其会构造一个EditsDoubleBuffer类来进行数据的缓存;该类是一个具有双Buffer缓存区结构的,其会将写入的操作数据放置到其中的写缓冲区;而此时的另一块同步缓冲区则可能正在进行上一次缓存数据同步到磁盘的同步操作;这样的设计可以保证输出流在进行磁盘同步的操作时,不影响操作数据的写入;其所对应的源码实现为:
public class EditsDoubleBuffer {
private TxnBuffer bufCurrent; // current buffer for writing
private TxnBuffer bufReady; // buffer ready for flushing
private final int initBufferSize;
public void writeOp(FSEditLogOp op) throws IOException {
bufCurrent.writeOp(op);
}
}
当调用editLogStream.write(op)操作时,其只是简单的将数据写入到当前的写缓存区bufCurrent中;在输出流准备将缓冲数据同步到磁盘上时,其会调用logSync()函数进行
- editLogStream.setReadyToFlush();
- editLogStream.flush();
其对应的源码如下:
public void logSync() {
long syncStart = 0;
// Fetch the transactionId of this thread.
long mytxid = myTransactionId.get().txid;
boolean sync = false;
try {
EditLogOutputStream logStream = null;
synchronized (this) {
try {
printStatistics(false);
// if somebody is already syncing, then wait
// 根据当前的txid来进行同步判断;如果有线程正在同步,则进行等待wait
while (mytxid > synctxid && isSyncRunning) {
try {
wait(1000);
} catch (InterruptedException ie) {
}
}
//
// If this transaction was already flushed, then nothing to do
//
if (mytxid <= synctxid) {
numTransactionsBatchedInSync++;
if (metrics != null) {
// Metrics is non-null only when used inside name node
metrics.incrTransactionsBatchedInSync();
}
return;
}
// now, this thread will do the sync
// 开始同步操作,先进行同步sync状态置位
syncStart = txid;
isSyncRunning = true;
sync = true;
// swap buffers
// 同步前的准备操作;先交换两个缓冲区
try {
if (journalSet.isEmpty()) {
throw new IOException("No journals available to flush");
}
editLogStream.setReadyToFlush();
} catch (IOException e) {
final String msg =
"Could not sync enough journals to persistent storage " +
"due to " + e.getMessage() + ". " +
"Unsynced transactions: " + (txid - synctxid);
LOG.fatal(msg, new Exception());
synchronized(journalSetLock) {
IOUtils.cleanup(LOG, journalSet);
}
terminate(1, msg);
}
} finally {
// Prevent RuntimeException from blocking other log edit write
doneWithAutoSyncScheduling();
}
//editLogStream may become null,
//so store a local variable for flush.
logStream = editLogStream;
}
// do the sync
long start = now();
try {
if (logStream != null) {
// 进行对应的同步操作
logStream.flush();
}
} catch (IOException ex) {
synchronized (this) {
final String msg =
"Could not sync enough journals to persistent storage. "
+ "Unsynced transactions: " + (txid - synctxid);
LOG.fatal(msg, new Exception());
synchronized(journalSetLock) {
IOUtils.cleanup(LOG, journalSet);
}
terminate(1, msg);
}
}
long elapsed = now() - start;
if (metrics != null) { // Metrics non-null only when used inside name node
metrics.addSync(elapsed);
}
} finally {
// Prevent RuntimeException from blocking other log edit sync
synchronized (this) {
if (sync) {
// 将已同步的txid保存赋值记录
synctxid = syncStart;
isSyncRunning = false;
}
this.notifyAll();
}
}
}
对于EditLogFileOutputStream中的双buffer缓冲结构;在setReadyToFlush方法中;其会简单的交换写缓存区与同步缓存区;也就是将当前已经写入op操作的缓冲区变成同步缓冲区,将同步缓冲区变为写缓冲区;
public void setReadyToFlush() {
assert isFlushed() : "previous data not flushed yet";
TxnBuffer tmp = bufReady;
bufReady = bufCurrent;
bufCurrent = tmp;
}
之后便会调用editLogStream.flush()方法进行同步缓冲区(当前已写入对应的op操作)的同步操作;将缓冲区中的数据持久化同步到editlog文件当中;并重置清空同步缓冲区,等待同步缓冲区与写缓存区进行下一次同步操作中的缓冲区交换:
public void flushTo(OutputStream out) throws IOException {
bufReady.writeTo(out); // write data to file
bufReady.reset(); // erase all data in the buffer
}
在editlogs文件的操作记录过程中,其使用了双缓冲区的结构来分离客户端修改的同步操作与写入操作;这种方式使得日志记录与刷新缓存区数据可以并发的执行;大大的提高了多线程记录editlog的并发性;最后来看一下实际editlog文件当中记录的数据内容;fsimage和editlog文件都可以使用oev(offline edits viewer[离线edits查看器])来进行查看;其在元数据文件目录下执行 hdfs oev -i edit文件 -o edits.xml;其editlogs文件内容为:
<?xml version="1.0" encoding="UTF-8"?>
<EDITS>
<EDITS_VERSION>-60</EDITS_VERSION>
<RECORD>
<OPCODE>OP_START_LOG_SEGMENT</OPCODE>
<DATA>
<TXID>242833</TXID>
</DATA>
</RECORD>
<RECORD>
<OPCODE>OP_ADD</OPCODE>
<DATA>
<TXID>242834</TXID>
<LENGTH>0</LENGTH>
<INODEID>80547</INODEID>
<PATH>/job/rmstore/FSRMStateRoot/RMDTSecretManagerRoot/DelegationKey_7.tmp</PATH>
<REPLICATION>1</REPLICATION>
<MTIME>1595990011855</MTIME>
<ATIME>1595990011855</ATIME>
<BLOCKSIZE>134217728</BLOCKSIZE>
<CLIENT_NAME>DFSClient_NONMAPREDUCE_-1914480089_1</CLIENT_NAME>
<CLIENT_MACHINE>192.168.100.50</CLIENT_MACHINE>
<OVERWRITE>true</OVERWRITE>
<PERMISSION_STATUS>
<USERNAME>hadoop</USERNAME>
<GROUPNAME>supergroup</GROUPNAME>
<MODE>420</MODE>
</PERMISSION_STATUS>
<RPC_CLIENTID>76212f08-f755-418c-9350-3d1106d35e58</RPC_CLIENTID>
<RPC_CALLID>31</RPC_CALLID>
</DATA>
</RECORD>
<RECORD>
<OPCODE>OP_ALLOCATE_BLOCK_ID</OPCODE>
<DATA>
<TXID>242835</TXID>
<BLOCK_ID>1073770792</BLOCK_ID>
</DATA>
</RECORD>
<RECORD>
<OPCODE>OP_SET_GENSTAMP_V2</OPCODE>
<DATA>
<TXID>242836</TXID>
<GENSTAMPV2>30017</GENSTAMPV2>
</DATA>
</RECORD>
<RECORD>
<OPCODE>OP_ADD_BLOCK</OPCODE>
<DATA>
<TXID>242837</TXID>
<PATH>/job/rmstore/FSRMStateRoot/RMDTSecretManagerRoot/DelegationKey_7.tmp</PATH>
<BLOCK>
<BLOCK_ID>1073770792</BLOCK_ID>
<NUM_BYTES>0</NUM_BYTES>
<GENSTAMP>30017</GENSTAMP>
</BLOCK>
<RPC_CLIENTID></RPC_CLIENTID>
<RPC_CALLID>-2</RPC_CALLID>
</DATA>
</RECORD>
</EDITS>