kafka学习笔记三
consumer group
很多传统的message queue都会在消息被消费完后将消息删除,一方面避免重复消费,另一方面可以保证queue的长度比较少,提高效率。而如上文所将,Kafka并不删除已消费的消息,为了实现传统message queue消息只被消费一次的语义,Kafka保证保证同一个consumer group里只有一个consumer会消费一条消息。与传统message queue不同的是,Kafka还允许不同consumer group同时消费同一条消息,这一特性可以为消息的多元化处理提供了支持.
实际上,Kafka的设计理念之一就是同时提供离线处理和实时处理。根据这一特性,可以使用Storm这种实时流处理系统对消息进行实时在线处理,同时使用Hadoop这种批处理系统进行离线处理,还可以同时将数据实时备份到另一个数据中心,只需要保证这三个操作所使用的consumer在不同的consumer group即可。
下图展示了Kafka在Linkedin的一种简化部署:
Consumer Rebalance
一个topic有N个partition,一个consumer group里有M个consummer,则有:
- N>M:consummer会消费>=1个消息
- N==M:每个consummer正好消费一个消息
- N<M:有的consummer会消费不到消息,即每个consummer消费<=1个消息。
consumer rebalance算法源码如下:
def assign(ctx: AssignmentContext) = {
//这个是个map用来存放分配后的所有的topic的partition分配方案。
val partitionOwnershipDecision = collection.mutable.Map[TopicAndPartition, ConsumerThreadId]()
//一次对每个topic所有可用的consumer来遍历分配
for ((topic, consumerThreadIdSet) <- ctx.myTopicThreadIds) {
val curConsumers = ctx.consumersForTopic(topic)
val curPartitions: Seq[Int] = ctx.partitionsForTopic(topic)
//n=10/3=3
val nPartsPerConsumer = curPartitions.size / curConsumers.size
//ext=10%3=1
val nConsumersWithExtraPart = curPartitions.size % curConsumers.size
info("Consumer " + ctx.consumerId + " rebalancing the following partitions: " + curPartitions +
" for topic " + topic + " with consumers: " + curConsumers)
for (consumerThreadId <- consumerThreadIdSet) {
//得到consummer的编号:0,1,2
val myConsumerPosition = curConsumers.indexOf(consumerThreadId)
assert(myConsumerPosition >= 0)
//得到每个consummer的起始位置:start=(0->0),(1->4),(2->7)
val startPart = nPartsPerConsumer * myConsumerPosition + myConsumerPosition.min(nConsumersWithExtraPart)
//npart=3+(index+1)>ext?0:1
val nParts = nPartsPerConsumer + (if (myConsumerPosition + 1 > nConsumersWithExtraPart) 0 else 1)
/**
* Range-partition the sorted partitions to consumers for better locality.
* The first few consumers pick up an extra partition, if any.
*/
if (nParts <= 0)
warn("No broker partitions consumed by consumer thread " + consumerThreadId + " for topic " + topic)
else {
for (i <- startPart until startPart + nParts) {
val partition = curPartitions(i)
info(consumerThreadId + " attempting to claim partition " + partition)
// record the partition ownership decision
//分配得到:
//consummer0:start=0,nparts=4 ,until(0,1,2,3)
//map+=(topicid,0)->consumer0
//map+=(topicid,1)->consumer0
//map+=(topicid,2)->consumer0
//map+=(topicid,3)->consumer0
//consummer1:start=4,nparts=3 ,until(4,5,6)
//map+=(topicid,4)->consumer1
//map+=(topicid,5)->consumer1
//map+=(topicid,6)->consumer1
//consummer2:start=7,nparts=3 ,until(7,8,9)
//map+=(topicid,7)->consumer2
//map+=(topicid,8)->consumer2
//map+=(topicid,9)->consumer2
partitionOwnershipDecision += (TopicAndPartition(topic, partition) -> consumerThreadId)
}
}
}
}
partitionOwnershipDecision
}
上述源码我解析得已经很详细了,还带有一个列子,一个topic的partition编号为:0 until 10,consumer为consumer0,consumer1consumer2
主要计算的东西为:start和每个consumer间隔part:
-
index=index(consumer)
;n=partitionSize/consumerSize
;m=partitionSize%consumersize
- index:
start=index*n+min(index,m)
,part=n+(index+1)>m?0:1
map+=(topicid,partitiontId)->consummerId
每一个consumer或者broker的增加或者减少都会触发consumer rebalance。因为每个consumer只负责调整自己所消费的partition,为了保证整个consumer group的一致性,所以当一个consumer触发了rebalance时,该consumer group内的其它所有consumer也应该同时触发rebalance。
消息Deliver guarantee
producer->broker
Kafka确保消息在producer和consumer之间传输。有这么几种可能的delivery guarantee:
-
At most once
消息可能会丢,但绝不会重复传输 -
At least one
消息绝不会丢,但可能会重复传输 -
Exactly once
每条消息肯定会被传输一次且仅传输一次,很多时候这是用户所想要的。
- Kafka的delivery guarantee semantic非常直接。当producer向broker发送消息时,一旦这条消息被commit,因数replication的存在,它就不会丢。但是如果producer发送数据给broker后,遇到的网络问题而造成通信中断,那producer就无法判断该条消息是否已经commit。这一点有点像向一个自动生成primary key的数据库表中插入数据。虽然Kafka无法确定网络故障期间发生了什么,但是producer可以生成一种类似于primary key的东西,发生故障时幂等性的retry多次,这样就做到了
Exactly one
- 目前默认情况下一条消息从producer和broker是确保了
At least once
,但可通过设置producer异步发送实现At most once
broker->consumer(只针对hign level API)
consumer在从broker读取消息后,可以选择commit,该操作会在Zookeeper中存下该consumer在该partition下读取的消息的offset。该consumer下一次再读该partition时会从下一条开始读取。如未commit,下一次读取的开始位置会跟上一次commit之后的开始位置相同。当然可以将consumer设置为autocommit,即consumer一旦读到数据立即自动commit。如果只讨论这一读取消息的过程,那Kafka是确保了Exactly once
。但实际上实际使用中consumer并非读取完数据就结束了,而是要进行进一步处理,而数据处理与commit的顺序在很大程度上决定了消息从broker和consumer的delivery guarantee semantic。
- 读完消息先commit再处理消息。这种模式下,如果consumer在commit后还没来得及处理消息就crash了,下次重新开始工作后就无法读到刚刚已提交而未处理的消息,这就对应于
At most once
- 读完消息先处理再commit。这种模式下,如果处理完了消息在commit之前consumer crash了,下次重新开始工作时还会处理刚刚未commit的消息,实际上该消息已经被处理过了。这就对应于
At least once
。在很多情况使用场景下,消息都有一个primary key,所以消息的处理往往具有幂等性,即多次处理这一条消息跟只处理一次是等效的,那就可以认为是Exactly once
。(人个感觉这种说法有些牵强,毕竟它不是Kafka本身提供的机制,而且primary key本身不保证操作的幂等性。而且实际上我们说delivery guarantee semantic是讨论被处理多少次,而非处理结果怎样,因为处理方式多种多样,我们的系统不应该把处理过程的特性–如是否幂等性,当成Kafka本身的feature) - 如果一定要做到
Exactly once
,就需要协调offset和实际操作的输出。精典的做法是引入两阶段提交。如果能让offset和操作输入存在同一个地方,会更简洁和通用。这种方式可能更好,因为许多输出系统可能不支持两阶段提交。比如,consumer拿到数据后可能把数据放到HDFS,如果把最新的offset和数据本身一起写到HDFS,那就可以保证数据的输出和offset的更新要么都完成,要么都不完成,间接实现Exactly once
。(目前就high level API而言,offset是存于Zookeeper中的,无法存于HDFS,而low level API的offset是由自己去维护的,可以将之存于HDFS中) 总之,Kafka默认保证At least once
,并且允许通过设置producer异步提交来实现At most once
。而Exactly once
要求与目标存储系统协作,幸运的是Kafka提供的offset可以使用这种方式非常直接非常容易。
consumer吞吐率
需要注意的是,replication factor并不会影响consumer的吞吐率测试,因为consumer只会从每个partition的leader读数据,而与replicaiton factor无关。同样,consumer吞吐率也与同步复制还是异步复制无关