HDFS 스냅 샷을 삭제할 수없는 문제 해결

머리말


우리 모두 알다시피 HDFS에는 실수로 데이터가 삭제되는 것을 방지하는 데 사용할 수있는 매우 유용한 스냅 샷 기능이 있습니다. 어떤 사람들은 데이터가 삭제되었다고 말할 수 있습니다. 휴지통 디렉토리에서 데이터를 복원 할 수 없습니까? HDFS 스냅 샷은 일반적으로 데이터가 휴지통 디렉토리로 삭제된다고 말하는 것과 동일하지 않습니다. 휴지통 디렉토리로의 HDFS 삭제 작업은 지연된 작업 삭제 전략입니다. 사용자가 실제로 데이터를 삭제하는 데이터 작업 동작을 수행하는 경우 (여기서는이 데이터가 네임 스페이스 수준에서 완전히 제거되고 휴지통에도 존재하지 않음을 나타냄) 데이터에 대한 스냅 샷 보호를 활성화했다고 가정하면 이번에는 복구 방법은 HDFS 스냅 샷에서 복구 할 수 있습니다. 그러나 스냅 샷에서 데이터를 복구하는 경우 스냅 샷 디렉토리에서 실제 삭제 된 경로로 이름을 바꾸는 간단한 작업이 아니라 실제 물리적 데이터의 복사본이 포함됩니다. 이 시점에서 스냅 샷과 휴지통의 복구 처리는 여전히 다릅니다. 이 기사의 저자는 내부 클러스터에서 HDFS 스냅 샷을 삭제할 수 없다는 문제를 공유 할 것입니다. 전체 문제 해결 프로세스의 타임 라인이 비교적 길고 중간에 우회가 많이있었습니다.

배경


HDFS 스냅 샷을 사용하는 내부 클러스터의 전략은 데이터 보호를 위해 일일 스냅 샷 전략을 사용하는 것입니다. 간단히 말해 24 시간 이내에 발생하는 실수로 인한 데이터 삭제에 대해서만 보호하고 있으며,이 시간 이전에 데이터가 삭제되거나 손실되는 경우이를 보장하지 않습니다. Snapshot 보유 시간이 더 길면 삭제되어야하는 Snapshot이 보유한 데이터가 점점 더 커질 것임을 의미하기 때문입니다. 좁은 클러스터 스토리지 공간이 여기에 많이 사용됩니다. 어느 날 갑자기 문제가 발생하여 클러스터의 저장 공간이 점점 더 커지고 NN 메타 데이터 볼륨의 총 객체 수가 여전히 높은 것을 발견했습니다. 나중에 많은 대형 디렉토리의 일일 스냅 샷이 삭제되지 않았 음을 발견했습니다. 일일 스냅 샷은 다음 그림과 유사합니다.
여기에 사진 설명 삽입
그런 다음 스냅 샷 생성 및 삭제와 관련된 스크립트를 빠르게 확인한 결과 deleteSnapshot delete 명령을 실행할 때 NPE 예외가 발생하고 그날의 스냅 샷이 제 시간에 삭제되지 않았 음을 확인했습니다. . 그런 다음 다음 날 후속 일일 스냅 샷이 다시 생성되기 시작했지만 삭제되지 않았습니다.

문제가 발생했습니다. 이제 가장 먼저해야 할 일은 이러한 중복 스냅 샷을 삭제하는 방법입니다. 삭제할 수없는 경우 클러스터의 저장 공간이 조만간 폭발 할 것입니다. 두 번째 단계는 근본 원인을 분석하는 것입니다.

문제 스냅 샷 정리


정리할 수없는 이러한 스냅 샷의 경우 deleteSnapshot을 사용하여 다시 실행하려고했습니다. 결과는 여전히 NPE 오류 였지만 deleteSnapshot은 여전히 ​​실패했습니다. 발생한 예외 스택 정보는 다음과 같습니다.

