스프링 배치작업 기록

JUNE 22, 2024

스프링 배치를 사용하여 매일 한번씩 카카오 계정 서버에서 휴면/탈퇴 계정 목록을 가져와 내가 담당하고 있는 서비스를 탈퇴 시키는 작업을 진행했고, 이에 대한 기록을 남긴다.

1. 요구사항

  • 매일 휴면계정 목록을 가져와서 카카오 전자증명서 서비스를 탈퇴시킨다.
  • 매일 탈퇴계정 가져와서 카카오 전자증명서 서비스 탈퇴 및 DB에 저장된 증명서 발급 내역 테이블에서 개인정보 컬럼을 null로 업데이트한다.

2. 배치 스케줄러를 어떻게 수행할 것인가?

2.1. k8s cronJob + api 서버

방법

  • 스케줄러는 k8s cronJob 이 담당
  • 배치 작업을 수행하는 api 를 생성하며, cronJob이 해당 api를 호출한다.
  • 해당 api 의 배치 로직 실패시에 알람 구현 필요

배치 오류 발생시?

  • 상세 에러 내용은 kibana에서 확인 (es에 서빙이 되도록 로그 작업 필요)
  • 배치 작업을 수행하는 api 를 수동으로 호출 필요

2.2. jenkins + api 서버

방법

  • 스케줄러는 jenkins cron이 담당
  • 배치 작업을 수행하는 api 를 생성하며, jenkins가 해당 api를 호출한다.
  • 실패에 대한 알람은 jenkins 플러그인 사용

배치 오류 발생시?

  • 상세 에러 내용은 kibana에서 확인 (es에 서빙이 되도록 로그 작업 필요)

    • 배치가 오래 걸릴수 있으니 http 커넥션을 계속 맺고 있을수 없다고 판단, @Async 어노테이션을 이용하여 비동기로 수행. 때문에 젠킨스에 배치에 대한 호출로그는 남겠지만, 수행 로그는 남지 않음.
  • 수동으로 실행하는 젠킨스 파이프라인 미리 생성하여 해당 파이프라인 수행.

2.3. jenkins + springbatch

방법

  • 스케줄러는 jenkins cron이 담당
  • 배치 작업은 spring batch가 담당, jenkins cron은 springbatch 애플리케이션 파드를 실행시킴
  • 수행 시간에 파드가 생겼다가 작업이 끝나면 사라짐.
  • 실패에 대한 알람은 jenkins 플러그인 사용

배치 오류 발생시?

  • 상세 에러 내용은 젠킨스 수행 로그에서 확인
  • 수동으로 실행하는 젠킨스 파이프라인 미리 생성하여 해당 파이프라인 수행.

3번 사용 결정 이유

  • 배치를 위한 파드(서버)를 계속 띄울 필요 없이, 배치가 수행되는 시간에만 파드를 띄울수 있다는 장점이 있다.

    • 리소스 절약 및 서버에 대한 관리가 불필요.
    • 파트 내에 이미 kubernetes plugin 이 설치된 젠킨스가 있어서 세팅이 간단 했던것도 한몫함.

  • 한 곳에서 (jenkins) 수행 로그(결과 또는 에러 로그) 확인 및 재수행(retry)을 진행할 수 있다는 장점이 있다.

    • api로 수행을 할 경우, 수행 로그 확인과 재수행(retry) 위치가 달라진다.

3. spring batch 작업을 진행하면서 겪은 일

3.1. multi datasource 설정

  • spring batch 를 사용하기 위해서는 기본적으로 필요한 메타 테이블들이 존재한다.
  • 해당 테이블은 비즈니스 로직에서 사용하는 DB에 생성하기에는 너무 불필요한 테이블이였다.
  • 때문에 비즈니스 DB와 spring batch DB를 분리하기로 결정.

    • 비즈니스 DB는 기존대로 mysql 사용, spring batch DB는 h2 사용.
    • 파드가 생성되어 애플리케이션이 시작될때 h2 로드 및 아래 spring batch 메타 테이블은 h2에 생성.
    • 애플리케이션이 종료되면 h2에 생성된 테이블 삭제 및 h2도 같이 종료. (메타 테이블에 저장된 배치 이력 데이터 사라짐)

01

  • 비즈니스 로직에서 사용하는 DB(mysql) 설정(application.yaml)
spring:
  jpa:
    database: mysql
    database-platform: org.hibernate.dialect.MySQL8Dialect
    ...
  • spring batch에서 필요한 메타 데이터 테이블 생성을 위한 설정 (BatchConfig.kt)
