본문 바로가기

10. SLO와 capacity planning — 약속을 숫자로 쓰기

이 장의 핵심 질문
  • SLO(서비스 수준 목표)는 무엇을 약속하는 것이고, 왜 평균이 아니라 tail(꼬리)로 써야 하는가?
  • goodput·saturation·headroom은 각각 무엇이며, 어떻게 capacity 계약이 되는가?
  • "이 시스템은 초당 N개를 처리한다"는 주장은 왜 그 자체로 불완전한가?

앞 장과의 연결 — 이 묶음(②)에서 PD 분리(07)·KV(08)·세 제어 루프(09)가 모두 "SLO를 지키며 처리량을 낸다"는 목표를 향했다. 이 장은 그 SLO를 정의하고, goodput·tail·saturation·headroom으로 capacity를 계약으로 쓰는 법을 다룬다. 07장에서 예고한 goodput이 여기서 정식 정의된다. 이 장으로 두 번째 묶음을 닫는다.

먼저: SLO가 뭔가요?

SLO(Service Level Objective, 서비스 수준 목표) 는 서비스가 지키기로 한 측정 가능한 품질 목표다. "빠르게 처리한다" 같은 막연한 말이 아니라, "어떤 지표를, 어떤 값 이하로, 어느 비율의 요청에서 지킨다"는 구체적 약속이다.

LLM 서빙에서 흔한 SLO 지표(선수 과정 ②의 추론 지표):

  • TTFT(Time To First Token) — 요청 후 첫 토큰까지의 시간. 사용자가 "반응이 시작됐다"고 느끼는 순간.
  • TPOT/TBT(Time Per Output Token / Time Between Tokens) — 토큰 하나하나가 나오는 간격. 생성이 "매끄러운가 끊기는가".

SLO는 이 지표에 목표값과 비율을 붙여 쓴다. 예: "TTFT를 500ms 이하로, 요청의 99%에서". 이 "99%"가 다음 절의 tail 이야기다.

왜 평균이 아니라 tail인가

초보적 실수는 SLO를 평균(mean) 으로 쓰는 것이다. "평균 TTFT 300ms"는 위험하게 오해를 부른다.

일상 비유: 어떤 식당의 "평균 대기 20분"이라 해도, 10명 중 1명이 2시간을 기다린다면 그 식당은 실패다. 사용자는 평균이 아니라 자기 경험을 겪고, 최악의 경험이 만족을 좌우한다.

그래서 SLO는 백분위(percentile), 특히 tail(꼬리) 로 쓴다.

  • p50(중앙값) — 절반이 이보다 빠름. "보통 경험".
  • p99(99번째 백분위) — 99%가 이보다 빠름, 즉 최악 1%의 경계. "나쁜 경험의 상한".

tail latency(꼬리 지연)는 이 p99·p99.9 같은 최악에 가까운 응답 시간을 말한다. 분산 시스템에서 tail은 평균보다 훨씬 나쁘게 벌어지기 쉽다: 한 요청이 여러 단계(라우팅·P·전송·D)를 거치면(07·09장), 각 단계의 지연이 겹쳐 최악의 경우가 증폭된다.

📌 핵심 — SLO는 평균이 아니라 tail(p99 등)로 써야 한다. 평균이 좋아도 tail이 나쁘면 상당수 사용자가 나쁜 경험을 한다. 분산 다단계 경로일수록 tail이 평균에서 멀어지므로, "평균 latency" 주장은 그 자체로 불완전하다.

goodput — SLO를 붙인 처리량

07장에서 예고한 goodput(굿풋) 을 정식으로 정의한다.

  • throughput(처리량) — 단위 시간당 처리한 요청·토큰의 총량. "얼마나 많이".
  • goodput(유효 처리량) — 그중 SLO를 만족한 요청·토큰만 센 처리량. "약속을 지키며 얼마나 많이".

왜 이 구분이 결정적인가: SLO를 어기며 낸 처리량은 사용자에게 늦어서 쓸모없는 응답일 수 있다. 예를 들어 부하를 무리하게 올려 throughput은 높지만 p99 TTFT가 목표를 크게 넘으면, 그 "높은 throughput"의 상당수는 SLO 위반 요청이라 goodput은 오히려 낮다.

flowchart LR
    LOAD["부하 증가 →"] --> TP["throughput<br/>계속 오르는 듯"]
    LOAD --> SLO{"SLO 만족?<br/>(p99 TTFT ≤ 목표)"}
    SLO -->|"만족"| GOOD["goodput에 포함 ✅"]
    SLO -->|"위반"| BAD["goodput에서 제외 ❌<br/>(늦은 응답)"]
    GOOD --> GP["goodput<br/>= SLO 지킨 처리량"]
    BAD -.->|"throughput엔 있지만<br/>goodput엔 없음"| GP

    classDef req fill:#fff3c4,stroke:#d4a017,color:#000
    classDef metric fill:#d1f0d1,stroke:#16a34a,color:#000
    classDef bad fill:#ffd6d6,stroke:#dc2626,color:#000
    class LOAD req
    class TP,GP,GOOD metric
    class BAD bad
