kafka

1.kafka概述

1.1定义

  • Kafka 是一个分布式的基于发布/订阅模式消息队列Message Queue),主要应用于大数据实时处理领域。

1.2 消息队列

1.2.1 传统消息队列的应用场景

  • MQ传统应用场景之异步处理

使用消息队列的好处 

  • 解耦:允许你独立的扩展或修改两边的处理过程,只要确保它们遵守同样的接口约束。
  • 可恢复性:系统的一部分组件失效时,不会影响到整个系统。消息队列降低了进程间的耦合度,所以即使一个处理消息的进程挂掉,加入队列中的消息仍然可以在系统恢复后被处理。
  • 缓冲:有助于控制和优化数据流经过系统的速度,解决生产消息和消费消息的处理速度不一致
    的情况。
  • 异步通信:很多时候,用户不想也不需要立即处理消息。消息队列提供了异步处理机制,允许用户把一个消息放入队列,但并不立即处理它。想向队列中放入多少消息就放多少,然后在需要的时候再去处理它们。
  • 灵活性(动态的增减机器 )& 峰值处理能力:在访问量剧增的情况下,应用仍然需要继续发挥作用,但是这样的突发流量并不常见。如果为以能处理这类峰值访问为标准来投入资源随时待命无疑是巨大的浪费。使用消息队列能够使关键组件顶住突发的访问压力,而不会因为突发的超负荷的请求而完全崩溃。

1.2.2消息队列的两种模式

  • 点对点模式一对一,消费者主动拉取数据,消息收到后消息清除)消息生产者生产消息发送到Queue中,然后消息消费者从Queue中取出并且消费消息.消息被消费以后,queue 中不再有存储,所以消息消费者不可能消费到已经被消费的消息。

  • 发布 / 订阅模式 一对多 ,消费者消费数据之后不会清除消息,但是消息仍然会有 过期时间 )消息生产者(发布)将消息发布到 topic 中,同时有多个消息消费者(订阅)消费该消息。和点对点方式不同,发布到 topic (Kafka将消息种子(Feed)分门别类,每一类的消息称之为一个主题(Topic) )的消息会被所有订阅者消费。 发布订阅模式中获取数据的方式仍然有两种:
    • 消费者主动拉取数据:kafka采用这种方式拉取数据,需要消费者不断地长轮询topic是否有新消息(若队列中一直没有新消息,仍然需要不断轮询,造成资源浪费)
    • 队列推送:推送的速度是固定的,若某个消费者处理速度不合适,会造成资源浪费.

kafka基础架构

名称解释
Broker
一台 kafka 服务器就是一个 broker 。一个集群由多个 broker 组成。一个 broker 可以容纳多个 topic
Topic
可以理解为一个队列, 生产者和消费者面向的都是一个 topic
Producer
消息生产者,就是向 kafka broker 发消息的客户端
Consumer
消息消费者,向 kafka broker 取消息的客户端
ConsumerGroup
消费者组,由多个 consumer 组成。 消费者组内每个消费者负 责消费不同分区的数据,一个分区只能由一个组内消费者消费;消费者组之间互不影响。 所 有的消费者都属于某个消费者组,即 消费者组是逻辑上的一个订阅者
Partition
为了实现扩展性,一个非常大的 topic 可以分布到多个 broker (即服务器)上, 一个 topic 可以分为多个 partition ,每个 partition 是一个有序的队列
目的是为了:提高负载均衡的能力,提高读写的并发度
Replica
副本,为保证集群中的某个节点发生故障时, 该节点上的 partition 数据不丢失, 且 kafka 仍然能够继续工作, kafka 提供了副本机制,一个 topic 的每个分区都有若干个副本,
一个 leader 和若干个 follower
leader
每个分区多个副本的“主”,生产者发送数据的对象,以及消费者消费数据的对象都是 leader
follower
每个分区多个副本中的“从”,实时从 leader 中同步数据,保持和 leader 数据的同步。leader 发生故障时,某个 follower 会成为新的 follower
  • Consumer:消费的详细信息要保存在zookeeper中,当消费者挂掉后,保证可以继续读取未消费的信息(10条消息,消费到第五条,该消费者挂了,重启后应该从第六条进行消费,就需要把消费者消费的详细信息保存在zookeeper中,内存中也会有一份,正常的时候用内存中的这个,挂掉之后,从zookeeper中取出来,继续维护这份详细信息),详细信息也就是offset
  • ConsumerGroup:提高消费能力并发度最好的时候:消费者组里面的消费者个数和某个主题的分区数相同的时候

Kafka 快速入门

  • kafka依赖zookeeper所以必须要先安装zookeeper

安装部署

  • jar 包下载:http://kafka.apache.org/downloads.html
     

  • 集群部署 
1.解压安装包
[atguigu@hadoop102 software]$ tar -zxvf kafka_2.11-0.11.0.0.tgz -C
/opt/module/
2.修改解压后的文件名称
[atguigu@hadoop102 module]$ mv kafka_2.11-0.11.0.0/ kafka
3.在/opt/module/kafka 目录下创建 logs 文件夹
[atguigu@hadoop102 kafka]$ mkdir logs
4.修改配置文件
[atguigu@hadoop102 kafka]$ cd config/
[atguigu@hadoop102 config]$ vi server.properties
输入以下内容:
#broker 的全局唯一编号,不能重复
broker.id=0
# 删除 topic 功能使能
delete.topic.enable=true
# 处理网络请求的线程数量
num.network.threads=3
# 用来处理磁盘 IO 的现成数量
num.io.threads=8
# 发送套接字的缓冲区大小
socket.send.buffer.bytes=102400
# 接收套接字的缓冲区大小
socket.receive.buffer.bytes=102400
# 请求套接字的缓冲区大小
socket.request.max.bytes=104857600
#kafka 运行日志存放的路径
log.dirs=/opt/module/kafka/logs
#topic 在当前 broker 上的分区个数
num.partitions=1
# 用来恢复和清理 data 下数据的线程数量
num.recovery.threads.per.data.dir=1
#segment 文件保留的最长时间,超时将被删除
log.retention.hours=168
# 配置连接 Zookeeper 集群地址
zookeeper.connect=hadoop102:2181,hadoop103:2181,hadoop104:2181
5.配置环境变量
[atguigu@hadoop102 module]$ sudo vi /etc/profile
#KAFKA_HOME
export KAFKA_HOME=/opt/module/kafka
export PATH=$PATH:$KAFKA_HOME/bin
[atguigu@hadoop102 module]$ source /etc/profile
//
6.分发安装包
[atguigu@hadoop102 module]$ xsync kafka/
//
7. 分别在 hadoop103 hadoop104 上修改配置文件 /opt/module/kafka/config/server.properties
中的 broker.id=1 broker.id=2
注: broker.id 不得重复
8.启动集群(-daemon:以后台形式启动)
依次在 hadoop102 hadoop103 hadoop104 节点上启动 kafka
[atguigu@hadoop102 kafka]$ bin/kafka-server-start.sh  -daemon
config/server.properties
[atguigu@hadoop103 kafka]$ bin/kafka-server-start.sh  -daemon
config/server.properties
[atguigu@hadoop104 kafka]$ bin/kafka-server-start.sh  -daemon
config/server.properties
9.关闭集群
[atguigu@hadoop102 kafka]$ bin/kafka-server-stop.sh stop
[atguigu@hadoop103 kafka]$ bin/kafka-server-stop.sh stop
[atguigu@hadoop104 kafka]$ bin/kafka-server-stop.sh stop
//
10. kafka 群起脚本
for i in hadoop102 hadoop103 hadoop104
do
echo "========== $i =========="
ssh $i   '/opt/module/kafka/bin/kafka-server-start.sh   -daemon /opt/module/kafka/config/server.properties'
done

