Skip to content
목록으로

저장 계층의 한계 : HDFS, 메타스토어, Iceberg

대규모 시스템2025년 9월 21일
저장 계층의 한계 : HDFS, 메타스토어, Iceberg

저번 포스팅에서는, 시간이 지나며 쌓여 생기는 문제들을 공부하였습니다. 지금까지는 증상 중심으로 봤는데, 이번 편부터는 기술별로 실제 무엇이 터지는지를 정리하려 합니다. 같은 원리라도 시스템마다 드러나는 모습과 지표, 대응 방법이 다르기 때문입니다.

이번 편은 저장과 테이블 포맷 쪽입니다. 각 문제마다 어떤 상황에서 나타나고, 무엇이 위험하고, 어떻게 푸는지를 함께 적었습니다.

온프레 Hadoop 3.3   데이터노드 50대 (24코어 / 128GB / 4TB x 12)
                   네임노드 2대 (32코어 / 256GB, 힙 48GB)
                   HDFS 1.4PB, 파일 8,000만 개, 블록 9,200만 개
Iceberg on S3      trades 테이블 42TB, 데이터 파일 약 30만 개
                   Glue 카탈로그, 파티션 62만 개
Spark 3.5          익스큐터 200개 (4 vCPU / 24GB)
Flink 1.18         체크포인트 5분  →  하루 288회 커밋

1. HDFS 네임노드


하둡 클러스터에서 가장 먼저 한계에 부딪히는 자리입니다.

1.1. 힙이 파일 수의 상한을 정한다

네임노드는 파일과 디렉토리, 블록의 메타데이터를 전부 메모리에 들고 있습니다.

상황.

파일 8,000만 개 + 블록 9,200만 개
항목당 메타데이터   평균 약 250바이트
추정 메모리        (8,000만 + 9,200만) x 250B ≈ 43GB
네임노드 힙        48GB   →  사용률 약 90%

원인을 보면
  1MB 미만 파일          5,100만 개
  그 파일의 용량 비중     약 18TB  =  전체 1.4PB 의 1.3%
  그 파일의 메타데이터 비중  약 27GB  =  힙의 56%

용량으로는 1.3% 인데 메타데이터로는 56% 를 씁니다. 이 비대칭이 문제의 핵심입니다.

왜 위험한가. 두 가지입니다.

  1. GC 중단이 길어집니다. 43GB 가 찬 힙에서 full GC 가 걸리면 28~35초 멈춥니다. 그동안 클러스터의 모든 메타데이터 연산이 멈추므로, 파일을 열 수도 목록을 받을 수도 없습니다. Hive 쿼리들이 타임아웃으로 무더기로 실패합니다.
  2. 디스크가 남아도 더 못 씁니다. 1.4PB 중 여유가 400TB 있어도, 파일을 더 만들면 네임노드가 먼저 죽습니다. 저장 용량이 아니라 파일 개수가 클러스터의 상한이 됩니다.

어떻게 푸나.

  1. 작은 파일을 합칩니다. 근본 해법입니다. 5,100만 개를 합치면 파일이 약 3,200만 개로 줄고 힙 사용이 16GB 수준이 됩니다. 매시간 도는 수집 잡의 출력 파일 수를 줄이는 것부터 시작합니다.
  2. 블록 크기를 키웁니다. 같은 데이터라도 블록이 크면 블록 수가 줄어 메타데이터가 줄어듭니다.
  3. GC 를 튜닝합니다. 중단 시간이 짧은 수집기로 바꾸면 35초가 수 초로 줄어듭니다. 근본 해결은 아니지만 장애의 크기를 줄입니다.
  4. 네임스페이스를 나눕니다. 페더레이션으로 경로를 갈라 여러 네임노드가 나눠 맡게 합니다.

1.2. 모든 요청이 한 곳을 지나간다

데이터 자체는 데이터노드에서 직접 주고받지만, "어느 블록이 어디 있는지" 는 반드시 네임노드에 물어야 합니다.

상황. 야간에 Spark 잡 40개가 동시에 돕니다. 각 잡이 파일 수만 개를 엽니다.

