在ReplicaManager.makeFollowers函数中,对于成为Follower的broker,需要执行这么一段代码:
val partitionsToMakeFollowerWithLeaderAndOffset = partitionsToMakeFollower.map(partition =>
new TopicAndPartition(partition) -> BrokerAndInitialOffset(
leaders.find(_.id == partition.leaderReplicaIdOpt.get).get,
partition.getReplica().get.logEndOffset.messageOffset)).toMap
replicaFetcherManager.addFetcherForPartitions(partitionsToMakeFollowerWithLeaderAndOffset)
这段代码首先获取Follower对应的(leader,leo),之后调用addFetcherForPartitions将这些partition纳入replicaFetcherManager的管理下,并不断同步leader中的log到follower中。
这里贴一下addFetcherForPartitions的代码:
def addFetcherForPartitions(partitionAndOffsets: Map[TopicAndPartition, BrokerAndInitialOffset]) {
mapLock synchronized {
val partitionsPerFetcher = partitionAndOffsets.groupBy{ case(topicAndPartition, brokerAndInitialOffset) =>
//fetchId是hash后取余,不同的topicAndPartition可能分配到同一个线程上
//另外groupby还有一个broker参数,也就是不同的broker肯定不在一个组
BrokerAndFetcherId(brokerAndInitialOffset.broker, getFetcherId(topicAndPartition.topic, topicAndPartition.partition))}
for ((brokerAndFetcherId, partitionAndOffsets) <- partitionsPerFetcher) {
var fetcherThread: AbstractFetcherThread = null
fetcherThreadMap.get(brokerAndFetcherId) match {
case Some(f) => fetcherThread = f
case None =>
//每个Thread对应一个broker上的多个partition,且broker为这些partition的leader
fetcherThread = createFetcherThread(brokerAndFetcherId.fetcherId, brokerAndFetcherId.broker)
fetcherThreadMap.put(brokerAndFetcherId, fetcherThread)
fetcherThread.start
}
fetcherThreadMap(brokerAndFetcherId).addPartitions(partitionAndOffsets.map { case (topicAndPartition, brokerAndInitOffset) =>
topicAndPartition -> brokerAndInitOffset.initOffset
})
}
}
}
这段代码做了如下的工作:
1. 根据leader broker 和 hash(topicAndPartition)%numFetchers将这些partitionAndOffsets进行groupby,结果就是同一个leader broker 下的几个topicAndPartition会分配到一组,因为对topicAndPartition进行了hash取余得到FetcherId,不同的topicAndPartition可能有相同的FetcherId。
2. 对于每个BrokerAndFetcherId 分配一个FetcherThread;
3. 调用FetcherThread.addPartitions给该线程添加Fetch任务;
添加任务后,FetcherThread会不停地同步执行Fetch操作:
response = simpleConsumer.fetch(fetchRequest)
simpleConsumer是连接到leader broker的,leader broker收到FetchRequest后会记录Fetch的起始offset,作为Follower的leo,同时会响应返回follower所需的数据。一旦收到response,Follower会解析data中包含的最后一条message的offset,作为下次Fetch的起点。
//从response中获取messages
val messages = partitionData.messages.asInstanceOf[ByteBufferMessageSet]
val validBytes = messages.validBytes
//下一次fetch的offset
val newOffset = messages.shallowIterator.toSeq.lastOption match {
case Some(m: MessageAndOffset) => m.nextOffset
case None => currentOffset.get
}
之后会调用processPartitionData函数,处理Fetch回来的数据:添加到本地log中,同时更新replica的hw。
replica.log.get.append(messageSet, assignOffsets = false)
//这里更新hw,当前的logEndOffset 与 leader返回的结果比较,求较小的
val followerHighWatermark=replica.logEndOffset.messageOffset.min(partitionData.hw)
除此以外,还需要考虑意外的情况,FetcherRequest请求中的offset可能不在leader允许的范围内,会返回ErrorMapping.OffsetOutOfRangeCode错误,这种情况下通过ReplicaFetcherThread.handleOffsetOutOfRange获取新的offset,作为下次Fetch的起点。这个函数里面对leader.leo < replica.leo的情况专门做了处理:
if (leaderEndOffset < replica.logEndOffset.messageOffset) {
//在截断follower的log之前,需要在配置中查看unclean leader selection 是否允许
//造成这种情况的原因是leader挂了以后,ISR为空,只有选择不在ISR中的Follower作为leader;
//而当挂掉的leader复活后,从新的leader拉取数据时就会出现这种场景,follower的offset > leader 的offset
if (!LogConfig.fromProps(brokerConfig.props.props, AdminUtils.fetchTopicConfig(replicaMgr.zkClient,
topicAndPartition.topic)).uncleanLeaderElectionEnable) {
//如果不允许unclean Leader Election,这种情况不应该发生,直接停止
Runtime.getRuntime.halt(1)
}
//本地log截断至leader leo
replicaMgr.logManager.truncateTo(Map(topicAndPartition -> leaderEndOffset))
//返回leader leo
leaderEndOffset
注释写的比较明了了,就是在允许unclean leader selection时,可能发生 follower的leo大于leader的leo的情况,需要截断follower的log到leader的leo,下次fetch时从leader leo开始。

本文详细介绍了KAFKA中,当broker成为Follower时,如何通过ReplicaFetcherManager进行日志同步。具体包括根据leader分配Fetcher,启动FetcherThread执行Fetch操作,处理Fetch响应并在本地log中更新数据。还讨论了当follower的offset超出leader允许范围时,如何处理unclean leader selection情况下的日志截断问题。
9323

被折叠的 条评论
为什么被折叠?