Kafka 命令行操作

  • 查看当前服务器中的所有 topic
[atguigu@hadoop102 kafka]$ bin/kafka-topics.sh --zookeeper hadoop102:2181 --list
  • 创建 topic
    • 选项说明(创建topic为first且有1个分区,该分区有三个副本):
      --topic 定义 topic
      --replication-factor 定义副本数
      --partitions 定义分区数
      --zookeeper  hadoop102:2181:创建主题需要跟我们的zookeeper做交互,主题类的元信息放在zookeeper上,主题中的数据放在broker中
      --replication-factor 定义副本数
      --partitions 定义分区数
[atguigu@hadoop102 kafka]$ bin/kafka-topics.sh --zookeeper
hadoop102:2181 --create --replication-factor 3 --partitions 1 --topic first
  • 删除 topic
    • 需要 server.properties 中设置 delete.topic.enable=true 否则只是标记删除。
[atguigu@hadoop102 kafka]$ bin/kafka-topics.sh --zookeeper hadoop102:2181
--delete --topic first
  • 发送消息
    • --broker-list hadoop102:9092:指定往哪台服务器写数据
[atguigu@hadoop102 kafka]$ bin/kafka-console-producer.sh --broker-list hadoop102:9092
--topic first
>hello world
>atguigu atguigu
  • 消费消息
    • 默认是消费最新的数据(消费者启动这一刻发送的消息)
    • --from-beginning:会把主题中以往所有的数据都读取出来
[atguigu@hadoop102 kafka]$ bin/kafka-console-consumer.sh \
--zookeeper hadoop102:2181 --topic first
[atguigu@hadoop102 kafka]$ bin/kafka-console-consumer.sh \
--bootstrap-server hadoop102:9092 --topic first
[atguigu@hadoop102 kafka]$ bin/kafka-console-consumer.sh \
--bootstrap-server hadoop102:9092 --from-beginning --topic first
//消费多个主题
[atguigu@hadoop102 kafka]$ bin/kafka-console-consumer.sh \
--bootstrap-server hadoop102:9092 --whitelist "test|test-2"
  • 查看某个 Topic 的详情
    • 第一行是所有分区的概要信息,之后的每一行表示每一个partition的信息。
    • leader:负责给定partition的所有读写请求.
    • replicas:表示某个partition在哪几个broker上存在备份。不管这个几点是不是"leader",甚至这个节点挂了,也会列出。
    • isr是replicas的一个子集, 它只列出当前还存活着的,并且已同步备份了该partition的节点。
    • 分区0的leader文件在0机器上,其他副本在1,2机器上
[atguigu@hadoop102 kafka]$ bin/kafka-topics.sh --zookeeper hadoop102:2181 --describe
--topic first

  • 修改分区数
[atguigu@hadoop102 kafka]$ bin/kafka-topics.sh --zookeeper hadoop102:2181 --alter --topic first --partitions 6
  • 单播消费:一条消息只能被某一个消费者消费的模式,类似queue模式,只需让所有消费者在同一个消费组里即可(一个topic一个分区)

分别在两个客户端执行如下消费命令(即两个消费者属于一个组),然后往主题里发送消息,结果只有一个客户端能收到消息
bin/kafka-console-consumer.sh -- bootstrap- server 192.168.65.60:9092

--consumer-property group.id=testGroup --topic test

  • 多播消费:一条消息能被多个消费者消费的模式,类似publish-subscribe模式费,针对Kafka同一条消息只能被同一个消费组下的某一个消费者消费的特性, 要实现多播只要保证这些消费者属于不同的消费组即可。我们再增加一个消费者,该消费者属于testGroup-2消费组,结果两个客户端都能收到消息

bin/kafka- console - consumer . sh -- bootstrap- server 192. 168.65.609092 - - consumer. property group. id-testGroup-2 - -topic test

  • 查看消费组名

bin/kafka-consumer.groups.sh --bootstrap-server 192.168.65.60:9092 --list

  • 查看消费组的消费偏移量(消费者记录偏移量是以消费者组为单位的,不是以某一个消费者为单位的)

bin/kafka-consumer-groups.sh --bootstrap-server 192.168.65.60:9092 --describe --group testGroup

        ---current-offset:当前消费组的已消费偏移量
        ---log-end-offset:主题对应分区消息的结束偏移量(HW)
        ---lag:当前消费组未消费的消息数

3.kafka架构深入

3.1Kafka 工作流程及文件存储机制

  • Kafka 中消息是以 topic 进行分类的,生产者生产消息,消费者消费消息,都是面向 topic 的。
  • topic 是逻辑上的概念,而partition 是物理上的概念,Partition是一个有序的message序列,每个 partition 对应于一个log文件,message按顺序添加到该文件中该 log 文件中存储的就是 producer 生产的数据。Producer 生产的数据会被不断追加到该 log 文件末端,且每条数据都有自己的 offset。消费者组中的每个消费者,都会实时记录自己消费到了哪个主题的哪个分区的offset,以便出错恢复时,从上次的位置继续消费。注意:一个消费者组只能有一个消费者消费同一个主题的同一个分区
  • 每个parition中的消息都有一个唯一的编号, 称之为offset,用来唯一标示某个分区中的message.一个parition中的message的offset都是唯一 的,但是不同的patition中的message的offset可能是相同的(如下所示:都存在offset为0-9的message)。
  • kafka一般不会删除消息,不管这些消息有没有被消费。只会根据配的日志保留时间log.retention.hours)确认消息多久被删除,默认保留最近一周的日志消息。 kafka的性能与保留的消息数据量大小没有关系,因此保存大量的数据消息日志信息不会有什么影响。每个consumer是基于自己在commit log中的消费进度(offset)来进行工作的。在kafka中, 消费offset由consumer自己来维护 (也可以说是有消费者组记录:因为个消费者组只能有一个消费者消费同一个主题的同一个分区);
  • 我们按照顺序逐条消费commit log中的消息,当然我可以通过指定ffset来重复消费某些消息,或者跳过某些消息。这意味kafka中的consumer对集群的影响是非常小的,添加一个或者减少-个consumer, 对于集群或者其他consumer来说,都是没有影响的,因为每个consumer维护各自的消费ffset

  • topic与partition的关系

  •  由于生产者生产的消息会不断追加到 log 文件末尾,为防止 log 文件过大导致数据定位
    效率低下, Kafka 采取了 分片 索引 机制,将每个 partition 分为多个 segment 。每个 segment 对应两个文件——“.index” 文件和 “.log” 文件。这些文件位于一个文件夹下,该文件夹的命名规则为:topic 名称 + 分区序号。例如, first 这个 topic 有三个分区,则其对应的文件夹为 first- 0,first-1,first-2。

部分信息的offset索引文件,kafka每次往分区发4k(可配置)消息就会记录一条当前消息的offset到index文件;如果要定位消息的offset会现在这个文件里快速定位,再去log文件里找具体消息