파일 열기 요청   초당 수천 건
네임노드 RPC 처리 능력   초당 약 1~2만 건 (구성에 따라)
→ 큐가 쌓이면 파일 하나 여는 데 수백 밀리초

왜 위험한가. 이 지연이 모든 작업에 더해집니다. 파일 1만 개를 여는 잡이라면 그것만으로 수십 분이 늘어납니다. 그리고 잡이 늘어날수록 서로를 느리게 만들어, 동시 실행을 늘렸는데 전체 처리량이 줄어드는 구간이 생깁니다.

어떻게 푸나. 읽기를 분산합니다. Observer NameNode 를 두면 조회 요청을 대기 노드가 나눠 받습니다. 메타데이터 읽기가 대부분인 워크로드에서 효과가 큽니다. 그리고 작업당 파일 수를 줄이는 것이 여기서도 직접적인 대응이 됩니다.

1.3. 블록 리포트 폭풍

데이터노드는 자기가 가진 블록 목록을 주기적으로 네임노드에 보고합니다.

상황. 정기 점검으로 클러스터를 재시작했습니다.

데이터노드 50대가 동시에 전체 블록 목록 전송
보고 대상 블록   9,200만 개
안전 모드 해제까지   42분

왜 위험한가. 그 42분 동안 클러스터가 읽기 전용입니다. 새벽 배치 창이 두 시간뿐이라면 그중 42분을 잃습니다. 그리고 이 시간은 블록 수에 비례해 계속 늘어나므로, 데이터가 쌓일수록 재시작이 점점 더 위험한 작업이 됩니다.

어떻게 푸나. 블록 수를 줄이는 것이 근본이다(작은 파일 합치기, 블록 크기 키우기). 운영 쪽에서는 데이터노드를 한꺼번에 올리지 않고 나눠서 올려 보고를 시간축에 흩습니다. 앞 편에서 본 "동시에 일어나는 것이 위험하다" 의 전형적인 예입니다.


2. YARN 클러스터


2.1. 자원을 잡고 서로 기다린다

YARN 은 애플리케이션 단위가 아니라 컨테이너 단위로 자원을 줍니다.

상황. 큐 용량이 200 컨테이너인데, 애플리케이션 마스터가 차지할 수 있는 비율이 제한되어 있지 않습니다.

동시 제출된 잡    200개
각 잡의 AM 이 컨테이너 1개씩 점유   200개  →  큐 가득 참
각 AM 이 요청하는 작업 컨테이너      0개 배정
→ 200개 모두가 자원을 쥔 채 서로를 기다림

왜 위험한가. 아무도 진행하지 못합니다. 자원 사용률은 100% 인데 실제 작업 처리량은 0 입니다. 그리고 스스로 풀리지 않아 사람이 잡을 죽여야 빠져나옵니다. 야간에 발생하면 아침까지 아무 배치도 돌지 않은 채 발견됩니다.

어떻게 푸나. 큐에서 애플리케이션 마스터가 차지할 수 있는 비율을 제한합니다. 흔히 10% 안팎으로 둡니다. 그리고 동시 제출 수 자체를 스케줄러 쪽에서 제한합니다.

2.2. 조각화

상황.

노드 50대, 각 128GB
현재 각 노드에 남은 여유   평균 4GB
대기 중인 요청의 컨테이너 크기   8GB

클러스터 전체 여유   50 x 4 = 200GB
그런데 8GB 짜리를 넣을 수 있는 노드   0대

왜 위험한가. 200GB 가 남았는데 아무 작업도 받지 못합니다. 모니터링에서는 여유가 있는 것으로 보이므로 "왜 대기하지" 를 설명할 수 없습니다.

어떻게 푸나. 컨테이너 크기를 노드 크기의 약수에 가깝게 정하고, 요청 크기의 종류를 지나치게 다양하게 두지 않습니다. 크기가 두세 가지로 통일되어 있으면 조각이 훨씬 덜 생깁니다.

2.3. 전부 뜨지 않으면 의미 없는 작업

상황. 분산 학습 잡이 컨테이너 10개를 요구하는데 7개만 배정되었습니다.

