分布式消息系统Kafka

1,362 阅读14分钟

概述

简介

Apache Kafka是一个快速、可扩展的、高吞吐的、可容错的分布式“发布-订阅”消息系统,使用ScalaJava语言编写,能够将消息从一个端点传递到零一个端点,较之传统的消息中间件(例如ActiveMQRabbitMQ),kafka具有高吞吐、内置分区、支持消息副本和高容错的特性,非常适合大规模消息处理应用程序。Kafka官网:kafka.apache.org/

系统架构

Kafka的系统架构如下图所示

image.png kafka架构包括若干个Producer,若干个Brokerkafka支持水平扩展,一般broker数量越多集群的吞吐量越大),若干个consumer group,一个Zookeeper集群(kafka通过Zookeeper管理集群配置,负责Broker Controller的选举)。

应用场景

用户活动追踪

用户在网站的活动(网页浏览、搜索或者其他的操作信息)发布到不同的话题中心,这些信息可实时处理,实时监测,也可加载到Hadoop或者离线处理数据仓库,这是用户画像的一种实现方式。

日志聚合

如下图所示

image.png

简单理解就是:日志采集客户端中会将日志信息按照日志的类别定时写入到Kafka消息队列的不同主题中,日志处理应用作为日志的消费者订阅Kafka消息队列中对应日志的主题进行消费。

限流削峰

Kafka可以作为服务降级的埋点,让消息写进Kafka,达到限流的目的,比如秒杀系统,团抢活动等,当请求并发量非常大的时候起到服务降级的作用,但是Kafka的服务降级与dubbo或者SpringCloud不一样(dubboSpringCloud的服务降级是通过兜底方法,返回的假数据的方式实现)。如下图所示

image.png

Kafka高吞吐率实现

Kafka与其他的MQ相比,其最大的特点就是高吞吐量,为了增加存储能力,kafka将所有的消息都写入到低速大容的硬盘,按理说,这将导致性能损失,但实际上,kafka仍可保持超高的吞吐率,性能并未受到影响,其主要采用了图下的方式实现了高吞吐率。

  • 顺序读写:kafka是将消息即数据写入到partition分区中,分区中的消息是顺序读写的,在磁盘是也是顺序存放的,即存放的位置是连续的,即读取的时候硬盘的磁头不需要寻道了,所以就快。

  • 零拷贝:使用到了sendfile函数实现零拷贝,可以参考:深入理解零拷贝

  • 批量发送:一般来说,消息生产者可以将消息批量发送到kafka缓存中,即内存中,可以设置算法或者比如缓存满了就将消息写入到kafka磁盘中。其实发送的策略可以设置,例如定时的方法。

  • 消息压缩:比如消息比较大的时候可以将消息进行压缩,即生产者将消息进行压缩,然后消息者消费消息的时候再进行解压。

入门操作

集群搭建

在生产环境中为了防止单点问题,Kafka都是以集群的方式出现的,下面搭建一个Kafka集群,包含三个Kafka主机,即三个Broker

image.png

  • 将下载的Kafka压缩文件进行解压,如下图所示

image.png

  • 创建软连接(为了方便操作,不是必须操作),如下图所示

image.png

  • 修改配置文件,在kafka安装目录下有一个config/server.properties文件,修改此文件,

image.png

image.png

image.png

主要配置参数说明如下:

  • broker.id=x 表示当前主机在kafka集群中的唯一标识。

  • listeners表示kafka之间进行通讯的地址,配置本机的IP地址,即哪个虚拟机上面安装kafka就配置该虚拟机的IP地址,注意不要写localhost

  • Log.dirs表示日志文件即消息存放的路径,如果不存在该目录,kafka会自动帮我们创建。

  • Num.partitions=1表示一个topic默认一个分区(除非指定,不指定就会用默认的)。

  • Zookeeper.connect表示zookeeper的连接地址。

同理在另外两台主机上也是如上述步骤配置,只是broker.idlisteners不一样而已。

基本命令

启动与停止

1.首先启动Zookeeper

image.png

2.启动Kafka。(注意如果加上-daemon的方式表示以守护进程的方式启动,不会占用当前窗口)

image.png

3.停止Kafka

image.png

Kafka操作

1.创建topic

image.png

  • 创建主题时,不能使用localhost,因为zookeeper不认识localhost,必须使用ip地址,之后zookeeper会自动分配给你指定连接的主机

  • partition中的值如果是1份(--partitions参数指定),并且副本也是1份(--replication-factor参数指定),表示kafka中一共只有一份partition,即副本就是partition

  • 创建主题test--topic参数指定)之后去/tmp/kafka-logs中查看,发现test-0文件夹就是partition,该文件夹下面的.log文件就是segment

  • 创建主题时,一般设置副本的数量与broker的数量一致。

2.查看topic

image.png

注意:_consumer_offsets跟偏移量offsets相关的主题,该主题一共有50partition

3.发送消息

image.png 上面的命令会创建一个生产者,然后由其生产信息

4.消费消息

image.png 启动消费者时,如果不加--from beginning,就不能获取消费者启动之前生产者发送的消息。

5.继续生产消费

image.png

image.png

6.删除topic

image.png

基本概念

topic

