-
SSE 기반 AI 스트리밍에서 성능을 제대로 측정하는 방법 (TTFT, Duration, Active Stream 설계)카테고리 없음 2026. 3. 26. 11:32728x90반응형
🚀 WebFlux SSE 스트리밍 성능 메트릭 완전 분석 (코드 기반)
이 글은 단순 개념 설명이 아니라, 실제 코드 기준으로
성능 메트릭이 어디서 어떻게 측정되고 왜 그렇게 설계되었는지를 하나씩 분해해서 설명합니다.
📌 핵심 코드
return Flux.defer(() -> { metrics.streamStarted.increment(); metrics.incActive(); Timer.Sample total = Timer.start(); final boolean[] ttftDone = {false}; Timer.Sample ttft = Timer.start(); return openRouterClient.chatCompletionStream(req) .doOnNext(chunk -> { if (!ttftDone[0]) { ttftDone[0] = true; ttft.stop(metrics.openRouterTtftTimer); } }) .doFinally(sig -> { total.stop(metrics.openRouterStreamTimer); metrics.decActive(); if (sig == SignalType.ON_COMPLETE) metrics.streamCompleted.increment(); else if (sig == SignalType.CANCEL) metrics.streamCancelled.increment(); else metrics.streamFailed.increment(); }); });
1️⃣ Flux.defer() — “측정 시작 시점”을 맞추는 핵심
문제
Flux는 lazy하다.
즉, 메서드 호출 ≠ 실제 실행해결
Flux.defer(() -> { ... })👉 실제 subscribe 시점에 실행됨
왜 중요할까?
잘못된 코드:
metrics.streamStarted.increment(); // ❌ 아직 실행 안됐는데 증가결과:
- 가짜 트래픽 증가
- 잘못된 duration
- 모니터링 왜곡
결론
👉 “실제 실행 시점 기준으로 측정”하기 위해 defer 사용
2️⃣ streamStarted — 전체 흐름의 기준점
metrics.streamStarted.increment();의미
- 실제 시작된 스트림 수
왜 필요할까?
이 값은 모든 지표의 기준이 된다.
예:
- started = 100
- completed = 92
- failed = 5
- cancelled = 3
👉 서비스 품질 한눈에 파악 가능
3️⃣ active stream — 현재 시스템 상태
metrics.incActive(); ... metrics.decActive();의미
현재 살아 있는 스트림 수 (Gauge)
왜 중요한가?
스트리밍은 “동시 연결 수”가 핵심이다.
예:
- active = 10 → 정상
- active = 200 → 과부하 가능성
이걸로 보는 것
- 현재 부하
- connection 점유
- 스트림 누수 여부
4️⃣ 전체 스트리밍 시간 (Duration)
Timer.Sample total = Timer.start(); ... total.stop(metrics.openRouterStreamTimer);의미
스트림 전체 수명
포함 범위
- 연결 시작
- 첫 응답 대기
- 전체 응답
- 종료
왜 필요한가?
👉 자원 점유 시간 측정
스트림이 길어지면:
- connection 점유 증가
- memory 사용 증가
- active 감소 안됨
5️⃣ TTFT (Time To First Token)
Timer.Sample ttft = Timer.start(); .doOnNext(chunk -> { if (!ttftDone[0]) { ttftDone[0] = true; ttft.stop(metrics.openRouterTtftTimer); } })의미
첫 응답까지 걸린 시간
왜 중요한가?
👉 사용자 체감 성능 핵심
예:
- TTFT 300ms → 빠름
- TTFT 4초 → 느림
왜 첫 chunk에서 측정?
스트리밍에서 “응답이 시작됐다”는 순간이 바로 첫 chunk 도착이다.
왜 한 번만 측정?
여러 번 측정하면:
- 값 왜곡
- 의미 없음
6️⃣ doFinally — 종료 관리의 핵심
.doFinally(sig -> {의미
스트림이 끝나는 모든 경우 처리
종류:
- COMPLETE
- CANCEL
- ERROR
왜 doFinally인가?
다른 연산자는 특정 케이스만 잡는다.
👉 doFinally는 “모든 종료”를 보장
7️⃣ 종료 시 duration 기록
total.stop(...)의미
전체 스트림 시간 확정
8️⃣ active 감소
metrics.decActive();중요 포인트
👉 반드시 모든 종료에서 실행돼야 함
안 그러면:
- active 누수
- 잘못된 모니터링
9️⃣ 종료 유형 분리 (핵심 설계)
if (sig == SignalType.ON_COMPLETE) metrics.streamCompleted.increment(); else if (sig == SignalType.CANCEL) metrics.streamCancelled.increment(); else metrics.streamFailed.increment();
왜 나누는가?
유형 의미 COMPLETE 정상 CANCEL 사용자 중단 FAILED 시스템 문제
핵심 포인트
👉 CANCEL ≠ ERROR
이걸 안 나누면?
- 사용자 취소를 장애로 오해
- 장애 분석 불가능
🔥 이 설계의 핵심 가치
1. 정확한 시작 시점 측정
Flux.defer로 해결
2. 사용자 경험 vs 시스템 성능 분리
- TTFT
- Duration
3. 현재 상태 + 누적 상태 동시 관찰
- active
- started / completed / failed
4. 원인 분리 가능
- cancel vs error
🎯 최종 한 줄 정리
👉 스트리밍을 하나의 요청이 아니라 “생명주기”로 보고 단계별로 측정한 설계
💡 면접용 한 줄
“Flux.defer로 실제 실행 시점 기준으로 메트릭을 시작하고, TTFT와 전체 duration을 분리 측정하며, 종료를 complete/cancel/error로 나눠 스트리밍 상태를 정확하게 관측할 수 있도록 설계했습니다.”
728x90반응형