본문으로 건너뛰기
김신건의 로그

[Voice AI] 파이프라인 디커플링: Redis Streams / Kafka 적용

· 수정 · 📖 약 1분 · 468자/단어 #voice-pipeline #queue #redis-streams #kafka #decoupling #scaling
voice pipeline decoupling, ASR LLM TTS decoupling, voice queue, stateless orchestrator, horizontal scaling voice

정의

대규모 음성 에이전트 = STT/LLM/TTS 를 분리 배포 + 메시지 큐로 디커플링. 수평 확장 + 독립 실패.

NOTE

소규모 (수십 동시 통화)monolith pipeline (Pipecat 한 프로세스) 으로 충분. 수백 ~ 수천 동시 부터 디커플링 의미.

Monolith vs Decoupled

flowchart TB
    subgraph Mono["Monolith"]
        M1["Pipecat process<br/>(STT + LLM + TTS 한 프로세스)"]
    end
    subgraph Decoupled["Decoupled"]
        D1["Orchestrator (stateless)"]
        D2[STT Workers]
        D3[LLM Workers]
        D4[TTS Workers]
        Q1[(Queue: stt-tasks)]
        Q2[(Queue: llm-tasks)]
        Q3[(Queue: tts-tasks)]
        D1 --> Q1 --> D2
        D2 --> Q2 --> D3
        D3 --> Q3 --> D4
        D4 --> D1
    end
MonolithDecoupled
운영 복잡도낮음높음
수평 확장동일 비율단계별 독립
독립 실패전체 다운단계별 격리
지연최저+ queue hop (~10ms)
동시 통화수십수천+

큐 선택

적합
Redis Streams가장 단순, < 수만 RPS
Redis Pub/Sub손실 OK, 알림만
Kafka큰 throughput, 영속, replay
NATS JetStream가벼움, edge
AWS SQSmanaged, AWS only

음성 에이전트 = Redis Streams 가 가장 흔함 (latency 낮음, 운영 단순).

Redis Streams 패턴

# Producer (STT worker)
async def stt_worker():
    while True:
        # stt-tasks 에서 받음
        msgs = await redis.xreadgroup(
            "stt-group", "stt-worker-1",
            {"stt-tasks": ">"},
            count=10, block=100,
        )
        for stream, items in msgs:
            for msg_id, data in items:
                audio_url = data["audio_url"]
                transcript = await deepgram.transcribe(audio_url)

                # 다음 단계로 전달
                await redis.xadd("llm-tasks", {
                    "session_id": data["session_id"],
                    "transcript": transcript,
                })
                await redis.xack("stt-tasks", "stt-group", msg_id)

자세한 Streams 는 Redis Streams.

Orchestrator (stateless)

class VoiceOrchestrator:
    """Session 만 관리. 실제 처리는 worker."""

    async def on_call_start(self, call_id: str):
        # Session 을 Redis 에 저장 (stateless 의 외부 상태)
        await redis.hset(f"call:{call_id}", mapping={
            "started_at": time.time(),
            "status": "active",
        })

    async def on_audio_chunk(self, call_id: str, audio: bytes):
        # 큐에 던지고 끝
        await redis.xadd("stt-tasks", {
            "call_id": call_id,
            "audio": audio,
        })

    async def on_llm_result(self, call_id: str, text: str):
        # TTS 큐로
        await redis.xadd("tts-tasks", {
            "call_id": call_id,
            "text": text,
        })

    async def on_tts_audio(self, call_id: str, audio_chunk: bytes):
        # 사용자에게 stream
        await self.send_to_user(call_id, audio_chunk)

Orchestrator 인스턴스 자체는 상태 없음. 모든 상태 Redis. 수평 확장 + 무중단 배포 자유.

수평 확장

flowchart TB
    LB[Load Balancer] --> O1[Orchestrator 1]
    LB --> O2[Orchestrator 2]
    LB --> O3[Orchestrator 3]
    O1 & O2 & O3 --> Redis[(Redis: 상태 + 큐)]
    Redis --> STT_W["STT Workers (auto-scale)"]
    Redis --> LLM_W["LLM Workers (auto-scale)"]
    Redis --> TTS_W["TTS Workers (auto-scale)"]
컴포넌트스케일링 기준
Orchestrator동시 connection 수
STT Workerstt-tasks lag
LLM Workerllm-tasks lag, GPU
TTS Workertts-tasks lag

각자 다른 자원. STT 는 CPU, LLM 은 GPU, TTS 는 GPU 또는 API. 분리 = 효율적 자원.

