消息队列

常用消息队列

消息模型

JMS规范(Java Message Service)

AMQP模型

MQTT模型

目的地

队列(queue):点对点(一对一)

主题(topic):发布/订阅(一对多)

队列(queues)

信箱(exchanges)

绑定(bingdings)

原理

消息先放到信箱里,根据绑定的路由规则再将消息发送到队列里

特点

支持事务,数据一致性高,多用于金融、银行行业

代表

Apache ActiveMQ

Pivotal RabbitMQ

两种模式

队列:点对点模式,单列,消费者确认收到后删除消息

主题:发布/订阅模式,多列,消费者确认收到后不删除消息

应用场景

解耦:允许你独立地扩展或修改两边的处理过程,只要确保它们遵守同样的接口约束。例如:

将不同数据源(如MySQL、Flume、网络端口)的数据传输到不同的目的地(如Hadoop、SparkStreaming、Flink、SpringBoot),如果不解耦,可能要写多达笛卡尔积的传输方法,使用Kafka做中转,则只需写数据源+目的地数量之和的方法。

异步:允许用户把不紧急的事放入队列,在需要的时候再去处理它。例如:

用户注册返回成功前使用异步消息发送邮件或短信验证信息,然后返回成功信息并提示用户需验证邮箱或手机号才能使用账号。

削峰:解决生产消息和消费消息的处理速度不一致的情况。例如:

用户行为实时分析:前端埋点-》日志服务器-》Flume传输-》Hadoop分析,当Flume传输速度和Hadoop上传速度跟不上日志增长速度时,使用Kafka集群缓解Flume和Hadoop的压力。

双十一秒杀系统:秒杀系统处理速度低于用户秒杀请求速度,使用Kafka做缓存。

工具使用

启动:进入安装后的bin目录,cmd运行activemq start

主页:127.0.0.1:8161默认用户名密码:admin/admin

用户名和密码在%ACTIVEMQ_HOME%\conf中的jetty-realm.properties中配置。如下:

五种消息格式

a) StreamMessage -- Java原始值的数据流

b) MapMessage--一套名称-值对

c) TextMessage--一个字符串对象(常用)

d) ObjectMessage--一个序列化的Java对象

e) BytesMessage--一个字节的数据流

代码示例

package com.activemq.queue;

import javax.jms.Connection;

import javax.jms.ConnectionFactory;

import javax.jms.Destination;

import javax.jms.JMSException;

import javax.jms.MessageProducer;

import javax.jms.Session;

import javax.jms.TextMessage;

import org.apache.activemq.ActiveMQConnection;

import org.apache.activemq.ActiveMQConnectionFactory;

/**

 * 消息生产者

 */

public class JMSProducer {

private static final String USERNAME = ActiveMQConnection.DEFAULT_USER;// 默认的连接用户名

private static final String PASSWORD = ActiveMQConnection.DEFAULT_PASSWORD;// 默认的连接密码

private static final String BROKEURL = ActiveMQConnection.DEFAULT_BROKER_URL;// 默认的连接地址

private static final int SENDNUM = 10;// 发送的消息数量

public static void main(String[] args) {

ConnectionFactory connectionFactory;// 连接工厂

Connection connection = null;// 连接

Session session;// 会话:接收或发送消息的线程

Destination destination;// 消息的目的地

MessageProducer messageProducer; // 消息生产者

// 实例化连接工厂

connectionFactory = new ActiveMQConnectionFactory(JMSProducer.USERNAME, JMSProducer.PASSWORD,

JMSProducer.BROKEURL);

try {

connection = connectionFactory.createConnection();// 通过连接工厂获取连接

connection.start();// 启动连接

session = connection.createSession(Boolean.TRUE, Session.AUTO_ACKNOWLEDGE);// 创建session(启用事务,自动确认

destination = session.createQueue("FirstQueue1");// 创建消息队列(获取?)

messageProducer = session.createProducer(destination);// 创建队列消息的生产者

// 发送消息

for (int i = 0; i < JMSProducer.SENDNUM; i++) {

TextMessage textmessage = session.createTextMessage("ActiveMQ 发送的消息" + i);

System.out.println("发送消息:" + "ActiveMQ 发送的消息" + i);

messageProducer.send(textmessage);

}

session.commit();// 确认事务

} catch (Exception e) {

e.printStackTrace();

} finally {

if (connection != null) {

try {

connection.close();

} catch (JMSException e) {

e.printStackTrace();

}

}

}

}

}

package com.activemq.queue;

import javax.jms.Connection;

import javax.jms.ConnectionFactory;

import javax.jms.Destination;

import javax.jms.JMSException;

import javax.jms.Message;

import javax.jms.MessageConsumer;

import javax.jms.MessageListener;

import javax.jms.Session;

import javax.jms.TextMessage;

import org.apache.activemq.ActiveMQConnection;

import org.apache.activemq.ActiveMQConnectionFactory;

/**

 * 消息消费者

 */

public class JMSConsumer {

private static final String USERNAME = ActiveMQConnection.DEFAULT_USER;// 默认的连接用户名

private static final String PASSWORD = ActiveMQConnection.DEFAULT_PASSWORD;// 默认的连接密码

private static final String BROKEURL = ActiveMQConnection.DEFAULT_BROKER_URL;// 默认的连接地址

public static void main(String[] args) {

ConnectionFactory connectionFactory;// 连接工厂

Connection connection = null;// 连接

Session session;// 会话:接收或发送消息的线程

Destination destination;// 消息的目的地

MessageConsumer messageConsumer; // 消息消费者

// 实例化连接工厂

connectionFactory = new ActiveMQConnectionFactory(JMSConsumer.USERNAME, JMSConsumer.PASSWORD,

JMSConsumer.BROKEURL);

try {

connection = connectionFactory.createConnection();// 通过连接工厂获取连接

connection.start();// 启动连接

session = connection.createSession(Boolean.FALSE, Session.AUTO_ACKNOWLEDGE);// 创建session(不加事务,自动确认

destination = session.createQueue("FirstQueue1");// 创建消息队列

messageConsumer = session.createConsumer(destination);// 创建队列消息的消费者

// 接收消息

/*// 方式一(不推荐):同步接收

while (true) {

TextMessage textMessage = (TextMessage) messageConsumer.receive(1000);

if (textMessage != null) {

System.out.println("收到的消息:" + textMessage.getText());

} else {

break;

}

}*/

// 方式二(推荐):异步接收——注册消息监听

messageConsumer.setMessageListener(new MessageListener() {

@Override

public void onMessage(Message message) {

try {

System.out.println("收到的消息:" + ((TextMessage) message).getText());

} catch (JMSException e) {

e.printStackTrace();

}

}

});

} catch (

JMSException e) {

e.printStackTrace();

}

}

}

package com.activemq.topic;

import javax.jms.Connection;

import javax.jms.ConnectionFactory;

import javax.jms.Destination;

import javax.jms.JMSException;

import javax.jms.MessageProducer;

import javax.jms.Session;

import javax.jms.TextMessage;

import org.apache.activemq.ActiveMQConnection;

import org.apache.activemq.ActiveMQConnectionFactory;

/**

 * 消息发布者

 */

public class JMSProducer {

private static final String USERNAME = ActiveMQConnection.DEFAULT_USER;// 默认的连接用户名

private static final String PASSWORD = ActiveMQConnection.DEFAULT_PASSWORD;// 默认的连接密码

private static final String BROKEURL = ActiveMQConnection.DEFAULT_BROKER_URL;// 默认的连接地址

private static final int SENDNUM = 10;// 发送的消息数量

public static void main(String[] args) {

ConnectionFactory connectionFactory;// 连接工厂

Connection connection = null;// 连接

Session session;// 会话:接收或发送消息的线程

Destination destination;// 消息的目的地

MessageProducer messageProducer; // 消息生产者

// 实例化连接工厂

connectionFactory = new ActiveMQConnectionFactory(JMSProducer.USERNAME, JMSProducer.PASSWORD,

JMSProducer.BROKEURL);

try {

connection = connectionFactory.createConnection();// 通过连接工厂获取连接

connection.start();// 启动连接

session = connection.createSession(Boolean.TRUE, Session.AUTO_ACKNOWLEDGE);// 创建session(启用事务,自动确认

// destination = session.createQueue("FirstQueue1");// 创建消息队列

destination = session.createTopic("FirstTopic1");// 创建主题

messageProducer = session.createProducer(destination);// 创建队列消息的生产者

// 发送消息

for (int i = 0; i < JMSProducer.SENDNUM; i++) {

TextMessage textmessage = session.createTextMessage("ActiveMQ 发布的消息" + i);

System.out.println("发布消息:" + "ActiveMQ 发布的消息" + i);

messageProducer.send(textmessage);

}

session.commit();// 确认事务

} catch (Exception e) {

e.printStackTrace();

} finally {

if (connection != null) {

try {

connection.close();

} catch (JMSException e) {

e.printStackTrace();

}

}

}

}

}

package com.activemq.topic;

import javax.jms.Connection;

import javax.jms.ConnectionFactory;

import javax.jms.Destination;

import javax.jms.JMSException;

import javax.jms.Message;

import javax.jms.MessageConsumer;

import javax.jms.MessageListener;

import javax.jms.Session;

import javax.jms.TextMessage;

import org.apache.activemq.ActiveMQConnection;

import org.apache.activemq.ActiveMQConnectionFactory;

/**

 * 消息订阅者

 */

public class JMSConsumer {

private static final String USERNAME = ActiveMQConnection.DEFAULT_USER;// 默认的连接用户名

private static final String PASSWORD = ActiveMQConnection.DEFAULT_PASSWORD;// 默认的连接密码

private static final String BROKEURL = ActiveMQConnection.DEFAULT_BROKER_URL;// 默认的连接地址

public static void main(String[] args) {

ConnectionFactory connectionFactory;// 连接工厂

Connection connection = null;// 连接

Session session;// 会话:接收或发送消息的线程

Destination destination;// 消息的目的地

MessageConsumer messageConsumer; // 消息消费者

// 实例化连接工厂

connectionFactory = new ActiveMQConnectionFactory(JMSConsumer.USERNAME, JMSConsumer.PASSWORD,

JMSConsumer.BROKEURL);

try {

connection = connectionFactory.createConnection();// 通过连接工厂获取连接

connection.start();// 启动连接

session = connection.createSession(Boolean.FALSE, Session.AUTO_ACKNOWLEDGE);// 创建session(不加事务,自动确认

// destination = session.createQueue("FirstQueue1");// 创建消息队列

destination = session.createTopic("FirstTopic1");// 创建主题

messageConsumer = session.createConsumer(destination);// 创建队列消息的消费者

// 接收消息

/*// 方式一(不推荐):同步接收

while (true) {

System.out.println(new Date());

// 接收消息(指定超时时间,为0则不超时,receive返回下一个消息;超时则消费者被关闭,返回null

TextMessage textMessage = (TextMessage) messageConsumer.receive(25000);

if (textMessage != null) {

System.out.println("收到的消息:" + textMessage.getText());

} else {

break;

}

}

System.out.println(new Date());*/

// 方式二(推荐):异步接收——注册消息监听

messageConsumer.setMessageListener(new MessageListener() {

@Override

public void onMessage(Message message) {

try {

System.out.println("收到的消息:" + ((TextMessage) message).getText());

} catch (JMSException e) {

e.printStackTrace();

}

}

});

} catch (JMSException e) {

e.printStackTrace();

}

}

}

代码示例:多组消息

import javax.jms.Connection;

import javax.jms.DeliveryMode;

import javax.jms.JMSException;

import javax.jms.Message;

import javax.jms.MessageConsumer;

import javax.jms.MessageListener;

import javax.jms.MessageProducer;

import javax.jms.Queue;

import javax.jms.Session;

import javax.jms.TextMessage;

import org.apache.activemq.ActiveMQConnectionFactory;

import org.apache.activemq.command.ActiveMQQueue;

public class JMSCorrelationIDTest {

private Queue queue;

private Session session;

public JMSCorrelationIDTest() throws JMSException {

ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory("vm://localhost");

Connection connection = factory.createConnection();

connection.start();

queue = new ActiveMQQueue("testQueue");

session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);

setupConsumer("ConsumerA");

setupConsumer("ConsumerB");

setupConsumer("ConsumerC");

setupProducer("ProducerA", "ConsumerA");

setupProducer("ProducerB", "ConsumerB");

setupProducer("ProducerC", "ConsumerC");

}

private void setupConsumer(final String name) throws JMSException {

// 创建一个消费者,它只接受属于它自己的消息

MessageConsumer consumer = session.createConsumer(queue, "receiver='" + name + "'");

consumer.setMessageListener(new MessageListener() {

public void onMessage(Message m) {

try {

MessageProducer producer = session.createProducer(queue);

System.out.println(name + " get:" + ((TextMessage) m).getText());

// 回复一个消息

Message replyMessage = session.createTextMessage("Reply from " + name);

// 设置JMSCorrelationID为刚才收到的消息的ID

replyMessage.setJMSCorrelationID(m.getJMSMessageID());

producer.send(replyMessage);

} catch (JMSException e) {

}

}

});

}

private void setupProducer(final String name, String consumerName)

throws JMSException {

MessageProducer producer = session.createProducer(queue);

producer.setDeliveryMode(DeliveryMode.NON_PERSISTENT);

// 创建一个消息,并设置一个属性receiver,为消费者的名字。

Message message = session.createTextMessage("Message from " + name);

message.setStringProperty("receiver", consumerName);

producer.send(message);

// 等待回复的消息

MessageConsumer replyConsumer = session.createConsumer(queue,"JMSCorrelationID='" + message.getJMSMessageID() + "'");

replyConsumer.setMessageListener(new MessageListener() {

public void onMessage(Message m) {

try {

System.out.println(name + " get reply:" + ((TextMessage) m).getText());

} catch (JMSException e) {

}

}

});

}

public static void main(String[] args) throws Exception {

new JMSCorrelationIDTest();

}

}

使用JMS发送消息

在Spring中搭建消息代理(使用ActiveMQ开源消息代理产品)

获取软件ActiveMQ 下载解压,无需安装

添加类路径:将根目录的jar文件添加到应用程序的类路径中。

启动ActiveMQ服务器:Windows版本下载解压后进入bin目录再进入对应CPU运算位数文件夹,运行activemq.bat。

测试成功与否

命令:netstat -an|find "61616" 查看TCP连接端口61616(ActiveMQ默认使用)

浏览器:http://127.0.0.1:8161/admin/ 用户名和密码都是admin

停止服务器:按Ctrl+Shift+C,然后输入y。

使用Spring和AMQP发送消息

消息驱动的POJO

笔记主要来源:

【学习笔记】大数据技术之Kafka3.x(2022版)_新版kafka_在学习的王哈哈的博客-CSDN博客

【学习笔记】大数据技术之Kafka3.x(监控,外部系统集成)_flink kafka3_在学习的王哈哈的博客-CSDN博客

【学习笔记】大数据技术之Kafka3.x(生产调优手册)_大数据调优手册_在学习的王哈哈的博客-CSDN博客

定义

传统定义:Kafka是一种高吞吐量的分布式发布订阅消息队列(Message Queue),主要用于大数据实时处理领域。

最新定义:Kafka是一个开源的分布式事件流平台(Event Streaming Platform),用于高性能数据管道、流分析、数据集成和关键任务应用。

特性

通过O(1)的磁盘数据结构提供消息的持久化,这种结构对于即使数以TB的消息存储也能够保持长时间的稳定性能。

高吞吐量:即使是非常普通的硬件Kafka也可以支持每秒数百万的消息。

支持通过Kafka服务器和消费机集群来分区消息。

支持Hadoop并行数据加载。

发行版本

Confluent Platform

Cloudera Kafka:大数据解决方案

Hortonworks Kafka:大数据解决方案

基础架构

│  

│  

Producer ┼

│  

│  

broker0:

TopicA-Partition0(leader)

TopicA-Partition1(follower)

broker1:

TopicA-Partition1(leader)

TopicA-Partition2(follower)

broker2:

TopicA-Partition2(leader)

TopicA-Partition0(follower)

group:

──→  Consumer

┎─→  Consumer

┃┎→  Consumer

┘┃  group:

┃    Consumer

┃    Consumer

─┘    Consumer

一个Kafka cluster(集群)中,一台Kafka服务器就是一个broker(中介)

为提高扩展性和可用性,一个非常大的topic(主题)分为多个partition(分区),每个partition都有若干replica(副本):一个leader和若干follower实时主从同步。partition的每个replica错开放在不同broker中,当某个broker故障导致其上的leader不可用时,leader对应的可用follower中将选举一个新的leader。Producer和Consumer只通过leader收发消息。

Zookeeper中记录谁是leader:

/brokers/ids/[0,1,2]

/brokers/topics/first/partitions/0/state

"leader":0,"isr":[0,2]

Kafka2.8.0以后也可配置不采用Zookeeper

Consumer Group(CG):消费者组。消费者组内每个消费者负责消费同一个主题不同分区的数据,一个分区只能由一个组内消费者消费,消费者组是逻辑上的一个订阅者,消费者组之间互不影响。

集群规划

hadoop102

hadoop103

hadoop104

zk

zk

zk

kafka

kafka

kafka

下载地址Apache Kafka

本例下载的是kafka_2.12-3.0.0.tgz,版本号2.12是指Kafka的broker部分由Scala2.12编译而成(Producer和Consumer部分由Java编译),版本号3.0.0是Kafka的版本号。

集群部署

1)解压:

[atguigu@hadoop102 software]$ tar -zxvf kafka_2.12-3.0.0.tgz -C /opt/module/

2)改名:

[atguigu@hadoop102 software]$ cd /opt/module/

[atguigu@hadoop102 module]$ mv kafka_2.12-3.0.0/ kafka

3)修改配置文件(黄色标修改):

[atguigu@hadoop102 module]$ cd kafka/config/

[atguigu@hadoop102 config]$ vim server.properties

#broker 的全局唯一编号,不能重复,只能是数字。

broker.id=0

#处理网络请求的线程数量

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 运行日志(数据)存放的路径,路径不需要提前创建,kafka 自动帮你创建,可以配置多个磁盘路径,路径与路径之间可以用","分隔

log.dirs=/opt/module/kafka/datas

#topic 在当前 broker 上的分区个数

num.partitions=1

#用来恢复和清理 data 下数据的线程数量

num.recovery.threads.per.data.dir=1

# 每个 topic 创建时的副本数,默认时 1 个副本

offsets.topic.replication.factor=1

#segment 文件保留的最长时间,超时将被删除

log.retention.hours=168

#每个 segment 文件的大小,默认最大 1G

log.segment.bytes=1073741824

# 检查过期数据的时间,默认 5 分钟检查一次是否数据过期

log.retention.check.interval.ms=300000

#配置连接 Zookeeper 集群地址(在 zk 根目录下创建/kafka,方便管理)

zookeeper.connect=hadoop102:2181,hadoop103:2181,hadoop104:2181/kafka

......

4)分发安装包

[atguigu@hadoop102 config]$ cd /opt/module/

[atguigu@hadoop102 module]$ xsync kafka/

5)分别在 hadoop103 和 hadoop104 上修改配置文件/opt/module/kafka/config/server.properties中的 broker.id=1、broker.id=2

6)配置环境变量

[atguigu@hadoop102 module]$ sudo vim /etc/profile.d/my_env.sh

新增以下环境变量:

#KAFKA_HOME

export KAFKA_HOME=/opt/module/kafka

export PATH=$PATH:$KAFKA_HOME/bin

刷新环境变量:

[atguigu@hadoop102 module]$ source /etc/profile

  1. 分发环境变量

[atguigu@hadoop102 module]$ sudo /home/atguigu/bin/xsync /etc/profile.d/my_env.sh

在分发到的节点上都刷新环境变量:

[atguigu@hadoop103 module]$ source /etc/profile

[atguigu@hadoop104 module]$ source /etc/profile

  1. 启动集群

先启动 Zookeeper 集群:

[atguigu@hadoop102 module]$ cd kafka/

[atguigu@hadoop102 kafka]$ zk.sh start

依次在 hadoop102、hadoop103、hadoop104 节点上启动 Kafka:

[atguigu@hadoop102 kafka]$ bin/kafka-server-start.sh -daemonconfig/server.properties

[atguigu@hadoop103 kafka]$ bin/kafka-server-start.sh -daemonconfig/server.properties

[atguigu@hadoop104 kafka]$ bin/kafka-server-start.sh -daemonconfig/server.properties

  1. 关闭集群

[atguigu@hadoop102 kafka]$ bin/kafka-server-stop.sh

[atguigu@hadoop103 kafka]$ bin/kafka-server-stop.sh

[atguigu@hadoop104 kafka]$ bin/kafka-server-stop.sh

  1. 集群启停脚本

[atguigu@hadoop102 kafka]$ cd /home/atguigu/bin

[atguigu@hadoop102 bin]$ vim kf.sh

#! /bin/bash

case $1 in

"start"){

 for i in hadoop102 hadoop103 hadoop104

 do

 echo " --------启动 $i Kafka-------"

 ssh $i "/opt/module/kafka/bin/kafka-server-start.sh -daemon /opt/module/kafka/config/server.properties"

 done

};;

"stop"){

 for i in hadoop102 hadoop103 hadoop104

 do

 echo " --------停止 $i Kafka-------"

 ssh $i "/opt/module/kafka/bin/kafka-server-stop.sh "

 done

};;

esac

添加执行权限

[atguigu@hadoop102 bin]$ chmod +x kf.sh

启动集群命令

[atguigu@hadoop102 ~]$ kf.sh start

停止集群命令

[atguigu@hadoop102 ~]$ kf.sh stop

注意:停止 Kafka 集群时,一定要等 Kafka 所有节点进程全部停止后再停止 Zookeeper集群。因为 Zookeeper 集群当中记录着 Kafka 集群相关信息,Zookeeper 集群一旦先停止,Kafka 集群就没有办法再获取停止进程的信息,只能手动杀死 Kafka 进程了。

操作主题

查看操作主题命令参数

[atguigu@hadoop102 kafka]$ bin/kafka-topics.sh

查看当前服务器中的所有 topic(选项说明:--bootstrap-server 连接的 Kafka Broker 主机名称和端口号)

