ADR-05: 워커-스케줄러 프로세스 분리
| 날짜 | 작성자 | 리포지토리 |
|---|---|---|
| 2024-12-18 | @KubrickCode | collector |
배경
비대칭적 스케일링 요구사항
백그라운드 작업 처리 시스템은 일반적으로 근본적으로 다른 스케일링 특성을 가진 두 개의 구분된 컴포넌트로 구성됨:
스케줄러:
- 주기적 작업 트리거 (cron 기반 태스크 enqueue)
- 중복 실행 방지를 위해 단일 인스턴스로 실행 필수
- 가벼운 리소스 사용량 (CPU <5%, 메모리 <256 MB)
- 변경 빈도 낮음 (cron 표현식 업데이트, 새 작업 유형)
워커:
- 큐에 있는 태스크 처리 (분석, 파일 작업, API 호출)
- 큐 깊이에 따라 수평 확장
- 높은 리소스 소비 (CPU 집약적, 대용량 페이로드를 위한 메모리)
- 잦은 업데이트 (비즈니스 로직 변경, 알고리즘 개선)
통합 프로세스의 문제점
스케줄러와 워커를 단일 프로세스로 실행하면 여러 문제가 발생함:
| 문제 | 영향 |
|---|---|
| 리소스 낭비 | 스케줄러 코드가 모든 워커 인스턴스에서 실행 (미사용) |
| 스케일링 비효율 | 스케줄러 복제 없이 워커만 확장 불가 |
| 장애 커플링 | 워커 OOM이 스케줄러까지 크래시 |
| 배포 종속 | 스케줄러 변경에 전체 재배포 필요 |
| 분산 락 오버헤드 | 모든 인스턴스가 락 획득 시도, 하나만 성공 |
서로 다른 의존성 요구사항
각 프로세스 유형은 고유한 의존성 요구사항을 가짐:
- 워커: 암호화 키 필요 (비공개 저장소를 위한 OAuth 토큰 복호화)
- 스케줄러: 분산 락 필요 (단일 인스턴스 보장), 암호화 불필요
이를 결합하면 불필요한 보안 노출과 설정 복잡성이 발생함.
결정
Worker와 Scheduler를 전용 진입점과 DI 컨테이너를 가진 독립 프로세스로 분리
아키텍처
┌──────────────┐ ┌───────────┐ ┌──────────────┐
│ Scheduler │─────>│PostgreSQL │<─────│ Workers │
│ (1 인스턴스) │ │River 큐 │ │ (0-N 확장) │
└──────────────┘ └───────────┘ └──────────────┘
│ │ │
└────────────────────┴────────────────────┘
│
┌──────────────┐
│ PostgreSQL │
│(데이터 저장) │
└──────────────┘진입점 분리
cmd/
├── worker/main.go # 큐 태스크 처리
└── scheduler/main.go # 주기적 작업 스케줄링각 진입점:
- 필요한 설정만 검증
- 필요한 의존성만 초기화
- 전용 graceful shutdown 처리
DI 컨테이너 분리
WorkerContainer:
├── 암호화 어댑터 (OAuth 토큰 복호화)
├── 분석 핸들러 (큐 태스크 프로세서)
├── 큐 클라이언트 (태스크 소비)
└── 공유: 데이터베이스 풀, PostgreSQL 연결
SchedulerContainer:
├── 분산 락 (단일 인스턴스 보장)
├── 스케줄러 핸들러 (주기적 작업 실행기)
├── 큐 클라이언트 (태스크 enqueue)
└── 공유: 데이터베이스 풀, PostgreSQL 연결핵심 원칙: 워커 컨테이너는 락을 초기화하지 않고, 스케줄러 컨테이너는 암호화를 초기화하지 않음.
검토한 옵션
옵션 A: 프로세스 분리 (선택)
설명:
전용 진입점과 DI 컨테이너를 가진 별도 바이너리.
장점:
- 독립적 스케일링 (워커: 0-N, 스케줄러: 정확히 1)
- 장애 격리 (워커 크래시가 스케줄링에 영향 없음)
- 리소스 최적화 (스케줄러: 최소, 워커: 대용량)
- 명확한 보안 경계 (암호화 키는 워커에만)
- 빌드 시점 최적화 (더 작은 스케줄러 바이너리)
단점:
- 두 개의 배포 파이프라인 유지 필요
- 설정 동기화 필요
- 더 복잡한 모니터링 설정
옵션 B: 런타임 모드 플래그가 있는 단일 바이너리
설명:
환경 변수나 플래그에 따라 동작을 전환하는 단일 바이너리.
bash
./collector --mode=worker
./collector --mode=scheduler장점:
- 단일 빌드 산출물
- 공유 코드베이스 (중복 감소)
- 더 단순한 CI/CD 파이프라인
단점:
- 바이너리에 미사용 코드 포함 (스케줄러가 워커 의존성을 메모리에 로드)
- 런타임 잘못된 설정 위험 (잘못된 모드 배포)
- 코드 검사로 서비스 역할 불명확
- 여전히 별도 배포 설정 필요
옵션 C: 통합 프로세스
설명:
단일 프로세스가 스케줄러와 워커를 다른 고루틴에서 실행함.
장점:
- 가장 단순한 배포 (하나의 서비스)
- 프로세스 간 통신 오버헤드 없음
- 단일 설정 파일
단점:
- 컴포넌트 독립 스케일링 불가
- 리소스 낭비 (모든 워커 인스턴스에 스케줄러)
- 장애 커플링 (워커 panic이 스케줄러 종료)
- max(스케줄러, 워커) 리소스로 프로비저닝 필요
구현 원칙
설정 검증
각 프로세스는 자신의 요구사항만 검증함:
Worker 시작:
├── DATABASE_URL 확인 (필수)
├── ENCRYPTION_KEY 확인 (필수) ← 워커 고유
└── 누락 시 즉시 실패
Scheduler 시작:
├── DATABASE_URL 확인 (필수)
├── 분산 락 초기화 ← 스케줄러 고유
└── 연결 실패 시 즉시 실패분산 락 전략
스케줄러는 PostgreSQL 기반 분산 락을 사용해 단일 인스턴스 실행 보장함:
인스턴스 A: 락 획득 → 스케줄된 작업 실행
인스턴스 B: 락 획득 실패 → 대기 상태 유지
인스턴스 C: 락 획득 실패 → 대기 상태 유지이점:
- Blue-green 배포 중 고가용성
- 활성 인스턴스 크래시 시 자동 장애조치
- 장시간 작업을 위한 락 heartbeat TTL 연장
큐 기반 통신
스케줄러와 워커는 메시지 큐를 통해서만 통신함:
Scheduler ──[태스크 Enqueue]──> River Queue (PostgreSQL) ──[태스크 Dequeue]──> Worker디커플링 이점:
- 스케줄러가 워커 완료를 기다리지 않음
- 워커가 자체 속도로 처리
- 큐 깊이를 통한 자연스러운 백프레셔
- 스케줄러-워커 간 직접 네트워크 통신 없음
Graceful Shutdown 처리
각 프로세스는 맞춤형 종료 동작을 가짐:
Worker 종료:
- 큐에서 새 태스크 수락 중지
- 진행 중 태스크 대기 (설정 가능한 타임아웃)
- 데이터베이스/PostgreSQL 연결 종료
- 종료
Scheduler 종료:
- cron 스케줄러 중지 (새 작업 트리거 방지)
- 현재 작업 완료 대기 (타임아웃 포함)
- 분산 락 해제
- 데이터베이스/PostgreSQL 연결 종료
- 종료
결과
긍정적
독립적 스케일링:
- 스케줄러 변경 없이 큐 깊이에 따라 워커 확장
- 스케줄러는 최소 상태 유지 (단일 인스턴스, 낮은 리소스)
- PaaS 자동 스케일링은 워커에만 적용
비용 최적화:
- 스케줄러: 고정된 최소 리소스 (~0.25 vCPU, 256 MB)
- 워커: 수요에 따라 0-N 스케일
- N개의 통합 인스턴스 실행 대비 상당한 절감
장애 격리:
- 워커 메모리 고갈이 스케줄링에 영향 없음
- 스케줄러 문제가 워커 태스크 처리 방해 안 함
- 전면 장애 대신 부분 저하
배포 독립성:
- 스케줄러 다운타임 없이 워커 로직 업데이트
- 워커 재배포 없이 cron 스케줄 변경
- 컴포넌트별 다른 릴리스 케이던스
보안 경계:
- 암호화 키가 워커 프로세스에만 제한
- 스케줄러가 축소된 권한으로 운영
- 프로세스 유형별 명확한 감사 추적
부정적
운영 복잡성:
- 모니터링, 배포, 유지관리할 서비스 두 개
- 다중 CI/CD 파이프라인
- 서비스 간 로그 집계
설정 관리:
- 공유 태스크 스키마 조율 필요
- 환경 변수 동기화
- 큐 네이밍 일관성
디버깅 오버헤드:
- 서비스 간 요청 추적
- 분산 로그 상관관계
- 다중 대시보드 모니터링
기술적 함의
| 측면 | 함의 |
|---|---|
| 인프라 | 별도 PaaS 서비스, 공유 PostgreSQL |
| 배포 | 독립 릴리스 사이클, 계약 변경 시 조율 |
| 스케일링 | 워커: 자동 스케일, 스케줄러: 고정 단일 인스턴스 |
| 모니터링 | 서비스별 메트릭, 통합 큐 깊이 모니터링 |
| Blue-Green 배포 | 워커: 중첩 가능, 스케줄러: 락 기반 핸드오프 |
참고 자료
- ADR-02: Clean Architecture 레이어 (컨테이너 분리)
- ADR-03: Graceful Shutdown (생명주기 관리)
- ADR-03: API와 Worker 서비스 분리 (Cross-cutting)
- ADR-04: 큐 기반 비동기 처리
