#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: 워크플로우 실행 중 프로세스 크래시
발생 상황:
- Axon 파이프라인이 스텝 N을 실행 중
- 프로세스 크래시 발생 (OOM, 시그널, 배포 재시작)
- 스텝 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/` — 데모 시나리오