⚠️ 경계 혼동 주의 — throughput ≠ goodput — "우리 시스템은 초당 N개를 처리한다"는 주장은 SLO 없이는 불완전하다. SLO를 어기며 낸 N이라면 실제 유효 처리(goodput)는 그보다 훨씬 적다. 성능 주장을 볼 때 항상 "그 처리량이 어떤 SLO를 지키며 나온 것인가"를 물어라. 이 묶음의 여러 기법(PD 분리·라우팅)이 최적화한 것은 throughput이 아니라 goodput이었다.

saturation — 부하가 임계점을 넘으면

saturation(포화) 은 시스템이 자기 처리 상한에 도달해, 부하를 더 넣어도 처리량이 늘지 않고 대기(큐)만 폭발하는 상태다.

핵심 성질: saturation 근처에서 tail latency가 비선형적으로 폭발한다. 부하가 상한의 70%일 때와 95%일 때, tail은 몇 배가 아니라 수십 배로 벌어질 수 있다. 큐 이론의 잘 알려진 성질이다(이용률이 100%에 가까워질수록 대기 시간이 급격히 발산).

일상 비유: 고속도로가 용량의 80%까지는 잘 흐르다가, 90%를 넘는 순간 정체가 폭발한다. 마지막 10%가 문제를 몇 배로 키운다.

이래서 08장의 backpressure가 필요하다: saturation에 가까워지면 유입을 막아, 큐·tail이 폭발하기 전에 시스템을 안정 구간에 유지한다. backpressure는 "일부 요청을 막아 나머지의 SLO를 지키는" 장치다.

📌 핵심 — saturation 근처에서 tail은 비선형적으로 폭발한다. 그래서 시스템을 이용률 100%로 몰아넣으면 안 된다. 최고 throughput 지점과 최고 goodput 지점은 다르다: throughput은 포화 근처에서 최대지만, 그 지점에서 tail이 터져 SLO를 어기므로 goodput은 그 전에 최대가 된다.

headroom — 여유를 남겨 계약을 지킨다

headroom(여유) 은 현재 부하와 saturation 사이에 남겨 둔 여유 용량이다. 시스템을 상한 가까이가 아니라, saturation에서 일정 거리를 두고 운영하는 것이다.

왜 필요한가:

  • 부하 변동 흡수 — 실제 부하는 튄다. 여유가 없으면 순간 급증이 곧바로 saturation·tail 폭발로 이어진다.
  • 오토스케일링의 느림 보상 — 09장에서 오토스케일링은 느리다(수십 초~분). 스케일링이 도착하기까지 headroom이 버텨 줘야 한다. headroom이 없으면 스케일링이 오기 전에 SLO가 무너진다.
  • tail 보호 — saturation에서 거리를 둘수록 tail이 안정적이다.
⚖️ 절충 — headroom(안정) vs 이용률(비용) — headroom을 크게 남기면 tail·SLO가 안정적이지만, 비싼 GPU가 놀아 비용 효율(이용률)이 낮아진다. headroom을 줄이면 이용률은 오르지만 tail 폭발·SLO 위반 위험이 커진다. "이용률을 100%로 올리자"는 비용 관점에선 매력적이지만 goodput·SLO 관점에선 위험하다. 적정 headroom은 부하 변동성·SLO 엄격도·스케일링 속도에 달렸다(조건 의존).

capacity 계약 — 이 모두를 하나로

이제 capacity planning을 계약으로 쓸 수 있다. "이 시스템은 초당 N개를 처리한다"가 아니라, 다음을 모두 명시한 문장이다:

capacity 계약(예시 형식) — "이 배포는 [워크로드: 프롬프트/생성 길이 분포] 하에서, [SLO: p99 TTFT ≤ Xms, p99 TPOT ≤ Yms] 를 지키며, [goodput: 초당 Z 요청] 까지 처리할 수 있고, [headroom: 이용률 W% 이하] 로 운영하며, 초과 시 [backpressure/오토스케일링] 로 대응한다."

이 계약에는 이 묶음의 모든 개념이 들어간다: 워크로드 조건, tail SLO, goodput, headroom, backpressure·스케일링. 분모(무엇에 대한 Z인가)와 조건(어떤 워크로드·SLO인가)이 빠지면 계약이 아니다.

🔬 증거 읽기 — capacity 주장 검증 (이 묶음의 종합) — "이 시스템은 초당 N개(또는 M 토큰/s)를 처리한다"는 주장을 만나면 반드시 붙여 물어라:
  • (a) 어떤 SLO(어떤 지표, tail 몇 %, 목표값)를 지키며인가? — 없으면 throughput일 뿐 goodput이 아니다.
  • (b) 어떤 워크로드(프롬프트/생성 길이, 부하 패턴)인가?
  • (c) 어떤 이용률/headroom에서인가? — saturation 근처 수치는 실운영에서 재현 불가.
  • (d) tail을 보고했나, 평균만 보고했나?