Sticky vs Stateless

flowchart LR
    Q{Voice agent stateless?}
    Q -->|"Yes (모든 state Redis)"| Stateless[수평 확장 자유]
    Q -->|"No (메모리에 conversation)"| Sticky[Sticky session 필수]

Sticky = 같은 통화 = 같은 orchestrator. SFU + WebRTC 환경에서 불가피할 때 있음.

Failure Recovery

flowchart LR
    Crash[Orchestrator 1 죽음]
    Crash --> Detect[Health check fail]
    Detect --> Drain[기존 통화 drain]
    Detect --> NewO[다른 orchestrator 가 이어받음]
    NewO --> Restore[Redis 에서 state 복원]
async def take_over_call(call_id: str):
    state = await redis.hgetall(f"call:{call_id}")
    if not state:
        return False

    # 이미 다른 orchestrator 처리 중?
    owner = await redis.set(
        f"call:{call_id}:owner",
        self.instance_id,
        nx=True, ex=30,
    )
    if not owner:
        return False

    # 이어받음
    await self.resume_call(call_id, state)
    return True

자세한 상태 관리Redis Sorted Sets 의 session 패턴.

Sentinel + Cluster

flowchart LR
    Voice[Voice agents] --> Sentinel["Redis Sentinel<br/>(자동 failover)"]
    Sentinel --> Primary[(Redis Primary)]
    Primary --> Replica1[(Replica 1)]
    Primary --> Replica2[(Replica 2)]

음성 = Redis 단일 장애 = 모든 통화 끊김. Sentinel 또는 Cluster + sticky session 보호 필수.

외부 API 복원력

flowchart TB
    Q[복원력 패턴]
    Q --> T1["Timeout (짧게)"]
    Q --> T2["Retry + backoff (idempotent only)"]
    Q --> T3[Circuit Breaker]
    Q --> T4["Fallback (다른 vendor)"]
    Q --> T5[Health check]
단계timeoutfallback
STT5초Deepgram → AssemblyAI
LLM30초GPT-4o-mini → Claude Haiku
TTS10초Cartesia → Azure

자세한 건 retry-with-backoff, circuit-breaker.

흔한 함정

WARNING

  1. Monolith → Decouple 전환 너무 일찍 = 운영 복잡도 폭증. 수십 동시 통화는 monolith.
  2. 상태 메모리 보관 = orchestrator restart = 통화 끊김. Redis 외부 상태.
  3. Redis Streams MAXLEN 무제한 = 메모리 폭증. MAXLEN ~ 100K 권장.
  4. Queue lag 모니터링 없음 = 어디서 병목인지 모름. Prometheus + alert.

관련 위키

이 글의 용어 (6개)
[Concurrency] Circuit Breaker: cascade failure 방어concurrency
정의 Circuit Breaker = 전기 회로의 차단기처럼, 백엔드 다운 / 느림 시 호출 자체를 차단 → 빠른 실패 + 백엔드 회복 시간. 3가지 상태 | 상태 | 의미 | …
[Concurrency] Retry + Exponential Backoff + Jitterconcurrency
정의 Retry with Exponential Backoff + Jitter = 실패 시 점점 긴 간격으로 재시도, 랜덤 분산. 분산 시스템의 thundering herd / r…
[Distributed] Kafka: 분산 로그, partition, consumer groupdistributed-systems
정의 Apache Kafka = 분산 commit log. 고처리량 (수백만 msg/s), 영속, 수평 확장. event-driven 아키텍처 의 de facto. 핵심 개념: …
[Redis] Pub/Sub vs Streams: 휘발 신호 vs 영속 로그database-internals
정의 - Pub/Sub ( / ): 지금 듣고 있는 구독자 에게만 메시지가 전달되는 휘발성 신호. 영속 없음, ACK 없음. fan-out. - Streams ( / / / ):…
[Redis] Sorted Set: Skiplist + Dict, 리더보드, 우선순위 큐database-internals
정의 Redis Sorted Set (ZSet) 은 멤버 + 점수 (score) 의 순서 보존 unique 집합. score 기준 정렬 + 멤버 기준 O(1) 조회 가 동시에 가…
[Voice AI] 음성 에이전트 아키텍처: Cascading / Streaming / S2S + Latency Budgetai
정의 실시간 음성 AI 의 3가지 패턴. 각자 지연 vs 유연성 vs 비용 트레이드오프. 3 패턴 비교 | | Cascading | Streaming | S2S | |---|--…

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기