kafka 0.9 重写消费者设计

Rebalance是将一个group订阅的topic的一组分区分配给该group组内的consumer实例,使每个实例都拥有一个独立的互斥的分区。即:一个topic的分区只能被group的一个consumer实例消费,再rebalance之前不能再被group的其他consumer消费。
在一个消费group Reblance成功之后,所有被订阅主题的每一个分区在其Group内都有且只有一个consumer实例去消费。
Rebalance的工作方式如下
每个broker都当选为消费组(consumer group)中某个consumer的协调者(co-ordinator);当一个group中的consumer成员个数变化或者其订阅的topics的分区变化时,作为这个group协调者的broker负责编排Reblance运算。他还负责在参与Reblance运算的所有consumer和分区之间通信。

消费者(Consumer)
1、当消费者启动或者协调者故障时,consumer会发送一个ConsumerMetadataRequest给 bootstrap.brokers配置的列表中的任何一个broker。
2、然后消费者连接协调者并且发送一个心跳请求(HeartbeatRequest),如果返回的HeartbeatResponse中错误码为IllegalGeneration。这表示协调者启动了Reblance。然后消费者停止获取数据,提交offsets并且发送一个JoinGroupRequest 请求给协调者所在broker,在返回的JoinGroupResponse中,它收到它有全消费的topic的分区列表,和所在group的新的Id。此时,消费者开始读取数据并且提交分区的offsets。

3、如果在HeartbeatResponse中没有错误,消费者就继续从它最后拥有权限的分区中获取数据,而不会被中断。

协调者(Co-ordinator)

1、在稳定状态下,协调者通过它的故障检测协议跟踪每个group中每个消费者的健康状况。
2、在选举结束或者启动时,协调者会从zookeeper中读取它管理的group列表和它们的成员信息。如果一个group之前没有任何成员信息,它不做任何事情,直到某个组的第一个consumer使用它进行注册。

3、在协调者没有加载完它负责的所有group的成员信息之前,当consumer发送HearbeatRequests, OffsetCommitRequests 和JoinGroupRequests请求时,会返回CoordinatorStartupNotComplete错误码,然后consumer将会在之后进行重新请求。

4、选举工作结束后或启动时,协调者启动了group中的所有消费者故障检测。被标记为死亡的消费者会被从group中移除,并且会触发Rebalance操作。

5、平衡是通过在HeartbeatResponse返回IllegalGeneration错误代码触发。一旦所有活着的消费者通过协调者发送JoinGroupRequests请求要求重新注册,那么它在JoinGroupResponse中告诉每个consumer他们拥有的新的分区来完成Rebalance操作

6、消费者还监听所有有consumer消费的topics的分区变化。如果检测到任何一个topic有新的分区,都会出发Reblance操作。

Failure detection protocol

消费者在向协调者请求加入消费group时可以在JoinGroupRequest中指定session的超市时间。当一个消费者成功加入group,故障检测程序在消费者和协调者上启动。消费者会定期的发起心跳请求(HeartbeatRequest),每session.timeout.ms/heartbeat.frequency发送一次心跳并等待返回。如果协调者在session.timeout.ms期间没有收到来自消费者的心跳请求,就会标记这个消费者死亡。同样的,如果消费在没有在session.timeout.ms时间内收到HeartbeatResponse回应,它就会假设协调者已经挂了,并且开始co-ordinator rediscovery过程。heartbeat.frequency是配置在consumer端的。heartbeat.frequency要设置比较高的值,特别是如果session.timeout.ms也设置的比高大的情况下。但是,应注意heartbeat.frequency不要设置的太高。

接收ConsumerMetadataResponse或JoinGroupResponse之后,消费者定期发送一个HeartbeatRequest到协调者(everysession.timeout.ms/heartbeat.frequency毫秒)。
收到请求的心跳后,协调者检查生成编号,消费者ID和消费group。如果消费者指定了无效或失效代Id,在HeartbeatResponse中会返回IllegalGeneration错误代码。
如果协调者未在session.timeout.ms内接收到任何一次来自消费者心跳请求,它标志着消费者已死并触发组的Rebalance。
如果消费者没有在 session.timeout.ms实践内收到协调者的HeartbeatResponse,或发现socket被关闭,它认为协调者已经挂掉了并触发重新发现协调者的过程。

