#Persistence 운영 런북

버전: 0.33.0 최종 업데이트: 2026-03-15 적용 대상: ranvier-runtime 카테고리: 가이드


#1. 어댑터 선택 가이드

내구성, 지연 시간, 인프라 요구사항에 맞는 어댑터를 선택하세요.

기준 InMemoryPersistenceStore PostgresPersistenceStore RedisPersistenceStore
내구성 없음 (프로세스 범위) 완전 (ACID) 구성 가능 (RDB/AOF)
체크포인트 지연 ~1 µs ~1–5 ms ~0.1–1 ms
크래시 후 재개 ❌ (데이터 손실) ✅ (persistence 활성화 시)
멀티 프로세스 확장 ❌ (노드 로컬)
적합한 용도 단위 테스트, 로컬 개발 프로덕션 워크플로우 높은 처리량, 임시 체크포인트
피처 플래그 없음 persistence-postgres persistence-redis

#의사결정 트리

프로세스 재시작 후에도 데이터가 유지되어야 하는가?
├── 아니오 → InMemoryPersistenceStore (테스트 전용)
└── 예    → 강력한 내구성이 필요한가 (감사 추적, 보상 로그)?
            ├── 예    → PostgresPersistenceStore
            └── 아니오 → 서브밀리초 체크포인트 지연이 필요한가?
                        ├── 예    → RedisPersistenceStore
                        └── 아니오 → PostgresPersistenceStore (기본 안전 선택)

#PostgreSQL 설정

# Cargo.toml
[features]
persistence-postgres = ["ranvier-runtime/persistence-postgres"]
let pool = sqlx::PgPool::connect(&env::var("DATABASE_URL")?).await?;
let store = PostgresPersistenceStore::new(pool.clone());
store.ensure_schema().await?;   // 멱등성 — 시작 시 한 번 호출

#Redis 설정

[features]
persistence-redis = ["ranvier-runtime/persistence-redis"]
let store = RedisPersistenceStore::connect(&env::var("REDIS_URL")?).await?;

#2. 크래시 복구 시나리오

#시나리오 A: 워크플로우 실행 중 프로세스 크래시

발생 상황:

  1. Axon 파이프라인이 스텝 N을 실행 중
  2. 프로세스 크래시 발생 (OOM, 시그널, 배포 재시작)
  3. 스텝 0..N-1의 외부 부작용은 이미 커밋됨

복구 절차:

// 재시작 시 중단된 트레이스를 로드
let trace_id = "order-trace-abc123";
let persisted = store.load(trace_id).await?;

if let Some(trace) = persisted {
    if trace.completion.is_none() {
        // 마지막으로 성공한 스텝을 찾기
        let last_step = trace.events.last().map(|e| e.step).unwrap_or(0);
        let cursor = store.resume(trace_id, last_step).await?;

        // 커서 위치부터 파이프라인 재실행
        tracing::info!(
            trace_id,
            resume_from = cursor.next_step,
            "resuming interrupted workflow"
        );
        // ... 커서로 재실행
    }
}

예방: PersistenceAutoComplete(true)를 설정하면 예상치 못한 종료 경로에서도 complete()가 호출됩니다.

#시나리오 B: 체크포인트 중 데이터베이스 장애

발생 상황: 데이터베이스에 접근할 수 없어 store.append()Err를 반환합니다.

권장 패턴:

match store.append(envelope).await {
    Ok(()) => { /* 계속 진행 */ }
    Err(e) => {
        tracing::error!(
            trace_id = %envelope.trace_id,
            step = envelope.step,
            error = %e,
            "checkpoint failed — applying circuit breaker"
        );
        // 옵션 A: 파이프라인 중지 (가장 안전 — 부분 진행 없음)
        return Outcome::Fault(anyhow::anyhow!("persistence unavailable: {e}"));
        // 옵션 B: 체크포인트 없이 계속 (멱등 워크플로우에만 해당)
        // tracing::warn!("continuing without checkpoint");
    }
}

감지: checkpoint_failures_total 카운터를 모니터링합니다 (tracing::info!metric=checkpoint_failure 레이블로 발행).

#시나리오 C: 네트워크 파티션 (부분 쓰기)

증상: append()가 타임아웃되며 쓰기가 커밋되었는지 불확실합니다.

안전한 전략: 멱등 쓰기를 사용합니다 — PostgreSQL 어댑터는 ON CONFLICT DO NOTHING을 사용하므로 재시도가 안전합니다.

// 지수 백오프로 재시도
for attempt in 0..3 {
    match store.append(envelope.clone()).await {
        Ok(()) => break,
        Err(e) if attempt < 2 => {
            tokio::time::sleep(Duration::from_millis(100 * 2u64.pow(attempt))).await;
        }
        Err(e) => return Outcome::Fault(e.into()),
    }
}

#3. 체크포인트 보관 정책

