ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • SSE 기반 AI 스트리밍에서 성능을 제대로 측정하는 방법 (TTFT, Duration, Active Stream 설계)
    카테고리 없음 2026. 3. 26. 11:32
    728x90
    반응형

    🚀 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
    반응형
Designed by Tistory.