注意,在协调者故障转移时,消费者可能会发现新的协调者,这个新的协调者可能还没有完成故障转或者已经完成故障转移,包括从ZK中服务消费群(consumer group )的元数据。在后一种情况下,协调者只能接受正常的心跳请求,前一种情况协调者可能会拒绝请求,导致消费者会重新执行发现过程并且重新连接,这比较正常。然而,如果消费者太晚去连接一个新的协调者,协调者会编辑这个消费者死亡,并且触发Rebalance操作。
状态图State diagram

消费者

Here is a description of the states -
Down - The consumer process is down

在启动了和寻找协调者状态,消费者为所在消费组寻找协调者。一旦它发现了协调了消费者就会发送JoinGroupRequest(没有消费ID)。如果有消费者在同组指定的分区分配策略有冲突,那么JoinGroupRequest请求就会收到InconsistentPartitioningStrategy错误代码。如果在JoinGroupRequest请求中指定的分区分配策略(PartitionAssignmentStrategy )Broker无法识别,则会返回UnknownPartitioningStrategy错误代码。在这些情况下,消费者无法加入消费组。

在已经加入组这个状态下,如果消费者收到的JoinGroupResponse没有错误代码,而有consumer id和消费组的generation id,那么消费者就会成为组的一个成员。在这种状态下,消费者发送心跳请求,根据收到的错误代码,消费者将保持这种状态,或者被协调者标记死亡停止消费,或者发起重新发现协调者过程。

在重新寻找协调者状态。消费者不会停止消费,但是会试图通过发送ConsumerMetadataRequest 请求去发起重寻协调者的过程,并且会一直等待一个没有错误代码的响应。

停止消费状态。消费者停止消费并且提交offsets,直到它再次重新加入消费组。

kafka 0.9 重写消费者设计_第1张图片

协调者

挂机(Down)--协调者死亡或者降级。
Catch up--协调者被选举但是还没准备好接受请求。
Ready--新当选的协调者完成加载它负责的所有消费组的元数据。
Prepare for rebalance--对于消费组内的所有消费者的心跳请求,协调者在心跳响应(HeartbeatResponse )中发送IllegalGeneration错误代码,并且等待消费者发送一个JoinGroupRequest请求。
Rebalancing--协调者从消费者那里收到一个JoinGroupRequest请求,重新生成一个组generation id,指定消费ID( consumer ids)并且分配消费分区。
稳定(Steady)--协调者从每一个消费组的所有消费者那里接收OffsetCommitRequests 请求和心跳请求。

kafka 0.9 重写消费者设计_第2张图片

消费者ID分配

启动后,消费者在从协调者那里收到的第一个JoinGroupResponse中得到它的Consumer Id。从这时起,消费者每次在发送HeartbeatRequest 和OffsetCommitRequest时请求中必须包含consumer id。如果协调者收到HeartbeatRequest或者OffsetCommitRequest中的Consumer Id和消费组中的任何一个都不一样,则会在该响应中发送一个UnknownConsumer错误代码。

协调者在Rebalance成功时会分配一个consumer id给消费者,并在JoinGroupResponse中返回。消费者可以选择在以后每次JoinGroupRequest中包含这个consumer id直到关机或死亡,这样做的好处是在进行rebalance操作时会有更低的延迟。当rebalance被触发,协调者会等待前一轮所有的消费者发送JoinGroupRequest,确定消费者是通过consumer id的方式。如果消费者选择发送JoinGroupRequest不包含consumer id,协调者会完全在配置的session.timeout.ms的时间中等待,才会进行重新rebalance操作。它这么做的原因是无法印证一个不存在consumer id的JoinGroupRequest的消费者来源,这使得rebalance有个高(视session.timeout.ms)的延迟。在另一方面,如果消费者在之后每次JoinGroupRequests中都包含了consumer id,协调者能立即识别消费者,并且一旦所有已知的消费者都发送了JoinGroupRequest就会立刻发起rebalance操作。

在收到消费组中所有存在的消费者的JoinGroupRequest请求之后,协调者开始分配consumer id。假设,消费者是新启动的或者是选择不发送之前非配的consumer ID,在这个时候,它给在每个在JoinGroupRequest中没有consumer id的消费者指定一个新的Id-