java.lang.NullPointerException
 at org.apache.hadoop.hdfs.server.namenode.INodeFile.storagespaceConsumedNoReplication(INodeFile.java:706)
 at org.apache.hadoop.hdfs.server.namenode.INodeFile.storagespaceConsumed(INodeFile.java:692)
 at org.apache.hadoop.hdfs.server.namenode.snapshot.FileWithSnapshotFeature.updateQuotaAndCollectBlocks(FileWithSnapshotFeature.java:147)
 at org.apache.hadoop.hdfs.server.namenode.snapshot.FileDiff.destroyDiffAndCollectBlocks(FileDiff.java:118)
 at org.apache.hadoop.hdfs.server.namenode.snapshot.FileDiff.destroyDiffAndCollectBlocks(FileDiff.java:38)
 at org.apache.hadoop.hdfs.server.namenode.snapshot.AbstractINodeDiffList.deleteSnapshotDiff(AbstractINodeDiffList.java:94)
 at org.apache.hadoop.hdfs.server.namenode.snapshot.FileWithSnapshotFeature.cleanFile(FileWithSnapshotFeature.java:135)
 at org.apache.hadoop.hdfs.server.namenode.INodeFile.cleanSubtree(INodeFile.java:504)
 at org.apache.hadoop.hdfs.server.namenode.INodeDirectory.cleanSubtreeRecursively

나중에 NN의 로컬 로그를 확인했지만 경로가 삭제 된 NPE 오류를 찾지 못했습니다. 현재 우리의 가상 문제는 HDFS NN 메모리의 네임 스페이스 메타 데이터가 문제가있는 것으로 추정된다는 것입니다.

스냅 샷은 명령을 통해 어떻게 삭제해도 삭제할 수없고 NN의 메모리 데이터에 문제가있는 것으로 의심되므로 NN을 다시 시작하고 새로 다시 시작된 NN으로 장애 조치 한 후 deleteSnapshot을 실행했습니다. 다시 스냅 샷이 마침내 정리되었습니다.

그러나 이것은 문제의 시작일 뿐이며 실제 근본 원인이 무엇인지 모릅니다. 처음에는 NN을 다시 시작하면 일시적으로 문제가 해결 될 수 있다고 생각하면서 이것이 가끔 문제라고 생각했지만 바로 삭제할 수없는 스냅 샷 문제가 며칠 후 다시 나타날 것이라고는 예상하지 못했습니다. 나중에 우리는 먼저 스냅 샷 기능을 비활성화하고 문제가 발생했을 때 NN의 fsimage 파일을 유지했습니다. 그런 다음 후속 문제에 대한 분석을 시작할 준비가되었습니다.

스냅 샷 NPE 비정상 코드 분석


NPE 예외를 발생시키는 Snapshot 코드 로직을 분석했으며 예외가 발생하는 위치는

  public final long storagespaceConsumedNoReplication() {
    
    
    FileWithSnapshotFeature sf = getFileWithSnapshotFeature();
    if(sf == null) {
    
    
      return computeFileSize(true, true);
    }

    // Collect all distinct blocks
    long size = 0;
    Set<Block> allBlocks = new HashSet<Block>(Arrays.asList(getBlocks()));
    List<FileDiff> diffs = sf.getDiffs().asList();
    for(FileDiff diff : diffs) {
    
    
      BlockInfoContiguous[] diffBlocks = diff.getBlocks();  <===== diff is null
      if (diffBlocks != null) {
    
    
        allBlocks.addAll(Arrays.asList(diffBlocks));
      }
    }
    for(Block block : allBlocks) {
    
    
      size += block.getNumBytes();
    }
    // check if the last block is under construction
    BlockInfoContiguous lastBlock = getLastBlock();
    if(lastBlock != null &&
        lastBlock instanceof BlockInfoContiguousUnderConstruction) {
    
    
      size += getPreferredBlockSize() - lastBlock.getNumBytes();
    }
    return size;
  }

그런 다음 부모 클래스 AbstractINodeDiffList에서 상속되는 sf.getDiffs ()에 해당하는 FileDiffList 클래스를 입력합니다. 여기서 중요한 문제는 AbstractINodeDiffList 목록에 null 요소가 있다는 것입니다. 그러나이 목록 클래스의 삽입 메소드를 살펴보면 다음 addDiff 메소드 만 삽입 작업을 수행합니다.

  /** Add an {@link AbstractINodeDiff} for the given snapshot. */
  final D addDiff(int latestSnapshotId, N currentINode) {
    
    
    return addLast(createDiff(latestSnapshotId, currentINode));
  }

