← 문제로

3. 긴급 점검 종료의 드레이닝 부재 (순서·대기·플러시 누락)

난이도 최상
내 리뷰 · C#
해설 · C#

해설 — 긴급 점검 종료의 드레이닝 부재 (순서·대기·플러시 누락)

난이도: 최상

요약

Shutdown은 "플래그 내리고(A) 리스너 닫고(B) 세션 전부 끊고(C) 즉시 프로세스 종료(D)"만 한다. 진행 중인 멀티스텝 요청을 기다리지 않고, 영속화 큐를 비우지(flush) 않으며, 클러스터에서 등록 해제하지 않는다. 결과적으로 분산 작업이 반쯤 적용된 채 끊기고(존 이전 원본 제거+대상 미추가), 큐에 쌓인 더티 상태가 통째로 유실되고, 다른 서버는 죽은 서버로 계속 라우팅한다.

문제점

1. (드레이닝) 진행 중 요청을 기다리지 않음 — (C)(D)

  • 증상: 멀티스텝 요청이 중간에 잘린다. 예: 존 이전에서 step2(대상 서버 RPC) 응답 전에 (C)로 세션이 끊기고 (D)로 프로세스가 죽으면, 원본에서 아이템/캐릭터는 빠졌는데 대상에는 안 들어간 상태로 영구 유실.
  • 재현조건: HandleRequest가 step2 RPC 대기 중일 때 Shutdown 호출.
  • 근본원인: in-flight 요청 수를 추적하거나 그것이 끝나길 기다리는 단계가 없다. _running=false(A)는 새 요청만 막을 뿐, 이미 시작된 멀티스텝을 안전 지점까지 진행시키지 못한다.

2. (데이터 유실) 영속화 큐 미플러시 — (D)

  • 증상: _persist에 쌓인 더티 상태가 DB에 못 가고 사라진다(플레이어 진행도 롤백처럼 보임).
  • 근본원인: Environment.Exit(0)은 백그라운드 saver를 기다리지 않고 즉시 죽인다. 종료 전에 큐를 비우는(flush+await) 단계가 없다.

3. (분산 라우팅) 클러스터 등록 해제 누락 — _cluster

  • 증상: 종료 중/직후에도 다른 서버·게이트웨이가 이 서버로 새 요청/세션을 보낸다 → 연결 거부·타임아웃·또 다른 반쪽 작업.
  • 근본원인: 종료의 첫 단계여야 할 Deregister()(트래픽 차단)가 아예 없다.

4. (순서·데드라인) 강제 종료 폴백 없음

  • 순서가 거꾸로다(세션부터 끊고 종료). 또 무한정 기다릴 수도 없으므로 유예 데드라인 + 데드라인 초과 시 강제 종료가 필요한데 둘 다 없다(지금은 데드라인=0인 강제 종료).

수정안

올바른 순서: 트래픽 차단 → 신규 거절 → in-flight 드레이닝(데드라인) → 영속화 플러시 → 세션 종료 → 종료.

private int _inFlight;   // Interlocked 로 증감
public async Task ShutdownAsync(TimeSpan grace)
{
    // 1) 새 트래픽부터 끊는다: 클러스터 등록 해제 + 리스너 닫기
    _cluster.Deregister();
    _listener.Close();
    _running = false;                       // 새 요청 거절 시작

    // 2) 진행 중 요청이 안전 지점까지 끝나길 데드라인까지 대기
    var deadline = DateTime.UtcNow + grace;
    while (Volatile.Read(ref _inFlight) > 0 && DateTime.UtcNow < deadline)
        await Task.Delay(50);

    // 3) 영속화 큐를 끝까지 비우고(flush) 확인
    await _persist.DrainAndFlushAsync(deadline);

    // 4) 그래도 남은 세션 정리 후 종료
    foreach (var s in _sessions) s.CloseGracefully();

    // 5) 데드라인 초과분만 강제 종료(데이터는 이미 플러시됨)
    Environment.Exit(0);
}

public async Task HandleRequest(Session s, Request req)
{
    if (!_running) { s.Reject("draining"); return; }
    Interlocked.Increment(ref _inFlight);
    try { /* step1..3: 멀티스텝은 중간 취소 대신 안전 지점까지 진행 */ }
    finally { Interlocked.Decrement(ref _inFlight); }
}
  • 멀티스텝 분산 작업(존 이전)은 사가/보상 트랜잭션으로 설계해, 드레이닝 중 끊겨도 재기동 후 "원본 제거 보상" 또는 "대상 추가 완료"로 수렴하게 한다(드레이닝만으론 부족, 아래 설계 참조).

더 나은 설계

  • 드레이닝 상태 머신: Running → Draining → Stopped. Draining에서 신규 거절(503/redirect), in-flight만 완료. 오케스트레이터(K8s preStop/lameduck)와 연동해 데드라인 부여.
  • 분산 작업의 원자성은 드레이닝이 아니라 프로토콜로: 존 이전 등은 2단계 커밋/사가 + 멱등 재개로 설계해야 "반쪽" 상태가 재기동 후 수렴한다. 드레이닝은 "정상 종료 시 손실 최소화"일 뿐, 갑작스런 크래시(전원 차단)에도 안전하려면 영속 로그/사가가 필수.
  • 영속화는 동기 체크포인트 경로 보유: 종료 시 큐를 flush하고, 더해 주기적 스냅샷으로 유실 창을 좁힌다.
  • 트레이드오프: 긴 grace는 안전하지만 점검 지연. 보통 수십 초 데드라인 + 강제 폴백. in-flight 추적 비용은 작다.

면접 포인트

  1. graceful shutdown의 순서: (등록 해제·리스너 닫기) → (신규 거절) → (in-flight 드레이닝, 데드라인) → (영속화 flush) → (세션 종료) → exit. 순서가 틀리면 트래픽이 새거나 데이터가 샌다.
  2. 드레이닝은 정상 종료를 매끄럽게 할 뿐, 갑작스런 크래시 안전성은 사가/2PC + 멱등 재개로 별도 보장해야 한다.
  3. Environment.Exit/즉시 종료는 백그라운드 영속화를 죽인다 — 종료 전에 큐 flush + 데드라인 초과 시에만 강제.