1个log文件大小为1G是为了方便把log文件加载到内存中去操作        

00000000000000000000.index

消息存储文件,主要保存offset和消息体

00000000000000000000.log
消息的发送时间索引文件,kafka每次往分区发4k(可配置)消息就会记录一条当前消息的发送时间戳与对应的offset到timeindex文件,如果需要按照时间来定位offset,会先从这个文件里查找
00000000000000000000.timeindex
00000000000000170410.index
00000000000000170410.log
00000000000000170410.timeindex
00000000000000239430.index
00000000000000239430.log
00000000000000239430.timeindex
kafkaBroker有一个参数, log.segment.bytes,限定了每个日志段文件的大小,最大是1GB
  • index 和 log 文件以当前 segment 的第一条消息的 offset 命名。下图为 index 文件和 log 文件的结构示意图。

  • .index”文件存储大量的索引信息,“.log”文件存储大量的数据,索引文件中的元数据指向对应数据文件中 message 的物理偏移地址

3.2 Kafka生产者

3.2.1 分区策略

  • 分区的原因
    • 方便在集群中扩展,每个 Partition 可以通过调整以适应它所在的机器,而一个 topic又可以有多个 Partition 组成,因此整个集群就可以适应任意大小的数据了;
    • 可以提高并发,因为可以以 Partition 为单位读写了。
  • 分区的原则
    • 我们需要将 producer 发送的数据封装成一个 ProducerRecord 对象
      • 指明 partition 的情况下,直接将指明的值直接作为 partiton 值
      • 没有指明 partition 值但有 key 的情况下,将 key 的 hash 值与 topic 的 partition
        数进行取余得到 partition 值;
      • 既没有 partition 值又没有 key 值的情况下,第一次调用时随机生成一个整数(后
        面每次调用在这个整数上自增),将这个值与 topic 可用的 partition 总数取余得到 partition 值,也就是常说的 round-robin 算法。

 3.2.2 数据可靠性保证

        为保证 producer 发送的数据,能可靠的发送到指定的 topictopic 的每个 partition 收到producer 发送的数据后,都需要向 producer 发送 ackacknowledgement 确认收到),如果producer收到 ack,就会进行下一轮的发送,否则重新发送数据

  •  副本数据同步策略
    • Kafka 选择了第二种方案,原因如下:
      • 同样为了容忍 n 台节点的故障,第一种方案需要 2n+1 个副本,而第二种方案只需要 n+1 个副本,而 Kafka 的每个分区都有大量的数据,第一种方案会造成大量数据的冗余。
      • 虽然第二种方案的网络延迟会比较高,但网络延迟对 Kafka 的影响较小。
方案
优点
缺点
半数以上完成同步,就发
ack
延迟低
选举新的 leader 时,容忍 n
节点的故障,需要 2n+1 个副
全部完成同步,才发送
ack
选举新的 leader 时,容忍 n
节点的故障,需要 n+1 个副
延迟高
  • ISR
    • 采用第二种方案之后,设想以下情景:leader 收到数据,所有 follower 都开始同步数据, 但有一个 follower,因为某种故障,迟迟不能与 leader 进行同步,那 leader 就要一直等下去,直到它完成同步,才能发送 ack。这个问题怎么解决呢?
    • Leader 维护了一个动态的 in-sync replica set (ISR),意为和 leader 保持同步的 follower 合。当 ISR 中的 follower 完成数据的同步之后,leader 就会给 follower 发送 ack。如果 follower长时间未向 leader 同 步 数 据 , 则 该 follower 将 被 踢 出 ISR , 该 时 间 阈 值 由replica.lag.time.max.ms 参数设定。Leader 发生故障之后,就会从 ISR 中选举新的 leader
  • ack 应答机制
    • 对于某些不太重要的数据,对数据的可靠性要求不是很高,能够容忍数据的少量丢失,所以没必要等 ISR 中的 follower 全部接收成功.所以 Kafka 为用户提供了三种可靠性级别,用户根据对可靠性和延迟的要求进行权衡,选择以下的配置。
      • acks 参数配置 acks
        • 0producer 不等待 broker ack,这一操作提供了一个最低的延迟,broker 一接收到还 ​​​​​​没有写入磁盘就已经返回,当 broker 故障时有可能丢失数据
        • 1producer 等待 broker ackpartition 的 leader 落盘成功后返回 ack,如果在follower同步成功之前 leader 故障,那么将会丢失数据
        • -1 all ): producer 等待 broker ack partition leader 和 follower ( ISR中的follower )全部落盘成功后才返回 ack。但是如果在 follower 同步完成后, broker 发送 ack 之前, leader 发生故障,那么会造成 数据重复
          • min.insync.replicas(默认为1,推荐配置为2),这个参数配置的副本个数都成功写入日志,这种策略会保证只要有一个备份存活就不会丢失数据,这是最强的数据保证.一般除非是金融级别或者跟钱打交道的场景才会使用这种配置
          • 在极限情况下有可能也会丢失数据;当ISR中只有一个leader时(退化到了acks=1的情况)

                acks = 1 数据丢失案例

                 acks = -1 数据重复案例

                          原先的leader和follow都已经完成同步,但此时leader挂掉了,挑选出了leader

                                但是producer未接收到ack,此时producer会重发数据,导致数据重复

  • 故障细节处理:保障的是存储一致性和消费一致性
    • Log 文件中的 HW LEO
      • LEO:指的是每个副本最大的 offset;
      • HW:指的是消费者能见到的最大的 offset,ISR 队列中最小的 LEO。对于leader新写入的消息,consumer并不能立即消费,leader会等到该消息被所有ISR中的副本同步后更新HW,此消息才能被consumer消费.这样就保证了leader所在的broker失效,该消息仍然可以从新选举中的leader中获取
    • follower 故障
      • follower 发生故障后会被临时踢出 ISR,待该 follower 恢复后,follower 会读取本地磁盘记录的上次的 HW,并将 log 文件高于 HW 的部分截取掉,从 HW 开始向 leader 进行同步。等该 follower LEO 大于等于该 Partition HW,即 follower 追上 leader 之后,就可以重新加入 ISR 了。
    • leader 故障
      • leader 发生故障之后,会从 ISR 中选出一个新的 leader,之后,为保证多个副本之间的
        数据一致性,其余的 follower 会先将各自的 log 文件 高于 HW 的部分截掉 ,然后从新的 leader同步数据。
        注意:这只能保证副本之间的数据一致性,并不能保证数据不丢失或者不重复。

  • 下图详细的说明了当producer生产消息至broker后, ISR以及HW和LEO的流转过程: 

  • 由此可见,Kafka的复制机制既不是完全的同步复制,也不是单纯的异步复制。事实上,同步复制要求所有能工作的follower都复制完,这条消息才会被commit,这种复制方式极大的影响了吞吐率。而异步复制方式下,follower异步的从leader复制数据, 数据只要被leader写 入log就被认为已经commit,这种情况下如果follower都还没有复制完,落后于leader时,突然leader宕机, 则会丢失数据。而Kafka的这种使用ISR的方式则很好的均衡了确保数据不丢失以及吞吐率。再回顾下消息发送端对发出消息持久化机制参数acks的设置,我们结合HW和LEO来看下acks=1的情况 

 

