Skip to content

[server] add support of rebuilding KV from local snapshot - #3977

Open
zuston wants to merge 6 commits into
apache:mainfrom
zuston:rebuildKV
Open

zuston wants to merge 6 commits into
apache:mainfrom
zuston:rebuildKV

Conversation

@zuston

@zuston zuston commented Aug 13, 2026

Copy link
Copy Markdown
Member

Purpose

This PR enables KV tablets to be rebuilt from valid local snapshots, speeding up recovery—especially when the tablet server restarts without a leader change.

Brief change log

  1. reserve the local latest snapshot to the local KV tablet main dir
  2. recover KV tablet from the latest snapshot if it's valid

Tests

API and Format

Documentation

@fresh-borzoni fresh-borzoni left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@zuston Thank you for the PR, left a couple of comments and questions, PTAL

@@ -323,16 +327,9 @@ protected NativeRocksDBSnapshotResources(

@Override
public void release() {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

release() is empty now and SnapshotRunner is its only caller. Remove it?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have introduced the dedicated option for this feature, so let's reserve this

// make as leader again, should restore from snapshot
// Recover as leader after the in-place restart, without an intervening role transition.
makeKvReplicaAsLeader(kvReplica, 2);
assertThat(downloadedRemoteSnapshot).isFalse();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we get the other half: invalid checkpoint, assert downloadedRemoteSnapshot is true?

}

// as we have downloaded kv files into the tablet dir, now, we can load it
kvTablet = kvManager.loadKv(tabletDir, schemaGetter, this::onKvFlushComplete);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the local copy won't open, loadKv throws and we never reach downloadKvSnapshots, so the broken-snapshot handling from #1482 never runs, and the retries at 743 just repeat. Therefore replica ends up stuck with nothing clearing the directory.

Shall we drop the local checkpoint and fall through to the download instead? Not via handleSnapshotBroken though, the remote snapshot is fine here.

}
// The local checkpoint is useful after an in-place restart. Keep the committed snapshot and
// remove older or uncommitted checkpoints only after the remote commit succeeds.
LocalKvSnapshotUtils.retainOnly(instanceBasePath, completedSnapshotId);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be behind a flag? Local recovery is normally opt-in, like Flink has state.backend.local-recovery and also #2179 shipped snapshot retention with a TTL and metrics.
This turns retention on for every KV bucket with no way off, so feels off.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This deletes the whole tablet dir for standbys too, so the retained checkpoint only survives while the replica stays leader. A standby that gets promoted still downloads from remote.

Is keeping it for standbys in scope, or deliberately out? Looks like what the TODO in processRetryOfflineLeader is anticipating.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. This PR only optimizes in-place restarts of the leader TabletServer. Standby promotion still recovers from remote storage; maintaining local snapshots on standbys requires a separate lifecycle and is out of scope for this PR. We can track it separately, that is another bigger feature to speed up the KV leader transition.

@zuston
zuston requested a review from fresh-borzoni August 29, 2026 14:27

@fresh-borzoni fresh-borzoni left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@zuston Thank you, LGTM 👍 One non-blocking comment

cc @swuferhong

+ "The default setting is 10 minutes.");

public static final ConfigOption<Boolean> KV_SNAPSHOT_LOCAL_RECOVERY_ENABLED =
key("kv.snapshot.local-recovery.enabled")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one's static, while the other KV knobs can be changed at runtime (kv.snapshot.interval, kv.leader-replica.memory-reserved, the disk ratios). Since it costs disk, someone watching DISK_USAGE_RATIO go up can only switch it off by restarting.

Could we do what the interval does? PeriodicSnapshotManager takes a LongSupplier for it and the key is in ALLOWED_CONFIG_KEYS.

Though switching it off wouldn't free anything by itself. release() would stop keeping new checkpoints, but snap-N is still there until the next remote restore, so notifySnapshotComplete would need to clear the snap-* dirs when it's off.

@zuston zuston Aug 31, 2026 •

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one's static, while the other KV knobs can be changed at runtime (kv.snapshot.interval, kv.leader-replica.memory-reserved, the disk ratios). Since it costs disk, someone watching DISK_USAGE_RATIO go up can only switch it off by restarting.

Yes, this could be updated dynamiclly to follow up in the next PR. One tip that this feature won't cost extra disk capacity because the snapshot is just a hard link referenced by the rocksdb files.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not quite. snap-N hard-links the SSTs at snapshot time. Compaction later removes them from db/, but snap-N keeps them alive until N+1 completes.