왜 위험한가. 7개가 자원을 잡은 채 나머지 3개를 기다립니다. 그 7개는 아무 계산도 하지 않으면서 자원을 점유하므로, 다른 잡도 못 돌고 자기도 못 돕니다. 대기 시간이 길어지면 타임아웃으로 실패하고, 재시도하면 같은 상황이 반복됩니다.

어떻게 푸나. YARN 도 쿠버네티스도 기본으로는 이것을 다루지 않습니다. 전원이 확보될 때만 시작하는 스케줄러를 얹어야 합니다. 그 전까지는 최소 자원이 보장되는 전용 큐를 두어 우회합니다.


3. Hive 메타스토어


잘 드러나지 않는데 아주 자주 병목이 되는 자리입니다.

3.1. 뒤에는 평범한 관계형 DB 가 있다

메타스토어는 테이블과 파티션 정보를 관계형 데이터베이스에 저장합니다.

상황.

파티션 62만 개  (일자 500 x 종목 2,800 중 실제 존재분)
파티션 조건 없는 조회
  파티션 목록 수신   62만 개, 약 90초
  실제 스캔          하루치 1.2GB, 약 6초
파티션 조건 있는 조회
  목록 수신          0.3초 + 스캔 6초

왜 위험한가. 조건 하나 유무로 96초와 6초가 갈립니다. 그리고 이것은 그 쿼리 하나만의 문제가 아닙니다. 조건 없는 조회 몇 개가 동시에 들어오면 메타스토어 DB 의 커넥션과 CPU 가 소진되어 다른 모든 쿼리가 함께 느려집니다. 데이터가 아니라 메타데이터를 담은 DB 가 클러스터 전체의 병목이 되는 상황입니다.

어떻게 푸나.

  1. 파티션 수를 줄입니다. 일자로만 나누면 500개입니다. 종목은 정렬 키로 두어 파일 안의 통계로 건너뜁니다.
  2. 쿼리에 파티션 조건을 강제합니다. 대시보드처럼 사람이 실수할 수 있는 경로에서는 조건 없는 조회를 막아둡니다.
  3. 메타스토어 DB 를 운영 DB 만큼 챙깁니다. 인덱스, 커넥션 풀, 인스턴스 크기를 점검합니다.
  4. 파티션 목록을 DB 가 아니라 파일로 관리하는 포맷으로 옮깁니다. 다음 장의 테이블 포맷이 이 문제를 다루는 방식입니다.

3.2. 여러 엔진이 한 곳을 본다

상황. Hive, Spark, Trino 가 모두 같은 메타스토어를 바라봅니다.

왜 위험한가. 한 엔진에서 무거운 메타데이터 작업(파티션 일괄 등록 등)이 돌면 다른 엔진의 쿼리까지 대기합니다. 그리고 메타스토어가 죽으면 세 엔진이 동시에 못 씁니다. 단일 장애점이 조용히 만들어져 있는 셈입니다.

어떻게 푸나. 메타스토어를 이중화하고, 무거운 메타데이터 작업은 시간대를 나눠 돌립니다. 읽기 부하가 크다면 읽기 전용 복제본을 두는 것도 방법입니다.


4. 오브젝트 스토리지


HDFS 에서 S3 같은 오브젝트 스토리지로 옮겨가면, 문제의 모양도 함께 바뀝니다.

4.1. 이름 바꾸기가 없다

오브젝트 스토리지에는 디렉토리라는 개념이 없습니다. 경로는 그냥 긴 이름 하나이고 슬래시는 관례일 뿐입니다.

상황. 전통적인 커밋 방식은 임시 경로에 쓴 뒤 최종 경로로 이름을 바꾸는 것이었습니다. HDFS 에서 이름 바꾸기는 네임노드의 메타데이터 연산 하나라 원자적입니다.

S3 에서의 "이름 바꾸기"   복사 + 삭제
파일 1만 개짜리 출력       1만 번 복사, 1만 번 삭제
소요                      수 분 ~ 수십 분
중간에 실패하면            절반만 옮겨진 상태로 남음

