Queue의 감각
이용률 70%와 95%는 뭐가 그렇게 다른가?
카페 줄이 갑자기 길어질 때
창구가 5개인 카페를 상상해 보세요. 바리스타 한 명이 음료 하나를 만드는 데 50초가 걸립니다.
손님이 적당히 오면 줄이 거의 안 생깁니다. 창구가 대략 70%만 차 있을 때예요.
그런데 창구를 95%까지 채워 돌리면 어떻게 될까요? 손님은 고작 조금 더 늘었을 뿐인데, 줄은 훨씬 길어집니다. 누군가 주문만 조금 헷갈려도 뒤에 선 사람들 전체가 밀립니다.
이용률이 선형으로 늘어도, 기다리는 시간은 선형으로 늘지 않습니다. 어느 지점부터 수직에 가깝게 튀어요. 이 랩은 그 무릎을 눈으로 확인합니다.
| 카페 | 서버 |
|---|---|
| 창구가 차 있는 비율 | 이용률 ρ (rho) |
| 줄 서는 시간 | 큐 대기시간 |
| 주문이 조금만 느려짐 | 느린 쿼리 / GC / 잠금 |
| “효율적으로 꽉 채우자” | 여유 없는 용량 설계 |
직접 돌려보세요
아래 그래프의 가로축은 이용률, 세로축은 이론 대기시간입니다. 슬라이더를 오른쪽으로 밀수록 점이 곡선을 타고 올라갑니다.
먼저 70%에 두고, 그다음 95%로 옮겨 보세요. 도착률은 고작 조금 늘었는데 대기시간은 몇 배로 뜁니다.
왜 이렇게 되는지, 산수로
공식은 곱셈과 나눗셈입니다.
이용률 ρ = (초당 요청 수 × 처리 시간) ÷ 창구 수
창구 5개, 처리 50ms(0.05초)일 때:
| 이용률 | 도착률 | 대략의 대기 |
|---|---|---|
| 70% | 70/초 | 짧음 (십 밀리초 전후) |
| 95% | 95/초 | 훨씬 김 (수백 밀리초) |
도착률 차이는 약 1.36배뿐입니다. 그런데 대기시간은 열 배 근처로 벌어질 수 있어요.
왜일까요? 창구가 거의 다 차 있으면, 잠깐의 흔들림을 흡수할 빈자리가 없습니다. 느린 요청 하나가 들어오면 뒤에 선 요청이 줄줄이 기다립니다. 여유 30%는 “노는 창구”가 아니라 충격 흡수 장치입니다.
흔한 오해
“창구를 95%까지 쓰는 게 효율적 아닌가요?”
회계 장부에는 그렇게 보일 수 있습니다. 자원 사용률이 높으니까요. 하지만 사용자 체감은 반대입니다. 같은 하드웨어에서 이용률만 올리면 p99(느린 쪽 1%의 응답시간)가 먼저 나빠집니다.
“평균 응답시간은 아직 괜찮은데요?”
평균은 빠른 요청에 가려질 수 있습니다. 줄이 생기기 시작하면 느린 꼬리(p99)부터 먼저 튀어요. 모니터에 평균만 보면 “괜찮았는데 갑자기 죽었다”처럼 느껴집니다.
“그럼 항상 50%만 쓰란 말인가요?”
목표는 고정 숫자가 아닙니다. 스파이크가 거의 없는 배치 작업과, 트래픽이 출렁이는 사용자 API는 여유가 달라야 합니다. 다만 대화형 API에서 여유 없이 95%를 상시 목표로 잡는 설계는 자주 실패합니다. 70%는 그 감각을 익히기 위한 출발점이에요.
챌린지
이제 피크 트래픽에서도 그 여유를 남겨 보세요. 풀만 최대로 올리는 해법은 일부러 통과하지 못하게 막아 두었습니다.
🎯 챌린지 — 피크에서도 70% 여유를 남겨라
평소 140/초이던 트래픽이 피크에 196/초까지 올라갑니다. 40초를 버티세요. 단, DB 예산 때문에 커넥션은 10개까지만 쓸 수 있습니다. 창구를 꽉 채워 돌리는 "효율"로는 통과할 수 없습니다.
확인 문제
⏱ 30초 리마인드
- 대기시간은 이용률에 비례하지 않는다. 100%에 가까워질수록 수직으로 튄다.
- 70%와 95%의 도착률 차이는 작아도, 대기시간 차이는 수십 배가 될 수 있다.
- 여유 용량은 낭비처럼 보여도 스파이크와 느린 요청을 흡수하는 완충이다.
- 평균만 보지 말고 p99와 큐 길이를 함께 본다.
- 풀을 더 못 늘리면 처리 시간을 줄여 ρ를 낮춘다.
현실에서는
Generic pattern은 단순합니다. 지속 가능한 이용률 목표를 정하고, 피크 λ와 측정된 S로 필요한 동시성을 역산합니다.
필요 동시성 ≈ 피크 RPS × 처리시간(초) ÷ 목표이용률
예: 196 × 0.05 ÷ 0.7 ≈ 14
상한이 10이라면 14를 만들 수 없습니다. 그때는 서버를 더 사는 이야기 전에 S를 줄일 지점(인덱스, 캐시, 불필요한 락, N+1)을 찾습니다.
구현 위치만 달라집니다.
- TypeScript / Node.js: 커넥션 풀 크기,
Promise동시성 제한, HTTP keep-alive 한도 - Go: worker pool,
semaphore, DBSetMaxOpenConns - JVM: HikariCP
maximum-pool-size, 스레드 풀 큐 길이 - Kubernetes: Pod를 늘려도 뒤의 DB·외부 API 이용률이 같이 올라갑니다. HPA만으로 ρ가 해결되지 않습니다.
- AWS / Serverless: Lambda concurrency가 RDS connection을 한꺼번에 밀 수 있습니다. 앞 단의 concurrency와 뒤 단의 pool을 같은 곱셈으로 맞춰야 합니다.
이 수치는 browser model입니다. 실제 서비스에서는 측정된 처리시간·피크 트래픽·타임아웃으로 다시 검증하세요.
다음 랩
이용률이 높아질 때 느린 꼬리가 먼저 나빠진다는 감각이 생겼을 겁니다. 다음은 평균은 멀쩡한데 사용자만 느리다고 느끼는 이유 — p50과 p99를 나란히 비교합니다.