如果在JoinGroupRequest 中指定的consumer id和组内成员所有的id都不匹配。协调者会在JoinGroupResponse 响应中发送UnknownConsumer 错误代码,并且拒绝消费者加入消费组。这不会导致组内其他消费者的rebalance操作,但是也不会允许这样的消费者加入现有组。

请求格式

对于每一个消费组,协调者存储一下信息:
1)对于每个消费组,元数据包括:

      组订阅的topic列表
      组的配置,包括session timeout等。

组中每一个消费者的元数据,包括hostname和consumer id。
每一个topic 分区消费的当前的offsets。
分区所有权元数据,包括消费者-分配-分区( consumer-assigned-partitions )映射。
2)对于每个主题,当前订阅它的消费组的列表。

It is assumed that all the following requests and responses have the common header. So the fields mentioned exclude the ones in the header.
ConsumerMetadataRequest

{
  GroupId                => String
}

ConsumerMetadataResponse

{
  ErrorCode              => int16
  Coordinator            => Broker
}

JoinGroupRequest

{
  GroupId                     => String
  SessionTimeout              => int32
  Topics                      => [String]
  ConsumerId                  => String
  PartitionAssignmentStrategy => String
 }

JoinGroupResponse

{
  ErrorCode              => int16
  GroupGenerationId      => int32
  ConsumerId             => String
  PartitionsToOwn        => [TopicName [Partition]]
}
TopicName => String
Partition => int32

HeartbeatRequest

{
  GroupId                => String
  GroupGenerationId      => int32
  ConsumerId             => String
}

HeartbeatResponse

{
  ErrorCode              => int16
}

OffsetCommitRequest (v1)

OffsetCommitRequest => ConsumerGroup GroupGenerationId ConsumerId [TopicName [Partition Offset TimeStamp Metadata]]
  ConsumerGroup => string
  GroupGenerationId => int32
  ConsumerId => String
  TopicName => string
  Partition => int32
  Offset => int64

TimeStamp => int64
Metadata => string

配置

服务端配置

This list is still in progress
max.session.timeout - 任何组的session timeout不应高于此值以减少对broker的开销。如果高于此值, the broker将会在JoinGroupResponse中返回SessionTimeoutTooHigh错误码。
partition.assignment.strategies - 都好分割的分区策略,用户可自定义,也可以使用kafka提供的默认策略。当消费者在JoinGroupRequest指定了分区策略。它必须使用该配置列表中存在的策略,否则将收到一个UnknownPartitionAssignmentStrategyException异常。

通配符订阅

基于通配符订阅(比如,白名单和黑名单),消费者有责任通过topic元数据请求检测匹配的topics。也就是说,它的topic metadata request会包含一个空的topic列表,它的响应会返回所有主题的分区信息,它会过滤topics选出和通配符匹配的主题,然后使用subscribe()接口更新订阅列表。同样,如果订阅列表改变了,它将会触发Reblance
考虑下有趣的场景Interesting scenarios to consider

协调者故障或者协调者断开了连接

协调者故障,控制器自动为由于协调者故障而影响的消费组选取新的leader。协调者从zookeeper读取它负责的消费组的元数据。包括consumer ids,the generation id和订阅的主题列表。知道协调者从zookeeper中读取了所有元数据,它才会在HeartbeatResponse中返回CoordinatorStartupNotComplete错误码。在这段时间内消费者的JoinGroupRequest都是非法的。

假如在broker通过UpdateMetadataRequest请求从控制器(zk?)更新消费组元数据之前,一个消费者向broker发送一个ConsumerMetadataRequest请求。ConsumerMetadataResponse将会返回一个过时的协调者信息,消费者在发送心跳,或者提交offset时将会受到一个NotCoordinatorForGroup错误代码。在收到NotCoordinatorForGroup错误代码后,消费者回滚并且重新发送ConsumerMetadataRequest请求。

在协调者故障或者重寻期间,消费者不会停止读取数据。

订阅的主题分区变化

组的协调者发现它订阅的主题的分区发生了变化。
协调者标记组准备Rebalance并在HeartbeatResponse中发送IllegalGeneration错误代码,然后消费者停止获取数据,提交offsets并且向协调者发送一个JoinGroupRequest。

