처리할 수 있는 양보다 요청이 많아지면 어떻게 버틸까? 백프레셔와 부하 차단

반응형

서버가 초당 100건을 처리할 수 있는데 500건이 계속 들어오면 일시적인 버퍼만으로 해결할 수 없다. 요청을 모두 받아 쌓으면 메모리와 연결, 데이터베이스 풀이 차례로 가득 차고 결국 정상 요청까지 오래 기다리다 실패한다.

백프레셔(backpressure: 뒤쪽 소비자가 처리할 수 있는 속도를 앞쪽 생산자에게 전달해 유입량을 조절하는 흐름 제어)는 과부하를 없애는 마법이 아니다. 시스템이 감당하지 못하는 일을 어디에서 기다리게 하고 언제 거절하며 무엇을 우선할지 정하는 규칙이다.

처리율보다 유입률이 크면 대기열은 계속 늘어난다

생산자(producer: 요청이나 작업을 만들어 다음 단계로 보내는 구성 요소)가 초당 500건을 보내고 소비자(consumer: 받은 요청이나 작업을 실제로 처리하는 구성 요소)가 초당 100건을 끝낸다면 매초 400건이 남는다.

유입 500건/초 → [대기열 +400건/초] → 처리 100건/초

10초이면 4,000건, 1분이면 24,000건이 쌓인다. 버퍼 크기를 키우면 실패 시점을 늦출 뿐 지속적인 처리량 차이를 해결하지 못한다.

