• 【无标题】


    Kafka

    为什么要使用消息队列

    1.使用同步的通信方式解决多个服务之间的通信

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-f6v9N9zl-1653142857621)(C:\Users\lenovo\AppData\Roaming\Typora\typora-user-images\1652791725686.png)]

    同步的通信方式会存在性能和稳定性的问题

    使用异步的通信方式

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pVVh10kZ-1653142857623)(Kafka.assets/1652795996570.png)]

    针对同步的通信方式来说,异步的方式,可以让上游快速成功,极大提高了系统的吞吐量。而且在分布式系统中,通过下游多个服务的分布式事务的保障,也能保障业务执行之后的最终一致性。

    消息队列解决具体的是什么问题——通信问题

    消息队列的流派

    目前消息队列的中间键选型有很多种:

    • RabbitMQ :内部的可玩性(功能性)是非常强的
    • RocketMQ :阿里内部一个大神,根据Kafka的内部执行原理,手写的一个消息队列中间件,性能上能和kafka相比肩,在功能上封装了更多功能
    • Kafka :全球消息处理性能最快的一款MQ
    • ZeroMQ
    • ActiveMQ

    这些消息中间件有什么区别?

    什么是MQ

    Message Queue(MQ),消息队列中间件。很多人都说:MQ 通过将消息的发送和接收分离来实现应用程序的异步和解偶,这个给人的直觉是——MQ 是异步的,用来解耦的,但是这个只是 MQ 的效果而不是目的。MQ 真正的目的是为了通讯,屏蔽底层复杂的通讯协议,定义了一套应用层的、更加简单的通讯协议。一个分布式系统中两个模块之间通讯要么是HTTP,要么是自己开发的(rpc) TCP,但是这两种协议其实都是原始的协议。HTTP 协议很难实现两端通讯——模块 A 可以调用 B,B 也可以主动调用 A,如果要做到这个两端都要背上WebServer,而且还不支持⻓连接(HTTP 2.0 的库根本找不到)。TCP 就更加原始了,粘包、心跳、私有的协议,想一想头皮就发麻。MQ 所要做的就是在这些协议之上构建一个简单的“协议”——生产者/消费者模型。MQ 带给我的“协议”不是具体的通讯协议,而是更高层次通讯模型。它定义了两个对象——发送数据的叫生产者;接收数据的叫消费者, 提供一个SDK 让我们可以定义自己的生产者和消费者实现消息通讯而无视底层通讯协议

    有 Broker 的 MQ

    这个流派通常有一台服务器作为 Broker,所有的消息都通过它中转。生产者把消息发送给它就结束自己的任务了,Broker 则把消息主动推送给消费者(或者消费者主动轮询)

    重Topic

    • Kafka、RocketMQ、ActiveMQ

    kafka、JMS(ActiveMQ)就属于这个流派,生产者会发送 key 和数据到 Broker,由 Broker比较 key 之后决定给哪个消费者。这种模式是我们最常⻅的模式,是我们对 MQ 最多的印象。在这种模式下一个 topic 往往是一个比较大的概念,甚至一个系统中就可能只有一个topic,topic 某种意义上就是 queue,生产者发送 key 相当于说:“hi,把数据放到 key 的队列中”

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aAa0LrS3-1653142857624)(Kafka.assets/1652797165942.png)]

    如上图所示,Broker 定义了三个队列,key1,key2,key3,生产者发送数据的时候会发送key1 和 data,Broker 在推送数据的时候则推送 data(也可能把 key 带上)。

    虽然架构一样但是 kafka 的性能要比 jms 的性能不知道高到多少倍,所以基本这种类型的MQ 只有 kafka 一种备选方案。如果你需要一条暴力的数据流(在乎性能而非灵活性)那么kafka 是最好的选择

    轻 Topic

    • RabbitMQ

    这种的代表是 RabbitMQ(或者说是 AMQP)。生产者发送 key 和数据,消费者定义订阅的队列,Broker 收到数据之后会通过一定的逻辑计算出 key 对应的队列,然后把数据交给队列

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gy8tgXr3-1653142857624)(Kafka.assets/1652797481885.png)]

    这种模式下解耦了 key 和 queue,在这种架构中 queue 是非常轻量级的(在 RabbitMQ 中它的上限取决于你的内存),消费者关心的只是自己的 queue;生产者不必关心数据最终给谁只要指定 key 就行了,中间的那层映射在 AMQP 中叫 exchange(交换机)。

    AMQP 中有四种 exchange

    • Direct exchange:key 就等于 queue
    • Fanout exchange:无视 key,给所有的 queue 都来一份
    • Topic exchange:key 可以用“宽字符”模糊匹配 queue
    • Headers exchange:无视 key,通过查看消息的头部元数据来决定发给那个
    • queue(AMQP 头部元数据非常丰富而且可以自定义)

    这种结构的架构给通讯带来了很大的灵活性,我们能想到的通讯方式都可以用这四种exchange 表达出来。如果你需要一个企业数据总线(在乎灵活性)那么 RabbitMQ 绝对的值得一用

    无 Broker 的 MQ

    在生产者和消费者之间没有使用broker,例如zeroMQ,直接使用socket进行通信。

    无Broker的MQ的代表是ZeroMQ。该作者非常睿智,他非常敏锐的意识到—―MQ是更高级的Socket,它是解决通讯问题的。所以ZeroMQ被设计成了一个"库"而不是一个中间件,这种实现也可以达到——没有Broker的目的
    节点之间通讯的消息都是发送到彼此的队列中,每个节点都既是生产者又是消费者。ZeroMQ做的事情就是封装出 —套类似于Socket 的API可以完成发送数据,读取数据
    ZeroMQ其实就是一个跨语言的、重量级的Actor模型邮箱库。你可以把自己的程序想象成一个Actor,ZeroMQ就是提供邮箱功能的库;ZeroMQ可以实现同一台机器的RPC通讯也可以实现不同机器的TCP、UDP通讯,如果你需要一个强大的、灵活、野蛮的通讯能力,别犹豫ZeroMO

    一、Kafka介绍

    Kafka是最初由Linkedin公司开发,是一个分布式、支持分区的(partition)、多副本的
    (replica),基于zookeeper协调的分布式消息系统,它的最大的特性就是可以实时的处理
    大量数据以满足各种需求场景:比如基于hadoop的批处理系统、低延迟的实时系统、
    Storm/Spark流式处理引擎,web/nginx日志、访问日志,消息服务等等,用scala语言编
    写,Linkedin于 2010 年贡献给了Apache基金会并成为顶级开源 项目。

    1.Kafka的使用场景

    日志收集:一个公司可以用Kafka收集各种服务的log,通过kafka以统一接口服务的方式
    开放给各种consumer,例如hadoop、Hbase、Solr等。
    消息系统:解耦和生产者和消费者、缓存消息等。
    用户活动跟踪:Kafka经常被用来记录web用户或者app用户的各种活动,如浏览网⻚、
    搜索、点击等活动,这些活动信息被各个服务器发布到kafka的topic中,然后订阅者通过
    订阅这些topic来做实时的监控分析,或者装载到hadoop、数据仓库中做离线分析和挖
    掘。
    运营指标:Kafka也经常用来记录运营监控数据。包括收集各种分布式应用的数据,生产
    各种操作的集中反馈,比如报警和报告。

    2.Kafka基本概念

    kafka是一个分布式的,分区的消息(官方称之为commit log)服务。它提供一个消息系统应该
    具备的功能,但是确有着独特的设计。可以这样来说,Kafka借鉴了JMS规范的思想,但是确
    没有完全遵循JMS规范。

    首先,让我们来看一下基础的消息(Message)相关术语:

    名称解释
    Broker消息中间件处理节点,⼀个Kafka节点就是⼀个broker,⼀个或者多个Broker可以组成⼀个Kafka集群
    TopicKafka根据topic对消息进⾏归类,发布到Kafka集群的每条消息都需要指定⼀个topic
    Producer消息⽣产者,向Broker发送消息的客户端
    Consumer消息消费者,从Broker读取消息的客户端
    ConsumerGroup每个Consumer属于⼀个特定的Consumer Group,⼀条消息可以被多个不同的Consumer Group消费,但是⼀个Consumer Group中只能有⼀个Consumer能够消费该消息
    Partition物理上的概念,⼀个topic可以分为多个partition,每个partition内部消息是有序的

    因此,从一个较高的层面上来看,producer通过网络发送消息到Kafka集群,然后consumer
    来进行消费,如下图:

    3.Kafka安装

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9z7VbcWY-1653142857625)(Kafka.assets/1652846202417.png)]

    服务端(brokers)和客户端(producer、consumer)之间通信通过 TCP协议 来完成。

    二、kafka基本使用

    1.安装前的环境准备

    • 安装jdk

    • 安装zk(由于没有zookeeper所以使用Docker安装)

      systemctl start docker # 启动Docker
      docker pull zookeeper # 先从docker hub拉去镜像
      docker run -d --name zookeeper -p 2181:2181 zookeeper # 运行此容器
      
      • 1
      • 2
      • 3
    • 官网下载kafka的压缩包:http://kafka.apache.org/downloads

    • 解压缩至如下路径

    /usr/local/kafka/
    
    • 1
    • 修改配置文件:/usr/local/kafka/kafka2.11-2.4/config/server.properties
    #broker.id属性在kafka集群中必须要是唯一
    broker.id= 0
    #kafka部署的机器ip和提供服务的端口号
    listeners=PLAINTEXT://192.168.65.60:9092
    #kafka的消息存储文件
    log.dir=/usr/local/data/kafka-logs
    #kafka连接zookeeper的地址
    zookeeper.connect= 192.168.65.60:2181
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    2.启动kafka服务器

    进入到bin目录下。使用命令来启动

    ./kafka-server-start.sh -daemon ../config/server.properties
    
    • 1

    验证是否启动成功:

    进入到zk中的节点看id是 0 的broker有没有存在(上线)

    ls /brokers/ids/
    
    • 1

    server.properties核心配置详解:

    PropertyDefaultDescription
    broker.id0每个broker都可以⽤⼀个唯⼀的⾮负整数id进⾏标识;这个id可以作为broker的“名字”,你可以选择任意你喜欢的数字作为id,只要id是唯⼀的即可。
    log.dirs/tmp/kafka-logskafka存放数据的路径。这个路径并不是唯⼀的,可以是多个,路径之间只需要使⽤逗号分隔即可;每当创建新partition时,都会选择在包含最少partitions的路径下进⾏。
    listenersPLAINTEXT://192.168.65.60:9092server接受客户端连接的端⼝,ip配置kafka本机ip即可
    zookeeper.connectlocalhost:2181zooKeeper连接字符串的格式为:hostname:port,此处hostname和port分别是ZooKeeper集群中某个节点的host和port;zookeeper如果是集群,连接⽅式为hostname1:port1, hostname2:port2,hostname3:port3
    log.retention.hours168每个⽇志⽂件删除之前保存的时间。默认数据保存时间对所有topic都⼀样。
    num.partitions1创建topic的默认分区数
    default.replication.factor1⾃动创建topic的默认副本数量,建议设置为⼤于等于2
    min.insync.replicas1当producer设置acks为-1时,min.insync.replicas指定replicas的最⼩数⽬(必须确认每⼀个repica的写数据都是成功的),如果这个数⽬没有达到,producer发送消息会产⽣异常
    delete.topic.enablefalse是否允许删除主题

    3.创建主题topic

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eDSDLvUg-1653142857625)(Kafka.assets/1652868603902.png)]

    • 执行以下命令创建名为“test”的topic,这个topic只有一个partition,并且备份因子也设置为1(通过Kafka命令向zk中创建一个主题)
    ./kafka-topics.sh --create --zookeeper 192.168.49.128:2181 --replication-factor 1 --partitions 1 --topic test
    
    • 1
    • 查看当前kafka内有哪些topic(查看当前zk中所有的主题)
    ./kafka-topics.sh --list --zookeeper 192.168.49.128:2181
    
    • 1

    4.发送消息

    kafka自带了一个producer命令客户端,可以从本地文件中读取内容,或者我们也可以以命令行中直接输入内容,并将这些内容以消息的形式发送到kafka集群中。在默认情况下,每一个行会被当做成一个独立的消息。使用kafka的发送消息的客户端,指定发送到的kafka服务器地址和topic

    ./kafka-console-producer.sh --broker-list 192.168.49.128:9092 --topic test
    
    • 1

    5.消费消息

    对于consumer,kafka同样也携带了一个命令行客户端,会将获取到内容在命令中进行输
    出, 默认是消费最新的消息 。使用kafka的消费者消息的客户端,从指定kafka服务器的指定
    topic中消费消息

    方式一:从最后一条消息的偏移量+1开始消费(从当前主题中的最后一条消息offset(偏移量位置)+1开始消费)

    ./kafka-console-consumer.sh --bootstrap-server 192.168.49.128:9092 --topic test
    
    • 1

    方式二:从头开始消费

    ./kafka-console-consumer.sh --bootstrap-server 192.168.49.128:9092 --from-beginning --topic test
    
    • 1

    几个注意点:

    • 消息会被存储
    • 消息是顺序存储
    • 消息是有偏移量的
    • 消费时可以指明偏移量进行消费

    关于消息的细节

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rUEVDy6d-1653142857626)(Kafka.assets/1652879410798.png)]

    • 生产者将消息发送给broker,broker会将消息保存在本地的日志文件中
    /usr/local/kafka/data/kafka-logs/主题-分区/00000000.log
    
    • 1
    • 消息的保存是有序的,通过offset偏移量来描述消息的有序性
    • 消费者消费消息时也是通过offset来描述当前要消费的那条消息的位置

    三、Kafka中的关键细节

    1.消息的顺序存储

    消息的发送方会把消息发送到broker中,broker会存储消息,消息是按照发送的顺序进行存储。因此消费者在消费消息时可以指明主题中消息的偏移量。默认情况下,是从最后一个消息的下一个偏移量开始消费。

    2. 单播消息的实现

    在一个Kafka的topic中,启动两个消费者,一个是生产者,问:生产者发送消息,这条消息是否同时会被两个消费者消费?

    如果多个消费者在同一个消费组,那么只有一个消费者可以收到订阅的topic中的消息。换言之,同一个消费组中只能有一个消费者收到一个topic中的消息。

    单播消息:一个消费组里 只会有一个消费者能消费到某一个topic中的消息。于是可以创建多个消费者,这些消费者在同一个消费组中。

    ./kafka-console-consumer.sh --bootstrap-server 192.168.49.128:9092 --consumer-property group.id=testGroup --topic test
    
    • 1

    3.多播消息的实现

    在一些业务场景中需要让一条消息被多个消费者消费,那么就可以使用多播模式。

    不同的消费组订阅同一个topic,那么不同的消费组中只有一个消费者能收到消息。实际上也是多个消费组中的多个消费者收到了同一个消息。

    kafka实现多播,只需要让不同的消费者处于不同的消费组即可。

    ./kafka-console-consumer.sh --bootstrap-server 192.168.49.128:9092 --consumer-property group.id=testGroup1 --topic test
    
    ./kafka-console-consumer.sh --bootstrap-server 192.168.49.128:9092 --consumer-property group.id=testGroup2 --topic test
    
    • 1
    • 2
    • 3

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-58j9yaQS-1653142857626)(Kafka.assets/1652881186858.png)]

    4.查看消费组及信息

    # 查看当前主题下有哪些消费组
    ./kafka-consumer-groups.sh --bootstrap-server 192.168.49.128:9092 --list
    # 查看消费组中的具体信息:比如当前偏移量、最后一条消息的偏移量、堆积的消息数量
    ./kafka-consumer-groups.sh --bootstrap-server 192.168.49.128:9092 --describe --group testGroup
    
    • 1
    • 2
    • 3
    • 4

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NyUwIEEx-1653142857627)(Kafka.assets/1652874550209.png)]

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

    四、主题、分区的概念

    1.主题Topic

    主题Topic可以理解成是一个类别的名称

    主题-topic在Kafka中是一个逻辑的概念,Kafka通过topic将消息进行分类。不同的topic会被订阅该tipic的消费者消费。

    但是有一个问题,如果说这个topic中的消息非常非常多,多到需要几T来存,因为消息是会被保存到log日志文件中的。为了解决这个文件过大的问题,Kafka提出了Partittion分区的概念

    2.partition分区

    分区概念:

    通过partition将一个topic中的消息分区来存储。这样的好处有多个:

    • 分区存储,可以解决统一存储文件过大的问题
    • 提供了读写的吞吐量:读和写可以同时在多个分区中进行

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5iCMcXOy-1653142857627)(Kafka.assets/1652922960970.png)]

    一个主题中的消息量是非常大的,因此可以通过分区的设置,来分布式存储这些消息。比如一个topic创建了 3 个分区。那么topic中的消息就会分别存放在这三个分区中。

    为一个主题创建多个分区

    ./kafka-topics.sh --create --zookeeper 192.168.49.128:2181 --partitions 2 --topic test1
    
    • 1

    可以通过这样的命令查看topic的分区信息

    ./kafka-topics.sh --describe --zookeeper 192.168.49.128:2181 --topic test1
    
    • 1

    分区的作用:

    • 可以分布式存储
    • 可以并行写

    实际上是存在data/kafka-logs/test-0 和 test-1中的0000000.log文件中

    小细节:

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

    五、Kafka集群及副本的概念

    1.搭建kafka集群, 3 个broker

    准备 3 个server.properties文件

    每个文件中的这些内容要调整

    • server.properties
    broker.id= 0
    listeners=PLAINTEXT://192.168.65.60:
    log.dir=/usr/local/data/kafka-logs
    
    • 1
    • 2
    • 3
    • server1.properties
    broker.id= 1
    listeners=PLAINTEXT://192.168.65.60:
    log.dir=/usr/local/data/kafka-logs-
    
    • 1
    • 2
    • 3
    • server2.properties
    broker.id= 2
    listeners=PLAINTEXT://192.168.65.60:
    log.dir=/usr/local/data/kafka-logs-
    
    • 1
    • 2
    • 3

    使用如下命令来启动 3 台服务器

    ./kafka-server-start.sh -daemon../config/server0.properties
    ./kafka-server-start.sh -daemon../config/server1.properties
    ./kafka-server-start.sh -daemon../config/server2.properties
    
    • 1
    • 2
    • 3

    搭建完后通过查看zk中的/brokers/ids 看是否启动成功

    2.副本的概念

    副本是对分区的备份。在集群中,不同的副本会被部署在不同的broker上。下面例子:创建 1个主题, 2 个分区、 3 个副本。

    ./kafka-topics.sh --create --zookeeper 172.16.253.35:2181 --replication-factor 3 --partitions 2 --topic my-replicated-topic
    
    • 1

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UaEcMKPy-1653142857628)(Kafka.assets/1652948407621.png)]

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-doBsFOjq-1653142857629)(Kafka.assets/1652949333680.png)]

    通过查看主题信息,其中的关键数据:

    • replicas:当前副本存在的broker节点

    • leader:副本里的概念

      ​ Kafka的写和读的操作,都发生在leader上。leader负责把数据同步给follower。当leader挂了,经过主从选举,从多个follower中选举产生一个新的leader

      • 每个partition都有一个broker作为leader。
      • 消息发送方要把消息发给哪个broker?就看副本的leader是在哪个broker上面。副本里的leader专⻔用来接收消息。
      • 接收到消息,其他follower通过poll的方式来同步数据。
    • follower:leader处理所有针对这个partition的读写请求,而follower被动复制leader,不提供读写(主要是为了保证多副本数据与消费的一致性),如果leader所在的broker挂掉,那么就会进行新leader的选举,至于怎么选,在之后的controller的概念中介绍。

    • isr

      可以同步和已同步的节点会被存入到isr集合中。这里有一个细节:如果isr中的节点性能较差,会被提出isr集合。

    通过kill掉leader后再查看主题情况

    # kill掉leader
    ps -aux | grep server.properties
    kill 17631
    # 查看topic情况
    ./kafka-topics.sh --describe --zookeeper 192.168.49.128:2181 --topic my-replicated-topic
    
    • 1
    • 2
    • 3
    • 4
    • 5

    isr:
    可以同步的broker节点和已同步的broker节点,存放在isr集合中。

    此时,broker、主题、分区、副本这些概念就全部展现了,大家需要把这些概念梳理清楚:
    集群中有多个broker,创建主题时可以指明主题有多个分区(把消息拆分到不同的分区中存储),可以为分区创建多个副本,不同的副本存放在不同的broker里。

    3.broker、主题、分区、副本

    • kafka集群中由多个broker组成
    • 一个broker中存放一个topic的不同partition——副本

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QeHXXz5S-1653142857629)(Kafka.assets/1652949274605.png)]

    4.kafka集群消息的发送

    ./kafka-console-producer.sh --broker-list 192.168.49.128:9092,192.168.49.128:9093,192.168.49.128:9094 --topic my-replicated-topic
    
    • 1

    5.kafka集群消息的消费

    ./kafka-console-consumer.sh --bootstrap-server 192.168.49.128:9092,192.168.49.128:9093,192.168.49.128:9094 --from-beginning --topic my-replicated-topic
    
    • 1

    6.关于分区消费组消费者的细节

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OlXgZeV7-1653142857630)(Kafka.assets/1652951373453.png)]

    图中Kafka集群有两个broker,每个broker中有多个partition。一个partition只能被一个消费组里的某一个消费者消费,从而保证消费顺序。Kafka只在partition的范围内保证消息消费的局部顺序性,不能在同一个topic中的多个partition中保证总的消费顺序性。一个消费者可以消费多个partition。

    消费组中消费者的数量不能比一个topic中的partition数量多,否则多出来的消费者消费不到消息。

    • 向集群发送消息:
    ./kafka-conole-consumer.sh --bootstrap-server 192.168.49.128:9092,192.168.49.128:9093,192.168.49.128:9094 --from-beginning --consumer-property group.id=testGroupl --topic my-replicated-topic
    
    • 1
    • 从集群中消费消息
    ./kafka-console-producer.sh --broker-list 192.168.49.128:9092,192.168.49.128:9093,192.168.49.128:9094 --topicmy-replicated-topic
    
    • 1
    • 指定消费组来消费消息

      ./kafka-console-consumer. sh --bootstrap-server 192.168.49.128:9092,192.168.49.128:9093,192.168.49.128:9094 --from-beginning --Consumer-property group.id=testGroupl --topic my-replicated-topic
      
      • 1
    • 一个partition只能被一个消费组中的一个消费者消费,目的是为了保证消费的顺序性,但是多个partion的多个消费者消费的总的顺序性是得不到保证的,那怎么做到消费的总顺序性呢?

    • partition的数量决定了消费组中消费者的数量,建议同一个消费组中消费者的数量不要超过partition的数量,否则多的消费者消费不到消息

    • 如果消费者挂了,那么会触发rebalance机制(后面介绍),会让其他消费者来消费该分区

    六、Kafka的Java客户端-生产者

    1.引入依赖

    <dependency>
        <groupId>org.apache.kafka</groupId>
        <artifactId>kafka-clients</artifactId>
        <version>2.4.1</version>
    </dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    2.生产者发送消息的基本实现

     //消息的发送方
    public class MyProducer {
    
    private final static String TOPIC_NAME = "my-replicated-topic";
    
    public static void main(String[] args) throws ExecutionException,InterruptedException {
    Properties props = new Properties();
    props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"10.31.167.10:9092,10.31.167.10:9093,10.31.167.10:9094");
    //把发送的key从字符串序列化为字节数组
    props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG,StringSerializer.class.getName());
    //把发送消息value从字符串序列化为字节数组
    props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,StringSerializer.class.getName());
    
    Producer<String, String> producer = new KafkaProducer<String,String>(props);
    
    Order order = new Order((long) i, i);
    ProducerRecord<String, String> producerRecord = new ProducerRecord<String, String>(TOPIC_NAME, order.getOrderId().toString(), JSON.toJSONString(order));
    RecordMetadata metadata = producer.send(producerRecord).get();
    //=====阻塞=======
    System.out.println("同步方式发送消息结果:" + "topic-" +metadata.topic() + "|partition-"+ metadata.partition() + "|offset-" +metadata.offset());
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    或者:

    package com.li.kafka;
    
    import org.apache.kafka.clients.producer.*;
    import org.apache.kafka.common.serialization.StringSerializer;
    
    import java.util.Properties;
    import java.util.concurrent.ExecutionException;
    
    public class MySimpleProducer {
        // 创建主题
        public static final String TOPIC_NAME = "my-replicated-topic";
    
        public static void main(String[] args) throws ExecutionException, InterruptedException {
            // 1.设置参数
            // 创建broker
            Properties props = new Properties();
            // 只有一个IP则为单机有多个ip则为集群
            props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.49.128:9092,192.168.49.128:9093,192.168.49.128:9094");
    
            // 把发送的key从字符串序列化为字节数组
            props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
            // 把发消息value从字符串序列化为字节数组
            props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
    
            // 2.创建生产者消息的客户端,传入参数
            // 设置producer的键和值
            Producer<String, String> producer = new KafkaProducer<String, String>(props);
            // 3.创建消息
            // 创建记录 key: 作用是决定了往哪个分区上发,value:具体要发送的消息内容
            ProducerRecord<String, String> producerRecord = new ProducerRecord<String, String>(TOPIC_NAME, "myKey", "hello-kafka");
    
            // 4.发送消息,得到消息发送的元数据并输出
            RecordMetadata metadata = producer.send(producerRecord).get();
            System.out.println("同步方式发送消息结果:" + "topic-" + metadata.topic() + "|partion-"
                    + metadata.partition() + "|offset-" + metadata.offset());
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    3.发送消息到指定分区上

    ProducerRecord<String, String> producerRecord = new ProducerRecord<String, String>(TOPIC_NAME, 0 , order.getOrderId().toString(), JSON.toJSONString(order));
    
    • 1

    4.未指定分区,则会通过业务key的hash运算,算出消息往哪个分区上发

    //未指定发送分区,具体发送的分区计算公式:hash(key)%partitionNum
    ProducerRecord<String, String> producerRecord = new ProducerRecord<String, String>(TOPIC_NAME, order.getOrderId().toString(), JSON.toJSONString(order));
    
    • 1
    • 2

    5.同步发送

    生产者同步发消息,在收到kafka的ack告知发送成功之前一直处于阻塞状态

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BPcCkfRV-1653142857630)(Kafka.assets/1652970091092.png)]

    如果生产者发送消息没有收到ack,生产者会阻塞,阻塞到3s的时间,如果还没有收到消息,会进行重试。重试的次数3次。

    //等待消息发送成功的同步阻塞方法
    RecordMetadata metadata = producer.send(producerRecord).get();
    System.out.println("同步方式发送消息结果:" + "topic-" +metadata.topic() + "|partition-"+ metadata.partition() + "|offset-" +metadata.offset());
    
    • 1
    • 2
    • 3

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BbBPn20a-1653142857631)(Kafka.assets/1652954286972.png)]

    6.异步发消息

    生产者发消息,发送完后不用等待broker给回复,直接执行下面的业务逻辑。可以提供callback,让broker异步的调用callback,告知生产者,消息发送的结果

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-R2ofxjXZ-1653142857631)(Kafka.assets/1652970916967.png)]

    异步发送,生产者发送完消息后就可以执行之后的业务,broker在收到消息后异步调用生产者提供的callback回调方法

    //要发送 5 条消息
    Order order = new Order((long) i, i);
    //指定发送分区
    ProducerRecord<String, String> producerRecord = new ProducerRecord<String, String>(TOPIC_NAME, 0 , order.getOrderId().toString(),JSON.toJSONString(order));
    //异步回调方式发送消息
    producer.send(producerRecord, new Callback() {
    public void onCompletion(RecordMetadata metadata, Exception exception) {
    if (exception != null) {
        System.err.println("发送消息失败:" +
        exception.getStackTrace());
    }
    if (metadata != null) {
    System.out.println("异步方式发送消息结果:" + "topic-" +metadata.topic() + "|partition-"+ metadata.partition() + "|offset-" + metadata.offset());
             }
        }
    });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    7.关于生产者的ack参数配置

    在同步发送的前提下,生产者在获得集群返回的ack之前会一直阻塞。那么集群什么时候返回ack呢?此时ack有3个配置:

    在同步发消息的场景下:生产者发动broker上后,ack会有 3 种不同的选择:

    • ( 1 )acks=0: 表示producer不需要等待任何broker确认收到消息的回复,就可以继续发送下一条消息。性能最高,但是最容易丢消息。

    • ( 2 )acks=1: (默认)至少要等待leader已经成功将数据写入本地log,但是不需要等待所有follower是否成功写入。就可以继续发送下一条消息。这种情况下,如果follower没有成功备份数据,而此时leader又挂掉,则消息会丢失。

    • ( 3 )acks=-1或all: 需要等待 min.insync.replicas(默认为 1 ,推荐配置大于等于2) 这个参数配置的副本个数都成功写入日志,这种策略会保证只要有一个备份存活就不会丢失数据。这是最强的数据保证。一般除非是金融级别,或跟钱打交道的场景才会使用这种配置。(最安全但是性能最差)

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-e6WAwCCJ-1653142857632)(Kafka.assets/1653029532111.png)]

      下面是关于ack和重试(如果没有收到ack就开始重试)的配置

      // 发出消息持久化机制参数
              /*(1) acks=8:表示producer不需要等待任何broker确认收到消息的回复,就可以继摸发送下一条消息。性能最高,但是最容易丢消息。(2)geks=1:至少要等待leader已经成功将数据写入本地log,但是不需要等待所有follower是否成功写入。就可以继续发送下一
              条消息。这种情况下,如果follower没有成功备份数据,而此时leader又挂掉,则消息会丢失。
              (3) gck5=-1戏a1l1∶需要等待 min.in克uns..eplicas(默认为1,推荐配置大于等于2〕这个参数配置的副本个数都成功写入日志,这种策略会保证
                      只要有一个备份存活就不会丢失数据。这是最强的数据保证。一般除非是金融级别,或跟钱打交道的场景才会使用这种配置。
                      props.put(ProducerConfig.ACKS_CONFI6,"1");
                              发递失败会重试,默认重试间隔100ms,重试能保证消息发送的可靠性,但是也可能造成消息重复发送,比如网络抖动,所以需要在接收者那边做好消息接收的幂等性处理*/
              props.put(ProducerConfig.RETRIES_CONFIG, 3);
              // 重试间隔设置
              props.put(ProducerConfig.RETRY_BACKOFF_MS_CONFIG, 300);
              // 设置发送消息的本地缓冲区,如果设置了该缓冲区,消息会先发送到本地缓冲区,可以提高消息发送性能,默认值是335544632,即32MB
              // props.put(ProducerConfig .BUFFER_内E附DRY_CONFIG,33556432);
              /* kafka本地线程会从缓冲区取数据,批量发送到broker,
                  设置批量发送消息的大小,默认值是16384,即16kb,就是说一个batch满了16kb就发送出去云 */
            props.put(ProducerConfig.BATCH_SIZE_CONFIG, 16384);
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15

      关于消息发送的缓冲区

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zpi63hS8-1653142857632)(Kafka.assets/1653029670758.png)]

    • kafka默认会创建一个消息缓冲区,用来存放要发送的消息,缓冲区是32M

      props.put(ProducerConfig .BUFFER_内E附DRY_CONFIG,33556432);
      
      • 1
    • kafka本地线程会去缓冲区中一次拉16K的数据,发送到broker

      props.put(ProducerConfig.BATCH_SIZE_CONFIG, 16384);
      
      • 1
    • 如果线程拉不到16K的数据,间隔10ms也会将已拉到的数据发送到broker

    props.put(ProducerConfig.LINGER_MS_CONFIG, 10);
    
    • 1

    code:

    props.put(ProducerConfig.ACKS_CONFIG, "1");
    
    • 1

    8.其他一些细节

    • 发送会默认会重试 3 次,每次间隔100ms
    • 发送的消息会先进入到本地缓冲区(32mb),kakfa会跑一个线程,该线程去缓冲区中取16k的数据,发送到kafka,如果到 10 毫秒数据没取满16k,也会发送一次。

    七、消费者

    1.消费者消费消息的基本实现

    public class MyConsumer {
    private final static String TOPIC_NAME = "my-replicated-topic";
    private final static String CONSUMER_GROUP_NAME = "testGroup";
    
    public static void main(String[] args) {
    Properties props = new Properties();
    props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,"192.168.49.128:9092,192.168.49.128:9093,192.168.49.128:9094");
    // 消费分组名
    props.put(ConsumerConfig.GROUP_ID_CONFIG, CONSUMER_GROUP_NAME);
    props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG,StringDeserializer.class.getName());
    props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,StringDeserializer.class.getName());
    //创建一个消费者的客户端
    KafkaConsumer<String, String> consumer = new KafkaConsumer<String,String>(props);
    // 消费者订阅主题列表
    consumer.subscribe(Arrays.asList(TOPIC_NAME));
    
    while (true) {
    /*
    * poll() API 是拉取消息的⻓轮询
    */
    ConsumerRecords<String, String> records =consumer.poll(Duration.ofMillis( 1000 ));
    for (ConsumerRecord<String, String> record : records) {
    System.out.printf("收到消息:partition = %d,offset = %d, key =%s, value = %s%n", record.partition(),record.offset(), record.key(), record.value());
                }
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    2.自动提交offset

    • 设置自动提交参数 - 默认
    // 是否自动提交offset,默认就是true
    props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "true");
    // 自动提交offset的间隔时间
    props.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, "1000");
    
    • 1
    • 2
    • 3
    • 4

    消费者poll到消息后默认情况下,会自动向broker的_consumer_offsets主题提交当前主题-分区消费的偏移量。

    自动提交会丢消息: 因为如果消费者还没消费完poll下来的消息就自动提交了偏移量,那么此 时消费者挂了,于是下一个消费者会从已提交的offset的下一个位置开始消费消息。之前未被消费的消息就丢失掉了。

    3.手动提交offset

    • 设置手动提交参数

      在消息消费完之后提交,不需要等到集群ack,直接执行之后的逻辑,可以设置一个回调方法,供集群调用

    props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false");
    
    • 1

    在消费完消息后进行手动提交

    • 手动同步提交
    if (records.count() > 0 ) {
    // 手动同步提交offset,当前线程会阻塞直到offset提交成功
    // 一般使用同步提交,因为提交之后一般也没有什么逻辑代码了
    consumer.commitSync();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 手动异步提交
    if (records.count() > 0 ) {
    // 手动异步提交offset,当前线程提交offset不会阻塞,可以继续处理后面的程序逻辑
    consumer.commitAsync(new OffsetCommitCallback() {
    @Override
    public void onComplete(Map<TopicPartition, OffsetAndMetadata>offsets, Exception exception) {
                if (exception != null) {
                    System.err.println("Commit failed for " + offsets);
                    System.err.println("Commit failed exception: " +exception.getStackTrace());
                }
             }
        });
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    关于消费者自动提交和手动提交offset

    提交内容

    消费者无论是自动提交还是手动提交,都需要把所属的消费组+消费的某个主题+消费的某个分区及消费的偏移量,这样的信息提交到集群的_consumer_offset主题里面。

    4.消费者poll消息的过程

    • 消费者建立了与broker之间的⻓连接,开始poll消息。
    • 默认一次poll 500条消息
    props.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, 500 );
    
    • 1

    可以根据消费速度的快慢来设置,因为如果两次poll的时间如果超出了30s的时间间隔,kafka会认为其消费能力过弱,将其踢出消费组。将分区分配给其他消费者。

    可以通过这个值进行设置:

    props.put(ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG, 30 * 1000 );
    
    • 1

    如果每隔1s内没有poll到任何消息,则继续去poll消息,循环往复,直到poll到消息。如果超出了1s,则此次⻓轮询结束。

      • 如果一次poll到500条,就直接执行for循环
      • 如果这一次没有poll到500条,且在1秒内,那么长轮询继续poll,要么到500条,要么到1s
      • 如果多次poll都没达到500条,且1秒时间到了,那么直接执行for循环。
    • 如果两次poll的间隔都查超过30秒,集群会认为该消费者的消费能力过弱,该消费者被踢出消费组,触发rebalance机制,rebalance机制会造成开销。可以通过设置这个参数,让一次poll的消息条数少一点

    ConsumerRecords<String, String> records =consumer.poll(Duration.ofMillis( 1000 ));
    
    • 1

    消费者发送心跳的时间间隔

    props.put(ConsumerConfig.HEARTBEAT_INTERVAL_MS_CONFIG, 1000 );
    
    • 1

    消费者每隔1S向kafka集群发送心跳,集群发现如果超过10秒没有续约的消费者,将被踢出消费组,触发rebalance机制将该分区交给消费组里的其他消费者进行消费

    props.put(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG, 10 * 1000 );
    
    • 1

    5.指定分区消费

    consumer.assign(Arrays.asList(new TopicPartition(TOPIC_NAME, 0 )));
    
    • 1

    6.消息回溯消费

    consumer.assign(Arrays.asList(new TopicPartition(TOPIC_NAME, 0 )));
    consumer.seekToBeginning(Arrays.asList(new TopicPartition(TOPIC_NAME,0 )));
    
    • 1
    • 2

    7.指定offset消费

    consumer.assign(Arrays.asList(new TopicPartition(TOPIC_NAME, 0 )));
    consumer.seek(new TopicPartition(TOPIC_NAME, 0 ), 10 );
    
    • 1
    • 2

    8.从指定时间点消费

    根据时间,去所有的partition中确定该时间对应的offset,然后去所有的partition中找到offset之后的消息开始消费

    List<PartitionInfo> topicPartitions =consumer.partitionsFor(TOPIC_NAME);
    //从 1 小时前开始消费
    long fetchDataTime = new Date().getTime() - 1000 * 60 * 60 ;
    Map<TopicPartition, Long> map = new HashMap<>();
    for (PartitionInfo par : topicPartitions) {
        map.put(new TopicPartition(TOPIC_NAME, 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.getValue();
        if (key == null || value == null) continue;
        Long offset = value.offset();
        System.out.println("partition-" + key.partition() +"|offset-" + offset);
        System.out.println();
        //根据消费里的timestamp确定offset
        if (value != null) {
            consumer.assign(Arrays.asList(key));
            consumer.seek(key, offset);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    9.新消费组的消费偏移量

    当消费主题的是一个新的消费组,或者指定offset的消费方式,offset不存在,那么应该如何消费?

    新消费组中的消费者在启动以后,默认会从当前分区的最后一条消息的offset+1开始消费(消费新消息)。可以通过以下的设置,让新的消费者第一次从头开始消费。之后开始消费新消息(最后消费的位置的偏移量+1)

    • latest(默认) :只消费自己启动之后发送到主题的消息
    • earliest:第一次从头开始消费,以后按照消费offset记录继续消费,这个需要区别于consumer.seekToBeginning(每次都从头开始消费)
    props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
    
    • 1

    八 、Springboot中使用Kafka

    1.引入依赖

    <dependency>
        <groupId>org.springframework.kafka</groupId>
        <artifactId>spring-kafka</artifactId>
    </dependency>
    
    
    • 1
    • 2
    • 3
    • 4
    • 5

    2.配置文件

    server:
        port: 8080
    spring:
        kafka:
            bootstrap-servers: 172.16.253.21: 9093
            producer: # 生产者
                retries: 3 # 设置大于 0 的值,则客户端会将发送失败的记录重新发送
                batch-size: 16384
                buffer-memory: 33554432
                acks: 1
                # 指定消息key和消息体的编解码方式
                key-serializer: org.apache.kafka.common.serialization.StringSerializer
                value-serializer: org.apache.kafka.common.serialization.StringSerializer
            consumer:
                group-id: default-group
                enable-auto-commit: false
                auto-offset-reset: earliest
                key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
                value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
                max-poll-records: 500
            listener:
            # 当每一条记录被消费者监听器(ListenerConsumer)处理之后提交
            # RECORD
            # 当每一批poll()的数据被消费者监听器(ListenerConsumer)处理之后提交
            # BATCH
            # 当每一批poll()的数据被消费者监听器(ListenerConsumer)处理之后,距离上次提交时间大于TIME时提交
            # TIME
            # 当每一批poll()的数据被消费者监听器(ListenerConsumer)处理之后,被处理record数量大于等于COUNT时提交
            # COUNT
            # TIME | COUNT 有一个条件满足时提交
            # COUNT_TIME
            # 当每一批poll()的数据被消费者监听器(ListenerConsumer)处理之后, 手动调用Acknowledgment.acknowledge()后提交
            # MANUAL
            # 手动调用Acknowledgment.acknowledge()后立即提交,一般使用这种
            # MANUAL_IMMEDIATE
                ack-mode: MANUAL_IMMEDIATE
        redis:
            host: 172.16.253.21
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    3.消息生产者

    • 发送消息到指定topic
    @RestController
    public class KafkaController {
        private final static String TOPIC_NAME = "my-replicated-topic";
        @Autowired
        private KafkaTemplate<String, String> kafkaTemplate;
        @RequestMapping("/send")
        public void send() {
            kafkaTemplate.send(TOPIC_NAME, 0 , "key", "this is a msg");
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    4.消息消费者

    • 设置消费组,消费指定topic
    @KafkaListener(topics = "my-replicated-topic",groupId = "MyGroup1")
    public void listenGroup(ConsumerRecord<String, String> record,Acknowledgment ack) {
        String value = record.value();
        System.out.println(value);
        System.out.println(record);
        //手动提交offset
        ack.acknowledge();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 设置消费组、多topic、指定分区、指定偏移量消费及设置消费者个数。
    @KafkaListener(groupId = "testGroup", topicPartitions = {
    @TopicPartition(topic = "topic1", partitions = {"0", "1"}),
    @TopicPartition(topic = "topic2", partitions = "0",partitionOffsets = @PartitionOffset(partition = "1",initialOffset = "100"))}
    ,concurrency = "3")//concurrency就是同组下的消费者个数,就是并发消费数,建议小于等于分区总数
    public void listenGroup(ConsumerRecord<String, String> record,Acknowledgment ack) {
        String value = record.value();
        System.out.println(value);
        System.out.println(record);
        //手动提交offset
        ack.acknowledge();
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    九、Kafka集群Controller、Rebalance和HW

    1.Controller

    • Kafka集群中的broker在zk中创建临时序号节点,序号最小的节点(最先创建的节点)将作为集群的controller,负责管理整个集群中的所有分区和副本的状态:
      • 当某个分区的leader副本出现故障时,由控制器负责为该分区选举新的leader副本。
      • 当检测到某个分区的ISR集合发生变化时,由控制器负责通知所有broker更新其元数据信息。
      • 当使用kafka-topics.sh脚本为某个topic增加分区数量时,同样还是由控制器负责让新分区被其他节点感知到。

    每个broker启动时会向zk创建一个临时序号节点,获得的序号最小的哪个broker将会作为集群中的controlller,负责这么几件事

    • 当集群中有一个副本的leader挂掉,需要在集群中选举出一个新的leader,选举的规则是从isr集合中最左边获得
    • 当集群中有broker新增或减少,controller会同步信息给其他broker
    • 当集群中有分区新增或减少,controller会同步信息给其他broker

    2.Rebalance机制

    • 前提︰消费组中的消费者没有指明分区来消费

    • 触发的条件:当消费组中的消费者和分区的关系发生变化的时候

    • 分区分配的策略:在rebalance之前,分区怎么分配会有这么三种策略

      • range:根据公示计算得到每个消费消费哪几个分区︰前面的消费者是分区总数/消费者数量+1,之后的消费者是分区总数/消费者数量
      • 轮询:大家轮着来
      • sticky:粘合策略,如果需要rebalance,会在之前已分配的基础上调整,不会改变之前的分配情况。如果这个策略没有开,那么就要进行全部的重新分配。建议开启。

    前提是:消费者没有指明分区消费。当消费组里消费者和分区的关系发生变化,那么就会触发rebalance机制。

    这个机制会重新调整消费者消费哪个分区。

    在触发rebalance机制之前,消费者消费哪个分区有三种策略:

    • range:通过公示来计算某个消费者消费哪个分区
    • 轮询:大家轮着消费
    • sticky:在触发了rebalance后,在消费者消费的原分区不变的基础上进行调整。

    3.HW和LEO

    LEO是某个副本最后消息的消息位置(log-end-offset)
    HW是已完成同步的位置。消息在写入broker时,且每个broker完成这条消息的同步后,hw才会变化。在这之前消费者是消费不到这条消息的。在同步完成之后,HW更新之后,消费者才能消费到这条消息,这样的目的是防止消息的丢失。

    HW俗称高水位,HighWatermark的缩写,取一个partition对应的ISR中最小的LEO(log-end-offset)作为HW,consumer最多只能消费到HW所在的位置。另外每个replica都有HW,leader和follower各自负责更新自己的HW的状态。对于leader新写入的消息,consumer不能立刻消费,leader会等待该消息被所有ISR中的replicas同步后更新HW,此时消息才能被consumer消费。这样就保证了如果leader所在的broker失效,该消息仍然可以从新选举的leader中获取。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZSJmbfrv-1653142857633)(Kafka.assets/1653133967892.png)]

    十、Kafka线上问题优化

    1.如何防止消息丢失

    • 发送方: ack是 1 或者-1/all 可以防止消息丢失,如果要做到99.9999%,ack设成all,把min.insync.replicas配置成分区备份数
    • 消费方:把自动提交改为手动提交。

    也就是:

    • 生产者:

      • 使用同步发送
      • 把ack设成1或者all,并且设置同步的分区数>=2
    • 消费者:

      • 把自动提交改为手动提交

    2.如何防止消息的重复消费

    在防止消息丢失的方案中,如果生产者发送完消息后,因为网络抖动,没有收到ack,但实际上broker已经收到了。此时生产者会进行重试,于是broker就会收到多条相同的消息,而造成消费者的重复消费。
    怎么解决:

    • 生产者关闭重试: 会造成丢消息(不建议)

    • 消费者解决非幂等性消费问题:
      所谓的幂等性: 多次访问的结果是一样的。对于rest的请求(get(幂等) 、post(非幂等)、put(幂等) 、delete (幂等))

      解决方案:

      • 在数据库中创建联合主键,防止相同的主键创造出多条记录
      • 使用分布式锁,以业务id为锁,保证只有一条记录能够创建成功

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-njZNAcAZ-1653142857633)(Kafka.assets/1653139321633.png)]

    一条消息被消费者消费多次。如果为了消息的不重复消费,而把生产端的重试机制关闭、消费端的手动提交改成自动提交,这样反而会出现消息丢失,那么可以直接在防治消息丢失的手段上再加上消费消息时的幂等性保证,就能解决消息的重复消费问题。

    幂等性如何保证:

    • mysql 插入业务id作为主键,主键是唯一的,所以一次只能插入一条
    • 使用redis或zk的分布式锁(主流的方案)

    3.如何做到顺序消费RocketMQ

    • 发送方:在发送时将ack不能设置 0 ,关闭重试,使用同步发送,等到发送成功再发送下一条。确保消息是顺序发送的。
    • 接收方:消息是发送到一个分区中,只能有一个消费组的消费者来接收消息。因此,kafka的顺序消费会牺牲掉性能。
    • 生产者:保证消息按顺序消费,且消息不丢失――使用同步的发送,ack设置成非O的值。
    • 消费者:主题只能设置一个分区,消费组中只能有一个消费者

    4.解决消息积压问题

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DKWHpA9p-1653142857634)(Kafka.assets/1653140377710.png)]

    消息积压问题的出现原因:

    消息的消费者的消费速度远赶不上生产者的生产消息的速度,导致kafka中有大量的数据没有被消费。随着没有被消费的数据堆积越多消费者寻址的性能会越来越差,最后导致整个kafka对外提供的服务的性能很差,从而造成其他服务也访问速度变慢,造成服务雪崩。

    消息积压会导致很多问题,比如磁盘被打满、生产端发消息导致kafka性能过慢,就容易出现服务雪崩,就需要有相应的手段:

    • 方案一:在一个消费者中启动多个线程,让多个线程同时消费。——提升一个消费者的消费能力(增加分区增加消费者)。
    • 方案二:如果方案一还不够的话,这个时候可以启动多个消费者,多个消费者部署在不同的服务器上。其实多个消费者部署在同一服务器上也可以提高消费能力——充分利用服务器的cpu资源。
    • 方案三:让一个消费者去把收到的消息往另外一个topic上发,另一个topic设置多个分区和多个消费者 ,进行具体的业务消费。
    • 在这个消费者中,使用多线程,充分利用机器的性能进行消费消息。

    • 通过业务的架构设计,提升业务层面消费的性能

    • 创建多个消费组,多个消费者,部署到其他机器上,一起消费,提高消费者的消费速度

    • 创建一个消费者,该消费者在kafka另建一个主题,配上多个分区,多个分区再配上多个消费者。该消费者将polI下来的消息,不进行消费,直接转发到新建的主题上。此时,新的主题的多个分区的多个消费者就开始一起消费了。 ——不常用

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SHPF1Jfv-1653142857634)(Kafka.assets/1653141013871.png)]

    5.延迟队列

    延迟队列的应用场景:在订单创建成功后如果超过 30 分钟没有付款,则需要取消订单,此时可用延时队列来实现

    • 创建多个topic,每个topic表示延时的间隔

      • topic_5s: 延时5s执行的队列
      • topic_1m: 延时 1 分钟执行的队列
      • topic_30m: 延时 30 分钟执行的队列
    • 消息发送者发送消息到相应的topic,并带上消息的发送时间

    • 消费者订阅相应的topic,消费时轮询消费整个topic中的消息

      • 如果消息的发送时间,和消费的当前时间超过预设的值,比如 30 分钟
      • 如果消息的发送时间,和消费的当前时间没有超过预设的值,则不消费当前的offset及之后的offset的所有消息都消费
      • 下次继续消费该offset处的消息,判断时间是否已满足预设值
    • kafka中创建创建相应的主题
    • 消费者消费该主题的消息(轮询)
    • 消费者消费消息时判断消息的创建时间和当前时间是否超过30分钟(前提是订单没支付)
      • 如果是:去数据库中修改订单状态为已取消
      • 如果否︰记录当前消息的offset,并不再继续消费之后的消息。等待1分钟后,再次向kafka拉取该offset及之后的消息,继续进行
        判断,以此反复。

    十一、Kafka-eagle监控平台

    安装Kafka-eagle

    官网下载压缩包

    http://www.kafka-eagle.org/

    • 安装jdk
    • 解压缩后修改配置文件 system-config.properties
    # 配置zk  去掉cluster2
    efak.zk.cluster.alias=cluster1
    cluster1.zk.list=172.16.253.35:2181
    # cluster2.zk.list=xdn10:2181,xdn11:2181,xdn12:2181
    
    # 配置mysql
    kafka.eagle.driver=com.mysql.cj.jdbc.Driver
    kafka.eagle.url=jdbc:mysql://172.16.253.22:3306/ke?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull
    kafka.eagle.username=root
    kafka.eagle.password= 123456
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 修改/etc/profile
    export  JAVA_HOME=/usr/local/jdk/jdk1.8.0_191
    CLASSPATH=.:$JAVA_HOME/lib/tools.jar:$JAVA_HOME/lib/dt.jar
    export KE_HOME=/home/aisys/efak-web-2.0.9
    export PATH=$PATH:$KE_HOME/bin:$JAVA_HOME/bin
    
    • 1
    • 2
    • 3
    • 4
    • 刷新配置
    source /etc/profile
    
    • 1
    • 进入到bin目录,为ke.sh增加可执行的权限
    chmod +x ke.sh
    
    • 1
    • 启动kafka-eagle
    ./ke.sh start
    
    • 1
  • 相关阅读:
    第六章 :Spring Boot web开发常用注解(一)
    Go语言 02
    每日一题:【LeetCode】349.两个数组的交集
    Abnova丨CMV CISH 探头解决方案
    损失函数-交叉熵的原理及实现
    差分隐私(Differential Privacy)定义及其理解
    机器学习-数值特征
    maven(一):是否有必要使用maven
    C语言之字符串函数二
    20道常见的kafka面试题以及答案
  • 原文地址:https://blog.csdn.net/m0_64892750/article/details/124904426