初始化editlog
在文档(10)中解析了FSImage类的loadFSImage方法, 这个方法主要用来加载元数据。它大致可以分为5个部分:是查找fsimage文件; 初始化editlog;加载editlog流;加载fsimage文件;执行editlog。
这里继续分析初始化editlog的initEditLog方法,其内容如下:
public void initEditLog(StartupOption startOpt) throws IOException {
Preconditions.checkState(getNamespaceID() != 0,
"Must know namespace ID before initting edit log");
String nameserviceId = DFSUtil.getNamenodeNameServiceId(conf);
if (!HAUtil.isHAEnabled(conf, nameserviceId)) {
// If this NN is not HA
editLog.initJournalsForWrite();
editLog.recoverUnclosedStreams();
} else if (HAUtil.isHAEnabled(conf, nameserviceId)
&& (startOpt == StartupOption.UPGRADE
|| startOpt == StartupOption.UPGRADEONLY
|| RollingUpgradeStartupOption.ROLLBACK.matches(startOpt))) {
// This NN is HA, but we're doing an upgrade or a rollback of rolling
// upgrade so init the edit log for write.
editLog.initJournalsForWrite();
if (startOpt == StartupOption.UPGRADE
|| startOpt == StartupOption.UPGRADEONLY) {
long sharedLogCTime = editLog.getSharedLogCTime();
if (this.storage.getCTime() < sharedLogCTime) {
throw new IOException("It looks like the shared log is already " +
"being upgraded but this NN has not been upgraded yet. You " +
"should restart this NameNode with the '" +
StartupOption.BOOTSTRAPSTANDBY.getName() + "' option to bring " +
"this NN in sync with the other.");
}
}
editLog.recoverUnclosedStreams();
} else {
// This NN is HA and we're not doing an upgrade.
editLog.initSharedJournalsForRead();
}
}
这个方法很简单,总体来说就是一个if语句。这个语句是用于处理 不同集群状态,首先是第5行的if语句,这是用来处理非HA状态的集群的情况;然 后是第9行的else if语句,这里是用来处理HA状态但集群正在更新的情况;最后 是第28行的else语句,这里是用来处理HA状态的集群的。
按照之前的配置,集群是HA状态并且没有更新,所以会执行第28行的语句。 这个else语句内部就一句:调用了editLog的initSharedJournalsForRead 方法。editLog是在FSImage对象创建的时候创建的一个类。
如上图,在创建的editLog的类为FSEditLog,创建的时候传入了 三个参数,第一个是conf,代表了hdfs的配置文件;第二个是storage, 文档(10)解析过;第三个是editsDirs,这个参数传入FSImage的第三个参数, 这个参数在文档(8)中解析过。
FSEditLog的构造方法如下:
FSEditLog(Configuration conf, NNStorage storage, List<URI> editsDirs) {
isSyncRunning = false;
this.conf = conf;
this.storage = storage;
metrics = NameNode.getNameNodeMetrics();
lastPrintTime = monotonicNow();
// If this list is empty, an error will be thrown on first use
// of the editlog, as no journals will exist
this.editsDirs = Lists.newArrayList(editsDirs);
this.sharedEditsDirs = FSNamesystem.getSharedEditsDirs(conf);
}
这个方法只是一些参数赋值而已。重点是最后的两个参数: editsDirs和sharedEditsDirs。editsDirs是传入的参数,它代表着本地 的editlog和远端的editlog。而sharedEditsDirs只代表着远端的editlog。
接着继续分析上文提到的initSharedJournalsForRead方法,其内容如下:
public synchronized void initSharedJournalsForRead() {
if (state == State.OPEN_FOR_READING) {
LOG.warn("Initializing shared journals for READ, already open for READ",
new Exception());
return;
}
Preconditions.checkState(state == State.UNINITIALIZED ||
state == State.CLOSED);
initJournals(this.sharedEditsDirs);
state = State.OPEN_FOR_READING;
}
这里重点就一个:第10行的initJournals方法。这里这个方法 传入的是sharedEditsDirs。上文解析过这个参数是代表着远端的edit目录。 注意这里因为是启动,所有的namenode都是以standby状态启动,所以这里的 edit目录只有远端目录。在standby状态转换成active状态的时候,会重新初 始化editlog,这时的目录有本地的与远端的两个目录。
initJournals方法的内容如下:
private synchronized void initJournals(List<URI> dirs) {
int minimumRedundantJournals = conf.getInt(
DFSConfigKeys.DFS_NAMENODE_EDITS_DIR_MINIMUM_KEY,
DFSConfigKeys.DFS_NAMENODE_EDITS_DIR_MINIMUM_DEFAULT);
synchronized(journalSetLock) {
journalSet = new JournalSet(minimumRedundantJournals);
for (URI u : dirs) {
boolean required = FSNamesystem.getRequiredNamespaceEditsDirs(conf)
.contains(u);
if (u.getScheme().equals(NNStorage.LOCAL_URI_SCHEME)) {
StorageDirectory sd = storage.getStorageDirectory(u);
if (sd != null) {
journalSet.add(new FileJournalManager(conf, sd, storage),
required, sharedEditsDirs.contains(u));
}
} else {
journalSet.add(createJournal(u), required,
sharedEditsDirs.contains(u));
}
}
}
if (journalSet.isEmpty()) {
LOG.error("No edits directories configured!");
}
}
首先是第7行,这里会创建一个JournalSet对象。然后遍历传入的 dirs,然后根据其类型来做不同的操作。如果是本地的editlog则执行第12行if 语句内的内容,如果是远端,则执行第18行的else语句内的内容。
在上文的分析中,提到了传入的参数代表着远端的editlog。 所以在会执行第18行的语句。这里就一句代码,使用journalset的add方法, 这个方法传入了三个参数:第一个是createJournal方法的返回值,第二个 是required,它是上文从创建的Boolean值,第三个也是一个boolean值。 其中重点是第一个参数,其调用的createJournal方法如下:
private JournalManager createJournal(URI uri) {
Class<? extends JournalManager> clazz
= getJournalClass(conf, uri.getScheme());
try {
Constructor<? extends JournalManager> cons
= clazz.getConstructor(Configuration.class, URI.class,
NamespaceInfo.class);
return cons.newInstance(conf, uri, storage.getNamespaceInfo());
} catch (Exception e) {
throw new IllegalArgumentException("Unable to construct journal, "
+ uri, e);
}
}
这个方法是用来创建JournalManager对象的,这里是利用java 的反射机制来创建对象。创建的类根据其获取的class来决定。
这里的class是通过getJournalClass方法来获取的, 这个方法的内容如下:
static Class<? extends JournalManager> getJournalClass(Configuration conf,
String uriScheme) {
String key
= DFSConfigKeys.DFS_NAMENODE_EDITS_PLUGIN_PREFIX + "." + uriScheme;
Class <? extends JournalManager> clazz = null;
try {
clazz = conf.getClass(key, null, JournalManager.class);
} catch (RuntimeException re) {
throw new IllegalArgumentException(
"Invalid class specified for " + uriScheme, re);
}
if (clazz == null) {
LOG.warn("No class configured for " +uriScheme
+ ", " + key + " is empty");
throw new IllegalArgumentException(
"No class configured for " + uriScheme);
}
return clazz;
}
这段代码的重点在第7行,这里调用的了conf的getClass方法, 这里传入的key定义在第3行是由DFS_NAMENODE_EDITS_PLUGIN_PREFIX和 uriScheme来确定的,其中DFS_NAMENODE_EDITS_PLUGIN_PREFIX的定义如下:
public static final String DFS_NAMENODE_EDITS_PLUGIN_PREFIX = "dfs.namenode.edits.journal-plugin";
uriScheme是由传入的uri来确定的,这里的值为qjournal。所以 这里的生成的key为dfs.namenode.edits.journal-plugin.qjournal。
getClass方法内容如下:
public <U> Class<? extends U> getClass(String name,
Class<? extends U> defaultValue,
Class<U> xface) {
try {
Class<?> theClass = getClass(name, defaultValue);
if (theClass != null && !xface.isAssignableFrom(theClass))
throw new RuntimeException(theClass+" not "+xface.getName());
else if (theClass != null)
return theClass.asSubclass(xface);
else
return null;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public Class<?> getClass(String name, Class<?> defaultValue) {
String valueString = getTrimmed(name);
if (valueString == null)
return defaultValue;
try {
return getClassByName(valueString);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
这里可以看见getClass有一个重载方法,最终getClass方法会 调用getTrimmed方法(第18行),从配置文件中获取数据,若返回值为空则返回 传入的默认值;不为空则返回获取的值。
getTrimmed方法内容如下:
public String getTrimmed(String name) {
String value = get(name);
if (null == value) {
return null;
} else {
return value.trim();
}
}
重点在第2行,这里会调用get方法从配置文件中获取值。 这个方法在文档(7)中解析过了。它主要是从configuration对象的 properties中获取数据,而properties中值是从配置文档中加载出来的。 这里需要获取的neme,上文解析了是 dfs.namenode.edits.journal-plugin.qjournal。这个参数配置 在hdfs-default.xml中,其内容如下:
<property>
<name>dfs.namenode.edits.journal-plugin.qjournal</name>
<value>org.apache.hadoop.hdfs.qjournal.client.QuorumJournalManager</value>
</property>
这里配置的类为QuorumJournalManager,所以在 createJournal方法中会创建一个这个类的对象来处理远端的edit日志。
QuorumJournalManager的构造方法如下:
public QuorumJournalManager(Configuration conf,
URI uri, NamespaceInfo nsInfo) throws IOException {
this(conf, uri, nsInfo, IPCLoggerChannel.FACTORY);
}
QuorumJournalManager(Configuration conf,
URI uri, NamespaceInfo nsInfo,
AsyncLogger.Factory loggerFactory) throws IOException {
Preconditions.checkArgument(conf != null, "must be configured");
this.conf = conf;
this.uri = uri;
this.nsInfo = nsInfo;
this.loggers = new AsyncLoggerSet(createLoggers(loggerFactory));
this.connectionFactory = URLConnectionFactory
.newDefaultURLConnectionFactory(conf);
// Configure timeouts.
this.startSegmentTimeoutMs = conf.getInt(
DFSConfigKeys.DFS_QJOURNAL_START_SEGMENT_TIMEOUT_KEY,
DFSConfigKeys.DFS_QJOURNAL_START_SEGMENT_TIMEOUT_DEFAULT);
this.prepareRecoveryTimeoutMs = conf.getInt(
DFSConfigKeys.DFS_QJOURNAL_PREPARE_RECOVERY_TIMEOUT_KEY,
DFSConfigKeys.DFS_QJOURNAL_PREPARE_RECOVERY_TIMEOUT_DEFAULT);
this.acceptRecoveryTimeoutMs = conf.getInt(
DFSConfigKeys.DFS_QJOURNAL_ACCEPT_RECOVERY_TIMEOUT_KEY,
DFSConfigKeys.DFS_QJOURNAL_ACCEPT_RECOVERY_TIMEOUT_DEFAULT);
this.finalizeSegmentTimeoutMs = conf.getInt(
DFSConfigKeys.DFS_QJOURNAL_FINALIZE_SEGMENT_TIMEOUT_KEY,
DFSConfigKeys.DFS_QJOURNAL_FINALIZE_SEGMENT_TIMEOUT_DEFAULT);
this.selectInputStreamsTimeoutMs = conf.getInt(
DFSConfigKeys.DFS_QJOURNAL_SELECT_INPUT_STREAMS_TIMEOUT_KEY,
DFSConfigKeys.DFS_QJOURNAL_SELECT_INPUT_STREAMS_TIMEOUT_DEFAULT);
this.getJournalStateTimeoutMs = conf.getInt(
DFSConfigKeys.DFS_QJOURNAL_GET_JOURNAL_STATE_TIMEOUT_KEY,
DFSConfigKeys.DFS_QJOURNAL_GET_JOURNAL_STATE_TIMEOUT_DEFAULT);
this.newEpochTimeoutMs = conf.getInt(
DFSConfigKeys.DFS_QJOURNAL_NEW_EPOCH_TIMEOUT_KEY,
DFSConfigKeys.DFS_QJOURNAL_NEW_EPOCH_TIMEOUT_DEFAULT);
this.writeTxnsTimeoutMs = conf.getInt(
DFSConfigKeys.DFS_QJOURNAL_WRITE_TXNS_TIMEOUT_KEY,
DFSConfigKeys.DFS_QJOURNAL_WRITE_TXNS_TIMEOUT_DEFAULT);
}
这个方法也有一个重载方法,方法内都是一些参数赋值操作。 其中重点在于第14行为loggers赋值。这个参数是实际用来管理editlog的对象。 它的类型为AsyncLoggerSet,在创建这个对象时需要传入一个参数, 这个参数会通过createLoggers方法来创建。而createLoggers方法又传入了 一个参数loggerFactory,这个参数是方法的传入值。这里的传入值是第3行的 IPCLoggerChannel.FACTORY,其内容如下:
static final Factory FACTORY = new AsyncLogger.Factory() {
@Override
public AsyncLogger createLogger(Configuration conf, NamespaceInfo nsInfo,
String journalId, InetSocketAddress addr) {
return new IPCLoggerChannel(conf, nsInfo, journalId, addr);
}
};
这里是一个匿名内部类,它实现该接口的createLogger方法, 这个方法也很简单就是创建了一个IPCLoggerChannel对象。
然后是createLoggers方法,这个方法内容如下:
protected List<AsyncLogger> createLoggers(
AsyncLogger.Factory factory) throws IOException {
return createLoggers(conf, uri, nsInfo, factory);
}
static List<AsyncLogger> createLoggers(Configuration conf,
URI uri, NamespaceInfo nsInfo, AsyncLogger.Factory factory)
throws IOException {
List<AsyncLogger> ret = Lists.newArrayList();
List<InetSocketAddress> addrs = getLoggerAddresses(uri);
String jid = parseJournalId(uri);
for (InetSocketAddress addr : addrs) {
ret.add(factory.createLogger(conf, nsInfo, jid, addr));
}
return ret;
}
这也是一个重载方法,重点在第10行,这里会调用 getLoggerAddresses方法将传入的uri解析成对应的ip与地址。 在之前提到这里传入的是代表远端的editlog,它在配置文件中的内容如下:
这里配置了三个节点ip与端口,这三个节点是配置的journalnode 节点。getLoggerAddresses方法会将这个字符串解析出各个节点的ip地址。
然后再看createLoggers方法的第12行,这里会遍历上面解析出 来的ip地址,然后利用传入的factory,创建与单个journalnode连接的对象。 这里的factory上文解析了,其实际会创建一个IPCLoggerChannel对象。
IPCLoggerChannel对象的构造方法如下:
public IPCLoggerChannel(Configuration conf,
NamespaceInfo nsInfo,
String journalId,
InetSocketAddress addr) {
this.conf = conf;
this.nsInfo = nsInfo;
this.journalId = journalId;
this.addr = addr;
this.queueSizeLimitBytes = 1024 * 1024 * conf.getInt(
DFSConfigKeys.DFS_QJOURNAL_QUEUE_SIZE_LIMIT_KEY,
DFSConfigKeys.DFS_QJOURNAL_QUEUE_SIZE_LIMIT_DEFAULT);
singleThreadExecutor = MoreExecutors.listeningDecorator(
createSingleThreadExecutor());
parallelExecutor = MoreExecutors.listeningDecorator(
createParallelExecutor());
metrics = IPCLoggerChannelMetrics.create(this);
}
这个方法主要是一些赋值操作。
自此,QuorumJournalManager类中的loggers便创建完成了。 这个loggers本身是一个AsyncLoggerSet,这个对象里主要存储了与每个 journalnode对应的IPCLoggerChannel对象。 namenode主要通过这个AsyncLoggerSet和其内部存储的IPCLoggerChannel 来与journalnode进行连接通信。