[atguigu@hadoop102 kafka]$ bin/kafka-topics.sh --bootstrap-server hadoop102:9092 --list

创建 first topic(选项说明:--topic 定义topic名;--partitions 定义分区数;--replication-factor 定义副本数)

[atguigu@hadoop102 kafka]$ bin/kafka-topics.sh --bootstrap-server hadoop102:9092 --create --partitions 1 --replication-factor 3 --topic first

查看 first 主题的详情

[atguigu@hadoop102 kafka]$ bin/kafka-topics.sh --bootstrap-server hadoop102:9092 --describe --topic first

修改分区数(注意:分区数只能增加,不能减少)

[atguigu@hadoop102 kafka]$ bin/kafka-topics.sh --bootstrap-server hadoop102:9092 --alter --topic first --partitions 3

再次查看 first 主题的详情

[atguigu@hadoop102 kafka]$ bin/kafka-topics.sh --bootstrap-server hadoop102:9092 --describe --topic first

删除 topic

[atguigu@hadoop102 kafka]$ bin/kafka-topics.sh --bootstrap-server hadoop102:9092 --delete --topic first

操作生产者

查看操作生产者命令参数

[atguigu@hadoop102 kafka]$ bin/kafka-console-producer.sh

发送消息

[atguigu@hadoop102 kafka]$ bin/kafka-console-producer.sh --bootstrap-server hadoop102:9092 --topic first

>hello world

>atguigu atguigu

操作消费者

查看操作消费者命令参数

[atguigu@hadoop102 kafka]$ bin/kafka-console-consumer.sh

消费 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

生产者消息发送流程

发送原理:

消息发送涉及到两个线程:main 线程和 sender 线程。在 main 线程中创建了一个 RecordAccumulator(消息累加器/缓冲区)。main 线程将消息发送给 RecordAccumulator,sender 线程不断从 RecordAccumulator 中拉取消息发送到 Kafka Broker。

发送流程:

  1. main线程创建生产者Producer
  2. Producer发送消息(send(ProducerRecord))
  3. Interceptors拦截器处理(可选)
  4. Serializer序列化器处理
  5. Partitioner分区器处理
  6. 发送到RecordAccumulator消息累加器。RecordAccumulator默认大小32M,其内部为每个分区都维护了一个双端缓冲队列,队列中的内容是 ProducerBatch。
  7. ProducerBatch大小累加到batch.size(默认16K)或时间累加到linger.ms(默认0,即来一条消息立即拉走)时,sender线程从RecordAccumulator 拉取消息发送到Kafka Broker,默认为每个Broker节点缓存5个请求。
  8. Kafka Broker应答acks。如果acks成功,则清除队列中的消息。如果acks失败,则重试发送消息。

不带回调函数的异步发送API

依赖

<dependency>

<groupId>org.apache.kafka</groupId>

<artifactId>kafka-clients</artifactId>

<version>3.0.0</version>

</dependency>

代码

package com.wanghaha.kafka.producer;

import org.apache.kafka.clients.producer.KafkaProducer;

import org.apache.kafka.clients.producer.ProducerConfig;

import org.apache.kafka.clients.producer.ProducerRecord;

import org.apache.kafka.common.serialization.StringSerializer;

import java.util.Properties;

public class CustomProducer {

    public static void main(String[] args) {

        // 0. 配置

        Properties properties = new Properties();

        // 连接集群

        properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"hadoop102:9092,hadoop103:9092");

        // 指定对应的kay和value序列化类型

        properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());

        properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,StringSerializer.class.getName());

        // 1. 创建kafka生产者对象

        // "" hello

        KafkaProducer<String, String> kafkaProducer = new KafkaProducer<>(properties);

        // 2. 发送数据

        for (int i = 0; i < 5; i++) {

            kafkaProducer.send(new ProducerRecord<>("first", "wanghaha"+i));

        }

        // 3. 关闭资源

        kafkaProducer.close();

    }

}

测试

在 hadoop102 上开启 Kafka 消费者

[atguigu@hadoop103 kafka]$ bin/kafka-console-consumer.sh --bootstrap-server hadoop102:9092 --topic first

在 IDEA 中执行代码,观察 hadoop102 控制台中是否接收到消息

[atguigu@hadoop102 kafka]$ bin/kafka-console-consumer.sh --bootstrap-server hadoop102:9092 --topic first

atguigu 0

atguigu 1

atguigu 2

atguigu 3

atguigu 4

带回调函数的异步发送API

回调函数会在 producer 收到 ack 时调用,为异步调用,该方法有两个参数,分别是元数据信息(RecordMetadata)和异常信息(Exception),如果 Exception 为 null,说明消息发送成功,如果 Exception 不为 null,说明消息发送失败。

注意:消息发送失败会自动重试,不需要我们在回调函数中手动重试。

依赖

同前

代码

package com.atguigu.kafka.producer;

import org.apache.kafka.clients.producer.*;

import java.util.Properties;

public class CustomProducerCallback {

    public static void main(String[] args) throws InterruptedException {

        // 1. 创建 kafka 生产者的配置对象

        Properties properties = new Properties();

        // 2. 给 kafka 配置对象添加配置信息

        properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "hadoop102:9092");

        // key,value 序列化(必须):key.serializer,value.serializer

        properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());

        properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());

        // 3. 创建 kafka 生产者对象

        KafkaProducer<String, String> kafkaProducer = new KafkaProducer<String, String>(properties);

        // 4. 调用 send 方法,发送消息

        for (int i = 0; i < 5; i++) {

            // 添加回调

            kafkaProducer.send(new ProducerRecord<>("first", "atguigu " + i), new Callback() {

                // 该方法在 Producer 收到 ack 时调用,为异步调用

                @Override

                public void onCompletion(RecordMetadata metadata, Exception exception) {

                    if (exception == null) {

                        // 没有异常,输出信息到控制台

                        System.out.println(" 主题: " + metadata.topic() + "->" + "分区:" + metadata.partition());

                    } else {

                        // 出现异常打印

                        exception.printStackTrace();

                    }

                }

            });

            // 延迟一会会看到数据发往不同分区

            Thread.sleep(2);

        }

        // 5. 关闭资源

        kafkaProducer.close();

    }

}

测试

在 hadoop102 上开启 Kafka 消费者

[atguigu@hadoop103 kafka]$ bin/kafka-console-consumer.sh --bootstrap-server hadoop102:9092 --topic first

在 IDEA 中执行代码,观察 hadoop102 控制台中是否接收到消息

[atguigu@hadoop102 kafka]$ bin/kafka-console-consumer.sh --bootstrap-server hadoop102:9092 --topic first

atguigu 0

atguigu 1

atguigu 2

atguigu 3

atguigu 4

在 IDEA 控制台观察回调信息

主题:first->分区:0

主题:first->分区:0

主题:first->分区:1

主题:first->分区:1

主题:first->分区:1

同步发送API

只需在异步发送的基础上,再调用一下 get()方法即可

package com.atguigu.kafka.producer;

import org.apache.kafka.clients.producer.KafkaProducer;

import org.apache.kafka.clients.producer.ProducerConfig;

import org.apache.kafka.clients.producer.ProducerRecord;

import java.util.Properties;

import java.util.concurrent.ExecutionException;

public class CustomProducerSync {

 public static void main(String[] args) throws

InterruptedException, ExecutionException {

 // 1. 创建 kafka 生产者的配置对象

 Properties properties = new Properties();

 // 2. 给 kafka 配置对象添加配置信息

properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"hadoop102

:9092");

 // key,value 序列化(必须):key.serializer,value.serializer

 properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG,

StringSerializer.class.getName());

properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,

StringSerializer.class.getName());

 // 3. 创建 kafka 生产者对象

 KafkaProducer<String, String> kafkaProducer = new

KafkaProducer<String, String>(properties);

 // 4. 调用 send 方法,发送消息

 for (int i = 0; i < 10; i++) {

 // 异步发送 默认

// kafkaProducer.send(new

ProducerRecord<>("first","kafka" + i));

 // 同步发送

 kafkaProducer.send(new

ProducerRecord<>("first","kafka" + i)).get();

 }

 // 5. 关闭资源

 kafkaProducer.close();

 }

}

测试

在 hadoop102 上开启 Kafka 消费者

[atguigu@hadoop103 kafka]$ bin/kafka-console-consumer.sh --bootstrap-server hadoop102:9092 --topic first

在 IDEA 中执行代码,观察 hadoop102 控制台中是否接收到消息

[atguigu@hadoop102 kafka]$ bin/kafka-console-consumer.sh --bootstrap-server hadoop102:9092 --topic first

atguigu 0

atguigu 1

atguigu 2

atguigu 3

atguigu 4

生产者分区

指定分区

可在ProducerRecord的构造方法中指定partition。

key哈希取余分区

如果没有指定partition,但指定了key,那么Kafka会将key的hash值与topic的partition数进行取余得到partition值。

黏性分区

如果既没有指定partition又没有指定key,那么Kafka将使用Sticky Partition(黏性分区器):随机选择一个分区,并尽可能一直使用该分区,待该分区的batch.size已满或linger.ms时间到,再随机选择另一个分区。

自定义分区

例如我们要实现一个自定义分区,发送的消息如果包含 atguigu,就发往 0 号分区,不包含 atguigu,就发往 1 号分区。

1)定义自定义分区器实现 Partitioner 接口,重写 partition(...)方法

package com.atguigu.kafka.producer;

import org.apache.kafka.clients.producer.Partitioner;

import org.apache.kafka.common.Cluster;

import java.util.Map;

import java.util.Optional;

/**

 * 自定义分区器

 */

public class MyPartitioner implements Partitioner {

    /**

     * 返回信息对应的分区

     */

    @Override

public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {

        String msgValue = Optional.ofNullable(value.toString()).orElse("");

        return msgValue.contains("atguigu") ? 0 : 1;

    }

    // 关闭资源

    @Override

    public void close() {

    }

    // 配置方法

    @Override

    public void configure(Map<String, ?> configs) {

    }

}

  1. 在生产者配置中添加自定义分区器参数

......

        properties.put(ProducerConfig.PARTITIONER_CLASS_CONFIG, MyPartitioner.class.getName());

......

生产者如何保证发送消息数据不丢失

acks应答级别:

配置

含义

可靠性、效率

生产使用情况

说明

acks=0

生产者发送过来数据,不需要等数据落盘应答

可靠性差

效率高

很少使用

acks=1

生产者发送过来数据,Leader收到数据后应答

可靠性中等

效率中等

传输普通日志

acks=1时丢数据的情形:Leader应答完成,准备同步副本时Leader挂了,新Leader无数据。

acks=-1(all)

生产者发送过来数据,Leader和ISR队列里的所有节点收齐数据后应答

可靠性高

效率低

传输和钱相关的数据

ISR:Leader维护了一个动态的in-sync replica set(ISR),意为和Leader保持同步的Follower+Leader集合(leader:0,isr:0,1,2),如果Follower长时间未向Leader发送通信请求或同步数据,则该Follower将被踢出ISR。该时间阈值由replica.lag.time.max.ms参数设定,默认30s。

如果分区副本设置为1个,或者ISR里应答的最小副本数量(min.insync.replicas默认为1)设置为1,和acks=0的效果是一样的,仍然有丢数据风险(leader:0,isr:0)

数据完全可靠条件 = acks设为-1 + 分区副本>=2 + ISR应答的最小副本数量>=2

代码配置

......

// 设置 acks

 properties.put(ProducerConfig.ACKS_CONFIG, "all");

 // 重试次数 retries,默认是 int 最大值,2147483647

 properties.put(ProducerConfig.RETRIES_CONFIG, 3);

......

生产者如何保证发送消息数据不重复

Kafka 0.11版本以后,引入了一项重大特性:幂等性和事务,即不论Producer向Broker发送多少次重复数据,Broker只会持久化一条。

重复数据的判断标准:消息具有<PID, Partition, SeqNumber>相同主键。其中PID是Kafka每次重启都会分配一个新的;Partition表示分区号;SeqNumber是单调自增的。所以幂等性只能保证的是在单分区单会话内不重复。

如何使用幂等性:开启参数enable.idempotence默认为true。

生产者事务

原理

  1. Producer向Transaction Coordinator(事务协调器)请求producer id(幂等性需要,Producer有了PID,即使重启也能处理未完成的事务)
  2. Coordinator返回producer id
  3. Producer发送消息到Topic
  4. Producer向Coordinator发送commit请求
  5. Coordinator将commit请求信息持久化到__transaction_state-分区-Leader(存储事务信息的特殊主题)
  6. Coordinator返回commit请求成功信息给Producer
  7. Coordinator将commit请求提交到Topic
  8. Topic将commit成功信息返回给Coordinator
  9. Coordinator将commit成功信息持久化到__transaction_state-分区-Leader

代码

package com.wanghaha.kafka.producer;

import org.apache.kafka.clients.producer.KafkaProducer;

import org.apache.kafka.clients.producer.ProducerConfig;

import org.apache.kafka.clients.producer.ProducerRecord;

import org.apache.kafka.common.errors.ProducerFencedException;

import org.apache.kafka.common.serialization.StringSerializer;

import java.util.Properties;

public class CustomProducerTranactions {

    public static void main(String[] args) {

        // 0. 配置

        Properties properties = new Properties();

        // 连接集群

        properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"hadoop102:9092,hadoop103:9092");

        // 指定对应的kay和value序列化类型

        properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());

        properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,StringSerializer.class.getName());

        // 指定事务id

        properties.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG,"tranactional_id_01");

        // 1. 创建kafka生产者对象

        // "" hello

        KafkaProducer<String, String> kafkaProducer = new KafkaProducer<>(properties);

        kafkaProducer.initTransactions();

        kafkaProducer.beginTransaction();

        try {

            // 2. 发送数据

            for (int i = 0; i < 5; i++) {

                kafkaProducer.send(new ProducerRecord<>("first", "wanghaha"+i));

            }

            int i = 1/0;

            

            kafkaProducer.commitTransaction();

        } catch (Exception e) {

            kafkaProducer.abortTransaction();

        }finally {

            // 3. 关闭资源

            kafkaProducer.close();

        }

    }

}

生产者如何保证发送消息数据有序

Kafka在1.x版本之前保证数据单分区有序,条件是:max.in.flight.requests.per.connection=1(缓冲队列最多放一个请求,不需考虑是否开启幂等性)

Kafka在1.x及以后版本保证数据单分区有序,条件是:未开启幂等性时,上述参数值需设置为1;开启幂等性时,上述参数值需设置为小于等于5(启用幂等性后broker会保证缓存的最近5个请求根据SeqNumber排序)

Kafka无法保证分区与分区间有序。

    1. Broker

Zookeeper 存储的 Kafka 信息

启动 Zookeeper 客户端:

[atguigu@hadoop102 zookeeper-3.5.7]$ bin/zkCli.sh

通过 ls 命令可以查看 kafka 相关信息

[zk: localhost:2181(CONNECTED) 2] ls /kafka

可使用PrettyZoo App更方便地查看kafaka目录

/kafka目录

/kafka

admin

delete_topics

brokers

ids

[0,1,2]

topics

first

partitions

0

state

{"controller_epoch":21,"leader":1,"version":1,"leader_epoch":17,"isr":[1,0,2]}

1

state

{"controller_epoch":21,"leader":2,"version":1,"leader_epoch":15,"isr":[2,1,0]}

2

state

{"controller_epoch":21,"leader":0,"version":1,"leader_epoch":19,"isr":[0,1,2]}

seqid

cluster

id

consumers

0.9版本之前用于保存offset信息,0.9版本之后offset存储在kafka主题中

controller

{"version":1,"brokerid":0,"timestamp":"1635907476"}

config

brokers

changes

topics

clients

users

Controller_epoch

Isr_change_notification

latest_producer_id_block

log_dir_event_notification

Kafka Broker 总体工作流程

1)broker启动后在Zookeeper中注册:

/brokers/ids/  [0,1,2]

  1. 哪个broker的Controller先注册到/kafka/controller(临时有序节点),该Controller就决定Leader的选举:

controller  "brokerid":0

  1. 选举规则:在ISR中存活为前提,按照AR(All Replica)中排在前面的优先。例如:ar[1,0,2],isr[1,0,2],那么就会按照1,0,2的顺序轮询
  2. Controller将节点信息上传到Zookeeper,其他Controller从Zookeeper同步信息:

/borkers/topics/first/partitions/0/state  "leader":0,"isr":[1,0,2]

  1. 假设broker1中Leader挂了,Controller监听到/kafka/brokers/ids节点变化,就去/borkers/topics/first/partitions/0/state获取ISR,选举新的Leader,再将新Leader和更新的ISR信息上传到/borkers/topics/first/partitions/0/state,其他Controller从/borkers/topics/first/partitions/0/state同步信息

模拟 Kafka 上下线,Zookeeper 中数据变化

停止 hadoop104 上的 kafka

[atguigu@hadoop104 kafka]$ bin/kafka-server-stop.sh

启动 hadoop104 上的 kafka

[atguigu@hadoop104 kafka]$ bin/kafka-server-start.sh -daemon ./config/server.properties

查看/kafka/brokers/ids 路径上的节点

[zk: localhost:2181(CONNECTED) 2] ls /kafka/brokers/ids

[0, 1, 2]

[0, 1]

查看/kafka/controller 路径上的数据

[zk: localhost:2181(CONNECTED) 15] get /kafka/controller

{"version":1,"brokerid":0,"timestamp":"1637292471777"}

{"version":1,"brokerid":0,"timestamp":"1637292471777"}

查看/kafka/brokers/topics/first/partitions/0/state 路径上的数据

[zk: localhost:2181(CONNECTED) 16] get /kafka/brokers/topics/first/partitions/0/state

{"controller_epoch":24,"leader":0,"version":1,"leader_epoch":18,"isr":[0,1,2]}

{"controller_epoch":24,"leader":0,"version":1,"leader_epoch":18,"isr":[0,1]}

服役新节点

1)新节点准备

(1)关闭 hadoop104,并右键执行克隆操作。

(2)开启 hadoop105,使用root用户登录,修改 IP 地址。

[root@hadoop104 ~]# vim /etc/sysconfig/network-scripts/ifcfg-ens33

DEVICE=ens33

TYPE=Ethernet

ONBOOT=yes

BOOTPROTO=static

NAME="ens33"

IPADDR=192.168.10.105

PREFIX=24

GATEWAY=192.168.10.2

DNS1=192.168.10.2

(3)在 hadoop105 上,修改主机名称为 hadoop105。

[root@hadoop104 ~]# vim /etc/hostname

hadoop105

(4)重新启动 hadoop104、hadoop105。

(5)删除 hadoop105 中 kafka 下的 datas 和 logs。

[atguigu@hadoop105 ~]$ cd /opt/module/kafka/

