You are viewing an old version of this page. View the current version.

Compare with Current View Page History

« Previous Version 4 Next »

The following is a draft design that uses a high-available consumer coordinator at the broker side to handle consumer rebalance. By migrating the rebalance logic from the consumer to the coordinator we can resolve the consumer split brain problem and help thinner the consumer client.

Overview:

One of the brokers is elected as the coordinator for all the consumer groups. It will be responsible for:

  1. Watch for new topics and new groups
  2. Watch for consumer group member changes and topic partition changes
  3. Rebalance logic for affected groups in response to watched change events
  4. Communicate the rebalance results to consumers

When a coordinator decides a rebalance is needed for certain group, it will first sends the stop-fetcher command to each consumers in the group (this communication channel is currently implemented using a ZK based queue), and then sends the start-fetcher command to each consumers with the assigned partitions. Each consumers will only receive the partition info of the partitions that are assigned to itself. The coordinator will finish the rebalance by waiting for all the consumers to finish starting the fetchers and respond (currently implemented as cleaning the queue).

Paths Stored in ZK:

Most of the original ZK paths storage are kept, in addition to the following paths:

  1. Coordinator path: stores the current coordinator info.  

/consumers/coordinator --> brokerId (ephemeral; created by coordinator)

  1. ConsumerChannel path: stores the coordinator commands to the consumer.    

/consumers/groups/group/channel/consumer/commandId --> command (sequential persistent; created by coordinator, removed by consumer)

Coordinator:

The consumer coordinator keeps the following fields:

coordinatorElector : ZkElection                     // A ZK based elector using the coordinator path mentioned above

groupsBeingRebalanced : Map[String, AtomicBoolean]  // For each group, a bit indicating if the group is under rebalancing

consumerGroupsPerTopic : Map[String, Set[String]]   // For each topic, the consumer groups that are interested in the topic

groupsWithWildcardTopics : Set[String]              // Groups that has wildcard interests for topics

rebalanceRequestQ : LinkedBlockingQueue[String]     // A blocking queue storing all the rebalance requests, the request just contain the group name

requestHandler : RebalanceRequestHandler            // A thread handling all the rebalance requests read from the rebalanceReques
A. On Coordinator Startup

Every server will create an coordinator instance as its member, whose construction function will only initialize the coordinatorElector by passing a callback function called coordinatorStartup.

The coordinatorElector, upon initialization, will immediately try to become the leader. If someone else has become the leader, it will listen to the coordinator path for data change, and try to re-elect whenever the current elector resigns (i.e. the data on the path is deleted).

Whenever it elects to become the leader, it will trigger the callback function that is provided by its caller, i.e. the coordinator.

coordinatorStartup :

1. Read all the topics from ZK and initialize consumerGroupsPerTopic

2. Read all the consumer groups from ZK

2.1 Get the current interested topics of each group, update consumerGroupsPerTopic by adding the group to each topic's interested group list

2.2 If the group has some consumer specifying wildcard topic-counts, then add the group to groupsWithWildcardTopics

2.3 Always try to rebalance every group by adding (group -> new AtomicBoolean(true)) to groupsBeingRebalanced and put group to rebalanceRequestQ

3. Register listeners for topics and their partition changes

3.1 Subscribe TopicChangeListener to /brokers/topics

3.2 Subscribe TopicPartitionChangeListener to each /brokers/topics/[topic]

4. Register listeners for consumer groups and their member changes

4.1 Subscribe registerGroupChangeListener to /consumers/groups/

4.2 Subscribe registerGroupMemeberChangeListener to each /consumers/groups/[groups]/ids

5. Register session expiration listener

6. Initialize and start the requestHandler thread
B. On Coordinator Change/Failover

Whenever the current coordinator's hosted server dies, other coordinator's elector will realize that through the ZK listener and will try to re-elect to be the leader, and whoever wins will trigger the callback function coordinatorStartup.

When the dead server comes back, the zkClient will atomically reconnect to it and trigger the handleNewSession function.

handleNewSession :

1. Reset its state by clearing consumerGroupsPerTopic, groupsWithWildcardTopics and rebalanceRequestQ, etc

2. Re-register the session expiration listener (this is because ZkClient does not re-register itself once fired)

