Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

13. 운영 패턴: 고가용성 시스템 설계 🔴

학습 목표:

  • watch 채널과 select!을 활용한 우아한 종료(Graceful Shutdown) 메커니즘을 구축합니다.
  • 백프레셔(Backpressure): 제한된 채널을 사용하여 시스템 과부하를 방지하는 법을 배웁니다.
  • 구조적 동시성(Structured Concurrency): JoinSetTaskTracker로 태스크 군단을 체계적으로 관리합니다.
  • 타임아웃, 재시도, 지수적 백오프(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**에게 맡기세요.