13. 운영 패턴: 고가용성 시스템 설계 🔴
학습 목표:
watch채널과select!을 활용한 우아한 종료(Graceful Shutdown) 메커니즘을 구축합니다.- 백프레셔(Backpressure): 제한된 채널을 사용하여 시스템 과부하를 방지하는 법을 배웁니다.
- 구조적 동시성(Structured Concurrency):
JoinSet과TaskTracker로 태스크 군단을 체계적으로 관리합니다.- 타임아웃, 재시도, 지수적 백오프(Exponential Backoff) 등 견고한 네트워크 요청 패턴을 익힙니다.
- **
thiserror**와 **anyhow**를 적재적소에 활용하는 에러 처리 아키텍처를 설계합니다.
1. 우아한 종료 (Graceful Shutdown)
운영 서버는 종료 시그널(Ctrl+C)을 받았을 때 즉시 죽지 않고, 진행 중인 요청을 안전하게 마무리해야 합니다.
async fn main() { let (shutdown_tx, shutdown_rx) = watch::channel(false); // 서버 실행 (종료 신호를 감시하는 태스크) let server = tokio::spawn(run_server(shutdown_rx)); // Ctrl+C 대기 tokio::signal::ctrl_c().await.unwrap(); // 종료 신호 전송 shutdown_tx.send(true).unwrap(); // 모든 작업이 끝날 때까지 최대 30초 대기 tokio::time::timeout(Duration::from_secs(30), server).await.ok(); }
핵심: watch 채널은 여러 워커 태스크가 동시에 종료 신호를 감지하고 각자의 작업을 정리할 수 있게 해주는 최고의 도구입니다.
2. 백프레셔: "감당할 수 있을 만큼만 받기"
제한이 없는(Unbounded) 채널은 생산자가 너무 빠를 경우 메모리 부족(OOM)으로 시스템을 다운시킵니다. 운영 환경에서는 반드시 제한된(Bounded) 채널을 쓰세요.
#![allow(unused)] fn main() { // 최대 100개까지만 버퍼링함. 꽉 차면 생산자가 잠시 대기(Backpressure) let (tx, rx) = mpsc::channel(100); }
3. 구조적 동시성 (Structured Concurrency)
여러 개의 태스크를 하나로 묶어 관리하면, 일부가 패닉을 일으키거나 에러를 내뱉을 때 우아하게 대처할 수 있습니다.
JoinSet: 태스크들을 그룹화하고 완료 순서대로 결과를 수집하거나 한꺼번에 취소할 때 유용합니다.TaskTracker: "모든 태스크가 끝날 때까지 기다려!"라는 로직을 짤 때 최적입니다.
4. 에러 처리 전략 (thiserror vs anyhow)
| 도구 | 용도 | 특징 |
|---|---|---|
thiserror | 라이브러리 제작 | 구체적인 에러 타입을 정의하여 사용자에게 명확한 정보 제공 |
anyhow | 애플리케이션 개발 | 모든 에러를 하나로 묶어 빠르게 전파하고 컨텍스트 추가 가능 |
💡 실무 팁: 타임아웃과 지터(Jitter)
재시도 로직을 짤 때 단순히 "1초 뒤, 2초 뒤, 4초 뒤"처럼 고정된 시간을 쓰면, 서버 장애 복구 시 모든 클라이언트가 동시에 요청을 보내 다시 서버를 죽이는 '천둥 치는 무리(Thundering Herd)' 현상이 발생합니다. 재시도 시간에 약간의 **랜덤한 오차(Jitter)**를 섞어 요청을 분산시키세요.
🏋️ 연습 문제: 우아한 워커 풀 구현
도전 과제: 4개의 워커 태스크가 작업 큐를 공유하며 처리하다가, 종료 시그널이 오면 현재 처리 중인 작업까지만 딱 끝내고 종료되는 시스템을 설계해 보세요.
🔑 정답 및 힌트 보기
`mpsc` 채널로 작업을 전달하고, `watch` 채널로 종료 신호를 보냅니다. 워커는 `tokio::select!`를 사용해 작업 수신과 종료 신호 수신을 동시에 기다리게 하면 됩니다. 종료 신호가 오면 루프를 빠져나오기 전에 현재 처리하던 `work_item`에 대한 로직이 완료되도록 순서를 조정하세요.📌 요약
watch채널로 전사적인 종료 신호를 동기화하세요.- 제한된 채널로 메모리 폭주를 막으세요.
- **
thiserror**로 에러를 체계화하고 **anyhow**로 컨텍스트를 풍부하게 만드세요. - 대규모 태스크 관리는 **
JoinSet**이나 **TaskTracker**에게 맡기세요.