-
PgBouncer 검토 및 Redis Checkpoint Sync 비교이코에코(Eco²)/Agent 2026. 1. 27. 10:50

https://www.pgbouncer.org/ Date: 2026-01-24
Author: Claude Code, mangowhoiscloud
Agenda: PostgreSQL Connection Pooling, PgBouncer, LangGraph Checkpoint 아키텍처 비교
Target: Eco² 프로젝트의 LangGraph 기반 Chat Pipeline에서 고빈도 checkpoint write 처리
Related: https://rooftopsnow.tistory.com/242, https://rooftopsnow.tistory.com/243
1. 문제 정의
1.1 LangGraph Checkpoint의 특성
LangGraph는 각 노드 실행 후 상태를 checkpoint로 저장합니다.
이는 장애 복구(fault tolerance)와 대화 이력 보존을 위한 핵심 메커니즘입니다.
Eco² Chat Pipeline (17 Nodes): ┌─────────────────────────────────────────────────────────────┐ │ intent_classifier │ │ ↓ │ │ router (Send API - 병렬 분기) │ │ ├─→ waste_node ────→ checkpoint write │ │ ├─→ weather_node ──→ checkpoint write │ │ └─→ location_node ─→ checkpoint write │ │ ↓ │ │ aggregator ─────────────→ checkpoint write │ │ ↓ │ │ answer_generator ───────→ checkpoint write │ │ ↓ │ │ ... (총 17개 노드) │ └─────────────────────────────────────────────────────────────┘1.2 병목의 본질
문제 시나리오: ┌────────────────────────────────────────────────────────────┐ │ 동시 사용자: 50명 │ │ Send API 병렬 노드: 3개 │ │ 노드당 checkpoint: 1회 │ │ │ │ → 순간 최대 checkpoint write: 50 × 3 = 150회 │ │ → PostgreSQL pool_size=5, max_overflow=10 → 최대 15 연결 │ │ → 135개 요청이 연결 대기 큐에서 직렬화 │ │ → PoolTimeout 발생 또는 LLM 파이프라인 지연 │ └────────────────────────────────────────────────────────────┘1.3 PostgreSQL 연결 = 프로세스
┌─────────────────────────────────────────────────────────────┐ │ PostgreSQL Server │ │ │ │ postmaster (main process) │ │ │ │ │ ├─→ backend process 1 (client connection 1) │ │ │ └─ private memory: 10-20MB │ │ ├─→ backend process 2 (client connection 2) │ │ │ └─ private memory: 10-20MB │ │ ├─→ backend process 3 (client connection 3) │ │ │ └─ private memory: 10-20MB │ │ └─→ ... │ │ │ │ Shared Memory: shared_buffers, WAL buffers, etc. │ └─────────────────────────────────────────────────────────────┘ 연결 생성 비용: - fork() 시스템콜 - private memory 할당 (work_mem, temp_buffers 등) - 인증 handshake - 총 비용: 수 ms + 10-20MB RAMPostgreSQL은 프로세스 기반 아키텍처를 채택하고 있습니다.
- 100 연결 = 100 프로세스 = 1-2GB RAM 추가 소비
- Context switch 오버헤드 (커널 레벨)
- 내부 Lock contention (ProcArrayLock, WALWriteLock 등)
PostgreSQL 공식 권장 연결 수:
max_connections ≈ (CPU 코어 수 × 2) + 유효 디스크 수 4코어 서버 → ~10개가 최적 throughput 구간 100개로 늘리면 → throughput 오히려 하락
2. PgBouncer 심층 분석
2.1 개요 및 아키텍처
PgBouncer는 PostgreSQL 전용 경량 Connection Pooler입니다. C로 작성되어 연결당 ~2KB만 사용합니다.
┌──────────────────────────────────────────────────────────────┐ │ PgBouncer │ │ │ │ Client Connections Server Connections │ │ (앱에서 오는 연결) (실제 PG 연결) │ │ │ │ ┌─────────┐ ┌─────────────────────┐ │ │ │ Client 1│─┐ │ │ │ │ └─────────┘ │ │ ┌───────────────┐ │ │ │ ┌─────────┐ │ ┌─────────┐ │ │ Server Conn 1 │──┼──→ PG │ │ │ Client 2│─┼─→│ Pool │──┼─→│ (SV_ACTIVE) │ │ │ │ └─────────┘ │ │ Manager │ │ └───────────────┘ │ │ │ ┌─────────┐ │ └─────────┘ │ ┌───────────────┐ │ │ │ │ Client 3│─┤ ↓ │ │ Server Conn 2 │──┼──→ PG │ │ └─────────┘ │ ┌─────────┐ │ │ (SV_IDLE) │ │ │ │ ... │ │ Waiting │ │ └───────────────┘ │ │ │ ┌─────────┐ │ │ Queue │ │ ... │ │ │ │Client N │─┘ └─────────┘ │ │ │ │ └─────────┘ └─────────────────────┘ │ │ │ │ max_client_conn=1000 default_pool_size=20 │ └──────────────────────────────────────────────────────────────┘2.2 Pool Mode 상세
PgBouncer의 핵심은 언제 서버 연결을 클라이언트에서 분리하는가입니다.
Session Mode (기본값)
Client A: ══════════════════════════════════════ Server Conn 1 │ connect │ BEGIN │ query │ COMMIT │ idle │ disconnect │ └─────────────────────────────────────────────────────┘ 연결 해제까지 점유 특징: - 세션 기능 전부 사용 가능 (SET, PREPARE, LISTEN, temp table 등) - 다중화 효과 없음 - 적합: 장기 연결, 세션 상태 필요한 앱Transaction Mode (Eco² 권장)
Client A: ─────[TX1]───────────────────[TX2]───── │ │ Server Conn 1: ═[TX1]═══[B:TX1]═══════[TX2]════ ↑ 다른 클라이언트가 사용 동작 원리: 1. Client A가 BEGIN 전송 → PgBouncer가 idle server conn 할당 2. TX 실행 중 → 해당 conn 점유 3. COMMIT/ROLLBACK → 즉시 conn을 pool에 반환 4. Client A는 여전히 연결 상태이지만 server conn 없음 5. 다음 TX 시작 → 다른 server conn 할당될 수 있음 제약사항: - SET 명령어: ❌ (TX 끝나면 conn 바뀜) → 대안: SET LOCAL (TX 범위에서만 유효) - PREPARE: ❌ (conn 바뀌면 prepared statement 사라짐) → 대안: PgBouncer 1.21+ max_prepared_statements - LISTEN/NOTIFY: ❌ (세션에 바인딩됨) - Temp Table: ❌ (세션 종속) - Advisory Lock: ❌ (세션 종속)Statement Mode
Client A: ─[Q1]─[Q2]─[Q3]─ │ │ │ Server: ═[Q1]═[B]═[Q3]═ ↑ 다른 클라이언트 특징: - 쿼리 단위로 conn 재할당 - 트랜잭션 불가 (autocommit only) - 극단적 다중화 - 적합: 읽기 전용 분석 쿼리2.3 내부 동작: 연결 상태 머신
┌───────────────────────────────────────────────────────────────┐ │ Client States Server States │ │ │ │ CL_FREE ──→ CL_LOGIN SV_FREE │ │ │ │ │ │ │ ↓ ↓ ↓ │ │ CL_ACTIVE ←── CL_WAITING ←───→ SV_LOGIN │ │ │ │ │ │ │ (transaction mode) ↓ │ │ │ TX 완료 시 SV_ACTIVE ←─→ CL_ACTIVE │ │ │ server conn 분리 │ (linked pair) │ │ ↓ ↓ │ │ CL_WAITING ──────────────────→ SV_IDLE │ │ │ │ │ │ └──── 다음 TX 시작 시 ─────────┘ │ │ 새 server conn과 link │ └───────────────────────────────────────────────────────────────┘2.4 Prepared Statement 처리 (PgBouncer 1.21+)
Transaction mode에서 prepared statement를 지원하기 위한 메커니즘입니다:
┌───────────────────────────────────────────────────────────────┐ │ Client → PgBouncer PgBouncer → PostgreSQL │ │ │ │ PARSE "stmt1" AS "SELECT..." (intercept & rename) │ │ │ │ │ │ ↓ ↓ │ │ PgBouncer internal: PARSE "PGBOUNCER_xxxx" │ │ - stmt1 → PGBOUNCER_xxxx mapping AS "SELECT..." │ │ - query text cached (global) │ │ │ │ EXECUTE "stmt1" (on conn B) (conn B에 stmt 없으면) │ │ │ → auto PARSE + EXECUTE │ │ ↓ │ │ Check: conn B has PGBOUNCER_xxxx? │ │ No → send PARSE first │ │ Yes → send EXECUTE directly │ └───────────────────────────────────────────────────────────────┘ 설정: max_prepared_statements = 200 # 0이면 기능 비활성화 제약: - Protocol-level prepared statement만 지원 - SQL PREPARE 명령어는 지원 불가2.5 asyncpg 호환성
asyncpg는 기본적으로 aggressive prepared statement caching을 사용합니다:
# asyncpg 기본 동작 conn.execute("SELECT * FROM users WHERE id = $1", user_id) # 내부적으로: # 1. statement_cache에 쿼리 존재? → 캐시된 prepared statement 사용 # 2. 없으면 → PREPARE → 캐시 → EXECUTE # Transaction mode 문제: # TX1: conn A에서 PREPARE → statement_cache에 기록 # TX2: conn B 할당됨 → conn B에는 해당 prepared statement 없음 # → "prepared statement does not exist" 에러해결책:
# 방법 1: prepared statement 완전 비활성화 pool = asyncpg.create_pool( dsn="postgresql://...@pgbouncer:6432/db", statement_cache_size=0, max_cached_statement_lifetime=0, ) # 방법 2: PgBouncer 1.21+ 사용 # pgbouncer.ini max_prepared_statements = 200 # asyncpg는 그대로 사용, PgBouncer가 중재2.6 고가용성 구성
┌─────────────────────────────────────────────────────────────────┐ │ Multi-PgBouncer Architecture │ │ │ │ ┌─────────────────┐ │ │ │ Load Balancer │ │ │ │ (HAProxy/NLB) │ │ │ └────────┬────────┘ │ │ │ │ │ ┌─────────────────┼─────────────────┐ │ │ │ │ │ │ │ ▼ ▼ ▼ │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ PgBouncer 1 │ │ PgBouncer 2 │ │ PgBouncer 3 │ │ │ │ (Active) │ │ (Active) │ │ (Active) │ │ │ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │ │ │ │ │ │ │ └─────────────────┼─────────────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────┐ │ │ │ PostgreSQL │ │ │ │ Primary │ │ │ └──────────────┘ │ └─────────────────────────────────────────────────────────────────┘ 이점: - SPOF 제거 - 수평 확장 (CPU bound 해소) - Rolling restart 가능 [peers] 섹션으로 cancel request 전파 설정 필요
3. 캐싱 패턴: Write-Through vs Write-Behind
3.1 Write-Through
┌─────────────────────────────────────────────────────────────────┐ │ Write-Through Pattern │ │ │ │ App ──write──→ Cache ──sync write──→ Database │ │ │ │ │ │ │ │ └── 동기적으로 DB 쓰기 ──┘ │ │ │ 완료될 때까지 대기 │ │ └── 응답 반환 │ │ │ │ 특징: │ │ - Strong Consistency (Cache = DB) │ │ - Write latency = Cache write + DB write │ │ - DB 장애 시 write 실패 │ └─────────────────────────────────────────────────────────────────┘3.2 Write-Behind (Write-Back)
┌─────────────────────────────────────────────────────────────────┐ │ Write-Behind Pattern │ │ │ │ App ──write──→ Cache ──queue──→ Background ──batch──→ Database│ │ │ │ │ Worker │ │ │ │ │ │ │ │ │ │ └─ 즉시 반환 └─ 비동기 동기화 │ └── 응답 반환 (~1ms) (~배치 주기)│ │ │ │ 특징: │ │ - Eventual Consistency │ │ - Write latency = Cache write only (~1ms) │ │ - DB 장애 시에도 write 성공 (queue에 축적) │ │ - 데이터 유실 위험 (cache crash before sync) │ └─────────────────────────────────────────────────────────────────┘3.3 Read-Through
┌─────────────────────────────────────────────────────────────────┐ │ Read-Through Pattern │ │ │ │ App ──read──→ Cache ──hit?──→ Yes: 즉시 반환 │ │ │ │ │ └── No (miss) │ │ │ │ │ ▼ │ │ Database ──read──→ Cache ──promote──→ 반환 │ │ │ │ │ (write-back) │ │ │ │ 특징: │ │ - Lazy loading: 요청 시점에 캐시 적재 │ │ - Temporal locality 활용: 최근 참조 데이터 재참조 가능성 높음 │ └─────────────────────────────────────────────────────────────────┘
4. Eco² 현재 구현: Write-Behind + Read-Through
4.1 아키텍처 개요
┌──────────────────────────────────────────────────────────────────────────┐ │ Eco² LangGraph Checkpoint Architecture │ │ │ │ ┌─────────────────────────────────────────────────────────────────────┐│ │ │ Chat Worker (LangGraph Pipeline) ││ │ │ ││ │ │ graph.ainvoke() ││ │ │ │ ││ │ │ ▼ ││ │ │ ┌─────────────────────────────────────┐ ││ │ │ │ ReadThroughCheckpointer │ ││ │ │ │ │ ││ │ │ │ Write Path: │ ││ │ │ │ aput() → SyncableRedisSaver │ ││ │ │ │ ├─ Redis SET (~1ms) │ ││ │ │ │ └─ LPUSH sync_queue │ ││ │ │ │ │ ││ │ │ │ Read Path: │ ││ │ │ │ aget_tuple() │ ││ │ │ │ ├─ Redis GET (hit) → return │ ││ │ │ │ └─ Redis miss │ ││ │ │ │ └─ PG read → Redis promote │ ││ │ │ └─────────────────────────────────────┘ ││ │ └─────────────────────────────────────────────────────────────────────┘│ │ │ │ │ │ sync_queue (Redis List) │ │ ▼ │ │ ┌─────────────────────────────────────────────────────────────────────┐│ │ │ Checkpoint Sync Service (별도 프로세스) ││ │ │ ││ │ │ while True: ││ │ │ batch = BRPOP + drain (max 50, 2s timeout) ││ │ │ deduplicate by thread_id ││ │ │ for event in batch: ││ │ │ checkpoint = Redis GET ││ │ │ PostgreSQL UPSERT (batch) ││ │ │ ││ │ │ PG Pool: min=1, max=5 (batch write이므로 충분) ││ │ └─────────────────────────────────────────────────────────────────────┘│ │ │ │ ┌─────────────┐ ┌─────────────┐ │ │ │ Redis │ │ PostgreSQL │ │ │ │ (Primary) │ ══════════▶ │ (Archive) │ │ │ │ TTL: 24h │ async sync │ Durable │ │ │ └─────────────┘ └─────────────┘ │ └──────────────────────────────────────────────────────────────────────────┘4.2 핵심 컴포넌트 상세
SyncableRedisSaver
# /apps/chat_worker/infrastructure/orchestration/langgraph/sync/syncable_redis_saver.py class SyncableRedisSaver(PlainAsyncRedisSaver): """Redis checkpoint 저장 + sync queue 이벤트 발행.""" async def aput(self, config, checkpoint, metadata, new_versions): # 1. Redis에 checkpoint 저장 (부모 클래스) result_config = await super().aput(config, checkpoint, metadata, new_versions) # 2. Sync queue에 이벤트 발행 (best-effort) event = json.dumps({ "thread_id": config["configurable"]["thread_id"], "checkpoint_ns": config["configurable"].get("checkpoint_ns", ""), "checkpoint_id": checkpoint["id"], }) await self._redis.lpush(SYNC_QUEUE_KEY, event) # O(1) return result_config async def aput_no_sync(self, ...): """Sync queue 없이 저장 (Read-Through promote용). PG에서 읽어온 checkpoint를 Redis에 적재할 때 사용합니다. 이미 PG에 존재하므로 sync queue에 넣으면 불필요한 upsert가 발생합니다. """ return await super().aput(...)ReadThroughCheckpointer
# /apps/chat_worker/infrastructure/orchestration/langgraph/sync/read_through_checkpointer.py class ReadThroughCheckpointer(BaseCheckpointSaver): """Redis Primary + PostgreSQL Cold Start Fallback. Temporal Locality (시간적 지역성) 활용: - Redis TTL(24h) 만료 후 재접속한 세션 → PG에서 복원 - 복원된 checkpoint를 Redis에 promote (LRU write-back) - 이후 동일 세션 요청은 Redis에서 직접 서빙 """ async def aget_tuple(self, config) -> Optional[CheckpointTuple]: # 1. Redis 조회 (hot path, ~1ms) result = await self._redis_saver.aget_tuple(config) if result is not None: return result # 2. Redis miss → PostgreSQL fallback (cold start, ~10-50ms) pg_result = await self._pg_saver.aget_tuple(config) if pg_result is None: self._miss_count += 1 return None # 3. Redis에 promote (LRU write-back) # aput_no_sync: 이미 PG에 존재하므로 sync queue bypass await self._redis_saver.aput_no_sync( config=pg_result.config, checkpoint=pg_result.checkpoint, metadata=pg_result.metadata, new_versions={}, ) self._promote_count += 1 return pg_resultCheckpointSyncService
# /apps/chat_worker/infrastructure/orchestration/langgraph/sync/checkpoint_sync_service.py class CheckpointSyncService: """Redis → PostgreSQL 비동기 동기화. 전략: 1. BRPOP으로 sync queue 대기 (blocking, timeout=5s) 2. 배치 수집 (max 50개 또는 2초) 3. thread_id별 deduplicate (최신 checkpoint만) 4. PostgreSQL batch upsert 5. 실패 시 DLQ로 이동 """ async def _collect_batch(self, stop_event) -> list[dict]: batch = [] # 첫 이벤트: blocking 대기 result = await self._redis.brpop(SYNC_QUEUE_KEY, timeout=5) if result is None: return batch batch.append(self._parse_event(result[1])) # 추가 이벤트: non-blocking drain deadline = time.monotonic() + self._drain_timeout # 2초 while len(batch) < self._batch_size: # 50개 if time.monotonic() > deadline: break raw = await self._redis.rpop(SYNC_QUEUE_KEY) if raw is None: break batch.append(self._parse_event(raw)) return batch async def _sync_batch(self, batch: list[dict]) -> int: # thread_id별 deduplicate (동일 thread의 최신 checkpoint만) latest = {} for event in batch: key = (event["thread_id"], event["checkpoint_ns"]) latest[key] = event # 마지막이 최신 for event in latest.values(): await self._sync_one(event) async def _sync_one(self, event: dict) -> bool: # Redis에서 checkpoint 읽기 tuple_data = await self._redis_saver.aget_tuple(config) if tuple_data is None: return False # TTL 만료 # PostgreSQL에 upsert await self._pg_saver.aput( config=tuple_data.config, checkpoint=tuple_data.checkpoint, metadata=tuple_data.metadata, new_versions={}, ) return True4.3 설계 결정의 근거
왜 Redis Primary인가?
LangGraph 노드 실행 특성: ┌─────────────────────────────────────────────────────────────┐ │ Node 1: LLM API 호출 (3~8초) │ │ └─ checkpoint write (~5ms with PG, ~1ms with Redis) │ │ Node 2: LLM API 호출 (3~8초) │ │ └─ checkpoint write │ │ Node 3: ... │ │ │ │ 총 파이프라인: 20~60초 │ │ 총 checkpoint write: 10~17회 │ │ checkpoint 시간 비중: (17 × 5ms) / 30s = 0.3% │ │ │ │ → Checkpoint latency 자체는 병목이 아닙니다 │ │ → 문제는 "동시 연결 수"입니다 │ └─────────────────────────────────────────────────────────────┘ Redis의 장점: - 단일 스레드 이벤트 루프 → 연결 수 증가해도 성능 일정 - Connection = 경량 (vs PG 프로세스 10-20MB) - 10,000+ 동시 연결 가능왜 Sync Queue인가?
대안 1: 주기적 polling (cron) - 단점: sync 지연 (polling 주기만큼) - 단점: 불필요한 scan (변경 없어도 전체 조회) 대안 2: CDC (Change Data Capture) - 단점: 복잡도 높음 - 단점: Redis에는 네이티브 CDC 없음 선택: Event-driven sync queue - 장점: 변경 즉시 이벤트 발행 → 최소 지연 - 장점: queue 비어있으면 대기 (BRPOP) → CPU 낭비 없음 - 장점: 배치 수집으로 효율적 bulk write - 장점: 실패 시 DLQ → 수동 복구 가능왜 batch_size=50, drain_timeout=2s인가?
Throughput vs Latency 트레이드오프: batch_size↑, timeout↑: - PG write 효율↑ (bulk insert) - sync 지연↑ - 장애 시 데이터 유실 범위↑ batch_size↓, timeout↓: - sync 지연↓ (near real-time) - PG 연결 점유 빈번 - 개별 insert 오버헤드 현재 설정 (50개/2초): - 일반 부하: 이벤트 수집되는 대로 즉시 sync - 피크 부하: 최대 2초 내 50개 모아서 batch write - sync 지연: 평균 < 1초, 최대 2초4.4 장애 시나리오 분석
┌─────────────────────────────────────────────────────────────────┐ │ Scenario 1: Redis 장애 │ │ │ │ 영향: │ │ - Write 실패 (checkpoint 유실) │ │ - Read fallback → PostgreSQL │ │ │ │ 대응: │ │ - Redis Cluster + AOF 영속화 │ │ - MemorySaver fallback (graceful degradation) │ └─────────────────────────────────────────────────────────────────┘ ┌─────────────────────────────────────────────────────────────────┐ │ Scenario 2: PostgreSQL 장애 │ │ │ │ 영향: │ │ - Write: 정상 (Redis에 저장됨) │ │ - Sync: 실패 → queue에 축적 │ │ - Read: Redis hit → 정상, cold start → 실패 │ │ │ │ 대응: │ │ - PG 복구 후 queue에 쌓인 이벤트 일괄 sync │ │ - Redis TTL을 PG 복구 예상 시간보다 길게 설정 │ └─────────────────────────────────────────────────────────────────┘ ┌─────────────────────────────────────────────────────────────────┐ │ Scenario 3: Sync Service 장애 │ │ │ │ 영향: │ │ - Write: 정상 (Redis + queue) │ │ - Read: 정상 (Redis 또는 PG fallback) │ │ - Sync: 중단 → queue 축적 │ │ │ │ 대응: │ │ - Sync service 재시작 시 queue부터 처리 │ │ - Kubernetes liveness probe로 자동 재시작 │ └─────────────────────────────────────────────────────────────────┘ ┌─────────────────────────────────────────────────────────────────┐ │ Scenario 4: Redis TTL 만료 + PG sync 미완료 │ │ │ │ 상황: Redis에서 만료됐는데 아직 PG에 sync 안 됨 │ │ │ │ 영향: │ │ - Checkpoint 유실 (Redis 없음 + PG 없음) │ │ │ │ 대응: │ │ - TTL > 예상 최대 sync 지연 (현재 24h >> 2s) │ │ - Sync lag 모니터링 + 알림 │ └─────────────────────────────────────────────────────────────────┘
5. 비교 분석
5.1 아키텍처 비교
┌─────────────────────────────────────────────────────────────────┐ │ Option A: PgBouncer (Pure PostgreSQL) │ │ │ │ LangGraph ─→ PostgresCheckpointer ─→ PgBouncer ─→ PostgreSQL │ │ │ │ │ │ 동기 write 연결 다중화 │ │ ~5ms pool=20 │ │ │ │ - 일관성: Strong (즉시 PG에 저장) │ │ - 복잡도: 낮음 (인프라 추가만) │ │ - 연결 문제: 해결 (다중화) │ │ - latency: 여전히 5ms/write (TX mode도 TX 중에는 점유) │ └─────────────────────────────────────────────────────────────────┘ ┌─────────────────────────────────────────────────────────────────┐ │ Option B: Redis Write-Behind (현재 구현) │ │ │ │ LangGraph ─→ ReadThroughCheckpointer │ │ │ │ │ ├─ Write: Redis (~1ms) + queue │ │ └─ Read: Redis (hit) or PG (miss→promote) │ │ │ │ │ CheckpointSyncService ─┴─→ PostgreSQL │ │ (batch, async) (archive) │ │ │ │ - 일관성: Eventual (async sync) │ │ - 복잡도: 높음 (3개 컴포넌트) │ │ - 연결 문제: 근본 해결 (PG 연결 최소화) │ │ - latency: 1ms/write (PG 완전 분리) │ └─────────────────────────────────────────────────────────────────┘5.2 정량 비교
항목 PgBouncer Redis Write-Behind Write Latency ~5ms ~1ms Read Latency (hot) ~5ms ~1ms Read Latency (cold) ~5ms ~10-50ms (PG + promote) PG 연결 수 20 (고정) 5 (batch sync only) 동시 처리 가능 ~200 req/s* ~1000+ req/s* 일관성 Strong Eventual (~2s) 구현 복잡도 낮음 높음 운영 복잡도 낮음 중간 장애 복구 단순 (PG 기준) 복잡 (reconciliation) 추가 인프라 PgBouncer Redis Cluster *환경에 따라 다릅니다. Eco²의 Redis Primary, Postgres Sync 모델 기준으로 작성됐습니다.
5.3 워크로드별 적합성
워크로드 PgBouncer Redis Write-Behind 짧은 TX + 높은 동시성 ✅ 적합 ✅ 최적 긴 TX ✅ 적합 ⚠️ sync 지연 Strong Consistency 필수 ✅ 적합 ❌ 부적합 극한 write throughput ⚠️ PG 한계 ✅ Redis 흡수 운영 단순성 ✅ 적합 ⚠️ 복잡 LLM 파이프라인 (긴 idle) ⚠️ TX 중 점유 ✅ 최적 KEDA 스케일링 ⚠️ 연결 폭발 ✅ 영향 없음 5.4 LangGraph Checkpoint 벤치마크 참고
Operation Redis PostgreSQL 비율 Get Checkpoint 2,950 ops/s 1,038 ops/s 2.8x List Checkpoints 696 ops/s 695 ops/s 1.0x Put Checkpoint 1,647 ops/s N/A - Fanout-100 846ms 1,959ms 2.3x Fanout-500 4,578ms 9,997ms 2.2x Redis가 read에서 2.8x, fanout 패턴에서 2.2x 빠릅니다. 특히 Send API를 사용하는 LangGraph 패턴에서 유리합니다.
6. 결론 및 권장사항
6.1 현재 Eco² 구현 평가
✅ 장점: - LangGraph Send API 병렬 패턴에 최적화 - PG 연결 수 근본적 해결 (15 → 5) - KEDA 스케일링에 안전 - Write latency 5x 개선 (5ms → 1ms) - Read-Through로 cold start 처리 ⚠️ 주의점: - Eventual Consistency (최대 2초 sync 지연) - Redis 장애 시 checkpoint 유실 가능 - 운영 복잡도 증가 (3개 컴포넌트)6.2 권장 개선사항
1. 모니터링 강화: - sync_queue 길이 메트릭 - sync lag 메트릭 (queue 체류 시간) - promote 횟수/miss 횟수 비율 2. Redis 내구성: - Redis Cluster (HA) - AOF 영속화 (fsync=everysec) - 또는 Redis Enterprise 3. 서비스 쿼리용 PgBouncer: - Checkpoint은 현재 구조 유지 - Auth/Users/Scan 등 서비스 쿼리에 PgBouncer 적용 - HPA 스케일링 안전성 확보6.3 PgBouncer 단독 사용이 적합한 경우
- Strong Consistency를 지향 - Redis 인프라 추가 불가 - 운영 복잡도 최소화 필요
7. References
PgBouncer
- PgBouncer Official Documentation
- PgBouncer Configuration Reference
- PgBouncer Features
- Demystifying PgBouncer: Transaction Pooling Source Code
- Heroku PgBouncer Best Practices
- Azure Multi-PgBouncer Architecture
PostgreSQL Connection Pooler 비교
- PgBouncer vs Pgcat vs Odyssey Comparison 2025
- PgBouncer vs Pgpool-II - EDB
- PostgreSQL EU Conference 2024 - Comparing Poolers
LangGraph Checkpoint
- LangGraph Redis Checkpoint 0.1.0 Redesign
- LangGraph Persistence Documentation
- langgraph-checkpoint-postgres PyPI
- langgraph-redis GitHub
Caching Patterns
'이코에코(Eco²) > Agent' 카테고리의 다른 글
이코에코(Eco²) LLM Precision 종합 리포트 (0) 2026.02.26 Agent Eval Pipeline: Swiss Cheese Grader 구현 리포트 (0) 2026.02.10 이코에코(Eco²) Agent: LangSmith Telemetry 토큰 추적 보강 (0) 2026.01.27 이코에코(Eco²) Agent: SSE Shard 기반 Redis Pub/Sub 연결 최적화 (1) 2026.01.25 이코에코(Eco²) Agent: OpenAI Agents SDK 전환 및 LLM Client 보강, E2E 검증 (1) 2026.01.25