#PostgreSQL — TTL 기반 정리

완료된 트레이스 중 보관 기간이 지난 것을 주기적으로 정리하는 작업을 구현합니다:

-- 30일 이상 전에 완료된 트레이스의 이벤트 삭제
DELETE FROM ranvier_persistence_events
WHERE trace_id IN (
    SELECT trace_id FROM ranvier_persistence_state
    WHERE completion IS NOT NULL
      AND completed_at < NOW() - INTERVAL '30 days'
);

DELETE FROM ranvier_persistence_state
WHERE completion IS NOT NULL
  AND completed_at < NOW() - INTERVAL '30 days';

또는 보상 멱등성을 위한 내장 정리 메서드를 사용합니다:

let cutoff_ms = (Utc::now() - Duration::days(30)).timestamp_millis();
let purged = idempotency_store.purge_older_than_ms(cutoff_ms).await?;
tracing::info!(purged_rows = purged, "idempotency cleanup complete");

#Redis — 체크포인트 키에 TTL 설정

스토어 생성 시 TTL을 설정하여 키가 자동으로 만료되도록 합니다:

// 체크포인트 7일 후 만료
let store = RedisPersistenceStore::with_prefix_and_ttl(
    manager,
    "ranvier:persistence:prod",
    7 * 24 * 60 * 60,  // 초 단위
);

#권장 보관 기간

환경 보관 기간 근거
개발 / 스테이징 7일 디버깅 기간
프로덕션 (표준) 30일 감사 + 재생 기간
프로덕션 (규제 대상) 90–365일 규정 준수 요구사항

#4. 확장 고려사항

#높은 동시성 체크포인팅 (PostgreSQL)

풀 크기 설정:

let pool = sqlx::postgres::PgPoolOptions::new()
    .max_connections(20)     // DB max_connections / 예상 동시성에 맞춤
    .min_connections(5)      // 웜 커넥션 유지
    .acquire_timeout(Duration::from_secs(3))
    .connect(&database_url).await?;

파티셔닝 (매우 높은 처리량):

초당 10,000건 이상의 체크포인트를 처리하는 서비스의 경우, ranvier_persistence_events 테이블을 trace_id 해시로 파티셔닝합니다:

-- 이벤트 테이블을 16개 버킷으로 파티셔닝
CREATE TABLE ranvier_persistence_events (
    trace_id TEXT NOT NULL,
    ...
) PARTITION BY HASH (trace_id);

CREATE TABLE ranvier_persistence_events_0
    PARTITION OF ranvier_persistence_events
    FOR VALUES WITH (MODULUS 16, REMAINDER 0);
-- ... 1..15에 대해 반복

#Redis — 샤딩 및 복제

수평 확장을 위해 Redis Cluster를 사용합니다:

// 클러스터 인식 클라이언트 URL 사용
let store = RedisPersistenceStore::connect(
    "redis+cluster://redis-node1:6379,redis-node2:6379,redis-node3:6379"
).await?;

참고: Redis Cluster는 슬롯 간 다중 키 작업을 지원하지 않습니다. 단일 트레이스의 모든 키가 동일한 슬롯에 매핑되도록 해시 태그를 추가합니다: {trace_id}: 접두사를 사용하세요.


#5. 백업 및 복원

#PostgreSQL

백업:

# 전체 덤프 (소규모 데이터베이스용)
pg_dump -U postgres -d mydb -t 'ranvier_persistence_*' \
  --format=custom -f ranvier_persistence_$(date +%Y%m%d).dump

# 지속적 WAL 아카이빙 (프로덕션용)
# postgresql.conf 설정:
# wal_level = replica
# archive_mode = on
# archive_command = 'cp %p /mnt/backup/wal/%f'

복원:

# 덤프에서 복원
pg_restore -U postgres -d mydb ranvier_persistence_20260226.dump

# 복원 후 행 수 확인
psql -U postgres -d mydb -c \
  "SELECT COUNT(*) FROM ranvier_persistence_state WHERE completion IS NOT NULL;"

#Redis

백업 (RDB 스냅샷):

# 즉시 저장 트리거
redis-cli BGSAVE

# 덤프 파일 복사
cp /var/lib/redis/dump.rdb /mnt/backup/redis_$(date +%Y%m%d).rdb

복원:

# Redis 중지, dump.rdb 교체, 재시작
systemctl stop redis
cp /mnt/backup/redis_20260226.rdb /var/lib/redis/dump.rdb
systemctl start redis

복원 후 키 상태 확인:

# persistence 키 존재 여부 확인
redis-cli KEYS "ranvier:persistence:*" | wc -l

#6. 참고 자료

  • `docs/manual/04_PERSISTENCE.md` — 개념 가이드 + 어댑터 선택
  • `ranvier/runtime/src/persistence.rs` — API 레퍼런스
  • `ranvier/examples/persistence-production-demo/` — 데모 시나리오