目录
kafka hello world
一. kafka 架构:
Partition存储结构
ACK前需要保证有多少个备份
二 kafka partition 分配原理探究
三 rebalance 过程
四:mafka 优化
PUSH SERVER
重试
两种
MafkaParallarWork
kafka 安装 & 常用API kafka安装 & Kafka java api 0.10.1.1
Partition存储结构
ACK前需要保证有多少个备份
和大部分分布式系统一样,Kafka处理失败需要明确定义一个Broker是否“活着”。对于Kafka而言,Kafka存活包含两个条件,一是它必须维护与ZooKeeper的session(这个通过ZooKeeper的Heartbeat机制来实现)。二是Follower必须能够及时将Leader的消息复制过来,不能“落后太多”。
kafka的官方文档提供了这样一段描述
In fact, the only metadata retained on a per-consumer basis is the offset or position of that consumer in the log. This offset is controlled by the consumer: normally a consumer will advance its offset linearly as it reads records, but, in fact, since the position is controlled by the consumer it can consume records in any order it likes. For example a consumer can reset to an older offset to reprocess data from the past or skip ahead to the most recent record and start consuming from "now".
kafka不同于其他mq,由于他基于硬盘的存储,所以kafka不会删除消费过的数据,所以consumer可以从指定的offset读取数据,针对这个特性做了以下实验。
生产者代码略,消费者代码如下
略
效果如下:
这里面有两个问题,
offset是否跨partiton ?
partiton与consumer如何指定 ?
首先第一个问题,kafka的最小物理单位是partition,所以offset是记录在partition中的(segment index中),那么partition是跨机器的,kafka有没有通过zk将partiton的节点统一管理呢,从以上的实验来看,kafka的不同partition是有可能出现相同offset的,所以可见offset的是partiton内管理的,并没有在manager中统一管理的。所以我们再指定offset的时候要同时指定partiton。
那么第二个问题,既然offset是以partiton作为单位存储的,那么当一个consumer监听多个partiton的时候,consumer如何知道自己该去哪个partiton拉数据呢?(因为consumer是poll方式,所以猜测是轮训)
在kafka的ZookeeperConsumerConnector中发现这样一段代码
代码块
scala
private def rebalance(cluster: Cluster): Boolean = {
val myTopicThreadIdsMap = TopicCount.constructTopicCount(
group, consumerIdString, zkUtils, config.excludeInternalTopics).getConsumerThreadIdsPerTopic
val brokers = zkUtils.getAllBrokersInCluster()
if (brokers.size == 0) {
// 1.如果目前没有可用的brokers ,那么就先在子节点上注册监听事件,当有brokers 启动的时候进行rebalance。
warn("no brokers found when trying to rebalance.")
zkUtils.zkClient.subscribeChildChanges(BrokerIdsPath, loadBalancerListener)
true
}
else {
// 2.kafka 的 rebalance 会暂时stop the world ,以防止消息的重复消费。
closeFetchers(cluster, kafkaMessageAndMetadataStreams, myTopicThreadIdsMap)
if (consumerRebalanceListener != null) {
info("Invoking rebalance listener before relasing partition ownerships.")
consumerRebalanceListener.beforeReleasingPartitions(
if (topicRegistry.size == 0)
new java.util.HashMap[String, java.util.Set[java.lang.Integer]]
else
topicRegistry.map(topics =>
topics._1 -> topics._2.keys // note this is incorrect, see KAFKA-2284
).toMap.asJava.asInstanceOf[java.util.Map[String, java.util.Set[java.lang.Integer]]]
)
}
releasePartitionOwnership(topicRegistry)
val assignmentContext = new AssignmentContext(group, consumerIdString, config.excludeInternalTopics, zkUtils)
// 3.partition 重分配
val globalPartitionAssignment = partitionAssignor.assign(assignmentContext)
后续代码略
val globalPartitionAssignment = partitionAssignor.assign(assignmentContext) 发现定义了这样一个类
继续跟踪...
val nPartsPerConsumer = curPartitions.size / curConsumers.size // 每个consumer至少保证消费的分区数 val nConsumersWithExtraPart = curPartitions.size % curConsumers.size // 还剩下多少个分区需要单独分配给开头的线程们 ... for (consumerThreadId <- consumerThreadIdSet) { // 对于每一个consumer线程 val myConsumerPosition = curConsumers.indexOf(consumerThreadId) //算出该线程在所有线程中的位置,介于[0, n-1] assert(myConsumerPosition >= 0) // startPart 就是这个线程要消费的起始分区数 val startPart = nPartsPerConsumer * myConsumerPosition + myConsumerPosition.min(nConsumersWithExtraPart) // nParts 就是这个线程总共要消费多少个分区 val nParts = nPartsPerConsumer + (if (myConsumerPosition + 1 > nConsumersWithExtraPart) 0 else 1) ... } |
这里 kafka 提供两种分配策略 range和roundrobin,由参数partition.assignment.strategy指定,默认是range策略。本文只讨论range策略。所谓的range其实就是按照阶段平均分配。
PartitionAssignor object定义了一个工厂方法用于创建不同策略的分区分配器,目前Kafka支持两种再平衡策略(也就是分区分配策略):round robin和range。值得注意的是,这里所说的分区策略其实是指指如何将分区分配给消费组内的不同consumer实例。
假设我们有一个topic:T1,T1有10个分区,分别是[P0, P9],然后我们有2个consumer,C1和C2。
下面我们来看看默认的range策略是如何分配分区的:
1. Range策略
对于每一个topic,range策略会首先按照数字顺序排序所有可用的分区,并按照字典顺序列出所有的consumer线程。结合我们上面的例子,分区顺序是0,1,2,3,4,5,6,7,8,9,而consumer线程的顺序是c1-0, c2-0, c2-1。然后使用分区数除以线程数以确定每个线程至少获取的分区数。在我们的例子中,10/3不能整除,余数为1,因此c1-0会被额外多分配一个分区。最后的分区分配如下:
c1-0 获得分区 0 1 2 3
c2-0 获得分区 4 5 6
c2-1 获得分区 7 8 9
如果该topic是11个分区,那么分区分配如下:
c1-0 获取分区 0 1 2 3
c2-0 获取分区 4 5 6 7
c2-1 获取分区 8 9 10
2. roundrobin策略——轮询策略
如果是轮询策略,我们上面假设的例子就不适用了,因为该策略要求订阅某个topic的所有consumer都必须有相同数目的线程数。round robin策略与range的一个主要的区别就是在再分配之前你是没法预测分配结果的——因为它会使用哈希求模的方式随机化排序顺序。
了解了consumer的分配,再了解producer的分配就比较容易了。producer的分配与consumer 大致相同
代码块
java
public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) { Listpartitions = cluster.partitionsForTopic(topic);
int numPartitions = partitions.size();
if (keyBytes == null) {
int nextValue = counter.getAndIncrement();
ListavailablePartitions = cluster.availablePartitionsForTopic(topic);
if (availablePartitions.size() > 0) {
int part = DefaultPartitioner.toPositive(nextValue) % availablePartitions.size();
return availablePartitions.get(part).partition();
} else {
// no partitions are available, give a non-available partition
return DefaultPartitioner.toPositive(nextValue) % numPartitions;
}
} else {
// hash the keyBytes to choose a partition
return DefaultPartitioner.toPositive(Utils.murmur2(keyBytes)) % numPartitions;
}
}
无key的时候,是随机选择一个Partition发送,如果有key的时候,则是根据key去选择一个partition发送。
coordinator 是kafka 的一个协调者,也起到中控的作用。对比rocketMQ将协调者放在了name_server 中,kafka是将协调者放到了其中的一个broker里面去做。
Kafka的coordiantor要做的事情就是group management,就是要对一个团队(或者叫组)的成员进行管理。Group management就是要做这些事情:
维持group的成员组成。这包括允许新的成员加入,检测成员的存活性,清除不再存活的成员。
协调group成员的行为。
Kafka 0.9 版本的一个亮点就是consumer对zk无依赖,而想做到这点无疑要将group放到broker中去管理。很明显,这就是consumer coordinator所要做的事情,是可以用group management 协议做到的。而cooridnator, 及这个协议,也是为了实现不依赖Zookeeper的高级消费者而提出并实现的。只不过,Kafka对高级消费者的成员管理行为进行了抽象,抽象出来group management功能共有的逻辑,以此设计了Group Management Protocol, 使得这个协议不只适用于Kafka consumer(目前Kafka Connect也在用它),也可以作为其它"group"的管理协议。
还是从consumer消费消息开始理清coordinator的过程。
首先consumer的pollOnOnce 方法会调用 GROUP_COORDINATOR request -→ kafka server
GROUP_COORDINATOR
代码块
java
def handleGroupCoordinatorRequest(request: RequestChannel.Request) {
val groupCoordinatorRequest = request.body[GroupCoordinatorRequest]
if (!authorize(request.session, Describe, new Resource(Group, groupCoordinatorRequest.groupId))) {
val responseBody = new GroupCoordinatorResponse(Errors.GROUP_AUTHORIZATION_FAILED, Node.noNode)
requestChannel.sendResponse(new RequestChannel.Response(request, responseBody))
} else {
// 收到客户端的GroupCoordinatorRequest请求后,
// 根据group id哈希运算完后模上一个特殊的内部topic:__consumer_offsets 的partition总个数(默认配置是50个)得到一个partitionId。
val partition = coordinator.partitionFor(groupCoordinatorRequest.groupId)
// get metadata (and create the topic if necessary)
val offsetsTopicMetadata = getOrCreateGroupMetadataTopic(request.listenerName)
val responseBody = if (offsetsTopicMetadata.error != Errors.NONE) {
new GroupCoordinatorResponse(Errors.GROUP_COORDINATOR_NOT_AVAILABLE, Node.noNode)
} else {
// 将这个PartitionId的leader所在的broker节点作为这个消费组的coordinator节点信息封装成GroupCoordinatorResponse返回给客户端。
val coordinatorEndpoint = offsetsTopicMetadata.partitionMetadata().asScala
.find(_.partition == partition)
.map(_.leader())
coordinatorEndpoint match {
case Some(endpoint) if !endpoint.isEmpty =>
new GroupCoordinatorResponse(Errors.NONE, endpoint)
case _ =>
new GroupCoordinatorResponse(Errors.GROUP_COORDINATOR_NOT_AVAILABLE, Node.noNode)
}
}
1.收到客户端请求后会根据 group id哈希运算完后模上一个特殊的内部topic:__consumer_offsets 的partition总个数(默认配置是50个)得到一个partitionId。zk节点位置如下图。
2. 将这个PartitionId的leader所在的broker节点作为这个消费组的coordinator节点信息封装成GroupCoordinatorResponse返回给客户端。
3. 如果元数据修改 或者更改了partition 规则(包含第一次请求),则发送rejoinGroup 请求.
4. consumer 参与到对应的consumer group中,group会对组内的consumer 选主,选主策略为第一个进入group的consumer为leader ,见代码: