Kafka와 Flink가 모델 서빙 경계에서 부딪히는 지점
요약
Kafka/Flink 파이프라인은 레코드 단위의 백프레셔에 최적화된 반면, 모델 서버는 배치 처리로 높은 처리량 확보에 초점을 맞춥니다. 이 두 시스템 경계에서 지연 시간(latency) 손실이 발생하며, 이를 해결하려면 스트림 자체를 모델 경계 앞에서 다시 묶음 처리해야 합니다.
핵심 포인트
- 스트림 프로세서와 모델 서버는 최적화 목표가 상반됨 (레코드 vs 배치).
- Flink의 백프레셔는 레코드당 예측 가능한 지연 시간을 가정함.
- 모델 서빙은 GPU 효율을 위해 요청들을 의도적으로 집계(batching)함.
- 단순 동기 호출은 전체 태스크 스레드를 블로킹하여 성능 저하를 초래함.
- Flink의 AsyncFunction 사용이 비동기 처리를 위한 올바른 접근법임.
요약 — Kafka/Flink 파이프라인은 레코드 단위(record-at-a-time)의 백프레셔(backpressure) 인식 처리를 기반으로 구축되는 반면, 모델 서버는 요청을 묶음(batching)으로 처리하여 처리량(throughput)을 확보합니다. 이 두 시스템 사이의 경계가 실시간 AI 파이프라인이 지연 시간(latency) 보장을 조용히 잃어버리는 곳입니다. 이를 해결하려면 단순히 비동기 용량을 확장하는 것이 아니라, 모델 경계 바로 앞에서 스트림 자체를 다시 묶음 처리해야 합니다.
실시간 AI 아키텍처 다이어그램은 모두 똑같이 생겼습니다: Kafka 토픽, Flink 작업이 기능을 계산하고, 화살표가 모델 서버를 가리키며, 구석에는 그럴듯해 보이는 지연 시간 수치가 있습니다. 하지만 실제로 문제가 발생하는 부분은 아무도 그리지 않습니다. 스트림 프로세서와 모델 서버 사이의 경계는 단순한 배관(plumbing) 디테일이 아닙니다. 이는 정반대의 목표를 위해 최적화된 두 가지 동시성 모델(concurrency models) 간의 충돌이며, 대부분의 팀은 설명할 수 없는 꼬리 지연 시간(tail latency)으로 그 충돌 비용을 치르고 있습니다.
서로 반대 방향으로 최적화된 두 시스템
Flink의 전체 설계 철학은 레코드 단위 처리와 결정론적 백프레셔에 있습니다. 연산자(operator)는 이벤트를 처리하고 결과를 방출하며, 다음 이벤트가 즉시 뒤따릅니다. 체크포인팅(Checkpointing), 워터마크(Watermarks), 그리고 정확히 한 번의 시맨틱스(exactly-once semantics) 모두 레코드당 처리 시간이 작고 대략적으로 예측 가능하다는 것을 가정합니다. Flink의 백프레셔 메커니즘은 다운스트림 연산자가 따라가지 못할 때 업스트림 연산자를 늦추기 위해 존재하며, 이를 통해 전체 파이프라인이 고장 나기보다는 점진적으로 저하됩니다.
모델 서빙은 정반대의 것에 최적화되어 있습니다. GPU는 배치 처리(batching)를 통해 처리량 이점을 얻습니다: Triton의 동적 배치 처리(dynamic batching), vLLM의 연속 배치 처리(continuous batching), 또는 모든 사용자 정의 배치 레이어는 한 번에 하나의 추론(inference)을 실행하는 것이 하드웨어 자원의 대부분을 낭비하기 때문에 존재합니다. 모델 서버의 '좋은' 지연 시간 수치는 거의 항상 GPU가 효율적인 배치 크기에서 측정되며, 배치 크기 1에서 측정되는 경우는 드뭅니다. GPU 추론에 대한 전체 경제적 근거는 요청들을 개별적으로 도착하는 즉시 제공하는 것이 아니라, 이를 집계(aggregating)하는 것에 달려 있습니다.
스트림 프로세서가 레코드당 지연 시간을 최소화하도록 구축되었고, 서버는 의도적으로 지연을 발생시켜 처리량(throughput)을 최대화하도록 구축된 상황입니다. 이 긴장감은 저절로 해결되지 않습니다. 두 시스템이 만나는 경계에서 아무도 예산을 책정하지 않은 지연 시간으로 나타납니다.
단순한 통합이 속도가 아닌 정확성을 깨뜨리는 이유
일반적인 첫 구현 방식은 Flink의 map 또는 process 함수 내부에서 모델을 동기적으로(synchronously) 호출하는 것입니다. 이는 거의 항상 실수이며, Flink 자체 문서에서도 경고하고 있습니다. 왜냐하면 오퍼레이터 내에서의 블로킹 호출은 전체 태스크 스레드(task thread)를 멈추게 만들기 때문입니다. 체크포인트 장벽(checkpoint barriers)이 진행 중인 추론(inference) 호출 뒤에 줄을 서게 됩니다. 워터마크(Watermarks)는 전진하는 것을 멈춥니다. 파이프라인 전체의 처리량은 클러스터의 총 용량이 아니라, 가장 느린 진행 중인 모델 호출에 의해 제한됩니다.
Flink의 해결책은 AsyncFunction입니다. 이는 오퍼레이터가 비차단 요청(non-blocking requests)을 발행하고 응답을 기다리는 동안 다른 레코드 처리를 계속할 수 있게 해줍니다. 이것이 올바른 기본 원시 요소(primitive)이지만, 단지 문제를 이동시킬 뿐—배치 처리 불일치(batching mismatch)를 해결하지는 못합니다. 각 비동기 호출은 여전히 하나의 요청입니다. 용량 설정(Capacity settings)은 오퍼레이터가 허용하는 동시 진행 중인 요청 수를 제어하지만, 동시성(concurrency)이 배치 처리(batching)와 같지는 않습니다. 백 개에 달하는 진행 중인 비동기 요청을 보내더라도 여전히 모델 서버에는 배치 크기가 1인 호출을 조금씩 보내는 것일 수 있으며, 이 경우 그 어떤 것도 애초에 GPU 추론을 경제적으로 만드는 배치 로직을 활용하지 못합니다.
팀들이 놓치는 부분은 이것입니다. 비동기 용량을 늘리는 것은 Flink 측의 처리량을 개선하지만, 모델 서버의 작업은 더 어렵게 만듭니다. 더 많은 동시 단일 레코드 요청은 단순히 GPU 시간 슬롯을 두고 경쟁하는 더 많은 부분적으로 채워진 배치만을 의미하며, 이는 대시보드에서 총 처리량이 괜찮아 보여도 p99 지연 시간을 실제로 증가시킬 수 있습니다.
올바게 보이지만 그렇지 않은 수정 방법들
세 가지 수정 방법이 대략 이 순서로 제안되며, 세 가지 모두 단독으로는 불완전합니다.
첫 번째는 '단순히 병렬성을 확장하는 것'입니다. Flink 태스크 슬롯을 늘리거나, 컨슈머 인스턴스를 더 많이 두거나, 파티션을 늘리는 것입니다. 이는 요청의 형태를 바꾸지 않으면서 모델 서버에 도달하는 동시 요청 수를 증가시킵니다. 만약 모델 서버의 배치(batching) 윈도우가 특정 도착 패턴에 맞춰 조정되었다면, 이를 더 많은 병렬 소스에서 발생하는 상관관계 없는 단일 요청으로 범람시키는 것은 배치를 채우기보다는 오히려 분산시키는 경향이 있습니다.
두 번째는 '모델 서버 앞에 큐를 두는 것'입니다. 이는 버스트(burst) 흡수에는 도움이 되지만, 배치 불일치 문제를 해결하지 못합니다. 단지 지연시킬 뿐입니다. 큐는 도착률을 부드럽게 하지만, 무언가가 능동적으로 그룹화하지 않는 한 단일 요청 스트림을 잘 형성된 배치로 변환시키지는 못합니다.
세 번째는 '파이프라인과 모델을 완전히 분리하는 것'으로, 피처를 싱크(sink)로 보내고 추론(inference)을 독립적인 컨슈머로 실행하는 것입니다. 이는 실패 도메인(failure domain)을 분리한다는 점에서 진정으로 좋지만, 귀중한 무언가를 잃게 만듭니다. 바로 스트림 프로세서가 이미 이벤트 시간 의미론(event-time semantics), 윈도우링(windowing), 그리고 특정 시점에서 피처를 정확하게 만드는 순서 보장(ordering guarantees)을 알고 있다는 점입니다. 단순한 다운스트림 컨슈머는 이 컨텍스트를 재구축하거나 무시해야 하는데, 이것이 바로 스코어링하는 이벤트에 비해 조용히 오래된(stale) 피처를 가지고 추론을 실행하게 되는 원인이 됩니다.
실제로 작동하는 방법: 경계에서 의도적으로 재배치(re-batching)하기
유효한 해결책은 Flink와 모델 서버 사이의 연결 지점(seam) 자체를 싱크가 아니라 고유의 계약을 가진 구성 요소로 취급하는 것입니다. 구체적으로는 비동기 요청 발행과 모델 서버 호출 사이에 의도적인 마이크로 배치(micro-batching) 계층을 삽입하는 것을 의미합니다. 이는 명시적인 크기와 시간 경계(time bound)를 가진 버퍼로서, 수 밀리초 이내에 도착하는 요청들을 단일 추론 호출로 축적하도록 구축됩니다. 이를 통해 GPU 기반 서빙이 의존하는 배치를 다시 도입하지만, 우발적이거나 무제한적인 것이 아니라 유한하고 명시적으로 유지합니다.
스트림 처리 SLA와는 별개로 해당 경계에 자체적인 지연 시간(latency) SLA를 부여하는 것 또한 중요합니다. Flink의 체크포인팅 간격, 이벤트 시간 지연 목표치(event-time lag targets), 그리고 모델이 허용할 수 있는 큐잉 지연 시간은 세 가지 다른 숫자입니다. 이들을 혼동하면 느린 모델이 체크포인팅 간격을 조용히 잠식하고, 엄격한 체크포인팅 간격은 모델 서버가 배치(batch)를 효율적으로 채우기 전에 배치를 조용히 잘라버립니다. 이들을 분리하는 것
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기