ADR-04: 큐 기반 비동기 처리
| 날짜 | 작성자 | 리포지토리 |
|---|---|---|
| 2024-12-17 | @KubrickCode | web, collector |
배경
장시간 실행 작업의 본질
연산 분석을 수행하는 시스템은 근본적인 도전에 직면함: 처리 시간이 크게 변동하며 사전 예측이 불가능함. 이는 빠른 응답에 대한 사용자 기대와 실제 분석 소요 시간 사이의 충돌을 야기함.
이러한 워크로드의 핵심 특성:
| 특성 | 설명 |
|---|---|
| 예측 불가능한 소요시간 | 입력 크기에 따라 수 초에서 수 분까지 변동 |
| 높은 리소스 소비 | CPU, 메모리, I/O 집약적 작업 |
| 사용자 기대치 | 작업 크기와 무관하게 빠른 확인(<1초) |
| 장애 모드 | 네트워크 문제, 메모리 부족, 타임아웃 시나리오 |
HTTP 프로토콜의 한계
표준 HTTP 상호작용은 실용적 제약을 부과함:
- 브라우저 타임아웃: 대부분의 브라우저는 30-60초 후 연결 해제
- 로드밸런서 제한: 인프라에서 일반적으로 60초 타임아웃 강제
- 연결 관리: 장시간 유지되는 연결은 비효율적으로 리소스 소비
- 사용자 경험: 동기 요청 중 사용자가 다른 페이지로 이동 불가
핵심 질문
수 초에서 수 분이 소요될 수 있는 작업을 시작하는 요청에서, 요청 수락과 결과 전달 사이의 통신을 어떻게 처리해야 하는가?
결정
장시간 실행 작업에 큐 기반 비동기 처리 채택
River를 선택한 이유:
- 폴링 문제: Asynq는 지속적인 Redis 폴링 필요, 지연시간 및 리소스 사용량 증가
- 트랜잭션 일관성: River는 PostgreSQL 사용, 동일 DB 트랜잭션 내 작업 enqueue 가능
- 운영 단순성: 데이터와 큐를 단일 PostgreSQL 인스턴스로 통합 (별도 Redis 불필요)
- 내구성: ACID 보장을 갖춘 PostgreSQL 기반 큐
패턴은 다음 흐름을 따름:
사용자 → API (요청 수락) → 큐 → 워커 (처리) → 데이터베이스
↓ ↓
작업 ID 반환 결과 저장
↓ ↓
사용자 상태 조회 ←─────────────────────────┘핵심 원칙:
- 즉각적 확인: API가 밀리초 내에 작업 식별자 반환
- 백그라운드 처리: 워커가 자체 속도로 큐에서 작업 소비
- 상태 가시성: 사용자가 블로킹 없이 진행 상황 확인 가능
- 재시도 기능: 실패한 작업이 백오프와 함께 자동 재시도
검토한 옵션
옵션 A: 큐 기반 비동기 처리 (선택)
작동 방식:
- API가 요청 수신, 입력 검증, 작업 레코드 생성
- 작업이 메타데이터(작업 ID, 파라미터)와 함께 큐에 등록
- API가 작업 ID와 함께 HTTP 202 Accepted 반환
- 워커가 큐에서 작업을 가져와 처리, 데이터베이스 업데이트
- 사용자가 상태 엔드포인트를 폴링하거나 알림 수신
장점:
- 처리 시간과 무관하게 즉각적인 사용자 피드백
- API와 워커 컴포넌트의 독립적 스케일링
- 장애 격리: 워커 장애가 API를 다운시키지 않음
- 지수 백오프를 포함한 내장 재시도 메커니즘
- 복구 불가 장애를 위한 Dead Letter Queue (DLQ)
- 백프레셔 처리: 큐가 트래픽 급증을 버퍼링
단점:
- 추가 인프라: 메시지 큐 시스템 필요
- 운영 복잡도: 모니터링할 컴포넌트 증가
- 최종적 일관성: 결과가 즉시 사용 불가
- 폴링 오버헤드 또는 실시간 연결의 복잡성
옵션 B: 동기 처리
작동 방식:
사용자 → API → 처리 (블로킹) → 응답
└────── 30+ 초 ──────┘장점:
- 단순한 구현: 단일 요청-응답 사이클
- 추가 인프라 불필요
- 성공 시 즉각적인 결과 전달
- 디버깅 용이: 단일 실행 경로
단점:
- 장시간 작업에서 HTTP 타임아웃 실패
- 리소스 경쟁: 처리가 API 스레드 블로킹
- 저조한 사용자 경험: 대기 중 피드백 없음
- 연쇄 장애: 메모리 부족이 전체 서비스에 영향
- 재시도 기능 없음: 사용자가 수동으로 재시도 필요
- 처리를 독립적으로 스케일링 불가
옵션 C: 웹훅 콜백
작동 방식:
- 사용자가 콜백 URL과 함께 작업 제출
- API가 수락을 반환하고 처리 시작
- 완료 시 시스템이 콜백 URL로 결과 POST
- 사용자 서버가 알림 수신
장점:
- 완료 시 실시간 알림
- 폴링 불필요
- 이벤트 기반 아키텍처와 정렬
- 상태 확인으로 인한 API 부하 감소
단점:
- 사용자가 콜백 엔드포인트 제공 및 유지 필요
- 전달 신뢰성 우려: 콜백을 위한 재시도, DLQ
- 보안 복잡도: URL 검증, HMAC 서명
- 최종 사용자 대면 애플리케이션에 부적합
- 소비자에게 높은 통합 장벽
결과
긍정적
사용자 경험
| 지표 | 동기 처리 | 비동기 처리 |
|---|---|---|
| 초기 응답 시간 | 30+ 초 | <500ms |
| 이탈률 | 40-60% | 10-20% |
| 에러율 (타임아웃) | 작업에 따라 변동 | 거의 0 |
| 진행 상황 가시성 | 없음 | 전체 상태 |
시스템 안정성
- 장애 격리: 워커 메모리 부족이 API 서비스를 다운시키지 않음
- 우아한 성능 저하: 큐가 다운스트림 장애 중 요청 버퍼링
- 자동 복구: 일시적 장애가 사용자 개입 없이 재시도
- 관측성: 큐 깊이가 명확한 상태 신호 제공
확장성
- 큐 깊이를 기반으로 워커 독립 스케일링
- 요청 빈도를 기반으로 API 스케일링
- 큐 버퍼링으로 트래픽 급증 처리
- 리소스 최적화: 워커에 고메모리, API에 저지연
부정적
운영 오버헤드
- 큐 시스템이 핵심 인프라가 됨
- 모니터링 필요: 큐 깊이, 처리 지연시간, 장애율
- 유지보수할 다중 배포 파이프라인
- 환경 설정 동기화 필요
복잡도
- 분산 시스템 디버깅 필요
- 사용자에게 전달할 최종적 일관성 모델
- 추가 장애 모드: 큐 불가용, 메시지 손실
- 컴포넌트 간 상태 동기화
기술적 함의
| 측면 | 함의 |
|---|---|
| 큐 선택 | 트랜잭션 일관성과 운영 단순성을 위한 PostgreSQL 기반 River |
| 재시도 전략 | 지터가 포함된 지수 백오프; 일시적 vs 영구적 장애 분류 |
| DLQ 처리 | 수동 검사 및 재실행 기능 필요 |
| 모니터링 | 큐 깊이, 처리 시간, 장애율 대시보드 |
| 멱등성 | 워커가 중복 작업 전달을 안전하게 처리해야 함 |
에러 분류 전략
| 에러 유형 | 재시도 동작 | 예시 |
|---|---|---|
| 일시적 | 지수 백오프 | 네트워크 타임아웃, 임시 DB 장애 |
| 비일시적 | 즉시 DLQ로 이동 | 잘못된 입력, 파싱 에러 |
| 리소스 제한 | 더 긴 대기 후 백오프 | 레이트 리밋, 메모리 압박 |
사용자 커뮤니케이션 패턴
- 제출: 예상 시간과 함께 작업 ID 반환
- 진행 중: 현재 단계와 퍼센티지 표시
- 완료: 결과 또는 에러 상세 제공
- 실패: 재시도 옵션과 함께 명확한 설명
