Published on

AutoGPT 메모리 누수? Redis+pgvector 회수법

Authors

AutoGPT를 장시간 돌리다 보면 프로세스 RSS가 천천히 올라가고, Redis 메모리와 Postgres 디스크가 같이 불어나면서 결국 OOM 또는 성능 저하로 이어지는 경우가 많습니다. 흔히 “AutoGPT 메모리 누수”라고 부르지만, 실제로는 애플리케이션 힙 누수보다 상태 저장소(Redis)와 벡터 저장소(pgvector)의 누적 데이터가 회수되지 않는 문제가 더 자주 원인입니다.

이 글에서는 Redis와 pgvector(PostgreSQL) 조합에서 회수(garbage collection) 전략을 어떻게 설계하고, 운영 중에 어떻게 점검·자동화할지 정리합니다. 목표는 두 가지입니다.

  1. Redis는 “필요한 것만 남기고 자동 만료”되게 만들기
  2. 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 사용
  • 해시를 쓸 경우 HSETEXPIRE를 트랜잭션으로 묶기
  • 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/RPUSHLTRIM을 묶거나, 아예 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 statsevicted_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가지

  1. Redis에 TTL을 “나중에” 걸기: 예외 경로에서 TTL 누락이 발생합니다. 쓰기와 TTL을 원자적으로 보장하세요.
  2. 스트림/큐 무한 성장 방치: 소비자 장애는 반드시 옵니다. 상한을 둬야 합니다.
  3. pgvector 삭제만 하고 끝: 삭제 후 bloat/인덱스 비대가 남습니다. vacuum/reindex까지가 회수입니다.
  4. 대량 삭제를 한 번에 실행: 락/IO 스파이크로 장애를 부릅니다. 반드시 배치로 쪼개세요.
  5. 관측 없이 “감”으로 튜닝: 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로 실제 회수
  • 관측: “누가 커지는지”를 지표로 확인하고 자동화로 굳히기

이 루틴을 갖추면 “메모리 누수” 디버깅에 쓰던 시간을, 에이전트 품질과 비용 최적화에 다시 투자할 수 있습니다.