왜 위험한가. 커밋이 원자적이지 않습니다. 중간에 잡이 죽으면 최종 경로에 절반의 데이터만 보이는 상태가 됩니다. 그 상태로 다음 작업이 읽으면 잘못된 결과를 내고, 오류도 나지 않습니다. 앞 편에서 본 "조용히 실패하는" 유형입니다.

어떻게 푸나. 커밋을 파일 이동이 아닌 방식으로 표현하는 포맷을 씁니다. 다음 장의 테이블 포맷이 메타데이터 포인터 하나를 바꾸는 방식으로 이것을 푼 것입니다.

4.2. 접두사당 요청 한도

상황.

Spark 익스큐터 200개, 익스큐터당 동시 읽기 4
초당 요청   약 5,500 GET
모든 데이터 파일이 같은 접두사 아래에 있음
접두사당 권장 한도   약 5,500 GET/초   →  한도에 걸림

왜 위험한가. 요청이 거절되면 재시도로 넘어가므로 실패하지는 않지만, 배치 소요가 40분에서 78분으로 두 배가 됩니다. 그리고 재시도가 늘면 요청 수가 더 늘어 스스로를 악화시킵니다. 로그에는 산발적인 오류만 찍혀 원인을 잡기 어렵습니다.

어떻게 푸나. 경로 앞쪽에 고르게 퍼지는 값을 넣어 접두사를 나눕니다. 그리고 파일 수를 줄이면 요청 수도 함께 줄어듭니다. 파일이 30만 개에서 17만 개가 되면 요청도 그만큼 줄어듭니다. 앞 편의 작은 파일 문제가 여기서 요청 한도로 나타나는 것입니다.

4.3. 비용이 요청 수에 비례하다

상황. 작은 파일 100만 개를 매일 읽는 배치가 있습니다.

하루 GET 요청   100만 건 x 배치 3회 = 300만 건
목록 조회        별도 과금
한 달             약 9,000만 건

왜 위험한가. 저장 용량은 그대로인데 요청 비용만 계속 늘어납니다. 그리고 이 비용은 데이터가 커져서가 아니라 파일이 잘게 쪼개져서 생긴 것이라, 청구서를 봐도 원인을 알기 어렵습니다.

어떻게 푸나. 컴팩션으로 파일 수를 줄이는 것이 성능뿐 아니라 비용에도 직접 작용합니다. 파일을 10분의 1로 합치면 요청도 10분의 1이 됩니다.


5. Apache Iceberg


테이블 포맷은 앞의 문제들을 상당 부분 해결하는데, 대신 새로운 종류의 문제를 가져옵니다.

5.1. 메타데이터가 계속 쌓인다

Iceberg 는 커밋할 때마다 새 메타데이터 파일과 매니페스트를 만듭니다.

상황. Flink 가 체크포인트 5분마다 커밋합니다.

커밋       하루 288회
6개월 누적  스냅샷 약 51,840개
           데이터 파일 약 125만 개 (컴팩션 없이)
           평균 파일 크기 34MB  (목표 256MB 의 7.5분의 1)

"어제 하루" 조회
  계획(매니페스트 읽기)   40초
  실제 스캔               8초
  전체                    48초

컴팩션 후 (파일 17만 개, 스냅샷 7일치)
  계획 3초 + 스캔 7초 = 10초

왜 위험한가. 데이터 양은 그대로인데 5배 가까이 느려집니다. 그리고 매일 조금씩 나빠지므로 "어느 날 나빠졌다" 고 말할 수 있는 날이 없습니다. 여섯 달 뒤에 돌아보면 이미 느려져 있고, 원인을 최근 변경에서 찾다가 헤맵니다.

저장 비용도 함께 늡니다. 오래된 스냅샷이 가리키는 데이터 파일은 지워지지 않기 때문입니다.

어떻게 푸나. 세 가지를 매일 도는 작업으로 걸어둡니다.

  1. 데이터 파일 재작성 : 작은 파일을 수백 MB 단위로 합칩니다.
  2. 매니페스트 재작성 : 계획 시간을 직접 줄입니다.
  3. 스냅샷 만료 : 오래된 것을 정리합니다. 다만 기간 설정은 5.5 를 참고해야 합니다.

5.2. 고아 파일 제거가 진행 중인 쓰기를 깨뜨린다

