LabHub
배우기 러닝패스 코스

One Slow Subscriber Froze the Livestream

Why livestreams fall behind

LabHub 에서 이어서 보기

한국어 원문으로 표시합니다.

한 줄 요약

큐는 처리할 일을 잠시 보관하지만, 느린 소비자를 빠르게 만들지는 않습니다.

Concept map: 한 줄 요약 · 왜 이게 필요했나 · 어떻게 동작하나 · 현장에서 만나는 모습

왜 이게 필요했나

고양이 구조대의 위치를 생중계한다고 상상해 봅시다. 구독자 아홉 명은 새 좌표를 바로 화면에 그리지만 한 명은 화면 처리를 멈췄습니다. 서버가 모든 구독자의 완료를 차례로 기다린다면, 멈춘 한 사람이 나머지 아홉 명의 지도까지 정지시킵니다. 반대로 일단 모두 큐에 넣고 다음 일을 하면 발행자는 편해지지만, 느린 사람의 큐는 계속 커집니다. 기다림을 없앤 것이 아니라 메모리로 옮긴 것입니다.

이 코스는 Python 함수·클래스·예외와 async/await 기초를 아는 학습자를 대상으로 합니다. 앞의 TCP 택배 코스를 마쳤다면 바이트 수신과 메시지 완료가 다르다는 점을 떠올려 보세요. 이번에는 메시지가 완성된 뒤에도 업무가 밀릴 수 있다는 문제를 다룹니다. 실제 외부 서비스를 공격하거나 인터넷을 열 필요 없이, 같은 실습 컨테이너 안의 두 TCP 구독자로 재현합니다.

어떻게 동작하나

초당 100개가 들어오고 60개를 처리하면, 버리지 않는 대기열은 처리율이 그대로인 동안 초당 약 40개씩 증가합니다. 큐를 4,000칸으로 늘리면 포화 시점만 늦출 뿐 지속적인 부족을 해결하지 못합니다. 큐의 길이뿐 아니라 가장 오래 기다린 항목의 나이도 보아야 합니다. 좌표 100개가 1초 치인지 10분 치인지에 따라 사용자가 보는 화면의 의미가 다릅니다.

asyncio.Queue(maxsize=4)는 큐 안에 기다리는 항목을 네 개로 제한합니다. 작업자가 get으로 가져간 한 항목은 그 길이에서 빠지지만, ACK를 기다리는 동안 여전히 메모리에 있습니다. 연결당 대기 4개와 처리 중 1개, 여기에 직렬화 버퍼와 소켓 버퍼가 따로 있습니다. 따라서 큐 길이 4를 전체 메모리 4항목 보장이라고 부르지 않습니다. 항목의 크기도 제한해야 바이트 단위 예산을 세울 수 있습니다.

다음 표에서 발행자는 생산자, 화면을 갱신하는 작업자는 소비자입니다.

호출 의미 흔한 오해
await queue.put(item) 빈칸이 생길 때까지 기다려 넣는다 모든 구독자의 처리가 끝났다
queue.put_nowait(item) 즉시 넣거나 QueueFull을 낸다 가득 차도 조용히 기다린다
await queue.get() 대기 항목 하나의 소유권을 가져온다 그 항목의 업무가 성공했다
queue.task_done() 가져간 작업의 정리를 기록한다 원격 데이터가 영구 저장됐다

이벤트 루프는 await로 제어권을 내놓는 동안 다른 작업을 진행합니다. await 없이 무한 반복하는 함수는 다른 연결도 멈춥니다. async def라는 선언 자체가 함수를 병렬 CPU 작업으로 바꾸지는 않습니다. 또한 asyncio.Queue의 maxsize=0은 영 칸이 아니라 무제한입니다. 첫 단계에서 양의 정수만 받도록 하는 이유입니다. bool은 Python에서 int의 하위 타입이지만 이 API의 용량으로는 거부합니다.

가득 찬 큐에서 기다리는 put을 취소하면 해당 발행도 종료되어야 합니다. 취소를 잡아서 성공처럼 반환하면 호출자는 들어가지 않은 이벤트를 보냈다고 믿을 수 있습니다. CancelledError를 무심코 삼키지 말고, 기존 항목이 그대로 남는지와 취소된 항목이 나중에 나타나지 않는지를 함께 확인합니다.

현장에서 만나는 모습

라이브 자막은 생성기가 잠시 빨라질 수 있어 작은 큐가 순간적인 차이를 흡수합니다. 그러나 모바일 화면이 계속 느리다면 어느 시점에 대기·생략·연결 종료 중 하나를 선택해야 합니다. 주문 처리는 오래된 주문을 버릴 수 없으므로 화면 좌표와 같은 정책을 쓰지 않습니다. 큐의 구현을 고르기 전에 데이터의 의미를 묻는 것이 먼저입니다.

이 수업의 ACK 보류는 구독자의 업무 처리가 느린 상황입니다. 커널 송신 버퍼 포화나 인터넷 패킷 손실을 직접 측정한 실험은 아닙니다. 측정한 계층을 분명히 해야 큐 길이를 바꾸고 네트워크 장애까지 해결했다고 착각하지 않습니다.

다음 확인에서 할 것

빈 용량 2 큐에 A와 B를 넣고 작업자가 A를 가져갔다고 적어 보세요. qsize는 1이지만 task_done을 아직 호출하지 않았으므로 미완료 작업은 둘입니다. C를 넣으면 qsize는 다시 2이고, D를 넣으려는 생산자는 기다립니다. 작업자가 B를 가져가는 순간 빈칸이 생겨 D가 들어갈 수 있습니다. A의 업무 완료와 다음 항목의 큐 삽입 가능 시점은 같은 사건이 아닙니다.

여기서 A가 영원히 ACK되지 않으면 작업자는 B를 가져가지 못합니다. 큐에는 B와 C가 남고 생산자는 D에서 멈춥니다. 이 연결의 큐만 보면 정상적인 역압력입니다. 그러나 그 생산자가 다른 모든 구독자를 담당한다면 전체 생중계의 정지로 번집니다. 같은 도구라도 소유 범위와 기다리는 위치에 따라 장애 반경이 달라집니다. 함수 하나만 보지 말고 누가 그 함수를 기다리는지 화살표로 그려 보세요.

큐 상한, 처리 중 항목, 취소된 put의 의미를 퀴즈로 구분합니다. 위 예제에서 A의 ACK가 돌아오는 경우도 추적해 막힌 생산자가 어떤 순서로 다시 진행하는지 비교하세요. 마지막 모듈의 통합 실습에서 make_queue와 offer_wait를 직접 구현한 뒤 같은 큐에 세 가지 정책을 적용합니다.