Spring Boot CRaC 체크포인트 복원

CRaC란?

CRaC(Coordinated Restore at Checkpoint)은 실행 중인 JVM의 상태를 파일로 스냅샷하고, 이후 해당 스냅샷에서 즉시 복원하여 애플리케이션을 시작하는 기술입니다. Spring Boot 3.2부터 공식 지원되며, 기존 수십 초 걸리던 시작 시간을 수십 밀리초로 단축할 수 있습니다.

GraalVM Native Image와 달리 표준 JVM(OpenJDK) 위에서 동작하므로, 리플렉션이나 동적 프록시 같은 JVM 기능을 그대로 사용할 수 있습니다.

구분 일반 JVM GraalVM Native CRaC
시작 시간 5~30초 50~200ms 30~100ms
빌드 시간 빠름 매우 느림 (수 분) 빠름 + 체크포인트
런타임 성능 최적 (JIT) 제한적 (AOT) 최적 (JIT 유지)
리플렉션 자유 설정 필요 자유
메모리 높음 낮음 높음 (스냅샷 크기)

의존성과 JDK 설정

CRaC는 Azul Zulu, Liberica, 또는 CRaC 패치된 OpenJDK가 필요합니다. Spring Boot 3.2+에서는 별도 라이브러리 없이 프레임워크 레벨에서 지원합니다.

# build.gradle.kts
plugins {
    id("org.springframework.boot") version "3.3.0"
}

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web")
    // CRaC API (Spring Boot에 포함, 명시적 추가 불필요)
    // implementation("org.crac:crac:1.4.0")
}
# Dockerfile — Azul Zulu CRaC JDK 사용
FROM azul/zulu-openjdk:21-crac AS builder
WORKDIR /app
COPY build/libs/app.jar app.jar

체크포인트 생성 흐름

CRaC의 핵심은 워밍업 → 체크포인트 → 복원 3단계입니다.

# 1단계: 애플리케이션 시작 + 워밍업
java -XX:CRaCCheckpointTo=/checkpoint -jar app.jar &

# 워밍업 요청 (JIT 컴파일 유도)
curl http://localhost:8080/health
curl http://localhost:8080/api/warmup

# 2단계: 체크포인트 생성 (JVM 상태 스냅샷)
jcmd $(pgrep java) JDK.checkpoint

# 3단계: 스냅샷에서 복원 (수십 ms)
java -XX:CRaCRestoreFrom=/checkpoint

체크포인트 시점에 열려 있는 파일 디스크립터, 소켓, 스레드 등은 모두 정리되어야 합니다. Spring Boot 3.2+는 이 과정을 자동으로 처리합니다.

Spring Boot의 자동 리소스 관리

Spring Boot는 체크포인트 시점에 다음 리소스를 자동으로 닫고, 복원 시 재생성합니다.

  • 웹 서버: Tomcat/Netty 서버 소켓 닫기 → 복원 시 재바인딩
  • 데이터소스: HikariCP 커넥션 풀 닫기 → 복원 시 재생성
  • JMS/Kafka: 메시지 리스너 컨테이너 중지 → 복원 시 재시작
  • 로깅: 파일 핸들 닫기 → 복원 시 재오픈
// application.yml — CRaC 활성화
spring:
  lifecycle:
    timeout-per-shutdown-phase: 30s
  # CRaC 체크포인트 관련 설정
  checkpoint:
    restore: on-refresh  # ApplicationContext 새로고침 시 자동 복원

커스텀 리소스 정리: Resource 인터페이스

직접 관리하는 리소스(파일, 외부 연결 등)가 있다면 org.crac.Resource 인터페이스를 구현하여 체크포인트/복원 콜백을 등록합니다.

@Component
public class ExternalConnectionManager implements Resource, InitializingBean {

    private Connection externalConn;

    @Override
    public void afterPropertiesSet() {
        // CRaC 글로벌 컨텍스트에 리소스 등록
        Core.getGlobalContext().register(this);
        this.externalConn = createConnection();
    }

    @Override
    public void beforeCheckpoint(Context<? extends Resource> context) throws Exception {
        // 체크포인트 전: 외부 연결 정리
        if (externalConn != null) {
            externalConn.close();
            externalConn = null;
        }
        log.info("체크포인트 전 외부 연결 정리 완료");
    }

    @Override
    public void afterRestore(Context<? extends Resource> context) throws Exception {
        // 복원 후: 외부 연결 재생성
        this.externalConn = createConnection();
        log.info("복원 후 외부 연결 재생성 완료");
    }

    private Connection createConnection() {
        // 외부 시스템 연결 로직
        return ExternalClient.connect("ext-service:5555");
    }
}

Docker 멀티 스테이지 빌드

프로덕션에서는 Docker 이미지 빌드 시 체크포인트를 미리 생성하여, 배포 시 복원만 하도록 구성합니다.

# Stage 1: 빌드
FROM azul/zulu-openjdk:21-crac AS builder
WORKDIR /app
COPY . .
RUN ./gradlew bootJar -x test

# Stage 2: 체크포인트 생성
FROM azul/zulu-openjdk:21-crac AS checkpoint
WORKDIR /app
COPY --from=builder /app/build/libs/app.jar app.jar

# CRIU 권한 필요
RUN java -XX:CRaCCheckpointTo=/checkpoint -jar app.jar & 
    sleep 10 && 
    curl -s http://localhost:8080/actuator/health && 
    jcmd $(pgrep java) JDK.checkpoint && 
    sleep 2

# Stage 3: 복원 전용 이미지
FROM azul/zulu-openjdk:21-crac
WORKDIR /app
COPY --from=checkpoint /checkpoint /checkpoint
COPY --from=checkpoint /app/app.jar app.jar

