Skip to content

ADR-03: Graceful Shutdown 및 컨텍스트 기반 생명주기 관리

🇺🇸 English Version

날짜작성자리포지토리
2024-12-18@KubrickCodecollector

배경

큐 기반 시스템의 종료 문제

큐 기반 비동기 처리(ADR-05)는 생명주기 관리 문제를 수반함:

장시간 실행 태스크 처리:

  • 분석 태스크가 상당 시간 실행될 수 있음 (저장소 clone, 파싱, 메트릭 계산)
  • 순진한 종료(즉시 중단)는 데이터 손실과 불일치 상태 유발
  • 완료까지 무한 대기는 배포를 차단

PaaS 환경 제약:

  • 플랫폼은 SIGKILL 전 유예 기간과 함께 SIGTERM 전송
  • 서비스는 이 윈도우 내에 정리 완료 필요
  • 응답 없는 프로세스는 강제 종료

취소 후 정리:

  • 일부 작업은 취소 후에도 완료 필요 (에러 로깅, 상태 업데이트)
  • 부모 context 취소는 자식 작업으로 전파
  • 취소된 context 사용 시 정리 코드 실패

적절한 생명주기 관리 없는 실패 시나리오

시나리오관리 없음관리 있음
긴 태스크 중 배포실행 중 태스크 중단태스크 완료 또는 타임아웃
SIGTERM 수신갑작스러운 종료우아한 배수 및 정리
태스크가 예상 시간 초과종료 무한 차단타임아웃으로 완료 강제
취소된 태스크 중 에러 발생정리 조용히 실패정리 독립적으로 성공

결정

4가지 핵심 원칙을 가진 context 기반 생명주기 관리 패턴 채택

1. 서버 생명주기 분리

서버 시작과 종료 제어 분리:

패턴:

Start() → 처리 시작
Shutdown() → 우아한 중지 신호, 진행 중 태스크 대기

근거:

  • Run() 패턴(라이브러리에서 흔함)은 내부 종료까지 차단
  • Start() + Shutdown()은 외부에서 생명주기 제어 가능
  • 여러 컴포넌트 간 조율된 종료 가능

2. 태스크 수준 타임아웃

개별 태스크 실행에 설정 가능한 타임아웃 적용:

패턴:

taskCtx, cancel := context.WithTimeout(parentCtx, taskTimeout)
defer cancel()
executeTask(taskCtx)

근거:

  • 단일 태스크가 전체 시스템 차단 방지
  • 예측 가능한 최대 실행 시간 제공
  • 리소스 계획 및 SLA 준수 가능

3. 정리 Context 독립성

취소 후 정리를 위해 독립적 context 사용:

패턴:

if err := executeTask(taskCtx); err != nil {
    cleanupCtx := context.Background()
    recordFailure(cleanupCtx, err)  // taskCtx 취소되어도 성공
}

근거:

  • 부모 취소가 에러 기록을 방해하면 안 됨
  • 실패 추적을 위한 데이터베이스 쓰기는 완료 필요
  • 감사 추적 무결성은 독립적 정리 필요

4. 스케줄러 Context 전파

조율된 스케줄러 종료를 위해 부모 context 전파:

패턴:

RunWithContext(ctx) → 종료를 위해 ctx.Done() 준수

근거:

  • 스케줄러 루프는 종료 신호에 응답 필요
  • 주기적 작업 루프에서 깔끔한 종료 가능
  • 서버 종료 시퀀스와 조율

검토한 옵션

옵션 A: Context 기반 생명주기 관리 (선택)

설명:

Go의 context 패키지를 사용해 호출 스택 전체에 취소, 타임아웃, 데드라인 전파. 명시적 Start/Shutdown 분리와 결합.

장점:

  • 네이티브 Go 패턴, 개발자에게 잘 알려짐
  • 구성 가능: 타임아웃, 취소, 값을 단일 추상화에
  • 호출 체인을 통해 자동 전파
  • 작업별 세밀한 제어 가능

단점:

  • context 전파에 규율 필요
  • 정리 context 패턴이 직관적이지 않을 수 있음
  • 테스트에 context 인식 Mocking 필요

옵션 B: 고정 대기 시간

설명:

종료 신호 후 고정 시간 대기, 이후 강제 종료.

SIGTERM → wait(30s) → force exit

장점:

  • 단순한 구현
  • 예측 가능한 종료 시간

단점:

  • 짧은 대기: 태스크 조기 종료
  • 긴 대기: 배포 지연, 리소스 낭비
  • 태스크별 세분화 불가
  • 실제 태스크 요구사항에 적응 불가

옵션 C: 무제한 대기 (타임아웃 없음)

설명:

모든 진행 중 태스크가 자연스럽게 완료될 때까지 대기.

장점:

  • 어떤 태스크도 실행 중 종료되지 않음
  • 단순한 멘탈 모델

단점:

  • 멈춘 태스크가 종료 무한 차단
  • PaaS는 어차피 유예 기간 후 SIGKILL
  • 무한 루프나 데드락에 대한 보호 없음
  • 배포 속도 저하

구현 원칙

Context 계층

applicationCtx (SIGTERM 시 취소)
  └── serverCtx (Shutdown() 시 취소)
        └── taskCtx (타임아웃 또는 부모 취소 시 취소)
              └── operationCtx (태스크에서 상속)

종료 시퀀스

  1. 종료 신호 수신 (SIGTERM, API 호출 등)
  2. 새 작업 수락 중지
  3. 애플리케이션 context 취소
  4. 진행 중 태스크 대기 (타임아웃 포함)
  5. 정리 핸들러 실행
  6. 종료

타임아웃 전략

컴포넌트타임아웃 고려사항
개별 태스크예상 최대 시간 + 버퍼 기반
서버 종료태스크 타임아웃 + 정리 시간의 합
플랫폼 유예기간서버 종료 타임아웃을 초과해야 함

결과

긍정적

배포 신뢰성:

  • Blue-green 배포 정상 동작
  • 고아 프로세스나 멈춘 태스크 없음
  • 예측 가능한 롤아웃 타이밍

리소스 관리:

  • 제한된 실행 시간으로 리소스 고갈 방지
  • 실패한 태스크가 리소스를 무한 소비하지 않음
  • 깔끔한 프로세스 종료로 모든 리소스 해제

관측성:

  • 실패 기록 항상 저장됨 (정리 context)
  • 타임아웃 이벤트 분석을 위해 로깅
  • 종료 시퀀스 감사 가능

PaaS 호환성:

  • SIGTERM/SIGKILL 계약 준수
  • 플랫폼 유예 기간 내 완료
  • 자동 스케일링 및 인스턴스 교체 가능

부정적

복잡도:

  • context 전파로 보일러플레이트 추가
  • 정리 context 패턴 설명 필요
  • 여러 타임아웃 값 설정 및 튜닝 필요

튜닝 필요:

  • 타임아웃 값은 워크로드 특성에 맞춰야 함
  • 너무 짧음: 조기 종료
  • 너무 긺: 느린 배포

테스트 오버헤드:

  • 테스트가 context 취소 시나리오 처리 필요
  • Mock 구현에 context 인식 필요
  • 타임아웃 테스트가 느리거나 불안정할 수 있음

참고 자료

Open-source test coverage insights