PgBouncer 뒤의 Chatbot 체크포인터 단일 연결
증상
chatbot-svc가 SSE 요청을 수락했지만 오랜 유휴 기간 후 즉시 일반 챗봇 오류를 내보내고 graph.astream(...)에 진입하는 동안 워커 로그에 psycopg.OperationalError: the connection is closed가 표시되는 경우 이 페이지를 사용하세요.
이 실패 모드는 백엔드의 대화 테이블이 아닌 LangGraph 체크포인트에 영향을 미쳤습니다. 워커가 그래프 실행을 시작하거나 재개할 수 없는 동안에도 사용자 대상 채팅 기록은 그대로 유지될 수 있습니다.
왜 이런 일이 일어나는가
원래 스캐폴드는 포드 수명 동안 하나의 psycopg.AsyncConnection를 보유하는 AsyncPostgresSaver.from_conn_string(dsn)를 사용했습니다.
업스트림 풀러와 네트워크가 유휴 연결을 회전하거나 닫을 수 있기 때문에 이 패턴은 CNPG 풀러 뒤에서 취약합니다. 해당 싱글톤 핸들이 오래되면 그래프가 스트리밍하거나 재개하기 전에 다음 aget_tuple(...)가 실패합니다.
lumie-worker/services/chatbot/main.py의 현재 진입점은 해당 회귀를 방지하기 위해 특별히 존재합니다.
진단 경로
다음 신호를 함께 확인하세요.
- 브라우저 또는 백엔드가
/api/chatbot/stream를 성공적으로 열었습니다. lumie-worker/services/chatbot/src/usecase.py는graph run failed를 기록했습니다.- 역추적은
langgraph.checkpoint.postgres.aio또는psycopg에서the connection is closed로 끝납니다. - 영향을 받은 이미지 또는 로컬 실험은
main.py의 풀링된 체크포인터를 사용하지 않습니다.
lumie-worker/services/chatbot/main.py의 대표적인 모양:
pool = AsyncConnectionPool(
conninfo=settings.langgraph_db_dsn,
min_size=1,
max_size=5,
max_idle=180.0,
check=AsyncConnectionPool.check_connection,
kwargs={
"autocommit": True,
"prepare_threshold": 0,
"row_factory": dict_row,
},
)
checkpointer = AsyncPostgresSaver(pool)
중요한 가드레일은 다음과 같습니다.
| 설정 | 왜 중요한가 |
|---|---|
check=AsyncConnectionPool.check_connection | 유인물 전에 핸들의 유효성을 검사하여 그래프가 사용하기 전에 끊어진 연결을 교체합니다. |
max_idle=180.0 | 업스트림 풀러보다 먼저 워커가 유휴 연결을 종료할 수 있도록 합니다 |
prepare_threshold=0 | psycopg가 트랜잭션 풀링된 PgBouncer 동작과 호환되도록 유지 |
row_factory=dict_row | AsyncPostgresSaver의 행별 액세스와 일치 |
수정
수명이 긴 워커에서는 싱글톤 체크포인트 연결을 유지하지 마 세요.
영구적인 수정 사항은 lumie-worker/services/chatbot/main.py의 현재 패턴입니다.
- FastAPI 수명 동안
AsyncConnectionPool를 엽니다. - 그래프를 컴파일하기 전에 풀을 기다립니다.
- 풀을
AsyncPostgresSaver에 전달합니다. - 진행 중인 체크포인트 쓰기가 완료될 수 있도록
usecase.aclose()이후에만 풀을 닫습니다.
이전 이미지에서 이 오류가 표시되는 경우 롤아웃을 다시 시작하면 새로운 연결을 강제하여 일시적으로 서비스를 복원할 수 있습니다.
kubectl rollout restart deployment -n lumie-worker chatbot-svc
그것은 단지 완화일 뿐입니다. 풀링된 패턴이 없으면 다음 유휴 기간 이후 동일한 Pod가 다시 실패할 수 있습니다.
예방
- 요청 전반에 걸쳐 LangGraph 또는 psycopg 상태를 열어두는 워커의 참조 구현으로
lumie-worker/services/chatbot/main.py를 취급합니다. - 라이브러리 문서에서 재연결 동작을 명시적으로 다루지 않는 한 장기 실행 서비스에서는
from_conn_string(...)와 같은 README 빠른 시작 패턴을 피하세요. - 디버깅할 때 체 크포인터 경로를 사용자 메시지 지속성과 별도로 유지하여 그래프 상태 중단을 모놀리스의 데이터 손실로 착각하지 않도록 하세요.
분리된 SSE 워커 뒤에 있는 더 넓은 아키텍처 선택은 LangGraph Chatbot Streaming Architecture를 참조하세요.
원본 사고 세부사항
원본 사고는 2026년 5월 26일, 3시간 넘는 idle window 뒤에 발견됐습니다. 사용자가 새 production chat을 열었고 browser는 SSE response를 받았지만, worker가 _run_graph의 일반 Korean error frame을 즉시 내보냈습니다.
| 세부사항 | 값 |
|---|---|
| 영향 서비스 | lumie-worker/services/chatbot |
| 실패한 persistence path | langgraph schema의 LangGraph checkpointer |
| 안전했던 persistence path | backend-owned ai_conversations, ai_messages row |
| 즉시 보인 오류 | psycopg.OperationalError: the connection is closed |
| 완화 | kubectl rollout restart deployment -n lumie-worker chatbot-svc |
| 지속 수정 | lifetime connection 하나를 AsyncConnectionPool과 check_connection으로 교체 |
핵심 timeline은 phase 기준입니다.
| Phase | Event |
|---|---|
| Phase 1 | Phase 4 cutover smoke는 통과했고, 이후 cleanup 동안 chatbot이 idle 상태로 있었습니다. |
| Phase 2 | 약 3h24m 뒤 다음 graph run이 AsyncPregelLoop에 들어가 checkpointer.aget_tuple(...)를 호출했습니다. |
| Phase 3 | Singleton psycopg connection은 이미 pooler/network idle path에서 닫혀 있었습니다. |
| Phase 4 | Rollout restart가 fresh pod를 만들고 service를 빠르게 복구했습니다. |
| Phase 5 | 영구 수정은 checkpointer를 pre-handoff health check가 있는 pool로 옮겼습니다. |
운영상 중요한 구분은 사용자 메시지 persistence가 손상되지 않았다는 점입니다. 죽은 handle은 graph checkpoint path에 있었고, conversation row를 저장하는 backend JPA persistence path에는 있지 않았습니다.
앞으로 worker scaffold에는 다음 후속 조건을 붙여야 합니다.
| 후속 조건 | 상태 |
|---|---|
| Long-running worker service에는 pooled DB client를 사용합니다. | 필수 패턴 |
PgBouncer transaction pooling 뒤에서는 prepare_threshold=0을 설정합니다. | psycopg 필수 조건 |
Shutdown 중에는 usecase.aclose() 이후 pool을 닫습니다. | in-flight checkpoint write 완료에 필요 |