继续为分析org.apache.hadoop.hdfs.server.namenode.FSNamesystem类做准备,这里分析与FSEditLog相关的几个类,当然,FSEditLog类才是核心的。
- FSEditLog.EditLogFileOutputStream内部静态类
该类是定义在org.apache.hadoop.hdfs.server.namenode.FSEditLog类内部的静态类,表示将EditLog日志通过打开一个该输出流实例写入到本地磁盘。该输出流类的继承层次结构如下所示:
- ◦java.io.OutputStream(implements java.io.Closeable, java.io.Flushable)
- ◦org.apache.hadoop.hdfs.server.namenode.EditLogOutputStream
- ◦org.apache.hadoop.hdfs.server.namenode.FSEditLog.EditLogFileOutputStream
首先,看EditLogOutputStream这个抽象类的定义。
EditLogOutputStream类是一个用来支持将EditLog日志文件中内容持久化(写入)到存储目录中的通用抽象类。该抽象类定义了两个统计变量,如下所示:
- private long numSync; // 对EditLog日志文件执行同步操作(同步写入磁盘)的次数
- private long totalTimeSync; // 同步EditLog日志文件到磁盘,所花费的总累加时间
该抽象类中实现的方法都是与更新统计变量相关的,如下所示:
- /**
- * 将数据持久化到存储目录,保持数据同步一致,同时刷新统计数据
- */
- public void flush() throws IOException {
- numSync++; // 执行该方法一次,则同步数据统计变量加1
- long start = FSNamesystem.now(); // 此次开始同步的时刻(用来计算此次同步所花费的时间,记入对应统计变量中)
- flushAndSync(); // 调用刷新与执行同步的操作
- long end = FSNamesystem.now(); // 此次同步结束时间
- totalTimeSync += (end - start); // 计算此次同步花费时间,累加到统计变量totalTimeSync上
- }
- /**
- * 获取执行flushAndSync()同步方法花费的总时间
- */
- long getTotalSyncTime() {
- return totalTimeSync;
- }
- /**
- * 获取调用flushAndSync()方法执行同步的次数
- */
- long getNumSync() {
- return numSync;
- }
该抽象类中定义了一些抽象方法,这些方法需要在该类的子类中给出具体实现:
- /**
- * 获取该EditLogOutputStream输出流的名称
- */
- abstract String getName();
- /**
- * 该方法继承自OutputStream,表示向该输出流对象中写入字节
- */
- abstract public void write(int b) throws IOException;
- /**
- * 将EditLog日志记录写入到该输出流对象中
- * (日志记录通过操作名称和一个Writable参数的数组来表征)
- */
- abstract void write(byte op, Writable ... writables) throws IOException;
- /**
- * 创建并初始化一个新的EditLog日志存储对象
- */
- abstract void create() throws IOException;
- /** {@inheritDoc} */
- abstract public void close() throws IOException;
- /**
- * 所有已经被写入到该输出流对象中的数据,将准备对其执行刷新(flush)操作。
- * (在执行刷新的过程中,新的数据仍然能够被写入到该输出流对象中)
- */
- abstract void setReadyToFlush() throws IOException;
- /**
- * Flush并且sync所有准备好的数据(调用了setReadyToFlush()方法表示准备),持久化到相应的(持久)存储中
- */
- abstract protected void flushAndSync() throws IOException;
- /**
- * 获取当前EditLog日志文件的大小
- * 得到EditLog日志文件的长度是为了检查日志文件是否大到需要启动一个检查点进程来处理
- */
- abstract long length() throws IOException;
通过对EditLogOutputStream抽象类的分析,我们获知:一个EditLog日志文件也是通过流式进行写的,而且定时对日志文件进行同步,写入持久存储中,其中支持写入字节和Writable类型数据;在同步的过程中,需要记录同步统计数据;当EditLog日志文件达到一定大小的时候,需要启动检查点进程来进行处理。
然后,我们看FSEditLog.EditLogFileOutputStream内部静态类的具体实现。
该类继承自EditLogOutputStream抽象类,实现了该抽象类中定义的抽象方法,能够在一个本地文件(local file)中存储EditLog日志文件。
该类定义了如下属性:
- private File file; // EditLog日志文件记录所存储的本地文件
- private FileOutputStream fp; // 存储EditLog日志记录的文件输出流对象
- private FileChannel fc; // 执行同步操作(sync)的文件输出流对象对应的通道
- private DataOutputBuffer bufCurrent; // 当前用于写操作的数据缓冲区
- private DataOutputBuffer bufReady; // 为了执行写操作而准备的数据缓冲区
- static ByteBuffer fill = ByteBuffer.allocateDirect(512); // 预分配大小为512的字节缓冲区
下面是FSEditLog.EditLogFileOutputStream类的构造方法:
- EditLogFileOutputStream(File name) throws IOException {
- super();
- file = name; // EditLog日志文件对应的本地文件名称
- bufCurrent = new DataOutputBuffer(sizeFlushBuffer); // 分配sizeFlushBuffer = 512*1024
- bufReady = new DataOutputBuffer(sizeFlushBuffer); // 分配sizeFlushBuffer = 512*1024
- RandomAccessFile rp = new RandomAccessFile(name, "rw"); // 根据文件File name创建一个随机读写文件实例
- fp = new FileOutputStream(rp.getFD()); // 为向fp输出流对象中追加数据而打开该输出流
- fc = rp.getChannel(); // 获取fp对应的通道
- fc.position(fc.size()); // 设置输出流通道对应文件流的当前位置
- }
通过上面的构造方法,可见通过构造方法已经准备好了写EditLog日志文件的条件,定位到EditLog对应于本地存储文件流的当前写入位置,也就是说对日志文件的操作时追加写操作。
下面看该类中实现的方法:
- @Override
- String getName() {
- return file.getPath(); // 返回EditLog日志文件对应的本地存储文件的路径
- }
- @Override
- public void write(int b) throws IOException {
- bufCurrent.write(b); // 将字节写入当前数据缓冲区bufCurrent
- }
- @Override
- void write(byte op, Writable ... writables) throws IOException {
- write(op); // 调用重载的write方法,写入操作名称(其实是一个操作代码)
- for(Writable w : writables) { // 迭代Writable对象数组
- w.write(bufCurrent); // 分别写入到前数据缓冲区bufCurrent
- }
- }
- /**
- * 创建一个空的EditLog日志文件
- */
- @Override
- void create() throws IOException {
- fc.truncate(0); // 设置当前文件的大小(如果文件大小大于0,删除文件中的全部字节数据)
- fc.position(0); // 设置当前写位置(初始化)
- bufCurrent.writeInt(FSConstants.LAYOUT_VERSION); // 在EditLog日志文件最前面写入版本号
- setReadyToFlush(); // 设置准备刷新状态
- flush(); // 执行刷新操作(将版本号写入到输出流中)
- }
- @Override
- public void close() throws IOException {
- // 在所有追加写事务执行flush与sync操作完成以后,该方法被调用
- int bufSize = bufCurrent.size();
- if (bufSize != 0) {
- throw new IOException("FSEditStream has " + bufSize + " bytes still to be flushed and cannot " + "be closed.");
- }
- bufCurrent.close(); // 关闭当前数据缓冲区
- bufReady.close(); // 关闭预执行flush准备数据缓冲区
- fc.truncate(fc.position()); // 从事务日志中删除最后面的OP_INVALID标记(因为在调用setReadyToFlush方法结束时,写入了OP_INVALID标记)
- fp.close(); // 关闭EditLog日志文件对应的本地存储文件输出流对象
- bufCurrent = bufReady = null; // 释放
- }
- /**
- * 所有已经被写入到该输出流对象中的数据,将准备对其执行刷新(flush)操作。
- * (在执行刷新操作的过程中,新的数据仍然能够被写入到该输出流对象中)
- */
- @Override
- void setReadyToFlush() throws IOException {
- assert bufReady.size() == 0 : "previous data is not flushed yet";
- write(OP_INVALID); // 在文件末尾写入一个标记OP_INVALID
- DataOutputBuffer tmp = bufReady; // 切换缓冲区:将当前的bufCurrent切换为准备好要执行flush操作的bufReady
- bufReady = bufCurrent;
- bufCurrent = tmp;
- }
- /**
- * 将bufCurrent中数据到持久存储中
- * currentBuffer is not flushed as it accumulates new log records
- * while readyBuffer will be flushed and synced.
- */
- @Override
- protected void flushAndSync() throws IOException {
- preallocate(); // 如果必要的话,为文件预分配内存
- bufReady.writeTo(fp); // 将bufReady中数据写入到文件
- bufReady.reset(); // 重启bufReady数据缓冲区
- fc.force(false); // 因为调用preallocate()方法对文件进行了预分配,也就没必要更新该文件元数据信息
- fc.position(fc.position()-1); // 设置当前写入位置:回退排除文件末尾的标记OP_INVALID
- }
- /**
- * 获取当前EditLog日志文件的大小(包括bufReady与bufCurrent)
- */
- @Override
- long length() throws IOException {
- return fc.size() + bufReady.size() + bufCurrent.size(); // 返回文件大小 = 文件实际大小 + bufReady大小 + bufCurrent大小
- }
- // 为日志文件预分配一个大数据块
- private void preallocate() throws IOException {
- long position = fc.position(); // 获取文件当前位置
- if (position + 4096 >= fc.size()) {
- FSNamesystem.LOG.debug("Preallocating Edit log, current size " + fc.size());
- long newsize = position + 1024*1024; // 1MB
- fill.position(0); // 预分配数据缓冲区fill写入位置为0
- int written = fc.write(fill, newsize); // 为日志文件增加分配newsize个字节
- FSNamesystem.LOG.debug("Edit log size is now " + fc.size() + " written " + written + " bytes " + " at offset " + newsize);
- }
- }
- /**
- * 获取与该输出流相关联的文件
- */
- File getFile() {
- return file;
- }
通过对上面方法的阅读分析,可以了解到,EditLog日志文件是与本地磁盘上的一个文件相对应的,实际也是写入到这个本地文件中的。在执行流式追加写的过程中,设置了两个重要的数据缓冲区,bufReady 与bufCurrent,通过切换这两个数据缓冲区来将不断追加写入到EditLogFileOutputStream流对象中的数据,同步到本地磁盘文件中。而且,当执行flushAndSync方法进行同步时候,如果必要会对EditLog日志文件对应的本地文件进行预分配,保证可持续做好写事务操作的记录。
- FSEditLog.EditLogFileInputStream内部静态类
该类org.apache.hadoop.hdfs.server.namenode.FSEditLog.EditLogFileInputStream是对EditLog日志文件进行读取的输入流实现类。该输入流类与EditLogFileOutputStream输出流类的组织关系类似,它的继承层次关系如下所示:
- ◦java.io.InputStream(implements java.io.Closeable)
- ◦org.apache.hadoop.hdfs.server.namenode.EditLogInputStream
- ◦org.apache.hadoop.hdfs.server.namenode.FSEditLog.EditLogFileInputStream
首先,看抽象流类EditLogInputStream。该类是一个用来支持读取本地存储目录中对应的文件,从而得到EditLog日志文件的通用抽象类。该抽象类比较简单,定义获取继承了一组抽象方法,如下所示:
- /**
- * 获取该输入流的名称
- */
- abstract String getName();
- /**
- * 估算大概存在多少字节可读的数据
- */
- public abstract int available() throws IOException;
- /**
- * 从该输入流中读取下一个字节的数据
- */
- public abstract int read() throws IOException;
- /**
- * 从该输入流off位置读取len个字节到字节数组b中
- */
- public abstract int read(byte[] b, int off, int len) throws IOException;
- /**
- * 关闭该输入流,释放相关资源
- */
- public abstract void close() throws IOException;
- /**
- * 获取当前EditLog日志文件的大小
- */
- abstract long length() throws IOException;
没什么可说的,继续看FSEditLog.EditLogFileInputStream内部静态类的具体实现吧。
FSEditLog.EditLogFileInputStream内部静态类也比较简单,就是提供了对EditLog日志文件的读取操作,也是基于流的,该类源代码如下所示:
- static class EditLogFileInputStream extends EditLogInputStream {
- private File file; // EditLog日志文件对应的本地文件
- private FileInputStream fStream; // 文件file对应的输入流对象
- EditLogFileInputStream(File name) throws IOException { // 构造方法
- file = name;
- fStream = new FileInputStream(name); // 构造file的输入流实例
- }
- @Override
- String getName() { // 获取file的路径
- return file.getPath();
- }
- @Override
- public int available() throws IOException {
- return fStream.available(); // 获取从该输入流可以读取到的字节数
- }
- @Override
- public int read() throws IOException {
- return fStream.read(); // 从该输入流中读取一个字节的数据
- }
- @Override
- public int read(byte[] b, int off, int len) throws IOException {
- return fStream.read(b, off, len); // 将该输入流中从off位置开始读取len个字节到字节数组b中
- }
- @Override
- public void close() throws IOException {
- fStream.close(); // 关闭该输入流
- }
- @Override
- long length() throws IOException {
- return file.length(); // 返回文件大小 = 文件实际大小 + bufReady大小 + bufCurrent大小
- }
- }
- FSEditLog类
该类维护一个记录对文件系统命名空间进行修改操作的日志文件。
我们已经对FSEditLog.EditLogFileOutputStream类与FSEditLog.EditLogFileInputStream类非常熟悉了,它们是与EditLog日志文件的读写密切相关的。
在FSEditLog类内部还有一个实现了Writable接口的可序列化内实体类FSEditLog.BlockTwo,它能够从以旧格式存储的块中读写数据,如下所示:
- static class BlockTwo implements Writable {
- long blkid; // 块ID
- long len; // 块长度
- static { // 注册
- WritableFactories.setFactory
- (BlockTwo.class,
- new WritableFactory() {
- public Writable newInstance() { return new BlockTwo(); }
- });
- }
- BlockTwo() {
- blkid = 0;
- len = 0;
- }
- public void write(DataOutput out) throws IOException {
- out.writeLong(blkid);
- out.writeLong(len);
- }
- public void readFields(DataInput in) throws IOException {
- this.blkid = in.readLong();
- this.len = in.readLong();
- }
- }
通过一个BlockTwo对象,能够读写块的ID和块的长度。
另一个是事务ID的封装内部实体类FSEditLog.TransactionId,表示EditLog日志文件记录的修改操作这样的事务ID,在FSEditLog类中主要是用来作为当前执行事务的线程对应的线程局部变量的拷贝:ThreadLocal<TransactionId>。该类如下所示:
- private static class TransactionId {
- public long txid;
- TransactionId(long value) {
- this.txid = value;
- }
- }
FSEditLog.TransactionId类只封装了一个事务ID,再没有其它内容了。
我们再看FSEditLog类。
首先看,对文件系统命名空间进行不同操作的操作代码,通过向EditLog日志文件中写入对应的操作代码来表示实际的操作,这些操作代码包含:
- private static final byte OP_INVALID = -1; // 非法操作
- private static final byte OP_ADD = 0; // 添加文件操作
- private static final byte OP_RENAME = 1; // 重命名
- private static final byte OP_DELETE = 2; // 删除
- private static final byte OP_MKDIR = 3; // 创建目录
- private static final byte OP_SET_REPLICATION = 4; // 设置副本因子
- // 下面两个仅仅为了保证后向兼容
- @Deprecated private static final byte OP_DATANODE_ADD = 5;
- @Deprecated private static final byte OP_DATANODE_REMOVE = 6;
- private static final byte OP_SET_PERMISSIONS = 7; // 设置权限
- private static final byte OP_SET_OWNER = 8; // 设置属主
- private static final byte OP_CLOSE = 9; // 写操作完成后的关闭操作
- private static final byte OP_SET_GENSTAMP = 10; // 存储时间戳
- // 下面两个已经不再使用,如果LAST_UPGRADABLE_LAYOUT_VERSION等于-17或者更新,则可以删除掉
- private static final byte OP_SET_NS_QUOTA = 11; // 设置文件系统命名空间配额
- private static final byte OP_CLEAR_NS_QUOTA = 12; // 清除文件系统命名空间配额
- private static final byte OP_TIMES = 13; // 为一个文件设置mod & access时间
- private static final byte OP_SET_QUOTA = 14; // 设置名称和磁盘配额
再看该类中定义的其它属性,如下所示:
- private static int sizeFlushBuffer = 512*1024; // 数据缓冲区大小,主要是写日志文件过程中为两个执行切换的数据缓冲区分配大小
- private ArrayList<EditLogOutputStream> editStreams = null; // EditLogOutputStream输出流对象列表
- private FSImage fsimage = null; // FsImage映像
- private long txid = 0; // 单调递增事务ID计数器,用来为事务分配ID的
- private long synctxid = 0; // 最后执行sync同步操作的事务ID
- private long lastPrintTime; // 最后一次将统计数据输出到日志文件的时间
- private boolean isSyncRunning; // 是否当前正在执行sync同步操作
- private long numTransactions; // 事务数量统计变量
- private long numTransactionsBatchedInSync; // 批量sync事务的数量的统计变量
- private long totalTimeTransactions; // 执行全部事务的时间统计变量
- private NameNodeMetrics metrics; // Namenode统计数据的统计变量
- // 存储线程的当前事务
- private static final ThreadLocal<TransactionId> myTransactionId = new ThreadLocal<TransactionId>() {
- protected synchronized TransactionId initialValue() {
- return new TransactionId(Long.MAX_VALUE);
- }
- };
该类的构造方法如下所示:
- FSEditLog(FSImage image) { // 通过FSImage实例来构造,FSImage类会在之后分析
- fsimage = image;
- isSyncRunning = false; // 当前没有进行同步sync
- metrics = NameNode.getNameNodeMetrics(); // 通过Namenode获取到其状态统计数据对象,用来跟踪与EditLog相关的信息(写入Namenode的统计数据对象中)
- lastPrintTime = FSNamesystem.now(); // 初始化时间
- }
关于一个FSEditLog类的实例能够做哪些事情,我们从该类中挑选几个比较关键的方法来详细解释说明。
1、创建一个EditLog日志文件
如下所示:
- public synchronized void createEditLogFile(File name) throws IOException {
- EditLogOutputStream eStream = new EditLogFileOutputStream(name); // 根据指定的文件名,构造EditLog日志文件的输出流
- eStream.create(); // 创建EditLog日志文件,并写入版本号LAYOUT_VERSION,同时设置当前待写入的位置
- eStream.close(); // 关闭输出流
- }
另外,还有一个用来表示当前最新的edit文件edit.new,当该文件丢失的时候,需要进行创建,该操作对应于方法createNewIfMissing,如下所示:
- synchronized void createNewIfMissing() throws IOException {
- or (Iterator<StorageDirectory> it = fsimage.dirIterator(NameNodeDirType.EDITS); it.hasNext();) { // 检查是否edit.new文件丢失
- File newFile = getEditNewFile(it.next());
- if (!newFile.exists())
- createEditLogFile(newFile); // 如果不存在,就调用上面的createEditLogFile创建一个
- }
- }
2、打开 一个EditLog日志文件
方法open实现了打开日志文件的功能。实际上,当创建一个FSEditLog类的实例以后(通过构造方法构造得到),调用open方法打开一个该文件的输出流,准备后继向流中持续追加日志记录。open方法如下所示:
- public synchronized void open() throws IOException {
- numTransactions = totalTimeTransactions = numTransactionsBatchedInSync = 0; // 初始化这些统计变量
- if (editStreams == null)
- editStreams = new ArrayList<EditLogOutputStream>(); // 初始化该日志文件实例的EditLogOutputStream流对象列表
- // 通过FSImage实例获取到Namenode对应的EDITS类型的存储目录的迭代器实例(因为这是一组存储目录,势必对应用来写EditLog日志的输出流EditLogFileOutputStream列表)
- // 注意:FSImage实例实际上只拥有一张登记了全部EDITS类型的存储目录的表,至于该存储目录中是否存在对应的文件,在加载的时候回检测出来,并采取相应的策略
- for (Iterator<StorageDirectory> it = fsimage.dirIterator(NameNodeDirType.EDITS); it.hasNext();) {
- StorageDirectory sd = it.next();
- File eFile = getEditFile(sd); // 获取一个EditLog文件
- try {
- EditLogOutputStream eStream = new EditLogFileOutputStream(eFile); // 打开该EditLog文件输出流
- editStreams.add(eStream); // 加入列表
- } catch (IOException e) {
- FSNamesystem.LOG.warn("Unable to open edit log file " + eFile);
- it.remove(); // 如果处理该目录sd发生异常,从存储目录列表中删除该目录sd
- }
- }
- }
调用open方法,其实是从Namenode来加载EditLog日志文件所对应的全部存储目录,并打开每一个与EditLog日志文件相关的本地文件的文件输出流,为了便于FSEditLog类实例管理这些输出流,使用一个ArrayList来存放于内存中。
3、处理输出流IO异常
通过前面分析,一个FSEditLog类的实例维护一个EditLogOutputStream输出流对象列表,那么当某个输出流对象发生IO异常的时候,需要对其作出处理,而不能影响其它事务的执行。 该类中给出了三个处理IO异常的方法,如下所示:
- /**
- * 如果发生与EditLog日志相关的IO异常,会从editStreams列表中删除掉发生IO异常的输出流对象
- * 第一个:根据输出流对象在editStreams列表中索引来处理
- */
- synchronized void processIOError(int index);
- /**
- * 如果发生与EditLog日志相关的IO异常,会从editStreams列表中删除掉发生IO异常的输出流对象
- * 第二个:根据存储目录名称来处理editStreams列表中对应的输出流对象
- */
- synchronized void processIOError(StorageDirectory sd);
- /**
- * 如果发生与EditLog日志相关的IO异常,会从editStreams列表中删除掉发生IO异常的输出流对象
- * 第三个:对列表editStreams中的输出流对象进行批量检查并对发生IO异常的输出流对象处理
- */
- private void processIOError(ArrayList<EditLogOutputStream> errorStreams);
在FSEditLog类中处理IO异常的时候,还需要将处理的情况报告到FSImage中,因为FSImage也维护了一个指定为要删除的存储目录列表,保证数据状态的同步与一致。
4、加载EditLog日志文件
实现的方法为loadFSEdits方法,因为加载一个EditLog日志文件的时候,需要将对应EditLog日志文件记录内容应用到一个滞留于内存中的结构上,保证内存中对应结构与日志同步。因为该方法比较重要,通过它能够看到如何在日志文件与其对应的内存映像之间进行切换,尽管该方法代码比较多。我还是贴出来分析,能够更好地看到这个过程,加深理解。必要的时候我会对代码行修改,减少显示行数。
loadFSEdits方法实现如下所示:
- static int loadFSEdits(EditLogInputStream edits) throws IOException {
- FSNamesystem fsNamesys = FSNamesystem.getFSNamesystem(); // 获取到一个FSNamesystem对象
- FSDirectory fsDir = fsNamesys.dir; // 得到一个fsNamesys.dir的引用
- int numEdits = 0, logVersion = 0;
- String clientName = null, clientMachine = null, path = null;
- // 这是一组统计变量
- int numOpAdd = 0, numOpClose = 0, numOpDelete = 0, numOpRename = 0, numOpSetRepl = 0, numOpMkDir = 0,
- numOpSetPerm = 0, numOpSetOwner = 0, numOpSetGenStamp = 0, numOpTimes = 0, numOpOther = 0;
- long startTime = FSNamesystem.now(); // EditLog日志文件开始加载的时间
- DataInputStream in = new DataInputStream(new BufferedInputStream(edits)); // EditLog日志文件对应的输入流
- try {
- in.mark(4); // 设置EditLog日志文件输入流当前位置,从而读取日志文件的版本(很可能发生版本号信息丢失的情况)
- // 如果EditLog日志文件大于2G,调用available方法将会返回一个负数,为了避免不得不调用available方法的情况,设置一个Boolean变量,并指定其值为true
- boolean available = true;
- try {
- logVersion = in.readByte(); // 从输入流中读取版本号信息
- } catch (EOFException e) {
- available = false; // 发生异常则置于available为false,表示当前EditLog日志文件不可用(可能日志文件存在问题)
- }
- if (available) { // 日志文件可用
- in.reset(); // 重置
- logVersion = in.readInt(); // 读取版本号
- if (logVersion < FSConstants.LAYOUT_VERSION) // 判断读取到的版本号是否合法
- throw new IOException("Unexpected version of the file system log file: " + logVersion + ". Current version = " + FSConstants.LAYOUT_VERSION + ".");
- }
- assert logVersion <= Storage.LAST_UPGRADABLE_LAYOUT_VERSION : "Unsupported version " + logVersion;
- while (true) {
- long timestamp = 0, mtime = 0, atime = 0, blockSize = 0;
- byte opcode = -1;
- try {
- opcode = in.readByte(); // 读取一个字节
- if (opcode == OP_INVALID) { // 因为我们已经知道,在开始写日志的时候,写完版本号以后,可能写入一个字节的OP_INVALID(也就是-1);并且在写日志过程中,会将这个标记删除掉的,如果没有及时删除掉,很可能在该日志上根本没有事务记录,也可能发生了IO异常
- FSNamesystem.LOG.info("Invalid opcode, reached end of edit log " + "Number of transactions found " + numEdits);
- break; // 没有事务记录,直接退出,不再读取EditLog日志文件了
- }
- } catch (EOFException e) {
- break; // 没有事务记录
- }
- numEdits++; // 每读取到EditLog日志文件中一个记录的事务,就增加一个计数
- switch (opcode) { // 根据读取到的操作代码,进行相应的处理
- case OP_ADD:
- case OP_CLOSE: { // OP_ADD(添加)或OP_CLOSE(关闭)文件操作
- int length = in.readInt();
- if (-7 == logVersion && length != 3|| -17 < logVersion && logVersion < -7 && length != 4 || logVersion <= -17 && length != 5) { // 判断读取到的版本号是否合法
- throw new IOException("Incorrect data format." + " logVersion is " + logVersion + " but writables.length is " + length + ". ");
- }
- path = FSImage.readString(in); // 读取文件路径
- short replication = adjustReplication(readShort(in)); // 读取副本数(必要的话需要修改该值)
- mtime = readLong(in); // 读取文件修改时间
- if (logVersion <= -17) {
- atime = readLong(in); // 如果版本号小于-17,读取文件访问时间
- }
- if (logVersion < -7) {
- blockSize = readLong(in); // 读取块大小
- }
- Block blocks[] = null;
- if (logVersion <= -14) {
- blocks = readBlocks(in); // 读取块列表
- } else {
- BlockTwo oldblk = new BlockTwo();
- int num = in.readInt();
- blocks = new Block[num];
- for (int i = 0; i < num; i++) {
- oldblk.readFields(in);
- blocks[i] = new Block(oldblk.blkid, oldblk.len, Block.GRANDFATHER_GENERATION_STAMP);
- }
- }
- // 旧版本HDFS没有在文件中存储块大小,如果某个文件块多于1个,使用第一个作为块大小,否则使用默认值
- if (-8 <= logVersion && blockSize == 0) {
- if (blocks.length > 1) {
- blockSize = blocks[0].getNumBytes();
- } else {
- long first = ((blocks.length == 1)? blocks[0].getNumBytes(): 0);
- blockSize = Math.max(fsNamesys.getDefaultBlockSize(), first);
- }
- }
- PermissionStatus permissions = fsNamesys.getUpgradePermission();
- if (logVersion <= -11) {
- permissions = PermissionStatus.read(in); // 读取权限
- }
- // 读取文件中最后一个块的clientname, clientMachine and block locations
- if (opcode == OP_ADD && logVersion <= -12) {
- clientName = FSImage.readString(in);
- clientMachine = FSImage.readString(in);
- if (-13 <= logVersion) {
- readDatanodeDescriptorArray(in);
- }
- } else {
- clientName = "";
- clientMachine = "";
- }
- // The open lease transaction re-creates a file if necessary.
- if (FSNamesystem.LOG.isDebugEnabled()) {
- FSNamesystem.LOG.debug(opcode + ": " + path + " numblocks : " + blocks.length + " clientHolder " + clientName + " clientMachine " + clientMachine);
- }
- fsDir.unprotectedDelete(path, mtime); // 从namespace中删除该文件,并更新目录配额信息
- // 将path添加到文件树fsDir中
- INodeFile node = (INodeFile)fsDir.unprotectedAddFile(path, permissions, blocks, replication, mtime, atime, blockSize);
- if (opcode == OP_ADD) { // 如果是添加操作码
- numOpAdd++; // 统计
- INodeFileUnderConstruction cons = new INodeFileUnderConstruction(
- node.getLocalNameBytes(), node.getReplication(), node.getModificationTime(),
- node.getPreferredBlockSize(), node.getBlocks(), node.getPermissionStatus(),
- clientName, clientMachine, null);
- fsDir.replaceNode(path, node, cons); // 使用cons替换node
- fsNamesys.leaseManager.addLease(cons.clientName, path); // 将cons加载到内存中,加入到租约管理器中
- }
- break;
- }
- case OP_SET_REPLICATION: { // 设置副本数操作
- numOpSetRepl++; // 统计
- path = FSImage.readString(in); // 读取文件
- short replication = adjustReplication(readShort(in)); // 读取并调整副本数
- fsDir.unprotectedSetReplication(path, replication, null); // 更新目录fsDir
- break;
- }
- case OP_RENAME: { // 重命名操作
- numOpRename++; // 统计
- int length = in.readInt(); // 读取长度
- if (length != 3) {
- throw new IOException("Incorrect data format. " + "Mkdir operation.");
- }
- String s = FSImage.readString(in); // 读取源文件名称
- String d = FSImage.readString(in); // 读取重命名后文件名称
- timestamp = readLong(in); // 读取重命名文件时间戳
- FileStatus dinfo = fsDir.getFileInfo(d); // 获取文件d的描述信息
- fsDir.unprotectedRenameTo(s, d, timestamp); // 更新目录fsDir
- fsNamesys.changeLease(s, d, dinfo); // 修改租约信息
- break;
- }
- case OP_DELETE: { // 删除操作
- numOpDelete++; // 统计
- int length = in.readInt(); // 删除文件名称长度
- if (length != 2) {
- throw new IOException("Incorrect data format. " + "delete operation.");
- }
- path = FSImage.readString(in); // 读取长度
- timestamp = readLong(in); // 读取删除文件时间戳
- fsDir.unprotectedDelete(path, timestamp); // 执行删除
- break;
- }
- case OP_MKDIR: { // 创建目录操作
- numOpMkDir++; // 统计
- PermissionStatus permissions = fsNamesys.getUpgradePermission(); // 获取文件系统中路径的默认权限
- int length = in.readInt();
- if (-17 < logVersion && length != 2 || <= -17 && length != 3) {
- throw new IOException("Incorrect data format. " + "Mkdir operation.");
- }
- path = FSImage.readString(in); // 读取目录名称
- timestamp = readLong(in); // 读取创建目录时间戳
- if (logVersion <= -17) {
- atime = readLong(in);
- }
- if (logVersion <= -11) {
- permissions = PermissionStatus.read(in);
- }
- fsDir.unprotectedMkdir(path, permissions, timestamp); // 在fsDir目录中创建目录path
- break;
- }
- case OP_SET_GENSTAMP: { // 设置时间戳操作
- numOpSetGenStamp++; // 统计
- long lw = in.readLong(); // 读取设置的时间戳
- fsDir.namesystem.setGenerationStamp(lw); // 在fsDir中设置时间戳
- break;
- }
- case OP_DATANODE_ADD: { // 丢弃的操作码
- numOpOther++;
- FSImage.DatanodeImage nodeimage = new FSImage.DatanodeImage();
- nodeimage.readFields(in);
- break;
- }
- case OP_DATANODE_REMOVE: { // 丢弃的操作码
- numOpOther++;
- DatanodeID nodeID = new DatanodeID();
- nodeID.readFields(in);
- break;
- }
- case OP_SET_PERMISSIONS: { // 设置权限
- numOpSetPerm++; // 统计
- if (logVersion > -11)
- throw new IOException("Unexpected opcode " + opcode + " for version " + logVersion);
- fsDir.unprotectedSetPermission(FSImage.readString(in), FsPermission.read(in)); // 在fsDir中同步设置
- break;
- }
- case OP_SET_OWNER: { // 设置属主操作
- numOpSetOwner++;
- if (logVersion > -11)
- throw new IOException("Unexpected opcode " + opcode + " for version " + logVersion);
- fsDir.unprotectedSetOwner(FSImage.readString(in), FSImage.readString_EmptyAsNull(in), FSImage.readString_EmptyAsNull(in)); // 在fsDir中同步设置属主
- break;
- }
- case OP_SET_NS_QUOTA: { // 设置namespace配额操作
- if (logVersion > -16) {
- throw new IOException("Unexpected opcode " + opcode + " for version " + logVersion);
- }
- fsDir.unprotectedSetQuota(FSImage.readString(in), readLongWritable(in), FSConstants.QUOTA_DONT_SET); // 在fsDir中同步设置namespace配额
- break;
- }
- case OP_CLEAR_NS_QUOTA: { // 清除namespace配额操作
- if (logVersion > -16) {
- throw new IOException("Unexpected opcode " + opcode + " for version " + logVersion);
- }
- fsDir.unprotectedSetQuota(FSImage.readString(in), FSConstants.QUOTA_RESET, FSConstants.QUOTA_DONT_SET); // 在fsDir中同步清除namespace配额
- break;
- }
- case OP_SET_QUOTA: // 设置名称和磁盘配额
- fsDir.unprotectedSetQuota(FSImage.readString(in), readLongWritable(in), readLongWritable(in)); // 在fsDir中同步设置磁盘配额
- break;
- case OP_TIMES: { // 设置文件的mod & access时间操作
- numOpTimes++;
- int length = in.readInt();
- if (length != 3) {
- throw new IOException("Incorrect data format. " + "times operation.");
- }
- path = FSImage.readString(in);
- mtime = readLong(in);
- atime = readLong(in);
- fsDir.unprotectedSetTimes(path, mtime, atime, true); // 在fsDir上同步设置
- break;
- }
- default: {
- throw new IOException("Never seen opcode " + opcode);
- }
- }
- }
- } finally {
- in.close();
- }
- FSImage.LOG.info("Edits file " + edits.getName() + " of size " + edits.length() + " edits # " + numEdits + " loaded in " + (FSNamesystem.now()-startTime)/1000 + " seconds.");
- if (FSImage.LOG.isDebugEnabled()) {
- FSImage.LOG.debug("numOpAdd = " + numOpAdd + " numOpClose = " + numOpClose
- + " numOpDelete = " + numOpDelete + " numOpRename = " + numOpRename
- + " numOpSetRepl = " + numOpSetRepl + " numOpMkDir = " + numOpMkDir
- + " numOpSetPerm = " + numOpSetPerm
- + " numOpSetOwner = " + numOpSetOwner
- + " numOpSetGenStamp = " + numOpSetGenStamp
- + " numOpTimes = " + numOpTimes
- + " numOpOther = " + numOpOther);
- }
- if (logVersion != FSConstants.LAYOUT_VERSION) // 如果版本号不等于LAYOUT_VERSION=-8
- numEdits++; // 也进行统计
- return numEdits;
- }
通过该方法的实现,实际上在从多个EditLog 中读取日志信息的时候,主要是将日志文件中对应的事务,通过FSDirectory fsDir = fsNamesys.dir及时更新到文件系统的目录上,保持数据状态同步一致。
5、其它方法
还有几个方法,分别表示对EditLog日志文件的操作,比如:
关闭日志文件方法rollEditLog,同时创建一个edits.new文件;
删除旧日志文件purgeEditLog方法,同时将edits.new文件重命名为edits;
同步日志文件方法logSync,只对当前线程对日志文件作出的全部修改,同步到日志文件上。