Blog
kafkaoperationsincident-responsestorageretention

[Kafka 운영 ⑦] 디스크가 가득 찼다 — 브로커 디스크 풀 긴급 대응과 재발 방지

Kafka 브로커 디스크가 가득 찼을 때의 증상 진단부터 안전한 긴급 대응, 그리고 재발을 막는 용량 계획·리텐션·모니터링 전략까지 운영 런북 형태로 정리합니다.

Data Dynamics2026年6月6日22 min read
This post is not yet translated. The original Korean version is shown below.

새벽 3시, 페이저가 울립니다. "프로듀서가 메시지를 보낼 수 없다"는 알림과 함께, Kafka 브로커 하나의 로그 디렉터리가 통째로 오프라인 상태로 표시됩니다. 디스크 사용률은 100%. 이 순간 가장 위험한 선택은 "급하니까 일단 세그먼트 파일 몇 개 지우자"입니다. 그 한 번의 rm이 로그를 손상시키고 오프셋을 망가뜨려, 디스크 풀보다 훨씬 긴 장애로 이어집니다. 이 글은 그 새벽을 위한 런북입니다.

이 글에서 배우는 것

  • 디스크 풀이 났을 때 브로커가 보내는 증상과 그 의미
  • 리텐션, 트래픽 급증, 컴팩션 지연 등 디스크를 채우는 근본 원인
  • 데이터 손실 없이 디스크를 회복하는 순서가 정해진 긴급 대응 절차
  • 절대 하면 안 되는 것: 세그먼트 파일 수동 삭제
  • 같은 새벽을 다시 맞지 않기 위한 용량 계획·쿼터·모니터링

1. 증상 — 디스크 풀은 이렇게 신고된다

디스크 풀은 조용히 오지 않습니다. Kafka는 여러 계층에서 동시에 비명을 지릅니다. 먼저 무엇을 보고 있는지 정확히 식별하는 것이 첫 단추입니다.

브로커가 보내는 신호

증상어디서 보이나의미
브로커 종료 또는 로그 디렉터리 오프라인브로커 로그, kafka.log.LogManagerlog.dir에 더 이상 쓸 수 없어 해당 디렉터리를 오프라인 처리
KafkaStorageException브로커 로그, 프로듀서 응답디스크 I/O 실패. 세그먼트 flush·롤 불가
프로듀스 요청 실패프로듀서 클라이언트NotEnoughReplicasException, 타임아웃, 백프레셔
파티션 오프라인kafka-topics.sh --describe, 컨트롤러 로그리더가 사라진 파티션. Leader: none
Under-Replicated Partitions 증가JMX UnderReplicatedPartitions팔로워가 따라오지 못함. 복제본 부족

핵심은 로그 디렉터리 단위로 오프라인이 된다는 점입니다. JBOD(여러 디스크) 구성에서는 한 디스크만 차도 그 디렉터리의 파티션만 오프라인이 되고, 다른 디스크의 파티션은 정상 동작합니다. 단일 디스크 구성이라면 사실상 브로커 전체가 마비됩니다.

증상 빠른 진단

# 1. 브로커 로그에서 스토리지 예외 확인
grep -i "KafkaStorageException\|offline\|No space left" \
  /var/log/kafka/server.log | tail -50
 
# 2. OS 레벨 디스크 사용률
df -h /data/kafka
 
# 3. 오프라인/언더리플리케이티드 파티션 집계
kafka-topics.sh --bootstrap-server localhost:9092 \
  --describe --under-replicated-partitions
 
kafka-topics.sh --bootstrap-server localhost:9092 \
  --describe --unavailable-partitions

No space left on device가 보이고 df가 100%에 근접하면 진단은 끝났습니다. 이제 찼는지와 어떻게 회복할지로 넘어갑니다.


2. 근본 원인 — 디스크는 왜 가득 찼는가

긴급 대응에 들어가기 전에, 원인 가설을 세우는 것이 회복 전략을 결정합니다. 같은 "디스크 풀"이라도 원인에 따라 손대는 노브가 다릅니다.

흔한 원인 목록

원인메커니즘단서
리텐션이 너무 김retention.ms / retention.bytes가 디스크 대비 과대토픽 크기가 계획치 초과, 오래된 세그먼트가 남아 있음
트래픽 급증갑작스러운 인입 증가로 리텐션 윈도 내 데이터가 급팽창바이트 인입(BytesIn) 그래프 급등
정체된 컨슈머컨슈머가 안 읽어도 리텐션이 시간 기반이면 데이터는 남음컨슈머 랙 급증 + 디스크 동반 상승
컴팩션 지연log.cleaner가 못 따라가 dirty 세그먼트 누적compacted 토픽 크기 비정상, 클리너 스레드 부족
재조인 후 복제 따라잡기브로커가 복귀하며 데이터를 일시에 받아 사용량이 두 배로한 브로커만 디스크 급증, 리밸런스 직후
과대 세그먼트segment.bytes가 커서 세그먼트가 안 닫히면 삭제 대상도 안 됨active 세그먼트가 거대, 삭제가 안 일어남