[atguigu@hadoop105 kafka]$ rm -rf datas/* logs/*

  1. 修改 haodoop105 中 kafka 的 broker.id 为 3。

[atguigu@hadoop105 kafka]$ cd config/

[atguigu@hadoop105 config]$ vim server.properties

broker.id=3

(7)启动 hadoop102、hadoop103、hadoop104 上的 zk和kafka 集群。

[atguigu@hadoop102 ~]$ zk.sh start

[atguigu@hadoop102 ~]$ kf.sh start

(8)单独启动 hadoop105 中的 kafka。

[atguigu@hadoop105 kafka]$ bin/kafka-server-start.sh -daemon ./config/server.properties

  1. 执行负载均衡操作

(0)查看当前负载均衡:

[wanghaha@hadoop102 kafka]$ bin/kafka-topics.sh --bootstrap-server hadoop102:9092 --topic first --describe

Topic: first TopicId: 4DtkHPe4R1KyXNF7QyVqBA PartitionCount: 3 ReplicationFactor: 3 Configs: segment.bytes=1073741824

Topic: first Partition: 0 Leader: 1 Replicas: 2,1,0 Isr: 1,0

Topic: first Partition: 1 Leader: 0 Replicas: 0,1,2 Isr: 0,1

Topic: first Partition: 2 Leader: 1 Replicas: 1,2,0 Isr: 1,0

(1)创建一个要均衡的主题。

[atguigu@hadoop102 kafka]$ vim topics-to-move.json

{

 "topics": [

 {"topic": "first"}

 ],

 "version": 1

}

(2)生成一个负载均衡的计划

[atguigu@hadoop102 kafka]$ bin/kafka-reassign-partitions.sh --bootstrap-server hadoop102:9092 --topics-to-move-json-file topics-to-move.json --broker-list "0,1,2,3" --generate

Current partition replica assignment

{"version":1,"partitions":[{"topic":"first","partition":0,"replicas":[0,2,1],"log_dirs":["any","any","any"]},{"topic":"first","partition":1,"replicas":[2,1,0],"log_dirs":["any","any","any"]},{"topic":"first","partition":2,"replicas":[1,0,2],"log_dirs":["any","any","any"]}]}

Proposed partition reassignment configuration

{"version":1,"partitions":[{"topic":"first","partition":0,"replicas":[2,3,0],"log_dirs":["any","any","any"]},{"topic":"first","partition":1,"replicas":[3,0,1],"log_dirs":["any","any","any"]},{"topic":"first","partition":2,"replicas":[0,1,2],"log_dirs":["any","any","any"]}]}

(3)创建副本存储计划(所有副本存储在 broker0、broker1、broker2、broker3 中)。

[atguigu@hadoop102 kafka]$ vim increase-replication-factor.json

输入如下内容(刚生成的计划):

{"version":1,"partitions":[{"topic":"first","partition":0,"replicas":[2,3,0],"log_dirs":["any","any","any"]},{"topic":"first","partition":1,"replicas":[3,0,1],"log_dirs":["any","any","any"]},{"topic":"first","partition":2,"replicas":[0,1,2],"log_dirs":["any","any","any"]}]}

(4)执行副本存储计划。

[atguigu@hadoop102 kafka]$ bin/kafka-reassign-partitions.sh --bootstrap-server hadoop102:9092 --reassignment-json-file increase-replication-factor.json --execute

(5)验证副本存储计划。

[atguigu@hadoop102 kafka]$ bin/kafka-reassign-partitions.sh --bootstrap-server hadoop102:9092 --reassignment-json-file increase-replication-factor.json --verify

Status of partition reassignment:

Reassignment of partition first-0 is complete.

Reassignment of partition first-1 is complete.

Reassignment of partition first-2 is complete.

Clearing broker-level throttles on brokers 0,1,2,3

Clearing topic-level throttles on topic first

退役旧节点

1)执行负载均衡操作

先按照退役一台节点,生成执行计划,然后按照服役时操作流程执行负载均衡。

(1)创建一个要均衡的主题。

[atguigu@hadoop102 kafka]$ vim topics-to-move.json

{

 "topics": [

 {"topic": "first"}

 ],

 "version": 1

}

(2)创建执行计划。

[atguigu@hadoop102 kafka]$ bin/kafka-reassign-partitions.sh --bootstrap-server hadoop102:9092 --topics-to-move-json-file topics-to-move.json --broker-list "0,1,2" --generate

Current partition replica assignment

{"version":1,"partitions":[{"topic":"first","partition":0,"replicas":[2,0,1],"log_dirs":["any","any","any"]},{"topic":"first","partition":1,"replicas":[3,1,2],"log_dirs":["any","any","any"]},{"topic":"first","partition":2,"replicas":[0,2,3],"log_dirs":["any","any","any"]}]}

Proposed partition reassignment configuration

{"version":1,"partitions":[{"topic":"first","partition":0,"replicas":[2,0,1],"log_dirs":["any","any","any"]},{"topic":"first","partition":1,"replicas":[0,1,2],"log_dirs":["any","any","any"]},{"topic":"first","partition":2,"replicas":[1,2,0],"log_dirs":["any","any","any"]}]}

(3)创建副本存储计划(所有副本存储在 broker0、broker1、broker2 中)。

[atguigu@hadoop102 kafka]$ vim increase-replication-factor.json

输入如下内容(刚生成的计划):

{"version":1,"partitions":[{"topic":"first","partition":0,"replicas":[2,0,1],"log_dirs":["any","any","any"]},{"topic":"first","partition":1,"replicas":[0,1,2],"log_dirs":["any","any","any"]},{"topic":"first","partition":2,"replicas":[1,2,0],"log_dirs":["any","any","any"]}]}

(4)执行副本存储计划。

[atguigu@hadoop102 kafka]$ bin/kafka-reassign-partitions.sh --bootstrap-server hadoop102:9092 --reassignment-json-file

increase-replication-factor.json --execute

(5)验证副本存储计划。

[atguigu@hadoop102 kafka]$ bin/kafka-reassign-partitions.sh --bootstrap-server hadoop102:9092 --reassignment-json-file increase-replication-factor.json --verify

Status of partition reassignment:

Reassignment of partition first-0 is complete.

Reassignment of partition first-1 is complete.

Reassignment of partition first-2 is complete.

Clearing broker-level throttles on brokers 0,1,2,3

Clearing topic-level throttles on topic first

2)在 hadoop105 上执行停止命令

[atguigu@hadoop105 kafka]$ bin/kafka-server-stop.sh

Kafka副本

副本基本信息

(1)Kafka 副本作用:提高数据可靠性。

(2)Kafka 默认副本 1 个,生产环境一般配置为 2 个,保证数据可靠性;太多副本会增加磁盘存储空间,增加网络上数据传输,降低效率。

(3)Kafka 中副本分为:Leader 和 Follower。Kafka 生产者只会把数据发往 Leader,然后 Follower 找 Leader 进行同步数据。

(4)Kafka 分区中的所有副本统称为 AR(Assigned Repllicas)。

AR = ISR + OSR

ISR,表示和 Leader 保持同步的 Follower 集合。如果 Follower 长时间未向 Leader 发送通信请求或同步数据,则该 Follower 将被踢出 ISR。该时间阈值由 replica.lag.time.max.ms参数设定,默认 30s。Leader 发生故障之后,就会从 ISR 中选举新的 Leader。

OSR,表示 Follower 与 Leader 副本同步时,延迟过多的副本。

Leader 选举流程

Kafka 集群中有一个 broker 的 Controller 会被选举为 Controller Leader,负责管理集群broker 的上下线,所有 topic 的分区副本分配和Leader选举等工作。

Controller 的信息同步工作是依赖于 Zookeeper 的。

(1)创建一个新的 topic,4 个分区,4 个副本

[atguigu@hadoop102 kafka]$ bin/kafka-topics.sh --bootstrap-server hadoop102:9092 --create --topic atguigu1 --partitions 4 --replication-factor

Created topic atguigu1.

(2)查看 Leader 分布情况

[atguigu@hadoop102 kafka]$ bin/kafka-topics.sh --bootstrap-server hadoop102:9092 --describe --topic atguigu1

Topic: atguigu1 TopicId: awpgX_7WR-OX3Vl6HE8sVg PartitionCount: 4 ReplicationFactor: 4

Configs: segment.bytes=1073741824

Topic: atguigu1 Partition: 0 Leader: 3 Replicas: 3,0,2,1 Isr: 3,0,2,1

Topic: atguigu1 Partition: 1 Leader: 1 Replicas: 1,2,3,0 Isr: 1,2,3,0

Topic: atguigu1 Partition: 2 Leader: 0 Replicas: 0,3,1,2 Isr: 0,3,1,2

Topic: atguigu1 Partition: 3 Leader: 2 Replicas: 2,1,0,3 Isr: 2,1,0,3

(3)停止掉 hadoop105 的 kafka 进程,并查看 Leader 分区情况

[atguigu@hadoop105 kafka]$ bin/kafka-server-stop.sh

[atguigu@hadoop102 kafka]$ bin/kafka-topics.sh --bootstrap-server hadoop102:9092 --describe --topic atguigu1

Topic: atguigu1 TopicId: awpgX_7WR-OX3Vl6HE8sVg PartitionCount: 4 ReplicationFactor: 4

Configs: segment.bytes=1073741824

Topic: atguigu1 Partition: 0 Leader: 0 Replicas: 3,0,2,1 Isr: 0,2,1

Topic: atguigu1 Partition: 1 Leader: 1 Replicas: 1,2,3,0 Isr: 1,2,0

Topic: atguigu1 Partition: 2 Leader: 0 Replicas: 0,3,1,2 Isr: 0,1,2

Topic: atguigu1 Partition: 3 Leader: 2 Replicas: 2,1,0,3 Isr: 2,1,0

(4)停止掉 hadoop104 的 kafka 进程,并查看 Leader 分区情况

[atguigu@hadoop104 kafka]$ bin/kafka-server-stop.sh

[atguigu@hadoop102 kafka]$ bin/kafka-topics.sh --bootstrap-server hadoop102:9092 --describe --topic atguigu1

Topic: atguigu1 TopicId: awpgX_7WR-OX3Vl6HE8sVg PartitionCount: 4 ReplicationFactor: 4

Configs: segment.bytes=1073741824

Topic: atguigu1 Partition: 0 Leader: 0 Replicas: 3,0,2,1 Isr: 0,1

Topic: atguigu1 Partition: 1 Leader: 1 Replicas: 1,2,3,0 Isr: 1,0

Topic: atguigu1 Partition: 2 Leader: 0 Replicas: 0,3,1,2 Isr: 0,1

Topic: atguigu1 Partition: 3 Leader: 1 Replicas: 2,1,0,3 Isr: 1,0

(5)启动 hadoop105 的 kafka 进程,并查看 Leader 分区情况

[atguigu@hadoop105 kafka]$ bin/kafka-server-start.sh -daemon config/server.properties

[atguigu@hadoop102 kafka]$ bin/kafka-topics.sh --bootstrap-server hadoop102:9092 --describe --topic atguigu1

Topic: atguigu1 TopicId: awpgX_7WR-OX3Vl6HE8sVg PartitionCount: 4 ReplicationFactor: 4

Configs: segment.bytes=1073741824

Topic: atguigu1 Partition: 0 Leader: 0 Replicas: 3,0,2,1 Isr: 0,1,3

Topic: atguigu1 Partition: 1 Leader: 1 Replicas: 1,2,3,0 Isr: 1,0,3

Topic: atguigu1 Partition: 2 Leader: 0 Replicas: 0,3,1,2 Isr: 0,1,3

Topic: atguigu1 Partition: 3 Leader: 1 Replicas: 2,1,0,3 Isr: 1,0,3

(6)启动 hadoop104 的 kafka 进程,并查看 Leader 分区情况

[atguigu@hadoop104 kafka]$ bin/kafka-server-start.sh -daemon config/server.properties

[atguigu@hadoop102 kafka]$ bin/kafka-topics.sh --bootstrap-server hadoop102:9092 --describe --topic atguigu1

Topic: atguigu1 TopicId: awpgX_7WR-OX3Vl6HE8sVg PartitionCount: 4 ReplicationFactor: 4

Configs: segment.bytes=1073741824

Topic: atguigu1 Partition: 0 Leader: 0 Replicas: 3,0,2,1 Isr: 0,1,3,2

Topic: atguigu1 Partition: 1 Leader: 1 Replicas: 1,2,3,0 Isr: 1,0,3,2

Topic: atguigu1 Partition: 2 Leader: 0 Replicas: 0,3,1,2 Isr: 0,1,3,2

Topic: atguigu1 Partition: 3 Leader: 1 Replicas: 2,1,0,3 Isr: 1,0,3,2

(7)停止掉 hadoop103 的 kafka 进程,并查看 Leader 分区情况

[atguigu@hadoop103 kafka]$ bin/kafka-server-stop.sh

[atguigu@hadoop102 kafka]$ bin/kafka-topics.sh --bootstrap-server hadoop102:9092 --describe --topic atguigu1

Topic: atguigu1 TopicId: awpgX_7WR-OX3Vl6HE8sVg PartitionCount: 4 ReplicationFactor: 4

Configs: segment.bytes=1073741824

Topic: atguigu1 Partition: 0 Leader: 0 Replicas: 3,0,2,1 Isr: 0,3,2

Topic: atguigu1 Partition: 1 Leader: 2 Replicas: 1,2,3,0 Isr: 0,3,2

Topic: atguigu1 Partition: 2 Leader: 0 Replicas: 0,3,1,2 Isr: 0,3,2

Topic: atguigu1 Partition: 3 Leader: 2 Replicas: 2,1,0,3 Isr: 0,3,2

Follower故障处理流程

LEO(Log End Offset):每个副本的最后一个offset,其实就是最新的offset + 1

HW(High Watermark):所有副本中最小的LEO

  1. Follower发生故障后会被临时踢出ISR
  2. 这个期间Leader和Follower继续接收数据
  3. 待该Follower恢复后,Follower会读取本地磁盘记录的上次HW,并将Log文件高于HW的部分截掉(被认为是没有验证过的数据),从HW开始向Leader进行同步
  4. 等该Follower的LEO大于等于该Partition的HW,即Follower追上Leader之后,就可以重新加入ISR了

Leader故障处理流程

  1. Leader故障后,会从ISR中选出一个新的Leader
  2. 为保证多个副本间的数据一致性,其余的Follower会现将各自的log文件高于HW的部分截掉,然后从新的Leader同步数据

注意:这只能保证副本之间的数据一致性,并不能保证数据不丢失或者不重复。

分区副本分配

如果 kafka 服务器只有 4 个节点,那么设置 kafka 的分区数大于服务器台数,在 kafka底层如何分配存储副本呢?

1)创建 16 分区,3 个副本

(1)创建一个新的 topic,名称为 second。

[atguigu@hadoop102 kafka]$ bin/kafka-topics.sh --bootstrap-server hadoop102:9092 --create --partitions 16 --replication-factor 3 --topic second

  1. 查看分区和副本情况。

[atguigu@hadoop102 kafka]$ bin/kafka-topics.sh --bootstrap-server hadoop102:9092 --describe --topic second

Topic: second4 Partition: 0 Leader: 0 Replicas: 0,1,2 Isr: 0,1,2

Topic: second4 Partition: 1 Leader: 1 Replicas: 1,2,3 Isr: 1,2,3

Topic: second4 Partition: 2 Leader: 2 Replicas: 2,3,0 Isr: 2,3,0

Topic: second4 Partition: 3 Leader: 3 Replicas: 3,0,1 Isr: 3,0,1

Topic: second4 Partition: 4 Leader: 0 Replicas: 0,2,3 Isr: 0,2,3

Topic: second4 Partition: 5 Leader: 1 Replicas: 1,3,0 Isr: 1,3,0

Topic: second4 Partition: 6 Leader: 2 Replicas: 2,0,1 Isr: 2,0,1

Topic: second4 Partition: 7 Leader: 3 Replicas: 3,1,2 Isr: 3,1,2

Topic: second4 Partition: 8 Leader: 0 Replicas: 0,3,1 Isr: 0,3,1

Topic: second4 Partition: 9 Leader: 1 Replicas: 1,0,2 Isr: 1,0,2

Topic: second4 Partition: 10 Leader: 2 Replicas: 2,1,3 Isr: 2,1,3

Topic: second4 Partition: 11 Leader: 3 Replicas: 3,2,0 Isr: 3,2,0

Topic: second4 Partition: 12 Leader: 0 Replicas: 0,1,2 Isr: 0,1,2

Topic: second4 Partition: 13 Leader: 1 Replicas: 1,2,3 Isr: 1,2,3

Topic: second4 Partition: 14 Leader: 2 Replicas: 2,3,0 Isr: 2,3,0

Topic: second4 Partition: 15 Leader: 3 Replicas: 3,0,1 Isr: 3,0,1

手动调整分区副本存储

在生产环境中,每台服务器的配置性能不一致,但是Kafka只会根据自己的代码规则创建对应的分区副本,就会导致个别服务器存储压力较大。所有需要手动调整分区副本的存储。

需求:创建一个新的topic,4个分区,2个副本。将该topic的所有副本存储到broker0和broker1两台服务器上。

(1)创建一个新的 topic,名称为 three。

[atguigu@hadoop102 kafka]$ bin/kafka-topics.sh --bootstrap-server hadoop102:9092 --create --partitions 4 --replication-factor 2 --topic three

(2)查看分区副本存储情况。

[atguigu@hadoop102 kafka]$ bin/kafka-topics.sh --bootstrap-server hadoop102:9092 --describe --topic three

(3)创建副本存储计划(所有副本都指定存储在 broker0、broker1 中)。

[atguigu@hadoop102 kafka]$ vim increase-replication-factor.json

输入如下内容:

{"version":1,"partitions":[{"topic":"three","partition":0,"replicas":[0,1]},{"topic":"three","partition":1,"replicas":[0,1]},{"topic":"three","partition":2,"replicas":[1,0]},{"topic":"three","partition":3,"replicas":[1,0]}]}

(4)执行副本存储计划。

[atguigu@hadoop102 kafka]$ bin/kafka-reassign-partitions.sh --bootstrap-server hadoop102:9092 --reassignment-json-file increase-replication-factor.json --execute

(5)验证副本存储计划。

[atguigu@hadoop102 kafka]$ bin/kafka-reassign-partitions.sh --bootstrap-server hadoop102:9092 --reassignment-json-file increase-replication-factor.json --verify

(6)查看分区副本存储情况。

[atguigu@hadoop102 kafka]$ bin/kafka-topics.sh --bootstrap-server hadoop102:9092 --describe --topic three

Leader Partition 负载平衡

正常情况下,Kafka本身会自动把Leader Partition均匀分散在各个机器上,来保证每台机器的读写吞吐量都是均匀的。但是如果某些broker宕机,会导致Leader Partition过于集中在其他少部分几台broker上,这会导致少数几台broker的读写请求压力过高,其他宕机的broker重启之后都是follower partition,读写请求很低,造成集群负载不均衡。

auto.leader.rebalance.enable默认为true,自动Leader Partition 平衡。生产环境中,leader 重选举的代价比较大,可能会带来性能影响,建议设置为false关闭。

leader.imbalance.per.broker.percentage默认10%,每个broker 允许的不平衡的leader的比率。如果每个broker 超过了这个值,控制器会触发leader 的平衡。

leader.imbalance.check.interval.seconds默认值300 秒。检查leader 负载是否平衡的间隔时间。

下面拿一个主题举例说明,假设集群只有一个主题如下所示:

Topic: atguigu1 Partition: 0 Leader: 0 Replicas: 3,0,2,1 Isr: 3,0,2,1

Topic: atguigu1 Partition: 1 Leader: 1 Replicas: 1,2,3,0 Isr: 1,2,3,0

Topic: atguigu1 Partition: 2 Leader: 2 Replicas: 0,3,1,2 Isr: 0,3,1,2

Topic: atguigu1 Partition: 3 Leader: 3 Replicas: 2,1,0,3 Isr: 2,1,0,3

针对broker0节点,分区2的AR优先副本是0节点,但是0节点却不是Leader节点,所以不平衡数加1,AR副本总数是4,所以broker0节点不平衡率为1/4>10%,需要再平衡。

broker2和broker3节点和broker0不平衡率一样,需要再平衡。

Broker1的不平衡数为0,不需要再平衡

增加副本因子

在生产环境当中,由于某个主题的重要等级需要提升,我们考虑增加副本。副本数的增加需要先制定计划,然后根据计划执行。

1)创建 topic

[atguigu@hadoop102 kafka]$ bin/kafka-topics.sh --bootstrap-server hadoop102:9092 --create --partitions 3 --replication-factor 1 --topic four

2)手动增加副本存储

(1)创建副本存储计划(所有副本都指定存储在 broker0、broker1、broker2 中)。

[atguigu@hadoop102 kafka]$ vim increase-replication-factor.json

输入如下内容:

{"version":1,"partitions":[{"topic":"four","partition":0,"replicas":[0,1,2]},{"topic":"four","partition":1,"replicas":[0,1,2]},{"topic":"four","partition":2,"replicas":[0,1,2]}]}

(2)执行副本存储计划。

[atguigu@hadoop102 kafka]$ bin/kafka-reassign-partitions.sh --bootstrap-server hadoop102:9092 --reassignment-json-file increase-replication-factor.json --execute

文件存储机制

存储层次结构

主题Topic-》分区Partition-》副本Replica-》分段Segment(索引、日志)

为防止log文件过大导致数据定位效率低下,Kafka采取了分段和索引机制:

分段:

将每个partition分为多个segment,每个segment包括4个主要文件:

偏移量索引文件.index

时间戳索引文件.timeindex

快照索引文件.snapshot

日志文件.log

同一个分区的这些文件以各自所属segment的第一条消息的offset命名,位于同一个文件夹下,该文件夹的命名规则为:“topic名称-分区号”。例如,first这个topic有三个分区,则其对应的文件夹为first-0,first-1,first-2。

顺序写入:

Producer生产的数据会被不断追加到日志文件.log末端,且每条数据都有自己的offset。消费者组中的每个消费者,都会实时记录自己消费到了哪个offset(保存在Kafka内部的主题__consumer_offsets中),以便出错恢复时,从上次的位置继续消费。

稀疏索引:

每当log文件写入了指定大小(参数log.index.interval.bytes,默认4K)的数据,就往index文件里记录一个索引。

查看数据存储位置

(1)启动生产者,并发送消息。

[atguigu@hadoop102 kafka]$ bin/kafka-console-producer.sh --bootstrap-server hadoop102:9092 --topic first

>hello world

(2)查看 hadoop102(或者 hadoop103、hadoop104)的/opt/module/kafka/datas/first-1(first-0、first-2)路径上的文件。

[atguigu@hadoop104 first-1]$ ls

00000000000000000092.index

00000000000000000092.log

00000000000000000092.snapshot

00000000000000000092.timeindex

leader-epoch-checkpoint

partition.metadata

(3)直接查看 log 日志,发现是乱码。

[atguigu@hadoop104 first-1]$ cat 00000000000000000092.log

\CYnF|©|©ÿ"hello world

(4)通过工具查看 index 和 log 信息。

[atguigu@hadoop104 first-1]$ kafka-run-class.sh kafka.tools.DumpLogSegments --files ./00000000000000000000.index

Dumping ./00000000000000000000.index

offset: 3 position: 152

[atguigu@hadoop104 first-1]$ kafka-run-class.sh kafka.tools.DumpLogSegments --files ./00000000000000000000.log

Dumping datas/first-0/00000000000000000000.log

Starting offset: 0

baseOffset: 0 lastOffset: 1 count: 2 baseSequence: -1 lastSequence: -1 producerId: -1

producerEpoch: -1 partitionLeaderEpoch: 0 isTransactional: false isControl: false position:

0 CreateTime: 1636338440962 size: 75 magic: 2 compresscodec: none crc: 2745337109 isvalid:

true

baseOffset: 2 lastOffset: 2 count: 1 baseSequence: -1 lastSequence: -1 producerId: -1

producerEpoch: -1 partitionLeaderEpoch: 0 isTransactional: false isControl: false position:

75 CreateTime: 1636351749089 size: 77 magic: 2 compresscodec: none crc: 273943004 isvalid:

true

baseOffset: 3 lastOffset: 3 count: 1 baseSequence: -1 lastSequence: -1 producerId: -1

producerEpoch: -1 partitionLeaderEpoch: 0 isTransactional: false isControl: false position:

152 CreateTime: 1636351749119 size: 77 magic: 2 compresscodec: none crc: 106207379 isvalid:

true

baseOffset: 4 lastOffset: 8 count: 5 baseSequence: -1 lastSequence: -1 producerId: -1

producerEpoch: -1 partitionLeaderEpoch: 0 isTransactional: false isControl: false position:

229 CreateTime: 1636353061435 size: 141 magic: 2 compresscodec: none crc: 157376877 isvalid:

true

baseOffset: 9 lastOffset: 13 count: 5 baseSequence: -1 lastSequence: -1 producerId: -1

producerEpoch: -1 partitionLeaderEpoch: 0 isTransactional: false isControl: false position:

370 CreateTime: 1636353204051 size: 146 magic: 2 compresscodec: none crc: 4058582827 isvalid:

true

Record搜索步骤

需求:在log文件中定位到offset=600的Record

Segment-0 [offset0-521]

00000000000000000000.index

00000000000000000000.log

Segment-1 [offset522-1004]

00000000000000000522.index

绝对Offset

相对Offset

Position

587

65

6410

639

117

13795

691

169

21060

743

221

28367

注:为节省空间,index文件中保存的offset是相对offset,因此需要算出绝对offset再找对应position

00000000000000000522.log

RecordBatch[baseOffset-lastOffset]

baseOffset

lastOffset

position

522

522

0

523

523

200

524

536

819

537

562

3092

563

587

6410

588

613

10090

614

639

13795

640

665

17481

666

691

21060

692

717

24781

728

743

28367

Segment-2 [offset1005-]

00000000000000001005.index

00000000000000001005.log

  1. 根据目标offset定位Segment文件
  2. 找到小于等于目标offset的最大offset索引文件
  3. 定位到log文件
  4. 向下遍历找到目标Record

日志存储参数配置

log.segment.bytes:日志划分成块的大小,默认值 1G。

log.index.interval.bytes:每当写入了指定大小(默认4kb)的日志,就往index文件里面记录一个索引(稀疏索引)。

日志清理策略

Kafka日志清理策略(参数log.cleanup.policy)有delete 和 compact 两种。

1)delete 日志删除:将过期数据删除

(1)基于时间:默认打开。以 segment 中所有记录中的最大时间戳作为该文件时间戳。

log.retention.hours 默认7天 数据保存时间。

log.retention.minutes 默认关闭 数据保存时间,分钟级别。

log.retention.ms 默认关闭 数据保存时间,毫秒级别。

log.retention.check.interval.ms 默认5分钟 检查数据是否保存超时的间隔。

(2)基于大小:默认关闭。超过设置的所有日志总大小,删除最早的 segment。

log.retention.bytes,默认等于-1,表示无穷大。

2)compact 日志压缩:对于相同key的不同value值,只保留最后一个版本

压缩后的offset可能是不连续的,当从压缩前的offset消费消息时,可能会拿到比压缩前offset大的压缩后offset对应的消息,并从这个位置开始消费。

这种策略只适合特殊场景,比如消息的key是用户ID,value是用户的资料,通过这种压缩策略,整个消息集里就保存了所有用户最新的资料。

为什么Kafka能高效读写数据

  1. 分布式集群:采用分区技术,提高生产端和消费端并行度。
  2. 稀疏索引读:可以快速定位要消费的数据。
  3. 顺序写磁盘:Producer生产的数据写入到log文件中是一直追加到文件末端,为顺序写。
  4. 页缓存技术:Kafka将尽可能多的空闲内存当成磁盘缓存来使用。

log.flush.interval.messages 强制页缓存刷写到磁盘的条数,默认是long的最大值。不建议修改,交给系统自己管理。

log.flush.interval.ms 每隔多久刷数据到磁盘,默认是 null。不建议修改,交给系统自己管理。

消费者和分区对应关系

  1. 每个分区的数据只能由消费者组中的一个消费者消费,或者分属不同消费者组的多个消费者消费。同组消费者多于分区数时,组内一部分消费者将被闲置。
  2. 一个消费者可以消费多个分区数据。
  3. 消费者组之间互不影响,消费者组是逻辑上的一个订阅者。
  4. 0.9版本后,每个消费者的offset由消费者提交到系统主题_consumer_offsets保存。

消费者组初始化流程

0)coordinator节点选择:根据groupid的hashcode值对_consumer_offsets的分区数(默认50)求模处理,就得到了消费者组提交offset的_consumer_offsets分区号,该分区号在哪个broker节点上,该节点的coordinator就负责本次消费者组的事务协调。

1)组内每个消费者都发送JoinGroup请求到coordinator

2)coordinator选出一个消费者作为Leader,并把要消费的topic情况发送给Leader

3)Leader制定消费方案,并把消费方案发送给coordinator

4)coordinator将消费方案发送给各消费者

5)每个消费者和coordinator保持心跳(默认3s),一旦超时(session.timeout.ms=45s)或者处理消息超时(max.poll.interval.ms=5min),该消费者会被移除,并触发再平衡。

消费者组消费流程

  1. consumer-》sendFetches-》ConsumerNetworkClient-》send-》TopicPartitionReplicaLeader
  2. TopicPartitionReplicaLeader-》onSuccess-》completedFetch-》completedFetches(queue)
  3. consumer-》completedFetches(queue)-》completedFetch-》parseRecord-》Interceptors-》处理数据

消费者API

独立消费者案例(订阅主题)

需求:创建一个独立消费者,消费first主题中数据

注意:在消费者 API 代码中必须配置消费者组 id。命令行启动消费者不填写消费者组id 会被自动填写随机的消费者组 id。

代码:

package com.atguigu.kafka.consumer;

import org.apache.kafka.clients.consumer.ConsumerConfig;

import org.apache.kafka.clients.consumer.ConsumerRecord;

import org.apache.kafka.clients.consumer.ConsumerRecords;

import org.apache.kafka.clients.consumer.KafkaConsumer;

import org.apache.kafka.common.serialization.StringDeserializer;

import java.time.Duration;

import java.util.ArrayList;

import java.util.Properties;

public class CustomConsumer {

    public static void main(String[] args) {

        // 1.创建消费者的配置对象

        Properties properties = new Properties();

        // 2.给消费者配置对象添加参数

        properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "hadoop102:9092,hadoop103:9092");

        // 配置序列化 必须

        properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());

        properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());

        // 配置消费者组(组名任意起名) 必须

        properties.put(ConsumerConfig.GROUP_ID_CONFIG, "test");

        // 创建消费者对象

        KafkaConsumer<String, String> kafkaConsumer = new KafkaConsumer<>(properties);

        // 注册要消费的主题(可以消费多个主题)

        ArrayList<String> topics = new ArrayList<>();

        topics.add("first");

        kafkaConsumer.subscribe(topics);

        // 拉取数据打印

        while (true) {

            // 设置1秒中消费一批数据

            ConsumerRecords<String, String> consumerRecords = kafkaConsumer.poll(Duration.ofSeconds(1));

            // 打印消费到的数据

            for (ConsumerRecord<String, String> consumerRecord : consumerRecords) {

                System.out.println(consumerRecord);

            }

        }

    }

}

测试:

(1)在 IDEA 中运行消费者程序。

(2)在 Kafka 集群控制台,创建 Kafka 生产者,并输入数据。

[atguigu@hadoop102 kafka]$ bin/kafka-console-producer.sh --bootstrap-server hadoop102:9092 --topic first

>hello

(3)在 IDEA 控制台观察接收到的数据。

ConsumerRecord(topic = first, partition = 1, leaderEpoch = 3, offset = 0, CreateTime = 1629160841112, serialized key size = -1, serialized value size = 5, headers = RecordHeaders(headers = [], isReadOnly = false), key = null, value = hello)

独立消费者案例(订阅分区

需求:创建一个独立消费者,消费first主题0号分区的数据

代码:

package com.wanghaha.kafka.consumer;

import org.apache.kafka.clients.consumer.ConsumerConfig;

import org.apache.kafka.clients.consumer.ConsumerRecord;

import org.apache.kafka.clients.consumer.ConsumerRecords;

import org.apache.kafka.clients.consumer.KafkaConsumer;

import org.apache.kafka.common.TopicPartition;

import org.apache.kafka.common.serialization.StringDeserializer;

import java.time.Duration;

import java.util.ArrayList;

import java.util.Properties;

public class CustomConsumerPartition {

    public static void main(String[] args) {

        // 0. 配置

        Properties properties = new Properties();

        properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,"hadoop102:9092,hadoop103:9092");

        properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());

        properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,StringDeserializer.class.getName());

        properties.put(ConsumerConfig.GROUP_ID_CONFIG,"test");

        // 1. 创建一个消费者

        KafkaConsumer<String, String> kafkaConsumer = new KafkaConsumer<>(properties);

        // 2. 订阅主题对应的分区

        ArrayList<TopicPartition> topicPartitions = new ArrayList<>();

        topicPartitions.add(new TopicPartition("first",0));

        kafkaConsumer.assign(topicPartitions);

        // 3. 消费数据

        while (true){

            ConsumerRecords<String, String> consumerRecords = kafkaConsumer.poll(Duration.ofSeconds(1));

            for (ConsumerRecord<String, String> consumerRecord : consumerRecords) {

                System.out.println(consumerRecord);

            }

        }

    }

}

测试:

(1)在 IDEA 中运行消费者程序。

(2)在 IDEA 中运行生产者程序 CustomProducerCallback(在本文档向上搜索该类名),在控制台观察生成几个 0 号分区的数据。

first 0 381

first 0 382

first 2 168

first 1 165

first 1 166

(3)在 IDEA 控制台,观察接收到的数据,只能消费到 0 号分区数据表示正确。

ConsumerRecord(topic = first, partition = 0, leaderEpoch = 14, offset = 381, CreateTime = 1636791331386, serialized key size = -1, serialized value size = 9, headers = RecordHeaders(headers = [], isReadOnly = false), key = null, value = atguigu 0)

ConsumerRecord(topic = first, partition = 0, leaderEpoch = 14, offset = 382, CreateTime = 1636791331397, serialized key size = -1, serialized value size = 9, headers = RecordHeaders(headers = [], isReadOnly = false), key = null, value = atguigu 1)

消费者组案例

需求:测试同一个主题的分区数据,每个分区只能由同一个消费者组中的一个消费者消费

代码:复制 CustomConsumer 类,创建 CustomConsumer1,在 IDEA 中同时启动,即可启动同一个消费者组中的两个消费者。

测试:

在 IDEA 中运行生产者程序 CustomProducerCallback(在本文档向上搜索该类名)发送消息,在 IDEA 消费者程序控制台即可看到两个消费者在消费不同分区的数据(如果只发送到一个分区,可以在发送时增加延迟代码 Thread.sleep(2);)

ConsumerRecord(topic = first, partition = 0, leaderEpoch = 2, offset = 3, CreateTime = 1629169606820, serialized key size = -1, serialized value size = 8, headers = RecordHeaders(headers = [], isReadOnly = false), key = null, value = hello1)

ConsumerRecord(topic = first, partition = 1, leaderEpoch = 3, offset = 2, CreateTime = 1629169609524, serialized key size = -1, serialized value size = 6, headers = RecordHeaders(headers = [], isReadOnly = false), key = null, value = hello2)

ConsumerRecord(topic = first, partition = 2, leaderEpoch = 3, offset = 21, CreateTime = 1629169611884, serialized key size = -1, serialized value size = 6, headers = RecordHeaders(headers = [], isReadOnly = false), key = null, value = hello3)

重新发送到一个全新的主题中,由于默认创建的主题分区数为 1,可以看到只能有一个消费者消费到数据。

分区分配以及再平衡

一个consumer group中有多个consumer组成,一个 topic有多个partition组成,现在的问题是,到底由哪个consumer来消费哪个partition的数据。

Kafka有四种主流的分区分配策略: Range、RoundRobin、Sticky、CooperativeSticky。可以通过配置参数partition.assignment.strategy,修改分区的分配策略。默认策略是Range + CooperativeSticky。Kafka可以同时使用多个分区分配策略。CooperativeSticky是3.0版本开始才有的。

Range 分区分配以及再平衡

Range 是对每个 topic 而言的。首先对同一个 topic 里面的分区按照序号进行排序,并对消费者按照字母顺序进行排序。

假如现在有 7 个分区,3 个消费者,排序后的分区将会是0,1,2,3,4,5,6;消费者排序完之后将会是C0,C1,C2。例如,7/3 = 2 余 1 ,除不尽,那么 消费者 C0 便会多消费 1 个分区(0,1,2)。 8/3=2余2,除不尽,那么C0和C1分别多消费一个。

通过 partitions数/consumer数 来决定每个消费者应该消费几个分区。如果除不尽,那么前面几个消费者将会多消费 1 个分区。

注意:如果只是针对 1 个 topic 而言,C0消费者多消费1个分区影响不是很大。但是如果有 N 个 topic,那么针对每个 topic,消费者 C0都可能多消费 1 个分区,topic越多,C0消费的分区会比其他消费者明显增多。容易产生数据倾斜

Range分区分配以及再平衡案例

需求:修改主题first为7 个分区;准备 3 个消费者,采用Range 分区分配策略,并进行消费,观察消费分配情况。然后再停止其中一个消费者,再次观察消费分配情况。

(1)修改主题 first 为 7 个分区。

[atguigu@hadoop102 kafka]$ bin/kafka-topics.sh --bootstrap-server hadoop102:9092 --alter --topic first --partitions 7

注意:分区数可以增加,但是不能减少。

(2)复制 CustomConsumer 类,创建 CustomConsumer2。这样可以由三个消费者CustomConsumer、CustomConsumer1、CustomConsumer2 组成消费者组,组名都为“test”,同时启动 3 个消费者。

(3)CustomProducerCallback类中for循环改为500次,启动 CustomProducerCallback生产者,发送 500 条消息,随机发送到不同的分区。

说明:Kafka 默认的分区分配策略就是 Range + CooperativeSticky,所以不需要修改策略。

  1. 观看 3 个消费者分别消费哪些分区的数据:

0 号消费者:消费到 0、1、2 号分区数据。

1 号消费者:消费到 3、4 号分区数据。

2 号消费者:消费到 5、6 号分区数据。

  1. 停掉 0 号消费者,快速重新发送消息观看结果(45s 以内,越快越好)

1 号消费者:消费到 3、4 号分区数据。

2 号消费者:消费到 5、6 号分区数据。

(5)再次重新发送消息观看结果(45s 以后)。

1 号消费者:消费到 0、1、2、3 号分区数据。

2 号消费者:消费到 4、5、6 号分区数据。

说明:0 号消费者挂掉后,消费者组需要按照超时时间 45s 来判断它是否退出,所以需要等待,时间到了 45s 后,判断它真的退出就会把任务重新按照 range 方式分配给其他消费者消费。

RoundRobin 分区分配以及再平衡

RoundRobin 针对集群中所有Topic而言,RoundRobin 轮询分区策略,是把所有的 partition 和所有的consumer 都列出来,然后按照 hashcode 进行排序,最后通过轮询算法来分配 partition 给到各个消费者。

RoundRobin 分区分配以及再平衡案例

需求:修改主题first为7 个分区;准备 3 个消费者,采用RoundRobin 分区分配策略,并进行消费,观察消费分配情况。然后再停止其中一个消费者,再次观察消费分配情况。

  1. 依次在 CustomConsumer、CustomConsumer1、CustomConsumer2 三个消费者代码中修改分区分配策略为 RoundRobin:

// 修改分区分配策略

properties.put(ConsumerConfig.PARTITION_ASSIGNMENT_STRATEGY_CONFIG, "org.apache.kafka.clients.consumer.RoundRobinAssignor");

  1. 重启 3 个消费者,重复发送消息的步骤,观看分区结果。

0 号消费者:消费到 0、3、6 号分区数据。

1 号消费者:消费到 1、4 号分区数据。

2 号消费者:消费到 2、5 号分区数据。

(3)停掉 0 号消费者,快速重新发送消息观看结果(45s 以内,越快越好)。

1 号消费者:消费到 1、4 号分区数据。

2 号消费者:消费到 2、5 号分区数据。

(4)再次重新发送消息观看结果(45s 以后)。

1 号消费者:消费到 0、2、4、6 号分区数据

2 号消费者:消费到 1、3、5 号分区数据

说明:0 号消费者挂掉后,消费者组需要按照超时时间 45s 来判断它是否退出,所以需要等待,时间到了 45s 后,判断它真的退出就会把任务重新按照 RoundRobin 方式分配给其他消费者消费。

Sticky 分区分配以及再平衡

粘性分区定义:可以理解为分配的结果带有“粘性的”。即在执行一次新的分配之前,考虑上一次分配的结果,尽量少的调整分配的变动,可以节省大量的开销。

粘性分区是 Kafka 从 0.11.x 版本开始引入这种分配策略,首先会尽量均衡地放置分区到消费者上面,在组内消费者出现问题的时候,会尽量保持原有分配的分区不变化。

Sticky 分区分配以及再平衡案例

需求:设置主题为 first,7 个分区;准备 3 个消费者,采用粘性分区策略,并进行消费,观察消费分配情况。然后再停止其中一个消费者,再次观察消费分配情况。

(1)依次在 CustomConsumer、CustomConsumer1、CustomConsumer2 三个消费者代码中修改分区分配策略为Sticky:

// 修改分区分配策略

properties.put(ConsumerConfig.PARTITION_ASSIGNMENT_STRATEGY_CONFIG, "org.apache.kafka.clients.consumer.StickyAssignor");

(2)重启 3 个消费者,重复发送消息的步骤,观看分区结果。

0 号消费者:消费到 0、1 号分区数据。

1 号消费者:消费到 2、3、5 号分区数据。

2 号消费者:消费到 4、6 号分区数据。

(3)停掉 0 号消费者,快速重新发送消息观看结果(45s 以内,越快越好)。

1 号消费者:消费到 2、3、5 号分区数据。

2 号消费者:消费到 4、6 号分区数据。

(4)再次重新发送消息观看结果(45s 以后)。

1 号消费者:消费到 2、3、5 号分区数据。

2 号消费者:消费到 0、1、4、6 号分区数据。

说明:0 号消费者挂掉后,消费者组需要按照超时时间 45s 来判断它是否退出,所以需要等待,时间到了 45s 后,判断它真的退出就会把任务重新按照 Sticky 方式分配给其他消费者消费。

CooperativeSticky分区分配以及再平衡

CooperativeSticky和Sticky类似,只是支持了cooperative协议。

offset 位移

offset 的默认维护位置

0.9版本前,consumer默认将offset保存在Zookeeper中(/kafka/consumers)

0.9版本起,consumer默认将offset保存在Kafka一个内置的topic中(_consumer_offsets)

__consumer_offsets 主题里面采用 key 和 value 的方式存储数据。key 是 group.id+topic+分区号,value 就是当前 offset 的值。每隔一段时间,kafka 内部会对这个 topic 进行compact,也就是每个 key就保留最新数据。

消费 offset 案例

(0)思想:__consumer_offsets 为 Kafka 中的 topic,那就可以通过消费者进行消费。

(1)在配置文件 config/consumer.properties 中添加配置 exclude.internal.topics=false,默认是 true,表示不能消费系统主题。为了查看该系统主题数据,所以该参数修改为 false。

(2)采用命令行方式,创建一个新的 topic。

[atguigu@hadoop102 kafka]$ bin/kafka-topics.sh --bootstrap-server hadoop102:9092 --create --topic atguigu --partitions 2 --replication-factor 2

(3)启动生产者往 atguigu 生产数据。

[atguigu@hadoop102 kafka]$ bin/kafka-console-producer.sh --topic atguigu --bootstrap-server hadoop102:9092

(4)启动消费者消费 atguigu 数据。

[atguigu@hadoop104 kafka]$ bin/kafka-console-consumer.sh --bootstrap-server hadoop102:9092 --topic atguigu --group test

注意:指定消费者组名称,更好观察数据存储位置(key 是 group.id+topic+分区号)。

(5)查看消费者消费主题__consumer_offsets。

[atguigu@hadoop102 kafka]$ bin/kafka-console-consumer.sh --topic __consumer_offsets --bootstrap-server hadoop102:9092 --consumer.config config/consumer.properties --formatter "kafka.coordinator.group.GroupMetadataManager\$OffsetsMessageFormatter" --from-beginning

[offset,atguigu,1]::OffsetAndMetadata(offset=7, leaderEpoch=Optional[0], metadata=, commitTimestamp=1622442520203, expireTimestamp=None)

[offset,atguigu,0]::OffsetAndMetadata(offset=8, leaderEpoch=Optional[0], metadata=, commitTimestamp=1622442520203, expireTimestamp=None)

自动提交offset和手动提交offset

对比

自动提交:自动向服务器提交offset

手动同步提交:必须等待offset手动提交完毕,才能消费下一批数据。offset手动同步提交失败将自动重试。可靠性高,效率低。

手动异步提交:发送完提交offset请求后,就开始消费下一批数据了。offset手动异步提交失败不会重试。可靠性低,效率高。

相关参数:

enable.auto.commit:是否自动提交偏移量,默认true

auto.commit.interval.ms:偏移量自动提交频率,默认5s

相关API:

自动提交offset:

        // 自动提交

        properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, true);

        // 提交时间间隔

        properties.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG,1000);

手动提交offset:

package com.wanghaha.kafka.consumer;

import org.apache.kafka.clients.consumer.ConsumerConfig;

import org.apache.kafka.clients.consumer.ConsumerRecord;

import org.apache.kafka.clients.consumer.ConsumerRecords;

import org.apache.kafka.clients.consumer.KafkaConsumer;

import org.apache.kafka.common.serialization.StringDeserializer;

import java.time.Duration;

import java.util.ArrayList;

import java.util.Properties;

public class CustomConsumerByHandSync {

    public static void main(String[] args) {

        // 0. 配置

        Properties properties = new Properties();

        // 连接 bootstrap.servers

        properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,"hadoop102:9092,hadoop103:9092");

        // 反序列化

        properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());

        properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,StringDeserializer.class.getName());

        // 配置消费者id

        properties.put(ConsumerConfig.GROUP_ID_CONFIG,"test2");

        // 手动提交

        properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,false);

        // 1. 创建一个消费者

        KafkaConsumer<String, String> kafkaConsumer = new KafkaConsumer<>(properties);

        // 2. 订阅主题  first

        ArrayList<String> topics = new ArrayList<>();

        topics.add("first");

        kafkaConsumer.subscribe(topics);

        // 3. 消费数据

        while (true){

            ConsumerRecords<String, String> consumerRecords = kafkaConsumer.poll(Duration.ofSeconds(1));

            for (ConsumerRecord<String, String> consumerRecord : consumerRecords) {

                System.out.println(consumerRecord);

            }

            // 手动同步提交

            //kafkaConsumer.commitSync();

            // 手动异步提交

            kafkaConsumer.commitAsync();

        }

    }

}

指定offset消费

相关参数:

auto.offset.reset:未初始偏移量(消费者组第一次消费)或当前偏移量不存在时(数据被删除)如何处理:

earliest:自动重置为最早偏移量,–from-beginning

latest(默认):自动重置为最新偏移量

none:如果未找到消费者组先前的偏移量,则向消费者抛出异常

anything:向消费者抛异常

任意指定 offset 位移开始消费(注意:每次执行完,需要修改消费者组名

package com.wanghaha.kafka.consumer;

import org.apache.kafka.clients.consumer.ConsumerConfig;

import org.apache.kafka.clients.consumer.ConsumerRecord;

import org.apache.kafka.clients.consumer.ConsumerRecords;

import org.apache.kafka.clients.consumer.KafkaConsumer;

import org.apache.kafka.common.TopicPartition;

import org.apache.kafka.common.serialization.StringDeserializer;

import java.time.Duration;

import java.util.ArrayList;

import java.util.Properties;

import java.util.Set;

public class CustomConsumerSeek {

    public static void main(String[] args) {

        // 0. 配置

        Properties properties = new Properties();

        // 连接

        properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,"hadoop102:9092,hadoop103:9092");

        // 反序列化

        properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());

        properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());

        // 配置用户组id

        properties.put(ConsumerConfig.GROUP_ID_CONFIG,"test3");

        // 1. 创建消费者

        KafkaConsumer<String, String> kafkaConsumer = new KafkaConsumer<>(properties);

        // 2. 订阅主题

        ArrayList<String> topics = new ArrayList<>();

        topics.add("first");

        kafkaConsumer.subscribe(topics);

        // 指定位置进行消费      第一次尝试有可能拿到的是空的分区信息,因为消费者要和broker进行大量的交互

        Set<TopicPartition> assignment = kafkaConsumer.assignment(); // 获取到对应的分区信息

        // 保证分区分配方案已经指定完毕

        while (assignment.size() == 0){

            kafkaConsumer.poll(Duration.ofSeconds(1));

            assignment = kafkaConsumer.assignment();

        }

        for (TopicPartition topicPartition : assignment) {   // 拿到了所有的分区信息

            // 指定offset进行消费

            kafkaConsumer.seek(topicPartition,600);

        }

        // 3. 消费数据

        while (true) {

            ConsumerRecords<String, String> consumerRecords = kafkaConsumer.poll(Duration.ofSeconds(1));

            for (ConsumerRecord<String, String> consumerRecord : consumerRecords) {

                System.out.println(consumerRecord);

            }

        }

    }

}

指定时间消费

需求:在生产环境中,会遇到最近消费的几个小时数据异常,想重新按照时间消费。

例如要求按照时间消费前一天的数据,怎么处理?

代码:

package com.wanghaha.kafka.consumer;

import org.apache.kafka.clients.consumer.*;

import org.apache.kafka.common.TopicPartition;

import org.apache.kafka.common.serialization.StringDeserializer;

import java.time.Duration;

import java.util.*;

public class CustomConsumerSeekTime {

    public static void main(String[] args) {

        // 0. 配置

        Properties properties = new Properties();

        // 连接

        properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,"hadoop102:9092,hadoop103:9092");

        // 反序列化

        properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());

        properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());

        // 配置用户组id

        properties.put(ConsumerConfig.GROUP_ID_CONFIG,"test3");

        // 1. 创建消费者

        KafkaConsumer<String, String> kafkaConsumer = new KafkaConsumer<>(properties);

        // 2. 订阅主题

        ArrayList<String> topics = new ArrayList<>();

        topics.add("first");

        kafkaConsumer.subscribe(topics);

        // 指定位置进行消费      第一次尝试有可能拿到的是空的分区信息,因为消费者要和broker进行大量的交互

        Set<TopicPartition> assignment = kafkaConsumer.assignment(); // 获取到对应的分区信息

        // 保证分区分配方案已经指定完毕

        while (assignment.size() == 0){

            kafkaConsumer.poll(Duration.ofSeconds(1));

            assignment = kafkaConsumer.assignment();

        }

        // 希望把时间转换对对应的offset

        HashMap<TopicPartition, Long> topicPartitionHashMap = new HashMap<>();

        // 封装集合存储,每个分区对应一天前的数据

        for (TopicPartition topicPartition : assignment) {

            topicPartitionHashMap.put(topicPartition, System.currentTimeMillis() - 1*24*3600*1000);

        }

        // 获取从 1 天前开始消费的每个分区的 offset

        Map<TopicPartition, OffsetAndTimestamp> topicPartitionOffsetAndTimestampMap = kafkaConsumer.offsetsForTimes(topicPartitionHashMap);

        //  遍历每个分区,对每个分区设置消费时间。

        for (TopicPartition topicPartition : assignment) {   // 拿到了所有的分区信息

            // 指定offset进行消费

            OffsetAndTimestamp offsetAndTimestamp = topicPartitionOffsetAndTimestampMap.get(topicPartition);

            kafkaConsumer.seek(topicPartition,offsetAndTimestamp.offset());

            

        }

        // 3. 消费数据

        while (true) {

            ConsumerRecords<String, String> consumerRecords = kafkaConsumer.poll(Duration.ofSeconds(1));

            for (ConsumerRecord<String, String> consumerRecord : consumerRecords) {

                System.out.println(consumerRecord);

            }

        }

    }

}

消费者事务

漏消费和重复消费

消息消费模式

含义

导致问题

问题发生场景

解决方法

at most once

最多一次

拉取消息后,先提交offset再消费

漏消费

提交offset后宕机导致消费失败,重启后拉取最近提交offset处之后的消息,导致最近提交offset处的消息未被消费

重置offset

指定漏消费的offset重新消费一次

at least once

至少一次(默认)

拉取消息后,先消费再提交offset

重复消费

消息消费后宕机导致未能提交offset,重启后重新拉取该offset处消息进行消费,导致重复消费

幂等设计:

INSERT INTO ...ON DUPLICATE KEY UPDATE

exactly once

精确一次

将消费过程和提交offset过程做原子绑定

消费者事务

如果想完成消费端的精准一次性消费,那么需要Kafka消费端将消费过程和提交offset过程做原子绑定。此时我们需要将Kafka的offset保存到支持事务的自定义介质(比如MySQL)。

数据积压(消费者如何提高吞吐量)

1)如果是Kafka消费能力不足:考虑增加Topic的分区数,并且同时提升消费组的消费者数量,消费者数 = 分区数,两者缺一不可。

2)如果是下游的数据处理不及时:在保证每批次处理时间一致的情况下,提高每批次拉取的数量max.poll.records(注意如果导致批次大小超过fetch.max.bytes上限,同时修改fetch.max.bytes上限)。

生产者重要参数

参数名称

描述

默认值

生产建议值

bootstrap.servers

生产者连接集群所需的broker地址清单。可以设置1个或多个逗号隔开。注意这里并非需要所有的broker地址,因为生产者从给定的 broker 里查找到其他的 broker 信息

key.serializer

指定发送消息的key的序列化类型

value.serializer

指定发送消息的value的序列化类型

buffer.memory

缓冲区(RecordAccumulator)总大小

32m

batch.size

缓冲区批次大小。达到指定值即发送该批次消息。

16k

可适当增大以提高吞吐量,但若设置太大会导致数据传输延迟增加

linger.ms

缓冲区批次等待时间。达到该时间即发送该批次消息。

0ms

5-100ms

acks

0:生产者发送过来的数据,不需要等数据落盘应答。

1:生产者发送过来的数据,Leader收到数据后应答。

-1(all):生产者发送过来的数据,Leader和ISR队列里面的所有节点收齐数据后应答。

-1(all)

传输普通日志,设置为1;

传输和钱相关的数据,设置为-1(all)

enable.idempotence

是否开启幂等性。(保证单分区单会话内数据不重)

true

max.in.flight.requests.per.connection

允许最多没有返回 ack 的次数(缓冲队列最大请求数)

5

如要保证数据单分区有序,

1.x版本之前:此参数设为1(不需考虑是否开启幂等性)

1.x及以后版本:未开启幂等性时,此参数值需设为1;开启幂等性时,此参数值需设为<=5(启用幂等性后broker会保证缓存的最近5个请求根据SeqNumber排序)

retries

消息发送失败重发次数

int的最大值

retry.backoff.ms

两次消息重发的时间间隔

100ms

compression.type

生产者发送的消息的压缩类型。支持的压缩类型:none、gzip、snappy、lz4 和 zstd

none

配置非none可提高吞吐量

auto.create.topics.enable

向/从未创建的主题发送/接收消息时,自动创建主题

true

非预期方式创建,增加了主题管理和维护的难度,生产建议false

num.partitions

主题分区数

1

default.replication.factor

默认分区副本数

1

Broker重要参数

参数名称

描述

默认值

生产建议值

replica.lag.time.max.ms

ISR中,如果Follower长时间未向Leader发送通信请求或同步数据,则该Follower将被踢出ISR。

30s

auto.leader.rebalance.enable

自动Leader Partition平衡。

true

leader重选举的代价比较大,可能会带来性能影响,建议设置为false关闭

leader.imbalance.per.broker.percentage

每个broker允许的不平衡的leader的比率。如果每个broker超过了这个值,控制器会触发leader的平衡。

10%

leader.imbalance.check.interval.seconds

检查leader负载是否平衡的间隔时间。

300s

log.segment.bytes

数据划分成块的大小。

1G

log.index.interval.bytes

每当写入了指定大小的数据,就往index文件里记录一个索引。

4K

log.retention.hours

数据保存时间。

7天

log.retention.minutes

数据保存时间,分钟级别。

关闭

log.retention.ms

数据保存时间,毫秒级别。

关闭

log.retention.check.interval.ms

检查数据是否保存超时的间隔。

5分钟

log.retention.bytes

超过设置的所有日志总大小,删除最早的segment。

-1(无穷大)

log.cleanup.policy

delete:所有数据启用删除策略;compact:所有数据启用压缩策略。

delete

num.io.threads

负责写磁盘的线程数。这个参数值要占总核数的1/2。

8

num.replica.fetchers

副本拉取线程数,这个参数占总核数的1/6

1

num.network.threads

数据传输线程数,这个参数占总核数的1/3

3

log.flush.interval.message

强制页缓存刷写到磁盘的条数。不建议修改,交给系统自己管理。

long的最大值

log.flush.interval.ms

每隔多久刷数据到磁盘。不建议修改,交给系统自己管理。

null

message.max.bytes

接收每个批次消息最大大小

1m

消费者重要参数

参数名称

描述

默认值

生产建议值

bootstrap.servers

向 Kafka 集群建立初始连接用到的 host/port 列表

key.deserializer

指定接收消息的key的反序列化类型

value.deserializer

指定接收消息的value的反序列化类型

group.id

消费者组ID

enable.auto.commit

是否自动提交偏移量

true

auto.commit.interval.ms

偏移量自动提交频率

5s

auto.offset.reset

未初始偏移量(消费者组第一次消费)或当前偏移量不存在时(数据被删除)如何处理:

earliest:自动重置为最早偏移量,–from-beginning

latest:自动重置为最新偏移量

none:如果未找到消费者组先前的偏移量,则向消费者抛出异常

anything:向消费者抛异常

latest

offsets.topic.num.partitions

__consumer_offsets分区数

50

heartbeat.interval.ms

消费者和 coordinator 之间的心跳时间,必须大于等于session.timeout.ms的1/3,小于session.timeout.ms

3s

session.timeout.ms

消费者和 coordinator 之间连接超时时间,超过该值,该消费者被移除,消费者组执行再平衡

45s

max.poll.interval.ms

消费者处理消息的最大时长,超过该值,该消费者被移除,消费者组执行再平衡

5min

fetch.min.bytes

消费者获取一批消息的最小字节数,大小达到则获取

1byte

fetch.max.wait.ms

消费者获取一批消息的最大等待时间,时间达到则获取

500ms

fetch.max.bytes

消费者获取一批消息的最大字节数。如果服务器端一批次的数据大于该值(50m)仍然可以拉取,因此,这不是一个绝对最大值。一批次的大小受 message.max.bytes (broker config)or max.message.bytes (topic config)影响

52428800(50 m)

max.poll.records

一次 poll 拉取数据返回消息的最大条数

500

partition.assignment.strategy

消费者分区分配策略:Range、RoundRobin、Sticky、CooperativeSticky

Range + CooperativeSticky

场景说明

100 万日活,每人每天 100 条日志,每天总共的日志条数是 100 万 * 100 条 = 1 亿条。

1 亿/24 小时/60 分/60 秒 = 1150 条/每秒钟。

每条日志大小:0.5k - 2k(取 1k)。

1150 条/每秒钟 * 1k ≈ 1m/s 。

高峰期每秒钟:1150 条 * 20 倍 = 23000 条。

每秒多少数据量:20MB/s。

服务器台数选择

    服务器台数= 2 * (生产者峰值生产速率 * 副本 / 100) + 1

    = 2 * (20m/s * 2 / 100) + 1

    = 3 台

建议 3 台服务器。

磁盘选择

Kafka 底层主要是顺序写,固态硬盘和机械硬盘的顺序写速度差不多。

建议选择普通的机械硬盘。

每天总数据量:1 亿条 * 1k ≈ 100g

100g * 副本 2 * 保存时间 3 天 / 0.7 ≈ 1T

建议三台服务器硬盘总大小,大于等于 1T。

内存选择

Kafka 内存组成:堆内存(kafka内部配置) + 页缓存(服务器内存)

1)Kafka 堆内存建议每个节点:10g ~ 15g

在 kafka-server-start.sh 中修改

if [ "x$KAFKA_HEAP_OPTS" = "x" ]; then

 export KAFKA_HEAP_OPTS="-Xmx10G -Xms10G"

fi

(1)查看 Kafka 进程号

[atguigu@hadoop102 kafka]$ jps

2321 Kafka

5255 Jps

1931 QuorumPeerMain

(2)根据 Kafka 进程号,查看 Kafka 的 GC 情况

[atguigu@hadoop102 kafka]$ jstat -gc 2321 1s 10

S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT

0.0 7168.0 0.0 7168.0 103424.0 60416.0 1986560.0 148433.5 52092.0 46656.1 6780.0 6202.2 13 0.531 0 0.000 0.531

0.0 7168.0 0.0 7168.0 103424.0 60416.0 1986560.0 148433.5 52092.0 46656.1 6780.0 6202.2 13 0.531 0 0.000 0.531

0.0 7168.0 0.0 7168.0 103424.0 60416.0 1986560.0 148433.5 52092.0 46656.1 6780.0 6202.2 13 0.531 0 0.000 0.531

0.0 7168.0 0.0 7168.0 103424.0 60416.0 1986560.0 148433.5 52092.0 46656.1 6780.0 6202.2 13 0.531 0 0.000 0.531

0.0 7168.0 0.0 7168.0 103424.0 60416.0 1986560.0 148433.5 52092.0 46656.1 6780.0 6202.2 13 0.531 0 0.000 0.531

0.0 7168.0 0.0 7168.0 103424.0 61440.0 1986560.0 148433.5 52092.0 46656.1 6780.0 6202.2 13 0.531 0 0.000 0.531

0.0 7168.0 0.0 7168.0 103424.0 61440.0 1986560.0 148433.5 52092.0 46656.1 6780.0 6202.2 13 0.531 0 0.000 0.531

0.0 7168.0 0.0 7168.0 103424.0 61440.0 1986560.0 148433.5 52092.0 46656.1 6780.0 6202.2 13 0.531 0 0.000 0.531

0.0 7168.0 0.0 7168.0 103424.0 61440.0 1986560.0 148433.5 52092.0 46656.1 6780.0 6202.2 13 0.531 0 0.000 0.531

0.0 7168.0 0.0 7168.0 103424.0 61440.0 1986560.0 148433.5 52092.0 46656.1 6780.0 6202.2 13 0.531 0 0.000 0.531

参数说明:

S0C:第一个幸存区的大小; S1C:第二个幸存区的大小

S0U:第一个幸存区的使用大小; S1U:第二个幸存区的使用大小

EC:伊甸园区的大小; EU:伊甸园区的使用大小

OC:老年代大小; OU:老年代使用大小

MC:方法区大小; MU:方法区使用大小

CCSC:压缩类空间大小; CCSU:压缩类空间使用大小

YGC:年轻代垃圾回收次数; YGCT:年轻代垃圾回收消耗时间

FGC:老年代垃圾回收次数; FGCT:老年代垃圾回收消耗时间

GCT:垃圾回收消耗总时间;

(3)根据 Kafka 进程号,查看 Kafka 的堆内存

[atguigu@hadoop102 kafka]$ jmap -heap 2321

Attaching to process ID 2321, please wait...

Debugger attached successfully.

Server compiler detected.

JVM version is 25.212-b10

using thread-local object allocation.

Garbage-First (G1) GC with 8 thread(s)

Heap Configuration:

 MinHeapFreeRatio = 40

 MaxHeapFreeRatio = 70

 MaxHeapSize = 2147483648 (2048.0MB)

 NewSize = 1363144 (1.2999954223632812MB)

 MaxNewSize = 1287651328 (1228.0MB)

 OldSize = 5452592 (5.1999969482421875MB)

 NewRatio = 2

 SurvivorRatio = 8

 MetaspaceSize = 21807104 (20.796875MB)

 CompressedClassSpaceSize = 1073741824 (1024.0MB)

 MaxMetaspaceSize = 17592186044415 MB

 G1HeapRegionSize = 1048576 (1.0MB)

Heap Usage:

G1 Heap:

 regions = 2048

 capacity = 2147483648 (2048.0MB)

 used = 246367744 (234.95458984375MB)

 free = 1901115904 (1813.04541015625MB)

 11.472392082214355% used

G1 Young Generation:

Eden Space:

 regions = 83

 capacity = 105906176 (101.0MB)

 used = 87031808 (83.0MB)

 free = 18874368 (18.0MB)

 82.17821782178218% used

Survivor Space:

 regions = 7

 capacity = 7340032 (7.0MB)

 used = 7340032 (7.0MB)

 free = 0 (0.0MB)

 100.0% used

G1 Old Generation:

 regions = 147

 capacity = 2034237440 (1940.0MB)

 used = 151995904 (144.95458984375MB)

 free = 1882241536 (1795.04541015625MB)

 7.471886074420103% used

13364 interned Strings occupying 1449608 bytes.

2)页缓存:页缓存是 Linux 系统服务器的内存。我们只需要保证 1 个 segment(1g)中25%的数据在内存中就好。

每个节点页缓存大小 =(分区数 * 1g * 25%)/ 节点数。例如 10 个分区leader,页缓存大小=(10 * 1g * 25%)/ 3 ≈ 1g

建议服务器内存大于等于 11G。

CPU 选择

num.io.threads = 8 负责写磁盘的线程数,整个参数值要占总核数的1/2。

num.replica.fetchers = 1 副本拉取线程数,这个参数占总核数的1/6。

num.network.threads = 3 数据传输线程数,这个参数占总核数的1/3。

建议 32 个 cpu core。

24(12,4,8)+ 8

网络选择

网络带宽 = 峰值吞吐量 ≈ 20MB/s 选择千兆网卡即可。

100Mbps 单位是 bit;10M/s 单位是 byte ; 1byte = 8bit,100Mbps/8 = 12.5M/s。

    1. (面试重点)

如何提升吞吐量

1)提升生产者吞吐量

(1)buffer.memory:发送消息的缓冲区大小,默认32m,可增加到64m。

(2)batch.size:默认16k,可增加到32k。如果batch设置太小,会导致频繁网络请求,吞吐量下降;如果 batch 太大,会导致一条消息需要等待很久才能被发送出去,增加网络延时。

(3)linger.ms,默认 0,意思就是消息必须立即被发送。建议 5-100毫秒。如果 linger.ms 设置的太小,会导致频繁网络请求,吞吐量下降;如果 linger.ms 太长,会导致一条消息需要等待很久才能被发送出去,增加网络延时。

(4)compression.type:默认是 none,不压缩,但是也可以使用 lz4 压缩,效率还是不错的,压缩之后可以减小数据量,提升吞吐量,但是会加大 producer 端的 CPU 开销。

......

        // batch.size:批次大小,默认 16K

        properties.put(ProducerConfig.BATCH_SIZE_CONFIG, 16384 * 2);

        // linger.ms:等待时间,默认 0

        properties.put(ProducerConfig.LINGER_MS_CONFIG, 5);

        // buffer.memory:缓冲区大小,默认 32M:buffer.memory

        properties.put(ProducerConfig.BUFFER_MEMORY_CONFIG, 33554432 * 2);

        // compression.type:压缩,默认 none,可配置值 gzip、snappy、lz4 和 zstd

        properties.put(ProducerConfig.COMPRESSION_TYPE_CONFIG,"snappy");

......

2)提升消费者吞吐量

1)如果是Kafka消费能力不足:考虑增加Topic的分区数,并且同时提升消费组的消费者数量,消费者数 = 分区数,两者缺一不可。

2)如果是下游的数据处理不及时:在保证每批次处理时间一致的情况下,提高每批次拉取的数量max.poll.records(注意如果导致批次大小超过fetch.max.bytes上限,同时修改fetch.max.bytes上限)。

如何保证数据不丢失

1)生产者角度

acks 设置为-1。

2)broker 服务端角度

分区副本>=2(–replication-factor 2)。

ISR 里应答的最小副本数量>=2 (min.insync.replicas = 2)。

如果分区副本设置为1个,或者ISR里应答的最小副本数量(min.insync.replicas默认为1)设置为1,和acks=0的效果是一样的,仍然有丢数据风险(leader:0,isr:0)

3)消费者角度

事务 + 手动提交 offset (enable.auto.commit = false)。

消费者输出的目的地必须支持事务(MySQL、Kafka)。

合理设置分区数

(1)创建一个只有 1 个分区的 topic。

(2)测试这个 topic 的 producer 吞吐量和 consumer 吞吐量。

(3)假设他们的值分别是 Tp 和 Tc,单位可以是 MB/s。

(4)然后假设总的目标吞吐量是 Tt,那么分区数 = Tt / min(Tp,Tc)。

例如:producer 吞吐量 = 20m/s;consumer 吞吐量 = 50m/s,期望吞吐量 100m/s;

分区数 = 100 / 20 = 5 分区

分区数一般设置为:3-10 个

分区数不是越多越好,也不是越少越好,需要搭建完集群,进行压测,再灵活调整分区个数。

单条日志大于1m怎么办

增大以下参数:

message.max.bytes 默认 1m,broker 端接收每个批次消息最大值。

max.request.size 默认 1m,生产者发往 broker 每个请求消息最大值。针对 topic级别设置消息体的大小。

replica.fetch.max.bytes 默认 1m,副本同步数据,每个批次消息最大值。

fetch.max.bytes 默认 Default: 52428800(50 m)。消费者获取服务器端一批消息最大的字节数。如果服务器端一批次的数据大于该值(50m)仍然可以拉取回来这批数据,因此,这不是一个绝对最大值。一批次的大小受 message.max.bytes (broker config)or max.message.bytes (topic config)影响。

    1. 压测

用 Kafka 官方自带的脚本(在/opt/module/kafka/bin目录下),对 Kafka 进行压测。

    生产者压测:kafka-producer-perf-test.sh

消费者压测:kafka-consumer-perf-test.sh

Kafka Producer 压力测试

(1)创建一个 test topic,设置为 3 个分区 3 个副本

[atguigu@hadoop102 kafka]$ bin/kafka-topics.sh --bootstrapserver hadoop102:9092 --create --replication-factor 3 --partitions 3 --topic test

(2)测试一下

[atguigu@hadoop105 kafka]$ bin/kafka-producer-perf-test.sh --topic test --record-size 1024 --num-records 1000000 --throughput 10000 --producer-props bootstrap.servers=hadoop102:9092,hadoop103:9092,hadoop104:9092 batch.size=16384 linger.ms=0

参数说明:

record-size 是一条信息有多大,单位是字节,本次测试设置为 1k。

num-records 是总共发送多少条信息,本次测试设置为 100 万条。

throughput 是每秒多少条信息,设成-1,表示不限流,尽可能快的生产数据,可测出生产者最大吞吐量。本次实验设置为每秒钟 1 万条。

producer-props 后面可以配置生产者相关参数,batch.size 配置为 16k。

输出结果:

ap.servers=hadoop102:9092,hadoop103:9092,hadoop104:9092 batch.size=16384

linger.ms=0

37021 records sent, 7401.2 records/sec (7.23 MB/sec), 1136.0 ms avg latency, 1453.0 ms max latency.

50535 records sent, 10107.0 records/sec (9.87 MB/sec), 1199.5 ms avg latency, 1404.0 ms max latency.

47835 records sent, 9567.0 records/sec (9.34 MB/sec), 1350.8 ms avg latency, 1570.0 ms max latency.

。。。 。。。

42390 records sent, 8444.2 records/sec (8.25 MB/sec), 3372.6 ms avg latency, 4008.0 ms max latency.

37800 records sent, 7558.5 records/sec (7.38 MB/sec), 4079.7 ms avg latency, 4758.0 ms max latency.

33570 records sent, 6714.0 records/sec (6.56 MB/sec), 4549.0 ms avg latency, 5049.0 ms max latency.

1000000 records sent, 9180.713158 records/sec (8.97 MB/sec), 1894.78 ms avg latency, 5049.00 ms max latency, 1335 ms 50th, 4128 ms 95th, 4719 ms 99th, 5030 ms 99.9th.

(3)调整 batch.size 大小

①batch.size 默认值是 16k。本次实验 batch.size 设置为 32k。

[atguigu@hadoop105 kafka]$ bin/kafka-producer-perf-test.sh --topic test --record-size 1024 --num-records 1000000 --throughput 10000 --producer-props bootstrap.servers=hadoop102:9092,hadoop103:9092,hadoop104:9092 batch.size=32768 linger.ms=0

输出结果:

49922 records sent, 9978.4 records/sec (9.74 MB/sec), 64.2 ms avg latency, 340.0 ms max latency.

49940 records sent, 9988.0 records/sec (9.75 MB/sec), 15.3 ms avg latency, 31.0 ms max latency.

50018 records sent, 10003.6 records/sec (9.77 MB/sec), 16.4 ms avg latency, 52.0 ms max latency.

。。。 。。。

49960 records sent, 9992.0 records/sec (9.76 MB/sec), 17.2 ms avg latency, 40.0 ms max latency.

50090 records sent, 10016.0 records/sec (9.78 MB/sec), 16.9 ms avg latency, 47.0 ms max latency.

1000000 records sent, 9997.600576 records/sec (9.76 MB/sec), 20.20 ms avg latency, 340.00 ms max latency, 16 ms 50th, 30 ms 95th, 168 ms 99th, 249 ms 99.9th.

②batch.size 默认值是 16k。本次实验 batch.size 设置为 4k。

[atguigu@hadoop105 kafka]$ bin/kafka-producer-perf-test.sh --topic test --record-size 1024 --num-records 1000000 --throughput 10000 --producer-props bootstrap.servers=hadoop102:9092,hadoop103:9092,hadoop104:9092 batch.size=4096 linger.ms=0

输出结果:

15598 records sent, 3117.1 records/sec (3.04 MB/sec), 1878.3 ms avg latency,

3458.0 ms max latency.

17748 records sent, 3549.6 records/sec (3.47 MB/sec), 5072.5 ms avg latency,

6705.0 ms max latency.

18675 records sent, 3733.5 records/sec (3.65 MB/sec), 6800.9 ms avg latency,

7052.0 ms max latency.

。。。 。。。

19125 records sent, 3825.0 records/sec (3.74 MB/sec), 6416.5 ms avg latency,

7023.0 ms max latency.

1000000 records sent, 3660.201531 records/sec (3.57 MB/sec), 6576.68 ms

avg latency, 7677.00 ms max latency, 6745 ms 50th, 7298 ms 95th, 7507 ms

99th, 7633 ms 99.9th.

(4)调整 linger.ms 时间

linger.ms 默认是 0ms。本次实验 linger.ms 设置为 50ms。

[atguigu@hadoop105 kafka]$ bin/kafka-producer-perf-test.sh --topic test --record-size 1024 --num-records 1000000 --throughput 10000 --producer-props bootstrap.servers=hadoop102:9092,hadoop103:9092,hadoop104:9092 batch.size=4096 linger.ms=50

输出结果:

16804 records sent, 3360.1 records/sec (3.28 MB/sec), 1841.6 ms avg latency,

3338.0 ms max latency.

18972 records sent, 3793.6 records/sec (3.70 MB/sec), 4877.7 ms avg latency,

6453.0 ms max latency.

19269 records sent, 3852.3 records/sec (3.76 MB/sec), 6477.9 ms avg latency,

6686.0 ms max latency.

。。。 。。。

17073 records sent, 3414.6 records/sec (3.33 MB/sec), 6987.7 ms avg latency,

7353.0 ms max latency.

19326 records sent, 3865.2 records/sec (3.77 MB/sec), 6756.5 ms avg latency,

7357.0 ms max latency.

1000000 records sent, 3842.754486 records/sec (3.75 MB/sec), 6272.49 ms

avg latency, 7437.00 ms max latency, 6308 ms 50th, 6880 ms 95th, 7289 ms

99th, 7387 ms 99.9th.

(5)调整压缩方式

①默认的压缩方式是 none。本次实验 compression.type 设置为 snappy。

[atguigu@hadoop105 kafka]$ bin/kafka-producer-perf-test.sh --topic test --record-size 1024 --num-records 1000000 --throughput 10000 --producer-props bootstrap.servers=hadoop102:9092,hadoop103:9092,hadoop104:9092 batch.size=4096 linger.ms=50 compression.type=snappy

输出结果:

17244 records sent, 3446.0 records/sec (3.37 MB/sec), 5207.0 ms avg latency,

6861.0 ms max latency.

18873 records sent, 3774.6 records/sec (3.69 MB/sec), 6865.0 ms avg latency,

7094.0 ms max latency.

18378 records sent, 3674.1 records/sec (3.59 MB/sec), 6579.2 ms avg latency,

6738.0 ms max latency.

。。。 。。。

17631 records sent, 3526.2 records/sec (3.44 MB/sec), 6671.3 ms avg latency,

7566.0 ms max latency.

19116 records sent, 3823.2 records/sec (3.73 MB/sec), 6739.4 ms avg latency,

7630.0 ms max latency.

1000000 records sent, 3722.925028 records/sec (3.64 MB/sec), 6467.75 ms

avg latency, 7727.00 ms max latency, 6440 ms 50th, 7308 ms 95th, 7553 ms

99th, 7665 ms 99.9th.

②默认的压缩方式是 none。本次实验 compression.type 设置为 zstd。

[atguigu@hadoop105 kafka]$ bin/kafka-producer-perf-test.sh --topic test --record-size 1024 --num-records 1000000 --throughput 10000 --producer-props bootstrap.servers=hadoop102:9092,hadoop103:9092,hadoop104:9092 batch.size=4096 linger.ms=50 compression.type=zstd

输出结果:

23820 records sent, 4763.0 records/sec (4.65 MB/sec), 1580.2 ms avg latency,

2651.0 ms max latency.

29340 records sent, 5868.0 records/sec (5.73 MB/sec), 3666.0 ms avg latency,

4752.0 ms max latency.

28950 records sent, 5788.8 records/sec (5.65 MB/sec), 5785.2 ms avg latency,

6865.0 ms max latency.

。。。 。。。

29580 records sent, 5916.0 records/sec (5.78 MB/sec), 6907.6 ms avg latency,

7432.0 ms max latency.

29925 records sent, 5981.4 records/sec (5.84 MB/sec), 6948.9 ms avg latency,

7541.0 ms max latency.

1000000 records sent, 5733.583318 records/sec (5.60 MB/sec), 6824.75 ms

avg latency, 7595.00 ms max latency, 7067 ms 50th, 7400 ms 95th, 7500 ms

99th, 7552 ms 99.9th.

③默认的压缩方式是 none。本次实验 compression.type 设置为 gzip。

[atguigu@hadoop105 kafka]$ bin/kafka-producer-perf-test.sh --topic test --record-size 1024 --num-records 1000000 --throughput 10000 --producer-props bootstrap.servers=hadoop102:9092,hadoop103:9092,hadoop104:9092 batch.size=4096 linger.ms=50 compression.type=gzip

输出结果:

27170 records sent, 5428.6 records/sec (5.30 MB/sec), 1374.0 ms avg latency,

2311.0 ms max latency.

31050 records sent, 6210.0 records/sec (6.06 MB/sec), 3183.8 ms avg latency,

4228.0 ms max latency.

32145 records sent, 6427.7 records/sec (6.28 MB/sec), 5028.1 ms avg latency,

6042.0 ms max latency.

。。。 。。。

31710 records sent, 6342.0 records/sec (6.19 MB/sec), 6457.1 ms avg latency,

6777.0 ms max latency.

31755 records sent, 6348.5 records/sec (6.20 MB/sec), 6498.7 ms avg latency,

6780.0 ms max latency.

32760 records sent, 6548.1 records/sec (6.39 MB/sec), 6375.7 ms avg latency,

6822.0 ms max latency.

1000000 records sent, 6320.153706 records/sec (6.17 MB/sec), 6155.42 ms

avg latency, 6943.00 ms max latency, 6437 ms 50th, 6774 ms 95th, 6863 ms

99th, 6912 ms 99.9th.

④默认的压缩方式是 none。本次实验 compression.type 设置为 lz4。

[atguigu@hadoop105 kafka]$ bin/kafka-producer-perf-test.sh --topic test --record-size 1024 --num-records 1000000 --throughput 10000 --producer-props bootstrap.servers=hadoop102:9092,hadoop103:9092,hadoop104:9092 batch.size=4096 linger.ms=50 compression.type=lz4

输出结果:

16696 records sent, 3339.2 records/sec (3.26 MB/sec), 1924.5 ms avg latency,

3355.0 ms max latency.

19647 records sent, 3928.6 records/sec (3.84 MB/sec), 4841.5 ms avg latency,

6320.0 ms max latency.

20142 records sent, 4028.4 records/sec (3.93 MB/sec), 6203.2 ms avg latency,

6378.0 ms max latency.

。。。 。。。

20130 records sent, 4024.4 records/sec (3.93 MB/sec), 6073.6 ms avg latency,

6396.0 ms max latency.

19449 records sent, 3889.8 records/sec (3.80 MB/sec), 6195.6 ms avg latency,

6500.0 ms max latency.

19872 records sent, 3972.8 records/sec (3.88 MB/sec), 6274.5 ms avg latency,

6565.0 ms max latency.

1000000 records sent, 3956.087430 records/sec (3.86 MB/sec), 6085.62 ms

avg latency, 6745.00 ms max latency, 6212 ms 50th, 6524 ms 95th, 6610 ms

99th, 6695 ms 99.9th.

(6)调整缓存大小

默认生产者端缓存大小 32m。本次实验 buffer.memory 设置为 64m。

[atguigu@hadoop105 kafka]$ bin/kafka-producer-perf-test.sh --topic test --record-size 1024 --num-records 1000000 --throughput 10000 --producer-props bootstrap.servers=hadoop102:9092,hadoop103:9092,hadoop104:9092 batch.size=4096 linger.ms=50 buffer.memory=67108864

输出结果:

20170 records sent, 4034.0 records/sec (3.94 MB/sec), 1669.5 ms avg latency,

3040.0 ms max latency.

21996 records sent, 4399.2 records/sec (4.30 MB/sec), 4407.9 ms avg latency,

5806.0 ms max latency.

22113 records sent, 4422.6 records/sec (4.32 MB/sec), 7189.0 ms avg latency,

8623.0 ms max latency.

。。。 。。。

19818 records sent, 3963.6 records/sec (3.87 MB/sec), 12416.0 ms avg

latency, 12847.0 ms max latency.

20331 records sent, 4062.9 records/sec (3.97 MB/sec), 12400.4 ms avg

latency, 12874.0 ms max latency.

19665 records sent, 3933.0 records/sec (3.84 MB/sec), 12303.9 ms avg

latency, 12838.0 ms max latency.

1000000 records sent, 4020.100503 records/sec (3.93 MB/sec), 11692.17 ms

avg latency, 13796.00 ms max latency, 12238 ms 50th, 12949 ms 95th, 13691

ms 99th, 13766 ms 99.9th.

Kafka Consumer 压力测试

  1. 修改/opt/module/kafka/config/consumer.properties 文件中的一次拉取条数为500:

max.poll.records=500

(2)消费 100 万条日志进行压测

[atguigu@hadoop105 kafka]$ bin/kafka-consumer-perf-test.sh --bootstrap-server hadoop102:9092,hadoop103:9092,hadoop104:9092 --topic test --messages 1000000 --consumer.config config/consumer.properties

参数说明:

--bootstrap-server 指定 Kafka 集群地址

--topic 指定 topic 的名称

--messages 总共要消费的消息个数。本次实验 100 万条。

输出结果:

start.time, end.time, data.consumed.in.MB, MB.sec, data.consumed.in.nMsg,

nMsg.sec, rebalance.time.ms, fetch.time.ms, fetch.MB.sec, fetch.nMsg.sec

2022-01-20 09:58:26:171, 2022-01-20 09:58:33:321, 977.0166, 136.6457,

1000465, 139925.1748, 415, 6735, 145.0656, 148547.1418

(3)一次拉取条数为 2000

①修改/opt/module/kafka/config/consumer.properties 文件中的一次拉取条数为 2000

max.poll.records=2000

②再次执行

[atguigu@hadoop105 kafka]$ bin/kafka-consumer-perf-test.sh --broker-list hadoop102:9092,hadoop103:9092,hadoop104:9092 --topic test --messages 1000000 --consumer.config config/consumer.properties

输出结果:

start.time, end.time, data.consumed.in.MB, MB.sec, data.consumed.in.nMsg,

nMsg.sec, rebalance.time.ms, fetch.time.ms, fetch.MB.sec, fetch.nMsg.sec

2022-01-20 10:18:06:268, 2022-01-20 10:18:12:863, 977.5146, 148.2206,

1000975, 151777.8620, 358, 6237, 156.7283, 160489.8188

(4)调整 fetch.max.bytes 大小为 100m

①修改/opt/module/kafka/config/consumer.properties 文件中的拉取一批数据大小 100m

fetch.max.bytes=104857600

②再次执行

[atguigu@hadoop105 kafka]$ bin/kafka-consumer-perf-test.sh --broker-list hadoop102:9092,hadoop103:9092,hadoop104:9092 --topic test --messages 1000000 --consumer.config config/consumer.properties

输出结果:

start.time, end.time, data.consumed.in.MB, MB.sec,

data.consumed.in.nMsg, nMsg.sec, rebalance.time.ms,

fetch.time.ms, fetch.MB.sec, fetch.nMsg.sec

2022-01-20 10:26:13:203, 2022-01-20 10:26:19:662, 977.5146,

151.3415, 1000975, 154973.6801, 362, 6097, 160.3272, 164175.004

Kafka-Eagle 框架可以监控 Kafka 集群的整体运行情况,在生产环境中经常使用。

MySQL环境准备

Kafka-Eagle 的安装依赖于 MySQL,MySQL 主要用来存储可视化展示的数据。如果集群中之前安装过 MySQL 可以跨过该步。

Kafka环境准备

1)关闭 Kafka 集群

[atguigu@hadoop102 kafka]$ kf.sh stop

2)修改/opt/module/kafka/bin/kafka-server-start.sh 命令

[atguigu@hadoop102 kafka]$ vim bin/kafka-server-start.sh

增加如下内容(黄色标,注释一条export,增加两条export):

if [ "x$KAFKA_HEAP_OPTS" = "x" ]; then

 #export KAFKA_HEAP_OPTS="-Xmx1G -Xms1G"

 export KAFKA_HEAP_OPTS="-server -Xms2G -Xmx2G -XX:PermSize=128m -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:ParallelGCThreads=8 -XX:ConcGCThreads=5 -XX:InitiatingHeapOccupancyPercent=70"

 export JMX_PORT="9999"

fi

3)命令分发到其他节点

[atguigu@hadoop102 bin]$ xsync kafka-server-start.sh

Kafka-Eagle安装及启动

0)官网:https://www.kafka-eagle.org/

1)上传压缩包 kafka-eagle-bin-2.0.8.tar.gz 到集群/opt/software 目录

2)解压到本地

[atguigu@hadoop102 software]$ tar -zxvf kafka-eagle-bin-2.0.8.tar.gz

3)进入刚才解压的目录

[atguigu@hadoop102 software]$ cd kafka-eagle-bin-2.0.8/

[atguigu@hadoop102 kafka-eagle-bin-2.0.8]$ ll

总用量 79164

-rw-rw-r--. 1 atguigu atguigu 81062577 10 月 13 00:00 efak-web2.0.8-bin.tar.gz

4)继续解压至/opt/module

[atguigu@hadoop102 kafka-eagle-bin-2.0.8]$ tar -zxvf efak-web2.0.8-bin.tar.gz -C /opt/module/

5)修改名称

[atguigu@hadoop102 kafka-eagle-bin-2.0.8]$ cd /opt/module/

[atguigu@hadoop102 module]$ mv efak-web-2.0.8/ efak

6)修改配置文件/opt/module/efak/conf/system-config.properties

[atguigu@hadoop102 module]$ cd efak/conf

[atguigu@hadoop102 conf]$ vim system-config.properties

######################################

# multi zookeeper & kafka cluster list

# Settings prefixed with 'kafka.eagle.' will be deprecated, use 'efak.' instead

######################################

efak.zk.cluster.alias=cluster1

cluster1.zk.list=hadoop102:2181,hadoop103:2181,hadoop104:2181/kafka

######################################

# zookeeper enable acl

######################################

cluster1.zk.acl.enable=false

cluster1.zk.acl.schema=digest

cluster1.zk.acl.username=test

cluster1.zk.acl.password=test123

######################################

# broker size online list

######################################

cluster1.efak.broker.size=20

######################################

# zk client thread limit

######################################

kafka.zk.limit.size=32

######################################

# EFAK webui port

######################################

efak.webui.port=8048

######################################

# kafka jmx acl and ssl authenticate

######################################

cluster1.efak.jmx.acl=false

cluster1.efak.jmx.user=keadmin

cluster1.efak.jmx.password=keadmin123

cluster1.efak.jmx.ssl=false

cluster1.efak.jmx.truststore.location=/data/ssl/certificates/kafka.truststore

cluster1.efak.jmx.truststore.password=ke123456

######################################

# kafka offset storage

######################################

# offset 保存在 kafka

cluster1.efak.offset.storage=kafka

######################################

# kafka jmx uri

######################################

cluster1.efak.jmx.uri=service:jmx:rmi:///jndi/rmi://%s/jmxrmi

######################################

# kafka metrics, 15 days by default

######################################

efak.metrics.charts=true

efak.metrics.retain=15

######################################

# kafka sql topic records max

######################################

efak.sql.topic.records.max=5000

efak.sql.topic.preview.records.max=10

######################################

# delete kafka topic token

######################################

efak.topic.token=keadmin

######################################

# kafka sasl authenticate

######################################

cluster1.efak.sasl.enable=false

cluster1.efak.sasl.protocol=SASL_PLAINTEXT

cluster1.efak.sasl.mechanism=SCRAM-SHA-256

cluster1.efak.sasl.jaas.config=org.apache.kafka.common.security.scram.ScramLoginModule required username="kafka" password="kafka-eagle";

cluster1.efak.sasl.client.id=

cluster1.efak.blacklist.topics=

cluster1.efak.sasl.cgroup.enable=false

cluster1.efak.sasl.cgroup.topics=

cluster2.efak.sasl.enable=false

cluster2.efak.sasl.protocol=SASL_PLAINTEXT

cluster2.efak.sasl.mechanism=PLAIN

cluster2.efak.sasl.jaas.config=org.apache.kafka.common.security.plain.PlainLoginModule required username="kafka" password="kafka-eagle";

cluster2.efak.sasl.client.id=

cluster2.efak.blacklist.topics=

cluster2.efak.sasl.cgroup.enable=false

cluster2.efak.sasl.cgroup.topics=

######################################

# kafka ssl authenticate

######################################

cluster3.efak.ssl.enable=false

cluster3.efak.ssl.protocol=SSL

cluster3.efak.ssl.truststore.location=

cluster3.efak.ssl.truststore.password=

cluster3.efak.ssl.keystore.location=

cluster3.efak.ssl.keystore.password=

cluster3.efak.ssl.key.password=

cluster3.efak.ssl.endpoint.identification.algorithm=https

cluster3.efak.blacklist.topics=

cluster3.efak.ssl.cgroup.enable=false

cluster3.efak.ssl.cgroup.topics=

######################################

# kafka sqlite jdbc driver address

######################################

# 配置 mysql 连接

efak.driver=com.mysql.jdbc.Driver

efak.url=jdbc:mysql://hadoop102:3306/ke?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull

efak.username=root

efak.password=000000

######################################

# kafka mysql jdbc driver address

######################################

#efak.driver=com.mysql.cj.jdbc.Driver

#efak.url=jdbc:mysql://127.0.0.1:3306/ke?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull

#efak.username=root

#efak.password=123456

7)添加环境变量

[atguigu@hadoop102 conf]$ sudo vim /etc/profile.d/my_env.sh

# kafkaEFAK

export KE_HOME=/opt/module/efak

export PATH=$PATH:$KE_HOME/bin

8)刷新环境变量

[atguigu@hadoop102 conf]$ source /etc/profile

9)启动 ZK 以及 KAFKA。

[atguigu@hadoop102 kafka]$ zk.sh start

[atguigu@hadoop102 kafka]$ kf.sh start

10)启动 efak

[atguigu@hadoop102 efak]$ bin/ke.sh start

Version 2.0.8 -- Copyright 2016-2021

*****************************************************************

* EFAK Service has started success.

* Welcome, Now you can visit 'http://192.168.10.102:8048'

* Account:admin ,Password:123456

*****************************************************************

* <Usage> ke.sh [start|status|stop|restart|stats] </Usage>

* <Usage> https://www.kafka-eagle.org/ </Usage>

*****************************************************************

注:如果要停止efak:

[atguigu@hadoop102 efak]$ bin/ke.sh stop

Kafka-Eagle页面操作

登录页面查看监控数据

http://192.168.10.102:8048/

账密:admin/123456

Kafka-Kraft架构

Kafka 现有架构:元数据保存在 zookeeper 中,运行时动态选举 controller,由controller 进行 Kafka 集群管理。

Kafka-Kraft架构(实验性):不再依赖 zookeeper 集群,而是用三台 controller 节点代替 zookeeper,元数据保存在 controller 中,由 controller 直接进行 Kafka 集群管理。

好处:

  1. Kafka 不再依赖外部框架,而是能够独立运行;
  2. controller 管理集群时,不再需要从 zookeeper 中先读取数据,集群性能上升;
  3. 由于不依赖 zookeeper,集群扩展时不再受到 zookeeper 读写能力限制;
  4. controller 不再动态选举,而是由配置文件规定。这样我们可以有针对性的加强controller 节点的配置,而不是像以前一样对随机 controller 节点的高负载束手无策。

Kafka-Kraft集群部署

集群部署

1)再次解压一份 kafka 安装包

[atguigu@hadoop102 software]$ tar -zxvf kafka_2.12-3.0.0.tgz -C /opt/module/

2)重命名为 kafka2

[atguigu@hadoop102 module]$ mv kafka_2.12-3.0.0/ kafka2

3)修改/opt/module/kafka2/config/kraft/server.properties 配置文件

[atguigu@hadoop102 module]$ cd kafka2/config/kraft/

[atguigu@hadoop102 kraft]$ vim server.properties

#kafka 的角色(controller 相当于主机、broker 节点相当于从机,主机类似 zk 功能)

process.roles=broker, controller

#节点 ID

node.id=2

#controller 服务协议别名

controller.listener.names=CONTROLLER

#全 Controller 列表

controller.quorum.voters=2@hadoop102:9093,3@hadoop103:9093,4@hadoop104:9093

#不同服务器绑定的端口

listeners=PLAINTEXT://:9092,CONTROLLER://:9093

#broker 服务协议别名

inter.broker.listener.name=PLAINTEXT

#broker 对外暴露的地址

advertised.Listeners=PLAINTEXT://hadoop102:9092

#协议别名到安全协议的映射

listener.security.protocol.map=CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,SSL:SSL,SASL_PLAINTEXT:SASL_PLAINTEXT,SASL_SSL:SASL_SSL

#kafka 数据存储目录

log.dirs=/opt/module/kafka2/data

4)分发 kafka2

[atguigu@hadoop102 module]$ xsync kafka2/

  1. 在hadoop103和hadoop104上修改/opt/module/kafka2/config/kraft/server.properties 配置文件:

修改node.id,值需要和controller.quorum.voters 对应

根据各自的主机名称,修改相应的advertised.Listeners地址

6)初始化集群数据目录

(1)首先生成存储目录唯一ID。

[atguigu@hadoop102 kafka2]$ bin/kafka-storage.sh random-uuid

J7s9e8PPTKOO47PxzI39VA

(2)用该 ID 格式化 kafka 存储目录(三台节点)。

[atguigu@hadoop102 kafka2]$ bin/kafka-storage.sh format -t J7s9e8PPTKOO47PxzI39VA -c /opt/module/kafka2/config/kraft/server.properties

[atguigu@hadoop103 kafka2]$ bin/kafka-storage.sh format -t J7s9e8PPTKOO47PxzI39VA -c /opt/module/kafka2/config/kraft/server.properties

[atguigu@hadoop104 kafka2]$ bin/kafka-storage.sh format -t J7s9e8PPTKOO47PxzI39VA -c /opt/module/kafka2/config/kraft/server.properties

6)启动 kafka 集群

[atguigu@hadoop102 kafka2]$ bin/kafka-server-start.sh -daemon config/kraft/server.properties

[atguigu@hadoop103 kafka2]$ bin/kafka-server-start.sh -daemon config/kraft/server.properties

[atguigu@hadoop104 kafka2]$ bin/kafka-server-start.sh -daemon config/kraft/server.properties

7)停止 kafka 集群

[atguigu@hadoop102 kafka2]$ bin/kafka-server-stop.sh

[atguigu@hadoop103 kafka2]$ bin/kafka-server-stop.sh

[atguigu@hadoop104 kafka2]$ bin/kafka-server-stop.sh

Kafka-Kraft 集群启停脚本

1)在/home/atguigu/bin 目录下创建文件 kf2.sh 脚本文件

[atguigu@hadoop102 bin]$ vim kf2.sh

脚本如下:

#! /bin/bash

case $1 in

"start"){

 for i in hadoop102 hadoop103 hadoop104

 do

 echo " --------启动 $i Kafka2-------"

 ssh $i "/opt/module/kafka2/bin/kafka-server-start.sh -daemon /opt/module/kafka2/config/kraft/server.properties"

 done

};;

"stop"){

 for i in hadoop102 hadoop103 hadoop104

 do

 echo " --------停止 $i Kafka2-------"

 ssh $i "/opt/module/kafka2/bin/kafka-server-stop.sh "

 done

};;

esac

2)添加执行权限

[atguigu@hadoop102 bin]$ chmod +x kf2.sh

3)启动集群命令

[atguigu@hadoop102 ~]$ kf2.sh start

4)停止集群命令

[atguigu@hadoop102 ~]$ kf2.sh stop

    1. Flume

Flume 是一个在大数据开发中非常常用的组件。可以用于 Kafka 的生产者,也可以用于Kafka 的消费者。

作为生产者:/opt/module/applog/app.*-》taildirsource-》memory channel-》kafkasink-》Kafka主题-》Kafka消费者

作为消费者:Kafka生产者-》Kafka主题-》kafkasource-》memory channel-》logger-》打印到控制台

Flume生产者

(1)启动 zk和kafka 集群

[atguigu@hadoop102 ~]$ zk.sh start

[atguigu@hadoop102 ~]$ kf.sh start

(2)启动 kafka 消费者

[atguigu@hadoop103 kafka]$ bin/kafka-console-consumer.sh --bootstrap-server hadoop102:9092 --topic first

(3)Flume 安装步骤

在 hadoop102 主机上安装 Flume。

(4)配置 Flume

在 hadoop102 节点的 Flume 的 job 目录下创建 file_to_kafka.conf

[atguigu@hadoop102 flume]$ mkdir jobs

[atguigu@hadoop102 flume]$ vim jobs/file_to_kafka.conf

配置文件内容如下

# 1 组件定义

a1.sources = r1

a1.sinks = k1

a1.channels = c1

# 2 配置 source

a1.sources.r1.type = TAILDIR

a1.sources.r1.filegroups = f1

a1.sources.r1.filegroups.f1 = /opt/module/applog/app.*

a1.sources.r1.positionFile =

/opt/module/flume/taildir_position.json

# 3 配置 channel

a1.channels.c1.type = memory

a1.channels.c1.capacity = 1000

a1.channels.c1.transactionCapacity = 100

# 4 配置 sink

a1.sinks.k1.type = org.apache.flume.sink.kafka.KafkaSink

a1.sinks.k1.kafka.bootstrap.servers = hadoop102:9092,hadoop103:9092,hadoop104:9092

a1.sinks.k1.kafka.topic = first

a1.sinks.k1.kafka.flumeBatchSize = 20

a1.sinks.k1.kafka.producer.acks = 1

a1.sinks.k1.kafka.producer.linger.ms = 1

# 5 拼接组件

a1.sources.r1.channels = c1

a1.sinks.k1.channel = c1

(5)启动 Flume

[atguigu@hadoop102 flume]$ bin/flume-ng agent -c conf/ -n a1 -f jobs/file_to_kafka.conf &

注:&表示后台启动

(6)向/opt/module/applog/app.log 里追加数据,查看 kafka 消费者消费情况

[atguigu@hadoop102 module]$ mkdir applog

[atguigu@hadoop102 applog]$ echo hello >> /opt/module/applog/app.log

(7)观察 kafka 消费者,能够看到消费的 hello 数据

Flume消费者

(1)配置 Flume

在 hadoop102 节点的 Flume 的/opt/module/flume/jobs 目录下创建 kafka_to_file.conf

[atguigu@hadoop102 jobs]$ vim kafka_to_file.conf

配置文件内容如下

# 1 组件定义

a1.sources = r1

a1.sinks = k1

a1.channels = c1

# 2 配置 source

a1.sources.r1.type = org.apache.flume.source.kafka.KafkaSource

a1.sources.r1.batchSize = 50

a1.sources.r1.batchDurationMillis = 200

a1.sources.r1.kafka.bootstrap.servers = hadoop102:9092

a1.sources.r1.kafka.topics = first

a1.sources.r1.kafka.consumer.group.id = custom.g.id

# 3 配置 channel

a1.channels.c1.type = memory

a1.channels.c1.capacity = 1000

a1.channels.c1.transactionCapacity = 100

# 4 配置 sink

a1.sinks.k1.type = logger

# 5 拼接组件

a1.sources.r1.channels = c1

a1.sinks.k1.channel = c1

(2)启动 Flume

[atguigu@hadoop102 flume]$ bin/flume-ng agent -c conf/ -n a1 -f jobs/kafka_to_file.conf -Dflume.root.logger=INFO,console

(3)启动 kafka 生产者

[atguigu@hadoop103 kafka]$ bin/kafka-console-producer.sh --bootstrap-server hadoop102:9092 --topic first

并输入数据,例如:hello world

(4)观察控制台输出的日志

    1. Flink

Flink是一个在大数据开发中非常常用的组件。可以用于 Kafka 的生产者,也可以用于Kafka 的消费者。

Flink环境准备

(1)创建一个 maven 项目 flink-kafka

(2)添加配置文件

<dependencies>

    <dependency>

        <groupId>org.apache.flink</groupId>

        <artifactId>flink-java</artifactId>

        <version>1.13.0</version>

    </dependency>

    <dependency>

        <groupId>org.apache.flink</groupId>

        <artifactId>flink-streaming-java_2.12</artifactId>

        <version>1.13.0</version>

    </dependency>

    <dependency>

        <groupId>org.apache.flink</groupId>

        <artifactId>flink-clients_2.12</artifactId>

        <version>1.13.0</version>

    </dependency>

    <dependency>

        <groupId>org.apache.flink</groupId>

        <artifactId>flink-connector-kafka_2.12</artifactId>

        <version>1.13.0</version>

    </dependency>

</dependencies>

(3)将 log4j.properties 文件添加到 resources 里面,就能更改打印日志的级别为 error

log4j.rootLogger=error, stdout,R

log4j.appender.stdout=org.apache.log4j.ConsoleAppender

log4j.appender.stdout.layout=org.apache.log4j.PatternLayout

log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss,SSS} %5p --- [%50t] %-80c(line:%5L) : %m%n

log4j.appender.R=org.apache.log4j.RollingFileAppender

log4j.appender.R.File=../log/agent.log

log4j.appender.R.MaxFileSize=1024KB

log4j.appender.R.MaxBackupIndex=1

log4j.appender.R.layout=org.apache.log4j.PatternLayout

log4j.appender.R.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss,SSS} %5p --- [%50t] %-80c(line:%6L) : %m%n

(4)在 java 文件夹下创建包名为 com.atguigu.flink

Flink生产者

(1)在 com.atguigu.flink 包下创建 java 类:FlinkKafkaProducer1

package com.atguigu.flink;

import org.apache.flink.api.common.serialization.SimpleStringSchema;

import org.apache.flink.streaming.api.datastream.DataStream;

import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;

import org.apache.flink.streaming.connectors.kafka.FlinkKafkaProducer;

import org.apache.kafka.clients.producer.ProducerConfig;

import java.util.ArrayList;

import java.util.Properties;

public class FlinkKafkaProducer1 {

    public static void main(String[] args) throws Exception {

        // 0 初始化 flink 环境

        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

        env.setParallelism(3);

        // 1 读取集合中数据

        ArrayList<String> wordsList = new ArrayList<>();

        wordsList.add("hello");

        wordsList.add("world");

        DataStream<String> stream = env.fromCollection(wordsList);

        // 2 kafka 生产者配置信息

        Properties properties = new Properties();

        properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "hadoop102:9092");

        // 3 创建 kafka 生产者

        FlinkKafkaProducer<String> kafkaProducer = new FlinkKafkaProducer<>(

                "first", new SimpleStringSchema(), properties);

        // 4 生产者和 flink 流关联

        stream.addSink(kafkaProducer);

        // 5 执行

        env.execute();

    }

}

(2)启动 Kafka 消费者

[atguigu@hadoop104 kafka]$ bin/kafka-console-consumer.sh --bootstrap-server hadoop102:9092 --topic first

(3)执行 FlinkKafkaProducer1 程序,观察 kafka 消费者控制台情况

Flink消费者

(1)在 com.atguigu.flink 包下创建 java 类:FlinkKafkaConsumer1

package com.atguigu.flink;

import org.apache.flink.api.common.serialization.SimpleStringSchema;

import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;

import org.apache.flink.streaming.connectors.kafka.FlinkKafkaConsumer;

import org.apache.kafka.clients.consumer.ConsumerConfig;

import org.apache.kafka.common.serialization.StringDeserializer;

import java.util.Properties;

public class FlinkKafkaConsumer1 {

    public static void main(String[] args) throws Exception {

        // 0 初始化 flink 环境

        StreamExecutionEnvironment env =

                StreamExecutionEnvironment.getExecutionEnvironment();

        env.setParallelism(3);

        // 1 kafka 消费者配置信息

        Properties properties = new Properties();

        properties.setProperty(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,

                "hadoop102:9092");

        // 2 创建 kafka 消费者

        FlinkKafkaConsumer<String> kafkaConsumer = new FlinkKafkaConsumer<>(

                "first", new SimpleStringSchema(), properties);

        // 3 消费者和 flink 流关联

        env.addSource(kafkaConsumer).print();

        // 4 执行

        env.execute();

    }

}

(2)启动 FlinkKafkaConsumer1 消费者

(3)启动 kafka 生产者

[atguigu@hadoop103 kafka]$ bin/kafka-console-producer.sh --bootstrap-server hadoop102:9092 --topic first

(4)观察 IDEA 控制台数据打印

Spark 是一个在大数据开发中非常常用的组件。可以用于 Kafka 的生产者,也可以用于Spark 的消费者。

Scala 环境准备

Spark 环境准备

(1)创建一个 maven 项目 spark-kafka

(2)在项目 spark-kafka 上点击右键,Add Framework Support=》勾选 scala

(3)在 main 下创建 scala 文件夹,并右键 Mark Directory as Sources Root=>在 scala 下创建包名为 com.atguigu.spark

(4)添加配置文件

<dependencies>

    <dependency>

        <groupId>org.apache.spark</groupId>

        <artifactId>spark-streaming-kafka-0-10_2.12</artifactId>

        <version>3.0.0</version>

    </dependency>

</dependencies>

(5)将 log4j.properties 文件添加到 resources 里面,就能更改打印日志的级别为 error

log4j.rootLogger=error, stdout,R

log4j.appender.stdout=org.apache.log4j.ConsoleAppender

log4j.appender.stdout.layout=org.apache.log4j.PatternLayout

log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss,SSS} %5p --- [%50t] %-80c(line:%5L) : %m%n

log4j.appender.R=org.apache.log4j.RollingFileAppender

log4j.appender.R.File=../log/agent.log

log4j.appender.R.MaxFileSize=1024KB

log4j.appender.R.MaxBackupIndex=1

log4j.appender.R.layout=org.apache.log4j.PatternLayout

log4j.appender.R.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss,SSS} %5p --- [%50t] %-80c(line:%6L) : %m%n

Spark生产者

  1. 在 com.atguigu.spark 包下创建 scala Object:SparkKafkaProducer

package com.atguigu.spark

import java.util.Properties

import org.apache.kafka.clients.producer.{KafkaProducer,ProducerRecord}

object SparkKafkaProducer {

    def main(args: Array[String]): Unit = {

        // 0 kafka 配置信息

        val properties = new Properties()

        properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "hadoop102:9092,hadoop103:9092,hadoop104:9092")

        properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, classOf[StringSerializer])

        properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, classOf[StringSerializer])

        // 1 创建 kafka 生产者

        var producer = new KafkaProducer[String, String](properties)

        // 2 发送数据

        for (i <- 1 to 5){

            producer.send(new ProducerRecord[String,String]("first","atguigu" + i))

        }

        // 3 关闭资源

        producer.close()

    }

}

(2)启动 Kafka 消费者

[atguigu@hadoop104 kafka]$ bin/kafka-console-consumer.sh --bootstrap-server hadoop102:9092 --topic first

(3)执行 SparkKafkaProducer 程序,观察 kafka 消费者控制台情况

Spark消费者

  1. 添加配置文件
  2. 在 com.atguigu.spark 包下创建 scala Object:SparkKafkaConsumer

<dependencies>

    <dependency>

        <groupId>org.apache.spark</groupId>

        <artifactId>spark-streaming-kafka-0-10_2.12</artifactId>

        <version>3.0.0</version>

    </dependency>

    <dependency>

        <groupId>org.apache.spark</groupId>

        <artifactId>spark-core_2.12</artifactId>

        <version>3.0.0</version>

    </dependency>

    <dependency>

        <groupId>org.apache.spark</groupId>

        <artifactId>spark-streaming_2.12</artifactId>

        <version>3.0.0</version>

    </dependency>

</dependencies>

package com.atguigu.spark

import org.apache.kafka.clients.consumer.{ConsumerConfig, ConsumerRecord}

import org.apache.kafka.common.serialization.StringDeserializer

import org.apache.spark.SparkConf

import org.apache.spark.streaming.dstream.{DStream, InputDStream}

import org.apache.spark.streaming.{Seconds, StreamingContext}

import org.apache.spark.streaming.kafka010.{ConsumerStrategies, KafkaUtils, LocationStrategies}

object SparkKafkaConsumer {

    def main(args: Array[String]): Unit = {

        //1.创建 SparkConf

        val sparkConf: SparkConf = new SparkConf().setAppName("sparkstreaming").setMaster("local[*]")

        //2.创建 StreamingContext

        val ssc = new StreamingContext(sparkConf, Seconds(3))

        //3.定义 Kafka 参数:kafka 集群地址、消费者组名称、key 序列化、value 序列化

        val kafkaPara: Map[String, Object] = Map[String, Object](

            ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG -> "hadoop102:9092,hadoop103:9092,hadoop104:9092",

            ConsumerConfig.GROUP_ID_CONFIG -> "atguiguGroup",

            ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG -> classOf[StringDeserializer],

            ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG -> classOf[StringDeserializer]

        )

        //4.读取 Kafka 数据创建 DStream

        val kafkaDStream: InputDStream[ConsumerRecord[String, String]]

                = KafkaUtils.createDirectStream[String, String](

                    ssc,

                    LocationStrategies.PreferConsistent, //优先位置

                    ConsumerStrategies.Subscribe[String, String](Set("first"), kafkaPara)// 消费策略:(订阅多个主题,配置参数)

        )

        //5.将每条消息的 KV 取出

        val valueDStream: DStream[String] = kafkaDStream.map(record => record.value())

        //6.计算 WordCount

        valueDStream.print()

        //7.开启任务

        ssc.start()

        ssc.awaitTermination()

    }

}

(3)启动 SparkKafkaConsumer 消费者

(4)启动 kafka 生产者

[atguigu@hadoop103 kafka]$ bin/kafka-console-producer.sh --bootstrap-server hadoop102:9092 --topic first

(5)观察 IDEA 控制台数据打印

环境

ZooKeeper、Kafka。

依赖

pom.xml 依赖文件如下,springboot 版本是 2.3.0.RELEASE:

<dependencies>

    <dependency>

        <groupId>org.springframework.boot</groupId>

        <artifactId>spring-boot-starter</artifactId>

    </dependency>

    <!-- kafka -->

    <dependency>

        <groupId>org.springframework.kafka</groupId>

        <artifactId>spring-kafka</artifactId>

        <version>2.4.0.RELEASE</version>

    </dependency>

    <!-- lombok -->

    <dependency>

        <groupId>org.projectlombok</groupId>

        <artifactId>lombok</artifactId>

    </dependency>

    <!-- test -->

    <dependency>

        <groupId>org.springframework.boot</groupId>

        <artifactId>spring-boot-starter-test</artifactId>

        <scope>test</scope>

    </dependency>

</dependencies>

配置

可以通过Java类配置和配置文件配置两种方式来配置 Kafka

Java 类配置

配置主题(KafkaAdmin)

创建配置类 KafkaTopicConfiguration 代码如下,该配置可选,通常会事先通过 Kafka 提供的脚本创建主题:

/**

 * kafka 主题配置类

 **/