그리고 프로그램이 addDiff를 실행할 때마다이 diff는 위의 createDiff 연산에 의해 생성되며 diffList에 null이 삽입되지 않아야합니다.

이 코드 분석에서 우리는 딜레마에 처해 있습니다. 후속 코드 레벨 수정에서 개선을 위해 다음 두 단계를 수행했습니다.

1) diff에서 null 항목 건너 뛰기
2) 스냅 샷 diff와 관련된 경로 정보 인쇄

나중에 위의 변경 사항을 재배포 한 후에도 NN은 diffList가 순회되는 다른 위치에서 NPE 오류를 계속보고하며 경로 정보는 우리에게 충분하지 않습니다. 나중에이 문제를 오프라인으로 재현하려고했는데 프로덕션 클러스터에서이 문제를 디버깅하는 것은 비용이 많이 들고 위험합니다.

오프라인 스냅 샷 문제를 복원하지 못했습니다.


새로운 코드를 온라인으로 배포 한 후에도 문제의 근본 원인을 찾는 데 여전히 도움이되지 않습니다. 따라서 순수 NN 모드 테스트 (JN, DN, HA 모드 없음)를 위해 이전에 백업 한 fsimage 파일을 다른 시스템에 복사 할 계획입니다.이 부분의 작업 단계는 작성자가 작성한 블로그 게시물 HDFS NameNode 를 참조하십시오. fsimage 파일이 손상되었습니다. 해결 방법 .

또한 우리는 우리가 만난 문제와 관련된 커뮤니티에서 관련 JIRA 문제를 찾고 있습니다. 이 과정에서 deleteSnapshot, HDFS-9406 (스냅 샷 삭제 후 FSImage가 손상 될 수 있음) 및 HDFS-13101 (스냅 샷과 관련된 또 다른 fsimage 손상 )과 매우 관련된 두 개의 JIRA를 발견했습니다 . 이전 문제는 이미 우리 버전에 있으므로 HDFS-13101의 문제 만 확인했습니다. 마지막으로 현재 Hadoop 버전에서 후자의 문제를 성공적으로 재현했습니다. 그러나 나중에 추가 분석, HDFS-13101과 우리의 스냅 샷 장면은 동일하지 않습니다.

첫째, HDFS-13101이 스냅 샷을 삭제하면 동시에 두 개의 스냅 샷이 포함됩니다.
둘째, 데이터 교차 스냅 샷 이름이 변경되는 상황이 있습니다.

사용 시나리오에서는 하나의 데이터 디렉토리 만 하나의 스냅 샷에 해당합니다. 이전 스냅 샷을 삭제해야만 다음 스냅 샷 생성을 시작할 수 있습니다. 따라서 나중에 HDFS-13101이 문제를 해결하는 방법이 아니라는 것을 분석했습니다.

이 유사한 문제가 커뮤니티에서 발견되지 않았기 때문에 내부 코드 변경으로 인한 스냅 샷 버그입니까? 우리는 내부 변화의 논리로 인한 버그라고 점점 더 의심하고 있습니다.

HDFS 내부 코드 변경의 재구성 및 분석


문제가있는 코드 메서드 AbstractINodeDiff # addDiff에 대한 호출 논리를 분석 한 결과 의심스러운 내부 변경 논리를 발견했습니다.

이전에 NN의 성능을 최적화했을 때 setTimes를 호출하면 경로의 액세스 시간 만 변경되었지만 쓰기 잠금 작업이 유지되어 NN에 더 큰 영향을 미치므로 setTimes의 쓰기 잠금 작업을 변경했습니다. 읽기 및 쓰기 작업.

  static boolean setTimes(
      FSDirectory fsd, INode inode, long mtime, long atime, boolean force,
      int latestSnapshotId) throws QuotaExceededException {
    
    
    fsd.readLock();  <---- swicth from write lock to read lock
    try {
    
    
      return unprotectedSetTimes(fsd, inode, mtime, atime, force,
                                 latestSnapshotId);
    } finally {
    
    
      fsd.readUnlock();
    }
  }