topic,主题。在kafka中,使用一个类别属性来划分消息的所属类,划分消息的这个类称为topic,相当于消息的分类标签,是一个逻辑概念,一个topicpartition数量一般设置为broker的整数倍。

partition

分区。topic中的消息被分割为一个或者多个partitionpartition是一个物理概念,对应到系统上面就是一个或者若干个目录。生产者发送的消息可能存在多个分区中,但是注意消息不是在多个分区中放置多份,而是多个分区中的数据组成了生产者的消息。如果说消息是存放在一个分区的话,消费者消费时能保证跟生产者发送消息的顺序是一样的,但是消息如果存放在多个分区就无法保证消费者消费时能跟生产者发送消息的顺序是一样的

疑问:怎么保证消费者读取消息的顺序跟生产者发送消息的顺序一致?

可以只设置一个partition,即把所有的消息都放在一个partition中,partition是一个先进先出的队列,就能保证消费者读取消息的顺序与生产者发送消息的顺序一致,如果是存放在多个partition中,那就无法保证消费者消费消息的顺序与生产者发送消息的顺序一致。

Segment

段。将partition进一步细分成了segment,每个segment文件大小相等。segment可以理解成是partition目录中的文件,segment是为了在partition中管理消息的,比如需要删除消息时,只需要删除对应的segment文件即可,删除文件比较快,另外partition的连续存储体现在segment文件在磁盘空间中存储的顺序是连续的,segment文件内部存放消息的顺序也是连续的,所以kafka很快。

Broker

Kafka集群包含一个或者多个服务器,每一个服务器节点称为一个broker。假设有NpartitionMbroker

  • N > M,且NM的整数倍,每个broker会平均存储partition

  • N > M,但N不是M的整数倍,此时会出现每个broker上分配的

  • N < M,则会出现有的broker中没有分配partition的情况

假设一个topic8patitionkafka集群一共有4broker,那么每个broker中就会存放2partitionkafka自动会这样分配的。

Producer

生产者。即消息的发布者,其会将某topic的消息发布到相应的partition中。注意生产者一般指的是我们开发的代码。

Consumer

消费者。可以从broker中读取消息。一个消费者可以消费多个topic的消息,但是同一个topic中的同一个partition只能被相同消费者组中的一个consumer消费。

Replicas of partition

分区副本。副本是一个分区的备份,是为了防止数据丢失而创建的备份。

Parition Leader

每个partition有多个副本,其中有且仅有一个作为LeaderLeader是当前负责消息读写的分区,即所有读写操作只能发生在Leader分区上。

如果partitionLeader挂了的话,即宕机,重新从ISR队列中选举,选举过程很简单,不像zookeeper那么复杂,它是直接选队列的头元素作为新的Leader

Parition Follower

所有的Follower都需要从Leader同步消息,FollowerLeader始终保持消息同步,这些Follower都会保存在由Leader负责维护的ISR列表中。

ISR (IN-Sync Replicas)

分区同步列表,每一个Leader对应的Follower列表,ISR列表中的Follower如果同步副本Leader中消息慢了或者Follower挂了,这时Leader会将该FollowerISR列表中剔除,删除的Follower会进入OSR中。

OSR(Outof-Sync Replicas)

由于有些Follower因为某种原因,例如同步消息很慢,被LeaderISR中剔除就会进入到OSR中。

AR(Assigned Replicas)

AR为开始初始化时的ISR,也就是还没有剔除对应的FollwerOSR为被剔除的Follwer列表,所以AR=ISR+OSRISR存放在zookeeper中。

Partition offset

分区偏移量。当consumerpartition中消费了若干消息后,consumer会将这些消费的消息中的最大偏移量提交给broker(从kafka0.9开始offset的管理与保存机制发生了很大的变化——zookeeper不再保存和管理offset了,offsetbroker自己管理,不再由zookeeper管理),表示当前partition已经消费到了该offset所标识的消息。

Zookeeper

Zookeeper负责维护和协调broker,当然,还负责Broker Controller的选举,需要注意,从kafka0.9开始offset的管理与保存机制发生了很大的变化——zookeeper不在保存和管理offset了。

HW与LEO

  • HW,高水位,表示当前consumer可以消费的最高partition偏移量,HW保证了kafka集群中消息的一致性。

  • LEO,日志最后消息的偏移量,消息在kafka中是被写入到日志文件中的,这是最后一个消息在partition中的偏移量。

  • 对于leader新写入的消息,consumer是不能立刻消费的,Leader会等待消息被所有ISR中的partition follower同步后才更新HW,将HW写入到ISR中,此时消息才能被consumer消费。

如下图所示,假设对于partition A此时LeaderFollower中的数据一致,包含三条消息。此时HWLEO如下图所示

image.png

然后生产者往partition A中发布编号为4,5两条消息,我们知道Kafka中对于partition的读写操作都是发生在Leader中的,所以4,5两条消息会写进partition ALeader中。此时HWLEO如下图所示

image.png

接着partition A的两个Follower会同步编号为4,5两条消息,假设Follower1的同步能力比Follower2的强,Follower1此时同步了编号为4,5两条消息,但是Follower2只同步到了编号为4这条消息。此时HWLEO如下图所示

image.png

最后Follower2同步编号为5这条消息。此时HWLEO如下图所示

