느린 구독자가 생중계를 멈췄다 · 생중계가 밀리는 이유 · 이론
생중계가 밀리는 이유
한 줄 요약
큐는 처리할 일을 잠시 보관하지만, 느린 소비자를 빠르게 만들지는 않습니다.
왜 이게 필요했나
고양이 구조대의 위치를 생중계한다고 상상해 봅시다. 구독자 아홉 명은 새 좌표를 바로 화면에 그리지만 한 명은 화면 처리를 멈췄습니다. 서버가 모든 구독자의 완료를 차례로 기다린다면, 멈춘 한 사람이 나머지 아홉 명의 지도까지 정지시킵니다. 반대로 일단 모두 큐에 넣고 다음 일을 하면 발행자는 편해지지만, 느린 사람의 큐는 계속 커집니다. 기다림을 없앤 것이 아니라 메모리로 옮긴 것입니다.
이 코스는 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를 직접 구현한 뒤 같은 큐에 세 가지 정책을 적용합니다.