-
Notifications
You must be signed in to change notification settings - Fork 578
[server] Batch LeaderAndIsr updates for controlled shutdown #3654
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
Changes from all commits
a42ad56
6c5b079
475c731
8d33183
3bb20b8
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 |
|---|---|---|
|
|
@@ -31,15 +31,19 @@ | |
| import org.apache.fluss.server.zk.ZooKeeperClient; | ||
| import org.apache.fluss.server.zk.data.LeaderAndIsr; | ||
| import org.apache.fluss.shaded.guava32.com.google.common.collect.Sets; | ||
| import org.apache.fluss.shaded.zookeeper3.org.apache.zookeeper.KeeperException; | ||
| import org.apache.fluss.utils.ExceptionUtils; | ||
|
|
||
| import org.slf4j.Logger; | ||
| import org.slf4j.LoggerFactory; | ||
|
|
||
| import javax.annotation.Nullable; | ||
|
|
||
| import java.util.ArrayList; | ||
| import java.util.HashMap; | ||
| import java.util.HashSet; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.Objects; | ||
| import java.util.Optional; | ||
| import java.util.Set; | ||
|
|
@@ -128,9 +132,15 @@ public void handleStateChange( | |
| BucketState targetState, | ||
| ReplicaLeaderElection replicaLeaderElection) { | ||
| try { | ||
| boolean isControlledShutdown = | ||
| replicaLeaderElection instanceof ControlledShutdownLeaderElection | ||
| && targetState == BucketState.OnlineBucket; | ||
| coordinatorRequestBatch.newBatch(); | ||
|
|
||
| if (checkIfCreateTablePartitionRequest(tableBuckets, targetState)) { | ||
| if (isControlledShutdown) { | ||
| batchHandleControlledShutdown( | ||
| tableBuckets, (ControlledShutdownLeaderElection) replicaLeaderElection); | ||
| } else if (checkIfCreateTablePartitionRequest(tableBuckets, targetState)) { | ||
| // batch register table bucket lead and isr | ||
| batchHandleOnlineChangeAndInitLeader(tableBuckets); | ||
| } else { | ||
|
|
@@ -145,6 +155,165 @@ public void handleStateChange( | |
| } | ||
| } | ||
|
|
||
| private void batchHandleControlledShutdown( | ||
| Set<TableBucket> tableBuckets, | ||
| ControlledShutdownLeaderElection controlledShutdownLeaderElection) { | ||
| Map<TableBucket, LeaderAndIsr> currentLeaderAndIsrs; | ||
| long batchReadStartTimeMs = System.currentTimeMillis(); | ||
| try { | ||
| currentLeaderAndIsrs = zooKeeperClient.getLeaderAndIsrs(tableBuckets); | ||
| } catch (Exception e) { | ||
| LOG.warn( | ||
| "Failed to batch read LeaderAndIsr for {} buckets during controlled shutdown. Falling back to individual updates.", | ||
| tableBuckets.size(), | ||
| e); | ||
| for (TableBucket tableBucket : tableBuckets) { | ||
| doHandleStateChange( | ||
| tableBucket, BucketState.OnlineBucket, controlledShutdownLeaderElection); | ||
| } | ||
| return; | ||
| } | ||
| LOG.info( | ||
| "Batch read LeaderAndIsr for {} of {} controlled shutdown buckets in {} ms.", | ||
| currentLeaderAndIsrs.size(), | ||
| tableBuckets.size(), | ||
| System.currentTimeMillis() - batchReadStartTimeMs); | ||
|
|
||
| long electionStartTimeMs = System.currentTimeMillis(); | ||
| Map<TableBucket, ElectionResult> electionResults = new HashMap<>(); | ||
| Map<TableBucket, String> partitionNames = new HashMap<>(); | ||
| Set<TableBucket> bucketsToRetryIndividually = new HashSet<>(); | ||
| for (TableBucket tableBucket : tableBuckets) { | ||
| coordinatorContext.putBucketStateIfNotExists( | ||
| tableBucket, BucketState.NonExistentBucket); | ||
| if (!checkValidTableBucketStateChange(tableBucket, BucketState.OnlineBucket)) { | ||
| continue; | ||
| } | ||
|
|
||
| BucketState currentState = coordinatorContext.getBucketState(tableBucket); | ||
| if (currentState == BucketState.NewBucket) { | ||
| bucketsToRetryIndividually.add(tableBucket); | ||
| continue; | ||
| } | ||
|
|
||
| String partitionName = null; | ||
| if (tableBucket.getPartitionId() != null) { | ||
| partitionName = coordinatorContext.getPartitionName(tableBucket.getPartitionId()); | ||
| if (partitionName == null) { | ||
| logFailedStateChange( | ||
| tableBucket, | ||
| currentState, | ||
| BucketState.OnlineBucket, | ||
| String.format( | ||
| "Can't find partition name for partition: %s.", | ||
| tableBucket.getPartitionId())); | ||
| continue; | ||
| } | ||
| } | ||
|
|
||
| LeaderAndIsr leaderAndIsr = currentLeaderAndIsrs.get(tableBucket); | ||
| if (leaderAndIsr == null) { | ||
| bucketsToRetryIndividually.add(tableBucket); | ||
| continue; | ||
| } | ||
|
|
||
| Optional<ElectionResult> optionalElectionResult = | ||
| electNewLeaderForTableBucket( | ||
| tableBucket, leaderAndIsr, controlledShutdownLeaderElection); | ||
| if (!optionalElectionResult.isPresent()) { | ||
| logFailedStateChange( | ||
| tableBucket, | ||
| currentState, | ||
| BucketState.OnlineBucket, | ||
| "Elect result is empty."); | ||
| continue; | ||
| } | ||
| electionResults.put(tableBucket, optionalElectionResult.get()); | ||
| partitionNames.put(tableBucket, partitionName); | ||
| } | ||
| LOG.info( | ||
| "Prepared {} controlled shutdown leader elections in {} ms; {} buckets require individual retry.", | ||
| electionResults.size(), | ||
| System.currentTimeMillis() - electionStartTimeMs, | ||
| bucketsToRetryIndividually.size()); | ||
|
|
||
| Map<TableBucket, LeaderAndIsr> leaderAndIsrsToUpdate = new HashMap<>(); | ||
| electionResults.forEach( | ||
| (tableBucket, electionResult) -> | ||
| leaderAndIsrsToUpdate.put(tableBucket, electionResult.leaderAndIsr)); | ||
| Set<TableBucket> updatedBuckets = | ||
| batchUpdateLeaderAndIsrWithFallback(leaderAndIsrsToUpdate); | ||
|
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 BadVersionException occurs, need to stop here directly, on need to bucketsToRetryIndividually later.
Contributor
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. A simple return may be unsafe because earlier |
||
|
|
||
| for (TableBucket tableBucket : updatedBuckets) { | ||
| ElectionResult electionResult = electionResults.get(tableBucket); | ||
| coordinatorContext.putBucketLeaderAndIsr(tableBucket, electionResult.leaderAndIsr); | ||
| doStateChange(tableBucket, BucketState.OnlineBucket); | ||
| coordinatorRequestBatch.addNotifyLeaderRequestForTabletServers( | ||
| new HashSet<>(electionResult.liveReplicas), | ||
| PhysicalTablePath.of( | ||
| coordinatorContext.getTablePathById(tableBucket.getTableId()), | ||
| partitionNames.get(tableBucket)), | ||
| tableBucket, | ||
| coordinatorContext.getAssignment(tableBucket), | ||
| electionResult.leaderAndIsr); | ||
| } | ||
|
|
||
| for (TableBucket tableBucket : bucketsToRetryIndividually) { | ||
| doHandleStateChange( | ||
| tableBucket, BucketState.OnlineBucket, controlledShutdownLeaderElection); | ||
| } | ||
| } | ||
|
|
||
| private Set<TableBucket> batchUpdateLeaderAndIsrWithFallback( | ||
| Map<TableBucket, LeaderAndIsr> leaderAndIsrs) { | ||
| Set<TableBucket> updatedBuckets = new HashSet<>(); | ||
| if (leaderAndIsrs.isEmpty()) { | ||
| return updatedBuckets; | ||
| } | ||
|
|
||
| try { | ||
| zooKeeperClient.batchUpdateLeaderAndIsr( | ||
|
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. batchUpdateLeaderAndIsr() splits the input into multiple ZooKeeper transactions of at most MAX_BATCH_SIZE, so an exception may be thrown after some earlier transactions have already committed. The current fallback retries every bucket individually, including buckets from transactions that were already confirmed successful.
Contributor
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. I think these retries write the same LeaderAndIsr values and validate against the coordinator epoch znode rather than the LeaderAndIsr node version, so they are idempotent and do not cause a correctness issue. The downside is limited to redundant ZooKeeper writes on the exceptional path. Propagating the definitely committed buckets would require changing the ZooKeeper client API or introducing a partial-progress exception/result type. Given the additional complexity and the goal of keeping this PR minimally invasive, |
||
| leaderAndIsrs, coordinatorContext.getCoordinatorZkVersion()); | ||
| updatedBuckets.addAll(leaderAndIsrs.keySet()); | ||
| return updatedBuckets; | ||
| } catch (Exception e) { | ||
| if (ExceptionUtils.findThrowable(e, KeeperException.BadVersionException.class) | ||
| .isPresent()) { | ||
| LOG.error( | ||
| "Aborted batch LeaderAndIsr update during controlled shutdown because the coordinator was fenced.", | ||
| e); | ||
| return updatedBuckets; | ||
| } | ||
| LOG.warn( | ||
| "Failed to batch update LeaderAndIsr for {} buckets during controlled shutdown. Falling back to individual updates.", | ||
| leaderAndIsrs.size(), | ||
| e); | ||
| } | ||
|
|
||
| for (Map.Entry<TableBucket, LeaderAndIsr> entry : leaderAndIsrs.entrySet()) { | ||
| try { | ||
| zooKeeperClient.updateLeaderAndIsr( | ||
| entry.getKey(), | ||
| entry.getValue(), | ||
| coordinatorContext.getCoordinatorZkVersion()); | ||
| updatedBuckets.add(entry.getKey()); | ||
| } catch (Exception e) { | ||
| if (ExceptionUtils.findThrowable(e, KeeperException.BadVersionException.class) | ||
| .isPresent()) { | ||
| LOG.error( | ||
| "Stopped individual LeaderAndIsr updates during controlled shutdown because the coordinator was fenced.", | ||
| e); | ||
| break; | ||
| } | ||
| LOG.error( | ||
| "Failed to update bucket LeaderAndIsr for table bucket {} during controlled shutdown.", | ||
| stringifyBucket(entry.getKey()), | ||
| e); | ||
| } | ||
| } | ||
| return updatedBuckets; | ||
| } | ||
|
|
||
| /** | ||
| * Handle the state change of TableBucket. It's the core state transition logic of the state | ||
| * machine. It ensures that every state transition happens from a legal previous state to the | ||
|
|
@@ -214,7 +383,7 @@ private void doHandleStateChange( | |
| targetState, | ||
| String.format( | ||
| "Can't find partition name for partition: %s.", | ||
| tableBucket.getBucket())); | ||
| tableBucket.getPartitionId())); | ||
| return; | ||
| } | ||
| } | ||
|
|
@@ -347,7 +516,7 @@ public void batchHandleOnlineChangeAndInitLeader(Set<TableBucket> tableBuckets) | |
| BucketState.OnlineBucket, | ||
| String.format( | ||
| "Can't find partition name for partition: %s.", | ||
| tableBucket.getBucket())); | ||
| tableBucket.getPartitionId())); | ||
| continue; | ||
| } | ||
| } | ||
|
|
@@ -486,6 +655,32 @@ private Optional<ElectionResult> electNewLeaderForTableBuckets( | |
| LOG.error("Can't get state for table bucket {}.", stringifyBucket(tableBucket), e); | ||
| return Optional.empty(); | ||
| } | ||
| Optional<ElectionResult> optionalElectionResult = | ||
| electNewLeaderForTableBucket(tableBucket, leaderAndIsr, electionStrategy); | ||
| if (!optionalElectionResult.isPresent()) { | ||
| return Optional.empty(); | ||
| } | ||
| ElectionResult electionResult = optionalElectionResult.get(); | ||
| try { | ||
| zooKeeperClient.updateLeaderAndIsr( | ||
| tableBucket, | ||
| electionResult.leaderAndIsr, | ||
| coordinatorContext.getCoordinatorZkVersion()); | ||
| } catch (Exception e) { | ||
| LOG.error( | ||
| "Fail to update bucket LeaderAndIsr for table bucket {}.", | ||
| stringifyBucket(tableBucket), | ||
| e); | ||
| return Optional.empty(); | ||
| } | ||
| coordinatorContext.putBucketLeaderAndIsr(tableBucket, electionResult.leaderAndIsr); | ||
| return optionalElectionResult; | ||
| } | ||
|
|
||
| private Optional<ElectionResult> electNewLeaderForTableBucket( | ||
| TableBucket tableBucket, | ||
| LeaderAndIsr leaderAndIsr, | ||
| ReplicaLeaderElection electionStrategy) { | ||
| if (leaderAndIsr.coordinatorEpoch() > coordinatorContext.getCoordinatorEpoch()) { | ||
| LOG.error( | ||
| "Aborted leader election for table bucket {} since the bucket state path was " | ||
|
|
@@ -505,21 +700,7 @@ private Optional<ElectionResult> electNewLeaderForTableBuckets( | |
| stringifyBucket(tableBucket)); | ||
| return Optional.empty(); | ||
| } | ||
| ElectionResult electionResult = optionalElectionResult.get(); | ||
| try { | ||
| zooKeeperClient.updateLeaderAndIsr( | ||
| tableBucket, | ||
| electionResult.leaderAndIsr, | ||
| coordinatorContext.getCoordinatorZkVersion()); | ||
| } catch (Exception e) { | ||
| LOG.error( | ||
| "Fail to update bucket LeaderAndIsr for table bucket {}.", | ||
| stringifyBucket(tableBucket), | ||
| e); | ||
| return Optional.empty(); | ||
| } | ||
| coordinatorContext.putBucketLeaderAndIsr(tableBucket, electionResult.leaderAndIsr); | ||
| return Optional.of(electionResult); | ||
| return optionalElectionResult; | ||
| } | ||
|
|
||
| private boolean checkValidTableBucketStateChange( | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.