상황. 저장 비용을 줄이려고 고아 파일 제거의 기준 시각을 기본 3일에서 1시간으로 줄였습니다.

그날 밤 Spark 배치
  대상        30일치 재처리
  쓴 파일     1,840개, 약 470GB
  소요        1시간 52분
  커밋 시점   시작 후 1시간 52분

고아 파일 제거 실행   배치 시작 후 1시간 20분
  기준 1시간  →  그때까지 써둔 파일 1,310개가 대상
  아직 커밋 전이라 어느 스냅샷도 참조하지 않음  →  고아로 판정  →  삭제

왜 위험한가. 배치가 커밋 단계에서 파일을 찾지 못해 실패합니다. 그리고 이것은 성능 문제가 아니라 작업 파괴입니다. 재실행으로 복구했지만 새벽 마감이 2시간 10분 밀렸습니다. 더 나쁜 경우, 부분 커밋이 이미 된 상태라면 데이터가 어긋난 채 남습니다.

왜 작을 때는 안 터졌나. 하루치만 쓰던 시절에는 배치가 6분이면 끝났습니다. 기준 1시간이 배치보다 훨씬 길어 절대 겹치지 않았습니다. 데이터가 늘어 배치가 1시간을 넘기면서 위험 구간에 들어간 것입니다.

어떻게 막나.

  1. 기본 기준 시각을 줄이지 않습니다. 가장 오래 도는 쓰기 작업보다 넉넉해야 합니다. 이 환경이라면 최소 6시간, 기본값 3일을 그대로 두는 것이 안전합니다.
  2. 정리 작업과 쓰기 작업의 시간대를 겹치지 않게 배치합니다.
  3. 지우기 전에 대상 목록을 먼저 뽑아 확인하는 단계를 둡니다.

5.3. 커밋 충돌

Iceberg 는 낙관적 동시성으로 커밋합니다. 메타데이터 포인터를 원자적으로 바꾸는 방식이라, 그 사이 다른 쪽이 먼저 커밋했으면 실패하고 다시 시도합니다.

상황. 같은 테이블에 세 주체가 씁니다.

Flink 실시간 적재   5분마다 커밋   →  하루 288회
Spark 야간 배치     1회, 소요 2시간
컴팩션              매일 1회, 소요 40분

겹치는 구간   배치와 컴팩션이 새벽 2~4시에 함께 실행
              그 사이 Flink 커밋 24회
커밋 재시도 기본   4회  →  배치 커밋이 4회 모두 실패

왜 위험한가. 두 시간 돌린 배치가 마지막 커밋에서 실패합니다. 계산은 다 끝났는데 반영이 안 되는 것이라, 자원 낭비가 가장 큰 형태입니다. 재실행하면 또 두 시간이고, 같은 조건이면 또 실패합니다.

어떻게 푸나.

  1. 시간대를 나눕니다. 컴팩션을 배치가 끝난 뒤로 미룹니다. 가장 확실합니다.
  2. 파티션을 나눠 씁니다. 실시간은 오늘 파티션만, 배치는 과거 파티션만 건드리게 하면 충돌 범위 자체가 줄어듭니다.
  3. 쓰는 주체를 줄입니다. 하나의 잡이 모아서 쓰는 구조로 바꿉니다.
  4. 재시도 횟수를 늘리는 것은 완충일 뿐 해결이 아닙니다.

5.4. 삭제 파일이 쌓이면 읽기가 느려진다

상황. 정정 처리로 행을 갱신할 때 데이터 파일을 다시 쓰지 않고 삭제 정보를 별도 파일로 남깁니다.

하루 정정        약 12만 건
삭제 파일 누적    한 달 약 3,600개
읽을 때           데이터 파일 + 삭제 파일을 함께 읽어 걸러냄
같은 조회 소요     도입 시 7초  →  한 달 뒤 23초

왜 위험한가. 쓰기를 싸게 만든 대가가 읽기 비용으로 계속 누적됩니다. 그리고 이 비용은 정정이 일어나는 한 계속 늘어나므로, 손대지 않으면 끝없이 나빠집니다.

