概述
为了提升集群的吞吐、减轻Active Name负载压力,社区提出了Observer Read的设计。Observer Read是尽可能的将读请求发送到新加入集群的Observer节点来达到提升集群吞吐的目的。但是节点间元数据的同步是有一定有延时的,所以对于Observer读会存在数据一致性的问题。社区提出了元数据的一致性模型,解决了元数据同步的问题,基于此我们看一下社区的处理方式及对应的代码实现。
背景
HA架构中的HDFS使用Standby NameNode来作为Active NameNode的一个热备份,在故障转换时可以快速接管Client的请求。Standby NameNode与Active NameNode通过JournalNode来进行元数据的同步。所以Standby NameNode与Active NameNode是一摸一样的,因此Standby NameNode提供了一个绕开Active NameNode来读取数据的机会。通过Standby NameNode来读取数据会大大降低Active NameNode的压力并且也会极大的提升集群的吞吐。为了区分原有架构中Standby NameNode(不会提供读操作)对于能够提供读操作的Standby NameNode称之为Observer Node.
原理
在现有的HDFS架构中加入Observer节点,Observer节点通过JournalNode进行EditsLog的消费。然后客户端在获取到Active节点的TXID时将读请求发送到Observer节点来达到读写分流的目的,这样去减少Active NameNode的压力。
注:获取Active NameNode的TXID不会加NameNode的全局锁
Observer Read存在问题
Active NameNode与Observer节点的元数据同步使用JournalNode来完成,Observer读取元数据必然会存在一定的延迟,所以对于一些新写入的数据Observer可能无法及时同步导致用户在读取新数据报错。所以实现Observer Read的主要挑战是解决主备之间的元数据的一致性问题。
一致性问题的主要解决思路
元数据一致性问题主要的解决思路是客户端访问Observer时带上客户端已知的Active NameNode的TXID。当客户端访问Observer时,Observer消费的TXID能够达到客户端已知主节点的TXID即可。总而言之:客户端需要尽可能的知道最新的TXID。例如:某一时刻Client1访问主节点,访问主节点时主节点生成的TXID为1000,此时如果Client1要换成访问Observer,此时只要Observer消费的TXID也要达到1000就可以。解决此问题的主要方式是Client要知道访问主节点时主节点的TXID是多少,然后再访问Observer。
一致性模型的实现
Client端
主要思路
获取Active的TXID,并将Active的TXID携带到Observer进行元数据一致性的判断。 获取Active的TXID有三种方式:
-
通过ClientProtocol新加msync()方法获取Active节点TXID(客户端初始化时会调用,然后以后可以周期调用此方法通过Active来获取,周期可配。默认情况下是每次读之前都调用msync())。
-
客户端在进行写操作的时候直接更新客户端记录的Active的TXID(写操作TXID必然要增加,要保证客户端读到新写的数据,写完必须更新TXID为最新)
-
访问Observer时,Observer已知的TXID大于客户端,更新为Observer的TXID。
代码实现
为了完成获取TXID的功能,Client端主要添加了org.apache.hadoop.hdfs.server.namenode.ha.ObserverReadProxyProvider
、org.apache.hadoop.hdfs.ClientGSIContext
等类并修改了ipc.client
中相关的逻辑。允许使用Observer进行读操作的时候,客户端需要配置dfs.client.failover.proxy.provider.nameservice=org.apache.hadoop.hdfs.server.namenode.ha.ObserverReadProxyProvider。ObserverReadProxyProvider
类封装了客户端获取主节点的TXID以及进行Observer Read的实现细节。对应的ClientGSIContext
类则主要是用于对客户端记录的TXID进行更新操作。这里主要介绍ObserverReadProxyProvider
的实现。
ObserverReadProxyProvider实现
在ObserverReadProxyProvider初始化的时候,会依次初始化以下几个对象:
类名 | 变量名 | 作用 |
---|---|---|
ConfiguredFailoverProxyProvider | failoverProxy | 用于寻找主节点调用msync()进行TXID的获取、Observer读失败最终退化为Active Read |
ClientGSIContext | alignmentContext | 用于记录、更新客户端的TXID |
ProxyInfo | combinedProxy | DFSClient使用的ClientProtocol的代理,包含配置的所有NN,并使用ObserverReadInvocationHandler进行实际逻辑的处理 |
当DFSClient拿到ClientProtocol的代理对象的时候会传入对应的handler对象,然后调用ClientProtocol提供的方法,通过代理会使用对应Handler的invoke进行处理,当使用ObserverReadProxyProvider时会使用ObserverReadInvocationHandler.invoke来处理对于ClientProtocol方法的处理。客户端Observer Read的主要逻辑也被包含在了ObserverReadInvocationHandler.invoke中。其主要逻辑如下:
在这里我们了解,使用observer进行读取的时候会优先通过ConfiguredFailoverProxyProvider寻找主节点然后调用msync()方法进行TXID的的获取,获取完成后再将操作转移到observer节点。获取observer节点的是通过getHAServiceState方法获取节点的状态然后找到状态为OBSERVER的节点进行访问。对于写操作都是直接与主节点进行交互,交互的同时会更新客户端自己的TXID。
更新TXID,在客户端启动的时候会更新。后面如果配了自动更新时间间隔会在达到更新间隔后更新(**默认是每次读之前都通过msync进行同步(这样保证获取到最新的操作)**),或者在写操作时更新。
observer如果都尝试完毕后都无法完成操作,最终会退化到active read。
我们可以看到在ObserverReadInvocationHandler.invoke()方法中没有对TXID进行更新的操作?那何时会更新客户端的记录的TXID呢?
客户端TXID更新
客户端会通过序列引擎来确定序列化方式,并在创建序列化引擎的时候创建具体与NameNode地址进行通信的的对象即ipc.Client。当使用ObserverReadProxyProvider
时,会将创建与NameNode链接的代理对象的工厂类传入客户端的ClientGSIContext对象,并在创建与NN具体通信的代理对象时会将ClientGSIContext对象传入对应的Invoker对象中.在接受到返回值时会将当server端的TXID更新到ClientGSIContext中,这样让客户端记录主节点的TXID。
问题:Observer中访问Active NameNode与Observer都会更新txid。那么用户先访问了Observer然后又访问了active,但是访问Active的先返回了,后续如果访问Observer返回的时候直接更新TXID就会有问题,那怎么解决的呢?
社区使用LongAccumulator
类来记录TXID,并在创建L、ongAccumulator
对象的时候传入Math.max()算数运算符,在更新的时候使用LongAccumulator.accumulate()
方法来更新便可以保证用户获取到的都是最新的TXID。访问Observer也可以更新TXID也保证了客户端的TXID在Observer消费TXID大于客户端记录TXID以及客户端没有调用msync()或者写操作的时候保持尽可能的新。
Server端
主要思路
在Server端的新加要给HA的状态HAServiceState.OBSERVER来允许读操作。其他的更改主要是在ipc.Server类中。主要的思路是将客户端的C-TXID与当前NN的N-TXID对比并进行处理。在Server端对TXID记录是由GlobalStateIdContext类对象,此对象与客户端ClientGSIContext都是实现了AlignmentContext接口,但是GlobalStateIdContext类对象会通过fsnamesystem获取NN的TXID,并判断客户端传入的TXID。此对象也会在NN启动时传入ipc.server对象中。
-
如果C-TXID<N-TXID(NN已经追上),直接由Handler执行
-
如果C-TXID>N-TXID&&NN is far behind,抛出
RetriableException
由客户端重试(如果Server消费落后的TXID耗费时间会超过客户端连接最长无请求时间) -
如果C-TXID>N-TXID&&NN is not far behind, reEnQueue 等待NN追上。
代码实现
Server.Connection#processRpcRequest
if(alignmentContext != null && call.rpcRequest != null &&
(call.rpcRequest instanceof ProtobufRpcEngine2.RpcProtobufRequest)) { //Client的TXID的开始
String methodName;
String protoName;
ProtobufRpcEngine2.RpcProtobufRequest req =
(ProtobufRpcEngine2.RpcProtobufRequest) call.rpcRequest;
try {
methodName = req.getRequestHeader().getMethodName();
protoName = req.getRequestHeader().getDeclaringClassProtocolName();
if (alignmentContext.isCoordinatedCall(protoName, methodName)) {
call.markCallCoordinated(true);//在connection中进行封装
long stateId;
stateId = alignmentContext.receiveRequestState(//判断当前Client的TXID与NN的TXID
header, getMaxIdleTime());
call.setClientStateId(stateId);
}
} catch (IOException ioe) {
throw new RpcServerException("Processing RPC request caught ", ioe);
}
}
GlobalStateIdContext#receiveRequestState():判断客户端的TXID与当前NN的TXID
public long receiveRequestState(RpcRequestHeaderProto header,
long clientWaitTime) throws IOException {
if (!header.hasStateId() &&
HAServiceState.OBSERVER.equals(namesystem.getState())) {//客户但没配Observer并且达到obser的时候
throw new StandbyException("Observer Node received request without "
+ "stateId. This mostly likely is because client is not configured "
+ "with " + ObserverReadProxyProvider.class.getSimpleName());
}
long serverStateId = getLastSeenStateId();//通过FSIMage获取最新的TXID
long clientStateId = header.getStateId();
FSNamesystem.LOG.trace("Client State ID= {} and Server State ID= {}",
clientStateId, serverStateId);
if (clientStateId > serverStateId &&
HAServiceState.ACTIVE.equals(namesystem.getState())) {//大于active的时候设置为active的TXID。一般不会出现
FSNamesystem.LOG.warn("The client stateId: {} is greater than "
+ "the server stateId: {} This is unexpected. "
+ "Resetting client stateId to server stateId",
clientStateId, serverStateId);
return serverStateId;
}
if (HAServiceState.OBSERVER.equals(namesystem.getState()) &&
clientStateId - serverStateId >//如果observer的txid落后太多,会抛重试异常由客户端重试
ESTIMATED_TRANSACTIONS_PER_SECOND
* TimeUnit.MILLISECONDS.toSeconds(clientWaitTime)
* ESTIMATED_SERVER_TIME_MULTIPLIER) {
throw new RetriableException(//根据时间来判断的
"Observer Node is too far behind: serverStateId = "
+ serverStateId + " clientStateId = " + clientStateId);
}
return clientStateId;
}
Server.Handler#run()
call = callQueue.take(); // pop the queue; maybe blocked here
startTimeNanos = Time.monotonicNowNanos();
if (alignmentContext != null && call.isCallCoordinated() &&
call.getClientStateId() > alignmentContext.getLastSeenStateId()) { //协作的操作可以重新放回call队列中
/*
* The call processing should be postponed until the client call's
* state id is aligned (<=) with the server state id.
// Re-queue the call and continue
*/
requeueCall(call);//重新放到callqueue中
continue;
}
流程综述
HDFS Observer流程综述图:
JN的优化
JournalNode对于所有成功事务都需要持久化到磁盘当中,当Standby读取EditsLog的时候JN需要大量的磁盘操作。为了加速对于EditsLog的回访,社区对于新写的EditsLog加入了缓存,此时Standby可以通过RPC消费缓存中的EditsLog,这样会减少大量的I/O操作,以及减少读操作的等待TXID回放时间。具体实现请参考:Allow SbNN to tail in-progress edits from JN via RPC
Observer failover
Observer NameNode参与故障转移的功能尚未实现。 因此,仅应使用haadmin -transitionToObserver -forcemanual
来启动Observer并将其放置在ZooKeeper控制的故障转移组之外。 因此Observer NameNode的主机不能运行ZKFC。
主要配置
<property>
<name>dfs.client.failover.proxy.provider.nameservice</name>
<value>org.apache.hadoop.hdfs.server.namenode.ha.ObserverReadProxyProvider</value>
<description>希望使用Observer NameNode进行读取访问的客户端可以在客户端的hdfs-site.xml配置文件中设置提供远程代理对象的实现类</description>
</property>
<property>
<name>hadoop.rpc.socket.factory.class.default<nameservice></name>
<value>org.apache.hadoop.net.StandardSocketFactory</value>
<description>连接NameNode使用的socket工厂类</description>
</property>
<property>
<name>dfs.namenode.state.context.enabled<nameservice></name>
<value>false</value>
<description>默认false,</description>
</property>
<property>
<name>dfs.client.failover.observer.probe.retry.period</name>
<value> 60 * 10 * 1000</value>
<description>默认十分钟</description>
</property>
<property>
<name>dfs.client.failover.observer.auto-msync-period.nnhost</name>
<value>-1</value>
<description>自动获取TXID的间隔,默认客户端启动后不会自动获取TXID</description>
</property>
参考
(jira: HDFS-12943) Consistent Reads from Standby Node
(jira: HDFS-13150) Allow SbNN to tail in-progress edits from JN via RPC
(CSDN: blog) HDFS Standby NameNode Read功能剖析
(apache: doc) Consistent Reads from HDFS Observer NameNode
(jira: HDFS-13182) Allow Observer to participate in NameNode failover