분산 시스템의 RetryCache 메커니즘

머리말


분산 시스템 작동 중에 네트워크 불안정성 (예 : 네트워크 시간 초과)으로 인한 클라이언트 요청 응답 시간 초과가 수시로 발생합니다. 이러한 종류의 낮은 확률 상황에서 클라이언트는 실제로 요청이 실제로 처리되었는지 여부를 인식 할 수 없으며 나쁜 상황 (즉, 요청이 서버에서 처리되지 않음)에 기초 할 수 있습니다. 시운전. 여기서 문제가 발생합니다. 일부 비전 원 작업의 경우 작업을 다시 시도하면 다른 결과가 반환됩니다. 이때 실제로 서버는 클라이언트의 첫 번째 요청을 성공적으로 처리했다고 가정하고 클라이언트가 시작한 두 번째 요청을 실행해서는 안됩니다. 이 기사에서는 비멱 등 연산 처리를위한 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

추천

출처blog.csdn.net/Androidlushangderen/article/details/106169970