자주 놓치는 함정: 리텐션은 "닫힌 세그먼트"만 지운다

Kafka의 삭제는 세그먼트 파일 단위입니다. 현재 쓰기 중인 active 세그먼트는 절대 삭제 대상이 아닙니다. segment.bytes(기본 1GB)나 segment.ms로 세그먼트가 롤(roll)되어 닫힌 뒤에야, 그 닫힌 세그먼트가 retention.ms/retention.bytes 기준으로 삭제 후보가 됩니다.

여기에 더해, 실제 삭제는 log.retention.check.interval.ms(기본 5분)마다 도는 백그라운드 스레드가 수행합니다. 즉 리텐션을 줄여도 즉시 디스크가 비지 않습니다. 이 지연을 모르면 "설정을 바꿨는데 왜 안 줄지?"로 시간을 허비합니다.

컴팩션 지연의 경우

compacted 토픽(cleanup.policy=compact)은 시간이 아니라 키별 최신 값만 남기는 정책입니다. 클리너가 못 따라가면 min.cleanable.dirty.ratio(기본 0.5)에 도달하지 못하거나, log.cleaner.threads가 부족해 dirty 영역이 계속 쌓입니다. 이 경우 리텐션을 줄이는 것으로는 안 줄어들고, 클리너 튜닝이 필요합니다.

# 컴팩션이 막혀 있는지 — 클리너 상태 확인
grep -i "LogCleaner\|cleaner" /var/log/kafka/log-cleaner.log | tail -30
 
# JMX: max-clean-time, max-buffer-utilization, dead-thread-count
# kafka.log:type=LogCleanerManager,name=max-dirty-percent

3. 긴급 대응 — 순서가 생명이다

경고: 세그먼트 파일을 rm으로 절대 직접 삭제하지 마세요. 디스크가 급하다고 .log / .index / .timeindex 파일을 수동으로 지우면, 브로커의 인메모리 인덱스와 오프셋 메타데이터가 어긋나 로그가 손상됩니다. 최악의 경우 해당 파티션이 복구 불가가 되고, 컨슈머 오프셋이 깨져 데이터 손실이나 중복으로 번집니다. 삭제는 반드시 Kafka가 리텐션을 통해 수행하게 하세요. 디스크 회복이 급할수록 이 원칙이 더 중요합니다.

다음 순서를 위에서부터 적용합니다. 각 단계는 "데이터 손실 위험이 가장 낮은 것"부터 배치되어 있습니다.

1단계 — 디스크를 가장 많이 먹는 대상을 찾는다

# 로그 디렉터리별 파티션 크기 (Kafka가 인식하는 값)
kafka-log-dirs.sh --bootstrap-server localhost:9092 \
  --describe --broker-list 3 \
  | python3 -c "import sys,json; \
d=json.loads([l for l in sys.stdin if l.startswith('{')][0]); \
print('\n'.join(sorted(((p['partition'], p['size']) \
for b in d['brokers'] for ld in b['logDirs'] for p in ld['partitions']), \
key=lambda x:-x[1])[:20]))"
 
# OS 레벨 — 실제 디스크에서 큰 디렉터리 상위 20개
du -h --max-depth=1 /data/kafka | sort -rh | head -20

kafka-log-dirs.sh는 브로커가 인식하는 파티션별 바이트를, du는 디스크의 실제 점유를 보여줍니다. 둘을 대조하면 "어떤 토픽-파티션"이 범인인지 분 단위로 좁혀집니다.

2단계 — 범인 토픽의 리텐션을 일시적으로 낮춘다

가장 빠르고 안전한 회복 수단입니다. 원인이 "리텐션 과대"거나 "트래픽 급증"이라면 여기서 대부분 끝납니다.

# 예: events 토픽 보관을 7일 → 6시간으로 일시 축소
kafka-configs.sh --bootstrap-server localhost:9092 \
  --alter --entity-type topics --entity-name events \
  --add-config retention.ms=21600000
 
# 또는 크기 기준으로 캡 (파티션당 50GB)
kafka-configs.sh --bootstrap-server localhost:9092 \
  --alter --entity-type topics --entity-name events \
  --add-config retention.bytes=53687091200

