본문으로 건너뛰기

[hermes-agent] SQLite WAL 모드를 활용한 Read-Path 최적화: Lock Contention 해결하기

PR 링크: NousResearch/hermes-agent#73344 상태: Merged | 변경: +336 / -47

들어가며

많은 Python 애플리케이션에서 sqlite3를 사용할 때, 단일 커넥션에 의존하며 threading.Lock을 통해 동시성 문제를 해결하곤 합니다. 하지만 Hermes-agent와 같이 읽기 작업이 빈번한 환경에서는, 쓰기 작업이 진행되는 동안 모든 읽기 작업이 self._lock에서 대기(convoy)하게 되어 심각한 성능 저하가 발생합니다. 본 PR은 SQLite의 WAL(Write-Ahead Logging) 모드를 활용하여, 읽기 전용 커넥션을 스레드별로 분리함으로써 쓰기 작업과 읽기 작업이 서로를 차단하지 않도록 최적화한 사례를 다룹니다.

코드 분석

1. Read-Path 분리 및 스레드별 커넥션 관리 (hermes_state.py)

핵심은 threading.local()을 사용하여 각 스레드가 자신만의 읽기 전용 커넥션을 갖게 하는 것입니다. 이를 통해 self._lock을 거치지 않고도 안전하게 데이터를 조회할 수 있습니다.

# Before: 모든 읽기 작업이 self._lock을 점유
with self._lock:
    cursor = self._conn.execute("SELECT * FROM sessions WHERE id = ?", (session_id,))

# After: _read_ctx()를 통해 WAL 모드에서 Lock 없이 읽기 수행
with self._read_ctx() as conn:
    cursor = conn.execute("SELECT * FROM sessions WHERE id = ?", (session_id,))

_get_read_conn 메서드는 mode=ro URI를 사용하여 읽기 전용 커넥션을 생성하며, _read_conns 세트를 통해 모든 활성 커넥션을 추적합니다. 이는 리소스 누수를 방지하고, close() 호출 시 모든 커넥션을 안전하게 정리하기 위함입니다.

2. 라이프사이클 및 Race Condition 방지

리뷰 과정에서 지적된 close() 호출 시점과 커넥션 생성 간의 Race Condition을 해결하기 위해 _read_conns_closed 플래그를 도입했습니다.

# _get_read_conn 내부의 안전 장치
with self._read_conns_lock:
    if self._read_conns_closed:
        conn.close()
        self._read_local.failed = True
        return None
    self._read_conns.add(conn)

왜 이게 좋은가

  1. Lock Contention 제거: WAL 모드에서는 읽기와 쓰기가 서로를 차단하지 않습니다. 기존에는 쓰기 작업이 완료될 때까지 모든 읽기 요청이 self._lock에서 대기해야 했으나, 이제는 각 스레드가 독립적으로 데이터를 조회할 수 있어 처리량(throughput)이 비약적으로 향상되었습니다.
  2. 리소스 관리: WeakSet 대신 강한 참조를 가진 set을 사용하고, close() 시점에 명시적으로 커넥션을 정리함으로써 파일 디스크립터(fd) 누수 문제를 완벽히 해결했습니다.
  3. 안정성: _read_conns_closed 플래그를 통해 종료 과정에서 발생하는 예기치 않은 커넥션 생성을 방지하여, 시스템 종료 시점의 안정성을 확보했습니다.

이러한 최적화는 데이터베이스 접근이 잦은 고성능 에이전트 시스템에서 필수적인 패턴입니다. 특히 SQLite를 사용할 때 WAL 모드와 스레드별 커넥션 전략을 결합하면, 단일 커넥션의 한계를 극복하고 동시성 성능을 극대화할 수 있습니다.

참고 자료

⚠️ 알림: 이 분석은 AI가 실제 코드 diff를 기반으로 작성했습니다.

댓글

관련 포스트

PR Analysis 의 다른글