四、Replication Subsystem
1、Replica
Replica是kafka分发数据的最小单元,主要代码如下:
class Replica(val brokerId: Int, val partition: Partition, time: Time = SystemTime, initialHighWatermarkValue: Long = 0L, val log: Option[Log] = None) extends Logging { // the high watermark offset value, in non-leader replicas only its message offsets are kept @volatile private[this] var highWatermarkMetadata: LogOffsetMetadata = new LogOffsetMetadata(initialHighWatermarkValue) // the log end offset value, kept in all replicas; // for local replica it is the log's end offset, for remote replicas its value is only updated by follower fetch @volatile private[this] var logEndOffsetMetadata: LogOffsetMetadata = LogOffsetMetadata.UnknownOffsetMetadata // the time when log offset is updated private[this] val logEndOffsetUpdateTimeMsValue = new AtomicLong(time.milliseconds) val topic = partition.topic val partitionId = partition.partitionId …………………………………… override def equals(that: Any): Boolean = { if(!(that.isInstanceOf[Replica])) return false val other = that.asInstanceOf[Replica] if(topic.equals(other.topic) && brokerId == other.brokerId && partition.equals(other.partition)) return true false } override def hashCode(): Int = { 31 + topic.hashCode() + 17*brokerId + partition.hashCode() } …………………………………… }
其中主要成员有以下几个:
highWatermarkMetadata,高水位线标记(简称HW),其实就是offset,每个(consumer,topic,partition)的组合都会记录一个offset,是用于记录consumer的消费状态的元数据。
logEndOffsetMetadata,log中offset的最大值(简称LEO),如果该replica在该broker的本地,则该值是本地log文件的最大值,否则是该broker通过followerfetch得到的offset值。还值得注意的一点是,上述两个变量都被打上了@volatile注解,使得在多线程环境下每个线程访问时都得到内存中的最新值。
logEndOffsetUpdateTimeMsValue,意思显而易见。
topic,partition中的topic。
partitionId,partition的id。
另外该类覆盖equals方法的代码值得学习,不光覆盖了equals还覆盖了hashCode,是书上推荐的最严谨的做法。
2、ReplicaManager
这个类提供了kafka最重要的HA能力。该类下的方法很多,如下图:
其中最主要的功能有以下几个(代码都比较简单,从略):
控制该对象下的ProducerRequestPurgatory和FetchRequestPurgatory对象,主动调用其update和respond方法来推动其中消息的消费。
启停和获取replicas。在kafka在0.8以前的版本中,是没有Replication的,一旦某一个Broker宕机,则其上所有的Partition数据都不可被消费。0.8以后版本加入了这一机制作为kafkaHA特性的一部分,为了更好的做负载均衡,Kafka尽量将所有的Partition均匀分配到整个集群上。一个典型的部署方式是一个Topic的Partition数量大于Broker的数量。同时为了提高Kafka的容错能力,也需要将同一个Partition的Replica尽量分散到不同的机器。实际上,如果所有的Replica都在同一个Broker上,那一旦该Broker宕机,该Partition的所有Replica都无法工作,也就达不到HA的效果。同时,如果某个Broker宕机了,需要保证它上面的负载可以被均匀的分配到其它幸存的所有Broker上。
读取数据,根据request的内容,获取每个(topic,partition)组合的replica数据,这个其实是通过调用获取replicas的方法来实现的。
成为leader或follower,这个是用于处理LeaderAndIsrRequest请求的,和kafka的LeaderElection机制有关。引入Replication之后,同一个Partition可能会有多个Replica,而这时需要在这些Replication之间选出一个Leader,Producer和Consumer只与这个Leader交互,其它Replica作为Follower从Leader中复制数据。因为需要保证同一个Partition的多个Replica之间的数据一致性(其中一个宕机后其它Replica必须要能继续服务并且即不能造成数据重复也不能造成数据丢失)。如果没有一个Leader,所有Replica都可同时读/写数据,那就需要保证多个Replica之间互相(N×N条通路)同步数据,数据的一致性和有序性非常难保证,大大增加了Replication实现的复杂性,同时也增加了出现异常的几率。而引入Leader后,只有Leader负责数据读写,Follower只向Leader顺序Fetch数据(N条通路),系统更加简单且高效。
3、PartitionLeaderSelector
实现这个特质用于实现LeaderElection机制的类共有5个,分别为OfflinePartitionLeaderSelector,ReassignedPartitionLeaderSelector,PreferredReplicaPartitionLeaderSelector,ControlledShutdownLeaderSelector,NoOpLeaderSelector(没什么用),在KafkaController中会通过不同的leader失效方式决定调用哪个Selector。(前面架构图中的ReplicationController现在已经包含在KafkaController中了。)
Kafka在ZooKeeper中动态维护了一个ISR(in-syncreplicas),这个ISR里的所有Replica都跟上了leader,只有ISR里的成员才有被选为Leader的可能。在这种模式下,对于f+1个Replica,一个Partition能在保证不丢失已经commit的消息的前提下容忍f个Replica的失败。在大多数使用场景中,这种模式是非常有利的。
以OfflinePartitionLeaderSelector为例,该类用于在一个online的partition突然offline时重新选择leader,代码如下,其算法介绍在头部注释:
/** * Select the new leader, new isr and receiving replicas (for the LeaderAndIsrRequest): * 1. If at least one broker from the isr is alive, it picks a broker from the live isr as the new leader and the live * isr as the new isr. * 2. Else, if unclean leader election for the topic is disabled, it throws a NoReplicaOnlineException. * 3. Else, it picks some alive broker from the assigned replica list as the new leader and the new isr. * 4. If no broker in the assigned replica list is alive, it throws a NoReplicaOnlineException * Replicas to receive LeaderAndIsr request = live assigned replicas * Once the leader is successfully registered in zookeeper, it updates the allLeaders cache */ class OfflinePartitionLeaderSelector(controllerContext: ControllerContext, config: KafkaConfig) extends PartitionLeaderSelector with Logging { this.logIdent = "[OfflinePartitionLeaderSelector]: " def selectLeader(topicAndPartition: TopicAndPartition, currentLeaderAndIsr: LeaderAndIsr): (LeaderAndIsr, Seq[Int]) = { controllerContext.partitionReplicaAssignment.get(topicAndPartition) match { case Some(assignedReplicas) => val liveAssignedReplicas = assignedReplicas.filter(r => controllerContext.liveBrokerIds.contains(r)) val liveBrokersInIsr = currentLeaderAndIsr.isr.filter(r => controllerContext.liveBrokerIds.contains(r)) val currentLeaderEpoch = currentLeaderAndIsr.leaderEpoch val currentLeaderIsrZkPathVersion = currentLeaderAndIsr.zkVersion val newLeaderAndIsr = liveBrokersInIsr.isEmpty match { case true => // Prior to electing an unclean (i.e. non-ISR) leader, ensure that doing so is not disallowed by the configuration // for unclean leader election. if (!LogConfig.fromProps(config.props.props, AdminUtils.fetchTopicConfig(controllerContext.zkClient, topicAndPartition.topic)).uncleanLeaderElectionEnable) { throw new NoReplicaOnlineException(("No broker in ISR for partition " + "%s is alive. Live brokers are: [%s],".format(topicAndPartition, controllerContext.liveBrokerIds)) + " ISR brokers are: [%s]".format(currentLeaderAndIsr.isr.mkString(","))) } debug("No broker in ISR is alive for %s. Pick the leader from the alive assigned replicas: %s" .format(topicAndPartition, liveAssignedReplicas.mkString(","))) liveAssignedReplicas.isEmpty match { case true => throw new NoReplicaOnlineException(("No replica for partition " + "%s is alive. Live brokers are: [%s],".format(topicAndPartition, controllerContext.liveBrokerIds)) + " Assigned replicas are: [%s]".format(assignedReplicas)) case false => ControllerStats.uncleanLeaderElectionRate.mark() val newLeader = liveAssignedReplicas.head warn("No broker in ISR is alive for %s. Elect leader %d from live brokers %s. There's potential data loss." .format(topicAndPartition, newLeader, liveAssignedReplicas.mkString(","))) new LeaderAndIsr(newLeader, currentLeaderEpoch + 1, List(newLeader), currentLeaderIsrZkPathVersion + 1) } case false => val liveReplicasInIsr = liveAssignedReplicas.filter(r => liveBrokersInIsr.contains(r)) val newLeader = liveReplicasInIsr.head debug("Some broker in ISR is alive for %s. Select %d from ISR %s to be the leader." .format(topicAndPartition, newLeader, liveBrokersInIsr.mkString(","))) new LeaderAndIsr(newLeader, currentLeaderEpoch + 1, liveBrokersInIsr.toList, currentLeaderIsrZkPathVersion + 1) } info("Selected new leader and ISR %s for offline partition %s".format(newLeaderAndIsr.toString(), topicAndPartition)) (newLeaderAndIsr, liveAssignedReplicas) case None => throw new NoReplicaOnlineException("Partition %s doesn't have replicas assigned to it".format(topicAndPartition)) } } }
4、OffsetManager
上文已经提到,在处理OffsetCommitRequest时,当offsetCommitRequest.versionId=1则调用handleProducerOrOffsetCommitRequest方法,将offset值存入一个特定的topic中(称为OffsetsTopic),而OffsetManager就是设计用于处理该场景的。将offset的管理从zookeeper迁移回kafka内部,我个人感觉还是效率目的,毕竟当(group,topic,partition)的组合数目非常多时,读写zookeeper下目录也有不小的开销。
主要代码如下,可以看到,管理offsets topic的方法和管理普通topic没有多大区别:
/** * Fetch the current offset for the given group/topic/partition from the underlying offsets storage. * * @param key The requested group-topic-partition * @return If the key is present, return the offset and metadata; otherwise return None */ private def getOffset(key: GroupTopicPartition) = { val offsetAndMetadata = offsetsCache.get(key) if (offsetAndMetadata == null) OffsetMetadataAndError.NoOffset else OffsetMetadataAndError(offsetAndMetadata.offset, offsetAndMetadata.metadata, ErrorMapping.NoError) } /** * Put the (already committed) offset for the given group/topic/partition into the cache. * * @param key The group-topic-partition * @param offsetAndMetadata The offset/metadata to be stored */ private def putOffset(key: GroupTopicPartition, offsetAndMetadata: OffsetAndMetadata) { offsetsCache.put(key, offsetAndMetadata) } def putOffsets(group: String, offsets: Map[TopicAndPartition, OffsetAndMetadata]) { // this method is called _after_ the offsets have been durably appended to the commit log, so there is no need to // check for current leadership as we do for the offset fetch trace("Putting offsets %s for group %s in offsets partition %d.".format(offsets, group, partitionFor(group))) offsets.foreach { case (topicAndPartition, offsetAndMetadata) => putOffset(GroupTopicPartition(group, topicAndPartition), offsetAndMetadata) } } /** * The most important guarantee that this API provides is that it should never return a stale offset. i.e., it either * returns the current offset or it begins to sync the cache from the log (and returns an error code). */ def getOffsets(group: String, topicPartitions: Seq[TopicAndPartition]): Map[TopicAndPartition, OffsetMetadataAndError] = { trace("Getting offsets %s for group %s.".format(topicPartitions, group)) val offsetsPartition = partitionFor(group) /** * followerTransitionLock protects against fetching from an empty/cleared offset cache (i.e., cleared due to a * leader->follower transition). i.e., even if leader-is-local is true a follower transition can occur right after * the check and clear the cache. i.e., we would read from the empty cache and incorrectly return NoOffset. */ followerTransitionLock synchronized { if (leaderIsLocal(offsetsPartition)) { if (loadingPartitions synchronized loadingPartitions.contains(offsetsPartition)) { debug("Cannot fetch offsets for group %s due to ongoing offset load.".format(group)) topicPartitions.map { topicAndPartition => val groupTopicPartition = GroupTopicPartition(group, topicAndPartition) (groupTopicPartition.topicPartition, OffsetMetadataAndError.OffsetsLoading) }.toMap } else { if (topicPartitions.size == 0) { // Return offsets for all partitions owned by this consumer group. (this only applies to consumers that commit offsets to Kafka.) offsetsCache.filter(_._1.group == group).map { case(groupTopicPartition, offsetAndMetadata) => (groupTopicPartition.topicPartition, OffsetMetadataAndError(offsetAndMetadata.offset, offsetAndMetadata.metadata, ErrorMapping.NoError)) }.toMap } else { topicPartitions.map { topicAndPartition => val groupTopicPartition = GroupTopicPartition(group, topicAndPartition) (groupTopicPartition.topicPartition, getOffset(groupTopicPartition)) }.toMap } } } else { debug("Could not fetch offsets for group %s (not offset coordinator).".format(group)) topicPartitions.map { topicAndPartition => val groupTopicPartition = GroupTopicPartition(group, topicAndPartition) (groupTopicPartition.topicPartition, OffsetMetadataAndError.NotOffsetManagerForGroup) }.toMap } } } /** * Asynchronously read the partition from the offsets topic and populate the cache */ def loadOffsetsFromLog(offsetsPartition: Int) { val topicPartition = TopicAndPartition(OffsetManager.OffsetsTopicName, offsetsPartition) loadingPartitions synchronized { if (loadingPartitions.contains(offsetsPartition)) { info("Offset load from %s already in progress.".format(topicPartition)) } else { loadingPartitions.add(offsetsPartition) scheduler.schedule(topicPartition.toString, loadOffsets) } } def loadOffsets() { info("Loading offsets from " + topicPartition) val startMs = SystemTime.milliseconds try { replicaManager.logManager.getLog(topicPartition) match { case Some(log) => var currOffset = log.logSegments.head.baseOffset val buffer = ByteBuffer.allocate(config.loadBufferSize) // loop breaks if leader changes at any time during the load, since getHighWatermark is -1 while (currOffset < getHighWatermark(offsetsPartition) && !shuttingDown.get()) { buffer.clear() val messages = log.read(currOffset, config.loadBufferSize).messageSet.asInstanceOf[FileMessageSet] messages.readInto(buffer, 0) val messageSet = new ByteBufferMessageSet(buffer) messageSet.foreach { msgAndOffset => require(msgAndOffset.message.key != null, "Offset entry key should not be null") val key = OffsetManager.readMessageKey(msgAndOffset.message.key) if (msgAndOffset.message.payload == null) { if (offsetsCache.remove(key) != null) trace("Removed offset for %s due to tombstone entry.".format(key)) else trace("Ignoring redundant tombstone for %s.".format(key)) } else { val value = OffsetManager.readMessageValue(msgAndOffset.message.payload) putOffset(key, value) trace("Loaded offset %s for %s.".format(value, key)) } currOffset = msgAndOffset.nextOffset } } if (!shuttingDown.get()) info("Finished loading offsets from %s in %d milliseconds." .format(topicPartition, SystemTime.milliseconds - startMs)) case None => warn("No log found for " + topicPartition) } } catch { case t: Throwable => error("Error in loading offsets from " + topicPartition, t) } finally { loadingPartitions synchronized loadingPartitions.remove(offsetsPartition) } } }
5、AbstractFetcherManager/AbstractFetcherThread
这两个抽象类是用于管理partition的fetcher,即kafka的数据消费机制。其中Manager的作用是创建Thread,并将Thread绑定到partition上(或从partition上移除)。Thread的作用是处理FetchRequest,从指定partition的当前offset处继续读取不超过HW的数据,读取的当前offset由内存中一个(topic,partition) -> offset的HashMap进行管理,且对该对象的读写都是互斥的。
ReplicaFetcherManager/ReplicaFetcherThread是对这两个抽象类的具体实现,代码从略。