느린 LLM 호출 중 DB connection을 잡지 않는 이유
AI 기능의 latency는 모델 응답 시간으로만 끝나지 않습니다. 요청이 LLM이나 embedding API를
기다리는 동안 데이터베이스 session까지 긴 범위로 유지하면, 느린 외부 호출이 DB connection
pool의 압력으로 전파될 수 있습니다.
이 글은 AI memory OSS인 Honcho의 변경 이력과 pinned source를 읽으면서, 외부 호출과 DB
transaction/session 경계를 어떻게 분리했는지 추적한 기록입니다.
Scenario
Honcho의 dialectic 경로(저장된 memory를 근거로 사용자 질문에 답하는 질의 경로)는 답하기 전에
다음 작업을 수행합니다.
peer·session·workspace 확인
-> 관련 memory 검색
-> embedding·LLM 호출
-> 필요하면 tool로 추가 조회·기록
-> 답변 생성
여기에는 짧은 DB 조회와 상대적으로 느리고 변동성이 큰 외부 호출이 섞여 있습니다. 두 작업을
하나의 session scope로 묶으면, DB가 필요하지 않은 대기 시간까지 session lifetime에 포함됩니다.
변경 전에는 무엇이 묶여 있었나
PR #477 직전의 agentic_chat은 하나의 tracked_db context 안에서 peer와 설정을 읽고,
그 session을 DialecticAgent에 전달한 뒤, agent.answer()가 끝날 때까지 같은 context를
유지했습니다. streaming 경로도 같은 형태였습니다.
구조를 단순화하면 다음과 같습니다.
DB session open
-> preflight read
-> DialecticAgent receives session
-> embedding / memory tools / LLM answer
DB session close
commit 0533c6d의 제목도 이 문제를 dialectic held connection으로 기록합니다. 다만 이번
분석에서는 실제 pool checkout 시간이나 장애를 재현하지 않았습니다. 여기서 확인한 것은 코드의
session scope와 변경 의도입니다.
SQLAlchemy의 session 객체를 만들었다고 곧바로 connection을 점유하는 것은 아닙니다. 하지만 이
경로처럼 SQL을 실행해 transaction이 시작되면 session은 pool에서 빌린 connection을
commit·rollback까지 유지합니다. Honcho의 tracked_db는 종료할 때 rollback()과 close()를
호출하므로, SQL 실행 뒤 이 context를 LLM 대기까지 유지하던 범위를 줄이는 것은 이 경로의
connection 점유 구간도 줄이는 일입니다.
어떻게 경계를 줄였나
변경 후에는 tracked_db("dialectic.preflight")가 본 작업 전 검증과 설정 조회(preflight), 즉
peer 존재 여부, session/workspace 설정, peer card를 읽는 구간만 감쌉니다. context가 끝난
다음에 DialecticAgent를 만들고 LLM 답변을 생성합니다. Agent 생성자에서도 DB session 인자가
제거됐습니다.
short DB preflight
-> 필요한 값 읽기
DB session close
agent execution
-> embedding / LLM / tools
tool needs DB
-> tool-owned short DB session
-> close
핵심은 DB 사용을 없앤 것이 아닙니다. 요청 전체가 session을 소유하는 대신, DB가 필요한
작업이 자기 범위의 session을 소유하도록 바꾼 것입니다.
pinned current code에서도 유지되는가
분석 기준 commit 85239a6에서도 이 경계는 유지되고 더 구체화돼 있습니다.
-
src/dialectic/chat.py의 일반·streaming 경로 모두 preflight context를 닫은 뒤 agent를 실행합니다. -
DialecticAgent는 DB session을 필드로 받지 않습니다. - observation prefetch는 embedding을 먼저 계산한 뒤
search_memory를 호출합니다. -
search_memory는query_documents(db=None, ...)를 사용해 session lifetime을 하위 함수에 맡깁니다. - external vector store 경로는 외부 검색으로 document ID를 얻은 후에만 짧은 DB session을 열어 row를 가져옵니다.
- PostgreSQL 안에서 vector 검색을 수행하는 pgvector 경로는 검색 자체가 DB 연산이므로 그 DB query 범위에는 session을 사용합니다.
- 읽기·쓰기 tool은 필요할 때 각자의
tracked_dbcontext를 엽니다.
따라서 transferable contract는 “외부 호출이 있으면 DB를 절대 쓰지 말라”가 아닙니다.
transaction consistency가 필요한 구간과 외부 네트워크 대기 구간을 분리하고, 각 DB 작업이
필요한 최소 session boundary를 소유하게 한다.
테스트에서 확인할 수 있는 것
PR #477은 테스트의 tool context에서도 공유 DB session을 제거했습니다. 테스트 fixture는
tool handler가 여는 독립 session에서 데이터를 볼 수 있도록 준비 데이터를 commit하고,
tracked_db를 fresh session factory로 대체합니다. 이는 tool이 요청 전체의 session에 의존하지
않는다는 계약을 테스트 구조에 반영한 것입니다.
이후 pinned code의 message search 테스트에는 external semantic lookup이 tracked_db 진입보다
먼저 발생하는지 call order로 확인하는 사례도 있습니다. 반면 이번 로컬 환경에서는 Honcho의
pytest dependency를 실행할 수 없어 테스트 코드를 정적으로만 확인했습니다.
적용할 때 같이 봐야 하는 위험
session scope를 줄이는 것에도 비용이 있습니다.
- session 종료 뒤 ORM lazy attribute에 접근하면 detached object 문제가 생길 수 있습니다.
- 하나의 transaction으로 보호해야 하는 read-modify-write를 무리하게 나누면 consistency가 깨질 수 있습니다.
- preflight와 이후 tool은 서로 다른 transaction snapshot을 볼 수 있습니다. 경계 사이의 데이터 변경을 허용해도 되는지 별도로 판단해야 합니다.
- tool마다 session을 열면 session 수는 짧아지지만 호출 횟수와 transaction 설계는 별도로 검토해야 합니다.
- pgvector처럼 외부 호출이 아니라 DB 자체에서 검색하는 경로는 DB session이 필요합니다.
그래서 순서는 “무조건 session을 빨리 닫기”가 아니라 다음과 같습니다.
1. 실제 consistency boundary를 정한다.
2. 외부 호출 전에 필요한 값을 미리 읽어 변수로 확보한다(materialize).
3. DB scope를 닫는다.
4. 느린 외부 작업을 수행한다.
5. 결과 저장이 필요하면 새롭고 짧은 write scope를 연다.
Limitations
- Honcho의 공개 코드와 변경 이력을 분석한 글이며, Honcho를 직접 구현·운영하거나 이 변경에 기여한 경험이 아닙니다.
- 분석 기준은
85239a69b262c944de3c35900b91c88ba9b84f1a로 고정했습니다. - production 부하, connection pool 고갈, connection checkout 시간을 재현하거나 benchmark하지 않았습니다.
- 테스트 파일은 확인했지만 현재 로컬 환경에는 요구 Python/pytest 조합이 없어 실행하지 못했습니다.
- 감사한 dialectic·memory search 경로 밖의 모든 외부 호출이 같은 계약을 지킨다고 주장하지 않습니다.
Sources
- Honcho repository
- PR #477 change commit
- Code immediately before the change
- Pinned dialectic preflight boundary
- Pinned
tracked_dbrollback/close boundary - Pinned observation prefetch call site
- Pinned memory search boundary
- Pinned external-vector/DB two-phase query
- Pinned independent-session test setup
- Pinned external-before-DB order test
- SQLAlchemy Session basics
Top comments (0)