kafka设计解析(二)

1. kafka在zk中的存储结构

http://www.cnblogs.com/byrhuangqiang/p/6367037.html

  1. /broker/ids/[0…N]

    broker 注册信息。 其中[0…N]表示broker id(broker id唯一,不可以重复)

  2. /broker/topics/[topic]/partitions/[0…N]

    Broker Topic 注册。

  3. /brokers/topics/[topic]/partitions/[partitionId]/state

    partitions状态信息。

     {
         "controller_epoch": 表示kafka集群中的中央控制器选举次数,`
         "leader": 表示该partition选举leader的brokerId,
         "version": 版本编号默认为1,
         "leader_epoch": 该partition leader选举次数,
         "isr": [同步副本组brokerId列表]
     }
    
  4. /consumers/[group_id]/ids/[consumer_id]

    Consumer 注册.

  5. /consumers/[group_id]/offsets/[topic]/[partition_id]

    跟踪每个Consumer目前所消费的partition中的最大offset.

  6. /consumers/[group_id]/owners/[topic]/[partition_id]

    Partition Owner 注册.首先进行"Consumer Id注册"; 然后在"Consumer id 注册"节点下注册一个watch用来监听当前group中其他consumer的"退出"和"加入";只要此znode path下节点列表变更,都会触发此group下consumer的负载均衡.(比如一个consumer失效,那么其他consumer接管partitions).

  7. /controller_epoch

    kafka集群中第一个broker第一次启动时为1,以后只要集群中center controller中央控制器所在broker变更或挂掉,就会重新选举新的center controller,每次center controller变更controller_epoch值就会 + 1;

  8. /controller

    Controller注册信息。包含version, brokerid, timestamp.

  9. /admin/reassign_partitions

    用于将一些Partition分配到不同的broker集合上。对于每个待重新分配的Partition,Kafka会在该znode上存储其所有的Replica和相应的Broker id。该znode由管理进程创建并且一旦重新分配成功它将会被自动移除。

  10. /admin/preferred_replica_election 和 /admin/delete_topics

    Admin目录下的znode只有在有相关操作时才会存在,操作结束时会将其删除

详见https://blog.csdn.net/lizhitao/article/details/23744675
http://kafka.apache.org/documentation/
https://cwiki.apache.org/confluence/display/KAFKA/Kafka+data+structures+in+Zookeeper

2. 复制集的几个问题

  1. 如何将所有Replica均匀分布到整个集群?

    Kafka分配Replica的算法如下:

    • 将所有Broker(假设共n个Broker)和待分配的Partition排序
    • 将第i个Partition分配到第(i mod n)个Broker上
    • 将第i个Partition的第j个Replica分配到第((i + j) mod n)个Broker上
  2. 在向Producer发送ACK前需要保证有多少个Replica已经收到该消息?

    Leader收到了ISR中的所有Replica的ACK之后。

    Kafka的复制机制既不是完全的同步复制,也不是单纯的异步复制。同步复制影响吞吐率,异步会丢数据。
    同步复制要求所有能工作的Follower都复制完,这条消息才会被认为commit。Follower异步的从Leader复制数据,数据只要被Leader写入log就被认为已经commit,这种情况下如果Follower都复制完都落后于Leader,而如果Leader突然宕机,则会丢失数据。

  3. 怎样处理某个Replica不工作的情况?

    Leader会跟踪与其保持同步的Replica列表,该列表称为ISR(即in-sync Replica)。如果一个Follower宕机,或者落后太多,Leader将把它从ISR中移除。

    “落后太多”指Follower复制的消息落后于Leader后的条数超过预定值(该值可在 $KAFKA_HOME/config/server.properties中通过replica.lag.max.messages配置,其默认值是4000)或者Follower超过一定时间(该值可在$KAFKA_HOME/config/server.properties中通过replica.lag.time.max.ms来配置,其默认值是10000)未向Leader发送fetch请求。

  4. 怎样处理Failed Replica恢复回来的情况?

    // @todo

3. kafka leader选举机制

主要包括Controller的选出过程,和由Controller来主导的partition leader的选举。 主要参考。

3.1 为什么用Controller来选举?

最简单最直观的方案是,所有Follower都在Zookeeper上设置一个Watch,一旦Leader宕机,其对应的ephemeral znode会自动删除,此时所有Follower都尝试创建该节点,而创建成功者(Zookeeper保证只有一个能创建成功)即是新的Leader,其它Replica即为Follower。

但是该方法会有3个问题:

  • split-brain 脑裂。这是由Zookeeper的特性引起的,虽然Zookeeper能保证所有Watch按顺序触发,但并不能保证同一时刻所有Replica“看”到的状态是一样的,这就可能造成不同Replica的响应不一致
  • herd effect 羊群效应(从众)。如果宕机的那个Broker上的Partition比较多,会造成多个Watch被触发,造成集群内大量的调整。
  • Zookeeper负载过重 每个Replica都要为此在Zookeeper上注册一个Watch,当集群规模增加到几千个Partition时Zookeeper负载会过重。

Kafka 0.8.*的Leader Election方案解决了上述问题,它在所有broker中选出一个controller,所有Partition的Leader选举都由controller决定。controller会将Leader的改变直接通过RPC的方式(比Zookeeper Queue的方式更高效)通知需为此作出响应的Broker。 这样,其他broker几乎不用监听zookeeper的状态变化,

3.2 Controller 的主要工作

Kafka:The Definitive Guide

集群控制器Controller也是一个broker,在承担一般的broker职责外,它还负责选举分区的leader replica主副本。集群中的broker尝试在Zookeeper的/controller路径下创建一个临时节点,先创建成功者成为Controller。当其他broker启动时,也会试图在/controller下创建一个临时节点,但是会收到一个“节点已存在”的异常,这样便知道当前已经存在集群控制器。这些broker会监听Zookeeper的这个/controller临时节点,当controller发生故障时,该临时节点会消失,这些broker便会收到通知,然后尝试去创建临时节点成为新的控制器。

对于一个Controller来说,如果它发现集群中的一个broker离开时,它会检查该broker是否有分区的leader replica,如果有则需要对这些分区选举出新的leader replica。Controller会在分区的副本列表中选出一个新的leader replica,并且发送请求给新的leader replica和其他的跟随者;这样新的leader replica便知道需要处理生产者和消费者的请求,而跟随者则需要向新的leader replica同步消息。

如果Controller发现有新的broker(这个broker也有可能是之前宕机的)加入时,它会通过此broker的ID来检查该broker是否有分区副本存在,如果有则通知该broker向相应的分区leader replica同步消息。

最后,每当选举一个新的Controller时,就会产生一个新的更大的Controller时间戳(controller epoch),这样集群中的broker收到Controller的消息时检查此时间戳,当发现消息来源于老的Controller,它们便会忽略,以避免脑裂(split brain)。

3.3 controller本身的选举

Kafka Controller的选举不像Paxos算法那么复杂。
是通过在zookeeper上创建/controller临时节点来实现Controller选举,并在该节点中写入当前broker的信息:
{“version”:1,”brokerid”:1,”timestamp”:”1512018424988”}
利用Zookeeper的强一致性特性,一个节点只能被一个客户端创建成功,创建成功的broker即为Controller,即先到先得原则。

kafka集群启动时,KafkaServer会启动多个KafkaController(每个broker一个)。

KafkaController在启动后注册zookeeper的会话超时监听器,并尝试选举controller。
先通过ZookeeperLeaderElector类添加/controller节点的IZkDataListener监听器,节点的数据发生变化时就会通知leaderChangeListener。然后调用选举方法elect,如下所示。


	def elect: Boolean = {
	    val timestamp = SystemTime.milliseconds.toString
	    val electString = Json.encode(Map("version" -> 1, "brokerid" -> brokerId, "timestamp" -> timestamp))
	
	    //先尝试获取/controller节点信息
	   leaderId = getControllerID 
	    /* 
	     * We can get here during the initial startup and the handleDeleted ZK callback. Because of the potential race condition, 
	     * it's possible that the controller has already been elected when we get here. This check will prevent the following 
	     * createEphemeralPath method from getting into an infinite loop if this broker is already the controller.
	     */
	    // 所以先获取节点信息,判断是否已经存在
	    if(leaderId != -1) {
	       debug("Broker %d has been elected as leader, so stopping the election process.".format(leaderId))
	       return amILeader
	    }
	    
	    try {
	      val zkCheckedEphemeral = new ZKCheckedEphemeral(electionPath,
	                                                      electString,
	                                                      controllerContext.zkUtils.zkConnection.getZookeeper,
	                                                      JaasUtils.isZkSecurityEnabled())
	      //创建/controller节点,并写入controller信息,brokerid, version, timestamp
	      zkCheckedEphemeral.create()
	      info(brokerId + " successfully elected as leader")
	      leaderId = brokerId
	      //写入成功,成为Leader,回调
	      onBecomingLeader()
	    } catch {
	      case e: ZkNodeExistsException =>
	        // If someone else has written the path, then
	        leaderId = getControllerID
	      case e2: Throwable =>
	        resign()
	    }
	    amILeader
	  }
	  def close = {
	    leaderId = -1
	  }
	
	  def amILeader : Boolean = leaderId == brokerId
	
	  def resign() = {
	    leaderId = -1
	    // 删除/controller节点
	    // 当/controller节点被删除时,会触发leaderChangeListener的handleDataDeleted,
	    // 会重新尝试选举成Leader,更重要的是也让其他broker有机会成为leader,避免某一个broker的onBecomingLeader一直失败造成整个集群一直处于“群龙无首”的尴尬局面。
	    controllerContext.zkUtils.deletePath(electionPath)
	  }

Controller Failover

就是controller的重新选举。

Controller也需要Failover。每个Broker都会在Controller Path (/controller)上注册一个Watch。当前Controller失败时,对应的Controller Path会自动消失(因为它是Ephemeral Node),此时该Watch被fire,所有“活”着的Broker都会去竞选成为新的Controller(创建新的Controller Path),但是只会有一个竞选成功(这点由Zookeeper保证)。竞选成功者即为新的Controller,竞选失败者则重新在新的Controller Path上注册Watch。因为Zookeeper的Watch是一次性的,被fire一次之后即失效。

3.4 broker failover

Controller对Broker failure的处理过程:

  1. Controller在Zookeeper的/brokers/ids节点上注册Watch。一旦有Broker宕机(包括但不限于机器断电,网络不可用,GC导致的Stop The World,进程crash等),其在Zookeeper对应的Znode会自动被删除,Zookeeper会fire Controller注册的Watch,Controller即可获取最新的幸存的Broker列表。

  2. Controller对所有“宕机”Broker上的所有partition:

    2.1 从/brokers/topics/[topic]/partitions/[partition]/state读取该Partition当前的ISR.

    2.2 决定该Partition的新Leader。如果当前ISR中有至少一个Replica还alive,则选择其中一个作为新Leader,新的ISR则包含当前ISR中所有幸存的Replica。否则选择该Partition的任意一个alive的Replica(不在ISR中),作为新的Leader以及ISR(该场景下可能会有潜在的数据丢失)。如果该Partition的所有Replica都宕机了,则将新的Leader设置为-1。

    2.3 将新的Leader,ISR和新的leader_epoch及controller_epoch写入/brokers/topics/[topic]/partitions/[partition]/state。注意,该操作只有Controller epoch在2.1至2.3的过程中无变化时才会执行,否则跳转到2.1。

  3. 直接通过RPC向set_p相关的Broker发送LeaderAndISRRequest命令。Controller可以在一个RPC操作中发送多个命令从而提高效率。

3.5 Partition Leader 选举

根据文章跟我学kafka之Controller控制器详解, KafkaController中共定义了五种selector选举器:

  1. ReassignedPartitionLeaderSelector
    从可用的ISR中选取第一个作为leader,把当前的ISR作为新的ISR,将重分配的副本集合作为接收LeaderAndIsr请求的副本集合。
  2. PreferredReplicaPartitionLeaderSelector
    如果从assignedReplicas取出的第一个副本就是分区leader的话,则抛出异常,否则将第一个副本设置为分区leader。
  3. ControlledShutdownLeaderSelector
    将ISR中处于关闭状态的副本从集合中去除掉,返回一个新新的ISR集合,然后选取第一个副本作为leader,然后令当前AR作为接收LeaderAndIsr请求的副本。
  4. NoOpLeaderSelector
    原则上不做任何事情,返回当前的leader和isr。
  5. OfflinePartitionLeaderSelector
    从活着的ISR中选择一个broker作为leader,如果ISR中没有活着的副本,则从assignedReplicas中选择一个副本作为leader,leader选举成功后注册到Zookeeper中,并更新所有的缓存。

又根据kafka官方文档:“在ISR中的任何副本都有资格当选leader”,所以可以认为partition leader选举是在ISR中随机选的的一个。
额, any replica in the ISR is eligible to be elected leader.

4. Consumer rebalancing 算法

消费者再平衡算法允许组中所有的消费者消费哪一个分区达成共识,同组中的broker和其他的消费者的每一次增加或移除触发消费者再平衡。对于一个给定的topic和给定的消费者组,组内的消费者之间均匀的分配broker分区。如果我们允许一个分区被多个消费者共同消费,这需要锁了,所以我们让一个分区永远只有一个消费者进行消费。这样设计简化了很多。如果消费者比分区多,那么一些消费者将不会获得任何数据。在rebalance期间,我们试图分配分区给消费者。以这样的方式来减少每个消费者连接到broker的节点数。

每个consumer的rebalance过程如下:

  1. For each topic T that Ci subscribes to
  2. let PT be all partitions producing topic T
  3. let CG be all consumers in the same group as Ci that consume topic T
  4. sort PT (so partitions on the same broker are clustered together)
  5. sort CG
  6. let i be the index position of Ci in CG and let N = size(PT)/size(CG)
  7. assign partitions from i*N to (i+1)*N - 1 to consumer Ci
  8. remove current entries owned by Ci from the partition owner registry
  9. add newly assigned partitions to the partition owner registry
    (we may need to re-try this until the original partition owner releases its ownership)

在这种策略下,每一个Consumer或者Broker的增加或者减少都会触发Consumer Rebalance。因为每个Consumer只负责调整自己所消费的Partition,为了保证整个Consumer Group的一致性,当一个Consumer触发了Rebalance时,该Consumer Group内的其它所有其它Consumer也应该同时触发Rebalance。

该方式有如下缺陷:

  • Herd effect(惊群)

    任何Broker或者Consumer的增减都会触发所有的Consumer的Rebalance

  • Split Brain(脑裂)

    每个Consumer分别单独通过Zookeeper判断哪些Broker和Consumer 宕机了,那么不同Consumer在同一时刻从Zookeeper“看”到的View就可能不一样,这是由Zookeeper的特性决定的,这就会造成不正确的Reblance尝试。

  • 调整结果不可控

    所有的Consumer都并不知道其它Consumer的Rebalance是否成功,这可能会导致Kafka工作在一个不正确的状态。

根据社区wiki介绍Kafka 0.9.*中对Consumer可能的设计方向及思路。

简化消费者客户端
  部分用户希望开发和使用non-java的客户端。现阶段使用non-java发SimpleConsumer比较方便,但想开发High Level Consumer并不容易。因为High Level Consumer需要实现一些复杂但必不可少的失败探测和Rebalance。如果能将消费者客户端更精简,使依赖最小化,将会极大的方便non-java用户实现自己的Consumer。
  
中心Coordinator
  如上文所述,当前版本的High Level Consumer存在Herd Effect和Split Brain的问题。如果将失败探测和Rebalance的逻辑放到一个高可用的中心Coordinator,那么这两个问题即可解决。同时还可大大减少Zookeeper的负载,有利于Kafka Broker的Scale Out。
  
允许手工管理offset
  一些系统希望以特定的时间间隔在自定义的数据库中管理Offset。这就要求Consumer能获取到每条消息的metadata,例如Topic,Partition,Offset,同时还需要在Consumer启动时得到每个Partition的Offset。实现这些,需要提供新的Consumer API。同时有个问题不得不考虑,即是否允许Consumer手工管理部分Topic的Offset,而让Kafka自动通过Zookeeper管理其它Topic的Offset。一个可能的选项是让每个Consumer只能选取1种Offset管理机制,这可极大的简化Consumer API的设计和实现。
  
Rebalance后触发用户指定的回调
  一些应用可能会在内存中为每个Partition维护一些状态,Rebalance时,它们可能需要将该状态持久化。因此该需求希望支持用户实现并指定一些可插拔的并在Rebalance时触发的回调。如果用户使用手动的Offset管理,那该需求可方便得由用户实现,而如果用户希望使用Kafka提供的自动Offset管理,则需要Kafka提供该回调机制。

非阻塞式Consumer API
  该需求源于那些实现高层流处理操作,如filter by, group by, join等,的系统。现阶段的阻塞式Consumer几乎不可能实现Join操作。

详见:

https://cwiki.apache.org/confluence/display/KAFKA/Kafka+0.9+Consumer+Rewrite+Design

https://cwiki.apache.org/confluence/display/KAFKA/Kafka+Client-side+Assignment+Proposal

.

.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值