协调者等待组内所有的消费者发送JoinGroupRequest。一旦收到所有预期的JoinGroupRequests,它在ZK中的组generation id增加。计算新分区分配,并且在JoinGroupResponse中返回更新后的分区分配和一个新的generation id。请注意:即使组内消费者成员没有改变generation id也会递增。

在收到JoinGroupResponse后,消费者在本地存储新的 generation id和consumer id并且开始从返回的分区列表中获取数据。在后面的JoinGroupResponse请求中将会使用新的generation id和consumer id。

rebalance期间的Offset提交

如果消费者受到IllegalGeneration错误代码,就停止获取数据并且在发送JoinGroupRequest之前提交offsets。

协调者会检查OffsetCommitRequest中的generation id,如果比协调者中的generation id高的时候会拒绝请求。这意味着在消费者代码中有bug。

协调者也不允许在提交offset请求时候的generation id比当前组在ZK中存储的generation id(身份认证)旧。在rebalance期间这个约束不是一个问题,从第一个消费者发送JoinGroupRequest到最后一个,协调者不会对组的generation id进行自增,从此时直到协调者返回JoinGroupResponse,它都不会从当前generation的组中的任何消费者那里接受OffsetCommitRequests请求。所以消费者发送的OffsetCommitRequest中的generation id应该和当前协调者中的匹配。

另外一种值得一说的情况是当消费者在rebalance期间有一个软错误,比如长时间的GC。如果一个消费者停顿的时间超过了session.timeout.ms配置,协调者就不会在session.timeout.ms时间内收到消费者的JoinGroupRequest请求。协调者标记这个消费者死亡,并且根据在新一轮(new generation)发送JoinGroupRequest的消费者完成rebalance操作。

Heartbeats during rebalance

Consumer periodically sends a HeartbeatRequest to the coordinator every session.timeout.ms/hearbeat.frequency milliseconds. If the consumer receives the IllegalGeneration error code in the HeartbeatResponse, it stops fetching, commits offsets and sends a JoinGroupRequest to the co-ordinator. Until the consumer receives a JoinGroupResponse, it does not send any more HearbeatRequests to the co-ordinator.
A higher heartbeat.frequency ensures lower latency on a rebalance operation since the co-ordinator notifies a consumer of the need to rebalance only on a HeartbeatResponse.
The co-ordinator pauses failure detection for a consumer that has sent a JoinGroupRequest until a JoinGroupResponse is sent out. It restarts the hearbeat timer once the JoinGroupResponse is sent out and marks a consumer dead if it does not receive a HeartbeatRequest from that time for another session.timeout.ms milliseconds.The reason the co-ordinator stops depending on heartbeats to detect failures during a rebalance is due to a design decision on the broker's socket server - Kafka only allows the broker to read and process one outstanding request per client. This is done to make it simpler to reason about ordering. This prevents the consumer and broker from processing a heartbeat request at the same time as a join group request for the same client. Marking failures based on JoinGroupRequest prevents the co-ordinator from marking a consumer dead during a rebalance operation. Note that this does not prevent the rebalance operation from finishing if a consumer goes through a soft failure during a rebalance operation. If the consumer pauses before it sends a JoinGroupRequest, the co-ordinator will mark it dead during the rebalance and complete the rebalance operation by including the rest of the consumers in the new generation. If a consumer pauses after it sends a JoinGroupRequest, the co-ordinator will send it the JoinGroupResponse assuming the rebalance completed successfully and will restart it's heartbeat timer. If the consumer resumes before session.timeout.ms, consumption starts normally. If the consumer pauses for session.timeout.ms after that, then it is marked dead by the co-ordinator and it will trigger a rebalance operation.
The co-ordinator returns the new generation id and consumer id only in the JoinGroupResponse. Once the consumer receives a JoinGroupResponse, it sends the next HeartbeatRequest with the new generation id and consumer id.
Co-ordinator failure during rebalance

一个rebalance操作要经过几个阶段
1、协调者接收rebalance的通知。无论是ZK监听到一个主题/分区改变,或者注册一个新的消费者又或是一个消费者死亡。
2、协调者在HeartbeatResponse中返回IllegalGeneration错误码来初始化rebalance操作。
3、消费者发送JoinGroupRequest。
4、协调者在ZK中自增组的generation id,并且告诉ZK分区的新所有权。
5、协调者返回JoinGroupResponse。

