JaeWon's Devlog
article thumbnail
반응형

작년 하반기 회사에서 발생한 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

반응형
profile

JaeWon's Devlog

@Wonol

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!