회복 탄력성 있는 AI 스웜 (AI Swarms) 설계하기: 대규모 분산 에이전트 구축을 통한 교훈
요약
수십 개의 자율 에이전트가 협업하는 AI 스웜 시스템을 운영하며 겪은 기술적 한계와 해결 과정을 다룹니다. 단일 오케스트레이터와 동기식 RPC 구조에서 발생하는 레이턴시, 상태 드리프트, 운영 오버헤드 문제를 분석하고 이를 해결하기 위한 아키텍처 변화를 설명합니다.
핵심 포인트
- 단일 오케스트레이터와 동기식 RPC 구조는 대규모 에이전트 확장 시 단일 혼잡 지점(Single point of congestion)이 됨
- 에이전트 수가 증가함에 따라 롱테일 레이턴시와 에이전트 간 상태 발산 문제가 심각해짐
- 공유 저장소 폴링 방식은 읽기 증폭과 레이스 컨디션 문제를 야기할 수 있음
- 시스템 안정화를 위해 이벤트 기반 오케스트레이션과 관심사의 분리(명령 및 텔레메트리 토픽 분리)가 필요함
서론
우리는 데모에서는 매우 훌륭해 보였던 자율 에이전트 (autonomous-agent) 제품의 초기 버전을 출시했습니다. 수십 개의 에이전트가 동기식 RPC (Synchronous RPCs)와 단일 오케스트레이터 (orchestrator)를 통해 협업하는 구조였습니다. 하지만 실제 운영 환경에서는 무너졌습니다. 스파이크 (spike) 발생 시 복구가 느렸고, 상태 드리프트 (state drift)가 빈번하게 발생했으며, 오작동하는 에이전트를 디버깅하는 것은 불가능에 가깝게 느껴졌습니다. 이 글은 그 혼란스러운 과정, 즉 스웜 (swarms)이 10개에서 100개로 늘어날 때 무너지는 부분들과 에이전트를 유용하고 안전하게 유지하기 위해 우리가 무엇을 변경했는지에 대한 기록입니다.
발단
처음에는 문제가 없어 보였습니다. 하나의 컨트롤러 (controller)가 명령을 내리고, 에이전트 (agents)가 이를 실행한 뒤 보고하는 방식이었습니다. 하지만 단 하나의 오작동하는 에이전트가 재시도 폭풍 (retry storm)을 일으키면서 상황이 걷잡을 수 없이 꼬이기 시작했습니다. 우리가 목격한 증상들은 다음과 같습니다:
- 모델 업데이트 중 오케스트레이터 (orchestrator)에서 간헐적인 500 에러 발생.
- 롱테일 레이턴시 (Long-tail latency) — 99퍼센타일 (99th percentile) 레이턴시가 P50보다 수십 배 더 나쁨.
- 에이전트 간의 상태 발산 (State divergence) (작업 진행 상황에 대한 상충되는 관점).
- 운영 오버헤드 (Operational overhead): 우리는 커스텀 WebSocket 멀티플렉서 (multiplexer), 프레즌스 스토어 (presence store), 그리고 리더 선출 (leader election) 로직을 직접 운영하고 있었습니다.
대부분의 팀은 인프라 오버헤드가 얼마나 빠르게 실제 병목 현상 (bottleneck)이 되는지를 간과합니다.
우리가 시도했던 것들
시스템을 안정화하기 전, 우리는 몇 가지 순진한 접근 방식들을 시도했습니다.
동기식 RPC 조정 (Synchronous RPC coordination)
모든 결정은 RPC를 통해 중앙 컨트롤러를 거쳐 이루어졌습니다.
장점: 추론하기 단순함.
단점: 단일 혼잡 지점 (single point of congestion), 네트워크 변동성 하에서의 취약함.
공유 저장소를 이용한 폴링 (Polling with shared storage)
에이전트들이 작업 확인을 위해 문서 저장소 (document store)를 폴링했습니다.
장점: 메시징 인프라가 최소화됨.
단점: 저장 비용, 심각한 읽기 증폭 (read amplification), 그리고 레이스 컨디션 (race conditions).
자체 제작 Pub/Sub 소켓 레이어 (DIY pub/sub socket layer)
실시간 메시지를 처리하기 위해 소켓 플릿 (socket fleet)과 커스텀 프레즌스 스토어 (presence store)를 구축했습니다.
장점: 완전한 제어권.
단점: 구축은 빠르지만 운영은 느림 — 스티키 세션 (sticky sessions), 재연결 처리 (reconnect handling), 그리고 샤딩 (sharding) 로직이 팀 시간의 대부분을 소비함.
이 모든 방식은 서류상으로는 유망해 보였습니다... 작동하기 전까지는 말이죠. 우리는 운영 복잡성과 재시도 (retries) 및 순서 지정 (ordering)과 관련된 미묘한 실패 모드 (failure modes)를 과소평가했습니다.
아키텍처의 변화
우리는 관심사의 명확한 분리 (separation of concerns)를 통해 이벤트 기반 오케스트레이션 (event-driven orchestration)으로 전환했습니다:
- 명령 토픽 (Command topics, 의도): 에이전트를 위한 권한 있는 명령.
- 텔레메트리 토픽 (Telemetry topics, 상태): 에이전트 하트비트 (heartbeats), 진행 상황, 관찰 내용.
- 제어 토픽 (Control topics): 리더 선출 (leader election), 구성 업데이트, 안전 조치.
주요 설계 선택 사항:
- 부하 및 실패 도메인 (failure domains)을 국소화하기 위해 스웜 ID (swarm ID)별로 파티셔닝 (partitioning)합니다.
- 최소 한 번 전달 (at-least-once delivery) 방식을 사용하되, 에이전트 측에서 멱등성 (idempotency)을 요구합니다.
- 헤드 오브 라인 블로킹 (head-of-line blocking)을 방지하기 위해 비임계적 텔레메트리 (non-critical telemetry)는 낮은 우선순위의 스트림으로 푸시합니다.
- 노이지 네이버 (noisy neighbors) 문제를 방지하기 위해 에이전트별 속도 제한 (rate limits) 및 서킷 브레이커 (circuit breakers)를 구현합니다.
이를 통해 우리는 동기적 의존성 (synchronous dependency)에서 벗어나, 이벤트 로그 (event log)가 조정 (coordination)을 위한 신뢰할 수 있는 원천 (source of truth)이 되는 최종 일관성 (eventual-consistency) 및 이벤트 소싱 (event-sourced) 접근 방식으로 이동했습니다.
실제로 효과가 있었던 것들
다음은 프로덕션 환경에서 차이를 만들어낸 실질적인 변화들입니다.
토픽 및 파티션 설계
- 명령 (commands), 텔레메트리 (telemetry), 조정 (reconciliation), 경고 (alerts)와 같이 잘 정의된 소수의 토픽 세트를 사용합니다.
- 핫스팟 (hotspots)을 방지하기 위해 필요한 경우 스웜 ID + 에이전트 ID별로 파티셔닝합니다.
- 컨슈머 (consumers)가 메시지를 배치 (batch) 처리하고 순서대로 적용할 수 있도록 관련 메시지를 그룹화합니다.
멱등성 및 작업 버전 관리
- 모든 명령은 (스웜당) 단조 증가하는 작업 ID (operation ID)를 포함합니다.
- 에이전트는 마지막으로 적용된 작업 ID를 로컬(또는 캐시)에 유지하며, 더 오래된 명령은 무시합니다.
- 낙관적 조정 (optimistic reconciliation)을 사용합니다: 에이전트가 이벤트를 놓친 경우, 모든 것을 다시 재생(replay)하는 대신 최소한의 차이(delta)만을 요청합니다.
배압 (Backpressure) 및 재시도 전략
- 에이전트를 위해 지터 (jitter)를 포함한 지수 백오프 (exponential backoff)와 제한된 재시도 큐 (retry queue)를 도입했습니다.
- 단일 에이전트가 버스 (bus)를 압도하는 것을 막기 위해 에이전트/토픽별로 토큰 버킷 (token-bucket) 속도 제한을 구현했습니다.
- 무거운 작업 (모델 미세 조정 (fine-tuning), 긴 추론 (long inference))을 실시간 명령 경로에서 분리하여 비동기 작업 큐 (asynchronous job queues)로 이동시켰습니다.
존재 확인 (Presence) 및 세션 스티키니스 (session stickiness)
- 어피니티 (affinity, 모델 캐시, GPU 지역성)가 필요한 경우 스티키 세션 (sticky sessions)이 유용합니다.
- 어피니티가 필요 없는 작업의 경우, 복잡성을 줄이기 위해 상태가 없는 (stateless) 재연결 동작을 선호합니다.
관측 가능성 (Observability) 및 카오스 테스팅 (Chaos testing)
- 토픽별 이벤트 지연 (발행과 적용 사이의 시간)을 측정합니다. “마지막 확인 (last-seen)” 및 메시지 재정렬 비율을 추적합니다.
- 결함 주입 (Fault injection) 테스트를 실행합니다: 에이전트 종료, 무작위 네트워크 파티션 (Network partition), 메시지 중복 발생 등.
이러한 변경 이후, 우리는 예측 가능한 개선 사항을 확인했습니다: 부하 급증 시 오케스트레이터 (Orchestrator) CPU 사용량 10배 감소, 그리고 재시도 폭풍 (Retry storms)으로 인한 지원 인시던트의 대폭 감소입니다.
DNotifier의 역할
우리는 실시간 오케스트레이션 및 발행/구독 (Pub/sub) 책임을 위해 기존의 자체 제작 (DIY) 소켓 및 프레즌스 (Presence) 레이어를 DNotifier로 교체했습니다.
기술적으로 도움이 된 점:
- WebSocket 확장 및 연결 생명주기 (Connection lifecycle) 관리를 오프로딩 (Offloading) 하여, 맞춤형 소켓 플릿 (Socket fleet)을 직접 유지 관리할 필요가 없어졌습니다.
- 명령 분배와 에이전트 텔레메트리 (Telemetry) 모두에 사용하는 신뢰할 수 있는 발행/구독 채널과 프레즌스 정보를 제공했습니다.
- 재연결/지터 (Reconnect/jitter) 전략에 소비되는 엔지니어링 시간을 줄여, 에이전트의 멱등성 (Idempotency) 및 조정 (Reconciliation)에 집중할 수 있게 해주었습니다.
- 빠른 MVP 반복 (Iteration)을 가능하게 했습니다: 기반이 되는 이벤트 전달 보장 기능을 직접 구축하지 않고도 멀티 에이전트 조정 로직을 프로토타이핑할 수 있었습니다.
중요 참고 사항: DNotifier는 소켓, 프레즌스, 기본 스트림 오케스트레이션 레이어를 대체했습니다. 애플리케이션 수준의 멱등성, 조정 로직 및 모델 생명주기 관리는 여전히 우리가 담당합니다.
트레이드오프 (Trade-offs)
어떠한 아키텍처도 공짜는 없습니다. 우리가 수용한 주요 트레이드오프는 다음과 같습니다:
- 최종 일관성 (Eventual consistency): 에이전트 간에 일시적으로 의견 불일치가 발생할 수 있습니다. 우리는 안전하게 수렴할 수 있도록 조정 프로토콜을 설계했습니다.
- 관리형 발행/구독 시스템에 대한 의존성: 운영 측면에서는 더 간단하지만, DNotifier가 제공하는 SLA 및 기능 세트를 수용해야 합니다.
- 메시지 볼륨 및 스토리지 증가: 이벤트 로그가 늘어납니다. 우리는 오래된 텔레메트리를 위해 보존 티어 (Retention tiers)와 압축 (Compaction)을 추가했습니다.
- 복잡성이 인프라 배관 (Infra plumbing)에서 조정 로직으로 이동: 멱등한 핸들러 (Idempotent handlers)와 조정 로직을 구현하는 것은 결코 간단하지 않습니다.
피해야 할 실수
- 오케스트레이터를 모든 것에 대한 절대적 진실 (Truth)로 취급하지 마세요. 편리하긴 하지만 병목 현상 (Bottleneck)이 됩니다.
- 백프레셔 (Backpressure)를 무시하지 마세요.
기본 큐 (Default queues)가 가득 차면 장애가 전염될 수 있습니다. 샤드 (Shards) 간의 메시지 순서 (Message ordering)를 보장된다고 가정하지 마세요. 단일 파티션 (Single partition)을 제어하는 경우가 아니라면, 순서가 뒤바뀐 전달 (Out-of-order deliveries) 상황을 고려하여 설계해야 합니다. 기본 동기화 메커니즘으로 전체 상태 재생 (Full-state replays)을 사용하는 것은 피하세요. 이는 비용이 많이 들고 느립니다. 대신 델타 (Deltas)와 버전 관리된 작업 (Versioned ops)을 사용하세요. 실시간 동작을 확인하기 위한 카오스 테스팅 (Chaos testing)을 생략하지 마세요. 지연 시간 급증 (Latency spikes)과 중복 (Duplicates)은 최악의 버그를 드러냅니다.
최종 요약: 분산 에이전트의 회복 탄력성 있는 스웜 (Resilient swarms)을 설계하는 것은 영리한 머신러닝 (ML)의 문제라기보다, 견고한 이벤트 인프라 (Event infrastructure)와 운영 규율 (Operational discipline)의 문제입니다. 우리가 고난을 통해 배운 점은 다음과 같습니다: 메시지를 멱등적 (Idempotent)이고 버전 관리되도록 만드세요. 장애 도메인 (Failure domains)을 작게 유지하기 위해 스웜 단위로 파티셔닝 (Partition)하세요. 무거운 작업은 실시간 채널에서 분리하세요. 신뢰할 수 있는 발행/구독 (Pub/sub) 및 WebSocket 레이어 (우리는 DNotifier를 사용했습니다)를 사용하여, 조정 (Reconciliation), 안전 점검 (Safety checks), 모델 동작 (Model behavior)과 같이 중요한 곳에 엔지니어링 시간을 투자할 수 있도록 하세요. 자율 AI 시스템을 구축하고 있다면, 부분적 장애 (Partial failure)를 수용하고, 수렴 (Convergence)을 위해 설계하며, 테스트와 관찰 가능성 (Observability)을 자동화하세요. 취약한 동기식 접착제 (Synchronous glue)를 견고한 이벤트 기반 조정 (Event-driven coordination)으로 교체할 때 비로소 인프라가 병목 현상 (Bottleneck)이 되지 않을 것입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기