3.2.3 Exactly Once 语义 

  • 将服务器的 ACK 级别设置为-1,可以保证 Producer Server 之间不会丢失数据,即 At Least Once 语义。相对的,将服务器 ACK 级别设置为 0,可以保证生产者每条消息只会被发送一次,即 At Most Once 语义。
  • At Least Once 可以保证数据不丢失,但是不能保证数据不重复;相对的,At Least Once 可以保证数据不重复,但是不能保证数据不丢失。但是,对于一些非常重要的信息,比如说交易数据,下游数据消费者要求数据既不重复也不丢失,即 Exactly Once 语义。
  • 0.11 本以前的 Kafka,对此是无能为力的,只能保证数据不丢失,再在下游消费者对数据做全局去重。对于多个下游应用的情况,每个都需要单独做全局去重,这就对性能造成了很大影响。
  • 0.11 版本的 Kafka,引入了一项重大特性:幂等性。所谓的幂等性就是指 Producer 不论Server 发送多少次重复数据,Server 端都只会持久化一条。幂等性结合 At Least Once 义,就构成了 Kafka Exactly Once 语义。即:At Least Once + 幂等性 = Exactly Once
  • 要启用幂等性,只需要将 Producer 的参数中 enable.idompotence 设置为 true 即可。Kafka 的幂等性实现其实就是将原来下游需要做的去重放在了数据上游。开启幂等性的 Producer 初始化的时候会被分配一个 PID,发往同一 Partition 的消息会附带 Sequence Number。而 Broker 端会对<PID, Partition, SeqNumber>做缓存,当具有相同主键的消息提交时,Broker 会持久化一条.但是 PID 重启就会变化,同时不同的 Partition 也具有不同主键,所以幂等性无法保证跨分区跨会话的 Exactly Once

3.3 Kafka消费者

3.3.1 消费方式

  • consumer 采用 pull(拉)模式从 broker 中读取数据:pull 模式不足之处是,如果 kafka 没有数据,消费者可能会陷入循环中,一直返回空数据。针对这一点,Kafka 的消费者在消费数据时会传入一个时长参数 timeout,如果当前没有数据可供消费,consumer 会等待一段时间之后再返回,这段时长即为 timeout
  • push(推)模式很难适应消费速率不同的消费者,因为消息发送速率是由 broker 决定的:它的目标是尽可能以最快速度传递消息,但是这样很容易造成 consumer 来不及处理消息,典型的表现就是拒绝服务以及网络拥塞。而 pull 模式则可以根据 consumer 的消费能力以适当的速率消费消息。

3.3.2 消费者Rebalance机制

  • rebalance就是说如果消费组里的消费者数量有变化消费的分区数有变化, kafka会重新分配消费者消费分区的关系。比如consumer group中某个消费者挂了,此时会自动把分配给他的分区交给其他的消费者,如果他又重启了,那么又会把一些分区重新交还给他。
  • 注意: rebalance只针对subscribe这种不指定分区消费的情况,如果通过assign这种消费方式指定了分区,kafka不会进行rebanlance.
  • 如下情况可能会触发消费者rebatance
    • 消费组里的consumer增加或减少了
    • 动态给topic增加了分区
    • 消费组订阅了更多的topic
  • rebalance过程中,消费者无法从kafka消费消息,这对kafka的TPS会有影响,如果kafka集群内节点较多,比如数百个,那重平衡可能会耗时极多,所以应尽量避免在系统高峰期的重平衡发生。 

3.3.2 分区分配策略

        当消费者组里面的消费者个数发生变化时,会触发这个策略,一个consumer group 中有多个 consumer,一个 topic 有多个 partition,所以必然会涉及partition 的分配问题,即确定那个 partition 由哪个 consumer 来消费。

        主要有三种rebalance的策略: range,round-robin,sticky。Kafka提供了消费者客户端参数partition.assignment.strategy来设置消费者与订阅主题之间的分区分配策略。默认情况为range分配策略。
      假设一个主题有10个分区(0-9),现在有三个consumer消费:

  • sticky策略初始时分配策略与round-robin类似,但是在rebalance的时候, 需要保证如”下两个原则。
    • 分区的分配要尽可能均匀。
    • 分区的分配尽可能与上次分配的保持相同。
      当两者发生冲突时,第一一个目标优先于第二个目标。这样可以最大程度维持原来的分区分配的策略。
    • 比如对于第一种range情况的分配, 如果第三个consumer挂了,那么重新用sticky策略分配的结果如下: .
      consumer1除了原有的0~3,会再分配-个7
      consumer2除了原有的4~6,会再分配8和9
  • RoundRobin
    • 会把当前消费者组订阅的所有主题当成一个整体,先进行排序,作为一个整体进行轮询
    • 前提:要保证消费者组里面订阅的主题都一样
    • TopicAndPartition:根据每个主题中的分区的hash值进行排序形成一个整体
    • round-robin策略就是轮询分配,比如分区0、3、6. 9给一个consumer, 分区1、4、7给一个consumer, 分区2、5. 8给一个consumer
  • Range
    • 按照单个主题来分的(单个主题是一个整体),谁订阅了这个主题,对订阅了这个主题的消费者进行划分

    • 问题:消费者消费的数据不对等

    • 默认的是Range

    • range策略就是按照分区序号排序,假设n=分区数/消费者数量=3,m=分区数%消费者数量= 1,那么前m个消费者每个分配n+1个分区,后面的(消费者数量- m)个消费者每个分配n个分区。比如分区0~3给一个consumer, 分区4~6给一个consumer, 分区7~9给一个consumer.

3.3.3 offset 的维护

  • 消费者组+分区+主题确定offset
  • 由于 consumer 在消费过程中可能会出现断电宕机等故障,consumer 恢复后,需要从故障前的位置的继续消费,所以 consumer 需要实时记录自己消费到了哪个 offset,以便故障恢复后继续消费。

  • 每个consumer会定期将自己消费分区的offset提交给kafka内部topi: _consumer_offsets , 提交过去的时候,key是consumerGroupld+topic+分区号,value就是当前offset的值,kafka会定期清理topic里的消息, 最后就保留最新的那条数据.因为_consumer_ offsets可能会接收高并发的请求,kafka默认给其分配50个分区(可以通过offsets.topic.num.partitions设置),这样可以通过加机器的方式抗大并发。通过如下公式可以选出consumer消费的offset要提交到_consumer_offsets的哪个分区
    • 公式: hash(consumerGroupld) %_consumer_offsets主题的分区数 

3.4 Kafka高效读写数据

  • 顺序写磁盘
    • Kafka producer 生产数据,要写入到 log 文件中,写的过程是一直追加到文件末端, 为顺序写。官网有数据表明,同样的磁盘,顺序写能到 600M/s,而随机写只有 100K/s。这与磁盘的机械机构有关,顺序写之所以快,是因为其省去了大量磁头寻址的时间。
  • 零复制技术
    • ​​​​​​​​​​​​​​ 

3.5 Zookeeper Kafka 中的作用

  • Kafka 集群中有一个 broker 会被选举为 Controller,负责管理集群 broker 的上下线,所topic 分区副本分配leader 选举等工作。
  • Controller 的管理工作都是依赖于 Zookeeper 的。