어떻게 푸나. 주기적으로 데이터 파일을 다시 써서 삭제를 실제로 반영합니다. 5.1 의 데이터 파일 재작성이 이 일도 함께 합니다. 정정이 잦은 테이블은 재작성 주기를 더 짧게 잡습니다.

5.5. 되돌릴 지점이 사라진다

상황. 스냅샷이 5만 개까지 쌓여 계획이 느려지자, 만료 기간을 1일로 줄였습니다.

커밋 주기 5분   →  하루 288 스냅샷
보관             최근 288개 = 24시간분

집계 로직 버그로 사흘치가 틀어짐
발견             나흘째
되돌리려는 시점    4일 전
남아 있는 가장 오래된 스냅샷   1일 전   →  시점 조회 불가

왜 위험한가. 성능 문제를 푸는 조치가 복구 가능성을 없앴습니다. 위 사례에서는 Kafka 보관이 7일이라 원본으로 재처리해 복구했지만, 하루만 늦게 발견했어도 불가능했습니다. 그리고 이 위험은 평소에는 전혀 드러나지 않다가 사고가 났을 때 한 번에 나타납니다.

어떻게 정하나. 보관 기간은 저장 비용이 아니라 "문제를 며칠 만에 발견할 수 있는가" 를 기준으로 정합니다. 대사가 매일 돈다면 3일로도 되지만, 월 단위로 확인하는 지표가 있다면 그만큼 길어야 합니다.

스냅샷 수가 문제라면 만료 기간을 줄일 것이 아니라 커밋 주기를 늘리거나 매니페스트를 정리해야 합니다. 5분 커밋을 15분으로 바꾸면 스냅샷이 3분의 1이 됩니다.

5.6. 카탈로그가 단일 지점이다

상황. Iceberg 의 커밋은 카탈로그에서 메타데이터 포인터를 바꾸는 것으로 이루어집니다. 카탈로그를 Hive 메타스토어로 두었습니다.

왜 위험한가. 3장의 문제가 그대로 따라옵니다. 카탈로그가 느리면 커밋이 느리고, 카탈로그가 죽으면 쓰기가 멈춥니다. 테이블 포맷을 바꿔 파티션 관리를 파일로 옮겼는데도, 커밋 경로에는 여전히 그 DB 가 있습니다.

어떻게 푸나. 카탈로그를 이중화하고 지연을 지표로 봅니다. 커밋 빈도가 높다면 커밋 주기를 늘려 카탈로그 요청 자체를 줄이는 것이 직접적인 대응입니다. 조건부 쓰기를 지원하는 저장소를 쓰면 카탈로그 의존을 줄일 수 있습니다.


6. 조금 더 들어가보기 : 커밋을 어떻게 원자적으로 만드는가


이번 편에서 여러 번 나온 질문입니다. 여러 파일을 쓰는 작업을 "전부 보이거나 전혀 안 보이거나" 로 만드는 것은 생각보다 까다롭습니다.

6.1. 파일 시스템 시절의 방법

상황. Spark 익스큐터 200개가 파티션 하나에 1,800개 파일을 쓰고 있습니다. 마지막에 임시 경로에서 최종 경로로 옮기는데, HDFS 라면 메타데이터 연산 한 번이지만 S3 라면 1,800번의 복사와 삭제입니다.

전통적인 방식은 임시 경로에 쓴 뒤 최종 경로로 이름을 바꾸는 것이었습니다. HDFS 에서 이름 바꾸기는 네임노드의 메타데이터 연산 하나라 원자적입니다. 그런데 4.1 에서 본 대로 오브젝트 스토리지에는 이름 바꾸기가 없습니다. 복사와 삭제로 흉내 낼 뿐이고, 중간에 죽으면 절반만 옮겨진 상태가 남습니다.

왜 위험한가. 그 사이에 잡이 죽으면 절반만 반영된 파티션이 남습니다. 읽는 쪽은 그것이 완성본인지 중간 상태인지 구분할 방법이 없어, 틀린 값을 정상값으로 받아 갑니다. 데이터가 깨지는 것보다 깨진 줄 모르는 것이 더 나쁩니다.

6.2. 포인터 하나만 바꾸는 방법