문제는 여기에서 비롯됩니다. unprotectedSetTimes의 후속 논리에서 INode 클래스의 setModificationTime 및 setAccessTime은 실제로 스냅 샷 차이에 대한 변경 사항을 포함합니다.

  private static boolean unprotectedSetTimes(
      FSDirectory fsd, INode inode, long mtime, long atime, boolean force,
      int latest) throws QuotaExceededException {
    
    
    // remove writelock assert due to HADP-35711
    // assert fsd.hasWriteLock();
    boolean status = false;
    if (mtime != -1) {
    
    
      inode = inode.setModificationTime(mtime, latest);
      status = true;
    }

    // if the last access time update was within the last precision interval,
    // then no need to store access time
    if (atime != -1 && (status || force
        || atime > inode.getAccessTime() + fsd.getFSNamesystem().getAccessTimePrecision())) {
    
    
      inode.setAccessTime(atime, latest);
      status = true;
    }
    return status;
  }

    /** Set the last modification time of inode. */
  public final INode setModificationTime(long modificationTime,
      int latestSnapshotId) {
    
    
    recordModification(latestSnapshotId);
    setModificationTime(modificationTime);
    return this;
  }

수정 시간 또는 액세스 시간을 할 때마다 마지막 스냅 샷 차이 시간을 마지막 시간으로 기록한 다음 현재 시간을 최신 시간으로 수정합니다. 읽기 및 쓰기 작업으로 변환되기 때문에 여러 스레드가 diff 업데이트 작업을 동시에 수행하는 경우가 있습니다. 즉, 이전 AbstractINodeDiff # addDiff가 동시에 실행될 수 있습니다. 스냅 샷 diffList는 기본적으로 구조의 ArrayList이며 ArrayList는 스레드로부터 안전하지 않습니다. 따라서 null 상황이 있습니다.

ArrayList를 테스트 할 때 ArrayList에 null이 삽입 된 상황도 재현했습니다. 테스트 코드는 다음과 같습니다.

  @Test
  public void test() throws InterruptedException {
    
    
    ArrayList<String> array = new ArrayList<>();
    
    int numThreads = 100;
    Thread[] threads = new Thread[numThreads];
    for (int i = 0; i < numThreads; i++) {
    
    
      threads[i] = new Thread() {
    
    

        @Override
        public void run() {
    
    
          array.add(System.currentTimeMillis() + "");
        }

      };
    }
    for (int i = 0; i < numThreads; i++) {
    
    
      threads[i].start();
    }

    for (int i = 0; i < numThreads; i++) {
    
    
      threads[i].join();
    }
    System.out.println("Array size: " + array.size());
    System.out.println(array);
    for (int i = 0; i < numThreads; i++) {
    
    
      if(array.get(i) == null) {
    
    
        System.out.println("Detect null element: " + i);
      }
    }
  }

setTimes는 스냅 샷 차이 업데이트 변경 사항을 무시합니다.


문제의 근본 원인을 찾은 후 즉시 코드 변경을 진행했으며 이전 변경 사항의 논리를 롤백하고 싶지 않았습니다. 그래서 우리는이 setTimes를 순수한 시간 값 업데이트 작업으로 만들기 위해 setTimes 메서드를 사용하여 스냅 샷 차이 업데이트 변경 사항을 무시했습니다. setModificationTime / setAccessTime은 동시에 다른 메소드에 의해 참조되므로 전용 setTimes 호출 메소드를 추가했으며 메소드는 다음과 같습니다.

  public final INode setAccessTimeWithoutSnapshot(long accessTime, int latestSnapshotId) {
    
    
    setAccessTime(accessTime);
    return this;
  }

요약하자면


지금까지이 글에서 설명한대로 스냅 샷을 삭제할 수 없다는 문제가 드디어 해결되었으며, 전체 문제 해결을위한 타임 라인은 실제로 비교적 긴 시간입니다. 이 질문의 교훈은 각 커밋의 코드 로직을 더 잘 검토하고 통합 된 코드의 안전성을 보장 할 수있는 충분한 테스트 케이스가 있는지 확인하는 것입니다. 그렇지 않으면 문제 해결에 많은 우회가 필요합니다. 이 기사에 설명 된 문제에서는 쓰기 잠금에서 읽기 잠금으로의 논리적 변경에서 setTimes의 잠재적 위험 지점을 신중하게 평가하지 않았습니다.

참고


[1] .https : //issues.apache.org/jira/browse/HDFS-9406
[2] .https : //issues.apache.org/jira/browse/HDFS-13101

추천

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