mirror of
https://github.com/kvcache-ai/Mooncake.git
synced 2026-11-05 01:17:55 +00:00
[GH-ISSUE #2756] [RFC]: OpLog Batch Record Ordered Writer and Durable Prefix #719
Labels
No labels
ascend
auto-closed
bug
bug
CI/Build
Common
documentation
enhancement
feature request
good first issue
help wanted
high priority
MetaX
nightly-failure
pull-request
PyTorch Backend
question
RFC
Roadmap
stale
Store
Tests
Transfer Engine
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
kvcache-ai/Mooncake#719
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Originally created by @Icedcoco on GitHub (Jul 6, 2026).
Original GitHub issue: https://github.com/kvcache-ai/Mooncake/issues/2756
Originally assigned to: @Icedcoco on GitHub.
Introduction
Mooncake Store HA should do more than restart the Master process quickly. After a primary failure and failover, the new Master should preserve as much useful KV cache metadata, segment registry state, and object-location index state as possible.
The existing HA work has two complementary recovery paths:
RFC #1200 proposed an etcd-backed hot-standby mechanism for Master metadata HA. The primary writes metadata mutations as OpLog entries, and the standby reads/watches those entries from etcd and applies them locally. RFC #1650 then introduced an
OpLogStore/OpLogChangeNotifierabstraction to decouple the upper-layer replication logic from the etcd-specific implementation.PR #2331 is the immediate baseline for this RFC. Although that PR was submitted by @Icedcoco, most of the HA hot-standby and OpLog implementation came from earlier work by @Libotry and @00fish0 in the
dev/snapshot_etcd_storage_integration_testing_fixbugbranch. The work in #2331 was mainly to rebase that legacy implementation onto the currentkvcache-ai/Mooncake:main, adapt it to the current Mooncake Store architecture, and integrate recently added Store features such as tenant-aware object metadata, segment lifecycle replication, and the updated standby promotion / snapshot handoff paths.This RFC builds on #2331 and proposes the next step for the OpLog write and replication protocol: batch records, an ordered writer, and a durable prefix. The goal is not to replace the business semantics that #2331 already adapted. Instead, this RFC keeps the current logical
OpLogEntrysemantics and replaces the underlying write and replay protocol to address lock-held durable waits, per-entry backend key amplification, and promotion boundary ambiguity.Relevant links:
Background
Roadmap Context
The Mooncake Store V3 Roadmap (#1035) tracks HA under
Milestone 2: Master Service Enhancements, specificallyMetadata Adaptation - HA. That item has three related lines of work:The relationship is straightforward: snapshot restores a complete baseline, OpLog catches the standby up from that baseline to a later primary state, and the HA runtime orchestrates snapshot bootstrap, OpLog following, promotion, and serving.
Snapshot Baseline
RFC #1150 was motivated by a practical limitation of early HA mode: fast Master restart alone does not recover KV cache location metadata. If all metadata is lost after failover, upper-layer inference must rebuild cache state, and the recovery time can be much longer than the Master process restart time.
PR #1431 implemented Master snapshot and restore:
PR #1465 added task-manager serialization/deserialization. PR #1381 designed an etcd snapshot object store so that snapshot data can also use the HA etcd deployment.
Snapshot is still periodic full-state persistence. It cannot recover mutations that happen after the latest snapshot. OpLog is therefore needed to reduce that loss window and let the standby catch up before promotion.
RFC #1200: Initial OpLog Hot Standby
RFC #1200 proposed using etcd as a reliable intermediate component for primary-standby OpLog synchronization:
OpLogManagerassigns a globally increasingsequence_idand createsOpLogEntryrecords for metadata mutations.EtcdOpLogStorepersists each entry to etcd, for example under/oplog/{cluster}/{sequence_id}./oplog/{cluster}/latestrecords the latest sequence.OpLogWatcherreceives real-time changes through etcd watch and reads historical entries during startup or reconnect.OpLogApplierchecks ordering and applies entries to standby metadata.HotStandbyServicemanages standby lifecycle and promotion.The main design points were correctness and RPO/RTO: global sequence ordering, etcd persistence, etcd watch, snapshot sequence recording, and OpLog cleanup.
PR #1451 and PR #1515: Early Implementation
PR #1451 wrapped the basic etcd interface needed by Master HA.
PR #1515 implemented the early hot-standby and OpLog interface:
EtcdOpLogStoreOpLogManagerOpLogWatcherOpLogApplierHotStandbyServiceStandbyStateMachinePUT_END,PUT_REVOKE, andREMOVEThis moved the OpLog design from RFC into a runnable implementation. However, the early code was tightly coupled to etcd, and the Mooncake Store metadata schema, tenant model, segment lifecycle, and promotion runtime have changed since then.
RFC #1650: OpLog Store Abstraction
RFC #1650 addressed the etcd coupling in the early OpLog subsystem. Several upper-layer components directly held or constructed
EtcdOpLogStore, while the old watcher mixed watch, reconnect, deserialization, and apply orchestration in one class.The RFC proposed:
OpLogStoreinterface for write, read, read-since, latest-sequence, snapshot-sequence, and cleanup operations;OpLogChangeNotifierinterface for backend-specific notification;OpLogReplicatorthat replaces the monolithic etcd-centric watcher;That RFC intentionally kept behavior unchanged. It extracted interfaces and clarified boundaries, but did not change the legacy per-entry OpLog layout or logical entry semantics.
HA Runtime Refactoring
PR #1678 introduced an HA backend abstraction and moved leader coordination from the legacy etcd helper path to
LeaderCoordinator. It also keptOpLogStoreandSnapshotStoreas backend boundaries for follow-up work. PR #1722 added a Redis leader coordinator, proving that the leadership path can be backend-independent.RFC #1648 discussed Kubernetes Lease-based HA. That work is about leader coordination and master-view discovery. It does not by itself provide OpLog durability or replay semantics.
PR #1777 split the always-on Master process runtime from the leader-only serving runtime, introduced standby-controller composition, and made snapshot bootstrap plus OpLog following part of the standby runtime. This is the runtime foundation needed for switching the standby between legacy per-entry replay and batch-record replay.
PR #2331: Current Foundation
PR #2331 (
[Store] add HA OpLog replication and tenant-aware standby promotion) rebased and adapted the earlier HA hot-standby work to current main. The key changes include:oplog_store_type,oplog_store_root_dir, andoplog_poll_interval_ms;SEGMENT_MOUNT,SEGMENT_UNMOUNT, andSEGMENT_UPDATEOpLog entries for standby segment registry replay;tenant_idinOpLogEntry, tenant-aware metadata store APIs, and tenant-aware standby snapshot/restore paths;tenant_id;After #2331, current main has a much more complete OpLog HA baseline. The remaining issue is the storage and write model: the legacy path still stores one backend key per logical
OpLogEntry, and/latestremains the latest-sequence pointer.EtcdOpLogStore::WriteOpLog(entry, true)already uses an internal batch writer, so the business thread is not directly writing each single key to etcd. But from the Master mutation path, synchronous waiting can still happen while holdingsnapshot_mutex_in shared mode and a metadata shard lock in exclusive mode.Motivation
The current #2331-based HA OpLog path covers many business cases, but the write protocol still has several structural problems.
1. Lock-held Durable Wait
Several paths can wait for durable OpLog persistence while holding
snapshot_mutex_and a metadata shard lock:Remove()BatchRemove()RemoveAll()BatchReplicaClear()BatchEvict()NoFBatchEvict()BatchEvictDiskReplica()The current sync write path enqueues entries into an internal pending batch and waits for the flush result. etcd latency, batch-flush delay, retry, or sync timeout can therefore block unrelated metadata mutations in the same shard.
2. Per-entry Backend Key Amplification
The legacy layout is:
Each logical OpLog entry becomes one backend key. Under high mutation concurrency, the backend must handle many keys, watch events, MVCC items, and compaction items. Internal group commit reduces the number of flush calls, but it does not reduce the number of persisted keys.
3. Split Safety Boundary
In the legacy model, an entry key means the entry exists, while
/latestmeans the primary has advanced the latest sequence pointer. For standby apply and promotion, the important concept is a verified, continuous, no-gap durable boundary.Batch-record mode should make this explicit. A batch record and the durable boundary should advance together through a CAS/txn protocol, and standby/promotion should trust only that durable boundary.
4. Need for a Post-#2331 Evolution Path
#2331 adapted the legacy HA OpLog implementation to current main and added tenant, segment, and promotion compatibility. The next step should not rewrite those semantics. It should keep:
OpLogEntrysemantics;PUT_END,REMOVE, andSEGMENT_*apply behavior;The part to replace is the underlying durable write and replay protocol.
Goals
OpLogEntryrecords over a continuous sequence range.{batch_id, last_seq}as the durable prefix and the only safe boundary for callbacks, standby apply, and promotion.OpLogEntry.Non-Goals
OpLogEntryformat.UpsertStartin this RFC. That path needs a separate design because its semantics are more complex.Proposal
1. Architecture
Introduce three layers:
OrderedOpLogWriter
OrderedOpLogWriteris the only OpLog admission, sequence assignment, batching, retry, and callback-dispatch entry point in a serving primary.Business code uses:
Reserve()before metadata mutation.Commit()inside the metadata shard lock to attach the finalOpLogEntryand durable callback.Abort()if the precondition check fails or no OpLog is needed.The writer owns:
accepting;OpLogBatchStorage
OpLogBatchStorageis a layout/storage helper between the writer and the KV backend. It owns:/latestread for cutover initialization.It does not own admission, sequence assignment, retry policy, callback dispatch, or Master business semantics.
HaKvBackend
HaKvBackendis a reusable HA KV adapter layer. It provides only generic KV/range/transaction capabilities. It does not know about OpLog, durable prefixes, leadership, snapshot, or metadata semantics.The first production implementation should be an etcd txn/CAS backend. Future leadership reuse should use sidecar interfaces such as
HaKvLeaseBackendorHaKvWatchBackend, instead of adding lease/watch/election methods to the minimal KV interface.2. Core Data Structures
DurablePrefixmeans every logical entry in[1, last_seq]is durable and safe to apply/promote from.batch_ididentifies the physical batch record that containslast_seq.OpLogBatchRecordmust satisfy:entriesis non-empty;sequence_id;first_seq == entries.front().sequence_id;last_seq == entries.back().sequence_id;batch_idis monotonic;3. Minimal KV Backend Interface
Batch-record HA initialization must check
SupportsTxn(). If the backend does not support txn/CAS, the first version should fail initialization or return unsupported.4. Storage Layout
Keep legacy keys unchanged:
Add batch-record keys under the same prefix:
Use fixed-width decimal encoding for
batch_id, preferably the same 20-digit padding used by legacy sequence keys. This keeps lexicographic range scan order equal to numeric order.Since the new keys remain under
/oplog/{cluster}/, readers must explicitly filter:5. Durable Prefix Value
/oplog/{cluster}/durable_prefixshould contain at least:The actual encoding can use an existing project serialization format, but it must include
schema_versionso later versions can add leader epoch, term, primary id, or layout version.Semantics:
[1, last_seq]is the only safe apply/promotion boundary.batch_idlocates the batch containinglast_seq./latest, as the safety boundary./latestvalues./latestis used only for cutover initialization and legacy reader compatibility.6. Cutover and Legacy Compatibility
Upgrade order:
If a batch-record writer starts and durable prefix does not exist:
Create durable prefix with CAS/create-if-absent:
If the compare fails, another initializer has created the durable prefix. The current writer must read the existing prefix and use it as the source of truth.
This design intentionally trusts
/latestduring cutover:seq > latest;This is an operational tradeoff. It assumes the old write path has been stopped during cutover, or that
/latestis the accepted legacy safety boundary.7. Writer Admission Protocol
Add:
This limits the number of entries in the current open waiting batch. It is an admission/backpressure limit, not a target batch size.
Business code uses:
Reserve
Reserve()runs before metadata mutation:Reserve()does not allocate a sequence ID. Therefore,Abort()does not create a gap.Commit
Commit()runs inside the metadata shard lock, but must stay short:sequence_id;If the caller already has a reservation and the entry uses a known serializable OpLog format, normal
Commit()should not fail.Abort
Abort()releases a reservation if the precondition check fails or no OpLog is needed:8. Batch Seal and Flush Policy
The first version uses a work-conserving single in-flight writer:
Implications:
oplog_batch_max_entriesis a backpressure limit, not a flush threshold.Seal does not wait for reservations that have not yet committed or aborted. Already committed entries can be drained and sealed. Slow business threads keep their reserved slots and their later commits go into a later seal.
9. Sequence and Batch ID
Logical sequence is assigned in
Commit():Guarantees:
Abort()creates no gap;batch_idis assigned at seal time:batch_iddoes not replace logical sequence. It locates batch records, describes the physical durable prefix position, supports standby range reads, and lets promotion verify batch continuity.10. Backend txn/CAS Protocol
For one writing batch:
After txn success:
new_prefix.last_seq.On txn failure or network timeout:
new_prefixand the batch record is readable and valid, treat the previous unknown outcome as success.expected_prefix, retry the txn.new_prefix, treat it as stale writer / concurrent primary / data anomaly. Keep writer unhealthy and do not skip.Durable prefix must be monotonic. Exact-value compare prevents overwrite and rollback.
11. Backend Failure Policy
The first version uses:
On the first backend txn or durable-prefix advancement failure:
For committed but not durable mutations:
After a successful retry:
Later batches must never pass a stuck batch. Passing it would create a durable-prefix gap and break standby/promotion safety.
12. Durable Callback and Finalize
Callbacks fire only after durable prefix has advanced:
Callback must not fire just because the batch record was written, and must not depend on legacy
/latest.The writer thread should only enqueue callback tasks. It should not perform heavy business logic, resource release, quota accounting, or metadata relocking.
Remove/evict/clear finalize callbacks may perform:
EraseMetadata();disk_object_countupdates;The callback context must include enough information to finalize safely:
Finalize must re-enter the shard lock and verify that current metadata still matches the pending mutation. If the object has been recreated by a higher-sequence write, the callback must not delete the new metadata.
13. Visibility Semantics
Operations That Reduce Readability
Operations that reduce readable replica/object state may make the state invisible on the primary before durability:
Remove()BatchRemove()RemoveAll()BatchReplicaClear()BatchEvict()NoFBatchEvict()BatchEvictDiskReplica()Target flow:
Resource release must not happen before durable callback. This includes buffer/resource reuse, quota release, local disk usage release, cache accounting, disk object count, and group membership cleanup.
ReplicaStatus::REMOVEDmeans:GetReplicaList();If other
COMPLETEreplicas remain, reads return those readable replicas. If all readable replicas are removed, reads returnOBJECT_NOT_FOUND.Operations That Increase Readability
Operations that add readable replica/object state may remain visible on the primary before durability. If the primary crashes before durability, the new primary may lose that added readable state.
This applies to:
PutEndAddReplicaNotifyOffloadSuccess()fallbackNotifyPromotionSuccess()COMPLETEThis keeps the first version close to current behavior and avoids introducing a pending-visible state for every readability-increasing path.
No New PendingVisibilityState
The first version does not add a new object-level
PendingVisibilityState. Read-path distinction should use existingReplicaStatus:14. Standby Apply
The first version uses polling, not batch-key watch:
Legacy and batch path transition:
/latestplus per-entry keys.batch_id == 0, it is a cutover boundary. Legacy[1,last_seq]is the safe boundary.batch_id > 0, standby must first apply legacy entries through the cutoverlast_seq, then read batch records.Batch-record mode must be strict no-gap. If durable prefix is unreadable, a referenced batch is unreadable, batch IDs are discontinuous,
first_seq != local_last_applied_seq + 1, entries are discontinuous, checksum fails, schema version is unsupported, or deserialization fails, standby must become unhealthy/fail-closed. It must not skip the gap, and it must not be promoted.Batch entries still use the current
OpLogEntry:PUT_END: apply full metadata post-state;REMOVE: delete object metadata;SEGMENT_*: update standby segment registry;tenant_id: keep #2331 tenant-aware semantics, defaulting missing legacy values todefault.As follow-up hardening,
PUT_ENDapply should consistently use objectlast_sequence_idstale checks. Strict no-gap reduces reordering risk, but stale checks still help against duplicates, retries, and legacy/batch mixed anomalies.15. Promotion
Before promotion, standby must catch up to durable prefix:
Promotion must fail closed if:
In batch-record mode,
/latestis not a promotion safety boundary.16. Relation to Current Code
The current write entry points should be gradually replaced or routed through the new writer:
AppendOpLogAndNotify()AppendOpLogAndNotifyDurable()AppendOpLogAndNotifyDurableOrAbort()PersistOpLogEntryWithSyncRetries()AppendOrPersistOrEnqueue()PersistRemoveForHA()PersistSegmentOpForHAOrEnqueue()Legacy
EtcdOpLogStorecan still be used for:/latestduring cutover;The batch-record writer should not write new entries through
EtcdOpLogStore::WriteOpLog().Implementation Plan
Phase 1: Batch-record Writer Foundation
oplog_batch_max_entries, default 1024.DurablePrefixandOpLogBatchRecord.ha/kv/HaKvBackendand an etcd txn/CAS backend.OpLogBatchStorage.OrderedOpLogWriter:Reserve / Commit / Abort;Commit();accepting=falseafter first backend failure;accepting=trueafter retry success;/latestvalues in batch-record mode.Phase 2: Standby Dual-format Reader
Phase 3: Basic Write-path Integration
Priority paths:
PutEnd/AddReplicaRemove/BatchRemove/RemoveAllBatchReplicaClearBatchEvict/NoFBatchEvict/BatchEvictDiskReplicaThe initial integration can keep most current visibility behavior and only replace the durable write entry point. This keeps the first PR reviewable and avoids changing all resource-release semantics at once.
Phase 4: Remove/Evict Logical Remove and Durable Finalize
REMOVED.Commit().ReplicaStatus:COMPLETE: OK;OBJECT_NOT_FOUND;REPLICA_IS_NOT_READY.Phase 5: Health and Operations
accepting=false/ unhealthy state to runtime/admin state.ha/kvlease/watch sidecar interfaces.Phase 6: UpsertStart Follow-up
UpsertStarthas more complex semantics:PROCESSINGpreemption;This RFC does not make
UpsertStarta blocker for the first batch-record writer version. A separate design should define old-readable visibility, new-readable visibility before durability, failure handling, quota/resource handling, and compatibility with current-format full-state entries.Phase 7: Delta Schema Follow-up
Delta schema is not part of the first version. Future work can introduce:
OBJECT_MARK_REMOVEDREPLICA_MARK_REMOVEDOBJECT_CLEAR_REPLICASOBJECT_MARK_UNAVAILABLEREPLICA_ADDREPLICA_UPDATEREPLICA_MARK_COMPLETEApply rules, idempotency, legacy mixed apply, snapshot relation, and compaction relation should be designed separately.
Testing Strategy
Storage and Codec
/latest,/snapshot/,/batches/, and/durable_prefixcorrectly.txn/CAS
Writer Admission
Reserve()does not allocate sequence;Abort()creates no gap;Commit()assigns continuous sequence;Reserve()limits open waiting batch only, not writing batch;oplog_batch_max_entriesmakes newReserve()fail fast;Commit()does no backend I/O, no waiting, and no full batch serialization.Writer Flush / Retry / Callback
accepting=false;Reserve()fails whileaccepting=false;accepting=true;Legacy Compatibility
{batch_id=0,last_seq=latest}from/latest;first_seq == legacy_latest + 1;/latest.Standby and Promotion
MasterService Integration
PutEnd/AddReplicaremain visible on primary according to current semantics;Remove/BatchRemove/RemoveAllkeep durable guard correctness;BatchReplicaClear,BatchEvict,NoFBatchEvict, andBatchEvictDiskReplicapreserve pin/refcnt/lease preconditions;group_id,data_type, and segment registry state;Key Design Points Summary
{batch_id,last_seq}.Reserve()does not allocate sequence andAbort()creates no gap.OpLogEntry.Before submitting a new issue...
@github-actions[bot] commented on GitHub (Jul 6, 2026):
Thanks for opening this issue, @Icedcoco!
102317026A maintainer will triage this when possible. To help us respond faster, please include:
Useful links: Documentation · Contributing guide
@00fish0 commented on GitHub (Jul 6, 2026):
Thanks for writing this up. Overall, the proposal makes sense to me, and I'm supportive of moving forward in this direction. Happy to discuss any remaining details.
@RuixiangMa commented on GitHub (Jul 7, 2026):
This is a well-designed RFC,have a few questions I'd like to ask:
1. Cutover baseline from /latest
FlushBatch() writes entry keys separately from /latest, which is updated afterward potentially via a background thread with 1s interval. The codebase even has GetMaxSequenceId() to work around /latest staleness. Would it be worth considering GetMaxSequenceId() as the cutover baseline, or adding a guard to ensure the old write path is quiesced before cutover?
2. Resource behavior during extended backend unavailability
The current code is fail-open, OpLogManager::Append() ) logs WARNING and continues on failure, and EraseMetadata() releases resources immediately. The RFC's move to fail-closed with deferred release is a great improvement, but if the backend is down for long, REMOVED-but-not-finalized replicas would accumulate while accepting=false blocks new writes. Would a backoff strategy, resource ceiling, or forced-failover trigger when stuck-batch age exceeds a threshold make sense?
@Icedcoco commented on GitHub (Jul 7, 2026):
@RuixiangMa Thanks, these are exactly the two points I was still hesitating about.
For the cutover baseline, I agree
/latestis not obviously the only correct choice. My original reason for using/latestwas to keep the cutover boundary aligned with the legacy published latest sequence, instead of treating any existing per-entry key as committed state. But as you pointed out,FlushBatch()and/latestare not advanced atomically today, andGetMaxSequenceId()already exists partly because/latestcan be stale.So I think there are two reasonable options:
/latest, but require an explicit quiesce + final flush +latest == max_seqguard before creatingdurable_prefix;GetMaxSequenceId()as the cutover baseline, and treat all existing per-entry keys up to that max sequence as the legacy boundary.I do not have a strong final preference yet. Option 1 keeps the boundary closer to the old published state; option 2 may be more robust against
/latestlag. I will think more about this and update the RFC once the tradeoff is clearer.For extended backend unavailability, my current assumption was:
This also implicitly assumes that leadership and OpLog both depend on etcd in the first version, so if etcd is unavailable, both write durability and HA coordination may be affected. That part definitely needs a more careful failure-mode discussion.
Your point about backoff strategy is good. We should at least define retry backoff to avoid hot-looping and log pressure, and probably expose stuck-batch age / pending-finalize resource usage so the supervisor can decide whether to stop serving or trigger failover. I still think we should not release
REMOVEDresources before durable prefix advances, but the operational policy around long unavailability needs more detail.If you have a better preference between
/latestvsGetMaxSequenceId(), or a cleaner failover policy for the long-backend-outage case, please share it. I am still open to adjusting this part of the design.@github-actions[bot] commented on GitHub (Oct 6, 2026):
This issue has had no activity for 90 days and will be closed in 7 days if there is no further activity. Please comment or react if it should stay open.
@github-actions[bot] commented on GitHub (Oct 13, 2026):
Closing due to 3 months of inactivity. If this is still relevant, please comment and we can reopen.