설정 후 즉시 줄지 않습니다. 닫힌 세그먼트가 삭제 후보가 되고, log.retention.check.interval.ms(기본 5분) 주기의 스레드가 돌아야 실제로 파일이 사라집니다. active 세그먼트가 거대해 롤이 안 되고 있다면, segment.ms를 짧게 줘서 강제로 롤을 유도할 수 있습니다.

# 세그먼트가 안 닫혀 삭제가 안 되는 경우 — 롤 유도
kafka-configs.sh --bootstrap-server localhost:9092 \
  --alter --entity-type topics --entity-name events \
  --add-config segment.ms=600000

이 값들은 비상용 임시값입니다. 회복 후 5단계에서 원복하거나 계획된 값으로 재설정하세요.

3단계 — 디스크를 추가/확장한다

리텐션을 줄여도 회복이 부족하거나, 데이터를 더 보관해야 한다면 스토리지 자체를 늘립니다. 클라우드 볼륨(EBS 등)은 온라인 확장이 가능하고, JBOD라면 새 로그 디렉터리를 log.dirs에 추가할 수 있습니다.

상황조치
클라우드 블록 스토리지볼륨 확장 후 파일시스템 resize2fs/xfs_growfs
JBOD에 빈 디스크 존재log.dirs에 디렉터리 추가 후 브로커 재기동
임박한 OOM-of-disk먼저 2단계로 시간을 벌고, 그 사이 확장 진행

디스크를 늘렸다고 기존 파티션이 자동으로 새 디스크로 이사하지는 않습니다. 새 파티션 할당이나 재배치 시점부터 균형이 잡힙니다.

4단계 — 핫 브로커에서 파티션을 옮긴다

특정 브로커만 가득 찼다면(예: 재조인 후 복제 따라잡기), 파티션을 여유 있는 브로커로 재배치해 부하를 분산합니다. 반드시 스로틀을 걸어, 회복용 복제 트래픽이 정상 트래픽을 잠식하지 않게 합니다.

# 1) 이동 계획(JSON) 작성: topics-to-move.json
# {"topics":[{"topic":"events"}],"version":1}
 
# 2) 재배치 계획 생성
kafka-reassign-partitions.sh --bootstrap-server localhost:9092 \
  --topics-to-move-json-file topics-to-move.json \
  --broker-list "1,2,4,5" --generate > reassignment.json
# 출력의 "Proposed partition reassignment"를 reassignment.json으로 저장
 
# 3) 스로틀(50MB/s)을 걸고 실행
kafka-reassign-partitions.sh --bootstrap-server localhost:9092 \
  --reassignment-json-file reassignment.json \
  --throttle 52428800 --execute
 
# 4) 진행률 확인 (완료되면 스로틀 해제됨)
kafka-reassign-partitions.sh --bootstrap-server localhost:9092 \
  --reassignment-json-file reassignment.json --verify

--verify가 모든 파티션을 "completed successfully"로 보고하면 스로틀이 자동 제거됩니다. 도중에 디스크가 더 차오를 수 있으니, 4단계는 2단계로 디스크에 여유를 만든 뒤 진행하는 편이 안전합니다.

5단계 — 안정화 후 원복과 검증

# 임시 리텐션/세그먼트 원복 (예: 7일로)
kafka-configs.sh --bootstrap-server localhost:9092 \
  --alter --entity-type topics --entity-name events \
  --add-config retention.ms=604800000
 
# 임시 segment.ms 제거
kafka-configs.sh --bootstrap-server localhost:9092 \
  --alter --entity-type topics --entity-name events \
  --delete-config segment.ms
 
# 오프라인/언더리플리케이티드가 0인지 최종 확인
kafka-topics.sh --bootstrap-server localhost:9092 --describe \
  --under-replicated-partitions

오프라인 로그 디렉터리가 있던 브로커는, 디스크 공간이 확보되면 자동으로 디렉터리를 다시 온라인 처리하려 시도합니다. 자동 복구가 안 되면 브로커를 재기동합니다(다른 브로커의 ISR이 충분한지 먼저 확인).


4. 긴급 대응 의사결정 트리

Loading diagram…

이 트리는 "삭제는 Kafka에게 맡기고, 사람은 노브만 돌린다"는 원칙으로 설계되어 있습니다. 어떤 가지에서도 수동 rm으로 빠지지 않습니다.


5. 재발 방지 — 같은 새벽을 다시 맞지 않으려면

긴급 대응은 불을 끄는 것이고, 재발 방지는 불이 안 나게 하는 것입니다. 다음을 운영 표준으로 박아두세요.

디스크 사용률 알림은 100%가 아니라 75/85%에서

100%에서 알림이 오면 이미 장애입니다. 여유를 두고 단계별로 경보를 받아야 사람이 개입할 시간이 생깁니다.

