핵심 한 줄

baseline 10 VU에서 190 rps가 나왔다. 처리량 같지만 아니다 — 190 = 10 VU ÷ 53ms(Little’s law), 지연에 묶인 운영점이지 시스템 용량이 아니다. 게다가 시딩 풀은 30초 만에 말랐다. 각 취소가 결제 1건을 소진하기 때문. 숫자 하나를 제대로 읽는 게 실측의 절반이다.

시리즈 — 부하가 드러낸 설계

1막 · 실측으로 잡고 증명하다 00 · 01 · 02 · 03 · 04 · 05 · 06 · 07 · 08 · 09 · 10 · 11 2막 · 관측을 코드로, 코드를 재측정으로 12 · 13 · 14 · 15


1. “처리량 190이네”

첫 baseline. 10 VU로 3분. 결과는 34,194건 처리, 약 190 rps. 지연은 med 53ms, p99 65ms. 숫자만 보면 “오, 190 처리하네” 싶다.

그런데 이 190이 무엇의 190인가를 물어야 한다. 시스템의 최대 처리량? 아니다.

2. Little’s law — 190은 계산된 값이다

닫힌 부하 모델(VU가 요청→응답→다음 요청을 반복)에서 처리량은 이렇게 정해진다.

처리량 = 동시성 ÷ 지연
       = 10 VU ÷ 0.053s
       ≈ 189 rps

190은 자원 한계가 아니라 지연에 묶인(latency-bound) 값이다. 10명이 각자 53ms짜리 요청을 쉼 없이 반복하니, 산술적으로 ~190이 나올 수밖에 없다. 시스템은 포화되지 않았다.

190은 용량이 아니라 "10 VU 운영점"

VU를 늘리면(포화 전까지) rps도 따라 오른다. 진짜 용량(knee)은 VU를 계속 올려 지연이 꺾이는 지점을 찾아야 나온다. baseline의 190은 그냥 **“10명이 붙었을 때의 지점”**일 뿐이다. 이걸 “우리 시스템은 190 TPS”라고 적으면 틀린 보고서가 된다.

3. 풀이 30초에 말랐다

baseline을 제대로 3분 돌리려는데, 처음엔 30초 만에 중단됐다.

test aborted: payment pool 소진 (5000건).

이 시스템의 취소는 결제 아이템을 전체 취소한다. 한 번 취소하면 그 결제는 COMPLETED가 되어 다시 취소할 수 없다. 즉 취소 1건 = 결제 1건 소진이다. 190 rps로 30초면 ~5,700건 — 5,000짜리 풀이 마르는 게 당연하다.

부하가 셀수록 풀이 빨리 마른다. 3분 baseline엔 최소 ~34,000건, ramp까지 보려면 수십만 건이 필요하다. 그래서 시딩을 SQL bulk INSERT로 바꿨다 — merchant는 HTTP로 몇 개 만들고, payment 10만 건은 payment_db에 직접 multi-row INSERT. 수만 건이 초 단위로 꽂힌다.

4. 190이 숨기는 것

한 가지가 더 걸렸다. 190은 10개 merchant에 분산된 부하의 집계다. merchant당으로 나누면 ~19 rps. 각 가맹점의 한도 row에는 초당 19번씩만 닿는다 — 경합이 거의 없다.

그런데 만약 트래픽이 한 가맹점에 쏠리면? 집계 190은 그대로여도, 그 한 row에는 초당 190번이 몰린다. 집계 숫자는 이 편중을 완전히 가린다.

이 질문이 7편의 씨앗이 됐다. “분산의 평균이 숨기는 최악의 경우는 무엇인가.”


숫자 하나(190)를 Little’s law로 분해하고, 풀 고갈의 원인을 취소의 도메인 특성에서 찾고, 집계가 숨기는 편중을 의심하는 것 — 이게 실측의 절반이다. 그리고 나머지 절반은, 그 baseline이 조용히 뱉은 7.63%라는 에러율에서 시작됐다.