하드 딜리트 배치 성능 저하 트러블 슈팅 - KimGyuBek/Threadly GitHub Wiki

문제 상황

기존 환경

  • 배치 시스템: Spring Batch 기반 사용자 하드 딜리트 작업
  • 대상 데이터: 소프트 딜리트된(DELETED) 사용자 레코드 10,000,000건
  • 기술 스택: Spring Batch + PostgreSQL + HikariCP
  • 테스트 환경: 로컬 맥북 프로 (14 core, 24GB RAM)

발생한 문제

  1. 급격한 성능 저하: 동일 조건임에도 배치 실행 시간 급증
  2. 데드락 발생: deadlock detected 에러로 잡 실패
  3. 멀티스레드 실패: JpaCursorItemReader 사용 시 조회 쿼리 무한 루프 현상
  4. 불균등 파티셔닝: 문자열 ID 범위 분할로 인한 롱테일 현상 발생
  5. 리소스 비효율: 과도한 메모리 사용량과 높은 GC 빈도

해결 과정

1단계: 베이스라인 성능 측정 및 문제 파악

1. 초기 성능 측정 (ExecutionId: 28)

설정:

  • 싱글 스레드 실행
  • 청크 크기: 1,000
  • 커넥션 풀: 기본 설정

결과:

실행시간: 00h 05m 40s 770ms (340.770초)
처리량: 29,345 rec/s (계산: 10,000,000 ÷ 340.770)
상태: COMPLETED
커밋 수: 10,001

2. 문제점 분석

성능 모니터링 결과:

  • CPU 사용률: 100% (단일 코어 포화)
  • 메모리 사용량: 8.5GB (과도한 힙 사용)
  • GC 횟수: 52회/분 (평균 125ms)
  • 커넥션 풀 사용률: 95%

주요 병목 지점:

  1. 단일 스레드 처리: 멀티코어 활용 불가
  2. JPA 오버헤드: 영속성 컨텍스트 관리 비용
  3. 메모리 집약적: 대용량 데이터 처리 시 힙 부족

2단계: 멀티스레드 적용 시도 및 실패 분석

1. 스텝 내부 멀티스레드 시도

설정:

  • JpaCursorItemReader + taskExecutor
  • 멀티스레드 청크 처리

결과:

상태: FAILED
문제: 조회 쿼리 무한 루프 발생
원인: Cursor 기반 리더의 비스레드세이프 특성

성능 모니터링:

  • CPU 사용률, 메모리 사용량 급증
  • 쿼리 실행 횟수 비정상 증가

2. 문제 원인 분석

  • JpaCursorItemReader: 스레드 안전하지 않음
  • 무한 루프: 커서 상태 공유로 인한 동시성 문제
  • 리소스 낭비: 불필요한 쿼리 반복 실행

3단계: 파티셔닝 적용 및 문자열 ID 문제 발견

1. 초기 파티셔닝 적용 (ExecutionId: 38)

설정:

  • Master-Slave 파티셔닝
  • gridSize: 4
  • 문자열 ID 범위 분할

결과:

실행시간: 00h 07m 26s 153ms (446.153초) (무한 루프로 인한 JOB 중지)
처리량: 22,413 rec/s (-23.6% 성능 저하)
문제: 일부 데이터 삭제 누락, 불균등 분할로 롱테일 발생

2. 문자열 ID 범위 분할의 한계

문제점:

  1. 분포 왜곡: 문자열은 숫자처럼 균일하지 않아 구간 N등분 시 샤드 간 작업량 치우침
  2. 경계 리스크: BETWEEN 분할 시 경계값 중복/누락 위험
  3. 처리량 불균형: 특정 파티션 과다 대기로 전체 처리 시간 증가

실측 데이터:

  • 파티션별 처리량 편차: 최대 300% 차이
  • 메모리 사용량: 특정 파티션에서 과도한 힙 사용
  • GC 빈도: 불균등 파티션에서 GC 횟수 3배 증가
  • 데이터 누락: 1.2% 데이터 누락 발생

4단계: 해시 기반 샤드 파티셔닝 적용

1. 해시 기반 파티셔닝 전환 (ExecutionId: 45)

설정:

  • 해시 함수 기반 균등 분할
  • gridSize: 20
  • taskExecutor: 128
  • JDBC 기반으로 변경

결과:

실행시간: 00h 01m 01s 701ms (61.701초)
처리량: 162,089 rec/s (+452.0% 성능 향상)
상태: COMPLETED
파티션 균등도: 99.74%

2. 핵심 개선 사항

1. 샤드 해시 기반 균등 분할

해시 함수 적용:

WHERE mod(abs(hashtext(user_id)), 20) = :shard
  AND status = 'DELETED' AND modified_at < :threshold

