- Published on
AutoGPT 메모리 폭주 해결 - SQLite→Redis TTL
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
AutoGPT를 장시간 돌리다 보면 어느 순간부터 RAM 사용량이 계속 증가하고, 디스크 IO도 함께 치솟아 결국 프로세스가 OOM으로 죽거나 시스템 전체가 느려지는 경우가 있습니다. 겉으로는 "LLM이 대화를 길게 끌어서"처럼 보이지만, 실제로는 에이전트의 메모리 저장소 구현(특히 SQLite 기반)과 검색/적재 패턴이 맞물리면서 메모리 폭주와 디스크 병목이 동시에 발생하는 일이 흔합니다.
이 글에서는 AutoGPT(또는 유사 에이전트 프레임워크)에서 자주 보이는 SQLite 메모리 저장소의 한계를 짚고, Redis로 전환하면서 TTL(Time To Live)로 메모리 상한을 강제하는 방식으로 문제를 해결하는 방법을 단계별로 정리합니다. 추가로 “TTL만 걸면 끝”이 아니라, 요약·샘플링·세션 스코프 설계까지 같이 해야 재발을 막을 수 있습니다.
증상: RAM과 디스크가 같이 폭주하는 패턴
다음 중 2개 이상이 동시에 나타나면, SQLite 메모리 저장소가 병목이거나 폭주의 촉매일 가능성이 큽니다.
- 프로세스 RSS가 서서히 증가하다가 특정 시점 이후 급격히 증가
- 디스크 사용량이 빠르게 늘거나, 디스크 IO wait가 상승
- SQLite DB 파일이 지속적으로 커짐
- 동일한 키워드 검색/리콜을 반복할수록 응답이 느려짐
- 컨테이너 환경에서는 overlay2 레이어가 커지며 노드가 불안정해짐
특히 “메모리”라고 부르지만, 실제 구현은 대개 다음과 같은 루프를 탑니다.
- 작업 로그/관찰/중간 산출물들을 계속 저장
- 다음 프롬프트를 만들 때 과거 기록을 검색
- 검색 결과를 다시 프롬프트에 붙여 넣으면서 토큰이 증가
- 토큰 증가를 막기 위해 요약을 생성
- 요약 또한 다시 저장
이 과정이 SQLite에 누적되면, DB는 커지고, 검색은 느려지고, 애플리케이션은 더 많은 결과를 한 번에 읽어 오면서 RAM 사용량이 올라갑니다.
원인: SQLite 기반 메모리의 구조적 한계
SQLite는 “단일 파일 DB”로 단순하고 배포가 쉽지만, 에이전트 메모리처럼 쓰기 빈도가 높고, 짧은 키로 자주 조회하며, 오래된 데이터를 빠르게 버려야 하는 워크로드에는 불리합니다.
1) 삭제가 곧바로 파일 크기를 줄이지 않음
SQLite에서 DELETE를 해도 파일 크기가 즉시 줄지 않습니다(페이지 재사용은 되지만 OS 관점에서 파일은 그대로). 장시간 실행할수록 DB 파일이 계속 커지는 것처럼 보일 수 있습니다.
2) 인덱스/쿼리 설계가 조금만 어긋나도 풀스캔
메모리 검색을 LIKE나 단순 텍스트 매칭으로 구현했거나, 메타데이터 인덱스가 부실하면 쿼리 시간이 기하급수로 늘어납니다.
3) “가져오고 나서 필터링” 패턴이 RAM을 먹음
DB에서 많이 읽어 와서 애플리케이션 레벨에서 필터링/정렬하면, 순간적으로 큰 리스트가 만들어지고 GC 압박과 함께 RSS가 튑니다.
4) 동시성 및 잠금 경합
여러 스레드/태스크가 동시에 쓰기(로그, 요약, 관찰)를 수행하면 SQLite는 잠금 경합이 생기기 쉽고, 재시도 로직이 있으면 더 많은 버퍼/큐가 쌓입니다.
해결 전략: Redis로 옮기고 TTL로 “상한”을 강제
핵심은 두 가지입니다.
- 메모리 저장소를 Redis로 전환해 접근 비용을 낮추고
- TTL로 데이터 생명주기를 강제해서 “무한 누적”을 원천 차단
Redis는 인메모리 기반이지만, TTL과 eviction 정책이 강력하고, 최근 데이터 중심의 접근에 최적화되어 있습니다. 에이전트 메모리는 대부분 “최근 컨텍스트”가 중요하므로 Redis가 잘 맞습니다.
어떤 데이터를 TTL로 관리할까
추천 스코프는 아래처럼 나눕니다.
- 세션 단위 컨텍스트(최근 대화/관찰): TTL 30분~6시간
- 작업 단위 중간 산출물(임시 계획/초안): TTL 10분~2시간
- 장기 지식(검증된 요약/규칙): TTL 없음 또는 매우 길게(예: 30일)
중요한 건 모든 것을 장기로 두지 않는 것입니다. 에이전트가 만들어 낸 중간 결과물은 대부분 재사용 가치가 낮고, 오히려 다음 루프에서 잡음을 늘립니다.
구현 1: Redis TTL 기반 메모리 저장소 설계
가장 단순한 형태는 session:{id}:events 같은 키에 이벤트를 쌓고, 키 자체에 TTL을 거는 방식입니다.
데이터 모델 예시
session:{sessionId}:events: 리스트(List) 또는 스트림(Stream)session:{sessionId}:summary: 최신 요약 문자열session:{sessionId}:kv:{name}: 작업 중 임시 KV
키 네이밍에 {}가 들어가지만, 본문에서 부등호는 금지이므로 예시는 백틱으로만 표기합니다.
Python 예제: redis-py로 TTL 적용
import json
import time
import redis
r = redis.Redis(host="localhost", port=6379, db=0, decode_responses=True)
SESSION_TTL_SEC = 60 * 60 # 1 hour
def append_event(session_id: str, event: dict):
key = f"session:{session_id}:events"
payload = json.dumps({"ts": time.time(), **event}, ensure_ascii=False)
# 리스트에 추가
r.rpush(key, payload)
# 핵심: 키 TTL을 매번 갱신(세션이 살아있는 동안 유지)
r.expire(key, SESSION_TTL_SEC)
def get_recent_events(session_id: str, limit: int = 50):
key = f"session:{session_id}:events"
# 최근 N개만 사용해서 프롬프트 길이를 제한
start = max(0, r.llen(key) - limit)
items = r.lrange(key, start, -1)
return [json.loads(x) for x in items]
def set_summary(session_id: str, summary: str):
key = f"session:{session_id}:summary"
r.set(key, summary, ex=SESSION_TTL_SEC)
def get_summary(session_id: str):
return r.get(f"session:{session_id}:summary")
포인트는 두 가지입니다.
- 이벤트를 무한정 읽지 않도록
limit로 상한을 둠 - TTL을
expire혹은set(..., ex=...)로 강제해서 세션 종료 후 자동 정리
구현 2: Stream을 쓰면 “최근 N개” 유지가 더 깔끔
Redis Stream은 이벤트 로그에 적합하고, 소비자 그룹을 붙이면 비동기 파이프라인도 만들기 쉽습니다. 다만 AutoGPT 메모리 용도라면 “최근 N개만 유지”가 핵심이므로 XADD와 MAXLEN을 함께 쓰면 TTL 없이도 크기 상한을 둘 수 있습니다.
import json
import time
import redis
r = redis.Redis(host="localhost", port=6379, db=0, decode_responses=True)
def xadd_event(session_id: str, event: dict, maxlen: int = 2000):
key = f"session:{session_id}:stream"
r.xadd(
key,
{"payload": json.dumps({"ts": time.time(), **event}, ensure_ascii=False)},
maxlen=maxlen,
approximate=True,
)
r.expire(key, 60 * 60)
MAXLEN으로 “개수 상한”EXPIRE로 “시간 상한”
둘을 같이 걸면 메모리 폭주 확률이 크게 떨어집니다.
Redis 설정: eviction 정책까지 같이 잡기
TTL을 걸어도 순간적으로 트래픽이 몰리면 메모리가 찰 수 있습니다. 이때 Redis가 어떤 키를 지울지 정책이 중요합니다.
권장 조합:
maxmemory를 컨테이너/서버 환경에 맞게 설정maxmemory-policy는 TTL 키가 많다면volatile-ttl또는allkeys-lru고려
예시(redis.conf 또는 컨테이너 커맨드에서 설정):
maxmemory 2gb
maxmemory-policy volatile-ttl
volatile-ttl은 TTL이 설정된 키 중에서 만료가 가까운 키를 우선 제거합니다. 세션성 데이터가 대부분 TTL을 갖는 구조라면 안전한 선택입니다.
마이그레이션: SQLite에서 Redis로 옮길 때 체크리스트
1) “장기 기억”과 “작업 로그”를 분리
SQLite에 쌓인 모든 것을 Redis로 그대로 옮기면, Redis도 금방 터집니다.
- 장기 기억: 별도 스토리지(예: Postgres, 벡터DB, 오브젝트 스토리지)
- 단기 기억: Redis TTL
2) 프롬프트에 넣는 메모리의 양을 제한
Redis로 바꿔도, 매번 1000개 이벤트를 읽어 프롬프트에 넣으면 토큰 비용과 지연이 폭증합니다.
- 최근 N개 제한
- 중요도 스코어로 샘플링
- 요약 우선, 원문은 필요할 때만
3) 요약도 TTL 대상
요약을 계속 누적 저장하면 “요약의 요약”이 쌓여 결국 같은 문제가 재발합니다. 요약은 “최신 1개”만 유지하거나, 세션 단위로 TTL을 걸어야 합니다.
운영 관점 트러블슈팅
디스크 100%와 함께 터질 때
SQLite DB가 커지거나, 로그 파일이 삭제되어도 프로세스가 계속 점유하는 상황이 겹치면 디스크가 100%에 도달합니다. 이 경우 lsof로 삭제된 파일 점유를 확인하는 접근이 유효합니다.
프로세스가 계속 재시작될 때
OOM 또는 헬스체크 실패로 systemd가 재시작 루프에 빠지면, 원인(Exit code, ExecStart, 환경변수)을 먼저 확정해야 합니다.
LLM 호출이 많아지며 429가 늘어날 때
메모리 폭주를 막으려고 요약을 자주 만들면 LLM 호출이 증가하고, rate limit에 걸릴 수 있습니다. 요약 주기, 배치 처리, 재시도 정책을 함께 설계하세요.
“SQLite에서 Redis로 바꿨는데도” 폭주하는 경우
스토리지는 바꿨는데도 메모리가 계속 증가한다면, 대개 애플리케이션 레벨에서 아래 중 하나가 원인입니다.
- 이벤트를 읽어 온 뒤 파이썬 리스트/문자열로 계속 누적(전역 변수, 싱글톤 캐시)
- 프롬프트 빌더가 이전 프롬프트를 재사용하면서 문자열이 점점 커짐
- 요약 생성 결과를 원문과 함께 둘 다 붙여 넣는 중복
- 비동기 태스크 큐가 소비 속도를 못 따라가 backlog가 쌓임
이때는 “저장소”가 아니라 “프롬프트 조립 파이프라인”을 점검해야 합니다.
권장 아키텍처: 단기 Redis, 장기 별도 저장소
안정적으로 운영하려면 다음 형태가 가장 무난합니다.
- 단기 컨텍스트: Redis TTL + 최근 N개 제한
- 장기 지식: 별도 DB(관계형 또는 벡터 검색)
- 산출물(파일): 오브젝트 스토리지
즉, Redis는 “기억”이라기보다 세션 캐시로 취급하는 것이 핵심입니다.
마무리
AutoGPT의 메모리 폭주는 단순히 RAM이 부족해서가 아니라, “무한 누적되는 단기 데이터”를 SQLite 같은 저장소에 쌓아 두고 매번 다시 읽어 오면서 발생하는 경우가 많습니다. Redis로 전환하고 TTL과 최근 N개 제한을 걸면, 시스템은 다음을 동시에 얻습니다.
- 메모리/디스크 사용량 상한이 생김
- 조회 지연이 줄어듦
- 세션 종료 후 자동 정리로 운영 부담 감소
마지막으로, TTL은 안전장치일 뿐이고, 프롬프트에 넣는 컨텍스트의 양을 제한하는 정책(요약, 샘플링, 중복 제거)을 같이 적용해야 장시간 실행에서도 안정적으로 유지됩니다.