← 문제로

45. 유휴 타임아웃 판정과 활동 시각 갱신 (C#)

난이도 하
내 리뷰 · C#
해설 · C#

해설 — 유휴 타임아웃 판정과 활동 시각 갱신 (C#)

난이도: 하

요약

LastActivity 가 수신(OnReceive) 경로에서만 갱신되고 송신 경로에서는 갱신되지 않는다. 서버가 대형 자산을 오래 보내는 동안 클라이언트는 수신만 하므로 세션의 LastActivity 가 정체되고, IdleReaper 가 이 정상 연결을 유휴로 오판해 전송 도중 끊어 버린다(다운로드 중 강제 절단).

문제점

  • (A) 활동 시각 갱신의 비대칭: OnReceive 에서만 LastActivity 를 갱신한다. "활동"을 수신으로만 정의했기 때문에 서버→클라 단방향 전송 구간은 활동으로 집계되지 않는다.

    • 재현: 30초 넘게 걸리는 맵/스냅샷 전송 중 클라가 아무것도 안 보내면 SendLargeAsset 로 활발히 보내는 중에도 now - LastActivity > 30s 가 되어 끊긴다.
    • 근본 원인: 연결의 생존/활성 지표를 수신 활동에만 의존.
  • (B) 판정 기준의 단순함: Sweep 은 오직 LastActivity 만 본다. 진행 중인 아웃바운드 전송이나 애플리케이션 하트비트 같은 다른 생존 신호를 고려하지 않아 (A)의 정체를 그대로 "유휴"로 확정한다.

수정안

활동 시각을 송·수신 양쪽에서 갱신하거나, "유휴"의 정의를 애플리케이션 하트비트 기준으로 바꾼다. 진행 중 전송을 고려하면 더 견고하다.

public void OnReceive(byte[] data) { Touch(); /* ... */ }

public void SendLargeAsset(byte[] chunk)
{
    // ... 소켓 send ...
    Touch();                                  // (A) 송신도 활동으로 집계
}

private void Touch() => LastActivity = DateTime.UtcNow;

// (B) 판정: 진행 중 전송이 있으면 유휴로 보지 않는다.
public void Sweep(IEnumerable<Session> sessions)
{
    var now = DateTime.UtcNow;
    foreach (var s in sessions)
        if (!s.HasPendingSend && now - s.LastActivity > IdleTimeout)
            Disconnect(s);
}

더 견고한 대안: 앱 레벨 하트비트(ping/pong) 를 도입해 방향과 무관하게 "상대가 살아 있는가"를 판정하고, idle 타임아웃은 하트비트 미수신으로만 트리거한다.

더 나은 설계

  • 하트비트 기반 liveness: 데이터 트래픽과 분리된 주기 ping 으로 죽은 연결을 감지하면 대형 전송/무통신 구간에 영향받지 않는다. TCP keepalive 는 보조로.
  • 분리된 두 타임아웃: "무통신 유휴"와 "느린 전송/백프레셔"를 다른 정책으로 관리.
  • 전송 진행 관측: 아웃바운드 큐 진척을 활동 지표에 포함.

면접 포인트

  • "활동"의 정의를 수신에만 두면 서버 주도 전송 중 오절단이 난다 — 양방향으로 정의.
  • liveness(살아 있는가)와 idle(방치되었는가)는 다른 개념. 하트비트로 분리.