샤드별 분포 검증:

SELECT mod(abs(hashtext(user_id)), 20) AS shard, count(*)
FROM users
WHERE status = 'DELETED'
GROUP BY shard;
shard 건수 편차
0 500,090 +0.018%
1 498,596 -0.281%
... ... ...
19 500,809 +0.162%

효과:

  • 샤드 분포: ±0.3% 이내 균등화
  • 파티션 조건: 명확·상호배타적으로 중복/누락 없음
  • 경계 계산: 불필요하여 선행 스캔 비용 제거

2. JPA → JDBC 전환

Reader (조회):

SELECT user_id
FROM users
WHERE status = 'DELETED'
  AND modified_at < :threshold
  AND mod(abs(hashtext(user_id)), :N) = :shard;

Writer (삭제):

DELETE
FROM users
WHERE user_id = ANY (?)
  AND status = ?
  AND modified_at < ?;

성능 효과:

  • JPA 벌크 오버헤드 제거
  • 배열 바인딩으로 SQL 생성/파싱 오버헤드 최소화
  • 네트워크 왕복 횟수 대폭 감소

3. 성능 개선 효과 검증

Before vs After 성능 비교

  • 실행 시간: 340.770초 → 61.701초 (-81.9% 단축)
  • 처리량: 29,345 rec/s → 162,089 rec/s (+452.0% 향상)
  • 파티션 균등도: 불균등 분할 → 99.74% 균등도 달성
  • 데이터 정합성: 1.2% 누락 → 0% 누락 (완벽한 데이터 처리)

5단계: 설정 최적화 및 인덱스 튜닝

1. 청크 크기별 성능 비교

청크 크기 실행 시간 처리량 CPU 사용률 메모리 사용량 GC 횟수 비고
500 01m 30s 111,111 rec/s 45% 800MB 45회 커밋 오버헤드 과다
800 02m 55s 57,143 rec/s 38% 1.2GB 32회 비효율적
1000 00m 34s 289,855 rec/s 62% 1.4GB 12회 최적
1200 02m 05s 79,365 rec/s 78% 2.8GB 58회 락 대기 증가
1500 00m 39s 252,145 rec/s 71% 2.1GB 28회 양호하지만 차선
2000 03m 03s 54,348 rec/s 85% 4.2GB 89회 락 경합 심화

최적 청크 크기: 1,000 (커밋당 약 0.5초 처리, 최적 메모리/CPU 효율)

2. gridSize별 성능 비교

gridSize 실행 시간 처리량 커넥션 풀 사용률 컨텍스트 스위칭 상태
20 00m 34s 289,855 rec/s 52% 낮음 최적
22 00m 57s 175,439 rec/s 65% 보통 양호
25 01m 07s 149,254 rec/s 78% 높음 수용 가능
50 00m 57s 175,439 rec/s 88% 매우 높음 오버헤드 발생
100 01m 01s 163,934 rec/s 95% 극도로 높음 커넥션 경합

최적 gridSize: 20 (CPU 코어의 1.4배, 커넥션 풀 효율성 최대화)

3. 인덱스 최적화

추가 인덱스:

CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_users_covering_batch_query
    ON users (status, modified_at) INCLUDE (user_id);

효과:

  • Index-Only Scan으로 테이블(Heap) 접근 최소화
  • 디스크 I/O: -45% 감소
  • 쿼리 실행 시간: -35% 단축
  • 전체 처리 시간: -8.3% 단축

6단계: 최고 성능 달성 및 모니터링

1. 최종 성능 결과 (ExecutionId: 73)

최종 결과:

실행시간: 00h 00m 34s 504ms (34.504초)
처리량: 289,855 rec/s
총 성능 향상: +887.6% (≈9.88배, 베이스라인 대비)
실행시간 단축: -89.9% (340.770초 → 34.504초)

실시간 성능 메트릭:

{
  "totalExecutionTimeMs": 34504,
  "totalItemsProcessed": 10000000,
  "overallThroughputItemsPerSec": 289855,
  "avgItemProcessingMs": 0.003,
  "systemMetrics": {
    "cpuUsagePercent": 62.4,
    "heapUsagePercent": 2.3,
    "gcCount": 12,
    "gcTimeMs": 28
  },
  "databaseMetrics": {
    "connectionUsagePercent": 52.0,
    "activeConnections": 13,
    "threadsAwaitingConnection": 0
  }
}

2. 단계별 성능 개선 과정

최적화 단계 실행시간 처리량(rec/s) 개선율(시간 기준) 누적 개선율 핵심 기법
베이스라인 340.770s 29,345 - - 싱글스레드
해시 파티셔닝 61.701s 162,089 -81.9% -81.9% 균등 분할
설정 최적화 37.629s 265,252 -39.0% -88.9% 리소스 튜닝
청크 튜닝 34.504s 289,855 -8.3% -89.9% chunk=1000

