【文章内容输出来源:拉勾教育Java高薪训练营】
--- 所有脑图均本人制作,未经允许请勿滥用 ---
中间件知识点up
登山则情满于山,观海则意溢于海
一、Kafka架构与实战
part 1 - 概念与基本架构
「主角登场」
最初,Kafka由Linkedin公司开发,初衷是开发一个 分布式
、分区
、生产者-订阅者模式
、基于zookeeper
协调的分布式日志系统,后续因为其强大的持久化能力
以及高吞吐率
也被当做了MQ系统来使用。(2010年被贡献给Apache基金会并成为了顶级开源项目)
主要应用场景:日志收集系统/消息系统
设计目标:
- 以时间复杂度为O(1)的方式提供消息持久化能力,即使对TB级以上数据也能保证常数时间的访问性能。
- 高吞吐率。即使在非常廉价的商用机器上也能做到单机支持每秒100K条消息的传输。
- 支持Kafka Server间的消息分区,及分布式消费,同时保证每个partition内的消息顺序传输。
- 支持离线数据处理和实时数据处理。
- 支持在线水平扩展
通常来说,消息的传递有两种模式:「点对点传递」&「发布-订阅模式」,而Kafka是典型的发布-订阅模式;同时,一般消息中间件有两种模式:「推模式」&「拉模式」,而Kafka只有拉没有推(可以通过轮询实现消息的推送)
- Kafka在一个或多个可以跨越多个数据中心的服务器上作为集群运行
- Kafka集群按照
主题
分类管理,一个主题可以有多个分区
,一个分区可以有多个副本分区
- 每个记录由一个
键
,一个值
和一个时间戳
组成
四个核心API:
- Producer API:允许应用程序将记录流发布到一个或多个Kafka主题
- Consumer API:允许应用程序订阅一个或多个主题并处理为其生成的记录流
- Streams API:允许应用程序充当流处理器,使用一个或多个主题的输入流,并生成一个或多个输出主题的输出流,从而有效地将输入流转换为输出流
- Connector API:允许构建和运行将Kafka主题连接到现有应用程序或数据系统的可重用生产者或使用者。例如,关系数据库的连接器可能会捕获对表的所有更改
「优势」
高吞吐量
:单机每秒处理几十上百万的消息量。即使存储了许多TB的消息,它也保持稳定的性能。高性能
:单节点支持上千个客户端,并保证零停机和零数据丢失。持久化数据存储
:将消息持久化到磁盘。通过将数据持久化到硬盘以及replication防止数据丢失。- 零拷贝
- 顺序读,顺序写
- 利用Linux的页缓存
分布式系统
,易于向外扩展。所有的Producer、Broker和Consumer都会有多个,均为分布式的。无需停机即可扩展机器。多个Producer、Consumer可能是不同的应用。可靠性
- Kafka是分布式,分区,复制和容错的。客户端状态维护
:消息被处理的状态是在Consumer端维护,而不是由server端维护。当失败时能自动平衡。支持online和offline的场景
。支持多种客户端语言
。Kafka支持Java、.NET、PHP、Python等多种语言。
「应用场景」
- 日志收集:通过Kafka以统一接口服务的方式开放给各种Consumer,从而收集各类服务Log
- 消息系统:解耦生产者和消费者、缓存消息等
- 用户活动跟踪:Kafka经常被用来记录Web用户或者App用户的各种活动,如浏览网页、搜索、点击等活动,这些活动信息被各个服务器发布到Kafka的Topic中,然后消费者通过订阅这些Topic来做实时的监控分析,亦可保存到数据库
- 运营指标:Kafka也经常用来记录运营监控数据。包括收集各种分布式应用的数据,生产各种操作的集中反馈,比如报警和报告
- 流式处理:如Spark Streaming和Storm
「基本架构」
消息&批次
Kafka的数据单元称为消息
。可以把消息看成是数据库里的一个“数据行”或一条“记录”。消息由字节数组组成。
为了提高效率,消息被分批写入Kafka
。批次就是一组消息,这些消息属于同一个主题和分区。
把消息分成批次可以减少网络开销。批次越大,单位时间内处理的消息就越多,单个消息的传输时间就越长。批次数据会被压缩,这样可以提升数据的传输和存储能力,但是需要更多的计算处理。
模式
消息模式(schema)
有许多可用的选项,以便于理解。如JSON
和XML
,但是它们缺乏强类型处理能力。
Kafka的许多开发者喜欢使用Apache Avro
。
Avro提供了一种紧凑的序列化格式,模式和消息体分开。当模式发生变化时,不需要重新生成代码,它还支持强类型和模式进化,其版本既向前兼容,也向后兼容。
数据格式的一致性对Kafka很重要,因为它消除了消息读写操作之间的耦合性。
主题&分区
Kafka的消息通过主题(Topic)
进行分类。主题可比作是数据库的表或者文件系统里的文件夹。主题可以被分为
若干分区,一个主题通过分区分布于Kafka集群中,提供了横向扩展的能力。
生产者&消费者
生产者
创建消息。消费者
消费消息。
一个消息会被生产者发布到一个指定的主题上,默认情况下,生产者会均衡的发布消息在该主题的所有分区中。 可归纳为以下步骤:
- 可指定消息的分区(不指定即为均衡发布)
- 根据消息的key散列取模得出分区
- 轮询指定分区
消费者通过偏移量来区分已经读过的消息,从而消费消息。
消费者是消费组的一部分。消费组保证每个分区只能被一个消费者使用,避免重复消费。
broker&集群
一个独立的Kafka服务器称为broker
。
broker接收来自生产者的消息,为消息设置偏移量,并提交消息到磁盘保存。broker为消费者提供服务,对读取分区的请求做出响应,返回已经提交到磁盘上的消息。单个broker可以轻松处理数千个分区以及每秒百万级的消息量。
每个集群都有一个broker是
集群控制器"首领"
(自动从集群的活跃成员中选举出来)。
"首领"负责将分区分配给broker并监控这些broker,一个分区可以分配给多个broker,此时会发生分区复制
(如上图中的Broker1中的首领将自己的分区复制给了Broker2)
「核心概念」
- Producer-生产者发布消息:
- 生产者将消息发布到Kafka的topic中。broker接收到生产者发送的消息后,broker将该消息追加到当前用于追加数据的 segment 文件中。
- 默认情况下通过轮询把消息均衡地分布到主题的所有分区上。
- 在某些情况下,生产者会把消息直接写到指定的分区。这通常是通过消息键和分区器来实现的,分区器为键生成一个散列值,并将其映射到指定的分区上。这样可以保证包含同一个键的消息会被写到同一个分区上。
- 生产者也可以使用自定义的分区器,根据不同的业务规则将消息映射到分区。
- 生产者将消息发布到Kafka的topic中。broker接收到生产者发送的消息后,broker将该消息追加到当前用于追加数据的 segment 文件中。
- Consumer-消费者读取消息:
- 消费者订阅一个或多个主题,并按照消息生成的顺序读取它们。
- 消费者
通过检查消息的偏移量来区分已经读取过的消息
。
偏移量是一种元数据,它是一个不断递增的整数值,在创建消息时,Kafka 会把它添加到消息里。在给定的分区里,每个消息的偏移量都是唯一的。消费者把每个分区最后读取的消息偏移量保存在Zookeeper 或Kafka上,如果消费者关闭或重启,它的读取状态不会丢失。 - 消费者是消费组的一部分。群组保证每个分区只能被一个消费者使用。
- 如果一个消费者失效,消费组里的其他消费者可以接管失效消费者的工作,再平衡,分区重新分配。
- Broker-为消费者提供服务:
- broker 为消费者提供服务,对读取分区的请求作出响应,返回已经提交到磁盘上的消息。
- 如果某topic有N个partition,集群有N个broker,那么每个broker存储该topic的一个partition
- 如果某topic有N个partition,集群有(N+M)个broker,那么其中有N个broker存储该topic的一个partition,剩下的M个broker不存储该topic的partition数据
- 如果某topic有N个partition,集群中broker数目少于N个,那么一个broker存储该topic的一个或多个partition。在实际生产环境中,尽量避免这种情况的发生,这种情况容易导致Kafka集群数据不均衡
- broker 为消费者提供服务,对读取分区的请求作出响应,返回已经提交到磁盘上的消息。
- Topic-主题用于划分消息类别:
- 主题就好比数据库的表,尤其是分库分表之后的逻辑表。
- Partition-分区实现数据冗余和伸缩:
- 主题可以被分为若干个分区,一个分区就是一个提交日志。
- 消息以追加的方式写入分区,然后以
先入先出
的顺序读取。 - 无法在整个主题范围内保证消息的顺序,但可以保证消息在单个分区内的顺序。
- Kafka 通过分区来实现数据冗余和伸缩性。
- 在需要严格保证消息的消费顺序的场景下,需要将partition数目设为1。
- Replicas-副本保证数据的一致性:
- Kafka 使用主题来组织数据,每个主题被分为若干个分区,每个分区有多个副本。那些副本被保存在broker 上,每个broker 可以保存成百上千个属于不同主题和分区的副本。
首领副本
:每个分区都有一个首领副本。为了保证一致性,所有生产者请求和消费者请求都会经过这个副本跟随者副本
:首领以外的副本都是跟随者副本。跟随者副本不处理来自客户端的请求,它们唯一的任务就是从首领那里复制消息,保持与首领一致的状态。如果首领发生崩溃,其中的一个跟随者会被提升为新首领。
- Kafka 使用主题来组织数据,每个主题被分为若干个分区,每个分区有多个副本。那些副本被保存在broker 上,每个broker 可以保存成百上千个属于不同主题和分区的副本。
- Offset-确保读写秩序:
- 目前ConsumerA消费到了offset=9的位置,ConsumerB消费到了offset=11的位置,当下一次他们来消费的时候,可以选择接着上一次继续消费,或者重头开始消费,也可以跳到尾端从最新消息开始消费。
part 2 - 安装与配置
因为Kafka基于Zookeeper,Zk又基于java,所以需要确保运行环境 jdk 和 zookeeper 的可用!
jdk在linux下的环境配置在此不赘述,有关zookeeper的额环境配置参考如下:
- 在
../zookeeper-x.x.x/conf/zoo.cfg
中编辑dataDir=../zookeeper.data
(zookeeper保存数据的目录) - 编辑 /etc/profile
export ZOOKEEPER_PREFIX=/root/my-software/apache-zookeeper-x.x.x export PATH=$PATH:$ZOOKEEPER_PREFIX/bin export ZOO_LOG_DIR=$PATH:$ZOOKEEPER_PREFIX/logs
- 使配置生效:
source /etc/profile
- 验证
zkServer.sh status
Kafka的安装与配置
- 官网下载 解压包
- 执行命令
tar -zxvf kafka_x.x.x.tgz -C /opt
解压 - 配置环境变量
vim /etc/profile
export KAFKA_HOME=/opt/kafka_x.x.x export PATH=$PATH:$KAFKA_HOME/bin
- 配置/opt/kafka_2.12-1.0.2/config中的server.properties文件
... # 指定用于存放相关日志的路径 log.dirs=/opt/kafka_x.x.x/logs ... # 指定zookeeper的映射路径(此处默认本主机,'myKafka'->将来在zk中创建的初始节点名) zookeeper.connect=localhost:2181/myKafka
- 启动zk后,使用命令
./bin/kafka-server-start.sh config/server.properties
启动kafka- 启动成功后:
- 当然也可以通过守护进程形式启动
./bin/kafka-server-start.sh -daemon config/server.properties
- 查看kafka后台进程
pa -ef | grep kafka
- 启动zookeeper客户端查看节点
./zkCli.sh
+ls /myKafka
- /brokers: 保存了Kafk集群的所有信息,包括每台broker的注册信息,集群上所有topic的信息等。
- /controller: 保存了Kafka controller组件(controller负责集群的领导者选举)的注册信息,同时也负责controller的动态选举.
- /admin: 保存管理脚本的输出结果,比如删除topic,对分区进行重分配等操作。
- /isr_change_notification: 保存isr列表发生变化的分区列表。controller会注册一个监听器实时监控该节点下子节点的变更。
- /config: 保存了Kafka集群下各种资源的定制化配置信息,比如每个topic可能有自己专属的一组配置。那么就保存在/config/topic/<topic>下
- /cluster: 保存了Kafka集群的简要信息,包括集群ID信息和集群版本号
- /controller_epoch: 保存了controller组件的版本号,Kafka使用该版本号来隔离无效的controller请求。
- (守护线程启动情况下)关闭后台运行的kafka:
./kafka-server-stop.sh
- 其他重要脚本:
- kafka-topics.sh : 主题管理命令
# 列出现有的主题 kafka-topics.sh --list --zookeeper localhost:2181/myKafka # 创建主题(以下语义'TOPIC_001主题包含一个分区1(默认Leader分区)') kafka-topics.sh --zookeeper localhost:2181/myKafka --create --topic TOPIC_001 --partitions 1 --replication-factor 1 # 查看分区信息 kafka-topics.sh --zookeeper localhost:2181/myKafka --list # 查看指定主题的详细信息 kafka-topics.sh --zookeeper localhost:2181/myKafka --describe # 删除指定主题 kafka-topics.sh --zookeeper localhost:2181/myKafka --delete --topic TOPIC_001
- kafka-console-producer.sh : 生产消息
# 开启生产者 kafka-console-producer.sh --topic TOPIC_001 --broker-list localhost:9092
- kafka-console-consumer.sh : 消费消息
# 开启消费者 kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic TOPIC_001 # 开启消费者2(从头消费,不按照偏移量消费) kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic TOPIC_001 --from-beginning
- kafka-topics.sh : 主题管理命令
part 3 - 开发实战
「消息的发送与接收」
生产者的主要对象:KafkaProducer
(发送消息) / ProducerRecord
(封装Kafka消息)
KafkaProducer创建时需指定的参数及含义:
参数 | 说明 |
---|---|
bootstrap.servers | 配置生产者与 broker 建立连接的方式。 该参数设置的是初始化参数。如果生产者需要连接的是Kafka集群,则这里配置集群中几个broker的地址,而不是全部,当生产者连接上此处指定的broker之后,在通过该连接发现集群中的其他节点。 |
key.serializer | 要发送信息的 key 数据的序列化类。 设置的时候可以写类名,也可以使用该类的Class对象。 |
value.serializer | 要发送消息的 value 数据的序列化类。 设置的时候可以写类名,也可以使用该类的Class对象。 |
acks | 应答消息。(默认值:all)acks=0 :生产者不等待broker对消息的确认,只要将消息放到缓冲区,就认为消息已经发送完成。(该情形不能保证broker是否真的收到了消息,retries配置也不会生效。发送的消息的返回的消息偏移量永远是-1。) acks=1 :表示消息只需要写到主分区即可,然后就响应客户端,而不等待副本分区的确认。(在该情形下,如果主分区收到消息确认之后就宕机了,而副本分区还没来得及同步该消息,则该消息丢失。) acks=all :首领分区会等待所有的ISR副本分区确认记录。(该处理保证了只要有一个ISR副本分区存活,消息就不会丢失。这是Kafka最强的可靠性保证,等效于 acks=-1) |
retries | 重试次数。 case1 ==> 当消息发送出现错误的时候,系统会重发消息。 case2 ==> 跟客户端收到错误时重发一样。 (如果设置了重试,还想保证消息的有序性,需要设置 MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION=1 ,否则在重试此失败消息的时候,其他的消息可能发送成功了) |
- 更多参数可以直接在
org.apache.kafka.clients.producer.ProducerConfig
中查找- 有关 acks 参数:消费者消费消息后,需要broker端的确认,可以同步确认但是效率低;可以异步确认效率高但是需要设置回调函数
下面通过一个java demo,简单看看消息的使用
-kafka- 服务预备
在kafka服务中创建好主题 topic_1,并且启动消费者对于该主题进行消费
# 创建主题
./bin/kafka-topics.sh --zookeeper localhost:2181 --create --topic topic_1 --partitions 1 --replication-factor 1
# 查看主题
./bin/kafka-topics.sh --list --zookeeper localhost:2181
topic_1
-pom- 依赖引入
提前查看自己的broker是哪个版本的:
<dependencies>
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<!-- 高版本兼容低版本,我们使用和broker一致的版本 -->
<version>2.7.0</version>
</dependency>
</dependencies>
-P1- 生产者A
public class MyProducerA {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Map<String, Object> configs = new HashMap<>();
// 1. 指定初始连接用到的broker地址
configs.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "117.50.40.96:9092");
// 2. 指定key的序列化类
configs.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, IntegerSerializer.class);
// 3. 指定value的序列化类
configs.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
// 4. 准备生产者
KafkaProducer<Integer, String> producer = new KafkaProducer<>(configs);
// 5. 设置用户自定义的消息头字段
List<Header> headers = new ArrayList<>();
headers.add(new RecordHeader("biz.name", "producer.demo".getBytes()));
// 6. 准备好预发送的消息(记录)
ProducerRecord<Integer, String> producerRecord = new ProducerRecord<>("topic_1", 0, 0, "Hello Kafka!");
/**
* 消息的同步确认
*/
/*Future<RecordMetadata> future = producer.send(producerRecord);
RecordMetadata metadata = future.get();
System.out.println("消息的主题:" + metadata.topic());
System.out.println("消息的分区号:" + metadata.partition());
System.out.println("消息的偏移量:" + metadata.offset());*/
/**
* 消息的确认
*/
producer.send(producerRecord, (metadata, exception) -> {
if (exception == null) {
System.out.println("消息的主题:" + metadata.topic());
System.out.println("消息的分区号:" + metadata.partition());
System.out.println("消息的偏移量:" + metadata.offset());
} else {
System.out.println("异常消息:" + exception.getMessage());
}
});
// 6. 关闭生产者
producer.close();
}
}
!执行过程如果遇到 :linux服务器上使用脚本可以实现收发,但是执行外部程序API调用Proeucer发送信息失败(超时),请参考解决方式
-P2- 生产者B(连发100次)
public class MyProducerB {
public static void main(String[] args) throws ExecutionException, InterruptedException {
HashMap<String, Object> configs = new HashMap<>();
// 1. 指定初始连接用到的broker地址
configs.put("bootstrap.servers", "117.50.40.96:9092");
// 2. 指定key的序列化类
configs.put("key.serializer", IntegerSerializer.class);
// 3. 指定value的序列化类
configs.put("value.serializer", StringSerializer.class);
// 4. 准备生产者
KafkaProducer<Integer, String> producer = new KafkaProducer<>(configs);
// 5. 设置用户自定义的消息头字段
List<Header> headers = new ArrayList<>();
headers.add(new RecordHeader("biz.name", "producer.demo".getBytes()));
// 6. 准备好预发送的消息(记录)
for (int i = 0; i < 100; i++) {
ProducerRecord<Integer, String> producerRecord = new ProducerRecord<>("topic_1", 0, i, "Hello Kafka!" + i, headers);
/**
* 消息的同步确认
*/
/*Future<RecordMetadata> future = producer.send(record);
RecordMetadata metadata = future.get();
System.out.println("消息的主题:" + metadata.topic());
System.out.println("消息的分区号:" + metadata.partition());
System.out.println("消息的偏移量:" + metadata.offset());*/
/**
* 消息的确认
*/
producer.send(producerRecord, (metadata, exception) -> {
if (exception == null) {
System.out.println("消息的主题:" + metadata.topic());
System.out.println("消息的分区号:" + metadata.partition());
System.out.println("消息的偏移量:" + metadata.offset());
} else {
System.out.println("异常消息:" + exception.getMessage());
}
});
}
// 6. 关闭生产者
producer.close();
}
}
-C1- 消费者(从最早的消息开始拉取)
public class MyConsumer {
public static void main(String[] args) {
HashMap<String, Object> configs = new HashMap<>();
// 1. 指定初始连接用到的broker地址
configs.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "117.50.40.96:9092");
// 2. 配置key的反序列化器
configs.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, IntegerDeserializer.class);
// 3. 配置value的反序列化器
configs.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
// 4. 配置消费组ID
configs.put(ConsumerConfig.GROUP_ID_CONFIG, "consumer_demo1");
// 5. 如果找不到当前消费者的有效偏移量,则自动重置到最开始
// (latest:直接重置到消息偏移量的最后一个 / earliest:从头开始消费)
configs.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
// 6. 准备好消费者
KafkaConsumer<Integer, String> consumer = new KafkaConsumer<>(configs);
// 7. 先订阅,再消费
consumer.subscribe(Arrays.asList("topic_1"));
/* --> 批量从主题的分区拉取消息 */
ConsumerRecords<Integer, String> consumerRecords = consumer.poll(3_000);// 3秒拉取一次
/* --> 遍历该次从主题拉取的批量消息 */
consumerRecords.forEach(record -> System.out.println(record.topic() + "\t"
+ record.partition() + "\t"
+ record.offset() + "\t"
+ record.key() + "\t"
+ record.value()));
// 8. 关闭消费者
consumer.close();
}
}
打印如下:
"C:\Program Files\Java\jdk1.8.0_131\bin\java.exe" "-javaagent:E:\IDE\IntelliJ IDEA 2019.3.5\lib\idea_rt.jar=1107:E:\IDE\IntelliJ IDEA 2019.3.5\bin" -Dfile.encoding=UTF-8 -classpath "C:\Program Files\Java\jdk1.8.0_131\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\deploy.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\access-bridge-64.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\cldrdata.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\jaccess.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\jfxrt.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\localedata.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\nashorn.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\sunec.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\sunjce_provider.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\sunmscapi.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\sunpkcs11.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\ext\zipfs.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\javaws.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\jce.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\jfr.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\jfxswt.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\management-agent.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\plugin.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\resources.jar;C:\Program Files\Java\jdk1.8.0_131\jre\lib\rt.jar;C:\Archie\archieself-study\java-kafka\target\classes;C:\work\mvnRepository\org\apache\kafka\kafka-clients\1.0.2\kafka-clients-1.0.2.jar;C:\work\mvnRepository\org\lz4\lz4-java\1.4\lz4-java-1.4.jar;C:\work\mvnRepository\org\xerial\snappy\snappy-java\1.1.4\snappy-java-1.1.4.jar;C:\work\mvnRepository\org\slf4j\slf4j-api\1.7.25\slf4j-api-1.7.25.jar" com.archie.consumer.MyConsumerA
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
topic_1 0 0 null hello 1
topic_1 0 1 null hello
topic_1 0 2 0 Hello Kafka!0
topic_1 0 3 0 Hello Kafka!1
topic_1 0 4 0 Hello Kafka!2
topic_1 0 5 99 Hello Kafka!3
topic_1 0 6 0 Hello Kafka!4
......
topic_1 0 103 97 Hello Kafka!97
topic_1 0 104 98 Hello Kafka!98
topic_1 0 105 99 Hello Kafka!99
Process finished with exit code 0
「SpringBoot Kafka」
-pom- 依赖声明
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>springboot-kafka</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<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>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
-properties- spring初始化配置
spring.application.name=springboot-kafka
server.port=8080
# kafka的配置
spring.kafka.bootstrap-servers=117.50.40.96:9092
# --- producer配置 ---
spring.kafka.producer.key-serializer=org.apache.kafka.common.serialization.IntegerSerializer
spring.kafka.producer.value-serializer=org.apache.kafka.common.serialization.StringSerializer
# 生产者每个批次最多放多少条记录
spring.kafka.producer.batch-size=16384
# 生产者端总可用发送缓冲区大小,此处设置为32MB
spring.kafka.producer.buffer-memory=33554432
# --- consumer配置 ---
spring.kafka.consumer.key-deserializer=org.apache.kafka.common.serialization.IntegerDeserializer
spring.kafka.consumer.value-deserializer=org.apache.kafka.common.serialization.StringDeserializer
spring.kafka.consumer.group-id=springboot-consumer
# 如果在kafka中找不到当前消费者的偏移量,则直接将偏移量重置为最早的
spring.kafka.consumer.auto-offset-reset=earliest
# 消费者的偏移量是自动提交还是手动提交,此处自动提交偏移量
spring.kafka.consumer.enable-auto-commit=true
# 消费者偏移量自动提交的时间间隔
spring.kafka.consumer.auto-commit-interval=1000
-SyncController- 同步控制器
@RestController
public class KafkaSyncProducerController {
@Autowired
private KafkaTemplate<Integer, String> template;
@RequestMapping("send/sync/{message}")
public String send(@PathVariable String message) {
final ListenableFuture<SendResult<Integer, String>> future = template.send("topic-spring", 0, 0, message);
// 同步发送消息
try {
final SendResult<Integer, String> sendResult = future.get();
final RecordMetadata metadata = sendResult.getRecordMetadata();
System.out.println(metadata.topic() + "\t" + metadata.partition() + "\t" + metadata.offset());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
return "success";
}
}
-AsyncController- 异步控制器
@RestController
public class KafkaAsyncProducerController {
@Autowired
private KafkaTemplate<Integer, String> template;
@RequestMapping("send/async/{message}")
public String send(@PathVariable String message) {
final ListenableFuture<SendResult<Integer, String>> future = this.template.send("topic-spring", 0, 1, message);
// 设置回调函数,异步等待broker端的返回结果
future.addCallback(new ListenableFutureCallback<SendResult<Integer, String>>() {
@Override
public void onFailure(Throwable throwable) {
System.out.println("发送消息失败:" + throwable.getMessage());
}
@Override
public void onSuccess(SendResult<Integer, String> result) {
final RecordMetadata metadata = result.getRecordMetadata();
System.out.println("发送消息成功:" + metadata.topic() + "\t" + metadata.partition() + "\t" + metadata.offset());
}
});
return "success";
}
}
-Listener- 客户端消费者(监听器)
@Component
public class MyConsumer {
@KafkaListener(topics = "topic-spring")
public void onMessage(ConsumerRecord<Integer, String> record) {
System.out.println("消费者收到的消息:"
+ record.topic() + "\t"
+ record.partition() + "\t"
+ record.offset() + "\t"
+ record.key() + "\t"
+ record.value());
}
}
-App- 启动入口
@SpringBootApplication
public class SpringbootKafkaApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootKafkaApplication.class, args);
}
}
现在就可以启动主入口试试了!
启动前无须担心自己的Kafka服务上没有topic-spring
而导致服务启动失败,因为Spring内部整合了 KafkaAdmin,启动时它如果识别到不存在对应主题,会主动创建出来,如下图:
同步
访问的测试结果:↓
异步
访问的测试结果:↓
当然,作为SpringBoot项目整合的任何技术,都允许我们自主扩展相关配置功能,并在最大的程度上提供了相关API,如下所述:
- 想要自定义式的创建主题(自定义名称和分区大小)
@Configuration public class KafkaConfig { @Bean public NewTopic topic1() { return new NewTopic("my-topic-01", 3, (short) 1); } @Bean public NewTopic topic2() { return new NewTopic("my-topic-02", 5, (short) 1); } }
启动后
: - 想要覆盖各类初始化配置
part 4 - 服务端参数配置
「zookeeper.connect」
该参数用于配置Kafka要连接的Zookeeper/集群的地址。
它的值是一个字符串,使用逗号分隔Zookeeper的多个地址。
Zookeeper的单个地址是 host:port形式的,可以在最后添加Kafka在Zookeeper中的根节点路径。
举例
:
zookeeper.connect=node2:2181,node3:2181,node4:2181/myKafka...
「listeners」
用于指定当前Broker向外发布服务的地址和端口。
与 advertised.listeners
配合,用于做内外网隔离。
举例
# 监听名称和安全协议的映射配置(每个监听器的名称只能在map中出现一次)
listener.security.protocol.map=INTEGER:PLAINTEXT,EXTERNAL:PLAINTEXT
# 用于配置broker监听的URI以及监听器名称列表,使用逗号隔开多个URI及监听器名称
listeners=INTERNAL://192.168.100.101:9092,EXTERNAL://192.168.100.130:9093
# 用于配置broker之间通信使用的监听器名称,该名称必须在advertised.listeners列表中
inter.broker.listener.name=EXTERNAL
# 需要将该地址发布到zookeeper供客户端使用,如果客户端使用的地址与listeners配置不同
#(必须是listeners中配置的或配置的一部分)
advertised.listeners=EXTERNAL://192.168.100.130:9092
「broker.id」
用于唯一标记一个Kafka的Broker,它的值是一个任意integer值
当Kafka以分布式集群运行的时候,尤为重要。
最好该值跟该Broker所在的物理主机有关的
如主机名为 host1.lagou.com ,则 broker.id=1 ,如果主机名为 192.168.100.101 ,则 broker.id=101 等等。
「log.dir」
通过该属性的值,指定Kafka在磁盘上保存消息的日志片段的目录。
它是一组用逗号分隔的本地文件系统路径。如果指定了多个路径,那么broker 会根据“最少使用”
原则,把同一个分区的日志片段保存到同一个路径下。
broker 会往拥有最少数目分区的路径新增分区,而不是往拥有最小磁盘空间的路径新增分区。
举例
log.dirs=/var/kafka-logs
有关Kafka还有很多高级特性需要结合源码进行分析说明,后续笔者会持续跟进;以下列出大致重点,小伙伴们可以自行深入:
- 生产者
- 数据发送流程
- 序列化器(自定义)
- 分区器
- 拦截器
- 消费者
- 概念:消费者 + 消费组 + 心跳机制
- 消息接收的订阅、反序列化(自定义)
- 消息的位移提交、位移管理
- 再均衡
- 消费者拦截器
- 主题
- 创建、查看、修改、删除
- 增加分区
- KafkaAdminClient应用
- 偏移量管理
- 分区
- 副本机制
- Leader选举
- 分区重新分配
- 自动再均衡
- 分区分配策略
- 物理量存储
- 概念:LogSegment、切分文件、索引文件切分过程
- 索引的偏移量、时间戳
- 日志删除、压缩策略
- 磁盘的零拷贝、页缓存、顺序写入
- 稳定性
- 事务的幂等性、事务操作
- broker选举
- 可靠性保证——失效副本、副本复制
- 消息重复的场景及解决方案
- __consumer_offsets
- 延时队列
- 重试队列
- Kafka集群与运维