image.png

注意:消费者只能消费partition分区中HW(而不是LEO)指向的位置以及之前的消息。

Consumer Group

Consumer groupkafka提供的可扩展并且具有容错性的消费者机制,组内可以有多个消费者,它们共享一个公共的ID,即group ID,组内的所有消费者协调在一起订阅主题的所有分区。

同一个topic主题的同一个partiton只能由同一个消费者组中的一个consumer消费。

对于topic中的partition在消费者组中的consumer的分配情况如下:

  • 1.当消费者组中只有一个consumer时,如下图所示

image.png

  • 2.当partition数量为4,并且consumer的数量为2时,Kafka会自动进行平均消费,即消费者组中的每个consumer随机消费两个partition,如下图所示

image.png

  • 2.当partition数量为4,并且consumer的数量为4时,Kafka会自动进行平均消费,即消费者组中的每个consumer随机消费一个partition,如下图所示 image.png

  • 3.当partition的数量小于consumer的数量时,消费者组有些consumer就不会消费该topicpartition,如下图所示

image.png

  • 4.当出现两个消费者组时,可以发现同一个topic中的同一个partition只能被同一个消费者组中的一个consumer消费,但是同一个topic中的同一个partition可以被不同消费者组中的不同consumer消费。如下图所示

image.png

Coordinator

Coordinator一般指的是运行在每个broker上的group Coordinator,用于管理Consumer Group中的各个成员,主要用于offset位移管理和Rebalance,可以同时管理多个消费者组。

Rebalance

当消费者组中的消费者数量发生变化,或者topic中的partition数量发生变化时,partition的消费所有权会在消费者之间转移,即partition会重新分配,这个过程称为Rebalance

Rebalance期间消费者组时不可用的,即不能消费,所以reblance应该尽量避免,否则可用性就下降了(Rebalance是重点)。

如果broker挂了,partition数量不会改变,因为在其他的broker中还有挂了的broker上面的partition的副本,所以partition数量不会改变。

offset commit

Consumer在消费过消息之后需要将其消费的消息的offset提交给broker,以让broker记录下哪些消息是消费过的,纪录已消费过的offset值有什么作用呢?处理标识哪些消息将来要被删除外,还有一个很重要的作用:在发生在均衡Rebalance时不会引发消息的丢失或者重复消费。

原理过程

消息路由规则

消息写入到哪一个partition并不是随机的,而是有路由策略的。

  • 若指定了partition,就将消息写入到指定的partition.
  • 若没有指定partition,指定了keykeyhash值与partition的数量的取模,取模的值就是partition的索引。
  • 若也没有指定key,则会根据轮询算法选出partition

消息写入算法

消息的发送者将消息发送给broker,并形成最终可消费的log,是一个比较复杂的过程。

  • producer将消息发送给对应partition Leader
  • Partition Leader将消息写入到本地log
  • ISR中的FollowerLeader最中同步消息到本地log,同步完成后向Leader发送ACK
  • Leader收到所有的FollowerACK之后,增加HW,并向producer发送ACK

HW截断机制

如果partition leader接收到了新的消息,其他ISR中的Follower正在同步过程中,还未同步完毕时Leader宕机了,此时需要重新选举出新的Leader,若没有HW截断机制,将会导致partition中的offset数据不一致,为了避免这种情况发生,引入了HW的截断机制。HW截断机制原理演示如下:

  • 1.首先Partition Leader中的消息已经更新到消息6,此时Follower B同步到了消息5Follower C同步到了消息4,此时HW(即消费者可消费的最大偏移量)为消息4的位置,如下图所示

image.png

  • 2.假设此时Partition Leader所在的broker宕机了,如下图所示

image.png

  • 3.接着就会在BC之间进行Partition Leader的选举,假设此时B当选为新的Partition Leader,如下图所示

image.png

  • 4.接着Leader B接收到生产者得消息6注意此时得消息6跟之前A中得消息6是不一样的)和7,如下图所示

image.png

  • 5.然后假设此时A又恢复过来了,A只能作为Follower去同步Leader B中的消息,当没有HW截断机制时,A只会同步消息7因为宕机之前ALEO处于消息6的位置)。就会导致A中的数据跟Leader B中的数据不一致。

image.png

有了截断机制之后,当A恢复过来作为Follower去同步Leader B中的消息时,会将它宕机之前的HW之后的数据丢弃,重置LEO到宕机之前HW的位置,即消息4的位置(宕机之前HW在消息4,那么A就会将宕机之前的消息5和消息6丢弃),重新同步Leader B中的,就不会导致A中的数据跟Leader B中的数据不一致。如下图所示

image.png

image.png

消息发送可靠性

