AI 에이전트에게 50ms SLA 체크포인트 엔진이 필요한 이유 (그리고 이를 구축한 방법)
요약
프로덕션 환경의 AI 에이전트가 직면하는 체크포인팅 지연 문제를 분석하고, 이를 해결하기 위한 아키텍처를 제안합니다. 데이터베이스 쓰기 잠금으로 인한 성능 저하를 방지하기 위해 실행 스레드를 차단하지 않는 구조의 필요성을 강조합니다.
핵심 포인트
- 프로덕션 환경의 동시 사용자 증가 시 체크포인팅 지연 발생
- SQLite의 단일 쓰기 모델은 동시 에이전트 워크로드에 부적합
- 에이전트 실행 스레드를 차단하지 않는 비동기적 체크포인팅 필요
- Living AI를 통한 핫 RAM 캐시 기반의 솔루션 제안
개발 환경에서 작동하는 AI 에이전트를 만드는 것과 프로덕션(production) 환경에서 생존할 수 있는 AI 에이전트를 만드는 것은 전혀 다른 문제입니다.
개발 단계에서는 에이전트가 동시 사용자 없이 당신의 로컬 머신에서 한 번 실행되며, 데이터베이스(database)는 밀리초(ms) 단위로 응답합니다. 하지만 프로덕션 환경에서는 50개의 에이전트가 동시에 실행되고, 대화 기록(conversation histories)은 수백 킬로바이트(KB)로 커지며, 데이터베이스는 때때로 잠금(lock)이 발생하거나, 타임아웃(timeout)이 나거나, 잠시 사용 불가능한 상태가 되기도 합니다.
대부분의 에이전트 프레임워크(agent frameworks)는 이러한 현실을 고려하여 설계되지 않았습니다. 그리고 그 격차는 바로 한 가지 특정 지점, 즉 체크포인팅(checkpointing)에서 나타납니다.
프로덕션 에이전트 아키텍처의 조용한 살인자
체크포인팅(Checkpointing)은 에이전트가 단계(step) 사이에서 자신의 상태(state)를 저장하는 방식입니다. 모든 주요 프레임워크가 이를 수행합니다. LangGraph에는 SqliteSaver와 PostgresSaver가 있습니다. CrewAI는 자체적인 지속성 계층(persistence layer)을 가지고 있습니다. OpenAI Agents SDK는 스레드 상태 관리(thread state management) 기능을 갖추고 있습니다.
하지만 이들 중 거의 어느 것도 데이터베이스가 느려질 때 어떤 일이 발생하는지는 고려하지 않습니다.
표준적인 구현 방식은 대략 다음과 같습니다:
async def save_checkpoint(state):
await database.write(state) # 완료될 때까지 블로킹(blocks)
continue_execution()
정상적인 조건에서는 문제가 없습니다. 하지만 SQLite를 사용하는 동시 부하(concurrent load) 상황에서는 재앙적입니다. SQLite는 파일 수준의 쓰기 잠금(file-level write locking)을 사용합니다. 50개의 에이전트가 동시에 쓰기를 시도하면 서로의 뒤에서 대기열(queue)을 형성하게 됩니다. 쓰기 지연 시간(write latencies)은 1밀리초 미만에서 700밀리초 이상으로 급증합니다. 2초 안에 응답해야 했던 당신의 에이전트는 이제 각 단계에서 상태를 저장하는 데만 0.75초를 기다리게 됩니다.
우리는 정확히 이 시나리오를 실행해 보았습니다. 50개의 동시 에이전트, 총 1,000번의 쓰기, 대화 기록이 누적됨에 따라 5KB에서 100KB로 증가하는 페이로드(payloads). SQLite를 백엔드 저장소(backing store)로 사용했을 때, 평균 쓰기 지연 시간은 282ms였습니다. p99(99퍼센타일)는 735ms였습니다. 50ms 이내에 쓰기를 완료하는 것으로 정의된 SLA(Service Level Agreement) 준수율은 0.5%에 불과했습니다.
이것은 설정의 문제가 아닙니다. 이는 SQLite의 단일 쓰기 모델(single-writer model)과 동시적인 에이전트 워크로드(concurrent agent workloads) 사이의 근본적인 아키텍처 불일치(architectural mismatch) 문제입니다.
우리가 구축한 아키텍처
Living AI는 이 문제에 대한 우리의 오픈 소스(open-source) 솔루션입니다. 핵심 통찰은 데이터베이스가 무엇을 하고 있든 상관없이, 체크포인팅(checkpointing)이 에이전트 실행 스레드(agent execution thread)를 절대 차단(block)해서는 안 된다는 것입니다.
이 아키텍처는 세 가지 구성 요소로 이루어져 있습니다.
첫 번째는 핫 RAM 캐시(hot RAM cache)입니다. 에이전트가 상태를 저장할 때, 설정 가능한 TTL(Time To Live)을 가진 프로세스 내 LRU 캐시(in-process LRU cache)에 동기적으로 기록합니다. 이 쓰기 작업은 디스크나 네트워크를 전혀 건드리지 않기 때문에 항상 1밀리초(sub-millisecond) 미만으로 완료됩니다. 읽기 작업은 이 캐시를 가장 먼저 확인합니다. 실행 중인 에이전트의 경우, 가장 최근의 상태는 거의 항상 캐시에 있으므로 일반적인 읽기 경로(read path)는 마이크로초(microseconds) 단위로 해결됩니다.
두 번째는 예산이 할당된 내구적 쓰기(budgeted durable write)입니다. RAM 캐시를 업데이트한 후, 엔진은 백킹 데이터베이스(backing database)에 쓰기를 시도합니다. 이 쓰기 작업은 asyncio.wait_for 내에서 실행되며, 기본값으로 50밀리초(milliseconds)라는 엄격한 타임아웃(timeout)이 적용됩니다. 만약 데이터베이스가 할당된 예산 내에 쓰기를 완료할 수 없다면, 엔진은 해당 쓰기를 드롭(drop)하고, 이를 누락된 체크포인트(missed checkpoint)로 로그에 기록한 뒤 계속 진행합니다. 에이전트 스레드는 절대 차단되지 않습니다.
세 번째는 자기 기술적 압축 계층(self-describing compression layer)입니다. 모든 상태 블롭(state blob)은 zlib 레벨 6로 압축되며, 1바이트 코덱 헤더(codec header)가 앞에 붙습니다. 헤더 값 0x00은 압축되지 않음을 의미하고, 0x01은 zlib를 의미합니다. 이 세부 사항은 생각보다 중요합니다. 이는 기존의 체크포인트를 깨뜨리지 않고도 향후 압축 알고리즘을 zstd로 변경할 수 있음을 의미합니다. 오래된 블롭들은 자신의 헤더를 읽어 현재의 기본 설정과 관계없이 올바르게 압축을 해제합니다.
처음 두 구성 요소의 순서를 결정하는 것이 핵심적인 설계 결정입니다. RAM 캐시(RAM cache)는 데이터베이스 쓰기(database write)를 시도하기 전에 업데이트됩니다. 이는 모든 데이터베이스 쓰기가 타임아웃(timeout)되더라도, 에이전트가 캐시를 통해 여전히 현재 상태에 접근할 수 있으며 충돌 복구(crash recovery)가 여전히 작동함을 의미합니다. 우리는 이를 직접 스트레스 테스트(stress test)했습니다. 150개의 동시 에이전트와 0.77MB 상태 페이로드(state payloads)를 사용한 하이퍼스케일(hyperscale) 테스트에서, 데이터베이스 쓰기의 99.3%가 타임아웃되었음에도 불구하고 1,500개 에이전트 전체에서 복구 성공률은 100%를 기록했습니다.
벤치마크 수치가 실제로 보여주는 것
우리는 세 가지 테스트 계층을 실행했으며, 각 계층이 무엇을 측정하는지 투명하게 공개하고자 합니다.
README 헤드라인 수치의 근거가 되는 단일 에이전트(single-agent) 벤치마크는 하나의 라이터(writer)와 50KB 압축 블롭(compressed blobs)을 사용하는 SQLite 저장소를 사용합니다. 체크포인트 쓰기 지연 시간(checkpoint write latency)은 p50에서 0.3ms, p95에서 0.8ms, p99에서 약 1ms입니다. 핫 캐시 읽기(Hot cache reads)는 약 4마이크로초(microseconds) 내에 완료됩니다.
프로덕션 워크로드(production workload) 테스트는 50개의 동시 에이전트, 총 1,000회의 쓰기, 그리고 5KB에서 100KB로 증가하는 페이로드를 사용합니다. 여기서 SQLite와 Redis의 비교가 의미를 갖게 됩니다:
| 지표 (Metric) | SQLite | Redis |
|---|---|---|
| 50ms 이내 SLA 준수 (SLA compliance within 50ms) | 0.5% | 100% |
| ... | ... | ... |
하이퍼스케일(hyperscale) 테스트는 150개의 동시 에이전트, 총 1,500회의 쓰기, 그리고 긴 히스토리와 광범위한 도구 호출(tool call) 기록을 포함하는 큰 컨텍스트 윈도우(context windows)를 나타내는 0.77MB 페이로드를 사용합니다:
| 지표 | SQLite | Redis |
|---|---|---|
| 50ms 이내 SLA 준수율 | 0.7% | 100% |
| ... |
Redis의 하이퍼스케일(hyperscale) p99가 62ms라는 점에 대한 솔직한 관찰을 덧붙이자면, 이는 50ms SLA를 약간 상회하지만, asyncio 루프 스케줄링(loop scheduling)이 이를 통과시켜 주었기 때문에 모든 쓰기(write) 작업이 여전히 완료되었습니다. 이 규모에서의 병목 현상(bottleneck)은 데이터베이스가 아닙니다. 바로 CPU입니다. 0.77MB 크기의 블롭(blob)을 zlib로 압축하는 것은 Python의 GIL(Global Interpreter Lock) 하에서 실행되는 CPU 바운드(CPU-bound) 작업입니다. 해당 페이로드 크기에서 압축 자체에 약 40ms가 소요되며, 이는 I/O를 위한 예산(budget)을 거의 남기지 않습니다. 이 한계에 부딪힌 팀들에게는 두 가지 선택지가 있습니다. 훨씬 더 빠르게 압축하는 zstd로 전환하거나, 압축 작업을 프로세스 풀 실행기(process pool executor)로 오프로드(offload)하는 것입니다. 향후 릴리스에서 이 두 가지를 모두 설정 옵션으로 추가할 예정입니다.
세 가지 계층 모두에서 나타나는 중요한 패턴은 SLA 준수 여부와 상관없이 복구 성공률(recovery success rate)이 100%라는 점입니다. 복구는 데이터베이스가 아닌 RAM 캐시에서 읽어오기 때문에 이 두 지표는 독립적입니다. SLA 준수율은 상태(state)의 얼마만큼이 영구 저장소(durable storage)에 도달했는지를 알려줍니다. 복구 성공률은 에이전트가 충돌(crash) 후 재개할 수 있는지를 알려줍니다. 둘 다 중요하지만, 동일한 수치는 아닙니다.
Living AI가 LangGraph 및 CrewAI와 결합되는 방식
Living AI는 에이전트 프레임워크(agent frameworks)를 대체하는 것이 아닙니다. LangGraph는 그래프 컴파일(graph compilation), 조건부 라우팅(conditional routing), 상태 스키마(state schemas), 그리고 복잡한 멀티 에이전트 워크플로우를 가능하게 하는 실행 모델(execution model)을 처리합니다. CrewAI는 크루 오케스트레이션(crew orchestration), 역할 할당(role assignment), 에이전트 협업(agent collaboration)을 담당합니다. 이것들은 Living AI가 해결하지 않으며, 해결하려고 시도하지도 않는 문제들입니다.
Living AI가 추가하는 것은 프레임워크 하단에 위치하는 프로덕션 신뢰성 계층(production reliability layer)입니다:
에이전트 로직 (Your agent logic)
↓
LangGraph / CrewAI / OpenAI Agents
...
프레임워크는 에이전트가 어디로 갈지를 결정합니다. Living AI는 에이전트가 그곳에 안정적으로 도달하고, 충돌 시 복구할 수 있으며, 디버깅 및 컴플라이언스(compliance)를 위해 완전한 실행 기록을 남길 수 있도록 보장합니다.
어댑터 레이어(adapter layer)는 이를 구성 가능(composable)하게 만듭니다. 각 프레임워크 어댑터는 프레임워크의 실행 이벤트(execution events)를 Living AI의 ExecutionNode 모델로 매핑하는 얇은 번역 레이어입니다. 핵심 런타임(core runtime)은 프레임워크에 대한 의존성이 전혀 없습니다. LangGraph에서 CrewAI로 교체하더라도 체크포인팅(checkpointing), 복구(recovery), 또는 리플레이(replay) 방식은 변하지 않습니다.
리플레이(replay) 기능
충돌 복구(crash recovery)는 명백한 사용 사례입니다. 하지만 일상적인 개발에서 더 흥미로운 기능은 리플레이입니다.
에이전트가 잘못된 답변을 내놓거나, 잘못된 항공권을 예약하거나, 잘못된 메시지를 보낼 때, 여러분이 답을 얻고자 하는 질문은 이것입니다:
핵심 라이브러리는 런타임 의존성(runtime dependencies)이 전혀 없습니다. 모든 기능은 Python 표준 라이브러리만을 사용합니다.
pip install livingai
Redis 사용 시:
pip install "livingai[redis]"
PostgreSQL 사용 시:
pip install "livingai[postgres]"
최소한의 크래시 복구 (crash recovery) 예제:
import asyncio
from livingai import (
CheckpointEngine, SQLiteStore, ExecutionNode,
...
중요한 것은 skip effects 라인입니다. 도구 노드(Tool nodes)는 기본적으로 비멱등적(non-idempotent)으로 표시됩니다. 복구 엔진은 이들을 절대 다시 실행하지 않습니다. 만약 에이전트가 6단계에서 카드를 결제하고 8단계에서 크래시가 발생했다면, 복구 시 카드는 다시 결제되지 않습니다.
저장소(repository)의 examples 디렉토리에는 크래시 복구, MOCK_TOOLS 디버깅, 비용 추적(cost tracking), 그리고 LangGraph 어댑터를 다루는 5개의 실행 가능한 데모가 포함되어 있습니다. 이 중 어떤 것도 LLM API 키나 네트워크 접속을 요구하지 않습니다.
워크로드에 적합한 스토어 선택하기
벤치마크를 통해 얻은 명시할 만한 교훈 중 하나는 다음과 같습니다: 적절한 스토어는 개인의 선호도가 아니라 동시성(concurrency) 수준에 따라 결정됩니다.
SQLite는 로컬 개발 및 단일 에이전트 워크로드에 적합한 기본값입니다. 설정이 전혀 필요 없고 Python에 포함되어 있으며, 낮은 동시성 환경에서 성능이 우수합니다. p99 기준 1ms 미만의 벤치마크 수치는 이 시나리오에서 실제로 달성 가능합니다.
Redis는 여러 에이전트가 동시에 실행되는 프로덕션 워크로드에 적합한 선택입니다. 전환 방법은 임포트(import) 문 하나를 바꾸고 연결 URL을 수정하는 것뿐입니다. 에이전트 로직을 변경할 필요도, 핵심 설정을 변경할 필요도 없습니다. SLA 준수율이 0.5%에서 100%로 올라갑니다.
PostgreSQL은 쿼리 기능, 실행 간 비용 집계(cost aggregation), 그리고 Redis 캐시가 밀려난(evicted) 프로세스 재시작 후 실행 이력을 재구성할 수 있는 능력을 갖춘 장기적이고 내구성이 있는 저장소가 필요할 때 적합한 선택입니다.
또한 이들을 계층화할 수도 있습니다. Redis를 활성 실행을 위한 핫 티어(hot tier)로 사용하고, PostgreSQL을 과거 기록을 위한 콜드 티어(cold tier)로 사용하는 방식입니다. 이는 에이전트를 대규모로 운영하는 팀에게 권장하는 구성입니다.
이 프로젝트는 Apache-2.0 라이선스이며 완전히 오픈 소스입니다.
GitHub: github.com/likkisamarthreddy/livingai
만약 프로덕션 (production) 환경에서 에이전트를 운영 중이며, 여기서 다루지 않은 신뢰성 문제에 직면했다면 GitHub Discussion을 열어주세요. 저희는 현재 웹 기반의 리플레이 (replay) UI를 갖춘 FastAPI 클라우드 백엔드를 다음 마일스톤 (milestone)으로 삼아 활발히 구축하고 있으며, 실제 프로덕션 피드백이 무엇을 만들지 결정하는 데 중요한 역할을 하고 있습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기