@Configuration

public class KafkaTopicConfiguration {

    /**

     * 创建 KafkaAmin,可以自动检测集群中是否存在topic,不存在则创建

     * @return

     */

    @Bean

    public KafkaAdmin kafkaAdmin() {

        Map<String, Object> props = new HashMap<>();

        props.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, "127.0.0.1:9092,127.0.0.1:9093,127.0.0.1:9094");

        return new KafkaAdmin(props);

    }

    @Bean

    public NewTopic newTopic() {

        // 创建 topic,指定 名称、分区数、副本数

        return new NewTopic("hello-kafka-test-topic", 3, (short) 2);

    }

}

配置生产者

创建配置类KafkaProducerConfiguration 代码如下,里面涉及基本配置和自定义分区器、拦截器、事务等配置:

/**

 * kafka 生产者配置类

 **/

@Configuration

public class KafkaProducerConfiguration {

    /**

     * 不包含事务 producerFactory

     * @return

     */

    public ProducerFactory<String, String> producerFactory() {

        Map<String, Object> props = new HashMap<>();

        //kafka 集群地址

        props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "127.0.0.1:9092,127.0.0.1:9093,127.0.0.1:9094");

        //重试次数

        props.put(ProducerConfig.RETRIES_CONFIG, 3);

        //应答级别

