Before/After #3226 — base overflow 처리 코드 비교
목적: PR #3226 (Daejun, incremental base+delta checkpoint) 적용 전후에 base payload overflow 처리가 어떻게 (혹은 거의 그대로) 유지되는지를 코드 라인 단위로 증명.
결론: #3226은 base overflow의 동작 자체를 바꾸지 않음. S2는 머지 후에도 그대로.
핵심 비교 표
| 항목 | 기존 (현재 mainline) | #3226 적용 후 | 변화 |
|---|---|---|---|
| base 직렬화 | json.dumps(snapshot, separators=...) | 동일 (json.dumps(snapshot, separators=...)) | ❌ 없음 |
| overflow 체크 위치 | _write_checkpoint 진입부 | 동일 함수, 동일 패턴 | ❌ 없음 |
| overflow 체크 코드 | if len(payload) > payload_cap: | 동일 라인 | ❌ 없음 |
| overflow 시 동작 | logger.warning(...); return False | 동일 — silent skip | ❌ 없음 |
| capacity 계산 식 | self._meta_container_bytes - self.block_align | self._meta_layout.base_payload_capacity (= base_copy_bytes - block_align) | 명칭만 바뀜, 값 동일 |
| mirror 내 공간 분배 | mirror = header + base payload | mirror = header + base payload + delta tail | ⚠️ base 가용 공간 감소 가능 |
| 부분 복구 가능성 | 불가 (base 못 쓰면 인덱스 전부 옛것/없음) | 가능 (이전 base + 살아있는 delta tail) | ✅ 영향 완화 |
| overflow 천장 자체 | ~64MB (기본 default 기준) | ~64MB (동일) | ❌ 없음 |
| 임계 엔트리 수 | ~321,000 (entry≈209B 기준) | ~321,000 (동일) | ❌ 없음 |
→ 변경 없음 행이 핵심. 천장과 silent fail은 그대로.
코드 라인 단위 증거
1. _write_checkpoint overflow 체크 — 변경 없음
before (mainline, lmcache/v1/storage_backend/raw_block/core.py:1164-1171):
def _write_checkpoint(self, payload: bytes, dirty_total_snapshot: int) -> bool:
"""Write one checkpoint copy and advance persisted metadata counters."""
payload_cap = self._meta_payload_capacity()
if len(payload) > payload_cap:
logger.warning(
"RawBlockCore metadata payload too large (%d > %d), "
"skipping checkpoint",
len(payload),
payload_cap,
)
return False
...
after (#3226 적용, diff:716-720):
def _write_full_base(self, payload: bytes, dirty_total_snapshot: int) -> bool:
"""...
Returns:
True when the checkpoint committed; False when the payload
exceeds the per-mirror capacity.
"""
payload_cap = self._meta_payload_capacity()
if len(payload) > payload_cap:
logger.warning(
...
)
return False
...
→ 함수명만 _write_checkpoint → _write_full_base로 약간 바뀌고, overflow 체크 5줄은 완전히 동일.
2. _meta_payload_capacity 구현 — 값은 동일
before (diff:462-464):
def _meta_payload_capacity(self) -> int:
"""Return usable bytes in one metadata checkpoint payload area."""
return self._meta_container_bytes - self.block_align
after (diff:464-465):
def _meta_payload_capacity(self) -> int:
return self._meta_layout.base_payload_capacity
_meta_layout.base_payload_capacity 정의 (diff:242-244):
@property
def base_payload_capacity(self) -> int:
return self.base_copy_bytes - self.block_align
→ 표현만 layout 추상화로 옮김. 계산식은 동일 (copy_bytes - block_align).
기본값 산수도 동일: meta_total_bytes=128MB / 2 = 64MB - block_align ≈ 64MB.
3. base 직렬화 — 변경 없음
before (mainline, core.py:1213-1217):
snapshot, dirty_total_snapshot = self._snapshot_state()
payload = json.dumps(snapshot, separators=(",", ":"), ensure_ascii=True).encode(
"utf-8"
)
return self._write_checkpoint(payload, dirty_total_snapshot)
after (#3226 적용, diff:804-808):
snapshot, dirty_total_snapshot = self._snapshot_state()
payload = json.dumps(
snapshot, separators=(",", ":"), ensure_ascii=True
).encode("utf-8")
return self._write_checkpoint(payload, dirty_total_snapshot)
→ 줄바꿈만 다르고 의미상 100% 동일. base는 여전히 전체 인덱스 JSON 직렬화.
4. delta tail이 mirror 공간 잠식 — base 가용 공간 약간 감소
before: mirror = [header (1 block)] [base payload (round_up 정렬)]
after (diff:228-235, 새 _MetaLayout 클래스 docstring):
"""...
The metadata region is split into ``len(base_copy_offsets)`` equal-size
mirrors. Within each mirror the layout is::
[ header (one block) ][ payload (round_up to block_align) ][ delta tail ]
The delta tail starts implicitly at the block-aligned end of the base
payload and extends to the mirror's end..."""
그리고 새 write 코드 (diff:751-755):
tail_start = target + self.block_align + payload_total_len
if tail_start + self.block_align <= target + self._meta_layout.base_copy_bytes:
zero_block = bytes(self.block_align)
raw.pwrite_from_buffer(...)
→ base + delta tail이 같은 mirror 공유. base가 클수록 delta 공간 줄고, delta tail이 있으면 base 가용도 (이론상) 감소. base가 mirror를 가득 채우려 하면 delta 공간 0이 됨.
5. compaction trigger — 천장 자체는 손대지 않음
after (#3226 신규, diff:836-854 _need_full_for_delta):
def _need_full_for_delta(self, payload: bytes) -> bool:
if self._meta_seq == 0:
return True
capacity = self._delta_tail_capacity_locked()
if capacity <= 0:
return True
record_bytes = self._delta_record_total_bytes(len(payload))
if record_bytes > capacity:
return True
if self._delta_tail_off + record_bytes > capacity:
return True
if self._delta_seq + 1 > self.meta_full_checkpoint_max_deltas:
return True
if (
time.monotonic() - self._last_full_ts
> self.meta_full_checkpoint_interval_sec
):
return True
return False
→ delta tail이 차면 full base로 compaction. 하지만 그 full base가 cap을 초과하면 여전히 #1번
코드(_write_full_base)에서 silent skip. Compaction 자체가 cap 문제를 해결하지 않음.
6. delta 실패 → full fallback (silent fail로 귀결)
after (#3226 신규, diff:798-808):
payload, op_count, dirty_total_snapshot = self._snapshot_dirty_log()
if payload is not None and not self._need_full_for_delta(payload):
if self._write_delta(payload, op_count, dirty_total_snapshot):
return True
# Fall through to full compaction if delta append failed.
snapshot, dirty_total_snapshot = self._snapshot_state()
payload = json.dumps(snapshot, separators=(",", ":"), ensure_ascii=True).encode("utf-8")
return self._write_checkpoint(payload, dirty_total_snapshot)
→ delta가 실패하면 full base 시도. full base도 overflow면 silent skip → 결국 동일한 S2 시나리오로 귀결.
영향 완화는 있음 (전량 유실 → 부분 복구)
before (mainline): full base가 첫 시도부터 cap 초과 → meta_seq=0 유지 → 재시작 시 _index={} → 전량 유실.
after (#3226 적용): 만약 인덱스가 작을 때 한 번 base가 써졌다면(meta_seq>0), 이후 cap을 초과해도 delta는 누적 가능 (delta tail이 가득 차기 전까지). 재시작 시:
- 이전 base 로드 → 그 시점의
_index복구 - delta tail replay → 이후 추가/삭제 반영
→ 첫 base 이후의 변경은 부분 복구 가능. 단, 다음 full base는 여전히 실패하므로 long-term에는 동일 문제.
결론
| 기존 | #3226 후 | |
|---|---|---|
| base가 capacity 초과 시 silent skip | ✓ | ✓ (동일) |
| capacity 한계 자체 | ~64MB | ~64MB (동일) |
| 임계 엔트리 수 | ~321K | ~321K (동일) |
| HC SSD 스케일 trigger | ✓ | ✓ (동일) |
| 재시작 시 전량 유실 | ✓ | 부분 복구 가능 (delta tail 살아 있으면) |
S2(base overflow)는 #3226 적용 후에도 그대로 살아있음. #3226은 우리 fix와 직교 (orthogonal) — 머지 후 위에 얹어서 해결 가능.
다음 단계 — Std SSD / HC SSD 분리
#3226 머지 후 S2 fix는 두 단계로 분리해서 진행. 자세한 분석:
private/work/s2_checkpoint_overflow/s2_verification.md §9.
단계 1 — Std SSD 커버 (modest fix, 머지 가능성 높음)
meta_total_bytes 128MB → 512MB (4x)
임계 디바이스 (Qwen3-480B TP=8): 2.4TB → 9.5TB
- Std SSD 1.92/3.84/7.68TB 전부 커버, HC SSD 미해결
meta_versionbump 필요_meta_layout/_write_full_base주변만 건드리므로 #3226과 conflict 적음
단계 2 — HC SSD 커버 (단계 1 머지 후)
두 옵션 중 선택:
옵션 2a — 한 번 더 bump (512MB → 2GB, 16x 총)
- 30TB HC SSD까지 커버 (layerwise 제외)
- 같은 코드 경로, 값만 변경
옵션 2b — 바이너리 base 포맷 (RFC)
- entry당 ~209B(JSON) → ~30B → 약 7배 헤드룸
- layerwise 모드도 처리 (단기 fix로는 불가)
_snapshot_state/_apply_loaded_state전면 수정
→ 단계 2는 단계 1 머지 결과 보고 결정.