ENTRYPOINT ["java", "-XX:CRaCRestoreFrom=/checkpoint"]

이 이미지로 컨테이너를 시작하면 수십 밀리초 만에 요청을 처리할 수 있습니다.

K8s 환경 적용

CRaC는 K8s HPA 오토스케일링과 결합하면 극적인 효과를 발휘합니다. 스케일 아웃 시 새 Pod가 밀리초 단위로 트래픽을 받을 수 있습니다.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: order-service
spec:
  replicas: 2
  template:
    spec:
      containers:
      - name: app
        image: order-service:crac
        securityContext:
          # CRIU 복원에 필요한 권한
          capabilities:
            add: ["CHECKPOINT_RESTORE", "SYS_PTRACE"]
        readinessProbe:
          httpGet:
            path: /actuator/health/readiness
            port: 8080
          initialDelaySeconds: 1  # CRaC 덕분에 1초면 충분
          periodSeconds: 2
        resources:
          requests:
            memory: "512Mi"
          limits:
            memory: "1Gi"

시크릿 처리 주의사항

체크포인트 파일에는 JVM 메모리 전체가 포함되므로, 환경 변수나 시크릿이 스냅샷에 남을 수 있습니다. 이를 방지하는 전략이 필요합니다.

@Component
public class SecretCleaner implements Resource, InitializingBean {

    @Value("${db.password}")
    private String dbPassword;

    private char[] sensitiveData;

    @Override
    public void afterPropertiesSet() {
        Core.getGlobalContext().register(this);
        this.sensitiveData = dbPassword.toCharArray();
    }

    @Override
    public void beforeCheckpoint(Context<? extends Resource> context) {
        // 체크포인트 전 민감 데이터 제거
        Arrays.fill(sensitiveData, '');
        sensitiveData = null;
        log.info("민감 데이터 정리 완료");
    }

    @Override
    public void afterRestore(Context<? extends Resource> context) {
        // 복원 후 시크릿 재로드 (Vault, K8s Secret 등에서)
        this.sensitiveData = loadFromVault("db/password").toCharArray();
        log.info("시크릿 재로드 완료");
    }
}
  • 체크포인트 이미지는 신뢰할 수 있는 레지스트리에만 저장합니다
  • 시크릿은 복원 시점에 Vault나 K8s Secret에서 동적으로 로드합니다
  • 환경별 설정은 복원 후 @RefreshScope로 갱신합니다

Actuator 체크포인트 엔드포인트

Spring Boot Actuator를 통해 HTTP 요청으로 체크포인트를 트리거할 수도 있습니다.

// application.yml
management:
  endpoint:
    checkpoint:
      enabled: true
  endpoints:
    web:
      exposure:
        include: health,checkpoint

# 체크포인트 트리거
curl -X POST http://localhost:8080/actuator/checkpoint

Spring Boot Lifecycle 통합

Lifecycle 인터페이스를 구현한 빈은 체크포인트 시 자동으로 stop()이 호출되고 복원 시 start()가 호출됩니다.

@Component
public class ScheduledTaskManager implements Lifecycle {

    private volatile boolean running = false;
    private ScheduledExecutorService scheduler;

    @Override
    public void start() {
        scheduler = Executors.newScheduledThreadPool(2);
        scheduler.scheduleAtFixedRate(this::syncData, 0, 30, TimeUnit.SECONDS);
        running = true;
        log.info("스케줄러 시작");
    }

    @Override
    public void stop() {
        if (scheduler != null) {
            scheduler.shutdown();
        }
        running = false;
        log.info("스케줄러 중지 (체크포인트 준비)");
    }

    @Override
    public boolean isRunning() {
        return running;
    }

    private void syncData() {
        // 주기적 데이터 동기화 로직
    }
}

성능 벤치마크

시나리오 일반 JVM CRaC 복원 개선율
단순 Web App 3.2초 45ms 71×
JPA + Redis + Kafka 12.5초 85ms 147×
대규모 모놀리스 28초 120ms 233×

제한사항과 실전 팁

  • CRIU 권한: CRaC 내부적으로 CRIU를 사용하므로 CAP_CHECKPOINT_RESTORE 권한이 필요합니다. Docker에서는 --cap-add 또는 --privileged가 필요할 수 있습니다
  • 체크포인트 크기: JVM 힙 전체가 저장되므로 이미지 크기가 클 수 있습니다. 불필요한 캐시를 체크포인트 전에 정리하세요
  • 난수 재시드: 복원 후 SecureRandom이 같은 시드를 사용할 수 있으므로, 복원 콜백에서 재시드합니다
  • 시간 기반 로직: 체크포인트와 복원 사이 시간 차이를 고려해야 합니다. Instant.now()는 복원 후 정상 동작하지만, 캐시 TTL 등은 검증이 필요합니다
  • JIT 캐시 유지: CRaC의 큰 장점은 JIT 컴파일 결과가 보존된다는 것입니다. 워밍업 후 체크포인트를 생성하면 복원 즉시 최적화된 코드로 실행됩니다

마무리

CRaC는 GraalVM Native Image의 제약 없이 JVM의 풍부한 생태계를 유지하면서도 밀리초 단위 시작을 달성하는 실용적인 기술입니다. Spring Boot 3.2+의 네이티브 지원 덕분에 기존 애플리케이션에 최소한의 변경으로 적용할 수 있으며, K8s HPA와 결합하면 서버리스에 준하는 탄력성을 확보할 수 있습니다.

위로 스크롤
WordPress Appliance - Powered by TurnKey Linux