序文
HDFSには、サービスの高可用性を確保するための非常に成熟したHAメカニズムがあります。HAモードでは、それぞれアクティブとスタンバイのNameNodeサービスがあります。Active NameNodeは外部データサービスを提供するために使用されますが、Standby NameNodeはチェックポイントを担当し、現在のActive NameNodeが予期せず使用できなくなった場合に、いつでもActiveNameNodeの役割を引き継ぐ準備ができています。実際、スタンバイNameNodeは日常業務があまりなく、定期的なチェックポイントとメタデータ情報の準リアルタイム同期を除いて、外部クライアントによって開始された読み取りおよび書き込み要求を処理しないため、スタンバイNameNodeサービスの負荷は比較的高くなります。低。Active NameNodeのサービスプレッシャーが重くなっている場合、Standby NameNodeに読み取りプレッシャーの一部をオフロードさせることはできますか?Hadoopコミュニティはこのアイデアを提案し、この機能を非常に早い段階で実現しました。この記事の作成者は、コードの一部を組み合わせて、HDFSスタンバイ読み取りの実装原理を簡単に分析します。
HDFSスタンバイ読み取りの背景と機能要件
まず、HDFSスタンバイ読み取りの背景と機能要件について説明します。Active NameNodeクラスターの継続的な拡張に伴い、そのサービスプレッシャーは増大します。この場合、私たちの一般的なアプローチは、HDFSフェデレーションを形成することによって水平サービス拡張の目標を達成することです。ただし、この方法ではNameNode自体のサービス機能がさらに最適化されることはなく、HDFSスタンバイ読み取りの機能はこの領域の大きな強化になります。
スタンバイ読み取りモードでは、HDFSの元の書き込み要求は引き続きActiveNameNodeによって処理されます。スタンバイサービスは読み取り操作の処理のみをサポートするため、NameNodeのメインロジックに大きな変更はありません。しかし、最も解決する必要のある問題は、スタンバイの一貫した読み取りの問題です。
スタンバイNameNodeがJournalNodeの編集ログを読み取って、トランザクションを同期することがわかっています。アクティブNameNodeに編集ログを書き込んでから、スタンバイNameNodeに移動して、この編集ログのバッチを読み取ります。途中に時間のギャップがあります。したがって、スタンバイ読み取り機能を実装する場合、読み取り要求をスタンバイNNに直接転送するだけでなく、これで完了です。ここでは、トランザクションの同期を待機する問題が発生します。次の著者は、このコミュニティがどのようにそれを行うかを詳細に紹介します。
スタンバイNameNodeの一貫した読み取りの実現を制御します
主成分分析
前のセクションで説明したスタンバイNameNodeのステータス同期の問題を考慮して、スタンバイNameNodeは、クライアントの読み取り要求操作の処理を許可する前に、クライアントの最後のtxidに到達する必要があります。
上記の文はどういう意味ですか?クライアントの場合、RPC要求を開始すると、アクティブNameNodeとスタンバイNameNodeはそれぞれ独自の現在のtxidを持ち、アクティブNameNodeのtxidはスタンバイのtxidよりも大きくなければなりません。ここでは、アクティブtxidをann txidとしてマークし、スタンバイをsnntxidとしてマークします。この時点で、クライアントがアクティブサービスへのフォローアップ要求を開始した場合、データ遅延の問題はなく、アクティブは常に最新の状態です。ただし、スタンバイNameNodeがクライアントの要求も処理できるようにする場合は、クライアントがRPCを開始した瞬間から、少なくともアクティブNameNodeのtxidの状態に到達する必要があります。つまり、snntxidもanntxidに到達する必要があります。値。
プロセスのこの部分は、次のように簡単に説明されます。
1)クライアントがRPC要求を開始する前に、現在のアクティブなNameNodeのtxid値を取得します。ここでは、これをlastSeenTxidと呼びます。
2)次に、クライアントはスタンバイNameNodeへの読み取り要求を開始し、前のステップのlastSeenTxid値がこの要求に含まれます。
3)スタンバイNameNodeが前のステップでRPC要求を処理するとき、現在のtxidがクライアントのlastSeenTxid値に到達したかどうかを比較し、値に到達した場合は正常に要求を処理し、そうでない場合は要求を再挿入します。 RPC呼び出しキューに入れて、次回処理されるのを待ちます。クライアントの場合、キューに再入する要求は、RPC呼び出しがまだ処理されていないことを意味します。
クライアントがスタンバイNameNodeがlastSeenTxid状態に達するまで長時間待機する可能性がある状況を回避するために、コミュニティは、editlog_inprogressで読み取られたトランザクションのサポートなど、スタンバイNameNode編集ログの同期にいくつかの改善を加えました。編集ログ情報のメモリ読み取り。
後で、作成者は実際のコードを組み合わせて上記のプロセスを分析します。
コード分析
実装の過程で、コミュニティは、クライアントとサーバーが「見る」ことができるtxid値を格納するために2つのクラスを定義しました。
クライアントに対応するクラスはClientGSIContextと呼ばれ、サーバー側のクラス(NameNode)はGlobalStateIdContextと呼ばれます。
まず、クライアント側のこのクラス、ClientGSIContextについて説明しましょう。lastSeenStateIdの値は、ClientGSIContextクラスによって内部的に維持されます。コードは次のとおりです。
/**
* Global State Id context for the client.
* <p>
* This is the client side implementation responsible for receiving
* state alignment info from server(s).
*/
@InterfaceAudience.Private
@InterfaceStability.Evolving
public class ClientGSIContext implements AlignmentContext {
private final LongAccumulator lastSeenStateId =
new LongAccumulator(Math::max, Long.MIN_VALUE);
...
}
lastSeenStateIdの値の更新と取得は、主にRPC応答フェーズが受信されたとき(現在のlastSeenStateId値を更新)とRPC要求が送信されたとき(現在のlastSeenStateId値を設定)に発生します。このクラスのロジックで。
/**
* Client接收到请求response时更新当前的lastSeenStateId值。
*/
@Override
public void receiveResponseState(RpcResponseHeaderProto header) {
lastSeenStateId.accumulate(header.getStateId());
}
/**
* Client发起请求时设置当前的lastSeenStateId值信息到RPC请求里。
*/
@Override
public void updateRequestState(RpcRequestHeaderProto.Builder header) {
header.setStateId(lastSeenStateId.longValue());
}
次に、サーバー側のGlobalStateIdContextがどのように処理されるかを見ていきます。1つ目は、GlobalStateIdContextのクラス定義です。
/**
* This is the server side implementation responsible for passing
* state alignment info to clients.
*/
@InterfaceAudience.Private
@InterfaceStability.Evolving
class GlobalStateIdContext implements AlignmentContext {
/**
* Estimated number of journal transactions a typical NameNode can execute
* per second. The number is used to estimate how long a client's
* RPC request will wait in the call queue before the Observer catches up
* with its state id.
*/
private static final long ESTIMATED_TRANSACTIONS_PER_SECOND = 10000L;
/**
* The client wait time on an RPC request is composed of
* the server execution time plus the communication time.
* This is an expected fraction of the total wait time spent on
* server execution.
*/
private static final float ESTIMATED_SERVER_TIME_MULTIPLIER = 0.8f;
/** FSNamesystem用来获取当前最新的txid值 */
private final FSNamesystem namesystem;
private final HashSet<String> coordinatedMethods;
...
}
GlobalStateIdContextがクライアントのRPC要求を処理するとき、主に次の2つのことを行います。
- 1)RPC要求を受信した後、RPC要求からクライアントのlastSeenTxidを抽出し、それを自身の最新のtxidと比較します。
- 2)RPCの処理後、RPC応答を設定するときに、最新のtxidをクライアントのlastSeenTxidに設定します。これは、クライアントがこの時点で更新されたtxidステータスを確認したことを意味します。
上記2つの部分の対応する操作方法は次のとおりです。
/**
* Server端处理完RPC后,设置RPC response时,设置自身最新的txid到client的lastSeenTxid里。
*/
@Override
public void updateResponseState(RpcResponseHeaderProto.Builder header) {
// Using getCorrectLastAppliedOrWrittenTxId will acquire the lock on
// FSEditLog. This is needed so that ANN will return the correct state id
// it currently has. But this may not be necessary for Observer, may want
// revisit for optimization. Same goes to receiveRequestState.
header.setStateId(getLastSeenStateId());
}
/**
* Server端请求状态的判断处理逻辑。
*/
@Override
public long receiveRequestState(RpcRequestHeaderProto header,
long clientWaitTime) throws IOException {
if (!header.hasStateId() &&
HAServiceState.OBSERVER.equals(namesystem.getState())) {
// This could happen if client configured with non-observer proxy provider
// (e.g., ConfiguredFailoverProxyProvider) is accessing a cluster with
// observers. In this case, we should let the client failover to the
// active node, rather than potentially serving stale result (client
// stateId is 0 if not set).
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();
long clientStateId = header.getStateId();
FSNamesystem.LOG.trace("Client State ID= {} and Server State ID= {}",
clientStateId, serverStateId);
if (clientStateId > serverStateId &&
HAServiceState.ACTIVE.equals(namesystem.getState())) {
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;
}
// 如果当前client的lastSeenTxid值远远大于当前server端的txid值,则抛出异常。
// 如果是小于serverStateId或者在正常范围内,则继续处理。
if (HAServiceState.OBSERVER.equals(namesystem.getState()) &&
clientStateId - serverStateId >
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;
}
// 获取自身当前最新的txid值
@Override
public long getLastSeenStateId() {
// Should not need to call getCorrectLastAppliedOrWrittenTxId()
// see HDFS-14822.
return namesystem.getFSImage().getLastAppliedOrWrittenTxId();
}
上記のreceiveRequestStateは、クライアント要求がrpcキューに入るための予備的な検証プロセスにすぎないことに注意してください。後続のハンドラーがrpcキューから呼び出し処理を取得すると、クライアントのlastSeenTxidとサーバーのtxidが比較されます。
/** Handles queued calls . */
private class Handler extends Thread {
public Handler(int instanceNumber) {
this.setDaemon(true);
this.setName("IPC Server handler "+ instanceNumber +
" on default port " + port);
}
@Override
public void run() {
LOG.debug(Thread.currentThread().getName() + ": starting");
SERVER.set(Server.this);
while (running) {
TraceScope traceScope = null;
Call call = null;
long startTimeNanos = 0;
// True iff the connection for this call has been dropped.
// Set to true by default and update to false later if the connection
// can be succesfully read.
boolean connDropped = true;
try {
1)从call queue 中获取一个call进行处理
call = callQueue.take(); // pop the queue; maybe blocked here
startTimeNanos = Time.monotonicNowNanos();
// 如果这个call是支持Standby Read,且其client seen txid大于server端txid,则执行此call的重新进queue操作,延迟这个call的处理,等待server端的txid reach到client 的txid值
if (alignmentContext != null && call.isCallCoordinated() &&
call.getClientStateId() > alignmentContext.getLastSeenStateId()) {
/*
* The call processing should be postponed until the client call's
* state id is aligned (<=) with the server state id.
* NOTE:
* Inserting the call back to the queue can change the order of call
* execution comparing to their original placement into the queue.
* This is not a problem, because Hadoop RPC does not have any
* constraints on ordering the incoming rpc requests.
* In case of Observer, it handles only reads, which are
* commutative.
*/
// Re-queue the call and continue
requeueCall(call);
continue;
}
...
}
}
分析のこの時点で、スタンバイ読み取りのサーバーロジック分析はほぼ完了していますが、上記の手順に戻りましょう。
1)クライアントがRPC要求を開始する前に、現在のアクティブなNameNodeのtxid値を取得します。ここでは、これをlastSeenTxidと呼びます。
2)次に、クライアントはスタンバイNameNodeへの読み取り要求を開始し、前のステップのlastSeenTxid値がこの要求に含まれます。
…
クライアントはどのようにしてアクティブNameNodeへの要求を開始し、最新のtxidを取得してから、スタンバイNameNodeへの後続の読み取り要求を開始します。これには2つのRPC呼び出しが含まれます。
コミュニティは、この部分のロジックをカプセル化するために、新しいProxyProviderクラスObserverReadProxyProviderを実装しました。ObserverReadInvocationHandlerのロジックでは、アクティブNameNodeにmsync呼び出しを送信して、クライアント側のClientGSIContextのlastSeenStateIdを同期してから、スタンバイNameNodeへの読み取り要求を開始します(ClientGSIContext#receiveResponseState操作はクライアント処理応答方式)。
この部分のロジックは次のとおりです、ObserverReadProxyProviderクラス。
private class ObserverReadInvocationHandler implements RpcInvocationHandler {
@Override
public Object invoke(Object proxy, final Method method, final Object[] args)
throws Throwable {
lastProxy = null;
Object retVal;
// 如果开启了Standby Read的功能并且,RPC call的请求方法是Read类型的
if (observerReadEnabled && shouldFindObserver() && isRead(method)) {
if (!msynced) {
// An msync() must first be performed to ensure that this client is
// up-to-date with the active's state. This will only be done once.
initializeMsync();
} else {
// 在每次发起请求时,先执行一遍msync操作方法到Active NameNode,进行client lastSeemTxid的同步
autoMsyncIfNecessary();
}
int failedObserverCount = 0;
int activeCount = 0;
int standbyCount = 0;
int unreachableCount = 0;
// 后续发起请求到Standby NameNode进行读请求的处理
for (int i = 0; i < nameNodeProxies.size(); i++) {
NNProxyInfo<T> current = getCurrentProxy();
HAServiceState currState = current.getCachedState();
if (currState != HAServiceState.OBSERVER) {
if (currState == HAServiceState.ACTIVE) {
activeCount++;
} else if (currState == HAServiceState.STANDBY) {
standbyCount++;
} else if (currState == null) {
unreachableCount++;
}
LOG.debug("Skipping proxy {} for {} because it is in state {}",
current.proxyInfo, method.getName(),
currState == null ? "unreachable" : currState);
changeProxy(current);
continue;
}
...
}
// 其它非读类型的请求,还是访问Active NameNode
LOG.debug("Using failoverProxy to service {}", method.getName());
ProxyInfo<T> activeProxy = failoverProxy.getProxy();
try {
retVal = method.invoke(activeProxy.proxy, args);
} catch (InvocationTargetException e) {
// This exception will be handled by higher layers
throw e.getCause();
}
// If this was reached, the request reached the active, so the
// state is up-to-date with active and no further msync is needed.
msynced = true;
lastMsyncTimeMs = Time.monotonicNow();
lastProxy = activeProxy;
return retVal;
}
}
プロセス分析図
上記のコードロジックとプロセス分析を組み合わせると、HDFSスタンバイ読み取り機能のプロセス図は次のようになります。
上の図のオブザーバーNameNodeは、スタンバイ読み取り機能で導入された新しい役割であり、本質的により軽量なスタンバイNameNodeです。元のスタンバイとの主な違いは、チェックポイントなどの操作を実行しないことです。NameNodeオブザーバーとスタンバイのステータスは相互に変換できますが、オブザーバーNameNodeはアクティブなNameNodeのステータスを直接切り替えることはできません。
HDFSスタンバイ読み取りの実装では、実装の半分以上がSNNの最適化にあり、編集ログをすばやく読み取ることができます。この部分に関心のある学生は、参照リンクを読むことができます。
参照リンク
[1]。https://issues.apache.org/jira/browse/HDFS-12943。スタンバイノードからの一貫した読み取り