一个能干掉90%候选人的Kafka面试连环炮!blog.csdn.net
kafka原理机制 blog.csdn.net
kafka21连问 mp.weixin.qq.com
kafka消费者组
原文地址 blog.csdn.net
目录
什么是消费者组
消费者组是kafka提供的可扩展且具有容错性的消费者机制。既然是一个组,那么组内必然可以有多个消费者或消费者实例,它们共享一个公共的ID,即group ID。
消费者与消费者组的关系
消费者负责订阅 Kafka 中的主题(Topic),并且从订阅的主题上拉取消息。与其他一些消息中间件不同的是:在 Kafka 的消费理念中还有一层消费组的概念,每个消费者都有一个对应的消费组。当消息发布到主题后,只会被投递给订阅它的每个消费组中的一个消费者。
如上图所示,某个主题中共有4个分区(Partition):P0、P1、P2、P3。有两个消费组A和B都订阅了这个主题,消费组A中有4个消费者(C0、C1、C2和C3),消费组B中有2个消费者(C4和C5)。按照 Kafka 默认的规则,最后的分配结果是消费组A中的每一个消费者分配到1个分区,消费组B中的每一个消费者分配到2个分区,两个消费组之间互不影响。每个消费者只能消费所分配到的分区中的消息。换言之,每一个分区只能被一个消费组中的一个消费者所消费。
消费组内的消费者个数变化时所对应的分区分配的演变
目前某消费组内只有一个消费者C0,订阅了一个主题,这个主题包含7个分区:P0、P1、P2、P3、P4、P5、P6。也就是说,这个消费者C0订阅了7个分区。
此时消费组内又加入了一个新的消费者C1,按照既定的逻辑,需要将原来消费者C0的部分分区分配给消费者C1消费,如上图所示。消费者C0和C1各自负责消费所分配到的分区,彼此之间并无逻辑上的干扰。
消费者与消费组这种模型可以让整体的消费能力具备横向伸缩性,我们可以增加(或减少)消费者的个数来提高(或降低)整体的消费能力。对于分区数固定的情况,一味地增加消费者并不会让消费能力一直得到提升,如果消费者过多,出现了消费者的个数大于分区个数的情况,就会有消费者分配不到任何分区。参考下图,一共有8个消费者,7个分区,那么最后的消费者C7由于分配不到任何分区而无法消费任何消息。
对于消息中间件而言,一般有两种消息投递模式:点对点(P2P,Point-to-Point)模式和发布/订阅(Pub/Sub)模式。
点对点模式是基于队列的,消息生产者发送消息到队列,消息消费者从队列中接收消息。
发布订阅模式定义了如何向一个内容节点发布和订阅消息,这个内容节点称为主题(Topic),主题可以认为是消息传递的中介,消息发布者将消息发布到某个主题,而消息订阅者从主题中订阅消息。主题使得消息的订阅者和发布者互相保持独立,不需要进行接触即可保证消息的传递,发布/订阅模式在消息的一对多广播时采用。
Kafka 同时支持两种消息投递模式,而这正是得益于消费者与消费组模型的契合。
如果所有的消费者都隶属于同一个消费组,那么所有的消息都会被均衡地投递给每一个消费者,即每条消息只会被一个消费者处理,这就相当于点对点模式的应用。
如果所有的消费者都隶属于不同的消费组,那么所有的消息都会被广播给所有的消费者,即每条消息会被所有的消费者处理,这就相当于发布/订阅模式的应用。
单播与多播
单播:一条消息只能被某一个消费者消费的模式称为单播。要实现消息单播,只要让这些消费者属于同一个消费者组即可。
多播:一条消息能够被多个消费者消费的模式称为多播。之所以不称之为广播,是因为一条消息只能被Kafka同一个分组下某一个消费者消费,而不是所有消费者都能消费,所以从严格意义上来讲并不能算是广播模式,当然如果希望实现广播模式只要保证每个消费者均属于不同的消费者组。
分区数量和消费者数量的关系
单个消费者组
当topic的分片数量小于处在同一消费者组内的消费者数量时,多于的消费者空闲(不能消费数据)。
当topic的分片数量等于处在同一消费者组内的消费者数量时,每个消费者都可以消费到部分数据。
当topic的分片数量大于处在同一消费者组内的消费者数量时,有的消费者对应多个分区。
多个消费者组
启动多个组,相同的数据会被不同组的消费者消费多次,组内规则如上。
kafka为什么使用消费者组
原文地址 blog.csdn.net
- 消费者组的特点
这是 kafka 集群的典型部署模式。
消费组保证了:
一个分区只可以被消费组中的一个消费者所消费
一个消费组中的一个消费者可以消费多个分区,例如 C1
消费了 P0, P3
。
一个消费组中的不同消费者消费的分区一定不会重复,例如:
`C1 -> P0、P3``C2 -> P1、P2`
所有消费者一起消费所有的分区,例如 C1
和 C2
共同完成了对 P0、P1、P2、P3
的消费。
在不同消费组中,每个消费组都会消费所有的分区,例如,消费组A、消费组B 都消费了 P0、P1、P2、P3
。
同一个消费组里面的消费者对分区是互斥的,例如 C1 和 C2
不会消费同一个分区;而分区在不同的消费组间是共享的。
- 消费者组的优势
2.1 高性能
假设一个主题有10个分区,如果没有消费者组,只有一个消费者对这10个分区消费,他的压力肯定大。
如果有了消费者组,组内的成员就可以分担这10个分区的压力,提高消费性能。
2.2 消费模式灵活
假设有4个消费者订阅一个主题,不同的组合方式就可以形成不同的消费模式。
使用4个消费者组,每组里放一个消费者,利用分区在消费者组间共享的特性,就实现了广播(发布订阅)模式。
只使用一个消费者组,把4个消费者都放在一起,利用分区在组内成员间互斥的特性,就实现了单播(队列)模式。
2.3 故障容灾
如果只有一个消费者,出现故障后就比较麻烦了,但有了消费者组之后就方便多了。
消费组会对其成员进行管理,在有消费者加入或者退出后,消费者成员列表发生变化,消费组就会执行再平衡的操作。
例如一个消费者宕机后,之前分配给他的分区会重新分配给其他的消费者,实现消费者的故障容错。
- 小结
消费者组的好处:
-
消费效率更高
-
消费模式灵活
-
便于故障容灾
1.Kafka如何保证消息的消费顺序?
原文地址 blog.csdn.net
我们在使用消息队列的过程中经常有业务场景需要严格保证消息的消费顺序,比如我们同时发了 2 个消息,这 2 个消息对应的操作分别对应的数据库操作是:
- 更改用户会员等级。
- 根据会员等级计算订单价格。
假如这两条消息的消费顺序不一样造成的最终结果就会截然不同。
我们知道 Kafka 中 Partition(分区)是真正保存消息的地方,我们发送的消息都被放在了这里。而我们的 Partition(分区) 又存在于 Topic(主题) 这个概念中,并且我们可以给特定 Topic 指定多个 Partition。
每次添加消息到 Partition(分区) 的时候都会采用尾加法,如上图所示。 Kafka 只能为我们保证 Partition(分区) 中的消息有序。
消息在被追加到 Partition(分区)的时候都会分配一个特定的偏移量(offset)。Kafka 通过偏移量(offset)来保证消息在分区内的顺序性。
解决方案:发送消息的时候指定 key/Partition。
Kafka 中发送 1 条消息的时候,可以指定 topic, partition, key,data(数据) 4 个参数。如果你发送消息的时候指定了 Partition 的话,所有消息都会被发送到指定的 Partition。并且,同一个 key 的消息可以保证只发送到同一个 partition,这个我们可以采用表/对象的 id 来作为 key 。
2. Kafka 如何保证消息不丢失
2.1 生产者丢失消息的情况
2.1.0 生产者弄丢消息场景
acks=1
,只保证写入leader成功,如果在 follower同步成功之前 leader 故障,那么将会丢失数据;- 使用异步模式的时候,当缓冲区满了,如果配置为0(还没有收到确认的情况下,缓冲池一满,就清空缓冲池里的消息),数据就会被立即丢弃掉。
2.1.1 不要使用 producer.send(msg),而要使用 producer.send(msg, callback)。
生产者(Producer) 调用send方法发送消息之后,消息可能因为网络问题并没有发送过去。
所以,我们不能默认在调用send方法发送消息之后消息发送成功了。为了确定消息是发送成功,我们要判断消息发送的结果。但是要注意的是 Kafka 生产者(Producer) 使用 send 方法发送消息实际上是异步的操作,我们可以通过 get()方法获取调用结果,但是这样也让它变为了同步操作(不推荐),可以采用为其添加回调函数的形式,示例代码如下:
`producer.send(new ProducerRecord<String, String>("first", Integer.toString(i), Integer.toString(i)), new Callback() {
//回调函数,该方法会在 Producer 收到 ack 时调用,为异步调用
@Override
public void onCompletion(RecordMetadata metadata,Exception exception) {
if (exception == null) {
System.out.println("success->" + metadata.offset());
} else {
exception.printStackTrace();
}
}`
2.1.2 参数(acks ,retries,retry.backoff.ms)
- 设置 acks = all
acks
是 Producer 的一个参数,如果设置成 all,则表明所有副本都要接收到该消息之后该消息才算真正成功被发送。 - 重试次数
retries
同样是 Producer 的参数,当出现网络的瞬时抖动时,消息发送可能会失败,此时配置了 retries > 0 的 Producer 能够自动重试消息发送,避免消息丢失。
另外这里推荐为 Producer 的retries (重试次数)设置一个比较合理的值,一般是 3 ,但是为了保证消息不丢失的话一般会设置比较大一点。设置完成之后,当出现网络问题之后能够自动重试消息发送,避免消息丢失。另外,建议还要设置重试间隔,因为间隔太小的话重试的效果就不明显了,网络波动一次你3次一下子就重试完了。 retry.backoff.ms
参数表示消息生产超时失败后重试的间隔时间。
2.1.4
异步方式缓冲区满了,就阻塞在那,等着缓冲区可用,不能清空缓冲区(一旦清空,数据就丢失了)
2.2 Kafka 弄丢了消息
2.2.1 replication.factor 设置副本个数
设置 replication.factor
。这也是 Broker 端的参数。
其实这里想表述的是,最好将消息多保存几份,毕竟目前防止消息丢失的主要机制就是冗余。
2.2.2 min.insync.replicas ISR最少副本数量
设置 min.insync.replicas
> 1。这依然是 Broker 端参数,控制的是消息至少要被写入到多少个副本才算是“已提交”。(设置成大于 1 可以提升消息持久性。在实际环境中千万不要使用默认值 1。)
注意
确保 replication.factor > min.insync.replicas。
如果两者相等,那么只要有一个副本挂机,整个分区就无法正常工作了。我们不仅要改善消息的持久性,防止数据丢失,还要在不降低可用性的基础上完成。
推荐设置成 replication.factor = min.insync.replicas + 1。
2.2.3 unclean.leader.election.enable=false 是否能把非ISR集合中的副本选举为leader副本
2.3 消费者丢失消息的情况(手动Offset:enable.auto.commit
设置成 false)
当消费者拉取到了分区的某个消息之后,消费者会自动提交了 offset。自动提交的话会有一个问题,试想一下,当消费者刚拿到这个消息准备进行真正消费的时候,突然挂掉了,消息实际上并没有被消费,但是 offset 却被自动提交了,发生了消息丢失。
解决办法也比较粗暴,我们手动关闭自动提交 offset(Consumer 端有个参数 enable.auto.commit
设置成 false。),每次在真正消费完消息之后再自己手动提交 offset 。
但是,细心的朋友一定会发现,这样会带来消息被重新消费的问题。比如你刚刚消费完消息之后,还没提交 offset,结果自己挂掉了,那么这个消息理论上就会被消费两次。
3. Kafka 如何保证消息不重复消费
3.1 kafka出现消息重复消费的原因
服务端侧已经消费的数据没有成功提交 offset(根本原因)。
Kafka 侧 由于服务端处理业务时间长或者网络链接等等原因让 Kafka 认为服务假死,触发了分区 rebalance。
3.2 消费者重复消费
- 业务id去重
- 消费消息服务做幂等校验,比如 Redis 的set、MySQL 的主键等天然的幂等功能。这种方法最有效。
- 将
enable.auto.commit
参数设置为 false,关闭自动提交,开发者在代码中手动提交 offset。
3.2 生产者重复消费
生产端重复发送:这个不重要,依赖消费端去重即可。
4. kafka和[rabbitmq]的区别
- Broker与Consume交互方式不同
RabbitMQ 采用push的方式
kafka采用pull的方式
2.使用场景
rabbitMQ支持对消息的可靠的传递,支持事务,不支持批量的操作;基于存储的可靠性的要求存储可以采用内存或者硬盘。
kafka具有高的吞吐量,内部采用消息的批量处理,zero-copy机制,数据的存储和获取是本地磁盘顺序批量操作,具有O(1)的复杂度(与分区上的存储大小无关),消息处理的效率很高。(大数据)
kafka大数量消息持续积压几个小时如何解决
发生了线上故障,几千万条数据在 MQ 里积压很久。是修复 consumer 的问题,让他恢 复消费速度,然后等待几个小时消费完毕?这是个解决方案。不过有时候我们还会进行临时 紧急扩容。
一个消费者一秒是 1000 条,一秒 3 个消费者是 3000 条,一分钟是 18 万条。1000 多万 条,所以如果积压了几百万到上千万的数据,即使消费者恢复了,也需要大概 1 小时的时间 才能恢复过来。
一般这个时候,只能操作临时紧急扩容了,具体操作步骤和思路如下:
(1)先修复 consumer 的问题,确保其恢复消费速度,然后将现有 consumer 都停掉。
(2)新建一个 topic,partition 是原来的 10 倍,临时建立好原先 10 倍或者 20 倍的 queue 数 量。然后写一个临时的分发数据的 consumer 程序,这个程序部署上去消费积压的数据,消 费之后不做耗时的处理,直接均匀轮询写入临时建立好的 10 倍数量的 queue。
(3)接着临时征用 10 倍的机器来部署 consumer,每一批 consumer 消费一个临时 queue 的 数据。
这种做法相当于是临时将 queue 资源和 consumer 资源扩大 10 倍,以正常的 10 倍速度 来消费数据。 等快速消费完积压数据之后,再恢复原先部署架构,重新用原先的 consumer 机器来消 费消息。
Kafka 集群扩容
原文地址 blog.csdn.net
kafka集群扩容后的数据均衡 blog.csdn.net
排查问题与分析
Kafka 消费组发生重平衡的条件有以下几个:
1.消费组成员发生变更,有新消费者加入或者离开,或者有消费者崩溃;
2.消费组订阅的主题数量发生变更;
3.消费组订阅的分区数发生变更。
对集群进行水平扩展,增加集群的负载能力,并对专门的主题进行分区重分配。
分区重分配方案的分析
目前集群一共有 6 个节点,扩容以 50% 为基准,那么需要在准备 3 个节点,在运维准备好机器并且将其加入到集群中后,接下来就要准备对主题进行分区重分配的策略文件了。
在执行分区重分配的过程中,对集群的影响主要有两点:
1.分区重分配主要是对主题数据进行 Broker 间的迁移,因此会占用集群的带宽资源;
2.分区重分配会改变分区 Leader 所在的 Broker,因此会影响客户端。
针对以上两点,第 1 点可以在晚间进行(太苦逼了,记得有个主题数据迁移进行了将近5小时),针对第二点,我想到了两个方案:
1.整个分配方案分成两个步骤:
1)手动生成分配方案,对原有的分区 Leader 位置不改变,只对副本进行分区重分配;
2)等待数据迁移完成后,再手动更改分区分配方案,目的是均衡 Leader。
2.直接用 Kafka 提供的 API 生成 分区重分配方案,直接执行分区重分配。
第一个方案理论上是对客户端影响最小的,把整个分配方案分成了两个步骤,也就是将对集群的带宽资源与客户端的影响分开了,对过程可控性更高了,但问题来了,
集群中的某些主题,有 64 个分区,副本因子为 3,副本一共有 192 个,你需要保持原有分区 Leader 位置不变的情况下,去手动均衡其余副本,这个考验难
度真的太大了,稍微有一点偏差,就会造成副本不均衡。
针对第二个方案我特意去看了分区重分配的源码,并对其过程进行了进一步分析,发现分配重分配的步骤是:
将分区原有的副本与新分配的副本的集合,组成一个分区副本集合,新分配的副本努力追上 Leader 的位移offset,最终加入 ISR,待全部副本都加入 ISR 之后,
就会进行分区 Leader 选举,选举完后就会将原有的副本删除。这里注意,由于是最后选举完成才删除原副本,所以重分配的过程中,日志存储量是会大幅增加的。
根据以上分析,意味着在数据进行重分配过程中,Leader并没有发生变动,所以客户端不会阻塞,数据迁移完成后进行Leader选举时发生变更,生产者会及时
拉取最新的元数据,并重新进行消息发送,影响并不大。
针对以上的分析与测试,我们决定采取第二种方案,具体步骤如下:
1.对每个主题生成分配分区分配策略:执行时间段(10:00-17:00),并对分配策略进行检查,并保存执行的 topic1_partition_reassignment.json 文件,
并把原来的方案保存到topic1_partition_reassignment_rollback.json 文件中,以备后续的 rollback 操作;
2.执行分配策略:执行时间段(00:30-02:30),准备好的 topic1_partition_reassignment.json 文件,执行完再验证并查看副本分配情况,每执行一个
分配策略都要查看 ISR 收缩扩张状况、消息流转状况,确定没问题后再执行下一个分配策略;
3.由于集群 broker 端的参数 auto.leader.rebalance.enable=true,因此会自动执行 Preferred Leader 选举,默认时间间隔为 300 秒,期间需要
观察 Preferred Leader 选举状况。
分区重分配
对于新增的 Broker,Kafka 是不会自动地分配已有主题的负载,即不会将主题的分区分配到新增的 Broker,但我们可以通过 Kafka 提供的 API 对主题分区
进行重分配操作
kafka如何扩容服务器、重新分区Partition
原文地址 blog.csdn.net
- 扩容
在新的物理机上安装[kafka]程序,修改config/server.properties
文件里的broker.id
必须在集群中唯一,修改其他必要的配置项,
其中zookeeper.connect
配置项,写上kafka集群现在使用的[zookeeper]集群的地址。
然后启动kafka就可以加入到集群中了。
但是新加入的机器只能对新产生的topic起作用,对已有的topic在没有做处理前,是不会承担任何任务的,所以不会分担集群的压力。
- 重新分区Partition
假设有一个名为test的topic,只有1个partition,现在由于存储空间不足,需要重新分区。
2.1 修改topic的partitions
./bin/kafka-topics.sh --zookeeper 10.0.210.152:2181 --alter --topic test --partitions 6
现在topic有6个partition,但是数据还没有迁移过去
2.2 迁移数据
扩容kafka之后,针对扩容之前的topic进行重新平衡leader,Replicas,Isr
使用kafka提供的工具kafka-reassign-partitions.sh
来迁移数据。迁移数据需要分三步做
第一步:生成迁移计划
先手动生成一个topic.json
,内容如下。这里topic可以是一个列表,
`{
"topics": [
{"topic": "test"}
],
"version": 1
}`
执行如下语句,
./bin/kafka-reassign-partitions.sh --zookeeper 10.0.210.152:2181 --topics-to-move-json-file topic.json --broker-list "0,1,2,3,4" --generate
将topic.json里的topic迁移到broker-list列表里列的broker上,会得到一个执行计划
`Current partition replica assignment
{"version":1,
"partitions":[....]
}
Proposed partition reassignment configuration
{"version":1,
"partitions":[.....]
}`
新建一个文件reassignment.json
,保存上边这些信息。其中Current partition replica assignment
指当前的分区情况,Proposed partition reassignment configuration
是计划的分区情况
第二步:迁移
执行如下命令
./bin/kafka-reassign-partitions.sh --zookeeper 10.0.210.152:2181 --reassignment-json-file reassignment.json --execute
第三步:验证
./bin/kafka-reassign-partitions.sh --zookeeper 10.0.210.152:2181 --reassignment-json-file reassignment.json --verify
3. 其他kafka常用命令
`./bin/kafka-console-producer.sh --broker-list 10.0.210.152:9092 --topic test
./bin/kafka-console-consumer.sh --zookeeper 10.0.210.152:2181 --topic test --from-beginning
./bin/kafka-topics.sh --zookeeper 10.0.210.152:2181 --list
./bin/kafka-topics.sh --zookeeper 10.0.210.152:2181 --create --replication-factor 2 --partition 6 --topic test
./bin/kafka-topics.sh --zookeeper 10.0.210.152:2181 --delete --topic test
./bin/kafka-topics.sh --zookeeper 10.0.210.152:2181 --describe --topic test`
Kafka 分区分配策略(Range分配策略 && RoundRobin分配策略)
原文地址 blog.csdn.net
前言
Kafka为了增加系统的伸缩性(Scalability),引入了分区(Partitioning)的概念。
Kafka 中的分区机制指的是将每个主题划分成多个分区(Partition),每个分区是一组有序的消息日志。主题下的每条消息只会保存在某一个分区中,而不会在多个分区中被保存多份。
通过这个设计,就可以以分区这个粒度进行数据读写操作,每个Broker的各个分区独立处理请求,进而实现负载均衡,提升了整体系统的吞吐量。
分区策略是决定生产者将消息发送到哪个分区的算法。
在 Kafka 实际生产过程中,每个 topic 都会有 多个 partitions。
1.多个Partitions有什么好处?
①多个 partition ,能够对 broker 上的数据进行分片,通过减少消息容量来提升 IO 性能;
②为了提高消费端的消费能力,一般情况下会通过多个 conusmer 去消费 同一个 topic 中的消息,即实现消费端的负载均衡。
2.针对多个Partition,消费者该消费哪个分区的消息?
Kafka 存在 消费者组 group.id 的概念,组内的所有消费者协调在一起来消费订阅的 topic 中的消息(消息可能存在于多个分区中)。那么同一个 group.id
组中的 consumer 该如何去分配它消费哪个分区里的数据。
针对下图中情况,3 个分区(test-0 ~ test-3),3 个消费者(ConsumerA ~ C),哪个消费者应该消费哪个分区的消息呢??
对于如上这种情况,3 个分区, 3 个消费者。这 3 个消费者都会分别去消费 test 中 topic 的 3 个分区,也就是每个 Consumer 会消费一个分区中的消息。
如果 4 个消费者消费 3 个分区,则会有 1 个消费者无法消费到消息;如果 2 个消费者消费 3 个分区,则会有 1 个消费者消费 2 个分区的消息。**针对
这种情况,分区数 和 消费者数 之间,该如何选择?**此处就涉及到 Kafka 消费端的分区分配策略了。
1.什么是分区分配策略
通过如上实例,我们能够了解到,同一个 group.id 中的消费者,对于一个 topic 中的多个 partition 中的消息消费,存在着一定的分区分配策略。
在 kafka 中,存在着两种分区分配策略。一种是 RangeAssignor 分配策略(范围分区),另一种是 RoundRobinAssignor分配策略(轮询分区)。
默认采用 Range 范围分区。 Kafka提供了消费者客户端参数 partition.assignment.strategy 用来设置消费者与订阅主题之间的分区分配策略。
默认情况下,此参数的值为:org.apache.kafka.clients.consumer.RangeAssignor,即采用RangeAssignor分配策略
1.1 RangeAssignor 范围分区
Range 范围分区策略是对每个 topic 而言的。首先对同一个 topic 里面的分区按照序号进行排序,并对消费者按照字母顺序进行排序。假如现在有 10 个分区,
3 个消费者,排序后的分区将会是0,1,2,3,4,5,6,7,8,9;消费者排序完之后将会是C1-0,C2-0,C3-0。通过 partitions数/consumer数 来决定每个
消费者应该消费几个分区。如果除不尽,那么前面几个消费者将会多消费 1 个分区。
例如,10/3 = 3 余 1 ,除不尽,那么 消费者 C1-0 便会多消费 1 个分区,最终分区分配结果如下:
C1-0 | 消费 0,1,2,3 分区 |
C2-0 | 消费 4,5,6 分区 |
C3-0 | 消费 7,8,9 分区(如果有11 个分区的话,C1-0 将消费0,1,2,3 分区, C2-0 将消费4,5,6,7分区 C3-0 将消费 8,9,10 分区) |
Range 范围分区的弊端:
如上,只是针对 1 个 topic 而言,C1-0消费者多消费1个分区影响不是很大。如果有 N 多个 topic,那么针对每个 topic,消费者 C1-0 都将多消费 1 个分区,
topic越多,C1-0 消费的分区会比其他消费者明显多消费 N 个分区。这就是 Range 范围分区的一个很明显的弊端了
由于 Range 范围分区存在的弊端,于是有了 RoundRobin 轮询分区策略,如下介绍↓↓↓
1.2 RoundRobinAssignor 轮询分区
RoundRobin 轮询分区策略,是把所有的 partition 和所有的 consumer 都列出来,然后按照 hascode 进行排序,最后通过轮询算法来分配 partition
给到各个消费者。
轮询分区分为如下两种情况:①同一消费组内所有消费者订阅的消息都是相同的 ②同一消费者组内的消费者锁定月的消息不相同
①如果同一消费组内,所有的消费者订阅的消息都是相同的,那么 RoundRobin 策略的分区分配会是均匀的。
例如:同一消费者组中,有 3 个消费者C0、C1和C2,都订阅了 2 个主题 t0 和 t1,并且每个主题都有 3 个分区(p0、p1、p2),那么所订阅的所以
分区可以标识为t0p0、t0p1、t0p2、t1p0、t1p1、t1p2。最终分区分配结果如下:
消费者C0 | 消费 t0p0 、 t1p0 分区 |
消费者C1 | 消费 t0p1 、t1p1 分区 |
消费者C2 | 消费 t0p2 、t1p2 分区 |
②如果同一消费者组内,所订阅的消息是不相同的,那么在执行分区分配的时候,就不是完全的轮询分配,有可能会导致分区分配的不均匀。如果某个消费者没有
订阅消费组内的某个 topic,那么在分配分区的时候,此消费者将不会分配到这个 topic 的任何分区。
例如:同一消费者组中,有3个消费者C0、C1和C2,他们共订阅了 3 个主题:t0、t1 和 t2,这 3 个主题分别有 1、2、3 个分区(即:t0有1个分区(p0),
t1有2个分区(p0、p1),t2有3个分区(p0、p1、p2)),即整个消费者所订阅的所有分区可以标识为 t0p0、t1p0、t1p1、t2p0、t2p1、t2p2。具体而言,
消费者C0订阅的是主题t0,消费者C1订阅的是主题t0和t1,消费者C2订阅的是主题t0、t1和t2,最终分区分配结果如下:
消费者C0 | 消费 t0p0 |
消费者C1 | 消费 t1p0 分区 |
消费者C2 | 消费 t1p1、t2p0、t2p1、t2p2 分区 |
RoundRobin轮询分区的弊端:
从如上实例,可以看到RoundRobin策略也并不是十分完美,这样分配其实并不是最优解,因为完全可以将分区 t1p1 分配给消费者 C1。
所以,如果想要使用RoundRobin 轮询分区策略,必须满足如下两个条件:
①每个消费者订阅的主题,必须是相同的
②每个主题的消费者实例都是相同的。(即:上面的第一种情况,才优先使用 RoundRobin 轮询分区策略)
2.什么时候触发分区分配策略
当出现以下几种情况时,Kafka 会进行一次分区分配操作,即 Kafka 消费者端的 Rebalance 操作
① 同一个 consumer 消费者组 group.id 中,新增了消费者进来,会执行 Rebalance 操作
② 消费者离开当期所属的 consumer group组。比如 **主动停机 ** 或者 宕机
③ 分区数量发生变化时(即 topic 的分区数量发生变化时)
④ 消费者主动取消订阅
Kafka 消费端的 Rebalance 机制,规定了一个 Consumer group 下的所有 consumer 如何达成一致来分配订阅 topic 的每一个分区。而具体如何执行
分区策略,就是上面提到的 Range 范围分区 和 RoundRobin 轮询分区 两种内置的分区策略。
Kafka 对于分区分配策略这块,也提供了可插拔式的实现方式,除了上面两种分区分配策略外,我们也可以创建满足自己使用的分区分配策略,即:自定义分区策略
3.Kafka实现自定义分区策略
【此处省略不再介绍,因为我不想介绍,还不是因为我也不会,不想继续深入了解的原因,太烧脑了,哈哈】
kafka顺序性投递,顺序性消费代码
原文地址 blog.csdn.net
针对消息有序的业务需求,还分为全局有序和局部有序。
全局有序:一个Topic下的所有消息都需要按照生产顺序消费。
局部有序:一个Topic下的消息,只需要满足同一业务字段的要按照生产顺序消费。例如:Topic消息是订单的流水表,包含订单orderId,业务要求同一个orderId的消息需要按照生产顺序进行消费。
全局有序
由于Kafka的一个Topic可以分为了多个Partition,Producer发送消息的时候,是分散在不同 Partition的。当Producer按顺序发消息给Broker,但进入Kafka之后,这些消息就不一定进到哪个Partition,会导致顺序是乱的。
因此要满足全局有序,需要1个Topic只能对应1个Partition。
而且对应的consumer也要使用单线程或者保证消费顺序的线程模型,否则会出现下图所示,消费端造成的消费乱序。
局部有序
要满足局部有序,只需要在发消息的时候指定Partition Key,Kafka对其进行Hash计算,根据计算结果决定放入哪个Partition。这样Partition Key相同的消息会放在同一个Partition。此时,Partition的数量仍然可以设置多个,提升Topic的整体吞吐量。
如下图所示,在不增加partition数量的情况下想提高消费速度,可以考虑再次hash唯一标识(例如订单orderId)到不同的线程上,多个消费者线程并发处理消息(依旧可以保证局部有序)。
topic: "topic_query_p3r1" 分配了三个partition分区
实现顺序性原理:
设置相同的key会把消息投递到同一个分区的topic中,再由一个消费者来消费该分区topic。
投递顺序消息
同一组行为设置相同的key,会把这组数据投递到同一分区topic中。
1. /**
2. * 投递顺序性消息,根据用户id做取模推送到不同分区的topic中
3. * 相同的key推送到同一分区中
4. */
5. @RequestMapping("/kafka2")
6. public String testKafka2() {
7. for (int userId = 0; userId < 300; userId++) {
8. kafkaTemplate.send("topic_query_p3r1", userId + "", "insert" + userId);
9. kafkaTemplate.send("topic_query_p3r1", userId + "", "update" + userId);
10. kafkaTemplate.send("topic_query_p3r1", userId + "", "delete" + userId);
11. }
12. return null;
13. }
消费顺序消息
方式1 - 直接进行消费
** 因为投递的相同行为的消息是有序的,所以直接消费也不会有问题。**
1. /**
2. * 消费topic_query_p3r1主题,ConsumerGroupId1消费组
3. */
4. @KafkaListener(topics = "topic_query_p3r1", groupId = "ConsumerGroupId1")
5. public void p3r2ConsumerGroupId0(ConsumerRecord<?, ?> consumer) throws InterruptedException {
6. System.out.println("消费者A topic名称:" + consumer.topic() +
7. ", key:" + consumer.key() +
8. ", value:" + consumer.value() +
9. ", 分区位置:" + consumer.partition() +
10. ", 下标" + consumer.offset()+" "+Thread.currentThread().getId());
11. Thread.sleep(10);
12. }
方式2.1 - 一个消费者来指定具体分区进行消费
指定具体分区来进行消费。
1. /**
2. * 消费者,解决消息顺序性
3. * 注解参数:partitions=0表示:只消费该主题中0分区的数据。
4. */
5. @KafkaListener(topicPartitions = {@TopicPartition(topic = "topic_query_p3r1", partitions = {"0"})}, groupId = "ConsumerGroupId1")
6. public void receive(ConsumerRecord<?, ?> consumer) {
7. System.out.println("消费者C topic名称:" + consumer.topic() +
8. ",key:" + consumer.key() + "," +
9. ",value:" + consumer.value() + "," +
10. "分区位置:" + consumer.partition() +
11. ", 下标" + consumer.offset());
12. }
方式2.2 - 多个消费者来指定不同分区进行消费。
写多个消费者方法来分别指向不同分区,提高消费速度,但是此方法不灵活。
1. /**
2. * 消费0分区的topic_query_p3r1主题消费者,ConsumerGroupId1消费组
3. */
4. @KafkaListener(topicPartitions = {@TopicPartition(topic = "topic_query_p3r1", partitions = {"0"})}, groupId = "ConsumerGroupId1")
5. public void p3r2ConsumerGroupId0(ConsumerRecord<?, ?> consumer) throws InterruptedException {
6. System.out.println("消费者A topic名称:" + consumer.topic() +
7. ", key:" + consumer.key() +
8. ", value:" + consumer.value() +
9. ", 分区位置:" + consumer.partition() +
10. ", 下标" + consumer.offset()+" "+Thread.currentThread().getId());
11. Thread.sleep(10);
12. }
13. /**
14. * 消费1分区的topic_query_p3r1主题消费者,ConsumerGroupId1消费组
15. */
16. @KafkaListener(topicPartitions = {@TopicPartition(topic = "topic_query_p3r1", partitions = {"1"})}, groupId = "ConsumerGroupId1")
17. public void p3r2ConsumerGroupId1(ConsumerRecord<?, ?> consumer) throws InterruptedException {
18. System.out.println("消费者A topic名称:" + consumer.topic() +
19. ", key:" + consumer.key() +
20. ", value:" + consumer.value() +
21. ", 分区位置:" + consumer.partition() +
22. ", 下标" + consumer.offset()+" "+Thread.currentThread().getId());
23. Thread.sleep(10);
24. }
25. /**
26. * 消费2分区的topic_query_p3r1主题消费者,ConsumerGroupId1消费组
27. */
28. @KafkaListener(topicPartitions = {@TopicPartition(topic = "topic_query_p3r1", partitions = {"2"})}, groupId = "ConsumerGroupId1")
29. public void p3r2ConsumerGroupId2(ConsumerRecord<?, ?> consumer) throws InterruptedException {
30. System.out.println("消费者A topic名称:" + consumer.topic() +
31. ", key:" + consumer.key() +
32. ", value:" + consumer.value() +
33. ", 分区位置:" + consumer.partition() +
34. ", 下标" + consumer.offset()+" "+Thread.currentThread().getId());
35. Thread.sleep(10);
36. }
多线程顺序消费
1. // 使用两个内存队列
2. final int queueLingth = 2;
4. // 创建两个内存队列
5. Queue<Map> queueA = new ConcurrentLinkedQueue<>();
6. Queue<Map> queueB = new ConcurrentLinkedQueue<>();
8. /**
9. * 投递顺序性消息,根据用户id做取模推送到不同分区的topic中
10. * 相同的key推送到相同的分区中
11. */
12. @RequestMapping("/kafka2")
13. public String testKafka2() {
14. for (int userId = 0; userId < 300; userId++) {
15. kafkaTemplate.send("topic_query_p3r1", userId + "", "insert" + userId);
16. kafkaTemplate.send("topic_query_p3r1", userId + "", "update" + userId);
17. kafkaTemplate.send("topic_query_p3r1", userId + "", "delete" + userId);
18. }
19. return null;
20. }
22. /**
23. * 主题消费者-把相同行为的数据放到同一内存队列中
24. */
25. @KafkaListener(topics = "topic_query_p3r1", groupId = "ConsumerGroupId1")
26. public void p3r2ConsumerGroupId0(ConsumerRecord<?, ?> consumer){
27. // 1.封装消息参数
28. Map param = new HashMap();
29. param.put("topic", consumer.topic());
30. param.put("key", consumer.key());
31. param.put("value", consumer.value());
32. param.put("p", consumer.partition());
34. // 2.把相同行为(key)数据添加到同一内存队列中
35. int queueHash = consumer.key().hashCode() % queueLingth;
36. if (queueHash == 0) {
37. queueA.add(param);
38. }
39. if (queueHash == 1) {
40. queueB.add(param);
41. }
42. }
45. // 开启两个线程消费内存队列中的消息
46. @Override
47. public void run(ApplicationArguments args) throws Exception {
48. new Thread() {
49. @Override
50. public void run() {
51. while (true) {
52. if (queueA.size() > 0) {
53. Map poll = queueA.poll();
54. System.out.println("Thrend-Id: "+ Thread.currentThread().getId() +
55. " topic:" + poll.get("topic") +
56. " key:" + poll.get("key") +
57. " value:" + poll.get("value") +
58. " partition:" + poll.get("p"));
60. try {
61. Thread.sleep(10);
62. } catch (InterruptedException e) {
63. e.printStackTrace();
64. }
65. }
66. }
67. }
68. }.start();
70. new Thread() {
71. @Override
72. public void run() {
73. while (true) {
74. if (queueB.size() > 0) {
75. Map poll = queueB.poll();
76. System.out.println("Thrend-Id: "+ Thread.currentThread().getId() + " topic:" + poll.get("topic") + " key:" + poll.get("key") + " value:" + poll.get("value") + " partition:" + poll.get("p"));
78. try {
79. Thread.sleep(10);
80. } catch (InterruptedException e) {
81. e.printStackTrace();
82. }
83. }
84. }
85. }
86. }.start();
88. }
**打印:**insert、update、delete都是有序的。相同行为都在同一线程下执行。
消息重试对顺序消息的影响
对于一个有着先后顺序的消息A、B,正常情况下应该是A先发送完成后再发送B,但是在异常情况下,在A发送失败的情况下,B发送成功,而A由于重试机制在B发送
完成之后重试发送成功了。这时对于本身顺序为AB的消息顺序变成了BA。
针对这种问题,严格的顺序消费还需要max.in.flight.requests.per.connection参数的支持。
该参数指定了生产者在收到服务器响应之前可以发送多少个消息。它的值越高,就会占用越多的内存,同时也会提升吞吐量。把它设为1就可以保证消息是按照发送
的顺序写入服务器的。
此外,对于某些业务场景,设置max.in.flight.requests.per.connection=1会严重降低吞吐量,如果放弃使用这种同步重试机制,则可以考虑在消费端
增加失败标记的记录,然后用定时任务轮询去重试这些失败的消息并做好监控报警。
🌈 🌈 🌈 🌈 🌈 🌈 🌈 🌈 🌈 🌈 🌈 🌈 🌈 🌈 🌈 🌈 🌈
🍎原创不易,感觉对自己有用的话就❤️点赞👉收藏💬评论把。
kafka自定义分区,数据流向指定分区,消费者组的消费者消费指定分区(java代码实现)
原文地址 blog.csdn.net
业务场景参考
在IOT领域,传感器设备的数据向服务器发送数据,预想一个类型的数据放一个topic里,但是根据实际情况。
kaka的topic数越多,吞吐量性能下降厉害。
所以想象将分区做个自定义,然后消费者组的消费者消费指定分区,达到这一目的。
一 自定义kafka分区
`public class SimplePartitioner implements Partitioner {
private final AtomicInteger atomicInteger = new AtomicInteger(0);
@Override
public void configure(Map<String, ?> configs) {
// TODO Auto-generated method stub
}
@Override
public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
// 从集群中获取所有分区信息,对key值进行分区
List<PartitionInfo> partitions = cluster.partitionsForTopic(topic);
//根据key进行分区 xx_01在01分区,xx_02在02分区
int numPartitions = partitions.size();
// key为null或空时使用轮训分区做负载均衡, 注意可以一定不能为null
if (null == keyBytes || keyBytes.length < 1) {
return atomicInteger.getAndIncrement() % numPartitions;
}
// 借用String的字符串计算方法,返回在哪个分区
String strKey = key.toString();
System.out.println(Integer.parseInt(strKey.split("_")[1]));
return Integer.parseInt(strKey.split("_")[1]);
}
@Override
public void close() {
// TODO Auto-generated method stub
}
}` ![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)
二 生产者生产数据放到指定分区
`public class TestPartitionProducer implements Serializable {
private KafkaProducer kafkaProducer;
public final static String TOPIC_SPARKSTREAMING="TOPIC_SPARKSTREAMING_part1";
/**
* 初始化
*/
{
//1. 创建配置对象 指定Producer的信息
Properties properties = new Properties();
properties.put("acks", "1");
//配置默认的分区方式 ☆☆☆这里指定我们自定义的分区
properties.put("partitioner.class", "com.dahai.kafka.producer.SimplePartitioner");
//配置topic的序列化类
properties.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
//配置value的序列化类
properties.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
//kafka broker对应的主机,格式为host1:port1,host2:port2
properties.put("bootstrap.servers", "192.168.1.109:9092,192.168.1.111:9092,192.168.1.112:9092");
//2. 创建Producer对象
kafkaProducer=new KafkaProducer(properties);
}
/**
* 发布消息
* 自定义的数据
*/
private void sendData(){
//3. 发布消息
int counter = 0;
String[] str=new String[]{"lj:","xf:","other:" };
String value;
while (true) {
if (counter%3==0){
value = str[new Random().nextInt(3)] + new Random().nextInt(2);
}else {
value = "other:"+new Random().nextInt(2);
}
//设计key值,可以根据指定的数据设计指定的key,其中最后的数据应该为分区数,必须提前定义好
String key ="zsli"+ counter + "_"+new Random().nextInt(3);
/**
* producer将 message发送数据到 kafka topic的时候,这条数据应该发到哪个partition分区里呢?
* message 有key,value组成
* 当message的key为null值,必须禁止使用,则将message随机发送到partition里
* 当message的key不为null值时,则通过自定义的key的最后的字符,得到数就是partition id.
*/
ProducerRecord<String, String> record = new ProducerRecord<String, String>(TOPIC_SPARKSTREAMING, key, value);
kafkaProducer.send(record);
System.out.println("生产数据"+value + " ---"+System.currentTimeMillis());
//每2条数据暂停1秒
if (0 == counter % 2) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
counter++;
}
}
/**
* 主方法
* @param args
*/
public static void main(String[] args) {
final TestPartitionProducer flumeKafkaProducer= new TestPartitionProducer();
new Thread(new Runnable() {
@Override
public void run() {
flumeKafkaProducer.sendData();
}
}).start();
}
}` ![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)
三 指定消费者组的消费者消费指定分区
consumer1
`public class JavaConsumer01 {
public final static String TOPIC_SPARKSTREAMING="TOPIC_SPARKSTREAMING_part";
public static void main(String[] args) {
//设置消费组的名称
//将属性值反序列化
Properties properties=new Properties();
properties.put("key.deserializer","org.apache.kafka.common.serialization.StringDeserializer");
properties.put("value.deserializer","org.apache.kafka.common.serialization.StringDeserializer");
properties.put("bootstrap.servers","192.168.1.109:9092,192.168.1.111:9092,192.168.1.112:9092");
properties.put("group.id","demo");
properties.put("enable.auto.commit", "true");
properties.put("auto.offset.reset", "earliest");
properties.put("auto.commit.interval.ms", "1000");
//创建一个消费者客户端实例
KafkaConsumer<String,String> consumer=new KafkaConsumer<>(properties);
//订阅主题,指定订阅分区
consumer.assign(Collections.singletonList(new TopicPartition(TOPIC_SPARKSTREAMING,0)));
//循环消费消息
while (true){
ConsumerRecords<String,String> records=consumer.poll(0);
for (ConsumerRecord<String,String> record:records){
// System.out.println("获取数据"+record.value()+"---"+System.currentTimeMillis());
System.out.printf("partition = %d,offset = %d, value = %s", record.partition(),record.offset(), record.value());
System.out.println("---"+System.currentTimeMillis());
}
}
}
}` ![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)
consumer2
`public class JavaConsumer02 {
public final static String TOPIC_SPARKSTREAMING="TOPIC_SPARKSTREAMING_part";
public static void main(String[] args) {
//设置消费组的名称
//将属性值反序列化
Properties properties=new Properties();
properties.put("key.deserializer","org.apache.kafka.common.serialization.StringDeserializer");
properties.put("value.deserializer","org.apache.kafka.common.serialization.StringDeserializer");
properties.put("bootstrap.servers","192.168.1.109:9092,192.168.1.111:9092,192.168.1.112:9092");
properties.put("group.id","demo");
properties.put("enable.auto.commit", "true");
properties.put("auto.offset.reset", "earliest");
properties.put("auto.commit.interval.ms", "1000");
//创建一个消费者客户端实例
KafkaConsumer<String,String> consumer=new KafkaConsumer<>(properties);
//订阅主题,指定订阅分区
consumer.assign(Collections.singletonList(new TopicPartition(TOPIC_SPARKSTREAMING,1)));
//循环消费消息
while (true){
ConsumerRecords<String,String> records=consumer.poll(0);
for (ConsumerRecord<String,String> record:records){
System.out.printf("partition = %d,offset = %d, value = %s", record.partition(),record.offset(), record.value());
System.out.println("---"+System.currentTimeMillis());
}
}
}
}` ![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)
consumer3
`public class JavaConsumer03 {
public final static String TOPIC_SPARKSTREAMING="TOPIC_SPARKSTREAMING_part";
public static void main(String[] args) {
//设置消费组的名称
//将属性值反序列化
Properties properties=new Properties();
properties.put("key.deserializer","org.apache.kafka.common.serialization.StringDeserializer");
properties.put("value.deserializer","org.apache.kafka.common.serialization.StringDeserializer");
properties.put("bootstrap.servers","192.168.1.109:9092,192.168.1.111:9092,192.168.1.112:9092");
properties.put("group.id","demo");
properties.put("enable.auto.commit", "true");
properties.put("auto.offset.reset", "earliest");
properties.put("auto.commit.interval.ms", "1000");
//创建一个消费者客户端实例
KafkaConsumer<String,String> consumer=new KafkaConsumer<>(properties);
//订阅主题,指定订阅分区
consumer.assign(Collections.singletonList(new TopicPartition(TOPIC_SPARKSTREAMING,3)));
//循环消费消息
while (true){
ConsumerRecords<String,String> records=consumer.poll(0);
for (ConsumerRecord<String,String> record:records){
System.out.printf("partition = %d,offset = %d, value = %s", record.partition(),record.offset(), record.value());
System.out.println("---"+System.currentTimeMillis());
}
}
}
}` ![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)
kafka - Producer 手动acks应答
原文地址 javab.blog.csdn.net
第一种acks
1. /**
2. * 生产者发送消息
3. *
4. * @param key 推送数据的key
5. * @param data 推送数据的data
6. */
7. private void send(String key, String data) {
8. // topic key名称 data消息数据
9. ListenableFuture<SendResult<String, String>> listenableFuture = kafkaTemplate.send("cqTestTopic", key, data);
11. // 开启消息监听
12. //发送成功后回调
13. SuccessCallback<SendResult<String,String>> successCallback = new SuccessCallback<SendResult<String,String>>() {
14. @Override
15. public void onSuccess(SendResult<String,String> result) {
16. System.out.println("发送消息成功 - "+"kay:"+key+" value:"+data);
17. }
18. };
19. //发送失败回调
20. FailureCallback failureCallback = new FailureCallback() {
21. @Override
22. public void onFailure(Throwable ex) {
23. System.out.println("发送消息失败-添加消息到发送失败日志中 - "+"kay:"+key+" value:"+data);
24. }
25. };
27. listenableFuture.addCallback(successCallback,failureCallback);
28. }
第二种acks
1. /**
2. * acks
3. * 消息发送的监听器,用于回调返回信息
4. *
5. * 使用:在需要监听Producer推送消息回调的kafkaTemplate.send(*)方法下引入
6. */
7. public void acks() {
8. kafkaTemplate.setProducerListener(new org.springframework.kafka.support.ProducerListener<String, String>() {
9. @Override
10. public void onSuccess(ProducerRecord<String, String> producerRecord, RecordMetadata recordMetadata) {
11. System.out.println("acks onSuccess >> topic:" +
12. producerRecord.topic() + " key:" +
13. producerRecord.key() + " value:" +
14. producerRecord.value() + " timestamp:" +
15. producerRecord.timestamp());
16. }
18. @Override
19. public void onError(ProducerRecord<String, String> producerRecord, Exception exception) {
20. System.out.println("acks onError >> topic:" +
21. producerRecord.topic() + " key:" +
22. producerRecord.key()+ " value:" +
23. producerRecord.value());
24. }
25. });
26. }
kafka - Consumer 手动acks应答
原文地址 javab.blog.csdn.net
目录
yaml配置
1. # 是否自动消费(true=自动应答)
2. enable-auto-commit: false
3. listener:
4. ## 手动提交ack offset模式
5. ack-mode: manual
1. # kafka
2. spring:
3. kafka:
4. # kafka服务器地址(可以多个)
5. bootstrap-servers: 192.168.2.190:9092
6. # 配置消费者
7. consumer:
8. # 指定一个默认的消费者组名
9. group-id: ConsumerGroupId1,ConsumerGroupId2
10. # earliest:当各分区下有已提交的offset时,从提交的offset开始消费;无提交的offset时,从头开始消费
11. # latest:当各分区下有已提交的offset时,从提交的offset开始消费;无提交的offset时,消费新产生的该分区下的数据
12. # none:topic各分区都存在已提交的offset时,从offset后开始消费;只要有一个分区不存在已提交的offset,则抛出异常
13. auto-offset-reset: earliest
14. # key/value的反序列化
15. key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
16. value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
17. # 是否自动消费(true=自动应答)
18. enable-auto-commit: false
19. listener:
20. ## 手动提交ack offset模式
21. ack-mode: manual
创建工厂类并交给spring管理
1. package com.cq.conf;
3. import org.springframework.context.annotation.Bean;
4. import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory;
5. import org.springframework.kafka.config.KafkaListenerContainerFactory;
6. import org.springframework.kafka.core.ConsumerFactory;
7. import org.springframework.kafka.listener.ConcurrentMessageListenerContainer;
8. import org.springframework.kafka.listener.ContainerProperties;
9. import org.springframework.stereotype.Component;
11. /**
12. * @Description 消费者offset和ack处理工厂类
13. * @Author qizhentao
14. * @Date 2020/7/16 10:55
15. * @Version 1.0
16. */
17. @Component
18. public class ConsumerListenerACK {
20. /**
21. * 每个poll(),都需要ack应答
22. */
23. @Bean(name = "simpleFactory")
24. KafkaListenerContainerFactory<ConcurrentMessageListenerContainer<String, String>> kafkaListenerContainerFactory(ConsumerFactory<String, String> consumerFactory) {
25. ConcurrentKafkaListenerContainerFactory<String, String> factory = new ConcurrentKafkaListenerContainerFactory<>();
26. factory.setConsumerFactory(consumerFactory);
27. factory.setConcurrency(3);
28. factory.getContainerProperties().setPollTimeout(3000);
29. factory.getContainerProperties().setGroupId("test");
30. //当使用手动提交时必须设置ackMode为MANUAL,否则会报错No Acknowledgment available as an argument, the listener container must have a MANUAL AckMode to populate the Acknowledgment.
31. factory.getContainerProperties().setAckMode(ContainerProperties.AckMode.MANUAL);
32. factory.getContainerProperties().setAckCount(10);
33. factory.getContainerProperties().setAckTime(10000);
34. return factory;
35. }
37. /**
38. * MANUAL 当每一批poll()的数据被消费者监听器(ListenerConsumer)处理之后, 手动调用Acknowledgment.acknowledge()后提交
39. * @param consumerFactory
40. * @return
41. */
42. @Bean("manualListenerContainerFactory")
43. public KafkaListenerContainerFactory<ConcurrentMessageListenerContainer<String, String>> manualListenerContainerFactory(
44. ConsumerFactory<String, String> consumerFactory) {
46. ConcurrentKafkaListenerContainerFactory<String, String> factory = new ConcurrentKafkaListenerContainerFactory<>();
47. factory.setConsumerFactory(consumerFactory);
48. factory.getContainerProperties().setPollTimeout(1500);
49. factory.setBatchListener(true);
50. //配置手动提交offset
51. factory.getContainerProperties().setAckMode(ContainerProperties.AckMode.MANUAL);
52. factory.getContainerProperties().setAckTime(10000);
53. return factory;
54. }
56. }
消费者手动ack应答
1. /**
2. * 消费者,ConsumerGroupId2消费组
3. */
4. @KafkaListener(topics = "cqTest", containerFactory = "simpleFactory")
5. public void ConsumerGroupId2(ConsumerRecord<?, ?> consumer, Acknowledgment ack) {
6. try{
7. // 业务逻辑
8. System.out.println("消费者B topic名称:" + consumer.topic() +
9. ", key:" + consumer.key() +
10. ", value:" + consumer.value() +
11. ", 分区位置:" + consumer.partition() +
12. ", 下标:" + consumer.offset()+
13. ", timestamp:"+consumer.timestamp());
15. // 手动确认提交offset,ack应答
16. ack.acknowledge();
17. }catch (Exception ex){
18. // 否定ack应答,服务重启时会再次消费未应答的offset消息
19. ack.nack(100);
20. // 加了消费失败日志中
21. }
22. }
重复消费问题:
https://blog.csdn.net/xianyuxiaoqiang/article/details/86477976
解决Kafka重复消费问题
原文地址 blog.csdn.net
1.问题背景
某服务(用了SpringBoot + spring-kafka处理Kafka消息时,发现每条消息处理时间长达60+秒。几百条消息处理完后,又重新从第一条开始重复消费。
2.原因分析
Kafka消费者有两个配置参数:
max.poll.interval.ms
两次poll操作允许的最大时间间隔。单位毫秒。默认值300000(5分钟)。
两次poll超过此时间间隔,Kafka服务端会进行rebalance操作,导致客户端连接失效,无法提交offset信息,从而引发重复消费。
max.poll.records
一次poll操作获取的消息数量。默认值50。
如果每条消息处理时间超过60秒,那么一批消息处理时间将超过5分钟,从而引发poll超时,最终导致重复消费。
3.对策分析
1)分析业务代码逻辑,进行性能优化,确保每条消息处理时间控制在合理范围
每条消息的处理时间不要超过5分钟。
如果超过5分钟,则考虑进行架构上的优化。
比如A线程消费消息,放到进程内部队列中,提交offset;其他线程从内部队列取消息,并处理业务逻辑。为防止内部队列消息积压,A线程需要监控队列中消息数量,
超过一定量时进入等待。
2)适当增大max.poll.interval.ms的值
SpringBoot没有提供可调节此数值的参数。如果修改此数值,需要自己封装方法创建Kafka客户端:
|
3)适当减小max.poll.records的值
可通过SpringBoot配置参数"spring.kafka.consumer.max-poll-records"调整。
原文地址 blog.csdn.net
kafka如何保证消息不丢失?—消费端的处理!!
前面博客小编向大家分享了 kafka如何保证消息不丢失?,
这篇博客呢,就跟大家一起聊一下 kafka 消费者如何消费的?如何避免重复消费?
二、消费者消费流程
消费流程:
- 从zk获取要消费的partition 的leader的位置 以及 offset位置
- 拉数据,这里拉数据是直接从broker的pagecash拉取,零拷贝 ,所以很快。
- 如果pagecash数据不全,就会从磁盘中拉取,并发送
- 消费完成后,可以手动提交offset,也可以自动提交offset。
消费策略有哪些?如何配置
一般我们消费测试是不会变的,都使用默认的,也就是第一种,range策略。
- Range 范围分配策略(默认)
默认策略,保证基本是均衡的。
计算公式 :
n = 分区数/消费者数
m = 分区数%消费者数
前m个消费者,消费n+1个,剩余的消费n个
eg:12个partition,9个消费者
12/9 = 1
12%9 = 3
前3台 消费2个partition,后6台各消费1个partition。
- RoundRobin 轮询
先根据topic和 topic的partition的hashcode进行一个排序,然后
以轮询的方式分配给各个消费者。
- stricky粘性分配策略
在没有reblence的时候和轮询策略一样
当发生rebalence的时候,尽可能的保证与上一次分配一致
比如默认是
比如consumer2 挂了,topicA p1 和topicB p2就没有消费者了,这个时候要进行消费组的rebalence。
然后按照轮询策略分配一下。
可以在配置消费配置的时候,指定消费策略:
`//Range
propsMap.put(ConsumerConfig.PARTITION_ASSIGNMENT_STRATEGY_CONFIG, org.apache.kafka.clients.consumer.RangeAssignor.class);
//RoundRobin
propsMap.put(ConsumerConfig.PARTITION_ASSIGNMENT_STRATEGY_CONFIG, org.apache.kafka.clients.consumer.RoundRobinAssignor.class);
//stricky
propsMap.put(ConsumerConfig.PARTITION_ASSIGNMENT_STRATEGY_CONFIG, org.apache.kafka.clients.consumer.StickyAssignor.class);`
什么是零拷贝?
普通把文件发送到远程服务器的方法:
_1.读磁盘内容,拷贝到内核缓冲区_
2.cpu把内核缓冲区数据拷贝到用户空间缓冲区
~3.调用write(),把用户空间缓冲区数据拷贝到内核的Socket Buffer中~
4.把sb中的数据拷贝到网卡缓冲区 NIC Buffer ,网卡在传输
从上面的流程看, 1和3 其实是多余的,用户和内核相互转换,会带来cpu上下文切换,对cpu性能有影响。
零拷贝 就是对这两次的拷贝忽略掉,应用程序可以直接把磁盘中的数据从内核中,直接传输到socket,不用互相拷贝。其中用到了Direct Memory Access
技术,可以把数据直接从内核空间传递到网卡设备,kafka中把数据直接从磁盘复制到 pagecash,给消费者读取,如图:
零拷贝其实不是没有拷贝,只是减少了不必要的拷贝次数,比如内核到用户空间的拷贝。
linux 中使用sendfile()实现零拷贝
java中nio用到零拷贝,比如filechannel.transferTo()。
mmap 文件映射机制:把磁盘文件映射到内存,用户通过修改内存,就可以修改磁盘文件。提高io效率,减少了复制开销。
三、如何避免重复消费?
分析原因:
1.生产者重复提交
2.rebalence引起重复消费
超过一定时间(max.poll.interval.ms设置的值,默认5分钟)未进行poll拉取消息,则会导致客户端主动离开队列,而引发Rebalance,提交offset失败。
其他消费者会从没有提交的位置消费,从而导致重复消费。
解决方案:
1.提高消费速度
- 增加消费者
- 多线程消费
- 异步消费
- 调整消费处理时间
2.幂等处理
-
消费者设置幂等校验
-
开启kafka幂等配置,生产者开启幂等配置,将消息生成md5,然后保存到redis中,处理新消息的时候先校验。这个尽量不要开启,消耗性能。
`props.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true);`
四、如何顺序消费?
我们知道kafka,整个topic有多个partition,每个partition内的消息是有顺序的。
五、如何延迟消费?
kafka是无状态的,没有延迟的功能。pulsar和rabbitmq实现更加方便。
开发延迟推送服务,定时检索延迟消息,发送给kafka。
六、频繁rebanlence怎么解决?
再均衡,保证所有消费者相对均衡消费。rebalence的时候,所有消费者,停止消费,直到rebanlence完成。
触发时机:
1.consumer个数变化
2.订阅topic个数变化
3.订阅的topic的partition变化
解决方案:
使用消息队列Kafka版时消费客户端频繁出现Rebalance
频繁出现rebalence,可能是消费者的消费时间过长,超过一定时间(max.poll.interval.ms设置的值,默认5分钟)未进行poll拉取消息,则会导致客户端
主动离开队列,而引发Rebalance。
1.参数调整:
session.timeout.ms:v0.10.2之前的版本可适当提高该参数值,需要大于消费一批数据的时间,但不要超过30s,建议设置为25s;而v0.10.2及其之后的
版本,保持默认值10s即可。
max.poll.records:降低该参数值,建议远远小于<单个线程每秒消费的条数> * <消费线程的个数> * <max.poll.interval.ms>的积。
max.poll.interval.ms: 该值要大于<max.poll.records> / (<单个线程每秒消费的条数> * <消费线程的个数>)的值。
2.尽量提高客户端的消费速度,消费逻辑另起线程进行处理。
3.减少Group订阅Topic的数量,一个Group订阅的Topic最好不要超过5个,建议一个Group只订阅一个Topic。
附:批量消费代码
`import com.ctrip.framework.apollo.ConfigService;
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.common.serialization.StringDeserializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.annotation.EnableKafka;
import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory;
import org.springframework.kafka.config.KafkaListenerContainerFactory;
import org.springframework.kafka.core.DefaultKafkaConsumerFactory;
import org.springframework.kafka.listener.ConcurrentMessageListenerContainer;
import java.util.HashMap;
import java.util.Map;
@Configuration
@EnableKafka
public class BehaviorConsumerConfig {
public Map<String, Object> consumerConfigs() {
Map<String, Object> propsMap = new HashMap<>();
propsMap.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, servers);
propsMap.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, enableAutoCommit);
propsMap.put(ConsumerConfig.GROUP_ID_CONFIG, groupId);
propsMap.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, autoCommitInterval);
propsMap.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, autoOffsetReset);
propsMap.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
propsMap.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
propsMap.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, maxPollRecordsConfig);
propsMap.put(ConsumerConfig.PARTITION_ASSIGNMENT_STRATEGY_CONFIG, org.apache.kafka.clients.consumer.StickyAssignor.class);
propsMap.put("security.protocol", protocol);
propsMap.put("ssl.truststore.location", truststoreLocation.replaceAll("file://", ""));
propsMap.put("ssl.truststore.password", truststorePassword);
propsMap.put("login.config.location", loginConfigLocation);
propsMap.put("sasl.mechanism", mechanism);
return propsMap;
}
@Bean("batchContainerFactory")
KafkaListenerContainerFactory<ConcurrentMessageListenerContainer<String, String>> kafkaListenerContainerFactory() {
ConcurrentKafkaListenerContainerFactory<String, String> factory = new ConcurrentKafkaListenerContainerFactory<>();
factory.setConsumerFactory(new DefaultKafkaConsumerFactory<>(consumerConfigs()));
// 并发创建的消费者数量
factory.setConcurrency(4);
factory.getContainerProperties().setPollTimeout(3000);
//设置为批量消费,每个批次数量在Kafka配置参数中设置ConsumerConfig.MAX_POLL_RECORDS_CONFIG
factory.setBatchListener(true);
return factory;
}
}` ![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)
七、小结
本篇我们基本上把消费者的消费梳理干净了,以及消费会遇到的 重复消费,顺序消费,延迟消费等问题都也解释了给出了解决方案。方案一通百通。
如何解决消息积压
如何解决消息队列的延时以及过期失效问题?消息队列满了以后该怎么处理?有几百分消息持续积压几个小时,说说怎么解决。
这些问法,本质上都是针对场景,都是说可能你的消息端出来问题,不消费了。或者消费的及其满。接着就坑爹了。可能你的消息队列集群的磁盘都快满了。都没人
消费,这个时候怎么办?或者是积压了几个小时,怎么办?或者是积压时间太长了,导致比如RabbitMQ设置了过期时间后就没了。其实这事,线上挺常见的,一般
不出,一出就是大case。举个例子,消费端每次消费之后要写mysql,结果mysql挂了,消费端不动了,或者是消费端出了什么叉子,导致消费速度灰常慢。
1、快速处理积压的消息
(1)大量消息在MQ里积压几个小时,还没解决
几千万条数据在MQ里,积压了七八个小时。这个时候就是恢复consumer的问题。让它恢复消费速度,然后傻傻地等待几个小时消费完毕。这个肯定不能再面试的时候说。
1个消费者1秒时1000条,1秒3个消费者是3000条。1分钟是18万条。1个小时是1000多万条。如果积压了上万条数据,即使消费者恢复了,也大概需要1个多小时
才能恢复过来。
原来3个消费者1个小时。现在30个消费者,需要10分钟搞定。
一般情况下,这个时候只能做临时扩容了。具体操作步骤和思路如下:
① 先修改consumer的问题,确保其恢复消费速度,然后将现有consumer都停掉。
② 新建1个topic,partition是原来的10倍,临时建立好原来10倍或者20倍的Queue。
③ 然后写一个临时的分发数据的consumer程序,这个程序部署上去,消费积压的数据。消费之后,不做耗时的处理。直接均匀轮训写入临时建立好的10倍数量的Queue。
④ 接着征用10倍的机器来部署consume。每一批consumer消费1个临时的queue。
⑤ 这种做法,相当于将queue资源和consume资源扩大10倍,以10倍的速度来消费数据。
⑥ 等快速消费完积压数据之后,恢复原来的部署架构,重新用原先的consumer来消费消息。
(2)过期失效了怎么办
过期失效就是TTL。如果消息在Queue中积压超过一定的时间就会被RabbitMQ给清理掉。这个数据就没了。这就不是数据积压MQ中了,而是大量的数据会直接搞丢。
在这种情况下,增加consume消费积压就不起作用了。此时,只能将丢失的那批数据,写个临时的程序,一点一点查出来,然后再灌入MQ中,把白天丢失的数据补回来。
kafka如何处理大量积压消息
原文地址 blog.csdn.net
1、consumer导致kafka积压了大量消息
方法:
1 增大partion数量,
2 消费者加了并发,服务, 扩大消费线程
3 增加消费组服务数量
4 kafka单机升级成了集群
5 避免消费者消费消息时间过长,导致超时
6 使Kafka分区之间的数据均匀分布
场景:
1 如果是Kafka消费能力不足,则可以考虑增加 topic 的 partition 的个数,同时提升消费者组的消费者数量,消费数 = 分区数 (二者缺一不可)
2 若是下游数据处理不及时,则提高每批次拉取的数量。批次拉取数量过少(拉取数据/处理时间 < 生产速度),使处理的数据小于生产的数据,也会造成数据积压。
2、消息过期失效
产生消息堆积,消费不及时,kafka数据有过期时间,一些数据就丢失了,主要是消费不及时
经验
1、消费kafka消息时,应该尽量减少每次消费时间,可通过减少调用三方接口、读库等操作, 从而减少消息堆积的可能性。
2、如果消息来不及消费,可以先存在数据库中,然后逐条消费(还可以保存消费记录,方便定位问题)
3、每次接受kafka消息时,先打印出日志,包括消息产生的时间戳。
4、kafka消息保留时间(修改kafka配置文件, 默认一周)
5、任务启动从上次提交offset处开始消费处理
3 综上使用kafka注意事项
1.由于Kafka消息key设置,在Kafka producer处,给key加随机后缀,使其均衡
2.数据量很大,合理的增加Kafka分区数是关键。
Kafka分区数是Kafka并行度调优的最小单元,如果Kafka分区数设置的太少,会影响Kafka consumer消费的吞吐量. 如果利用的是Spark流和Kafka
direct approach方式,也可以对KafkaRDD进行repartition重分区,增加并行度处理.
设计消息队列中间件思路
如何让你来设计消息队列中间件,如何设计?
主要考察两块。①是有没有对某个消息队列做过较为深入的原理的了解。或者从整体把握一个mq的架构原理。②是看看你的设计能力,给你一个常见的系统,就是
消息队列系统,看能够从全局把握一下整体架构设计的关键点。
比如说,这个消息队列,我们从以下几个方面来了解下:
1、首先MQ得支持可伸缩性吧。就是需要的时候增加吞吐量和容量?
2、其次,需要考虑一下MQ的数据是不是要持久化到磁盘
3、再次,考虑一下MQ的可用性。
4、最后,考虑一下能不能支持数据零丢失