3.5.1Kafka核心总控制器Controller

  • 在Kafka集群中会有一个或者多 个broker,其中有一个broker会 被选举为控制器(Kafka Cotollr),它负责管理整个集群中所有分区和副本的状态。当某个分区的leader副本出现故障时,由控制器负责为该分区选举新的leader副本。当检测到某个分区的ISR集合发生变化时,由控制器负责通知所有broker更新其元数据信息。当使用kafka-topics.sh脚本为某个topic增加分区数量时,同样还是由控制器负责让新分区被其他节点感知到。 
  • Controller选举机制
    • 在kafka集群启动的时候,会自动选举-台broker作为contoller来管理整个集群, 选举的过程是集群中每个broker都会尝试在zookeeper上创建一个/controllr临时节点,zookeeper 会保证有且仅有一个broker能创建成功,这个broker就会成为集群的总控器controller.
    • 当这个contoller角色的broker宕机了,此时zookeeperI临时节点会消失, 集群里其他broker会-直监听这个临时节点, 发现临时节点消失了,就竞争再次创建临时节点,就是我们上面说的选举机制,zookeeper又会保证有一个broker成为新的ontoller。
    • 具备控制器身份的broker需要比其他普通的broker多一份职责, 具体细节如下:
      • 1.监听broker相关的变化。为Zookeeper中的/brokers/ids/节点添加BrokerChangeListener,用来处理broker增减的变化。
        • 每个borker启动的时候也会往/brokers/ids/目录下注册临时节点
      • 2.监听topic相关的变化。为Zookeeper中的/brokers/topics节 点添加TopicChangeListener,用来处理topic增减的变化; 为zookeeper中的/admin/delete. topics节 点添加TopicDeletionListener,用来处理删除topic的动作。
      • 3.从Zookeeper中读取获取当前所有与topic,partition以及broker有关的信息并进行相应的管理。对于所有topic所对应的Zookeeper中的/brokers/topics/[topic]节点添加PartitionModificationsLitener,用来监听topic中的分区分配变化。
      • 4.更新集群的元数据信息,同步到其他普通的broker节点中。 
  • Partition副本选举Leader机制
    • cotrollr感知到分区leader所在的broker挂了(contoller监听了很多zk节点可以感知到broker存活), controller会从ISR列表(参数unclean.leader.election.enable=false的前提下)里挑第一个broker作为leader(第一个broker最先放进ISR列表, 可能是同步数据最多的副本),如果参数unclean.leader.election.enable为true,代表在ISR列表里所有副本都挂了的时候可以在ISR列表以外的副本中选leader,这种设置,可以提高可用性,但是选出的新leader有可能数据少很多。
    • 副本进入ISR列表有两个条件:
      • 副本节点不能产生分区,必须能与zookeeper保持会话以及跟leader副本网络连通
      • 副本能复制leader上的所有写操作,并且不能落后太多。(与leader副本同步滞后的副本,是由replica.lag .time.max.ms配置决定的,超过这个时间都没有跟leade同步过一次的副本会被移除ISR列表

3.6 Kafka事务

        Kafka 从 0.11 版本开始引入了事务支持。事务可以保证 Kafka Exactly Once 语义的基
础上, 生产和消费可以跨分区和会话,要么全部成功,要么全部失败

3.6.1 Producer 事务

  • 为了实现跨分区跨会话的事务,需要引入一个全局唯一的 Transaction ID,并将 Producer 获得的PID Transaction ID 绑定。这样当Producer 重启后就可以通过正在进行的 Transaction ID 获得原来的 PID。 为了管理 TransactionKafka 引入了一个新的组件 Transaction CoordinatorProducer 是通过和 Transaction Coordinator 交互获得 Transaction ID 对应的任务状态。Transaction Coordinator 还负责将事务所有写入 Kafka 的一个内部 Topic,这样即使整个服务重启,由于事务状态得到保存,进行中的事务状态可以得到恢复,从而继续进行。

3.6.2 Consumer 事务

  • 上述事务机制主要是从 Producer 方面考虑,对于 Consumer 而言,事务的保证就会相对较弱,尤其时无法保证 Commit 的信息被精确消费。这是由于 Consumer 可以通过 offset 访问任意信息,而且不同的 Segment File 生命周期不同,同一事务的消息可能会出现重启后被​​​​​​​删除的情况

4.Kafka API

4.1 Producer API

4.1.1 消息发送流程

  • Kafka Producer 发送消息采用的是异步发送的方式。在消息发送的过程中,涉及到了两个线程——main 线程和 Sender 线程,以及一个线程共享变量——RecordAccumulatormain 线程将消息发送给 RecordAccumulatorSender 线程不断从 RecordAccumulator 中拉取消息发送到 Kafka broker
  • KafkaProducer 发送消息流程

  • 相关参数:
    • batch.size:只有数据积累到 batch.size 之后,sender 才会发送数据。
    • linger.ms:如果数据迟迟未达到 batch.sizesender 等待 linger.time 之后就会发送数据。

4.1.2 异步发送 API

  • 导入依赖
<dependency>
        <groupId>org.apache.kafka</groupId>
        <artifactId>kafka-clients</artifactId>
        <version>0.11.0.0</version>
</dependency>
  • 前提知识
    • 需要用到的类:
      • KafkaProducer:需要创建一个生产者对象,用来发送数据
      • ProducerConfig:获取所需的一系列配置参数
      • ProducerRecord:每条数据都要封装成一个 ProducerRecord 对象
  •  不带回调函数的 API
public class CustomProducer {
public static void main(String[] args) throws ExecutionException,InterruptedException {
        Properties props = new Properties();
        //kafka 集群,broker-list
        props.put("bootstrap.servers", "hadoop102:9092");
        //发送消息持久化机制参数,可以见上面有详细介绍
        props.put("acks", "all");
        //发送失败会重试,默认重试间隔100ms,重试能保证消息发送的可靠性,但是也可能造成消息重复发送,比如网络抖动,所以需要在接收者那边做好接受的幂等性处理
        props.put("retries", 1);
        //重试间隔设置
        props.put(ProducerConfig.RETRY.BACKOFF_MS_CONFIG, 300))
        //批次大小
        props.put("batch.size", 16384);
        //等待时间        
        props.put("linger.ms", 1);
        //RecordAccumulator 缓冲区大小,如果设置了该缓冲区,消息会先发送到本地缓冲区,可以提高发送性能
        props.put("buffer.memory", 33554432);
        //kafka本地线程会从缓冲区取数据,批量发送到broker,设置批量发送消息的大小,默认值是16384即16kb,就是说一个batch满了16kb就会发送出去
        props.put(ProducerConfig.BATCH_SIZE_CONFIG, 16394);
        //默认是0,意思就是消息必须立即被发送,但这样会影响性能,一般设置10毫秒左右,就是说这个消息发送完后会进入本地的一个batch.如果10ms内,这个batcher满了就会随batch一起被发送出去,如果10ms内.batch没有满,那么也必须把消息发送出去,不能让消息的发送延迟时间长
        props.put(ProducerConfig.LINGER_MS_CONFIG, 10);
        props.put("key.serializer",
        "org.apache.kafka.common.serialization.StringSerializer");
        props.put("value.serializer",
        "org.apache.kafka.common.serialization.StringSerializer");
        Producer<String, String> producer = new
        KafkaProducer<>(props);
        for (int i = 0; i < 100; i++) {
        producer.send(new ProducerRecord<String, String>("first",
                Integer.toString(i), Integer.toString(i)));
        }
        producer.close();
        }
}
  •  带回调函数的 API
    • 回调函数会在 producer 收到 ack 时调用,为异步调用,该方法有两个参数,分别是 RecordMetadata Exception,如果 Exception null,说明消息发送成功,如果 Exception 不为 null,说明消息发送失败.注意:消息发送失败会自动重试,不需要我们在回调函数中手动重试