协调者可能在上面任何一步失败:
If the co-ordinator fails at step #1 after receiving a notification but not getting a chance to act on it, the new co-ordinator has to be able to detect the need for a rebalance operation on completing the failover. As part of failover, the co-ordinator reads a group's metadata from zookeeper, including the list of topics the group has subscribed to and the previous partition ownership decision. If the # of topics or # of partitions for the subscribed topics are different from the ones in the previous partition ownership decision, the new co-ordinator detects the need for a rebalance and initiates one for the group. Similarly if the consumers that connect to the new co-ordinator are different from the ones in the group's generation metadata in zookeeper, it initiates a rebalance for the group.
If the co-ordinator fails at step #2, it might send a HeartbeatResponse with the error code to some consumers but not all. Similar to failure #1 above, the co-ordinator will detect the need for rebalance after failover and initiate a rebalance again. If a rebalance was initiated due to a consumer failure and the consumer recovers before the co-ordinator failover completes, the co-ordinator will not initiate a rebalance. However, if any consumer (with an empty or unknown consumer id) sends it a JoinGroupRequest, it will initiate a rebalance for the entire group.
If a co-ordinator fails at step #3, it might receive JoinGroupRequests from only a subset of consumers in the group. After failover, the co-ordinator might receive a HeartbeatRequest from all alive consumers OR JoinGroupRequests from some. Similar to #1, it will trigger a rebalance for the group.
If a co-ordinator fails at step #4, it might fail after writing the new generation id and group membership in zookeeper. The generation id and membership information is written in one atomic zookeeper write operation. After failover, the consumer will send HeartbeatRequests to the new co-ordinator with an older generation id. The co-ordinator triggers a rebalance by returning an IllegalGeneration error code in the response that causes the consumer to send it a JoinGroupRequest. Note that this is the reason why it is worth sending both the generation id as well as the consumer id in the HeartbeatRequest and OffsetCommitRequest
If a co-ordinator fails at step #5, it might send the JoinGroupResponse to only a subset of the consumers in a group. A consumer that received a JoinGroupResponse will detect the failed co-ordinator while sending a heartbeat or committing offsets. At this point, it will discover the new co-ordinator and send it a heartbeat with the new generation id. The co-ordinator will send it a HeartbeatResponse with no error code at this point. A consumer that did not receive a JoinGroupResponse will discover the new co-ordinator and send it a JoinGroupRequest. This will cause the co-ordinator to trigger a rebalance for the group.

慢消费者Slow consumers

如果没有在session.timeout.ms时间内收到心跳请求,协调者可以将慢消费者从组中移除。通常,如果消息处理比session.timeout.ms慢,就会成为慢消费者。导致两次poll()方法的调用间隔比session.timeout.ms时间长。由于心跳只在 poll()调用时才会发送,这就会导致协调者标记慢消费者死亡。下面是协调者如何处理一个慢消费者:
如果没有在session.timeout.ms时间内收到心跳请求,协调者标记消费者死亡并且断开和它的连接。
同时,通过向组内其他消费者的HeartbeatResponse中发送IllegalGeneration 错误代码 触发rebalance操作。
如果慢消费者在协调者收到组内其他任何消费者的HeartbeatRequest之前发送了心跳请求,它会取消Rebalance操作的意图,并在HeartbeatResponses 中不会发送错误码。
如果不是,协调者依然会进行Rebalance操作。并且会给慢消费者也返回IllegalGeneration 错误码。
由于协调者只能从活着的消费者那里等待JoinGroupRequest请求,一旦受到其他消费者的加入请求就会完成rebalance操作。如果慢消费者恰好也发送了JoinGroupRequest,协调者就会在当前一轮包含它,反之就不会包含这个慢消费者。
如果协调者已经返回了JoinGroupResponse,它会在当前Rebalance完成之后,再触发新的Rebalance操作。
如果本轮需要很长时间,慢消费者的接收JoinGroupResponse超时,它就会发起重寻协调者,并且给协调者重新发送JoinGroupRequest。

你可能感兴趣的:(kafka 0.9 重写消费者设计)