← 문제로

25. 수용 제어·부하 차단 부재로 인한 메타안정 장애 (C#)

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

해설 — 수용 제어·부하 차단 부재로 인한 메타안정 장애 (C#)

난이도: 상

요약

서버가 들어오는 요청을 무제한 큐에 쌓고(수용 제어 없음), 워커는 데드라인이 지난 요청까지 처리한다(헛일). 부하 스파이크가 오면 큐가 폭증해 지연이 데드라인을 넘고, 클라이언트 재시도가 새 부하로 유입되는 양의 피드백 루프가 형성된다. 그 결과 스파이크가 사라진 뒤에도 시스템이 과부하 상태에 갇히는 메타안정 장애(metastable failure) 가 된다.

문제점

분류 태그: admission control/load shedding 부재 / 데드라인 무시 헛일 → 메타안정 과부하

증상: 순간 부하 후 지연이 계속 높게 유지되고, 부하 그래프상 트래픽이 정상으로 돌아와도 큐 길이·지연이 내려오지 않는다. CPU 는 100%인데 유효 처리량(데드라인 내 응답)은 오히려 감소한다(goodput 붕괴).

재현 시나리오(피드백 루프):

  1. (A) 스파이크로 도착률 > 처리율 → 큐가 무제한으로 자란다.
  2. 큐 대기시간이 데드라인을 초과 → 클라이언트가 응답을 버리고 재시도 → 도착률이 더 올라간다.
  3. (B) 워커는 이미 데드라인이 지나 아무도 기다리지 않는 요청까지 처리 → CPU를 "버려질 응답"에 쓴다. 유효 처리량이 줄어 큐가 더 밀린다.
  4. 스파이크가 끝나 원래 도착률로 돌아와도, 쌓인 백로그 + 재시도가 처리율을 계속 초과 → 시스템이 과부하 평형에 고착. 외부 개입(트래픽 차단, 재시작) 없이는 회복 불가.

근본 원인: (1) 큐에 상한과 거절 정책이 없어 과부하가 흡수되지 않고 증폭된다. (2) 데드라인을 무시해 "이미 실패한 일"에 자원을 낭비하는 것이 goodput 을 무너뜨린다. 이 둘이 재시도 트래픽과 결합해 메타안정 상태를 만든다.

수정안

유한 큐 + 부하 차단(가득 차면 빠른 거절) + 데큐 시 데드라인 검사(만료 요청 폐기)로 피드백 루프를 끊는다.

public sealed class RequestServer
{
    private readonly BlockingCollection<Request> _queue;
    public RequestServer(int workers, int capacity)
    {
        _queue = new BlockingCollection<Request>(new ConcurrentQueue<Request>(), capacity);
        for (int i = 0; i < workers; i++)
            new Thread(WorkerLoop) { IsBackground = true }.Start();
    }

    // 가득 차면 즉시 거절 → 클라는 백오프. 큐가 무한히 자라지 않는다.
    public bool Submit(Request req)
    {
        return _queue.TryAdd(req);   // false = 부하 차단(load shedding)
    }

    private void WorkerLoop()
    {
        foreach (var req in _queue.GetConsumingEnumerable())
        {
            if (DateTime.UtcNow >= req.DeadlineUtc)   // 만료 요청은 버림(헛일 방지)
                continue;
            req.Work();
        }
    }
}

추가로: 큐 대기시간이 데드라인의 일정 비율을 넘으면 애초에 거절(예: 예상 대기 > 데드라인 → 즉시 shed), 클라이언트는 지수 백오프 + 지터 + 재시도 예산(retry budget) 으로 재시도 폭증을 억제한다.

더 나은 설계

  • 동시성 제한 + 대기열 상한: 워커 수와 큐 용량을 용량에 맞춰 정하고, 초과분은 429/재시도-불가로 빠르게 거절.
  • CoDel/adaptive LIFO 같은 큐 규율: 오래 기다린 요청을 우선 폐기(어차피 만료)해 최신 요청의 goodput 을 지킨다. LIFO 는 과부하 시 일부 요청을 빨리 끝내 전체 붕괴를 막는다.
  • 재시도 거버넌스: 서버가 Retry-After 를 주고, 클라이언트는 재시도 예산·서킷브레이커로 증폭을 차단.
  • 부하 자동 완화: 부하 신호(큐 길이·지연)에 따라 표본 비율로 shed(부분 저하)해 완전 붕괴 대신 우아한 성능 저하.
  • 트레이드오프: 거절은 사용자 경험을 해치지만, 무제한 수용은 "모두 실패"로 이어진다. 부분 거절이 전체 붕괴보다 낫다.

참고: 이 문제는 요청 경로의 데드라인 전파·재시도 증폭(별도 문제)과 달리, 서버 자신의 수용 제어·부하 차단 부재로 트리거가 사라진 뒤에도 과부하가 지속되는 메타안정 평형이 핵심이다.

면접 포인트

  • 메타안정 장애의 정의(트리거 제거 후에도 지속되는 과부하 평형)와, 이를 만드는 양의 피드백(재시도·헛일)을 설명.
  • load shedding/admission control 이 왜 "친절한 거절"로 전체 goodput 을 지키는가.
  • 데드라인 지난 요청을 버리는 것이 왜 처리량이 아니라 유효 처리량(goodput)을 높이는가.