public static void main(String[] args) throws ExecutionException, InterruptedException {
        Properties props = new Properties();
        props.put("bootstrap.servers", "hadoop102:9092");//kafka 集群,broker-list
        props.put("acks", "all");
        props.put("retries", 1);//重试次数
        props.put("batch.size", 16384);//批次大小
        props.put("linger.ms", 1);//等待时间
        props.put("buffer.memory", 33554432);//RecordAccumulator 缓 冲区大小
        props.put("key.serializer",
        "org.apache.kafka.common.serialization.StringSerializer");
        props.put("value.serializer",
        "org.apache.kafka.common.serialization.StringSerializer");
        Producer<String, String> producer = new
        KafkaProducer<>(props);
        for (int i = 0; i < 100; i++) {
                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();
                                                }
                                }
                        });
                }
                producer.close();
        }
}

4.1.3 同步发送 API  

  • 同步发送的意思就是,一条消息发送之后,会阻塞当前线程,直至返回 ack。( sender线程将数据传递到kafka集群中后,向main线程发送一个ack(可以继续发送信息了) ).由于 send 方法返回的是一个 Future 对象,根据 Futrue 对象的特点,我们也可以实现同步发送的效果,只需在调用 Future 对象的 get 方发即可。
public static void main(String[] args) throws ExecutionException, InterruptedException {
        Properties props = new Properties();
        props.put("bootstrap.servers", "hadoop102:9092");//kafka 集群,broker-list
        props.put("acks", "all");
        props.put("retries", 1);//重试次数
        props.put("batch.size", 16384);//批次大小
        props.put("linger.ms", 1);//等待时间
        props.put("buffer.memory", 33554432);//RecordAccumulator 缓冲区大小
        props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
        props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
        Producer<String, String> producer = new
        KafkaProducer<>(props);
        for (int i = 0; i < 100; i++) {
                producer.send(new ProducerRecord<String, String>("first", Integer.toString(i),                 Integer.toString(i))) .get() ;
        }
        producer.close();
        }
}

4.2 Consumer API

        Consumer 消费数据时的可靠性是很容易保证的,因为数据在 Kafka 中是持久化的,故
不用担心数据丢失问题。由于 consumer 在消费过程中可能会出现断电宕机等故障, consumer 恢复后,需要从故障前的位置的继续消费,所以 consumer 需要实时记录自己消费到了哪个 offset ,以便故障恢复后继续消费。所以 offset 的维护是 Consumer 消费数据是必须考虑的问题。

4.2.1 自动提交 offset

  • 导入依赖
<dependency>
        <groupId>org.apache.kafka</groupId>
        <artifactId>kafka-clients</artifactId>
        <version>0.11.0.0</version>
</dependency>
  •  前提知识
    • KafkaConsumer :需要创建一个消费者对象,用来消费数据
    • ConsumerConfig :获取所需的一系列配置参数
    • ConsuemrRecord :每条数据都要封装成一个 ConsumerRecord 对象
    • 为了使我们能够专注于自己的业务逻辑, Kafka 提供了自动提交 offset 的功能.自动提交 offset 的相关参数
      enable.auto.commit 是否开启自动提交 offset 功能
      auto.commit.interval.ms 自动提交 offset 的时间间隔
  • 自动提交 offset 的代码
public class CustomConsumer {
        public static void main(String[] args) {
                Properties props = new Properties();
                props.put("bootstrap.servers", "hadoop102:9092");
                //消费分组名
                props.put("group.id", "test");
                //是否自动提交offset,默认为true
                props.put("enable.auto.commit", "true");
                //自动提交offset的间隔时间;这个时间不容把握,如果设置自动间隔时间为1s,业务处理为5s,poll之后的1秒内拉取到5条数据,将offset提交,如果在生效的3秒内消费端宕机,会导致数据丢失
                //如果设置自动间隔时间为1s,业务处理为0.5s,在还没有提交offset的时候,业务处理已经完了,并且将数据写到了mysql,此时程序已经宕机,这种情况会出现重复消费
                props.put("auto.commit.interval.ms", "1000");
                //一次poll最大拉去消息的条数,如果消费者处理速度很快,可以设置的大一点,否则,可以设置的小点
                props.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, 500);
                //如果两次poll操作间隔超过了这个时间,broker就会认为这个consumer处理能力太弱,会将其提出消费者组,将分区分配给别的consumer消费
                 props.put(ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG,30*1000);
                props.put("key.deserializer","org.apache.kafka.common.serialization.StringDeserializer");
                props.put("value.deserializer","org.apache.kafka.common.serialization.StringDeserializer");

                KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);

                //订阅主题

                consumer.subscribe(Arrays.asList("first"));
                while (true) {
                        //poll()API是拉取消息的长轮询,默认从服务端拉1秒钟,1秒钟之内拉取到数据了,根据设置的参数值,拉去;如果1秒钟内没有拉取到数据,走下面的数据
                        ConsumerRecords<String, String> records =  consumer.poll(Duration.ofMillis(1000));
                        for (ConsumerRecord<String, String> record : records)
                                System.out.printf("offset = %d, key = %s, value
                                = %s%n", record.offset(), record.key(), record.value());
                        }
                }
        }

4.2.2 手动提交 offset

        虽然自动提交 offset 十分简介便利,但由于其是基于时间提交的,开发人员难以把握
offset 提交的时机。因此 Kafka 还提供了手动提交 offset API. 手动提交 offset 的方法有两种:分别是 commitSync (同步提交) commitAsync (异步提交) 。两者的相同点是,都会将 本次 poll 的一批数据最高的偏移量提交 ;不同点是,commitSync 阻塞当前线程,一直到提交成功,并且会自动失败重试(由不可控因素导致,也会出现提交失败);而 commitAsync 则没有失败重试机制,故有可能提交失败。
  •  同步提交 offset:由于同步提交 offset 有失败重试机制,故更加可靠,以下为同步提交 offset 的示例。
public class CustomComsumer {
        public static void main(String[] args) {
                Properties props = new Properties();
                //Kafka 集群
                props.put("bootstrap.servers", "hadoop102:9092");
                //消费者组,只要 group.id 相同,就属于同一个消费者组
                props.put("group.id", "test");
                props.put("enable.auto.commit", "false");//关闭自动提交 offset
                props.put("key.deserializer","org.apache.kafka.common.serialization.StringDeserializer");
                props.put("value.deserializer","org.apache.kafka.common.serialization.StringDeserializer");
                KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
                consumer.subscribe(Arrays.asList("first"));//消费者订阅主题
                while (true) {
                        //消费者拉取数据
                        ConsumerRecords<String, String> records = consumer.poll(100);
                        for (ConsumerRecord<String, String> record : records) {
                                System.out.printf("offset = %d, key = %s, value = %s%n",                                 record.offset(), record.key(), record.value());
                        }
                        //同步提交,当前线程会阻塞直到 offset 提交成功
                        consumer.commitSync();
                }
        }
}
  •  异步提交 offset:虽然同步提交 offset 更可靠一些,但是由于其会阻塞当前线程,直到提交成功。因此吞吐量会收到很大的影响。因此更多的情况下,会选用异步提交 offset 的方式。
    以下为异步提交 offset 的示例:
