ADR-03: Graceful Shutdown 및 컨텍스트 기반 생명주기 관리
| 날짜 | 작성자 | 리포지토리 |
|---|---|---|
| 2024-12-18 | @KubrickCode | collector |
배경
큐 기반 시스템의 종료 문제
큐 기반 비동기 처리(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 (태스크에서 상속)종료 시퀀스
- 종료 신호 수신 (SIGTERM, API 호출 등)
- 새 작업 수락 중지
- 애플리케이션 context 취소
- 진행 중 태스크 대기 (타임아웃 포함)
- 정리 핸들러 실행
- 종료
타임아웃 전략
| 컴포넌트 | 타임아웃 고려사항 |
|---|---|
| 개별 태스크 | 예상 최대 시간 + 버퍼 기반 |
| 서버 종료 | 태스크 타임아웃 + 정리 시간의 합 |
| 플랫폼 유예기간 | 서버 종료 타임아웃을 초과해야 함 |
결과
긍정적
배포 신뢰성:
- Blue-green 배포 정상 동작
- 고아 프로세스나 멈춘 태스크 없음
- 예측 가능한 롤아웃 타이밍
리소스 관리:
- 제한된 실행 시간으로 리소스 고갈 방지
- 실패한 태스크가 리소스를 무한 소비하지 않음
- 깔끔한 프로세스 종료로 모든 리소스 해제
관측성:
- 실패 기록 항상 저장됨 (정리 context)
- 타임아웃 이벤트 분석을 위해 로깅
- 종료 시퀀스 감사 가능
PaaS 호환성:
- SIGTERM/SIGKILL 계약 준수
- 플랫폼 유예 기간 내 완료
- 자동 스케일링 및 인스턴스 교체 가능
부정적
복잡도:
- context 전파로 보일러플레이트 추가
- 정리 context 패턴 설명 필요
- 여러 타임아웃 값 설정 및 튜닝 필요
튜닝 필요:
- 타임아웃 값은 워크로드 특성에 맞춰야 함
- 너무 짧음: 조기 종료
- 너무 긺: 느린 배포
테스트 오버헤드:
- 테스트가 context 취소 시나리오 처리 필요
- Mock 구현에 context 인식 필요
- 타임아웃 테스트가 느리거나 불안정할 수 있음
