머리말
분산 시스템 작동 중에 네트워크 불안정성 (예 : 네트워크 시간 초과)으로 인한 클라이언트 요청 응답 시간 초과가 수시로 발생합니다. 이러한 종류의 낮은 확률 상황에서 클라이언트는 실제로 요청이 실제로 처리되었는지 여부를 인식 할 수 없으며 나쁜 상황 (즉, 요청이 서버에서 처리되지 않음)에 기초 할 수 있습니다. 시운전. 여기서 문제가 발생합니다. 일부 비전 원 작업의 경우 작업을 다시 시도하면 다른 결과가 반환됩니다. 이때 실제로 서버는 클라이언트의 첫 번째 요청을 성공적으로 처리했다고 가정하고 클라이언트가 시작한 두 번째 요청을 실행해서는 안됩니다. 이 기사에서는 비멱 등 연산 처리를위한 RetryCache에 대해 설명하고 RetryCache를 통해 반복되는 요청 처리를 방지 할 수 있습니다.
비멱 등 연산의 반복 처리 문제
여기서는 멱 등성이 아닌 작업의 반복적 인 처리로 인해 발생할 수있는 문제에 대해 이야기합니다.
간단히 요약하면 몇 가지 잠재적 인 문제가 있습니다.
1) 서버에서 반환 한 비정상 결과 정보를 수신하여 신청이 실패했습니다. 비멱 등성 유형은 서버에서 두 번째로 반복적으로 요청하므로 잘못된 결과가 반환 될 수 있습니다. 예를 들어 파일 생성 요청이 반복되면 시스템은 두 번째로 FileAlreadyExistException과 같은 오류를 반환합니다.
2) 서버 측의 메타 데이터 정보 파기. 파일 생성 작업을 수행 한 다음 관련 작업에서 파일을 읽고 정상적으로 정리했지만 클라이언트의 재시 도로 인해 파일이 다시 생성되어 메타 데이터 정보가 손상 될 수 있다고 가정합니다.
3) 서버 HA 장애 조치 전환 중 메타 데이터 일관성 문제. 서비스가 HA 장애 조치 전환을 수행 할 때 서비스 활성 / 대기 전환은 상대적으로 무거운 작업이며 장애 조치 전환 기간 동안 클라이언트 요청이 타임 아웃에 응답하지 않는 경우가 있습니다. 이때 요청의 일부가 처리 될 수 있으며 일부는 실제로 처리되지 않을 수 있습니다. 서비스가 Active와 Standby로 전환 된 후 서버 상태의 완전한 일관성을 보장하기 위해 RetryCache를 사용하여 서버가 반복적 인 요청 처리를 수행 할 수 있도록해야합니다. 물론 내부 RetryCache를 재 구축하려면 새로운 활성 서버가 필요합니다.
위의 문제를 고려하여 비멱 등 연산의 반복 처리를 방지하기 위해 실행 된 요청 호출의 결과를 저장하는 내부 캐시를 도입해야합니다. 여기서는 위의 캐시를 RetryCache라고합니다.
RetryCache의 구현 세부 사항
완전한 RetryCache를 구현하려는 경우 고려해야 할 핵심 사항은 무엇입니까?
다음 사항이 주로 여기에 나열됩니다.
- 클라이언트는 호출의 독립적 인 식별을 요청합니다. 현재 RPC 서버는 일반적으로 요청을 구별하기 위해 callId와 유사한 개념을 가지고 있지만 단일 callId는 요청이 동일한 시스템의 클라이언트에서 오는지 아니면 여러 시스템의 클라이언트에서 오는지 구별 할 수 없습니다. 여기서 우리는 <callId + clientId>의 공동 ID 메서드를 형성하기 위해 추가 clientId 필드를 도입해야합니다.
- 이 표시는 작업 방법이 멱등인지 비멱 등인지를 구분하며 후자의 유형 요청 결과 만 RetryCache에 저장합니다.
- RetryCache 내의 각 캐시 항목은 영구 저장을 보장 할 수 없으며 만료 시간 제한이 있어야합니다.
- RetryCache의 정보 지속성 및 재구성 프로세스를 고려하며 이는 주로 HA 서비스가 마스터-슬레이브 스위치 인 경우에 발생합니다.
RetryCache 구현 예
위의 구현 세부 사항을 고려하여보다 자세한 이해를 위해 Hadoop에서 사용하는 RetryCache 클래스에서 가져온 특정 예제를 사용합니다.
첫 번째는 캐시 항목의 정의입니다.
/**
* 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