生产者向kafka发送消息时,可以选择消息发送的可靠性级别,通过request.required.acks参数的值进行设置。

  • 0:异步发送,生产者向kafka发送消息,kafka不需要向生产者反馈成功ACK,但是可能会存在消息丢失的问题。

    • kafka根本没有收到消息。
    • Kafka的缓存满了,正在向partition中写入消息时,这时生产者发送过来的消息就会丢失。
    • kafka写入消息顺序可能跟生产者发送消息的顺序不一致。因为比如生产者先发送了A消息,然后发送了B消息,但是因为网络原因,B消息比A消息先到达kafka,那么kafka就先写入B消息,然后再写入A消息。
  • 1:同步发送。生产者向kafka发送消息,brokerpartition Leader在收到消息后马上发送成功ack(无需等待ISR中的Follower同步),生产者如果一直没有收到kafkaack,则认为消息发送失败,会自动重发消息。

  • -1:同步发送,生产者发送消息给kafkakafka收到消息后要等ISR列表种的所有副本同步消息完成后,才想生产者发送成功ack,生产者如果一直没有收到kafkaack,则认为消息发送失败,会自动重发消息。但是可能存在消息重复接收的问题。可能存在消息重复接收的问题,比如生产者发送了5条消息给LeaderLeader已经把3条消息同步给了ISR列表中的Follower,但是这时Leader宕机了,因为消息都没同步完成,这时就不会给生产者回复ACK,所以生产者会认为这次的消息发送失败,又会重新发送那5条消息,Kafka就会出现消息重复接收的问题,因为已经接收了5条中的3条了。

消费者消费消息过程

  • 消费者订阅指定topic的消息。
  • broker controller会为消费者分配partition,并将该partition的当前offset发送给消费者。
  • 消费者接收到broker推送的消息后对消息进行消费。
  • 当消费者消费完消息后,消费者会向broker发送一个该消息已被消费的反馈。
  • broker接收到消费者的反馈后,broker会更新partition中的offset

Partition Leader选举范围

leader宕机后broker controller会从ISR中选一个Follower成为新的Leader,若ISR中的所有副本都宕机怎么办?可以通过unclean.leader.election.enable的取值来设置Leader选举的范围。

  • false:必须等待ISR列表种等待所有副本活过来才进行新的选举,该策略可靠性有保证,但可用性低。

  • true:可以选择任何一个没有宕机的Follower,但该Follower可能不在ISR中如果ISR中的某一个Follower同步Leader数据比较慢或者宕机了,就会从ISR剔除,进入到OSR中,所以true值表示该主机可能是之前因为同步数据慢被从ISR列表剔除的主机。如果选择该主机为Leader会存在安全隐患,因为该主机存在案底【可能该主机与Follower进行通信会有问题】,例如出现跟ISR中的Follower不能通讯,它就会把该FollowerISR中剔除,但是实际上是它自己的问题。

怎么才能从OSR中变到ISR中, Broker Controller会定期检查OSR中的主机,进行判断,选择没有问题的Follower进入ISR

重复消息问题及解决方案

最常见的重复消费有两种如下:

  • 同一个consumer重复消费:consumer本身没有消费完消息,所以也没有发送offset,比如这个consumer的消费能力比较低,例如从partition中拉取了5条消息,但是在自动提交时间offset超时时间auto.commit.interval.ms它只能消费3条消息,这时它会给broker中的partition回复异常信息,而不是回复offsetbroker不会认为该consumer宕机,因为可以发送异常信息给broker,下回进行消费时,还会从之前的offset中拉取5条消息进行拉取消息消费,所以出现重复消费问题。

  • 不同的consumer重复消费:consumer消费完了消息,也自动发送了offset,但是由于网路原因partition没有收到offset,比如因为网络原因,consumer没有把offset发送给partition,并且心跳也发不过去,这时可能该consumer会被认为是已经宕机了,这时会出现前面学过的rebalance即重新分配,别的consumer会从之前宕机的consumer最开始的offset出开始消费(因为broker没有接收到之前的被认为是宕机的consumeroffset)。

无论哪种重复消费,其解决方案都一样,有两种:

  • 增加会话超时时限session.timeout.ms的值,延长offset提交时间。
  • 设置enable.auto.commitfalse,将kafka自动提交offset改成手动提交。

日志查看

查看段segment

segment文件名

segment是一个逻辑概念,其由两类物理文件组成,分别是 .index文件和 .log文件,.log文件中存放的是消息内容,而.index文件中存放的是.log文件中消息的索引。这两个文件的文件名都是成对出现的,即会出现相同文件名的.log.index文件,文件名由20位数字字符组成,其要表示一个64位长度的数值(264次方是一个长度为20的数字),但作为文件名,其数值长度不足20位的全部用0填补。

每一个segment中包含很多消息,每一个partition中包含很多个个segement文件,segment中的log文件名的数字表示该segment中的消息前面还有多少个消息00000000000000000001.log表示前面有一个消息

  • 0segment文件

    00000000000000000000.index

    00000000000000000000.log

  • 170210segmen文件

    00000000000000170210.index

    00000000000000170210.log

segment文件内容

image.png

消息查找

partition中如何通过offset查找消息的呢?以查找offset170213的消息为例进行分析。

首先拿到170213这个offset通过二分法在所有的segment文件名中查找对应文件,当定位到00000000000000170210.log文件后,再进行减法运算:170213-170210=3,然后再在该index文件中定位到2号(编号从0号开始),查找到其偏移地址为348,最后在当前的log文件中直接定位到348地址处即可找到该消息。

image.png

查看segment

对于segment中的log文件,不能直接通过cat命令查看其内容,需要通过kafka自带的一个工具查看

bin/kafka-run-class.sh kafka.tools.DumpLogSegment --files /tmp/kafka-logs/test-0/00000000000000000000.log --print-data-log

