← 문제로

47. 이벤트 루프의 연결당 무제한 드레인 (공정성 붕괴 / 기아)

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

해설 — 이벤트 루프의 연결당 무제한 드레인 (공정성 붕괴 / 기아)

난이도: 상

요약

Tick의 바깥 순회 (A) 안에서, 각 연결을 (B)의 while (TryReceive) 로 수신 큐가 빌 때까지 전부 처리한다. 특정 연결이 폭주(firehose)해 큐가 계속 차 있으면, 그 연결의 while이 끝나지 않아 순회가 다음 연결로 넘어가지 못한다. 결과적으로 한 연결이 단일 스레드 루프를 독점하고, 다른 연결·타이머·하트비트 처리가 굶는다(starvation). 처리 속도보다 유입이 빠르면 사실상 무한 루프처럼 다른 세션이 응답 불능이 된다.

문제점

  • 분류태그: 공정성/스케줄링 결함 — 연결당 작업량 상한 부재(head-of-line 독점)
  • 증상: 한 클라이언트가 대량 전송하는 동안 다른 유저들의 핑 급증·응답 지연·하트비트 타임아웃으로 인한 오연결 해제. 큐 메모리 급증.
  • 재현조건: 어떤 연결의 유입률 ≥ 처리율. 또는 한 tick에 한 연결이 수만 개 메시지를 밀어 넣음.
  • 근본원인: 루프가 "이 연결에 데이터가 있는 한 계속"을 종료 조건으로 삼아, 연결 간 **시간 분할(time-slicing)**이 없다. 공정성 정책이 없다.

수정안

연결당 tick 예산(처리 메시지 수 또는 바이트)을 두고, 예산을 소진하면 남은 작업을 다음 tick으로 미룬다(라운드로빈). 아직 데이터가 남은 연결은 "여전히 준비됨"으로 표시해 다음 tick에 이어 처리한다.

public int ReadBudgetPerConn = 64;   // 튜닝값

public void Tick()
{
    foreach (var conn in _conns)
    {
        int processed = 0;
        while (processed < ReadBudgetPerConn && conn.TryReceive(out var msg))
        {
            Handler(conn, msg);
            processed++;
        }
        // 큐에 남아 있으면 다음 tick에서 이어서 처리(연결은 계속 ready 상태 유지).
        // 필요하면 conn.Inbox.Count>0 인 연결을 별도 ready 집합에 남겨둔다.
    }
    // 타이머/하트비트 등은 매 tick 반드시 실행된다.
}

핵심: 한 연결이 한 tick에 할 수 있는 일에 상한을 둬 루프가 반드시 다른 연결·타이머로 진행하게 한다. 추가로 연결별 유입 레이트 리밋/백프레셔(수신 윈도우 축소, 큐 상한 초과 시 읽기 일시 중단)를 얹으면 메모리 급증도 막는다.

더 나은 설계

  • 가중 공정 큐잉(WFQ)/DRR: 연결마다 quantum을 부여해 바이트 단위로 공정 분배. 대용량 메시지가 소량 메시지를 밀어내지 못하게.
  • 백프레셔: 큐 길이가 상한을 넘으면 그 소켓의 읽기 이벤트 등록을 잠시 해제(레벨 트리거 재무장 지연)해 유입을 늦춘다.
  • 워커 분리: I/O 루프와 무거운 핸들러를 분리(핸들러는 작업 큐로). 단 순서 보장/세션 어피니티 설계 필요.
  • 타이머 우선 보장: tick 예산과 별개로 만료 타이머·하트비트는 매 tick 실행 보장.

면접 포인트

  • 단일 스레드 이벤트 루프에서 "한 fd를 완전히 드레인"이 왜 공정성/지연 문제를 만드는가(예: epoll ET에서 EAGAIN까지 드레인 vs 예산 분할의 균형).
  • 백프레셔와 공정 스케줄링(DRR/WFQ)의 관계, 큐 상한과 메모리 보호.
  • 하트비트/타이머 기아가 어떻게 정상 연결의 오해제(false disconnect)로 이어지는가.