BackEnd/Spring Batch

[Spring] Spring Batch 장애 회고(Feat. ChunkListner 사용하기)

Wonol 2024. 7. 12. 09:00
반응형

작년 하반기 회사에서 발생한 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 가 동작합니다.

- 이때 위에 작성된 메소드를 간단하게 살펴보겠습니다.

  1. step()
    - chunk 단위가 5 로서 2-3-4 반복 수행한다.
  2. reader()
    - Member 테이블에서 코드가 'N' 인 대상자들을 조회한다.
  3. processor()
    - 날짜를 validation 하여 'N' 이면 Null 을 전달하고, 아니면 Member 를 전달한다.
  4. 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

반응형