1. kafka在zk中的存储结构
http://www.cnblogs.com/byrhuangqiang/p/6367037.html
-
/broker/ids/[0…N]
broker 注册信息。 其中[0…N]表示broker id(broker id唯一,不可以重复)
-
/broker/topics/[topic]/partitions/[0…N]
Broker Topic 注册。
-
/brokers/topics/[topic]/partitions/[partitionId]/state
partitions状态信息。
{ "controller_epoch": 表示kafka集群中的中央控制器选举次数,` "leader": 表示该partition选举leader的brokerId, "version": 版本编号默认为1, "leader_epoch": 该partition leader选举次数, "isr": [同步副本组brokerId列表] }
-
/consumers/[group_id]/ids/[consumer_id]
Consumer 注册.
-
/consumers/[group_id]/offsets/[topic]/[partition_id]
跟踪每个Consumer目前所消费的partition中的最大offset.
-
/consumers/[group_id]/owners/[topic]/[partition_id]
Partition Owner 注册.首先进行"Consumer Id注册"; 然后在"Consumer id 注册"节点下注册一个watch用来监听当前group中其他consumer的"退出"和"加入";只要此znode path下节点列表变更,都会触发此group下consumer的负载均衡.(比如一个consumer失效,那么其他consumer接管partitions).
-
/controller_epoch
kafka集群中第一个broker第一次启动时为1,以后只要集群中center controller中央控制器所在broker变更或挂掉,就会重新选举新的center controller,每次center controller变更controller_epoch值就会 + 1;
-
/controller
Controller注册信息。包含version, brokerid, timestamp.
-
/admin/reassign_partitions
用于将一些Partition分配到不同的broker集合上。对于每个待重新分配的Partition,Kafka会在该znode上存储其所有的Replica和相应的Broker id。该znode由管理进程创建并且一旦重新分配成功它将会被自动移除。
-
/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. 复制集的几个问题
-
如何将所有Replica均匀分布到整个集群?
Kafka分配Replica的算法如下:
- 将所有Broker(假设共n个Broker)和待分配的Partition排序
- 将第i个Partition分配到第(i mod n)个Broker上
- 将第i个Partition的第j个Replica分配到第((i + j) mod n)个Broker上
-
在向Producer发送ACK前需要保证有多少个Replica已经收到该消息?
Leader收到了ISR中的所有Replica的ACK之后。
Kafka的复制机制既不是完全的同步复制,也不是单纯的异步复制。同步复制影响吞吐率,异步会丢数据。
同步复制要求所有能工作的Follower都复制完,这条消息才会被认为commit。Follower异步的从Leader复制数据,数据只要被Leader写入log就被认为已经commit,这种情况下如果Follower都复制完都落后于Leader,而如果Leader突然宕机,则会丢失数据。 -
怎样处理某个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请求。
-
怎样处理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 的主要工作
集群控制器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的处理过程:
-
Controller在Zookeeper的/brokers/ids节点上注册Watch。一旦有Broker宕机(包括但不限于机器断电,网络不可用,GC导致的Stop The World,进程crash等),其在Zookeeper对应的Znode会自动被删除,Zookeeper会fire Controller注册的Watch,Controller即可获取最新的幸存的Broker列表。
-
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。
-
直接通过RPC向set_p相关的Broker发送LeaderAndISRRequest命令。Controller可以在一个RPC操作中发送多个命令从而提高效率。
3.5 Partition Leader 选举
根据文章跟我学kafka之Controller控制器详解, KafkaController中共定义了五种selector选举器:
- ReassignedPartitionLeaderSelector
从可用的ISR中选取第一个作为leader,把当前的ISR作为新的ISR,将重分配的副本集合作为接收LeaderAndIsr请求的副本集合。 - PreferredReplicaPartitionLeaderSelector
如果从assignedReplicas取出的第一个副本就是分区leader的话,则抛出异常,否则将第一个副本设置为分区leader。 - ControlledShutdownLeaderSelector
将ISR中处于关闭状态的副本从集合中去除掉,返回一个新新的ISR集合,然后选取第一个副本作为leader,然后令当前AR作为接收LeaderAndIsr请求的副本。 - NoOpLeaderSelector
原则上不做任何事情,返回当前的leader和isr。 - 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过程如下:
- For each topic T that Ci subscribes to
- let PT be all partitions producing topic T
- let CG be all consumers in the same group as Ci that consume topic T
- sort PT (so partitions on the same broker are clustered together)
- sort CG
- let i be the index position of Ci in CG and let N = size(PT)/size(CG)
- assign partitions from i*N to (i+1)*N - 1 to consumer Ci
- remove current entries owned by Ci from the partition owner registry
- 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
.
.