- Published on
AutoGPT 메모리 누수? Redis+pgvector 회수법
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
AutoGPT를 장시간 돌리다 보면 프로세스 RSS가 천천히 올라가고, Redis 메모리와 Postgres 디스크가 같이 불어나면서 결국 OOM 또는 성능 저하로 이어지는 경우가 많습니다. 흔히 “AutoGPT 메모리 누수”라고 부르지만, 실제로는 애플리케이션 힙 누수보다 상태 저장소(Redis)와 벡터 저장소(pgvector)의 누적 데이터가 회수되지 않는 문제가 더 자주 원인입니다.
이 글에서는 Redis와 pgvector(PostgreSQL) 조합에서 회수(garbage collection) 전략을 어떻게 설계하고, 운영 중에 어떻게 점검·자동화할지 정리합니다. 목표는 두 가지입니다.
- Redis는 “필요한 것만 남기고 자동 만료”되게 만들기
- pgvector는 “벡터를 삭제해도 디스크와 인덱스가 실제로 회수”되게 만들기
관련해서 TTL/압축 접근을 먼저 보고 싶다면 다음 글도 함께 참고하세요.
“누수”처럼 보이는 대표 증상과 원인 지도
1) Redis 메모리가 계속 증가
- 작업 큐(리스트/스트림)가 소비 속도보다 생산 속도가 빠름
- 세션/에이전트 상태 키가 TTL 없이 남아 무한 누적
- 큰 값(대화 히스토리, tool 결과 원문)을 그대로 Redis에 저장
maxmemory-policy가 기본값이거나, eviction이 기대대로 동작하지 않음
2) Postgres 디스크가 계속 증가
- 임베딩(벡터) 레코드가 계속 insert 되고 삭제/만료 정책이 없음
- 삭제는 했지만 테이블 bloat 때문에 디스크 사용량이 줄지 않음
- ivfflat/hnsw 인덱스가 커지고, 유지보수(VACUUM/REINDEX)가 부족
3) AutoGPT 프로세스 RSS 증가
- Python 객체 캐시/메모리 풀 특성상 반환이 OS로 안 내려가는 경우
- 프롬프트/툴 결과를 너무 크게 유지(로그, history)
- 동시 작업 수가 늘며 in-memory 버퍼가 증가
이 글의 초점은 1)과 2)입니다. 프로세스 RSS 자체는 “진짜 누수”가 아니라도 운영 안정성에 영향을 주므로, 마지막에 보조 팁도 덧붙입니다.
Redis 회수법: TTL, 트리밍, 메모리 정책을 한 세트로
핵심 원칙
- 상태성 키는 무조건 TTL: 세션, 에이전트 스냅샷, intermediate 결과
- 큐/스트림은 길이 제한: 처리 실패로 적체될 때 상한이 필요
- 큰 payload는 Redis에 오래 두지 않기: 원문은 오브젝트 스토리지나 Postgres로 내리고 Redis에는 포인터만
1) TTL을 “기본값”으로 강제하는 패턴
Redis 자체에 “모든 키 기본 TTL” 같은 전역 기능은 없습니다. 그래서 애플리케이션 레벨에서 다음 중 하나로 강제합니다.
- 키를 만들 때 항상
SETEX또는SET ... EX사용 - 해시를 쓸 경우
HSET후EXPIRE를 트랜잭션으로 묶기 - Lua 스크립트로 “쓰기+TTL”을 원자적으로 보장
예시: 세션 상태 저장을 원자적으로
-- save_session.lua
-- KEYS[1] = session_key
-- ARGV[1] = ttl_seconds
-- ARGV[2] = json_payload
redis.call('SET', KEYS[1], ARGV[2], 'EX', ARGV[1])
return 1
Node/Python 어디서든 EVALSHA로 호출해 “TTL 없는 상태 키”가 생기지 않게 막습니다.
2) Streams/Lists 적체를 막는 트리밍
AutoGPT 파이프라인에서 작업 큐를 Redis Streams로 운용하면, 소비가 멈췄을 때 스트림이 무한히 커질 수 있습니다. 이때는 생산 단계에서 MAXLEN 트리밍을 같이 걸어야 합니다.
예시: XADD에 approximate 트리밍
redis-cli XADD autogpt:jobs MAXLEN ~ 100000 * type "plan" payload "..."
~는 approximate trimming이라 성능이 좋습니다.- 운영에서 “최대 몇 건까지 보관할지”는 재처리 요구사항과 디스크/메모리 예산으로 결정합니다.
List 기반이면 LPUSH/RPUSH 후 LTRIM을 묶거나, 아예 Streams로 전환하는 편이 관측/운영이 쉽습니다.
3) maxmemory와 eviction 정책을 현실적으로 설정
TTL만으로는 부족합니다. 장애/버그로 TTL이 누락되면 Redis는 결국 메모리를 다 먹습니다. 따라서 Redis에는 최후의 안전장치가 필요합니다.
권장 체크리스트:
maxmemory를 VM/컨테이너 메모리의 일정 비율로 고정- 정책은 캐시 성격이면
allkeys-lru또는allkeys-lfu - TTL 기반 세션이 많으면
volatile-ttl또는volatile-lru도 고려
예시: redis.conf 주요 항목
maxmemory 8gb
maxmemory-policy allkeys-lru
activedefrag yes
lazyfree-lazy-eviction yes
lazyfree-lazy-expire yes
activedefrag는 메모리 단편화가 심할 때 도움이 됩니다.lazyfree-*는 큰 키 삭제 시 메인 스레드 블로킹을 줄입니다.
4) 점검 명령: “무엇이 커지고 있는지”부터 확인
운영 중에 아래를 주기적으로 확인하면, 누적의 주범을 빨리 찾습니다.
redis-cli INFO memory
redis-cli INFO stats
redis-cli --bigkeys
redis-cli --memkeys --samples 10000
--bigkeys는 큰 키 유형을 빠르게 보여주지만, 샘플링 기반이라 100% 정확하진 않습니다.--memkeys는 키별 메모리 추정치를 보여주며, 분석에 유용합니다.
pgvector 회수법: “삭제”와 “회수”는 다르다
PostgreSQL에서 DELETE는 행을 지우는 게 아니라 **죽은 튜플(dead tuples)**을 남깁니다. 디스크를 실제로 줄이려면 VACUUM/REINDEX 같은 유지보수가 필요합니다. 벡터 테이블은 삽입이 많고(임베딩), 삭제도 주기적으로 발생(TTL/만료)하므로 bloat가 빠르게 커질 수 있습니다.
1) 보관정책부터: TTL 컬럼과 배치 삭제
가장 단순하고 운영 친화적인 방식은 다음입니다.
created_at또는expires_at컬럼을 둔다- 만료된 레코드를 배치로 삭제한다
- 삭제 배치가 너무 커지지 않게 “조각내기”한다
예시 스키마
CREATE TABLE autogpt_memory (
id bigserial PRIMARY KEY,
agent_id text NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
expires_at timestamptz,
content text,
embedding vector(1536)
);
CREATE INDEX autogpt_memory_expires_at_idx
ON autogpt_memory (expires_at);
CREATE INDEX autogpt_memory_agent_id_idx
ON autogpt_memory (agent_id);
예시: 만료 데이터 배치 삭제(조각내기)
WITH doomed AS (
SELECT id
FROM autogpt_memory
WHERE expires_at IS NOT NULL
AND expires_at < now()
ORDER BY id
LIMIT 5000
)
DELETE FROM autogpt_memory m
USING doomed d
WHERE m.id = d.id;
이 쿼리를 크론으로 1분마다 돌리거나, 워커가 유휴 시간에 반복 실행하는 식으로 운영합니다.
2) 파티셔닝으로 “드롭이 곧 회수”가 되게
만료 개념이 명확하고 기간 기반이라면, 파티셔닝이 가장 강력합니다.
- 월/주 단위 파티션을 만든다
- 만료된 기간 파티션은
DROP TABLE로 제거한다 DROP은 즉시 디스크를 회수한다(대부분의 경우)
예시: 월 단위 파티션(개념 예시)
CREATE TABLE autogpt_memory (
id bigserial,
agent_id text NOT NULL,
created_at timestamptz NOT NULL,
content text,
embedding vector(1536)
) PARTITION BY RANGE (created_at);
CREATE TABLE autogpt_memory_2026_02
PARTITION OF autogpt_memory
FOR VALUES FROM ('2026-02-01') TO ('2026-03-01');
운영 포인트:
- 파티션 생성 자동화(다음 달 미리 생성)
- 쿼리가 파티션 프루닝을 타도록
created_at조건을 넣는 습관
3) VACUUM 전략: autovacuum 튜닝과 수동 VACUUM
삭제를 배치로 한다면 autovacuum이 따라오지 못할 수 있습니다. 벡터 테이블은 크기가 커지기 쉬워 기본 설정이 부족한 경우가 많습니다.
테이블 단위 autovacuum 튜닝 예시
ALTER TABLE autogpt_memory SET (
autovacuum_vacuum_scale_factor = 0.02,
autovacuum_analyze_scale_factor = 0.02,
autovacuum_vacuum_threshold = 5000,
autovacuum_analyze_threshold = 5000
);
- 스케일 팩터를 낮춰 “조금만 바뀌어도” vacuum이 돌게 합니다.
- 실제 값은 테이블 크기/쓰기 패턴에 맞춰 조정해야 합니다.
수동으로는 다음을 고려합니다.
VACUUM (ANALYZE)는 bloat를 줄이진 않지만 dead tuples 정리와 통계 갱신에 좋습니다.- 디스크를 실제로 줄이려면
VACUUM FULL이 필요하지만, 테이블 락이 크고 운영 부담이 큽니다.
예시: 야간 작업으로 통계 갱신
VACUUM (ANALYZE) autogpt_memory;
4) 인덱스 회수: REINDEX와 ivfflat/hnsw 고려
pgvector 인덱스는 크기가 빠르게 커질 수 있습니다. 특히 삭제가 많으면 인덱스도 비대해집니다.
- 정기적으로
REINDEX INDEX또는REINDEX TABLE을 검토 - 대규모 삭제 후에는 인덱스 재구성이 효과적
예시
REINDEX TABLE autogpt_memory;
주의:
- REINDEX는 부하가 크므로 트래픽이 낮은 시간대에 수행하고, 가능하면 리드 레플리카/블루그린으로 우회합니다.
Redis+pgvector “회수 루틴”을 운영 자동화로 묶기
1) 매일/매시간 실행할 작업 정의
권장 스케줄 예시:
- 1분마다: pg 만료 레코드 5천 건씩 삭제(조각내기)
- 10분마다: Redis Streams 길이 점검 및 필요 시 트리밍(생산단에서 이미 하면 생략)
- 1시간마다:
VACUUM (ANALYZE)는 상황에 따라(쓰기/삭제가 많을 때) - 매일 새벽: 인덱스 bloat 점검, 필요 시 REINDEX(주 1회)
2) systemd 타이머로 회수 잡 안정적으로 돌리기
크론도 가능하지만, 실패 재시도/로그 관리는 systemd가 편합니다. 무한 재시작 루프가 걱정된다면 아래 글도 참고하세요.
예시: 만료 삭제 스크립트 서비스
# /etc/systemd/system/autogpt-pg-reclaim.service
[Unit]
Description=AutoGPT pgvector reclaim job
[Service]
Type=oneshot
User=autogpt
ExecStart=/usr/local/bin/autogpt_pg_reclaim.sh
# /etc/systemd/system/autogpt-pg-reclaim.timer
[Unit]
Description=Run AutoGPT pgvector reclaim every minute
[Timer]
OnCalendar=*:0/1
Persistent=true
[Install]
WantedBy=timers.target
예시 스크립트(실패 시 안전하게 종료)
set -euo pipefail을 쓰되 예외 처리 설계를 해두는 게 좋습니다.
#!/usr/bin/env bash
set -euo pipefail
DB_URL="${DB_URL:?DB_URL is required}"
psql "$DB_URL" -v ON_ERROR_STOP=1 <<'SQL'
WITH doomed AS (
SELECT id
FROM autogpt_memory
WHERE expires_at IS NOT NULL
AND expires_at < now()
ORDER BY id
LIMIT 5000
)
DELETE FROM autogpt_memory m
USING doomed d
WHERE m.id = d.id;
SQL
set -euo pipefail 사용 시 함정과 안전한 예외 처리 패턴은 다음 글이 도움이 됩니다.
관측 지표: “회수되고 있는지”를 숫자로 확인
Redis
used_memory,used_memory_rss,mem_fragmentation_ratio- 키 수:
DBSIZE - 스트림 길이:
XLEN - eviction 발생:
INFO stats의evicted_keys
예시
redis-cli DBSIZE
redis-cli XLEN autogpt:jobs
redis-cli INFO memory | sed -n '1,80p'
Postgres
- 테이블/인덱스 크기
- dead tuples 비율
- autovacuum 동작 여부
예시
SELECT
relname,
n_live_tup,
n_dead_tup,
last_autovacuum,
last_vacuum
FROM pg_stat_user_tables
WHERE relname = 'autogpt_memory';
SELECT
pg_size_pretty(pg_total_relation_size('autogpt_memory')) AS total_size,
pg_size_pretty(pg_relation_size('autogpt_memory')) AS table_size;
n_dead_tup가 계속 높게 유지되면 삭제 정책만 있고 vacuum이 못 따라오는 신호입니다.
자주 하는 실수 5가지
- Redis에 TTL을 “나중에” 걸기: 예외 경로에서 TTL 누락이 발생합니다. 쓰기와 TTL을 원자적으로 보장하세요.
- 스트림/큐 무한 성장 방치: 소비자 장애는 반드시 옵니다. 상한을 둬야 합니다.
- pgvector 삭제만 하고 끝: 삭제 후 bloat/인덱스 비대가 남습니다. vacuum/reindex까지가 회수입니다.
- 대량 삭제를 한 번에 실행: 락/IO 스파이크로 장애를 부릅니다. 반드시 배치로 쪼개세요.
- 관측 없이 “감”으로 튜닝: Redis는
--bigkeys, Postgres는pg_stat_user_tables부터 보세요.
보너스: 프로세스 RSS가 줄지 않을 때의 현실적인 대응
Redis/pgvector를 정리했는데도 AutoGPT 프로세스 RSS가 내려오지 않는 경우가 있습니다. Python 메모리 할당자 특성상 OS로 반환이 즉시 안 될 수 있고, 장기 실행 워커는 파편화가 누적되기도 합니다.
실전 대응은 보통 다음 중 하나입니다.
- 워커를 “일정 작업 수/시간 후 재시작”하는 수명 정책(예: 6시간 또는 500 작업)
- 동시성 제한(작업자 수, tool 호출 수)
- 프롬프트/히스토리 압축 또는 요약 저장(원문 보관 위치 분리)
이때 중요한 건 “재시작이 장애가 되지 않도록” 설계하는 것입니다. 큐 기반으로 idempotent하게 만들고, 재시작이 정상 운영의 일부가 되게 하면 RSS 문제는 훨씬 덜 위협적입니다.
결론: 회수는 기능이 아니라 운영 루틴이다
AutoGPT에서 메모리 누수처럼 보이는 문제는 대개 Redis와 pgvector에 쌓이는 상태/임베딩 데이터가 정책 없이 누적되기 때문입니다. 해결은 단발성 최적화가 아니라, 다음을 한 세트로 묶는 것입니다.
- Redis: TTL 강제 + 큐/스트림 트리밍 +
maxmemory안전장치 - pgvector: 보관정책(TTL/파티션) + 배치 삭제 + VACUUM/REINDEX로 실제 회수
- 관측: “누가 커지는지”를 지표로 확인하고 자동화로 굳히기
이 루틴을 갖추면 “메모리 누수” 디버깅에 쓰던 시간을, 에이전트 품질과 비용 최적화에 다시 투자할 수 있습니다.