public class CustomConsumer {
        public static void main(String[] args) {
                Properties props = new Properties();
                //Kafka 集群
                props.put("bootstrap.servers", "hadoop102:9092");
                //consumer给broker发送心跳的间隔时间,broker接收到心跳,如果此时有rebalance发生会通过心跳响应将rebalance方案发给consumer,这个时间可以稍微短一点
                props.put(ConsumerConfig.Heartbeat_interval_MS_CONFIG, 1000)
                //当消费主题是一个新的消费者组,或者指定offset的消费方式,offset不存在,那么应该如何消费;latest(默认):只消费自己启动之后发送到主题的消息;earliest:第一次从头消费,以后按照消费offset记录继续消费.
                props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, 1000)
                //服务端broker多久感知不到一个consumer心跳就认为他故障了,会将其踢出消费组,对应的partition也会分给其他concumer,默认是10s
                props.put(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG, 10 * 1000)
                //消费者组,只要 group.id 相同,就属于同一个消费者组
                props.put("group.id", "test");
                //关闭自动提交 offset
                props.put("enable.auto.commit", "false");
                props.put("key.deserializer","org.apache.kafka.common.serialization.StringDeserializer");
                props.put("value.deserializer","org.apache.kafka.common.serialization.StringDeserializer");
                KafkaConsumer<String, String> consumer = new
                KafkaConsumer<>(props);
                //消费者订阅主题(当前这个和下面的需要一个配置就行)
                consumer.subscribe(Arrays.asList("first"));
                //消费指定分区:主题为first的0分区
                consumer.'assign(Arrays.asList(new TopicPartition("first",0)))
                //消息回溯消费
                consumer.assign(Arrays.asList(new TopicPartition("first",0)))
                consumer.seekToBeginning(Arrays.asList(new TopicPartition("first",0)))
                //指定offset消费       
                consumer.assign(Arrays.asList(new TopicPartition("first",0)))
                consumer.seek(new TopicPartition("first",0), 10)
                //从指定的时间开始消费
                List<PartitionInfo> topicPartitions = consumer.partitionsFor("first");
                long fetchDataTime = new Date().getTime -1000 * 60 * 60
                Map<TopicPartition, Long> map =  new HashMap();
                for (PartitionInfo par : topicPartitions){
                        map.put(new TopicPartition("first", par.partition(), fetchDataTime));
                }
                Map<TopicPartition, OffsetAndTimestamp > parMap =             consumer.offsetsForTimes(map);
                for(Map.Entry<TopicPartition, OffsetAndTimestamp> entry : parMap.entrySet){
                           TopicPartition key   = entry.getKey();
                           OffsetAndTimestamp value =  entry.getVaule();
                           if(key ==null || value == null) continue;
                           //得到一小时前的第一条消息的offset
                           long offset = value.offset();
                           if(value != null){
                                   consumer.assign(Arrays,asList<key>);
                                   consumer.seek(key, offset);
                           }
                            
                }
                while (true) {
                         //消费者拉取数据
                        ConsumerRecords<String, String> records = consumer.poll(100);
                        for (ConsumerRecord<String, String> record : records) {
                                System.out.printf("offset = %d, key = %s, value
                                = %s%n", record.offset(), record.key(), record.value());
                        }
                        //异步提交
                        consumer.commitAsync(new OffsetCommitCallback() {
                        @Override
                        public void onComplete(Map<TopicPartition,
                                OffsetAndMetadata> offsets, Exception exception) {
                                if (exception != null) {
                                        System.err.println("Commit failed for" + offsets);
                                }
                        }});
                }
        }
}

4.2.3 自定义offset

  • 如果没有明确的指定属于某一个消费者组的消费者消费一个确定分区,在Rebalance时,可能会导致消费的分区发生变换
  • 消费者发生 Rebalance 之后,每个消费者消费的分区就会发生变化。因此消费者要首先获取到自己被重新分配到的分区,并且定位到每个分区最近提交的 offset 位置继续消费。
  • 要实现自定义存储 offset,需要借助 ConsumerRebalanceListener,以下为示例代码,其中提交和获取 offset 的方法,需要根据所选的 offset 存储系统自行实现​​​​​​​
public class CustomConsumer {
        private static Map<TopicPartition, Long> currentOffset = new HashMap<>();
        public static void main(String[] args) {
                //创建配置信息
                Properties props = new Properties();
                //Kafka 集群
                props.put("bootstrap.servers", "hadoop102:9092");
                //消费者组,只要 group.id 相同,就属于同一个消费者组
                props.put("group.id", "test");
                //关闭自动提交 offset
                props.put("enable.auto.commit", "false");
                //Key 和 Value 的反序列化类
                props.put("key.deserializer",
                        "org.apache.kafka.common.serialization.StringDeserializer");
                props.put("value.deserializer",
                        "org.apache.kafka.common.serialization.StringDeserializer");
                //创建一个消费者
                KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
                //消费者订阅主题
                consumer.subscribe(Arrays.asList("first"), new
                ConsumerRebalanceListener() {
                        //该方法会在 Rebalance 之前调用
                        @Override
                        public void onPartitionsRevoked(Collection<TopicPartition> partitions) {
                                commitOffset(currentOffset);
                        }
                        //该方法会在 Rebalance 之后调用
                        @Override
                        public void onPartitionsAssigned(Collection<TopicPartition> partitions) {
                                currentOffset.clear();
                                for (TopicPartition partition : partitions) {
                                        consumer.seek(partition, getOffset(partition));//
                                        定位到最近提交的 offset 位置继续消费
                                }
                        }
                });
                while (true) {
                        ​​​​​​​//消费者拉取数据
                        ConsumerRecords<String, String> records = consumer.poll(100);
                        for (ConsumerRecord<String, String> record : records) {
                                System.out.printf("offset = %d, key = %s, value
                                                = %s%n", record.offset(), record.key(), record.value());
                                currentOffset.put(new TopicPartition(record.topic(), record.partition()),                                 record.offset());
                }
                commitOffset(currentOffset);//异步提交
        }
}
        //获取某分区的最新 offset
        private static long getOffset(TopicPartition partition) {
                return 0;
        }
        //提交该消费者所有分区的 offset
        private static void commitOffset(Map<TopicPartition, Long>
                currentOffset) {
        }
}

         

4.3 Spring-boot整合kafka

  • 相关配置不再介绍,主要介绍listeren

MyConsumer

@KafkaListener(id = "thing2", topicPartitions =
        { @TopicPartition(topic = "topic1", partitions = { "0", "1" }),
          @TopicPartition(topic = "topic2", partitions = "0",
             partitionOffsets = @PartitionOffset(partition = "1", initialOffset = "100"))
        })

@Component

public class MyConsumer{

        

        @KafkaListener(topics="my-topic", groupId="myGroup")

        public void Listener(ConsumerRecord<String,String> record, Acknowledgment ack){

                //业务逻辑

                //手动提交offset 

                ack.acknowledge()

        

        }

}

//对应的.yml文件

Spring:

        kafka:

                Listener:

                //当每一条记录被消费者监听器(ListenerConsumer)处理后提交

                //RECORD

