序文
分散システムの運用中に、ネットワークの不安定性(ネットワークタイムアウトなど)によって引き起こされるクライアント要求応答タイムアウトが時々発生します。この種の確率の低い状況では、クライアントは実際に要求が実際に処理されているかどうかを認識できず、悪い状況(つまり、要求がサーバーによって処理されていない)に基づいているだけであり、再実行します。試運転。ここで問題が発生します。一部の非電源操作では、操作を再試行すると異なる結果が返されます。現時点では、実際には、サーバーがクライアントの最初の要求を正常に処理したと仮定して、サーバーはクライアントによって開始された2番目の要求を実行するべきではありません。この記事では、非理想的な操作処理のためのRetryCacheについて説明します。RetryCacheを使用すると、繰り返し要求が処理されるのを防ぐことができます。
非理想的な操作の繰り返し処理の問題
ここに戻って、非理想的な操作の繰り返し処理によって引き起こされる可能性のある問題について説明します。
簡単に要約すると、いくつかの潜在的な問題があります。
1)サーバーから返された異常な結果情報を受信したため、アプリケーションが失敗しました。非理想的なタイプがサーバーから2回目に繰り返し要求されるため、誤った結果が返される可能性があります。たとえば、ファイル作成要求が繰り返されると、システムはFileAlreadyExistExceptionのようなエラーを2回返します。
2)サーバー側のメタデータ情報の破棄。ファイルの作成操作を実行した後、関連するタスクによってファイルが読み取られ、通常どおりクリーンアップされたが、クライアントの再試行によってファイルが再度作成され、メタデータ情報が破損する可能性があるとします。
3)サーバーHAフェイルオーバースイッチング中のメタデータ整合性の問題。サービスがHAフェイルオーバー切り替えを行っている場合、サービスのアクティブ/スタンバイ切り替えは比較的重い操作です。フェイルオーバー切り替え期間中、クライアント要求がタイムアウトに応答しない場合があります。現時点では、リクエストの一部が処理され、一部が実際に処理されない場合があります。サービスがアクティブとスタンバイに切り替えられた後、サーバーステータスの完全な一貫性を確保するために、サーバーが繰り返しリクエスト処理を実行できるようにRetryCacheを使用する必要があります。もちろん、これには、内部RetryCacheを再構築するための新しいアクティブサーバーが必要です。
上記の問題を考慮して、実行された要求呼び出しの結果を格納するための内部キャッシュを導入して、非理想的な操作の繰り返し処理を防ぐ必要があります。ここでは、上記のキャッシュをRetryCacheと呼びます。
RetryCacheの実装の詳細
完全なRetryCacheを実装する場合、考慮すべき重要なポイントは何ですか?
主に以下の点が記載されています。
- クライアントは、通話の独立した識別を要求します。現在、RPCサーバーには一般にcallIdと同様の概念があり、要求を区別しますが、単一のcallIdでも、要求が同じマシンのクライアントからのものか、複数のマシンのクライアントからのものかを区別できません。ここでは、追加のclientIdフィールドを導入して、<callId + clientId>のジョイントIDメソッドを形成する必要があります。
- マークは、操作方法が非理想的であるか非理想的であるかを区別し、後者のタイプの要求の結果のみをRetryCacheに格納します。
- RetryCacheの各キャッシュエントリは永続的なストレージを保証できず、有効期限の制限が必要です。
- RetryCacheの情報の永続性と再構築プロセスが考慮されます。これは主に、HAサービスがマスタースレーブスイッチである場合に発生します。
RetryCacheの実装例
上記の実装の詳細を考慮して、Hadoopで使用されるRetryCacheクラスから取得した、より詳細な理解のための特定の例を使用します。
1つ目は、キャッシュエントリの定義です。
/**
* CacheEntry is tracked using unique client ID and callId of the RPC request
*/
public static class CacheEntry implements LightWeightCache.Entry {
/**
* Processing state of the requests
*/
private static byte INPROGRESS = 0;
private static byte SUCCESS = 1;
private static byte FAILED = 2;
/** 此entry代表的请求目前的状态,正在被处理,或者已经处理成功或失败*/
private byte state = INPROGRESS;
...
private final int callId;
private final long expirationTime;
private LightWeightGSet.LinkedElement next;
/**
* 一个全新的cache entry,它需要有clientId,callId以及过期时间.
*/
CacheEntry(byte[] clientId, int callId, long expirationTime) {
// ClientId must be a UUID - that is 16 octets.
Preconditions.checkArgument(clientId.length == ClientId.BYTE_LENGTH,
"Invalid clientId - length is " + clientId.length
+ " expected length " + ClientId.BYTE_LENGTH);
// Convert UUID bytes to two longs
clientIdMsb = ClientId.getMsb(clientId);
clientIdLsb = ClientId.getLsb(clientId);
this.callId = callId;
this.expirationTime = expirationTime;
}
...
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof CacheEntry)) {
return false;
}
CacheEntry other = (CacheEntry) obj;
// cache entry的equal通过callId和clientId联合比较,确保请求是来自重试操作的client
return callId == other.callId && clientIdMsb == other.clientIdMsb
&& clientIdLsb == other.clientIdLsb;
}
}
/**
* CacheEntry with payload that tracks the previous response or parts of
* previous response to be used for generating response for retried requests.
*/
public static class CacheEntryWithPayload extends CacheEntry {
// palyload简单理解为带了返回结果对象实例的RPC call
private Object payload;
CacheEntryWithPayload(byte[] clientId, int callId, Object payload,
long expirationTime) {
super(clientId, callId, expirationTime);
this.payload = payload;
}
以下は、コアRetryCache結果取得のメソッド呼び出しです。
*/
private CacheEntry waitForCompletion(CacheEntry newEntry) {
CacheEntry mapEntry = null;
lock.lock();
try {
// 1)从Cache中获取是否有对应Cache Entry
mapEntry = set.get(newEntry);
// 如果没有,则加入此entry到Cache中
if (mapEntry == null) {
if (LOG.isTraceEnabled()) {
LOG.trace("Adding Rpc request clientId "
+ newEntry.clientIdMsb + newEntry.clientIdLsb + " callId "
+ newEntry.callId + " to retryCache");
}
set.put(newEntry);
retryCacheMetrics.incrCacheUpdated();
return newEntry;
} else {
retryCacheMetrics.incrCacheHit();
}
} finally {
lock.unlock();
}
// Entry already exists in cache. Wait for completion and return its state
Preconditions.checkNotNull(mapEntry,
"Entry from the cache should not be null");
// Wait for in progress request to complete
// 3)如果获取到了Cache Entry,如果状态是正在执行中的,则等待其结束
synchronized (mapEntry) {
while (mapEntry.state == CacheEntry.INPROGRESS) {
try {
mapEntry.wait();
} catch (InterruptedException ie) {
// Restore the interrupted status
Thread.currentThread().interrupt();
}
}
// Previous request has failed, the expectation is that it will be
// retried again.
if (mapEntry.state != CacheEntry.SUCCESS) {
mapEntry.state = CacheEntry.INPROGRESS;
}
}
// 4)Cache Entry对应的call已经结束,则返回之前cache的结果
return mapEntry;
}
実際のRetryCache呼び出しシナリオを見てみましょう。
public long addCacheDirective(
CacheDirectiveInfo path, EnumSet<CacheFlag> flags) throws IOException {
checkNNStartup();
namesystem.checkOperation(OperationCategory.WRITE);
// 1)从RetryCache中查询是否已经是执行过的RPC call调用
CacheEntryWithPayload cacheEntry = RetryCache.waitForCompletion
(retryCache, null);
// 2)如果有同一调用,并且是成功状态的,则返回上次payload的结果
// 否则进行后续处理操作的调用
if (cacheEntry != null && cacheEntry.isSuccess()) {
return (Long) cacheEntry.getPayload();
}
boolean success = false;
long ret = 0;
try {
ret = namesystem.addCacheDirective(path, flags, cacheEntry != null);
success = true;
} finally {
// 3)操作完毕后,在RetryCache内部更新Entry的状态结果,
// 并设置payload对象(返回结果对象)
RetryCache.setState(cacheEntry, success, ret);
}
return ret;
}
上記の実装の詳細については、以下の参照リンクコードを参照してください。
見積もり
[1] .https://issues.apache.org/jira/browse/HDFS-4979
[2] .https://github.com/apache/hadoop/blob/trunk/hadoop-common-project/hadoop-common /src/main/java/org/apache/hadoop/ipc/RetryCache.java