该命令中的kafka-run-class.sh表示要运行一个指定的代码,DumpLogSegments就是一个专门用于查看消息日志文件工具

image.png 从以上输出结果可以看出,一共有三条消息元数据,每条元数据的格式都是相同的,参数payload表示消息内容。

查看offset存储目录

查看存储目录

前面我们讲过,消费者回向一个称为_consumer_offset的特殊主题发送消息,消息里包括每个分区的偏移量,通过这种方式提交offset,既然是主题,其就会有对应的分区,这个特殊的主题默认包括50个分区,这50个分区分别存放在所有的broker中。

image.png

任意打开一个_consumer_offsets-数字目录,均可以看到其中的segment

image.png

offset存放分区计算

每一个用户自定义主题的offset都会存放在同一个_consumer_offsets-数字目录中,那么存放在哪个目录呢?其计算方式是topic主题字符串的hash值与50取模的结果即为-consumer_offsets-数字目录的数字

Kafka API

准备工作

1.首先通过命令行创建一个名称为cities的主题。


bin/kafka-topics.sh --create --bootstrap-server 192.168.59.151:9092 --replication-factor 2 --partition 3 --topic cities

2.创建订阅者

bin/kafka-console-consumer.sh --bootstrap-server 192.168.59.151:9092  --topic cities --from beginning

Kafka原生API

创建工程

创建一个mavenjava工程,命名为kafkaDemo,创建时无需导入依赖。为了简单,后面的生产者与消费者均创建在该工程中。

image.png

引入依赖

pom.xml配置文件中引入依赖如下:

<!-- kafka依赖 -->
<dependency>
  <groupId>org.apache.kafka</groupId>
  <artifactId>kafka_2.12</artifactId>
  <version>1.1.1</version>
</dependency>

生产者

1.没有回调的生产者代码如下:

public class OneProducer {
    // 第一个泛型为key的类型,第二个泛型为消息本身的类型
    private KafkaProducer<Integer, String> producer;

    public OneProducer() {
        Properties properties = new Properties();
        //kafka集群,其实这里写集群中的一个kafka节点也可以,只要能连接上kafka集群中的节点
        properties.put("bootstrap.servers",
                       "192.168.237.139:9092,192.168.237.140:9092,192.168.237.140:9092");
        //键的序列化器
        properties.put("key.serializer",
                          "org.apache.kafka.common.serialization.IntegerSerializer");
        //值的序列化器
        properties.put("value.serializer",
                          "org.apache.kafka.common.serialization.StringSerializer");

        this.producer = new KafkaProducer<Integer, String>(properties);
    }

    public void sendMsg() {
        // 创建记录(消息)
        // 参考消息路由算法
        // 指定主题及消息本身 第一个参数为消息主题,第二个参数为消息,partition与key都不指定,默认使用轮询写入对应的partition
        // ProducerRecord<Integer, String> record =
        //                        new ProducerRecord<>("cities", "shanghai");
        //
        // 指定主题、key,及消息本身 第一个参数为消息主题,第二个参数为key(key的hash值与partition数量取模得出消息路由到哪一个partition),第三个参数为消息
        // ProducerRecord<Integer, String> record =
        //                        new ProducerRecord<>("cities", 1, "shanghai");
        // 指定主题、要写入的patition、key,及消息本身,
        ProducerRecord<Integer, String> record =
                                  new ProducerRecord<>("four", 0, 1, "beijing");

        // 发布消息,其返回值为Future对象,表示其发送过程为异步,不过这里不使用该返回结果
        // Future<RecordMetadata> future = producer.send(record);

        //这种发送有一个问题就是发送端不知道是否已经成功了
        producer.send(record);
    }
}

测试代码如下:

public class OneProducerTest {

    public static void main(String[] args) throws IOException {
        OneProducer producer = new OneProducer();
        producer.sendMsg();
        /**
         * 因为发送消息producer.sendMsg()过程是一个异步过程,
         * 如果不加下面的代码,就会出现主线程先结束,
         * 那就无法发送消息了
         *
         *
         * producer.sendMsg();
         */
        System.in.read();
    }
}

2.我们知道生产者发送消息的没有回调逻辑的话,肯本不知道自己发送消息是否成功,于是加上回调逻辑,代码如下:

public class TwoProducer {
    // 第一个泛型为key的类型,第二个泛型为消息本身的类型
    private KafkaProducer<Integer, String> producer;

    public TwoProducer() {
        Properties properties = new Properties();
        properties.put("bootstrap.servers",
                       "kafkaOS1:9092,kafkaOS2:9092,kafkaOS3:9092");
        properties.put("key.serializer",
                          "org.apache.kafka.common.serialization.IntegerSerializer");
        properties.put("value.serializer",
                          "org.apache.kafka.common.serialization.StringSerializer");

        this.producer = new KafkaProducer<Integer, String>(properties);
    }

