KAFKA-20865: Do not unregister KRaft voter set members - #23152
Conversation
junrao
left a comment
There was a problem hiding this comment.
@kevin-wu24 : Thanks for the PR. Left a few comments.
| * <li>{@link org.apache.kafka.common.errors.InvalidRequestException} | ||
| * If the request tries to unregister the current active controller id.</li> | ||
| * If the request tries to unregister the current active controller id or a controller id | ||
| * which is part of the voter set.</li> |
There was a problem hiding this comment.
The latter covers the former. So, we can just say "If controller id is part of the voter set." Ditto in Controller.
| import java.util.function.Supplier; | ||
|
|
||
| /** | ||
| * Checks if the provided node id is a voter according to the raft client. |
There was a problem hiding this comment.
Hmm, this comment seems misplaced. There is no node id provided to the supplier.
| int nodeId, | ||
| Map<String, VersionRange> localSupportedFeatures, | ||
| List<Integer> quorumNodeIds | ||
| Supplier<Set<Integer>> votersSupplier |
There was a problem hiding this comment.
Does the mean Feature update is broken without this PR in dynamic quorum? In dynamic quorum, the passed in quorumNodeIds is empty and that list is used in the following code for feature verifciation.
FeatureControlManager.reasonNotSupported (:360-362)
for (int id : quorumFeatures.quorumNodeIds()) {
if (!foundControllers.contains(id))
return Optional.of("controller " + id + " has not registered, ...");
}
There was a problem hiding this comment.
Does the mean Feature update is broken without this PR in dynamic quorum?
I'm not sure if I would use the word broken. This behavior has existed since dynamic quorum was released, so you can argue it is not a regression. controller.quorum.voters, whose ids ultimately populate quorumNodeIds, can be set on a dynamic quorum cluster, just not on a brand new cluster. For example, if you upgrade from kraft.version=0 to kraft.version=1, you can leave controller.quorum.voters as is, since it will be ignored. This check specifically blocks feature upgrades if not all of the active controller's controller.quorum.voters members have registered with the node. If that configuration is empty, then this check is a no-op.
| MetadataVersion.MINIMUM_VERSION.featureLevel(), | ||
| MetadataVersion.latestProduction().featureLevel())); | ||
| quorumFeatures = new QuorumFeatures(0, localSupportedFeatures, List.of(0)); | ||
| quorumFeatures = new QuorumFeatures(0, localSupportedFeatures, () -> Set.of(0)); |
There was a problem hiding this comment.
This is an existing issue. It's a bit weird to provide a supplier for testing in the production code. It will be useful to at least add a comment.
| return true; | ||
| } | ||
| if (featureControl.isControllerId(nodeId)) { | ||
| if (featureControl.isVoterId(nodeId)) { |
There was a problem hiding this comment.
This is an existing issue, isVoterId() is true doesn't mean the voter is registered. Should we rename this method to sth like isNodeIdKnown()?
There was a problem hiding this comment.
Yeah, I can rename it. There is only one caller of this method, and I think the only issue with isNodeIdRegistered is that the name is not an accurate description of what the method evaluates.
| () -> { | ||
| if (nodeId == controllerId) { | ||
| throw new InvalidRequestException("Controller cannot unregister itself while it is active."); | ||
| } else if (featureControl.isVoterId(controllerId)) { |
There was a problem hiding this comment.
The check before this line now seems redundant.
| return nodeId == other.nodeId && | ||
| localSupportedFeatures.equals(other.localSupportedFeatures) && | ||
| quorumNodeIds.equals(other.quorumNodeIds); | ||
| localSupportedFeatures.equals(other.localSupportedFeatures); |
There was a problem hiding this comment.
Could we add a comment that votersSupplier is excluded deliberately?
|
@junrao Thanks for the review. I pushed a commit to address your comments. Please take a look when you have time. |
junrao
left a comment
There was a problem hiding this comment.
@kevin-wu24 : Thanks for the updated PR. Just a minor comment.
| * Checks if a node id is registered as a broker, controller in static/dynamic quorum. | ||
| */ | ||
| private boolean isNodeIdRegistered(int nodeId) { | ||
| private boolean isNodeIdKnown(int nodeId) { |
There was a problem hiding this comment.
Could we adjust the comment above too?
junrao
left a comment
There was a problem hiding this comment.
@kevin-wu24 : Thanks for the updated PR. LGTM. Could we update the changes in the original KIP and send a summary to the KIP mailing list?
Updated both the KIP and send a message to the mailing list. Thanks again @junrao for the reviews on this feature. |
What changed
Follow up PR to c274a73, which implements controller unregistration. This PR adds a check so that KRaft voter set members cannot be unregistered, which can limit the severity of weird edge cases around races between unregistration requests and feature upgrades. Voters who are unregistered and then re-register with a cluster who completed an unsupported feature upgrade thanks to the unregistration would experience unavailability, which affects quorum health.
The KRaft voter set is internal to the
/raftmodule, so this PR adds theRaftClientVotersSupplieralongside some new publicRaftClientAPI to expose the latest voter set. Only the latest is necessary, because withkraft.version=1enabled, the controller being unregistered depends only on the uncommitted voter set when it is present, described below.When dynamic quorum is enabled, the unregistration voter check only needs to check the uncommitted voter set, if one exists. This is because the unregistration record will be written, and thus committed, after the uncommitted
VotersRecordif one exists. The logic is as follows for unregistering controller X, with the latest CVS (committed voter set) and UVS (uncommitted voter set):CVS.contains(X) && UVS.contains(X): cannot unregister, because X is definitely a voterCVS.contains(X) && !UVS.contains(X): can unregister, because the offset of X's unregistration's record will come after its removal from the voter set. X's unregistration cannot be committed without the new voter set also being committed first.!CVS.contains(X) && UVS.contains(X): cannot unregister, because committing both the new voter set and X's unregistration results in unregistering a voter!CVS.contains(X) && !UVS.contains(X): can unregister, because X is definitely not a voterSo long as UVS is present, X's unregistration request validity only depends on that whether or not X is contained in UVS. This also applies to the registration check that is needed for feature upgrades. If there is an uncommitted voter set, all of that set's voters must be registered to the cluster in order to upgrade a feature to a certain value, since those
FeatureLevelRecordswill be committed after the uncommitted voter set.Out of scope (for now)
Concurrent
AddVoterandUnregisterControllercan lead to a voter set member being unregistered. This is because the KRaft voter set history that the controller layer can access lags slightly behind theBatchAccumulator. Given a starting voter set CVS that does not contain the observer controller X, it is possible for X to be unregistered at offset N and then added to the voter set at offset N' where N' > N. In this case, the consequence is the same as before this PR, where controller X would see its unregistration and then register again.There is not a great solution for this without leaking metadata state to KRaft. The interleaved requests are also unlikely to occur, since
UnregisterControlleris likely to be sent alongsideRemoveVoter, notAddVoter. This interleaving withRemoveVotercannot result in an unregistered controller in the voter set.Testing
KRaftClusterTestReviewers: Jun Rao junrao@gmail.com