LabHub
배우기 러닝패스 코스

キューと非同期API

キューは時間を買う道具だ

LabHub 에서 이어서 보기

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

한 줄 요약

큐는 처리 속도를 높이지 않습니다. 요청을 받는 속도와 처리하는 속도를 분리해서, 순간의 폭주를 시간으로 흡수합니다.

概念マップ: 지연을 받아들이고 가용성을 삽니다.・평균 유입이 평균 처리 능력보다 큰 경우・비선형으로 폭발・큐는 조용한 블랙홀이 됩니다.

왜 이게 필요했나

이미지 업로드 API 가 있다고 합시다. 원본을 저장하고, 썸네일 세 개를 만들고, 메타데이터를 추출하고, 검색 색인에 넣습니다. 전부 동기로 하면 응답이 4초 걸립니다. 평소에는 견딜 만합니다. 그런데 프로모션 날 업로드가 열 배로 늘면, 4초짜리 요청이 열 배 쌓이면서 워커 스레드가 전부 묶이고 헬스체크까지 실패합니다.

큐를 넣으면 API 는 원본만 저장하고 "접수했다" 고 답합니다. 200ms 입니다. 나머지는 워커가 자기 속도로 처리합니다. 폭주가 와도 API 는 계속 200ms 로 답하고, 큐 깊이만 늘어납니다. 늘어난 깊이는 처리 지연이 됩니다 — 4초 걸리던 것이 3분 걸릴 수 있습니다. 대신 시스템은 죽지 않습니다.

이 교환이 큐의 전부입니다. 지연을 받아들이고 가용성을 삽니다.

안정 조건은 부등식 하나다

큐를 쓸 만한 조건은 셋입니다. 첫째, 작업이 즉시 끝나지 않아도 되는가. 둘째, 요청 속도가 처리 속도보다 순간적으로 빠를 수 있는가. 셋째, 실패했을 때 나중에 다시 해도 되는가. 셋 다 예이면 큐가 맞습니다.

반대로 큐를 넣지 말아야 할 경우도 분명합니다. 사용자가 결과를 즉시 봐야 하는 조회, 그리고 평균 유입이 평균 처리 능력보다 큰 경우 입니다. 후자를 오해하는 사람이 많습니다. 큐는 버스트를 흡수하지만 만성 과부하는 해결하지 못합니다.

λ = 초당 유입 건수      μ = 워커 하나의 초당 처리 건수 × 워커 수

λ < μ  →  큐 깊이가 0 근처로 돌아온다 (안정)
λ = μ  →  깊이가 무작위로 떠돈다 (경계 — 운영하면 안 되는 지점)
λ > μ  →  깊이가 선형으로 자란다 (터진다)

수치로 보면 명확합니다. μ = 100/s 이고 λ 가 평소 60/s 인데 30분 동안 90/s 로 오른다면, 이 구간에서 초당 없어지는 여유는 10건입니다. 30분이면 큐에 남는 것은 없고 잘 흡수됩니다. 그런데 λ 가 110/s 로 오르면 초당 10건씩 쌓여 30분에 18,000건이 됩니다. 이 상태가 끝나지 않으면 메모리나 디스크를 다 먹고 터집니다.

λ > μ 가 지속되면 필요한 것은 큐가 아니라 워커 증설이거나 유입 제한입니다.

지연은 이용률에 비례하지 않는다

여기서 사람들이 자주 놓치는 것이 있습니다. 대기 시간은 이용률(ρ = λ/μ)에 비례해 늘지 않고 비선형으로 폭발 합니다. 대략적인 감각으로 이렇습니다.

이용률 ρ 대기 시간 배수(1/(1−ρ))
0.5 2배 여유롭다
0.8 5배 슬슬 티가 난다
0.9 10배 조금만 더 오르면 급격히 나빠진다
0.95 20배 운영하기 어렵다
0.99 100배 사실상 장애

그래서 워커 용량을 "평균 유입에 딱 맞게" 잡으면 안 됩니다. 이용률 70~80% 를 목표로 잡고, 그 위는 자동 확장이나 유입 제한이 받아 냅니다.

현장에서 만나는 모습

큐를 넣고 나면 새로운 운영 지표가 생깁니다. 큐 깊이와 소비자 지연(consumer lag) 입니다. 이 둘을 대시보드에 올리지 않으면 큐는 조용한 블랙홀이 됩니다. 사용자는 "업로드했는데 안 나와요" 라고 신고하고, API 대시보드는 전부 초록색입니다. 문제는 API 뒤에 쌓여 있습니다.

깊이만 보는 것으로는 부족합니다. 깊이 1,000 이 심각한지 아닌지는 처리율에 따라 다릅니다. 깊이 ÷ 처리율 = 소진 시간 을 함께 봐야 판단이 됩니다. 초당 100건을 처리하면 10초짜리이고, 초당 1건이면 17분짜리입니다. 경보도 이 값으로 겁니다.

경보 하나 더. 깊이가 0 에서 안 움직이는 것도 이상 신호입니다. 소비자가 전부 죽어 아무도 꺼내지 않으면 유입만 쌓여야 하는데, 생산자까지 죽었으면 깊이가 0 에서 평평합니다. 처리 완료 건수가 0 인 채로 깊이가 0 이면 파이프라인 전체가 멈춘 것입니다.

그리고 큐를 넣으면 사용자 경험 설계가 함께 필요해집니다. "접수됨" 상태를 어떻게 보여 줄지, 얼마나 기다리면 되는지, 실패하면 어떻게 알릴지. 이걸 정하지 않고 큐만 넣으면 사용자는 그냥 사라진 요청을 봅니다. 최소한 세 가지는 정해야 합니다.

큐를 고르기 전에 답해야 할 네 가지

도구를 고르는 것은 마지막입니다. 그 앞에 네 가지 성질을 먼저 정해야 하고, 이것을 정하지 않고 Redis 나 Kafka 를 고르면 나중에 전부 다시 만들게 됩니다.

전달 보장. 최소 한 번(at-least-once)이 기본입니다. 정확히 한 번은 큐가 주는 것이 아니라 소비자가 멱등하게 만들어 얻는 것 입니다. 처리한 작업 ID 를 기록해 두고 같은 것이 다시 오면 건너뜁니다.

순서. 전역 순서를 지키는 큐는 병렬 처리를 못 합니다. 실무에서는 대개 키 단위 순서면 충분합니다 — 같은 사용자의 작업만 순서대로면 되고, 다른 사용자와의 순서는 상관없습니다. 파티션 키를 사용자 ID 로 두면 이것이 얻어집니다.

재시도와 포기. 몇 번 재시도하고, 간격은 어떻게 늘리고, 언제 포기할지 정합니다. 지수 백오프에 지터를 섞지 않으면 실패한 작업들이 같은 시각에 몰려 재시도합니다.

죽은 편지 큐(DLQ). 재시도를 다 쓴 작업이 갈 곳입니다. DLQ 가 없으면 그 작업은 영원히 순환하거나 조용히 사라집니다. DLQ 에 쌓인 건수는 반드시 경보를 겁니다 — 여기 쌓인 것은 사람이 봐야 하는 것들입니다.

다음 퀴즈에서 확인할 것

이 모듈은 개념만 다룹니다. 다음 모듈부터 Redis 리스트와 스트림으로 큐를 직접 만들고, 순서·중복·유실이 어디서 생기는지 하나씩 재현합니다.