        //acks=0 把消息发送到kafka就认为发送成功

        //acks=1 把消息发送到kafka leader分区,并且写入磁盘就认为发送成功

        //acks=all 把消息发送到kafka leader分区,并且leader分区的副本follower对消息进行了同步就任务发送成功

        props.put(ProducerConfig.ACKS_CONFIG, "all");

        //KafkaProducer.send() 和 partitionsFor() 方法的最长阻塞时间 单位 ms

        props.put(ProducerConfig.MAX_BLOCK_MS_CONFIG, 6000);

        //批量处理的最大大小 单位 byte

        props.put(ProducerConfig.BATCH_SIZE_CONFIG, 4096);

        //发送延时,当生产端积累的消息达到batch-size或接收到消息linger.ms后,生产者就会将消息提交给kafka

        props.put(ProducerConfig.LINGER_MS_CONFIG, 1000);

        //生产者可用缓冲区的最大值 单位 byte

        props.put(ProducerConfig.BUFFER_MEMORY_CONFIG, 33554432);

        //每条消息最大的大小

        props.put(ProducerConfig.MAX_REQUEST_SIZE_CONFIG, 1048576);

        //客户端ID

        props.put(ProducerConfig.CLIENT_ID_CONFIG, "hello-kafka");

        //Key 序列化方式