이 조건이 다르면 두 capacity 수치는 직접 비교 불가다(15장에서 본격화). SLO·워크로드·headroom이 다른 "초당 N개"를 나란히 순위 매기는 것은 무의미하다.

두 번째 묶음(②) 정리 — 배포에 서비스 경로가 생겼다

flowchart LR
    D1["D1<br/>병렬화된 엔진<br/>(①묶음 결과)"] -->|"07: PD 분리<br/>간섭 제거, KV 페이로드화"| S1["phase 분리"]
    S1 -->|"08: KV 전송·계층화<br/>ownership/lifetime/tier/backpressure"| S2["KV 관리"]
    S2 -->|"09: 세 제어 루프<br/>라우팅/LB/오토스케일링"| S3["요청·자원 조율"]
    S3 -->|"10: SLO·goodput·<br/>saturation·headroom"| D_SVC["서비스형 배포<br/>capacity 계약 보유"]

    classDef base fill:#cfe2ff,stroke:#2563eb,color:#000
    classDef step fill:#c4f1f4,stroke:#0891b2,color:#000
    classDef done fill:#d1f0d1,stroke:#16a34a,color:#000
    class D1 base
    class S1,S2,S3 step
    class D_SVC done

이제 배포는 "SLO 계약을 지키며 요청을 처리하는 서비스"가 됐다. 하지만 아직 이 서비스를 실제로 무엇으로 구현하는가 — 어떤 추론 엔진, 어떤 서빙 계층, 어떤 하드웨어 — 를 다루지 않았다. 다음 묶음(③·11–14)이 그 구현 층을, 추천이 아니라 안정적 비교 축으로 다룬다.

이 묶음에서 확립한 재사용 어휘: PD 분리(P=producer/D=consumer), KV의 ownership/lifetime/tier, backpressure, 세 제어 루프(라우팅/LB/오토스케일링)와 시간축, goodput vs throughput, tail(p99), saturation, headroom, capacity 계약.

이 장에서 배운 것

  • SLO는 지표(TTFT·TPOT 등)에 목표값과 비율(tail, p99 등) 을 붙인 측정 가능한 약속이다. 평균이 아니라 tail로 써야 한다.
  • 분산 다단계 경로일수록 tail이 평균에서 멀어진다. "평균 latency" 주장은 불완전하다.
  • goodput은 SLO를 지킨 처리량이다. throughput과 다르며, 이 묶음의 기법들이 최적화한 대상이다.
  • saturation 근처에서 tail이 비선형 폭발한다. 그래서 최고 throughput 지점과 최고 goodput 지점은 다르다. backpressure가 이 폭발을 막는다.
  • headroom은 saturation과의 여유다. 부하 변동·느린 스케일링·tail을 흡수한다. headroom(안정) vs 이용률(비용)은 절충이다.
  • capacity 계약은 워크로드·tail SLO·goodput·headroom·대응을 모두 명시한 문장이다. 분모와 조건이 없으면 계약이 아니다.

🔎 이 장의 '지지하지 않는 결론' + 'unknown으로 남겨야 할 것'

  • 지지하지 않는 결론 — "throughput이 높으면 좋은 시스템"이라거나 "이용률을 100%로 올리는 게 효율적"이라는 것은 지지되지 않는다. SLO를 어긴 throughput은 goodput이 아니고, 포화 운영은 tail을 터뜨린다.
  • unknown으로 남길 것 — 특정 배포의 실제 goodput·tail 분포·운영 headroom·SLO 목표값은 그 배포가 공개하지 않으면 unknown. 벤더의 "초당 N개"는 SLO·워크로드·이용률 조건 없이는 옮기지 않는다.

✍️ 확인 문제

  1. 왜 SLO를 "평균 TTFT 300ms"가 아니라 "p99 TTFT ≤ 500ms"처럼 tail로 써야 하는가? 분산 다단계 경로(07·09장)가 tail을 평균에서 멀어지게 하는 이유와 함께 설명하라.
  1. (비교 가능성 유형 — 이 묶음의 종합) 시스템 A는 "초당 1000 요청", 시스템 B는 "초당 800 요청"이라 주장한다. 이 두 수치만으로 A가 낫다고 결론 낼 수 있는가? 반드시 붙여 확인해야 할 조건(SLO·tail·워크로드·headroom)을 들고, 어떤 경우 B의 goodput이 오히려 클 수 있는지 예를 들어라.
  1. (절충 유형) 비용을 줄이려 이용률을 90%→98%로 올리자는 제안이 있다. saturation·tail·headroom·오토스케일링의 느림을 근거로, 이 제안이 goodput·SLO에 어떤 위험을 만드는지 설명하라.
이것으로 두 번째 묶음(②·서비스 경로·SLO)을 마친다. 다음 묶음(③)에서는 이 서비스를 구현하는 층 — 추론 엔진(vLLM·SGLang·TensorRT-LLM), 서빙 계층(Dynamo·llm-d), 공개 배포 사례, 하드웨어 — 을 추천이 아니라 안정적 비교 축으로 다룬다.
다음11. vLLM·SGLang·TensorRT-LLM 비교 — 엔진을 안정적 개념 축으로 읽기