리틀의 법칙(Little's Law: 안정된 시스템에서 평균 대기 항목 수는 평균 도착률과 평균 체류 시간의 곱이라는 관계)을 떠올리면 대기열이 길수록 응답 시간도 커진다는 사실을 볼 수 있다. 처리량만 유지된다고 건강한 상태는 아니다.

유한한 버퍼는 실패 경계를 보이게 한다

버퍼(buffer: 처리 전 데이터를 잠시 보관하는 메모리나 큐)를 무한히 늘릴 수 없으므로 최대 크기와 가득 찼을 때의 행동을 정한다.

버퍼 여유 있음 → 요청 수락
버퍼 가득 참   → 기다림 / 즉시 거절 / 낮은 우선순위 제거

bounded queue(유한 큐: 보관할 수 있는 항목 수나 바이트 수에 명시적인 상한이 있는 대기열)는 과부하를 초기에 드러낸다. 오류가 빨리 보이는 것이 불편해 보여도 모든 요청이 30초 후 실패하는 것보다 복구하기 쉽다.

개수만 제한하면 큰 요청 몇 개가 메모리를 차지할 수 있다. 항목 수, 전체 바이트, 예상 작업 비용 가운데 무엇을 기준으로 제한할지 정해야 한다.

흐름 제어는 앞 단계가 속도를 줄일 수 있을 때 작동한다

스트림 소비자가 한 번에 받을 양을 요청하거나, 큐 소비자가 처리 완료 뒤 다음 작업을 가져오거나, TCP 수신 창이 받을 수 있는 바이트를 알리는 방식은 모두 속도 피드백을 만든다.

수신 창(receive window: TCP 수신자가 현재 더 받을 수 있는 바이트 범위를 송신자에게 알리는 값)이 줄면 송신자는 무한히 데이터를 밀어 넣지 못한다. 애플리케이션 스트림도 소비자가 처리한 만큼만 다음 항목을 요청할 수 있다.

하지만 외부 인터넷 요청처럼 생산자를 직접 늦출 수 없다면 서버가 일부 요청을 거절해야 한다. 백프레셔는 항상 “기다려 달라”는 신호만이 아니라 수락 범위를 지키는 거절도 포함한다.

부하 차단은 실패할 일을 일찍 거절한다

로드 셰딩(load shedding: 과부하 때 일부 요청을 의도적으로 거절해 핵심 처리 능력을 보존하는 방식)은 서버가 완전히 무너지기 전에 일을 줄인다.

우선 유지: 로그인 확인, 결제 상태 조회
먼저 제한: 무거운 통계, 자동 추천 갱신, 낮은 우선순위 배치

모든 요청을 무작위로 버리기보다 업무 중요도와 비용을 기준으로 한다. 이미 오래 기다린 요청을 계속 실행해도 클라이언트 데드라인이 지났다면 결과를 사용할 수 없다. 남은 시간 예산이 없는 작업을 시작하지 않는 것도 부하 차단이다.

HTTP에서는 과부하를 429 Too Many Requests나 503 Service Unavailable로 알리고 재시도 가능한 시점을 Retry-After로 전달할 수 있다. 상태 코드만 보내고 모든 클라이언트가 동시에 재시도하면 다시 폭주하므로 백오프가 필요하다.

속도 제한은 평균과 순간 폭주를 함께 다룬다

레이트 리미터(rate limiter: 사용자나 키, 서비스별 요청 속도를 정해진 범위로 제한하는 구성 요소)는 공정성과 보호를 위해 유입을 제어한다.

토큰 버킷(token bucket: 일정 속도로 토큰을 채우고 요청마다 토큰을 써서 평균 속도와 짧은 폭주를 함께 허용하는 알고리즘)을 예로 들 수 있다.

버킷 용량: 20토큰
충전 속도: 초당 5토큰
요청 1건: 1토큰 사용

버킷에 모인 토큰이 있으면 짧은 순간에는 초당 5건보다 많이 처리할 수 있다. 장기 평균은 충전 속도로 제한된다. 제한 키를 IP 하나로만 잡으면 공유 네트워크 사용자를 함께 막을 수 있고 사용자 키만 잡으면 로그인 전 공격을 구분하기 어렵다. 여러 경계를 조합한다.

재시도는 과부하를 더 키울 수 있다

서버가 느려지면 클라이언트가 시간 초과 후 재시도한다. 원래 요청이 서버에서 계속 실행 중이면 동일한 부하가 겹친다. 많은 클라이언트가 같은 간격으로 재시도하면 재시도 폭풍(retry storm: 실패한 요청의 동시 재시도가 원래 장애보다 더 큰 부하를 만드는 현상)이 생긴다.

지수 백오프(exponential backoff: 재시도 간격을 실패할 때마다 점점 늘리는 방식)와 jitter(jitter: 많은 클라이언트의 재시도 시각이 겹치지 않도록 무작위 차이를 더하는 값)를 사용해 재시도를 분산한다.

재시도 횟수뿐 아니라 전체 데드라인과 멱등성을 확인해야 한다. 과부하 상황에서 무제한 재시도는 성공률을 높이기보다 회복 시간을 늦춘다.

관찰할 숫자는 큐 길이 하나가 아니다

큐 길이, 가장 오래 기다린 작업의 나이, 수락·거절률, 처리율, 실행 시간, 시간 초과율을 함께 본다. 큐 길이가 짧아도 계속 버리고 있을 수 있고 길어도 처리율이 높아 곧 회복될 수 있다.

용량 계획에서는 정상 평균보다 급증과 의존성 지연을 고려한다. 데이터베이스가 느려지면 애플리케이션의 처리율이 떨어지고 같은 유입에서도 큐가 급격히 늘 수 있다.

이해 확인 질문과 답변

버퍼를 크게 만들면 과부하를 해결할 수 있을까?

짧은 폭주를 흡수하는 데는 도움이 되지만 지속적으로 유입률이 처리율보다 크면 결국 가득 찬다. 큰 버퍼는 실패를 늦추면서 응답 시간을 더 길게 만들 수도 있다.

백프레셔와 레이트 리밋은 같은가?

겹치는 부분이 있지만 초점이 다르다. 백프레셔는 뒤쪽 처리 능력을 앞쪽에 전달하는 흐름이고 레이트 리밋은 사용자나 서비스별 허용 속도를 정책으로 제한한다. 함께 사용할 수 있다.

요청을 빨리 거절하는 것이 왜 도움이 될까?

성공 가능성이 낮은 작업이 연결과 메모리, 스레드, 데이터베이스를 오래 점유하지 않게 한다. 핵심 요청이 처리될 자원을 남기고 클라이언트도 대체 행동을 빨리 선택할 수 있다.

재시도 횟수만 줄이면 재시도 폭풍을 막을 수 있을까?

충분하지 않을 수 있다. 많은 클라이언트가 같은 시각에 한 번만 재시도해도 큰 파동이 생긴다. 지수 백오프, jitter, 전체 데드라인, 서버의 Retry-After 신호를 함께 사용한다.

AI에게 이렇게 요청할 수 있다

초당 500건이 들어오고 100건만 처리할 수 있는 서버의 큐 길이와 대기 시간을 시간표로 보여 줘. 유한 버퍼, 429와 503, 토큰 버킷, 지수 백오프와 jitter가 어디에서 유입을 줄이는지 비교해 줘.

과부하를 견딘다는 말은 모든 요청을 끝까지 받는다는 뜻이 아니다. 처리 가능한 양을 앞단에 알리고, 기다림의 상한을 두고, 성공 가능성이 낮은 일을 일찍 줄여 핵심 경로를 보존하는 일이다.

반응형
KEEP READING
카테고리 전체 보기 →

댓글