        props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());

        //Value 序列化方式

        props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());

        //消息压缩:none、lz4、gzip、snappy,默认为 none。

        props.put(ProducerConfig.COMPRESSION_TYPE_CONFIG, "gzip");

        //自定义分区器

        props.put(ProducerConfig.PARTITIONER_CLASS_CONFIG, MyPartitioner.class.getName());

        return new DefaultKafkaProducerFactory<>(props);

    }

    /**

     * 包含事务 producerFactory

     * @return

     */

    public ProducerFactory<String, String> producerFactoryWithTransaction() {

        DefaultKafkaProducerFactory<String, String> defaultKafkaProducerFactory = (DefaultKafkaProducerFactory<String, String>) producerFactory();

        //设置事务Id前缀

        defaultKafkaProducerFactory.setTransactionIdPrefix("tx");

        return defaultKafkaProducerFactory;

    }

    /**

     * 不包含事务 kafkaTemplate

     * @return

     */

    @Bean("kafkaTemplate")

    public KafkaTemplate<String, String> kafkaTemplate() {

        return new KafkaTemplate<>(producerFactory());

    }

    /**

     * 包含事务 kafkaTemplate

     * @return

     */

    @Bean("kafkaTemplateWithTransaction")

    public KafkaTemplate<String, String> kafkaTemplateWithTransaction() {

        return new KafkaTemplate<>(producerFactoryWithTransaction());

    }

    /**

     * 以该方式配置事务管理器:就不能以普通方式发送消息,只能通过 kafkaTemplate.executeInTransaction 或

     * 在方法上加 @Transactional 注解来发送消息,否则报错

     * @param producerFactory

     * @return

     */