    public void sendMsg() {
        // 创建记录(消息)
        // 指定主题及消息本身
        // ProducerRecord<Integer, String> record =
        //                        new ProducerRecord<>("cities", "shanghai");
        // 指定主题、key,及消息本身
        // ProducerRecord<Integer, String> record =
        //                        new ProducerRecord<>("cities", 1, "shanghai");
        // 指定主题、要写入的patition、key,及消息本身
        ProducerRecord<Integer, String> record =
                                  new ProducerRecord<>("cities", 0, 1, "shanghai");

        // 可以调用以下两个参数的send()方法,可以在消息发布成功后触发回调的执行
        producer.send(record, new Callback() {
            // RecordMetadata,消息元数据,即主题、消息的key、消息本身等的封装对象
            @Override
            public void onCompletion(RecordMetadata metadata, Exception exception) {
                System.out.print("partition = " + metadata.partition());
                System.out.print(",topic = " + metadata.topic());
                System.out.println(",offset = " + metadata.offset());
            }
        });

    }
}

测试代码如下:

public class TwoProducerTest {

    public static void main(String[] args) throws IOException {
        TwoProducer producer = new TwoProducer();
        producer.sendMsg();
        System.in.read();
    }
}

3.以上两种均每次只能发送一条消息,批量发送消息代码如下:

/**
 * 测试批量发送消息
 */
public class SomeProducerBatch {
    // 第一个泛型为key的类型,第二个泛型为消息本身的类型
    private KafkaProducer<Integer, String> producer;

    public SomeProducerBatch() {
        Properties properties = new Properties();
        properties.put("bootstrap.servers", "kafkaOS1:9092,kafkaOS2:9092,kafkaOS3:9092");
        properties.put("key.serializer", "org.apache.kafka.common.serialization.IntegerSerializer");
        properties.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");

        //======================批量发送消息的核心在这==============
        // 指定要批量发送的消息个数,默认16k个
        properties.put("batch.size", 16384);  // 16K
        // 指定积攒消息的时长,默认值为0ms 积攒ms就会批量发送一次消息
        properties.put("linger.ms", 50);  // 50ms
        //注意:上面两个条件都指定了,只要满足其中一个就会批量发送一次消息(谁先到按谁算)
        //======================批量发送消息的核心上面两个参数的设定=============

        this.producer = new KafkaProducer<Integer, String>(properties);
    }

    /**
     * 下面的发送满足的是上面50ms那个条件(而不是消息个数达到16384个),即每个50毫秒就会批量发送一次消息
     */
    public void sendMsg() {
        //只是加了一个循环,其实批量发送消息的核心在上面的两个设值处
        //注意下面满足的是ms批量发送
        for (int i=0; i<50; i++) {
            ProducerRecord<Integer, String> record =
                    new ProducerRecord<>("cities", 0, i * 10, "city-" + i*100);

            producer.send(record, new Callback() {
                // RecordMetadata,消息元数据,即主题、消息的key、消息本身等的封装对象
                @Override
                public void onCompletion(RecordMetadata metadata, Exception exception) {
                    System.out.print("partition = " + metadata.partition());
                    System.out.print(",topic = " + metadata.topic());
                    System.out.println(",offset = " + metadata.offset());
                }
            });
        }
    }
}

上述配置中增加了两个参数batch.size指定要批量发送的消息个数,默认16k个,即消息个数达到16k个才会发送一次)和linger.ms指定积攒消息的时长,默认值为0ms 积攒ms就会批量发送一次消息),两个参数只要满足其中一个就会批量发送一次消息,否则不发送知道满足其中的一个条件。

测试代码如下:

public class ProducerBatchTest {

    public static void main(String[] args) throws IOException {
        SomeProducerBatch producer = new SomeProducerBatch();
        producer.sendMsg();
        System.in.read();
    }
}

消费者

1.演示自动提交(即消费者消费完消息后自动提交offsetbroker),代码如下:

/**
 * 演示自动提交
 */
public class SomeConsumer extends ShutdownableThread {
    private KafkaConsumer<Integer, String> consumer;

    //得调用父类ShutdownableThread的构造器
    public SomeConsumer() {
        //第一个参数表示当前消费者的名称,false表示消费过程中能否被中断,一般设置不能中断
        super("KafkaConsumerTest", false);

        Properties properties = new Properties();
        String brokers = "kafkaOS1:9092,kafkaOS2:9092,kafkaOS3:9092";
        properties.put("bootstrap.servers", brokers);
        // 指定消费者组ID
        properties.put("group.id", "cityGroup1");
        // 开启offset自动提交,默认也是开启的
        properties.put("enable.auto.commit", "true");

        // 指定自动提交的最晚时间间隔
        //消费时间为消息读取过来,消费消息,提交offset
        //注意:auto.commit.interval.ms值指的是消费消息的时间
        //如果在这段时间内没有消费完消息,就会发送异常给broker(同一个consumer重复消费情况)
        properties.put("auto.commit.interval.ms", "10000");

        // 指定broker认定consumer宕机的时限。从consumer读取消息开始计时,一直到其收到consumer
        // 提交的offset,这个时间段不能超过该值,否则broker认定当前consumer宕机
        properties.put("session.timeout.ms", "30000");

        // 消费者向broker controller发送心跳,即心跳发送频率 生产中一般设置该值是上面值的1/3
        //即是session.timeout.ms值的1/3(不同的consumer重复消费)
        properties.put("heartbeat.interval.ms", "10000");

        // 若没有指定初始的offset或指定的offset不存在,则offset要读取其指定的默认值
        // earliest:从该partition的最开始的offset开始,一般是0
        // lastest:从该partition的最后offset开始,即HW
        properties.put("auto.offset.reset", "earliest");

        //指定反序列化器
        properties.put("key.deserializer",
                "org.apache.kafka.common.serialization.IntegerDeserializer");
        properties.put("value.deserializer",
                "org.apache.kafka.common.serialization.StringDeserializer");

        this.consumer = new KafkaConsumer<Integer, String>(properties);
    }