상황. 같은 42TB 테이블에 Spark 배치와 Flink 스트리밍이 동시에 씁니다. 배치가 1,800개 파일을 쓰는 20분 동안, 읽는 쪽에는 아무것도 바뀌지 않은 것처럼 보여야 합니다.

테이블 포맷이 쓰는 방식입니다. 데이터 파일은 미리 다 써두고, 마지막에 "현재 메타데이터는 이것" 이라는 포인터 하나만 바꿉니다. 포인터를 바꾸기 전까지 새로 쓴 파일들은 아무도 참조하지 않으므로 보이지 않습니다. 포인터가 바뀌는 순간 한꺼번에 보입니다. 원자성이 필요한 범위를 한 값으로 줄인 것이 핵심입니다.

그 포인터를 바꾸는 것은 카탈로그가 맡습니다. 관계형 DB 의 트랜잭션을 쓰거나, 저장소가 조건부 쓰기를 지원하면 그것을 씁니다. 앞서 정리한 조건부 쓰기와 낙관적 동시성 제어가 여기서 다시 나옵니다.

6.3. 그래서 고아 파일이 생긴다

상황. 하루에 커밋이 2,400번 일어나고, 그중 실패로 끝나는 것이 20건 안팎입니다. 한 번 실패할 때마다 파일이 수십에서 수백 개 남으니 한 달이면 고아 파일이 수만 개 쌓입니다.

이 방식의 부작용이 5.2 에서 본 고아 파일입니다. 데이터 파일을 먼저 쓰고 포인터를 나중에 바꾸므로, 그 사이에 작업이 실패하면 참조되지 않는 파일이 남습니다. 데이터가 잘못되지는 않습니다. 아무도 그 파일을 보지 않기 때문입니다. 다만 저장 공간을 차지하므로 언젠가 치워야 하고, 그 치우는 작업이 진행 중인 쓰기와 겹치면 위험해지는 것입니다.

원자성을 얻은 대가로 청소할 것이 남는 구조라고 정리할 수 있습니다. 그리고 그 청소가 5.2 의 사고로 이어집니다.


결론


저장 쪽을 기술별로 정리하면서 가장 크게 느낀 것은, 문제가 옮겨 다닐 뿐 사라지지 않는다는 점이었습니다. HDFS 의 네임노드 한계를 피해 오브젝트 스토리지로 옮기면 목록 조회와 요청 수 문제가 생기고, 파티션 관리를 메타스토어에서 테이블 포맷으로 옮기면 메타데이터 파일이 쌓이는 문제가 생깁니다. 더 나은 선택이 있을 뿐 문제가 없는 선택은 없었습니다.

두 번째는 정리 작업 자체가 위험하다는 점이었습니다. 고아 파일 제거는 공간을 되찾는 좋은 일인데, 진행 중인 쓰기를 모르면 그 작업을 깨뜨립니다. 스냅샷 만료도 마찬가지로, 공간을 아끼려다 되돌릴 수단을 없앱니다. 청소가 사고의 원인이 되는 구조라는 것을 알고 나니, 기본값을 함부로 줄이면 안 되는 이유가 이해되었습니다.

세 번째는 원자성의 범위를 줄이는 발상이었습니다. 여러 파일을 한꺼번에 원자적으로 만드는 것은 어려우니, 파일은 미리 다 써두고 포인터 하나만 바꾸는 것으로 문제를 옮겼습니다. 어려운 것을 푸는 대신 어려운 부분을 최소한으로 줄이는 방식이 인상 깊었고, 이것이 앞서 여러 시스템에서 본 조건부 쓰기와 같은 구조라는 점도 확인했습니다.


공부 참고 자료


📑 1. HDFS - Observer NameNode 와 읽기 분산

📑 2. HDFS - Federation

📑 3. YARN - Capacity Scheduler

📑 4. Apache Iceberg - Maintenance

📑 5. Apache Iceberg - Spec (매니페스트와 스냅샷)

📑 6. Apache Iceberg - Configuration (커밋 재시도)

📑 7. Amazon S3 - 성능 설계 패턴

📑 8. Hive Metastore 관리

Comments