//    @Bean

//    public KafkaTransactionManager<Integer, String> kafkaTransactionManager(ProducerFactory<Integer, String> producerFactory) {

//        return new KafkaTransactionManager<>(producerFactory);

//    }

}

配置消费者

创建配置类KafkaConsumerConfiguration代码如下:

/**

 * kafka 消费者配置类

 **/

@Slf4j

@Configuration

public class KafkaConsumerConfiguration {

    @Bean

    public KafkaListenerContainerFactory<ConcurrentMessageListenerContainer<String, String>> kafkaListenerContainerFactory() {

        ConcurrentKafkaListenerContainerFactory<String, String> factory = new ConcurrentKafkaListenerContainerFactory<>();

        //设置 consumerFactory

        factory.setConsumerFactory(consumerFactory());

        //设置是否开启批量监听

        factory.setBatchListener(false);

        //设置消费者组中的线程数量

        factory.setConcurrency(1);

        return factory;

    }

    /**

     * consumerFactory

     * @return

     */

    public ConsumerFactory<String, Object> consumerFactory() {

        Map<String, Object> props = new HashMap<>();

        //kafka集群地址

        props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "127.0.0.1:9092,127.0.0.1:9093,127.0.0.1:9094");

        //自动提交 offset 默认 true

        props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);

        //自动提交的频率 单位 ms

        props.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, 1000);

        //批量消费最大数量

        props.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, 100);

        //消费者组

        props.put(ConsumerConfig.GROUP_ID_CONFIG, "testGroup");

        //session超时,超过这个时间consumer没有发送心跳,就会触发rebalance操作

        props.put(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG, 120000);

        //请求超时

        props.put(ConsumerConfig.REQUEST_TIMEOUT_MS_CONFIG, 120000);

        //Key 反序列化类

        props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());

        //Value 反序列化类

        props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());

        //当kafka中没有初始offset或offset超出范围时将自动重置offset

        //earliest:重置为分区中最小的offset

        //latest:重置为分区中最新的offset(消费分区中新产生的数据)

        //none:只要有一个分区不存在已提交的offset,就抛出异常

        props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "latest");

        //设置Consumer拦截器

        props.put(ConsumerConfig.INTERCEPTOR_CLASSES_CONFIG, MyConsumerInterceptor.class.getName());

        return new DefaultKafkaConsumerFactory<>(props);

    }

    /**

     * 消费异常处理器

     * @return

     */

    @Bean

    public ConsumerAwareListenerErrorHandler consumerAwareListenerErrorHandler() {

        return new ConsumerAwareListenerErrorHandler() {

            @Override

            public Object handleError(Message<?> message, ListenerExecutionFailedException exception, Consumer<?, ?> consumer) {

                //打印消费异常的消息和异常信息

                log.error("consumer failed! message: {}, exceptionMsg: {}, groupId: {}", message, exception.getMessage(), exception.getGroupId());

                return null;

            }

        };

    }

}