@EnableConfigurationProperties(BatchProperties::class)
@Configuration
class BatchConfig {
		// @Bean 없이 생성해야 primany datasource bean 은 mysql 이 된다.
    private val batchDataSource =
        EmbeddedDatabaseBuilder().setType(EmbeddedDatabaseType.H2)
            .addScript("classpath:org/springframework/batch/core/schema-drop-h2.sql")
            .addScript("classpath:/org/springframework/batch/core/schema-h2.sql")
            .build()

		// 메타 테이블 스키마에 대한 초기화를 진행한다. 해당 설정이 없으면 테이블 생성은 mysql에 생성된다.
    @Bean
    fun batchDataSourceInitializer(
        resourceLoader: ResourceLoader?,
        properties: BatchProperties?,
    ): BatchDataSourceInitializer =
        BatchDataSourceInitializer(
            batchDataSource,
            resourceLoader,
            properties,
        )

		// 배치설정를 진행하며, 여기서 설정된 datasource에 배치 메타 데이터에 대해 crud 가 발생
    @Bean
    fun batchConfigurer(): BatchConfigurer = DefaultBatchConfigurer(batchDataSource)
}

3.2. JPA로 수행되는 CUD 쿼리가 수행이 안되는 현상.

  • 위처럼 생성하게 되면 batch 관련 DataSource 는 h2 기준으로 생성이 된다.
  • Spring Batch는 기본적으로 DataSourceTransactionManager 를 사용하므로 JPA로 수행되는 쿼리도 해당 트랜잭션 매니저로 수행이 된다.
  • DataSourceTransactionManager으로 수행이 되면 JpaTransaction이 생성되지 않아 기본 쿼리인 조회(select)만 수행이 가능하게 된다.
  • 이를 해결하기 위해 JpaTransaction bean을 생성하여 batch job에 주입해준다.
@Configuration
class JpaConfig {
		// datasource bean 은 mysql 이다.
    @Bean
    @Primary
    fun jpaTransactionManager(dataSource: DataSource): JpaTransactionManager {
        return JpaTransactionManager().apply { this.dataSource = dataSource }
    }
}

class JobConfig {
		@Bean
    fun dormantStep(
        jpaTransactionManager: JpaTransactionManager,
    ): Step {
        return stepBuilderFactory["dormantStep"]
            .tasklet(dormantUserTasklet)
            .transactionManager(jpaTransactionManager)
            .build()
    }

    @Bean
    fun dormantJob(
        dormantStep: Step,
        jobCompletionListener: JobExecutionListener
    ): Job {
        return jobBuilderFactory["dormantJob"]
            .listener(jobCompletionListener)
            .start(dormantStep)
            .build()
    }
}

3.3. index가 없을 경우 update, delete 쿼리가 수행되면?

  • update, delete 쿼리에 필요한 인덱스를 DB팀에 요청 후에 jenkins 세팅을 하게 되었다.
  • 리얼에서 파이프라인 세팅 및 테스트를 하다가 실수로 spring batch job을 수행하게 되었다.
  • 갑자기 약 1-2분간 ‘PessimisticLockingFailureException’ 오류가 발생하여 서비스 불능 상태가 되었고, batch job 수행이 완료된 후 락이 풀렸다.
  • where 절로 인해 테이블 전체가 Lock이 걸리진 않을거라고 예상했는데… InnoDB에서 UPDATE또는 DELETE는 where 절과 상관없이 스캔된 모든 인덱스 레코드에 잠금을 건다. 수행 당시에는 조건에 걸린 필드에 인덱스가 없었기 때문에 모든 row에 락이 걸리게 된 것이다.

  • 실시간 서비스에서 서치가 오래걸리는 update, delete 의 경우는 꼭꼭 인덱스를 세팅하고 수행하자.

4. Transaction 이 너무 긴 현상

class ATestlet() : Tasklet {
	override fun execute( {
		...
		loop (100) {
			run()
		}
	}
}

class RunnerClass() {
	@Transactional
	fun run() {
		...
	}
}
  • 한개의 tasklet 을 사용하여 loop를 돌며 트랜젝션 어노테이션이 붙은 함수를 수행하도록 하였고, 해당 함수에서는 update, delete 쿼리가 수행된다.
  • loop 가 한번 돌떄마다 commit 이 발생할 것이라고 예상했는데, 예상한바와 다르게 프로세스가 끝나야 실제 DB에 반영이 되었다.
  • 해당 원인파악은 아직 못한 상태…

작업 기록 블로그