Could we at least mention this in the option description?

@zuston

zuston commented Sep 15, 2026

Copy link
Copy Markdown
Member Author

rebased @fresh-borzoni

@fresh-borzoni fresh-borzoni left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@zuston Thank you, left some comments, PTAL

*/
public void startup() {
cleanupStaleKvDirectories();
if (!conf.get(ConfigOptions.KV_SNAPSHOT_LOCAL_RECOVERY_ENABLED)) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the flag on this skips the #4401 cleanup, so #4400 comes back.

Controlled shutdown keeps the old leader's KV dir, since CoordinatorRequestBatch's addNotifyLeaderRequestForTabletServers skips shutting-down servers. The server restarts as a follower, and Replica.dropKv() is a no-op because kvTablet == null. So db/ + snap-N stay on every bucket it now follows, and they survive a table drop.

Reproduced this behaviour in ReplicaTest: with the flag on, db/ + snap-0 survive restart -> follower -> Replica.delete(). With the flag off, the startup cleanup removes them.

Could we keep the dirs at startup, but delete a leftover dir when the replica becomes follower with no open tablet? And how would dirs of buckets that are never assigned back get reclaimed?

@swuferhong WDYT?

+ "The default setting is 10 minutes.");

public static final ConfigOption<Boolean> KV_SNAPSHOT_LOCAL_RECOVERY_ENABLED =
key("kv.snapshot.local-recovery.enabled")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not quite. snap-N hard-links the SSTs at snapshot time. Compaction later removes them from db/, but snap-N keeps them alive until N+1 completes.

Could we at least mention this in the option description?


// as we have downloaded kv files into the tablet dir, now, we can load it
kvTablet = kvManager.loadKv(tabletDir, schemaGetter, this::onKvFlushComplete);
kvTablet = restoreKvTablet(completedSnapshot);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not specifically coming from this PR, but noticed while reading and reproduced on main: if recoverKvTablet fails once after a snapshot restore, all 5 initKvTablet attempts fail. Attempt 1's RocksDB is still open, so each retry re-downloads the snapshot and fails with RocksDBException: lock hold by current process .../db/LOCK. The no-snapshot branch already reuses the open tablet. Shall we file an issue for this since we are here?

completedSnapshot.getSnapshotID(),
System.currentTimeMillis() - start);
return restoredKvTablet;
} catch (KvBuildingException localRecoveryException) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: a counter for local hits vs fallbacks would show whether this actually kicks in. #2179 shipped metrics with snapshot retention, is it applicable here, wdyt?

Path restoreDirectory = kvTabletDir.toPath().resolve(RESTORE_DIRECTORY_PREFIX + snapshotId);
Path activeDbDirectory = RocksDBKvBuilder.getInstanceRocksDBPath(kvTabletDir).toPath();
try {
FileUtils.deleteDirectory(restoreDirectory.toFile());

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: a leftover .db-restore- gets removed only by a later restore with the same id.
Could retainOnly also sweep .db-restore-*?


private KvTablet restoreKvTablet(CompletedSnapshot completedSnapshot) throws Exception {
checkNotNull(kvManager);
long start = System.currentTimeMillis();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: clock.milliseconds() like initKvTablet above? Is there any specific reason to use different one?


// as we have downloaded kv files into the tablet dir, now, we can load it
kvTablet = kvManager.loadKv(tabletDir, schemaGetter, this::onKvFlushComplete);
kvTablet = restoreKvTablet(completedSnapshot);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Which setup is this aimed at?

With RF≥2, leadership moves to another replica on controlled shutdown or crash, so this server restarts as a follower. The local copy helps only if leadership returns before the new leader's next snapshot. After that the snapshot id moves on and we download anyway.

So it mostly helps RF=1 or ISR={leader}. Shall we say that in the option description?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good point. the motivation of this pr is to speed up the local recovery for multi RFs. When I upgrade the whole fluss cluster to do some quick experiment or the new features validation, I will shutdown the coordinator directly firstly, and then restart the tablet servers to avoid leader changes to reduce the recovery time cost. (btw for the recommended recovery way, the recovery time is really too long especially when rolling upgrading for the large scale bucket numbers)

based on the above requirements, I have to optimize the local recovery speed without leader change.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants