하드 딜리트 배치 성능 저하 트러블 슈팅 - KimGyuBek/Threadly GitHub Wiki
- 배치 시스템: Spring Batch 기반 사용자 하드 딜리트 작업
- 대상 데이터: 소프트 딜리트된(DELETED) 사용자 레코드 10,000,000건
- 기술 스택: Spring Batch + PostgreSQL + HikariCP
- 테스트 환경: 로컬 맥북 프로 (14 core, 24GB RAM)
- 급격한 성능 저하: 동일 조건임에도 배치 실행 시간 급증
- 데드락 발생: deadlock detected 에러로 잡 실패
- 멀티스레드 실패: JpaCursorItemReader 사용 시 조회 쿼리 무한 루프 현상
- 불균등 파티셔닝: 문자열 ID 범위 분할로 인한 롱테일 현상 발생
- 리소스 비효율: 과도한 메모리 사용량과 높은 GC 빈도
설정:
- 싱글 스레드 실행
- 청크 크기: 1,000
- 커넥션 풀: 기본 설정
결과:
실행시간: 00h 05m 40s 770ms (340.770초)
처리량: 29,345 rec/s (계산: 10,000,000 ÷ 340.770)
상태: COMPLETED
커밋 수: 10,001
- CPU 사용률: 100% (단일 코어 포화)
- 메모리 사용량: 8.5GB (과도한 힙 사용)
- GC 횟수: 52회/분 (평균 125ms)
- 커넥션 풀 사용률: 95%
- 단일 스레드 처리: 멀티코어 활용 불가
- JPA 오버헤드: 영속성 컨텍스트 관리 비용
- 메모리 집약적: 대용량 데이터 처리 시 힙 부족
설정:
- JpaCursorItemReader + taskExecutor
- 멀티스레드 청크 처리
결과:
상태: FAILED
문제: 조회 쿼리 무한 루프 발생
원인: Cursor 기반 리더의 비스레드세이프 특성
성능 모니터링:
- CPU 사용률, 메모리 사용량 급증
- 쿼리 실행 횟수 비정상 증가
- JpaCursorItemReader: 스레드 안전하지 않음
- 무한 루프: 커서 상태 공유로 인한 동시성 문제
- 리소스 낭비: 불필요한 쿼리 반복 실행
설정:
- Master-Slave 파티셔닝
- gridSize: 4
- 문자열 ID 범위 분할
결과:
실행시간: 00h 07m 26s 153ms (446.153초) (무한 루프로 인한 JOB 중지)
처리량: 22,413 rec/s (-23.6% 성능 저하)
문제: 일부 데이터 삭제 누락, 불균등 분할로 롱테일 발생
문제점:
- 분포 왜곡: 문자열은 숫자처럼 균일하지 않아 구간 N등분 시 샤드 간 작업량 치우침
- 경계 리스크: BETWEEN 분할 시 경계값 중복/누락 위험
- 처리량 불균형: 특정 파티션 과다 대기로 전체 처리 시간 증가
실측 데이터:
- 파티션별 처리량 편차: 최대 300% 차이
- 메모리 사용량: 특정 파티션에서 과도한 힙 사용
- GC 빈도: 불균등 파티션에서 GC 횟수 3배 증가
- 데이터 누락: 1.2% 데이터 누락 발생
설정:
- 해시 함수 기반 균등 분할
- gridSize: 20
- taskExecutor: 128
- JDBC 기반으로 변경
결과:
실행시간: 00h 01m 01s 701ms (61.701초)
처리량: 162,089 rec/s (+452.0% 성능 향상)
상태: COMPLETED
파티션 균등도: 99.74%
해시 함수 적용:
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% 이내 균등화
- 파티션 조건: 명확·상호배타적으로 중복/누락 없음
- 경계 계산: 불필요하여 선행 스캔 비용 제거
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 생성/파싱 오버헤드 최소화
- 네트워크 왕복 횟수 대폭 감소
- 실행 시간: 340.770초 → 61.701초 (-81.9% 단축)
- 처리량: 29,345 rec/s → 162,089 rec/s (+452.0% 향상)
- 파티션 균등도: 불균등 분할 → 99.74% 균등도 달성
- 데이터 정합성: 1.2% 누락 → 0% 누락 (완벽한 데이터 처리)
| 청크 크기 | 실행 시간 | 처리량 | 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 효율)
| 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배, 커넥션 풀 효율성 최대화)
추가 인덱스:
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% 단축
최종 결과:
실행시간: 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
}
}| 최적화 단계 | 실행시간 | 처리량(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 |

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

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

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

스레드 동시성 — 활성 스레드가 설정값
gridSize=20과 정확히 일치(피크=20)하고, 스텝 종료 시 0으로 정상 복귀. 오버구동/풀 경합 징후 없음.
- 베이스라인: 100% (단일 코어 포화)
- 해시 파티셔닝: 75% (-25% 감소, 멀티코어 활용)
- 최종 최적화: 62% (-38% 감소, 효율적 리소스 사용)
- 베이스라인: 8.5GB (과도한 메모리 사용)
- 해시 파티셔닝: 2.1GB (-75.3% 감소)
- 최종 최적화: 1.4GB (-83.5% 감소)
- 베이스라인: 52회/분, 평균 125ms
- 해시 파티셔닝: 15회/분 (-71.2% 감소), 평균 45ms
- 최종 최적화: 8회/분 (-84.6% 감소), 평균 28ms
- 해시 분할 권장: 범위 분할은 롱테일 위험
- 균등 분배 중요: 99% 이상의 균등도 유지 필요
- 수학적 검증: 이항분포 모델로 이론적 뒷받침
- 리소스 정렬: gridSize ≤ poolSize - margin 준수
- 병목 지점 파악: 커넥션 풀이 성능 제약 요소
- 모니터링 기반: CPU, 메모리, I/O 종합 분석
- 메트릭 기반 접근: 실행 시간, 처리량, 리소스 사용률 종합 분석
- 단계적 변경: 한 번에 하나씩 변경하여 영향도 측정
- 지속적 모니터링: 파티션별 성능, 풀 대기 시간 추적
- 성능 향상(피크): 29,345 → 289,855 items/s (+887.6%, ≈9.88×)
- 전체 시간 단축: 340.77s → 34.50s (−89.9%)
- 안정성 확보: FAILED → COMPLETED (대량 삭제 배치 정상 종료)
- 리소스 효율: 메모리 사용 −83.5%, GC 성능 +84.6% (대시보드: 리소스 섹션 참조)
- 동시성 제어: 활성 스레드가 gridSize=20과 정확히 일치(피크 20) 후 0으로 정상 복귀
- 파티셔닝 전략: 키(문자열 ID) 해시 기반 분할이 대량 삭제에 효과적
- 모니터링 주도 최적화: 실시간 메트릭 기반으로 병목을 정량 확인 후 단계별 튜닝
- 단계별 개선의 누적 효과: READ/PROCESS/WRITE 병렬 파이프라인 균형이 전체 성능 결정
- 리소스 균형 최적화: CPU/메모리/읽기/쓰기를 종합적으로 맞춰야 지속 성능 확보
- 대용량 배치: 해시 기반 파티셔닝을 기본 전략으로 채택
- 지속 모니터링 체계: 실행 시간·처리량·지연·동시성 대시보드 상시 운영
- 청크 크기·gridSize 등 병렬 파라미터 단계적 튜닝(관찰→조정→검증)
- 삭제 경로는 JDBC 직접 처리로 JPA 오버헤드 최소화