채널톡 벌크액션 서버가 Spike 트래픽에 흔들리지 않도록 Worker 스케일링 알고리즘을 만든 과정이다. 한 번의 요청으로 수백에서 수백만 건을 처리해야 하는 서버라 Long Lived Job이 많으면 모든 Worker가 바빠져 자원이 고갈되는데, 기존에는 Pod 스케일링이나 수동 조정으로 대응해야 했다. 여러 지표 후보를 실험한 끝에 Queue Depth를 현재 Worker 수로 나눈 PWQD를 고르고, TCP AIMD에서 영감을 받은 뒤 최종적으로 지수적 Scale Up과 Time Decay 기반 Scale Down을 결합했다.
핵심 포인트- Queue Depth 단독은 Rate Limit 초기 탐색 구간에서 실제 부하와 무관하게 급등해, 이때 Worker를 늘리면 아무 일도 못 하는 Worker만 늘어난다.
- 실행 중인 Task 수는 스로틀링되는 Task도 잠깐 In-Flight를 거쳐 집계돼, Worker 수와 함께 선형적으로 증가만 할 뿐 실제 부하를 반영하지 못했다.
- 스로틀 비율은 초기 탐색 구간에서 치솟았다 급락하는 변동이 심해 안정적 기준으로 쓰기 어려웠다.
- PWQD는 같은 Queue Depth라도 Worker가 많은 쪽을 여유로운 상황으로 표현해, 절대 임계치를 어디에 둘지의 문제를 상대값으로 전환하며 자연스럽게 해결했다.
- 지수적 증감만 쓰면 Scale Down 과정에서 Oscillation이 크게 일어나, 감소는 보수적으로 가야 했다.
상세 정리- 서비스 맥락: 채널톡은 약 20만 고객사가 쓰는 B2B SaaS이고, 고객 데이터 수십만 건 업로드나 특정 조건 고객 대상 프로모션 발송처럼 한 요청이 대량 처리로 이어지는 벌크액션을 다룬다.
- 기존 구조가 이미 갖춘 것: 똑똑한 backoff 방식, 스로틀된 Job을 Non-ready Queue로 보내 HOL Blocking을 최소화하는 처리, 정해진 TPS를 준수하며 시간 내에 처리하는 메커니즘.
- 남은 문제: Job Queue에 Long Lived Job이 많으면 모든 Worker가 Busy해져 여유 자원이 없어진다. Worker 수나 Pod 스케일링 조정은 상황 의존적이라 지속적인 조치가 될 수 없었다.
- 요청 특성: 클라이언트가 보내는 Job 하나가 여러 처리를 묶는 형태인데 이는 I/O 비용 최적화를 위한 의도된 설계다. 그래서 Job 하나의 실행이 수 초까지 지연될 수 있다.
- 이 경우 TPS 제한은 준수하지만 Non-ready Queue로 적재되지 않아 똑똑한 스케줄링의 이점을 살리지 못하고, 느린 처리 속도가 꼬리 지연을 만들었다. 이런 지연은 CPU나 메모리 지표로 표현되기 어려웠다.
- 지표 후보 1 Queue Depth: 임계치를 넘으면 Worker를 2배로 늘리는 단순 규칙을 시도했다. 문제는 새 Job이 큐에 들어오면 실행 전까지 Rate Limit 기준을 알 수 없어 최소 한 번은 실행을 시도한 뒤 스로틀된다는 점이었다.
- 지표 후보 2 실행 중 Task 수: Worker 활용률을 재려 했지만 스로틀되는 Task도 In-Flight로 잡혀 실제 부하를 반영하지 못했다.
- 지표 후보 3 대기 시간 계열: Queue Depth를 시간 관점으로 해석하는 데는 유용했지만 큐 안에 서로 다른 벌크액션의 Job이 섞여 있어 실제로 얼마나 걸릴지는 처리해보기 전까지 알 수 없어 선행 지표로 한계가 있었다.
- 보조 지표 검토: 스로틀 비율이 높은 구간에서는 Worker를 늘려도 처리량이 늘지 않으니 Scale Up을 억제하는 보조 지표로 쓸 수 있을까 조사했지만, 변동이 너무 심해 포기했다.
- PWQD 채택 이유: 사용자가 어떤 패턴으로 Job을 보내는지에 최대한 의존하지 않는 값이라, 특정 서비스의 예상 latency에 맞춰 미리 스케일링을 대비하는 고민을 하지 않아도 된다는 점이 큰 이점이었다.
- 연속 초과 조건: 임계치를 연속으로 초과할 때만 Scale Up을 트리거해 순간적인 Spike에 과잉 반응하지 않게 했다. Rate Limit 초기 탐색 구간의 Queue Depth 급등도 Job이 재스케줄링되며 비율이 빠르게 안정돼 불필요한 Scale Up을 막는다.
- 첫 알고리즘: PWQD가 임계치를 넘으면 바로 지수적으로 Worker를 올리는 방식이다. 직관적이고 높은 트래픽에도 잘 대응했지만 임계치 아래로 내려갈 때 Scale Down에서 진폭이 커지는 현상이 지속됐다.
- 두 번째 시도: TCP AIMD에서 영감을 받아 Scale Down에 선형적 감소를 적용했고 실제로 진폭이 줄었다.
- 그럼에도 최종이 아니었던 이유: ssthresh 시점부터 선형 증감을 적용하는데, 초기 벌크액션에는 즉각 반응하지만 비슷하거나 더 큰 요청이 이어서 들어오는 경우 대응이 부족했다.
- 최종 설계: 즉각적인 대응과 보수적인 감소라는 상반된 요구를 절충하기 위해, PWQD가 지속적으로 높은 부하를 보일 때 Scale Up하고 Scale Down은 Time Decay로 천천히 진행하도록 했다. 느린 감소로 추가 요청에 대비하면서 빠른 증가와 Oscillation 회피를 함께 얻었다.
- 성과: Worker 수가 부하에 따라 자동 스케일링되면서 개발자의 수동 개입이 줄었고 Spike에도 Oscillation 없이 안정적인 스케일링이 가능해졌다.
- 남은 한계: 모든 Job이 하나의 Queue를 공유하는 구조에서는 대규모 벌크액션이 Queue를 점유해 다른 요청 처리가 밀리는 문제가 남아, 다음 글에서 논리적 파티셔닝으로 다룬다고 예고한다.
왜 읽나큐 기반 워커 풀의 스케일링 기준을 정해야 하는 백엔드 엔지니어에게, 흔히 쓰는 지표들이 왜 부적합했는지와 상대 지표 설계·증감 알고리즘 선택의 근거를 실패 순서대로 보여준다.