                //当一批poll()的数据被消费者监听器(ListenerConsumer)处理候提交

                //BATCH

                //当一批poll()的数据被消费者监听器(ListenerConsumer)处理之后,距离上次提交时间大于TIME时提交

                //TIME

                // 当一批poll()的数据被消费者监听器(ListenerConsumer)处理之后,被处理record数量大于等于COUNT时提交

                //COUNT

                //TIME | COUNT 有一个条件满足时提交

                //COUNT_TIME

                // 当一批poll()的数据被消费者监听器(ListenerConsumer)处理之后,手动调用Acknowledgment.ackonwledge()后提交

                //MANUAL

                //手动调用Acknowledgment.ackonwledge()后提交

                //MANUAL_IMMEDIATE

                ack-mode: manual_immediate

kafka线上容量规划 

JVM参数设置

kafka是scala语言开发,运行在JVM上,需要对JVM参数合理设置
修改bin/kafka-start-server.sh中的jvm设置,假设机器是32G内存,可以如下设置:
export KAFKA_ HEAP_ OPTS=" -Xmx16G -Xms16G -Xmn10G -XX:MetaspaceSize=256M -XX:+UseG1GC -XX:MaxGCPauseMi1lis=50 -XX:G1HeapRegionSize=16M"


这种大内存的情况一般都要用G1垃圾收集器, 因为年轻代内存比较大,用G1可以设置QC最大停顿时间,不至于一次minor gc就花费太长时间,当然,因为像kafka, rocketmq, es
这些中间件,写数据到磁盘会用到操作系统的page cache,所以JVM内存不宜分配过大,需要给操作系统的缓存留出几个G。

消息丢失情况

消息发送端

  • acks=0: 表示producer不需要 等待任何broker确认收到消息的回复,就可以继续发送下一条消息。性能最高,但是最容易丢消息。大数据统计报表场景,对性能要求很高,对数据丢失不敏感的情况可以用这种。
  • acks=1: 至少要等待leader已经成功将数据写 入本地log,但是不需要等待所有follower是否成功写入。就可以继续发送下一条消息。 这种情况下,如果follower没有成功备份数据,而此时leader又挂掉,则消息会丢失。
  • acks=-1或all: 这意味着leader需要等待所有 备份(min.insync.replicas配置的备份个数)都成功写入日志,这种策略会保证只要有一个备份存活就不会丢失数据。这是最强的数据保证。一般除非是金融级别,或跟钱打交道的场景才会使用这种配置。当然如果min.insync.replicas配置的是1则也可能丢消息, 跟acks=1情况类似。(min.insync.replicas>=2)

消息消费端:

  • 如果消费这边配置的是自动提交,万一消费到数据还没处理完,就自动提交ffset了, 但是此时你consumer直接宕机了,未处理完的数据丢失了,下次也消费不到了。 所以应该手动提交offset

消息重复消费

消息发送端

  • 发送消息如果配置了重试机制,比如网络抖动时间过长导致发送端发送超时,实际broker可能已经接收到消息,但发送方会重新发送消息

消息消费端

  • 如果消费这边配置的是自动提交,刚拉取了一批数据处理了一部分,但还没来得及提交,服务挂了,下次重启又会拉取相同的一批数据重复处理,一般消费端都是要做消费幂等处理的(订单id,搞一个redis分布式锁)
    • 所谓幂等性:多次操作的结果是一样的。对于rest的请求get为幂等操作,post为非幂等操作,put为幂等操作,delete为幂等操作
    • 在数据库中创建联合主键,防止相同的主键创建出多条记录.
    • 使用分布式锁,以业务id为锁,保证只有一条记录能够创建成功

消息乱序

  • 如果发送端配置了重试机制,kafka不会等之前那条消息完全发送成功才去发送下一条消息,这样可能会出现,发送了1, 2, 3条消息,第一条超时了,后面两条发送成功,再重试发送第1条消息,这时消息在broker端的顺序就是2, 3,1.这里的话因为发送端的关系导致消费不一致,所以,是否一定要配置重试要根据业务情况而定。也可以用同步发送的模式去发消息,当然acks不能设置为0, 这样也能保证消息发送的有序。
  • kafka保证全链路消息顺序消费,需要从发送端开始,将所有有序消息发送到同一个分区,然后用一个消费者去消费,但是这种性能比较低,
    • 消费端:发送的消息增加一个全局Id,通过CountDownLatch一次收集需要一起处理的几个消息,通过全局id进行排序,保证有序
    • 同样使用一个分区:可以在消费者端接收到消息后将需要保证顺序消费的几条消费发到内存队列可以搞多个,一个内存队列开启一个线程顺序处理消息。

消息积压

  • 线上有时因为发送方发送消息速度过快,或者消费方处理消息过慢,可能会导致brokerf积压大量未消费消息。此种情况如果积压了上百万未消费消息需要紧急处理,可以修改消费端程序,让其将收到的消息快速转发到其他topic(可以设置很多分区),然后再启动多个消费者同时消费新主题的不同分区。
    •  在这个消费者中,使用多线程,充分利用机器的性能进行消费消息
    • 创建多个消费者组,分别从以及较远的offset开始消费(总共积压5000万的数据,当前offset消费到一百万,新启动一个消费者offset从2000万开始消费,一个消费者offset从4000万开始消费,到达对应的offset就停止)
    • 创建一个消费者,另外创建一个主题,配上多个分区,多个分区配上多个消费者。该消费者将poll下来的信息不进行消费直接转发到新主题的新分区上,让多个消费者开始消费
  • 由于消息数据格式变动或消费者程序有bug,导致消费者-直消费不成功,也可能导致broker积压大量未消费消息。此种情况可以将这些消费不成功的消息转发到其它队列里去(类似死信队列),后面再慢慢分析死信队列里的消息处理问题。

延时队列.

  • 延时队列存储的对象是延时消息。所谓的延时消息”是指消息被发送以后,并不想让消费者立刻获取,而是等待特定的时间后,消费才能获取这个消息进行消费,延时队列的使用场景有很多,比如:
    • 在订单系统中,一个用户下单之后通常有30分钟的时间进行支付,如果30分钟之内没有支付成功,那么这个订单将进行异常处理,这时就可以使用延时队列来处理这些订单
    • 订单完成1小时后通知用户进行评价。
  • 实现思路:发送延时消息时先把消息按照不同的延迟时间段发送到指定的队列中(topic_ 1s, topic _5s, topic_ 10s, 这个一般不能支持任意时间段的延时),然后通过定时器进行轮训消费这些topic,查看消息是否到期,如果到期就把这个消息发送到具体业务处理的topic中,队列中消息越靠前的到期时间越早,具体来说就是定时器在一次消费过程中,对消息的发送时间做判断,看下是否延迟到对应时间了,如果到了就转发,如果还没到这一次定时任务就可以提前结束了,记住当前的offset,下一次就从当前offset开始消费

消息回溯

  • 如果某段时间对已消费消息计算的结果觉得有问题,可能是由于程序bug导致的计算错误,当程序bug修复后,这时可能需要对之前已消费的消息重新消费,可以指定从多久之前的消息回溯消费,这种可以用consumer的offsetsForTimes. seek等方法指定从某个offset偏移的消息开始消费

分区数和吞吐量的关系

  • 不断增加分区`数目,直到达到一个瓶颈

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值