    @Override
    public void doWork() {
        // 指定要消费的主题,可以指定多个主题
        consumer.subscribe(Collections.singletonList("cities"));
        // poll()是阻塞的方法,其参数表示,若broker中没有消息,该poll()等待的最长时间
        // 到时仍没有消息,则返回null
        ConsumerRecords<Integer, String> records = consumer.poll(1000);
        for(ConsumerRecord record : records) {
            System.out.print("topic = " + record.topic());
            System.out.print(" partition = " + record.partition());
            System.out.print(" key = " + record.key());
            System.out.println(" value = " + record.value());
        }
    }
}

上述代码中的三个重要参数说明如下:

  • group.id:指定消费者id
  • enable.auto.commit:是否开启offset自动提交,默认是开启的。
  • auto.commit.interval.ms:指的是消费消息的时间,即消息完消息后自动发送offsetbroker,如果在这段时间内没有消费完消息,就会发送异常给broker(重复消息中的同一个consumer重复消费情况)
  • session.timeout.ms:指定会话超时时间,如果在会话超时时间内,broker没有检测到该consumer发送的心跳,则会认为该consumer已经宕机了,宕机就会发生我们上面说的再均衡rebalance
  • heartbeat.interval.ms:指定consumer消费者每隔多长时间发送一次心跳给broker
  • auto.offset.reset:指定consumer消费者从partition的哪个offset位置开始消费消息,
    • earliest:从该partition的最开始的offset开始,一般是0
    • lastest:从该partition的最后offset开始,即HW

测试代码如下:

public class ConsumerTest {
    public static void main(String[] args) {
        SomeConsumer consumer = new SomeConsumer();
        //因为SomeConsumer继承了ShutdownableThread,而ShutdownableThread继承了Thread
        //本质就是一个线程
        consumer.start();
    }
}

2.演示消费者向broker手动提交offset,并且一直等待broker成功响应(同步手动提交),代码如下:

/**
 * 演示同步手动提交
 */
public class SyncManualConsumer extends ShutdownableThread {
    private KafkaConsumer<Integer, String> consumer;

    public SyncManualConsumer() {
        super("KafkaConsumerTest", false);

        Properties properties = new Properties();
        String brokers = "kafkaOS1:9092,kafkaOS2:9092,kafkaOS3:9092";
        properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, brokers);
        //group.id
        properties.put(ConsumerConfig.GROUP_ID_CONFIG, "cityGro11");
        //改成手动提交
        properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false");
        // properties.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, "1000");
        // 设置一次提交的offset个数 因为消费者每消费一个消息就会提交一个offset
        properties.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, 10);
        //auto.offset.reset
        properties.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
        properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG,
                "org.apache.kafka.common.serialization.IntegerDeserializer");
        properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,
                "org.apache.kafka.common.serialization.StringDeserializer");

        this.consumer = new KafkaConsumer<Integer, String>(properties);
    }

    /**
     * 同步提交,即消费者向broker提交offset,
     * 消费者需要等待broker响应成功
     *
     * 例子:上面设置一次提交10个offset,
     * 第一次消费者提交10个offset,broker响应成功后,
     * 之后消费者再提交10个offset,broker再相应成功
     */
    @Override
    public void doWork() {
        // 指定要消费的主题
        consumer.subscribe(Collections.singletonList("cities"));
        ConsumerRecords<Integer, String> records = consumer.poll(1000);
        for(ConsumerRecord record : records) {
            System.out.print("topic = " + record.topic());
            System.out.print(" partition = " + record.partition());
            System.out.print(" key = " + record.key());
            System.out.println(" value = " + record.value());

            // 手动同步提交(consumer提交offset等待broker的响应),如果broker没有响应,会再次提交
            consumer.commitSync();
        }
    }
}

测试代码如下:

public class SyncManualTest {
    public static void main(String[] args) {
        SyncManualConsumer consumer = new SyncManualConsumer();
        consumer.start();
    }
}

3.演示消费者向broker手动提交offset,并且无需等待broker成功响应,提交成功后会回调(异步手动提交),代码如下:

/**
 * 异步手动提交
 */
public class AsynManualConsumer extends ShutdownableThread {
    private KafkaConsumer<Integer, String> consumer;