3. 실시간 성능 모니터링 결과

1. 실행 시간 변화 추이

실행 시간 변화

분석: 최적화 과정에서 실행 시간이 삼각형 패턴으로 상승 후 하락하며 한 번의 피크(스텝 길어짐) 후 안정. 최종적으로 평균 약 0.55분(약 33초) 근처로 수렴하여 340초 → 33초 개선 효과 확인.

2. 처리량(Throughput) 분석

처리량 분석

처리량(합계) - Read/Process/Write가 같은 타이밍에 램프업하여 plateau(피크 ≈170–190K items/s)를 형성하고 종료 시 0으로 정상 복귀함. 전체 평균은 각 단계 ≈31.7K items/s으로 유하여 특정 단계 병목 징후는 보이지 않음.

3. 지연시간(Latency) 상세 분석

지연시간 분석

전체 지연 기여도는 Write ≈63%, Read ≈35%, Process ≈2%. 병목은 쓰기 I/O이며, 현 상태를 기준선으로 운영.

4. 배치 동시 실행 스레드

리소스 사용률

스레드 동시성 — 활성 스레드가 설정값 gridSize=20과 정확히 일치(피크=20)하고, 스텝 종료 시 0으로 정상 복귀. 오버구동/풀 경합 징후 없음.


고려사항

1. 리소스 사용량 최적화

CPU 사용률 개선:

  • 베이스라인: 100% (단일 코어 포화)
  • 해시 파티셔닝: 75% (-25% 감소, 멀티코어 활용)
  • 최종 최적화: 62% (-38% 감소, 효율적 리소스 사용)

메모리 사용량 개선:

  • 베이스라인: 8.5GB (과도한 메모리 사용)
  • 해시 파티셔닝: 2.1GB (-75.3% 감소)
  • 최종 최적화: 1.4GB (-83.5% 감소)

GC 성능 개선:

  • 베이스라인: 52회/분, 평균 125ms
  • 해시 파티셔닝: 15회/분 (-71.2% 감소), 평균 45ms
  • 최종 최적화: 8회/분 (-84.6% 감소), 평균 28ms

2. 파티셔닝 전략 선택 기준

문자열 Key 처리:

  • 해시 분할 권장: 범위 분할은 롱테일 위험
  • 균등 분배 중요: 99% 이상의 균등도 유지 필요
  • 수학적 검증: 이항분포 모델로 이론적 뒷받침

동시성 설계 원칙:

  • 리소스 정렬: gridSize ≤ poolSize - margin 준수
  • 병목 지점 파악: 커넥션 풀이 성능 제약 요소
  • 모니터링 기반: CPU, 메모리, I/O 종합 분석

3. 성능 튜닝 방법론

  • 메트릭 기반 접근: 실행 시간, 처리량, 리소스 사용률 종합 분석
  • 단계적 변경: 한 번에 하나씩 변경하여 영향도 측정
  • 지속적 모니터링: 파티션별 성능, 풀 대기 시간 추적

결론

핵심 성과

  1. 성능 향상(피크): 29,345 → 289,855 items/s (+887.6%, ≈9.88×)
  2. 전체 시간 단축: 340.77s → 34.50s (−89.9%)
  3. 안정성 확보: FAILED → COMPLETED (대량 삭제 배치 정상 종료)
  4. 리소스 효율: 메모리 사용 −83.5%, GC 성능 +84.6% (대시보드: 리소스 섹션 참조)
  5. 동시성 제어: 활성 스레드가 gridSize=20과 정확히 일치(피크 20) 후 0으로 정상 복귀

핵심 학습사항

  1. 파티셔닝 전략: 키(문자열 ID) 해시 기반 분할이 대량 삭제에 효과적
  2. 모니터링 주도 최적화: 실시간 메트릭 기반으로 병목을 정량 확인 후 단계별 튜닝
  3. 단계별 개선의 누적 효과: READ/PROCESS/WRITE 병렬 파이프라인 균형이 전체 성능 결정
  4. 리소스 균형 최적화: CPU/메모리/읽기/쓰기를 종합적으로 맞춰야 지속 성능 확보

실무 적용 권장사항

  • 대용량 배치: 해시 기반 파티셔닝을 기본 전략으로 채택
  • 지속 모니터링 체계: 실행 시간·처리량·지연·동시성 대시보드 상시 운영
  • 청크 크기·gridSize병렬 파라미터 단계적 튜닝(관찰→조정→검증)
  • 삭제 경로는 JDBC 직접 처리로 JPA 오버헤드 최소화
⚠️ **GitHub.com Fallback** ⚠️