임계치알림 단계권장 조치
75%Warning용량/리텐션 검토 시작
85%High리텐션 조정 또는 확장 계획 실행
90%Critical즉시 긴급 대응(3장) 착수

용량 계획: 보관할 바이트를 먼저 계산한다

토픽이 차지할 디스크는 대략 다음으로 추정합니다.

파티션당 디스크 ≈ 시간당 바이트 인입 × 보관 시간 × 복제 계수
                  ÷ 파티션 수 × (1 + 여유율)

이 계산으로 retention.bytes물리 디스크보다 충분히 작게 정해두면, 트래픽이 급증해도 시간 기반 리텐션이 무너지기 전에 크기 기반 캡이 디스크를 보호합니다. 시간·크기 리텐션을 함께 거는 것이 안전합니다.

토픽별 리텐션 정기 리뷰

점검 항목질문
과대 리텐션"이 토픽을 정말 N일이나 보관해야 하나?"
정체 컨슈머 의존"느린 컨슈머 때문에 리텐션을 늘려둔 건 아닌가?"
compacted 토픽"클리너가 인입을 따라가고 있나?"
segment 설정"segment.bytes가 디스크 대비 과대하지 않은가?"

프로듀서/컨슈머 쿼터

특정 프로듀서의 폭주가 디스크를 채우는 것을 막으려면, 클라이언트/유저 단위로 바이트 레이트 쿼터를 겁니다.

# 프로듀서 바이트 레이트 제한 (10MB/s) — client-id 단위
kafka-configs.sh --bootstrap-server localhost:9092 \
  --alter --add-config 'producer_byte_rate=10485760' \
  --entity-type clients --entity-name ingest-app

핵심 메트릭 상시 모니터링

메트릭무엇을 보나경보 기준
log.dirs 여유 공간 (OS)실제 디스크 잔량< 15%
UnderReplicatedPartitions (JMX)복제 건강성> 0 지속
OfflineLogDirectoryCount오프라인 로그 디렉터리 수> 0
BytesInPerSec인입 추세 (급증 감지)기준 대비 급등
max-dirty-percent (LogCleaner)컴팩션 적체지속 상승
컨슈머 랙정체 컨슈머 탐지급증

6. 운영 체크리스트

긴급 대응 시 (3장)

  • 증상 확정: df, 브로커 로그의 KafkaStorageException/No space left
  • kafka-log-dirs.sh + du로 최대 점유 토픽 식별
  • 원인 분류: 리텐션 / 트래픽 / 컴팩션 / 복제 따라잡기 / 세그먼트
  • 대상 토픽 리텐션 일시 축소 (5분 체크 주기 인지)
  • 필요 시 디스크 확장 또는 스로틀 건 파티션 재배치
  • 절대 금지: 세그먼트 파일 수동 rm
  • 안정화 후 임시 설정 원복 + --verify

재발 방지 (상시)

  • 75/85/90% 단계별 디스크 알림
  • 토픽별 시간+크기 리텐션, 크기 캡은 물리 디스크보다 작게
  • 프로듀서/컨슈머 바이트 쿼터
  • UnderReplicatedPartitions, OfflineLogDirectoryCount 상시 모니터링
  • 분기별 토픽 리텐션 리뷰

이 사건의 사후 정리와 포스트모템 작성 방법은 이 시리즈의 9편(장애 대응 런북) 에서 더 자세히 다룹니다. 디스크 풀은 9편 런북의 한 시나리오로 들어가며, 동일한 "탐지 → 분류 → 대응 → 검증 → 회고" 골격을 공유합니다.


마치며

  • 디스크 풀의 첫 신고는 로그 디렉터리 오프라인과 KafkaStorageException 입니다. JBOD라면 디스크 단위로 부분 마비됩니다.
  • 원인은 하나가 아닙니다. 리텐션 과대, 트래픽 급증, 정체 컨슈머, 컴팩션 지연, 복제 따라잡기, 과대 세그먼트 — 원인에 따라 돌리는 노브가 다릅니다.
  • 긴급 대응은 순서가 생명입니다: 범인 식별 → 리텐션 일시 축소 → 디스크 확장 → 스로틀 건 재배치 → 원복.
  • 가장 중요한 한 줄: 세그먼트 파일을 직접 rm 하지 마세요. 삭제는 Kafka가 리텐션으로 하게 두는 것이 가장 빠르고 안전한 길입니다.
  • 재발 방지는 100% 이전에 작동해야 합니다. 단계별 알림, 크기 캡 리텐션, 쿼터, 그리고 UnderReplicatedPartitions·여유 공간 모니터링.
  • 새벽 3시의 페이저를 줄이는 가장 좋은 방법은, 디스크가 75%일 때 받는 낮 시간의 알림입니다.

참고 자료


— Data Dynamics 엔지니어링 팀