    public AsynManualConsumer() {
        super("KafkaConsumerTest", false);

        Properties properties = new Properties();
        String brokers = "kafkaOS1:9092,kafkaOS2:9092,kafkaOS3:9092";
        properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, brokers);
        properties.put(ConsumerConfig.GROUP_ID_CONFIG, "cityGro11");
        //enable.auto.commit
        properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false");
        // properties.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, "1000");
        // 设置一次提交的offset个数
        properties.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, 10);
        properties.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
        properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG,
                "org.apache.kafka.common.serialization.IntegerDeserializer");
        properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,
                "org.apache.kafka.common.serialization.StringDeserializer");

        this.consumer = new KafkaConsumer<Integer, String>(properties);
    }

    @Override
    public void doWork() {
        // 指定要消费的主题
        consumer.subscribe(Collections.singletonList("cities"));
        ConsumerRecords<Integer, String> records = consumer.poll(1000);
        for(ConsumerRecord record : records) {
            System.out.print("topic = " + record.topic());
            System.out.print(" partition = " + record.partition());
            System.out.print(" key = " + record.key());
            System.out.println(" value = " + record.value());

            // 手动同步提交
            // consumer.commitSync();
            // 手动异步提交
            // consumer.commitAsync();
            // 带回调功能的手动异步提交
            consumer.commitAsync((offsets, e) -> {
                if (e != null) {
                    System.out.print("提交失败,offsets = " + offsets);
                    System.out.println(",exception = " + e);
                }
            });
        }
    }
}

测试代码如下:

public class AsyncManualTest {
    public static void main(String[] args) {
        AsynManualConsumer consumer = new AsynManualConsumer();
        consumer.start();
    }
}

4.消费着正常情况下使用异步提交的方式,如果异步提交出现异常的话,就进行同步提交(同步异步提交,常用这种方式),主要是为了防止broker宕机之后,发生rebalance时的重复消费,代码如下:

/**
 * 同步异步手动提交,可以避免重复消费
 */
public class SyncAsyncManualConsumer extends ShutdownableThread {
    private KafkaConsumer<Integer, String> consumer;

    public SyncAsyncManualConsumer() {
        super("KafkaConsumerTest", false);

        Properties properties = new Properties();
        String brokers = "kafkaOS1:9092,kafkaOS2:9092,kafkaOS3:9092";
        properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, brokers);
        properties.put(ConsumerConfig.GROUP_ID_CONFIG, "cityGro11");
        properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false");
        // properties.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, "1000");
        // 设置一次提交的offset个数
        properties.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, 10);
        properties.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
        properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG,
                "org.apache.kafka.common.serialization.IntegerDeserializer");
        properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,
                "org.apache.kafka.common.serialization.StringDeserializer");

        this.consumer = new KafkaConsumer<Integer, String>(properties);
    }

    @Override
    public void doWork() {
        // 指定要消费的主题
        consumer.subscribe(Collections.singletonList("cities"));
        ConsumerRecords<Integer, String> records = consumer.poll(1000);
        for(ConsumerRecord record : records) {
            System.out.print("topic = " + record.topic());
            System.out.print(" partition = " + record.partition());
            System.out.print(" key = " + record.key());
            System.out.println(" value = " + record.value());

            try {
                //正常情况下
                // 带回调功能的手动异步提交
                //只要大的offset提交成功,小的offset提交成不成功无所谓了
                consumer.commitAsync((offsets, e) -> {
                    if (e != null) {
                        System.out.print("提交失败,offsets = " + offsets);
                        System.out.println(",exception = " + e);
                    }
                });
            }catch (Exception e) {
                e.printStackTrace();
                //异常情况下同步提交,一直等到你的broker回应
                // 同步提交
                //有可能broker宕机了之后,它会一直同步提交,一直失败
                //当rebalance完成后,就会提交成功
                //可以避免rebalance之后的重复消费
                consumer.commitSync();
            }
        }
    }
}

测试代码如下:

public class SyncAsyncManualTest {
    public static void main(String[] args) {
        SyncAsyncManualConsumer consumer = new SyncAsyncManualConsumer();
        consumer.start();
    }
}

SpringBoot整合Kafka

1.首先在pom.xml中引入依赖如下:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!--引入kafka依赖-->
    <dependency>
        <groupId>org.springframework.kafka</groupId>
        <artifactId>spring-kafka</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

2.由于Spring是通过KafkaTemplate来操作Kafka的,所以在application.yml配置文件中配置的信息如下:

# 自定义属性,定义主题
kafka:
  topic: cities

# 配置KafkaTemplate信息
spring:
  kafka:
    # kafka集群主机地址
    bootstrap-servers: kafka主机1ip地址:9092,kafka主机2ip地址:9092,kafka主机3ip地址:9092
    # 配置生产者
    producer:   
      key-serializer: org.apache.kafka.common.serialization.StringSerializer
      value-serializer: org.apache.kafka.common.serialization.StringSerializer
    # 配置消费者
    consumer: 
      # 消费者组id
      group-id: group0  
      key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
      value-deserializer: org.apache.kafka.common.serialization.StringDeserializer

3.编写生产者代码如下:

@RestController
public class SomeProducer {
    @Autowired
    private KafkaTemplate<String, String> template;

    // 从配置文件读取自定义属性
    @Value("${kafka.topic}")
    private String topic;

    // 由于是提交数据,所以使用Post方式
    @PostMapping("/msg/send")
    public String sendMsg(@RequestParam("message") String message) {
        template.send(topic, message);
        return "send success";
    }
}

4.编写消费者代码如下:

@Component
public class SomeConsumer {

    @KafkaListener(topics = "${kafka.topic}")
    public void onMsg(String message) {
        System.out.println("Kafka消费者接受到消息 " + message);
    }
}