-
Notifications
You must be signed in to change notification settings - Fork 621
[server] Parallelize replica leader transitions #3999
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
3c52f23
7388677
ef4189d
9c60225
3f3423b
623b294
4437fe2
2b0ae5f
74d3ada
3bb5efc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -405,7 +405,7 @@ public class ConfigOptions { | |
| public static final ConfigOption<Integer> SERVER_IO_POOL_SIZE = | ||
| key("server.io-pool.size") | ||
| .intType() | ||
| .defaultValue(10) | ||
| .defaultValue(2) | ||
| .withDescription( | ||
| "The size of the IO thread pool to run blocking operations for both coordinator and tablet servers. " | ||
| + "This includes discard unnecessary snapshot files, transfer kv snapshot files, " | ||
|
|
@@ -649,6 +649,14 @@ public class ConfigOptions { | |
| "The rack for the tabletServer. This will be used in rack aware bucket assignment " | ||
| + "for fault tolerance. Examples: `RACK1`, `cn-hangzhou-server10`"); | ||
|
|
||
| public static final ConfigOption<Integer> TABLET_SERVER_REPLICA_TRANSITION_THREAD_NUM = | ||
| key("tablet-server.replica-transition-thread-num") | ||
| .intType() | ||
| .defaultValue(10) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The configured value only bounds the number of bucket transitions; it does not bound the actual recovery concurrency. Each KV leader recovery creates a private This burst happens precisely during failover or rebalancing, when the server is already under pressure, and may amplify CPU, native-memory, file-descriptor, disk-I/O, and remote-storage load. Ten concurrent RocksDB initializations are also potentially expensive even when remote-log replay is not required. Could we either:
I think this should be evaluated before making 10 the production default. WDYT @platinumhamburg
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Makes sense. For now, we could use a more conservative default. How about setting it to 2? |
||
| .withDescription( | ||
| "The maximum number of replica role transitions that can run " | ||
| + "concurrently in a TabletServer."); | ||
|
|
||
| public static final ConfigOption<Double> TABLET_SERVER_ADVERTISED_RESOURCE_CPU_CORES = | ||
| key("tablet-server.advertised-resource.cpu-cores") | ||
| .doubleType() | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -145,12 +145,15 @@ | |
| import java.util.Collections; | ||
| import java.util.HashMap; | ||
| import java.util.HashSet; | ||
| import java.util.LinkedHashMap; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.Optional; | ||
| import java.util.Set; | ||
| import java.util.concurrent.CompletableFuture; | ||
| import java.util.concurrent.CompletionException; | ||
| import java.util.concurrent.ConcurrentHashMap; | ||
| import java.util.concurrent.ExecutionException; | ||
| import java.util.concurrent.ExecutorService; | ||
| import java.util.concurrent.atomic.AtomicBoolean; | ||
| import java.util.concurrent.atomic.AtomicInteger; | ||
|
|
@@ -196,6 +199,7 @@ public class ReplicaManager implements ServerReconfigurable { | |
|
|
||
| private final TabletServerMetadataCache metadataCache; | ||
| private final ExecutorService ioExecutor; | ||
| private final ExecutorService replicaTransitionExecutor; | ||
| private final ProjectionPushdownCache projectionsCache = new ProjectionPushdownCache(); | ||
| private final Lock replicaStateChangeLock = new ReentrantLock(); | ||
|
|
||
|
|
@@ -259,6 +263,7 @@ public ReplicaManager( | |
| ScannerManager scannerManager, | ||
| Clock clock, | ||
| ExecutorService ioExecutor, | ||
| ExecutorService replicaTransitionExecutor, | ||
| LocalDiskManager localDiskManager, | ||
| @Nullable PluginManager pluginManager) | ||
| throws IOException { | ||
|
|
@@ -287,6 +292,7 @@ public ReplicaManager( | |
| scannerManager, | ||
| clock, | ||
| ioExecutor, | ||
| replicaTransitionExecutor, | ||
| localDiskManager, | ||
| pluginManager); | ||
| } | ||
|
|
@@ -310,6 +316,7 @@ public ReplicaManager( | |
| ScannerManager scannerManager, | ||
| Clock clock, | ||
| ExecutorService ioExecutor, | ||
| ExecutorService replicaTransitionExecutor, | ||
| LocalDiskManager localDiskManager, | ||
| @Nullable PluginManager pluginManager) | ||
| throws IOException { | ||
|
|
@@ -361,6 +368,7 @@ public ReplicaManager( | |
| this.userMetrics = userMetrics; | ||
| this.clock = clock; | ||
| this.ioExecutor = ioExecutor; | ||
| this.replicaTransitionExecutor = replicaTransitionExecutor; | ||
| this.minInSyncReplicas = conf.get(ConfigOptions.LOG_REPLICA_MIN_IN_SYNC_REPLICAS_NUMBER); | ||
| this.scannerManager = checkNotNull(scannerManager, "scannerManager"); | ||
| // Historical lookup cache capacity currently uses only the first data volume. | ||
|
|
@@ -567,12 +575,18 @@ public void becomeLeaderOrFollower( | |
| inLock( | ||
| replicaStateChangeLock, | ||
| () -> { | ||
| Map<TableBucket, NotifyLeaderAndIsrData> dataByTableBucket = | ||
| new LinkedHashMap<>(); | ||
| for (NotifyLeaderAndIsrData data : notifyLeaderAndIsrDataList) { | ||
| dataByTableBucket.put(data.getTableBucket(), data); | ||
| } | ||
|
|
||
| // check or apply coordinator epoch. | ||
| validateAndApplyCoordinatorEpoch(requestCoordinatorEpoch, "notifyLeaderAndIsr"); | ||
|
|
||
| List<NotifyLeaderAndIsrData> replicasToBeLeader = new ArrayList<>(); | ||
| List<NotifyLeaderAndIsrData> replicasToBeFollower = new ArrayList<>(); | ||
| for (NotifyLeaderAndIsrData data : notifyLeaderAndIsrDataList) { | ||
| for (NotifyLeaderAndIsrData data : dataByTableBucket.values()) { | ||
| LOG.info( | ||
| "Try to become leaderAndFollower for {} with isr {}, replicas: {}", | ||
| data.getTableBucket(), | ||
|
|
@@ -1389,30 +1403,62 @@ private void makeLeaders( | |
| .map(NotifyLeaderAndIsrData::getTableBucket) | ||
| .collect(Collectors.toSet())); | ||
|
|
||
| List<CompletableFuture<NotifyLeaderAndIsrResultForBucket>> makeLeaderFutures = | ||
| new ArrayList<>(replicasToBeLeader.size()); | ||
| for (NotifyLeaderAndIsrData data : replicasToBeLeader) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we guarantee or validate that The normal coordinator path currently builds the request from a map, but the RPC representation is a repeated list and the Replica.makeLeader() is protected by its per-replica lock, but Please either reject duplicate buckets, deduplicate the request before submission, or serialize transition tasks by
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good point, this is necessary to ensure the unique before entering the parallelize transitions |
||
| TableBucket tb = data.getTableBucket(); | ||
| try { | ||
| Replica replica = getReplicaOrException(tb); | ||
| // register replica to remote log manager first. | ||
| remoteLogManager.registerReplica(replica); | ||
|
|
||
| // Load the latest lake progress before leader activation. Historical KV recovery | ||
| // requires its lake log end offset, while failures remain best effort for normal | ||
| // replicas. | ||
| if (replica.isDataLakeEnabled()) { | ||
| updateWithLakeTableSnapshot(replica); | ||
| } | ||
| replica.makeLeader(data); | ||
|
|
||
| // start the remote log tiering tasks for leaders | ||
| remoteLogManager.startLogTiering(replica); | ||
| result.put(tb, new NotifyLeaderAndIsrResultForBucket(tb)); | ||
| makeLeaderFutures.add( | ||
| CompletableFuture.supplyAsync( | ||
| () -> makeLeader(replica, data), replicaTransitionExecutor)); | ||
| } catch (Exception e) { | ||
| LOG.error("Error make replica {} to leader", tb, e); | ||
| result.put( | ||
| tb, new NotifyLeaderAndIsrResultForBucket(tb, ApiError.fromThrowable(e))); | ||
| } | ||
| } | ||
|
|
||
| try { | ||
| CompletableFuture.allOf( | ||
| makeLeaderFutures.toArray( | ||
| new CompletableFuture<?>[makeLeaderFutures.size()])) | ||
| .get(); | ||
| } catch (InterruptedException e) { | ||
| makeLeaderFutures.forEach(future -> future.cancel(false)); | ||
| Thread.currentThread().interrupt(); | ||
| throw new CompletionException(e); | ||
| } catch (ExecutionException e) { | ||
| throw new CompletionException(e.getCause()); | ||
| } | ||
| for (CompletableFuture<NotifyLeaderAndIsrResultForBucket> future : makeLeaderFutures) { | ||
| NotifyLeaderAndIsrResultForBucket leaderResult = future.join(); | ||
|
swuferhong marked this conversation as resolved.
|
||
| result.put(leaderResult.getTableBucket(), leaderResult); | ||
| } | ||
| } | ||
|
|
||
| private NotifyLeaderAndIsrResultForBucket makeLeader( | ||
| Replica replica, NotifyLeaderAndIsrData data) { | ||
| TableBucket tb = data.getTableBucket(); | ||
| try { | ||
| // register replica to remote log manager first. | ||
| remoteLogManager.registerReplica(replica); | ||
|
|
||
| // Load the latest lake progress before leader activation. Historical KV recovery | ||
| // requires its lake log end offset, while failures remain best effort for normal | ||
| // replicas. | ||
| if (replica.isDataLakeEnabled()) { | ||
| updateWithLakeTableSnapshot(replica); | ||
| } | ||
| replica.makeLeader(data); | ||
|
|
||
| // start the remote log tiering tasks for leaders | ||
| remoteLogManager.startLogTiering(replica); | ||
| return new NotifyLeaderAndIsrResultForBucket(tb); | ||
| } catch (Exception e) { | ||
| LOG.error("Error make replica {} to leader", tb, e); | ||
| return new NotifyLeaderAndIsrResultForBucket(tb, ApiError.fromThrowable(e)); | ||
| } | ||
| } | ||
|
|
||
| // NOTE: This method can be removed when fetchFromLake is deprecated | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -189,6 +189,9 @@ public class TabletServer extends ServerBase { | |
| @GuardedBy("lock") | ||
| private ExecutorService replicaStateChangeExecutor; | ||
|
|
||
| @GuardedBy("lock") | ||
| private ExecutorService replicaTransitionExecutor; | ||
|
|
||
| public TabletServer(Configuration conf) { | ||
| this(conf, SystemClock.getInstance()); | ||
| } | ||
|
|
@@ -285,6 +288,11 @@ protected void startServices() throws Exception { | |
| this.replicaStateChangeExecutor = | ||
| Executors.newSingleThreadExecutor( | ||
| new ExecutorThreadFactory("tablet-server-replica-state-change")); | ||
| this.replicaTransitionExecutor = | ||
| Executors.newFixedThreadPool( | ||
| conf.get(ConfigOptions.TABLET_SERVER_REPLICA_TRANSITION_THREAD_NUM), | ||
| new ExecutorThreadFactory( | ||
| "tablet-server-replica-transition-" + serverId)); | ||
|
|
||
| this.scannerManager = new ScannerManager(conf, scheduler); | ||
|
|
||
|
|
@@ -307,6 +315,7 @@ protected void startServices() throws Exception { | |
| scannerManager, | ||
| clock, | ||
| ioExecutor, | ||
| replicaTransitionExecutor, | ||
| localDiskManager, | ||
| pluginManager); | ||
| replicaManager.startup(); | ||
|
|
@@ -478,6 +487,14 @@ CompletableFuture<Void> stopServices() { | |
| exception = ExceptionUtils.firstOrSuppressed(t, exception); | ||
| } | ||
|
|
||
| try { | ||
| if (replicaTransitionExecutor != null) { | ||
| ExecutorUtils.gracefulShutdown(5, TimeUnit.SECONDS, replicaTransitionExecutor); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If the pool is saturated, a running transition exceeds the five-second timeout, and other transitions remain queued, Meanwhile, Could we explicitly track and cancel the transition futures during forced shutdown, use an interruptible wait, and ensure every submitted future reaches a terminal state? Please also add a deterministic shutdown test with a one-thread transition pool, one blocked transition, and a second queued transition. |
||
| } | ||
| } catch (Throwable t) { | ||
| exception = ExceptionUtils.firstOrSuppressed(t, exception); | ||
| } | ||
|
|
||
| try { | ||
| if (replicaStateChangeExecutor != null) { | ||
| ExecutorUtils.gracefulShutdown(5, TimeUnit.SECONDS, replicaStateChangeExecutor); | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.