配置文件配置

以配置文件的方式来进行配置的话,所有配置都在application.yml中,配置内容如下:

spring:

  application:

    name: hello-kafka

  kafka:

    listener:

      #设置是否批量消费,默认 single(单条),batch(批量)

      type: single

    # 集群地址

    bootstrap-servers: 127.0.0.1:9092,127.0.0.1:9093,127.0.0.1:9094

    # 生产者配置

    producer:

      # 重试次数

      retries: 3

      # 应答级别

      # acks=0 把消息发送到kafka就认为发送成功

      # acks=1 把消息发送到kafka leader分区,并且写入磁盘就认为发送成功

      # acks=all 把消息发送到kafka leader分区,并且leader分区的副本follower对消息进行了同步就任务发送成功

      acks: all

      # 批量处理的最大大小 单位 byte

      batch-size: 4096

      # 发送延时,当生产端积累的消息达到batch-size或接收到消息linger.ms后,生产者就会将消息提交给kafka

      buffer-memory: 33554432

      # 客户端ID

      client-id: hello-kafka

      # Key 序列化类

      key-serializer: org.apache.kafka.common.serialization.StringSerializer

      # Value 序列化类

      value-serializer: org.apache.kafka.common.serialization.StringSerializer

      # 消息压缩:none、lz4、gzip、snappy,默认为 none。

      compression-type: gzip

      properties:

        partitioner:

          #指定自定义分区器

          class: top.zysite.hello.kafka.partitioner.MyPartitioner

        linger:

          # 发送延时,当生产端积累的消息达到batch-size或接收到消息linger.ms后,生产者就会将消息提交给kafka

          ms: 1000

        max:

          block:

            # KafkaProducer.send() 和 partitionsFor() 方法的最长阻塞时间 单位 ms

            ms: 6000

    # 消费者配置

    consumer:

      # 默认消费者组

      group-id: testGroup

      # 自动提交 offset 默认 true

      enable-auto-commit: false

      # 自动提交的频率 单位 ms

      auto-commit-interval: 1000

      # 批量消费最大数量

      max-poll-records: 100

      # Key 反序列化类

      key-deserializer: org.apache.kafka.common.serialization.StringDeserializer

      # Value 反序列化类

      value-deserializer: org.apache.kafka.common.serialization.StringDeserializer

      # 当kafka中没有初始offset或offset超出范围时将自动重置offset

      # earliest:重置为分区中最小的offset

      # latest:重置为分区中最新的offset(消费分区中新产生的数据)

      # none:只要有一个分区不存在已提交的offset,就抛出异常

      auto-offset-reset: latest

      properties:

        interceptor:

          classes: top.zysite.hello.kafka.interceptor.MyConsumerInterceptor

        session:

          timeout:

            # session超时,超过这个时间consumer没有发送心跳,就会触发rebalance操作

            ms: 120000

        request:

          timeout:

            # 请求超时

            ms: 120000

启用Kafka

在 SpringBoot 启动类上添加注解@EnableKafka启用Kafka:

@SpringBootApplication

@EnableKafka

public class HelloKafkaApplication {

    public static void main(String[] args) {

        SpringApplication.run(HelloKafkaApplication.class, args);

    }

}

自定义分区器

可以看到生产者端配置了自定义分区器:

        props.put(ProducerConfig.PARTITIONER_CLASS_CONFIG, MyPartitioner.class.getName());

自定义分区器需实现Partitioner接口,代码如下:

/**

 * 自定义分区器

 **/

public class MyPartitioner implements Partitioner {

    /**

     * 分区策略核心方法

     * @param topic

     * @param key

     * @param keyBytes

     * @param value

     * @param valueBytes

     * @param cluster

     * @return

     */

    @Override

    public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {

        //具体分区逻辑,这里全部发送到0号分区

        return 0;

    }

    @Override

    public void close() {

    }

    @Override

    public void configure(Map<String, ?> configs) {

    }

}

生产者拦截器

生产者拦截器需要实现ProducerInterceptor接口,参考消费者拦截器。

生产者端配置生产者拦截器:

        props.put(ConsumerConfig.INTERCEPTOR_CLASSES_CONFIG, MyProducerInterceptor.class.getName());

消费者拦截器

可以看到消费者端配置了消费者拦截器:

        props.put(ConsumerConfig.INTERCEPTOR_CLASSES_CONFIG, MyConsumerInterceptor.class.getName());

消费者拦截器需实现ConsumerInterceptor接口,代码如下:

/**

 * 消费者拦截器

 **/

@Slf4j

public class MyConsumerInterceptor implements ConsumerInterceptor<String, String> {

    /**

     * KafkaConsumer 会在 poll 方法返回之前调用该方法,可以在该方法中对消息进行过滤

     * @param records

     * @return

     */

    @Override

    public ConsumerRecords<String, String> onConsume(ConsumerRecords<String, String> records) {

        log.debug("********** before interceptor: " + records.count() + "**********");

        Map<TopicPartition, List<ConsumerRecord<String, String>>> newRecords = new HashMap<>();

        //遍历每个topic、partition

        for (TopicPartition topicPartition : records.partitions()) {

            //获取特定topic、partition下的消息列表

            List<ConsumerRecord<String, String>> recordList = records.records(topicPartition);

            //过滤

            List<ConsumerRecord<String, String>> filteredList = recordList.stream()

                    .filter(record -> !record.value().contains("filter")).collect(Collectors.toList());

            //放入新的消息记录里

            newRecords.put(topicPartition, filteredList);

        }

        ConsumerRecords<String, String> filteredRecords = new ConsumerRecords<>(newRecords);

        log.debug("********** after interceptor: " + filteredRecords.count() + "**********");

        //返回过滤后的消息记录

        return filteredRecords;

    }

    /**

     * 提交完offset之后调用该方法

     * @param offsets

     */

    @Override

    public void onCommit(Map<TopicPartition, OffsetAndMetadata> offsets) {

        if (!offsets.isEmpty()) {

            offsets.forEach(((topicPartition, offsetAndMetadata) -> {

                log.info("partition : " + topicPartition + ", offset : " + offsetAndMetadata);

            }));

        }

    }

    @Override

    public void close() {

    }

    @Override

    public void configure(Map<String, ?> configs) {

    }

}

测试

为了方便看到效果,这里统一测试生产者和消费者。

创建生产者服务类,代码如下:

/**

 * kafka 生产服务

 *

 * @author Leo

 * @create 2020/12/31 16:06

 **/

@Slf4j

@Service

public class KafkaProducerService {

    @Qualifier("kafkaTemplate")

    @Resource

    private KafkaTemplate<String, String> kafkaTemplate;

    @Qualifier("kafkaTemplateWithTransaction")

    @Resource

    private KafkaTemplate<String, String> kafkaTemplateWithTransaction;

    /**

     * 发送消息(同步)

     * @param topic 主题

     * @param key 键

     * @param message 值

     */

    public void sendMessageSync(String topic, String key, String message) throws InterruptedException, ExecutionException, TimeoutException {

        //可以指定最长等待时间,也可以不指定

        kafkaTemplate.send(topic, message).get(10, TimeUnit.SECONDS);

        log.info("sendMessageSync => topic: {}, key: {}, message: {}", topic, key, message);

        //指定key,kafka根据key进行hash,决定存入哪个partition

//        kafkaTemplate.send(topic, key, message).get(10, TimeUnit.SECONDS);

        //存入指定partition

//        kafkaTemplate.send(topic, 0, key, message).get(10, TimeUnit.SECONDS);

    }

    /**

     * 发送消息并获取结果

     * @param topic

     * @param message

     * @throws ExecutionException

     * @throws InterruptedException

     */

    public void sendMessageGetResult(String topic, String key, String message) throws ExecutionException, InterruptedException {

        SendResult<String, String> result = kafkaTemplate.send(topic, message).get();

        log.info("sendMessageSync => topic: {}, key: {}, message: {}", topic, key, message);

        log.info("The partition the message was sent to: " + result.getRecordMetadata().partition());

    }

    /**

     * 发送消息(异步)

     * @param topic 主题

     * @param message 消息内容

     */

    public void sendMessageAsync(String topic, String message) {

        ListenableFuture<SendResult<String, String>> future = kafkaTemplate.send(topic, message);

        //添加回调

        /* future.addCallback(new ListenableFutureCallback<SendResult<String, String>>() {

            @Override

            public void onFailure(Throwable throwable) {

                log.error("sendMessageAsync failure! topic : {}, message: {}", topic, message);

            }

            @Override

            public void onSuccess(SendResult<String, String> stringStringSendResult) {

                log.info("sendMessageAsync success! topic: {}, message: {}", topic, message);

            }

        });*/

        future.addCallback(

                result -> logger.info("成功发送消息到topic:{} partition:{}", topic, result.getRecordMetadata().partition()),

                ex -> logger.error("发送消失败,原因:{}", ex.getMessage()));

    }

    /**

     * 可以将消息组装成 Message 对象和 ProducerRecord 对象发送

     * @param topic

     * @param key

     * @param message

     * @throws InterruptedException

     * @throws ExecutionException

     * @throws TimeoutException

     */

    public void testMessageBuilder(String topic, String key, String message) throws InterruptedException, ExecutionException, TimeoutException {

        // 组装消息

        Message msg = MessageBuilder.withPayload(message)

                .setHeader(KafkaHeaders.MESSAGE_KEY, key)

                .setHeader(KafkaHeaders.TOPIC, topic)

                .setHeader(KafkaHeaders.PREFIX,"kafka_")

                .build();

        //同步发送

        kafkaTemplate.send(msg).get();

        // 组装消息

//        ProducerRecord<String, String> producerRecord = new ProducerRecord<>(topic, message);

//        kafkaTemplate.send(producerRecord).get(10, TimeUnit.SECONDS);

    }

    /**

     * 以事务方式发送消息

     * @param topic

     * @param key

     * @param message

     */

    public void sendMessageInTransaction(String topic, String key, String message) {

        kafkaTemplateWithTransaction.executeInTransaction(new KafkaOperations.OperationsCallback<String, String, Object>() {

            @Override

            public Object doInOperations(KafkaOperations<String, String> kafkaOperations) {

                kafkaOperations.send(topic, key, message);

                //出现异常将会中断事务,消息不会发送出去

                throw new RuntimeException("exception");

            }

        });

    }

}

创建消费消息服务类,代码如下:

/**

 * kafka 消费服务

 *

 * @author Leo

 * @create 2020/12/31 16:06

 **/

@Slf4j

@Service

public class KafkaConsumerService {

    /**

     * 消费单条消息,topics 可以监听多个topic,如:topics = {"topic1", "topic2"}

     * @param message 消息

     */

    @KafkaListener(id = "consumerSingle", topics = "hello-kafka-test-topic")

    public void consumerSingle(String message) {

        log.info("consumerSingle ====> message: {}", message);

    }

/*    @KafkaListener(id = "consumerBatch", topicPartitions = {

            @TopicPartition(topic = "hello-batch1", partitions = "0"),

            @TopicPartition(topic = "hello-batch2", partitionOffsets = @PartitionOffset(partition = "2", initialOffset = "4"))

    })*/

    /**

     * 批量消费消息

     * @param messages

     */

    @KafkaListener(id = "consumerBatch", topics = "hello-batch")

    public void consumerBatch(List<ConsumerRecord<String, String>> messages) {

        log.info("consumerBatch =====> messageSize: {}", messages.size());

        log.info(messages.toString());

    }

    /**

     * 指定消费异常处理器

     * @param message

     */

    @KafkaListener(id = "consumerException", topics = "hello-kafka-test-topic", errorHandler = "consumerAwareListenerErrorHandler")

    public void consumerException(String message) {

        throw new RuntimeException("consumer exception");

    }

    /**

     * 验证ConsumerInterceptor

     * @param message

     */

    @KafkaListener(id = "interceptor", topics = "consumer-interceptor")

    public void consumerInterceptor(String message) {

        log.info("consumerInterceptor ====> message: {}", message);

    }

}

使用 Junit 整合 SpringBoot 来测试,先启动应用程序,然后依次执行 Junit 中的测试方法

@RunWith(SpringRunner.class)

@SpringBootTest

class HelloKafkaApplicationTests {

    @Resource

    private KafkaProducerService kafkaProducerService;

    @Test

    void testSendMessageSync() throws Exception {

        String topic = "hello-kafka-test-topic";

        String key = "key1";

        String message = "firstMessage";

        kafkaProducerService.sendMessageSync(topic, key, message);

    }

    @Test

    public void testSendMessageGetResult() throws Exception {

        String topic = "hello-kafka-test-topic";

        String key = "key";

        String message = "helloSendMessageGetResult";

        kafkaProducerService.sendMessageGetResult(topic, key, message);

        kafkaProducerService.sendMessageGetResult(topic, null, message);

    }

    @Test

    public void testSendMessageAsync() {

        String topic = "hello-kafka-test-topic";

        String message = "firstAsyncMessage";

        kafkaProducerService.sendMessageAsync(topic, message);

        异步发送完消息 Junit 方法就结束了,没法看到成功或失败的回调打印。可通过System.in.read或 Thread.sleep来阻塞

    }

    @Test

    public void testMessageBuilder() throws Exception {

        String topic = "hello-kafka-test-topic";

        String key = "key1";

        String message = "helloMessageBuilder";

        kafkaProducerService.testMessageBuilder(topic, key, message);

    }

    /**

     * 测试事务

     */

    @Test

    public void testSendMessageInTransaction() {

        String topic = "hello-kafka-test-topic";

        String key = "key1";

        String message = "helloSendMessageInTransaction";

        kafkaProducerService.sendMessageInTransaction(topic, key, message);

    }

    /**

     * 测试批量消费

     * 执行此方法前:

     *     将KafkaConsumerConfiguration#kafkaListenerContainerFactory方法中factory.setBatchListener(false)参数改为true

     *     将KafkaConsumerService#consumerBatch方法上的@KafkaListener注解注释掉

     *     停止 SpringBoot 应用程序

     * 执行此方法

     *     启动 SpringBoot 应用程序,可以看到批量消费的日志打印

     * @throws Exception

     */

    @Test

    public void testConsumerBatch() throws Exception {

        //写入多条数据到批量topic:hello-batch

        String topic = "hello-batch";

        for(int i = 0; i < 20; i++) {

            kafkaProducerService.sendMessageSync(topic, null, "batchMessage" + i);

        }

    }

    /**

     * 测试消费者拦截器

     * 执行此方法前:

     *     将KafkaConsumerConfiguration#kafkaListenerContainerFactory方法中factory.setBatchListener(false)参数改回false

     *     重启SpringBoot 应用程序

     * @throws Exception

     */

    @Test

    public void testConsumerInterceptor() throws Exception {

        String topic = "consumer-interceptor";

        for(int i = 0; i < 2; i++) {

            kafkaProducerService.sendMessageSync(topic,null, "normalMessage" + i);

        }

        kafkaProducerService.sendMessageSync(topic, null, "filteredMessage");

        kafkaProducerService.sendMessageSync(topic, null, "filterMessage");

    }

}

基础架构

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值