3. Try to re-elect to be the coordinator by directly calling the elect function of its coordinatorElector.
C. On ZK Watcher Fires

Handle group change

GroupChangeListener.handleChildChange :

1. Get the newly added group (since /consumers/groups are persistent nodes, no groups should be deleted even if there is no consumers any more inside the group)

2. Subscribe the registerGroupMemeberChangeListener on /consumers/groups/group

3. Read all the topics this group is interested in, for each topic:

3.1 If the topic already exists in consumerGroupsPerTopic, update its list by adding this group

3.2 If the topic is not in consumerGroupsPerTopic yet, add the entry (topic \-> Set(group))

4. If some of this group's consumers has wildcard interests, add that to groupsWithWildcardTopics

5. If the group already has some interested existed topics, put (group \-> new AtomicBoolean(true)) to groupsUnderRebalance, and put the group to rebalanceRequestQ;

   Otherwise just put (group -> new AtomicBoolean(false)) to groupsUnderRebalance

Handle group member change

GroupMemberChangeListener.handleChildChange :

1. If some topics are no longer interested due to the deletion of some consumer, update consumerGroupsPerTopic by removing the group from these topics' list

2. If the group no longer contain any consumer, do nothing;

   Otherwise if groupsBeingRebalanced(group).compareAndSet(false, true) succeeds, put the group to rebalanceRequestQ.

Handle topic change

TopicChangeListener.handleChildChange :

1. Get the newly added topic (since /brokers/topics are persistent nodes, no topics should be deleted even if there is no consumers any more inside the group)

2. For each newly added topic:

2.1 Subscribe TopicPartitionChangeListener to /brokers/topics/topic

2.2 Get the set of groups that are interested in this topic from both consumerGroupsPerTopic(topic) and groupsWithWildcardTopics (filtered by wildcard pattern regex), and try to request rebalance for each group*

* By trying to request rebalance, we do the following:

if (groupsBeingRebalanced(group).compareAndSet(false, true)) rebalanceRequestQ.put(group)

Handle topic partition change

TopicPartitionChangeListener.handleChildChange :

Get the set of groups that are interested in this topic from consumerGroupsPerTopic(topic) and groupsWithWildcardTopics (filtered by wildcard pattern regex), and try to request rebalance for each group
D. On Rebalance Handling

The requestHandler thread keep block-reading from rebalanceRequestQ, and for each rebalance request for a specific group it calls the rebalance function.

If the rebalance succeeds it will reset groupsBeingRebalanced(group); otherwise it will retry rebalance again.

If the handler cannot finish rebalance successfully with config.maxRebalanceRetries retries, it will throw a ConsumerRebalanceFailedException.

rebalance (group) :

1. Get the topics that are interested by the group.

2. Compute the new ownership assignment after reading from ZK the number of partitions and number of threads for each topic.

3. Check if a rebalance is necessary by trying to get the current ownership from ZK for each topic.

3.1 If there is no registered ownership info in ZK, rebalance is necessary

3.2 If some partitions are not owned by any threads, rebalance is necessary

3.3 If some partitions registered in the ownership map do not exist any longer, rebalance is necessary

3.4 If ownership map do not match with the new one, rebalance is necessary

3.5 Otherwise rebalance is not necessary

4. If rebalance is necessary, do the following

4.1 For each consumer in the group, send the "stop-fetcher" command (details of communication is introduced later)

4.2 Then for each consumer in the group, send the "start-fetcher" command with part of the newly computed ownership specific to the consumer

4.3 Then wait until all the consumer has finished starting the fetcher (details of waiting is introduced later)

5. If waiting has timed out, return false; otherwise return true.

Consumer:

A. On Consumer Startup

B. On Consumer Failover

Coordinator-Consumer Communication

A. Coordinator Commands Format:

CommandCommand {

  option                                  :  String                                                                                              // Can either be "start-fetcher" or "stop-fetcher"

  ownershipMap                  :  Map[topicPartition: String  =>  consumerThread: String]      // a map of owned partitions to consumer threads, only available for "start-fetcher"

}

B. Coordinator-Side Sending Commands to Consumer

C. Consumer-Side Handling of Coordinator Commands

  • No labels