작년 하반기 회사에서 발생한 Spring Batch 오류로 인해 긴급 장애 대응이 있었습니다.
해당 글에서는 발생한 원인과 조치한 방법에 대해 기록하고자 합니다.
1. 장애 발생
Spring Batch 특정 Job이 수행 중 내부 로직으로 인해 reader() -> processor() -> writer() 간에 무한으로 반복 수행이 발생하였고, Spring Batch 내부 로직에 따른 BATCH_STEP_EXECUTION 테이블 안에 Select/Update 문이 무한정 발생하였습니다.
- 이로 인해 DML 쿼리가 무한정으로 쌓이면서 디스크 용량 과부하가 발생하였고, 긴급 디스크 증설이 진행되었습니다.
2. 소스코드
- 발생한 Job 은 Chunk 지향 기반 처리로 수행되고, reader() -> processor() -> writer() 순으로 진행됩니다.
- 각 메소드는 아래와 같습니다.
모든 코드는 블로그 예제 작성을 위한 임의 소스입니다.
장애 상황과 비슷하게 로직을 구성하였습니다.
- step()
@Bean(name = "JobParameterBatchStep")
@JobScope
public Step step() {
return stepBuilderFactory.get("JobParameterBatchStep")
.<Member, Member>chunk(5)
.reader(reader())
.processor(processor())
.writer(writer())
.build();
}
- reader()
@Bean(name = "JobParameterBatchReader")
@StepScope
public JpaPagingItemReader<Member> reader() {
return new JpaPagingItemReaderBuilder<Member>()
.name("JobParameterBatchReader")
.entityManagerFactory(entityManagerFactory)
.pageSize(5)
.queryString("SELECT m FROM Member m WHERE m.code = 'N'")
.parameterValues(parameters)
.build();
}
- processor()
@Bean(name = "JobParameterBatchProcessor")
@StepScope
public ItemProcessor<Member, Member> processor() {
log.info("ItemProcessor 에서는 데이터를 처리 하는 로직 작성");
return item -> {
if("N".equals(validDate(jobParameter.getDate()))) {
return null;
}
return item;
};
}
- writer()
@Bean(name = "JobParameterBatchWriter")
@StepScope
public ItemWriter<Member> writer() {
log.info("ItemWriter 에서는 DB 저장과 같은 Transactional 한 로직 작성");
return items -> {
for(Member member: items) {
member.setCode("Y");
member.update();
log.info("Member Info : " + member);
}
};
}
3. 원인
- Job 을 호출하면 해당 Job에서 Step 을 수행하고, Chunk 단위로 reader -> processor -> writer 가 동작합니다.
- 이때 위에 작성된 메소드를 간단하게 살펴보겠습니다.
- step()
- chunk 단위가 5 로서 2-3-4 반복 수행한다. - reader()
- Member 테이블에서 코드가 'N' 인 대상자들을 조회한다. - processor()
- 날짜를 validation 하여 'N' 이면 Null 을 전달하고, 아니면 Member 를 전달한다. - writer()
- 전달받은 Member 의 코드를 Y 로 변경한다.
- 작성된 내용으로만 보았을 때는 크게 잘못된 부분이 없어 보입니다.
- 정상적인 로직 수행으로 Writer 에서 Member 의 코드가 'Y' 로 바뀐다면 Chunk 단위인 5건씩 반복 수행이 이상 없이 동작할 것입니다.
- 그렇지만, 3번 reader() 에서 글에서 생략된 validDate 메소드에서 무조건 N 으로 전달된다면?
- 처음에 5건을 조회하였지만, return null 이 계속되면서 writer 가 정상 동작하지 않게 되고, 이로 인해 reader 에서는 같은 5건을 또 조회를 하게 되고 이 과정이 무한 반복이 되었습니다.
- 이때 Spring Batch 내부 로직에서 chunk 단위로 수행될 때 BATCH_STEP_EXECUTION 테이블이 계속 업데이트가 되는데 이 과정도 무한정 수행이 되고, DML 쿼리도 무한정 쌓이게 되어 디스크 용량을 채우게 되었습니다.
BATCH_STEP_EXECUTION 은 Spring Batch 에서 필요한 Meta Table 중 하나로, 작업이 수행될 때마다 실행된 Job 혹은 Step 에 대한 다양한 정보를 저장 관리합니다.
4. 조치방법
결론!!!
ChunkListener 를 통해 Chunk 단위로 수행될 때, ReadCount 를 조회하여 최대 조회 카운트를 넘으면 오류를 발생시키도록 하였습니다.
- ChunkListener 는 Chunk 지향 처리 단계에서 이벤트를 가로채고 특정 작업을 수행할 수 있는 인터페이스입니다.
- Chunk 지향 처리에서는 데이터를 일정한 크기(Chunk Size)로 나누어 각 Chunk를 트랜잭션 단위로 처리합니다.
- ChunkListener 에서는 이 Chunk 처리 과정에서 발생하는 이벤트를 감지하여 추가적인 로직을 수행할 수 있습니다.
- ChunkListener 는 3가지 메소드를 제공합니다.
- beforeChunk(ChunkContext context)
- Chunk 처리 전에 호출됩니다.
- 초기화 작업이나 로그 기록, 상태 체크 등의 작업을 수행합니다. - afterChunk(ChunkContext context)
- Chunk 처리가 성공적으로 완료된 후에 호출됩니다.
- 처리 결과에 대한 로그를 남기거나 후속 작업을 수행하는 데 사용합니다. - afterChunkError(ChunkContext context)
- Chunk 처리 도중 예외가 발생했을 때 호출됩니다.
- 예외 처리 로직을 구현하거나 에러 로그를 남기는 데 사용합니다.
- 위 내용을 기반으로 ChunkListner 를 새로 만들어보겠습니다.
@Slf4j
public class CustomChunkListener implements ChunkListener {
private static final int MAX_READ_COUNT = 10;
@Override
public void beforeChunk(ChunkContext context) {
}
@Override
public void afterChunk(ChunkContext context) {
StepExecution stepExecution = context.getStepContext().getStepExecution();
int readCount = stepExecution.getReadCount();
if (readCount > MAX_READ_COUNT) {
log.error("최대 ReadCount 초과");
throw new RuntimeException("최대 ReadCount 초과");
}
}
@Override
public void afterChunkError(ChunkContext context) {
}
}
- Chunk 단위로 수행될 때, StepExecution 에서 ReadCount(ItemReader 를 통해 조회된 갯수)를 확인하여 특정 카운트 이상인 경우 오류를 발생시킵니다.
- 이렇게 작성된 CustomChunkListner 를 Step 에 적용시킵니다.
@Bean(name = "JobParameterBatchStep")
@JobScope
public Step step() {
return stepBuilderFactory.get("JobParameterBatchStep")
.<Member, Member>chunk(5)
.reader(reader())
.processor(processor())
.writer(writer())
.listener(new CustomChunkListener()) // CustomChunkListner 등록
.build();
}
- 적용 이후 ReadCount 가 넘는 다면 오류가 발생합니다.
5. 마무리
- 해당 글은 예방(조치) 방법이고, 실제로는 validDate 메소드와 같이 메소드에서 잘못된 로직으로 인해 발생하였습니다.
- Spring Batch 를 적용하고 1~2년 운영하면서 발생하지 않았다가 처음 장애가 발생하였을 때는 원인 파악에 많은 어려움이 있었습니다.
- 이번 이슈를 해결하기 위해, 강의나 서치를 통해 Spring Batch 에 대해 조금 더 알아볼 수 있어 배운 것이 많았던 것 같습니다.
참고
- https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B0%B0%EC%B9%98/dashboard
'BackEnd > Spring Batch' 카테고리의 다른 글
[Spring Batch 정리하기] 2. DB Schema (0) | 2024.07.22 |
---|---|
[Spring Batch 정리하기] 1. Spring Batch 개요 (0) | 2024.07.17 |
[Spring] Spring Batch JobParameter 활용 하기(With. Custom 하기, 공통 관리하기) (1) | 2023.10.27 |
[Spring] Spring Batch 사용해보기(3) - step(tasklet, chunk) (0) | 2022.09.18 |
[Spring] Spring Batch 사용해보기(2) - step(skip, retry) (0) | 2022.09.14 |