一、基本概念
1、了解Kafka
1)Apache Kafka 是一个开源消息系统,由 Scala 写成。是由 Apache 软件基金会开发的一个开源消息系统项目。
2)Kafka 最初是由 LinkedIn 公司开发,并于 2011 年初开源。2012 年 10 月从 Apache Incubator 毕业。该项目的目标是为处理实时数据提供一个统一、高通量、低延时的平台。
3)Kafka 是一个分布式消息队列,具有很好的吞吐量,内置分区,复制和固有容错功能。Kafka 对消息保存时根据 Topic 进行归类,发送消息者称为 Producer,消息接受者称为 Consumer,此外 kafka 集群有多个 kafka 实例组成,每个实例(server)成为 broker。Redis 分布式界内的小钢炮!!!
4)无论是 kafka 集群,还是 producer 和 consumer 都依赖于 zookeeper 集群保存一些meta 信息,来保证系统可用性。
2、kafka消息队列内部实现原理及作用
kafka的两种模式:
(1)点对点模式(一对一,消费者主动拉取数据,消息收到后消息清除)点对点模型通常是一个基于拉取或者轮询的消息传送模型,这种模型从队列中请求信息,而不是将消息推送到客户端。这个模型的特点是发送到队列的消息被一个且只有一个接收者接收处理,即使有多个消息监听者也是如此。
(2)发布/订阅模式(一对多,数据生产后,推送给所有订阅者)发布订阅模型则是一个基于推送的消息传送模型。发布订阅模型可以有多种不同的订阅者,临时订阅者只在主动监听主题时才接收消息,而持久订阅者则监听主题的所有消息,即使当前订阅者不可用,处于离线状态。
3、Kafka 架构组成
Producer :消息生产者,就是向 kafka broker 发消息的客户端
Consumer :消息消费者,向 kafka broker 取消息的客户端
Topic :一个存储消息的队列
Consumer Group :这是kafka用来实现一个topic消息的广播(发给所有的consumer)和单播(发给任意一个 consumer)的手段。一个 topic 可以有多个 CG。topic 的消息会复制给 consumer。如果需要实现广播,只要每个 consumer 有一个独立的 CG 就可以了。要实现单播只要所有的 consumer 在同一个 CG。用 CG 还可以将 consumer 进行自由的分组而不需要多次发送消息到不同的 topic。
Broker :一台 kafka 服务器就是一个 broker。一个集群由多个 broker 组成。一个 broker可以容纳多个 topic。
Partition:为了实现扩展性,一个非常大的 topic 可以分布到多个 broker上,一个 topic 可以分为多个 partition,每个 partition 是一个有序的队列。partition 中的每条消息
都会被分配一个有序的 id(offset)。kafka 只保证按一个 partition 中的顺序将消息发给
consumer,不保证一个 topic 的整体(多个 partition 间)的顺序。
Offset:kafka 的存储文件都是按照 offset.kafka 来命名,用 offset 做名字的好处是方便查找。例如你想找位于 1024的位置,只要找到 1023.kafka 的文件即可。当然 the first offset 就是 00000000000.kafka
replication :patition的副本,一般存储在集群的多个broker上,提高故障容错。
4、kafka 分布式
Kafka 每个主题的多个分区日志分布式地存储在 Kafka 集群上,同时为了故障容错,每
个分区都会以副本的方式复制到多个消息代理节点上。其中一个节点会作主副本(Leader),其他节点作为备份副本(Follower,也叫作从副本)。主副本会负责所有的客户端读写操作,备份副本仅仅从主副本同步数据。当leader出现故障时,备份副本中的一个副本会被选择为新的主副本。因为每个分区的副本中只有主副本接受读写,所以每个服务器端都会作为某些分区的主副本,以及另外一些分区的备份副本,这样 Kafka 集群的所有服务端整体上对客户端是负载均衡的。
Kafka 的生产者和消费者相对于服务器端而言都是客户端。
Kafka 生产者客户端发布消息到服务端的指定主题,会指定消息所属的分区。生产者发
布消息时根据消息是否有键,采用不同的分区策略。消息没有键时,通过轮询方式进行客户端负载均衡;消息有键时,根据分区语义(例如 hash)确保相同键的消息总是发送到同一分区。
Kafka 的消费者通过订阅主题来消费消息,并且每个消费者都会设置一个消费组名称。
因为生产者发布到主题的每一条消息都只会发送给消费者组的一个消费者。所以,如果要实现传统消息系统的“队列”模型,可以让每个消费者都拥有相同的消费组名称,这样消息就会负载均衡到所有的消费者;如果要实现“发布-订阅”模型,则每个消费者的消费者组名称都不相同,这样每条消息就会广播给所有的消费者。
分区是消费者现场模型的最小并行单位。如下图(图 1)所示,生产者发布消息到一台
服务器的 3 个分区时,只有一个消费者消费所有的 3 个分区。在下图(图 2)中,3 个分区分布在 3 台服务器上,同时有 3 个消费者分别消费不同的分区。假设每个服务器的吞吐量时300MB,在下图(图 1)中分摊到每个分区只有 100MB,而在下图(图 2)中,集群整体的吞吐量有 900MB。可以看到,增加服务器节点会提升集群的性能,增加消费者数量会提升处理性能。
同一个消费组下多个消费者互相协调消费工作,Kafka 会将所有的分区平均地分配给所
有的消费者实例,这样每个消费者都可以分配到数量均等的分区。Kafka 的消费组管理协议会动态地维护消费组的成员列表,当一个新消费者加入消费者组,或者有消费者离开消费组,都会触发再平衡操作。
二、安装与启动
1、kafka依赖于zookeeper,所以先安装启动zookeeper
上传zookeeper压缩包 并解压
修改/my/zookeeper/zookeeper-3.4.9/conf/zoo_sample.cfg配置文件名为zoo.cfg
在zk根目录 直接启动zk:
1. 启动ZK服务: sh bin/zkServer.sh start
2. 查看ZK服务状态: sh bin/zkServer.sh status
3. 停止ZK服务: sh bin/zkServer.sh stop
4. 重启ZK服务: sh bin/zkServer.sh restart
2、启动使用kafka
在/my/kafka/kafka_2.12-2.3.0/config/server.properties 配置以下信息:
broker.id=0
#listeners=PLAINTEXT://:9092 #默认监听9092端口
log.dirs=/my/kafka/log/kafkalog #表示kafka数据的存放目录,而非Kafka的日志目录
num.partitions=1
zookeeper.connect=10.135.128.39:2181
常用命令
启动kafka 在kafka的根目录执行:
bin/kafka-server-start.sh config/server.properties
创建topic
创建一个叫做“test”的topic,它只有一个分区,一个副本
bin/kafka-topics.sh --create --zookeeper 10.135.128.39:2181 --replication-factor 1 --partitions 1 --topic test
查看topic
bin/kafka-topics.sh --list --zookeeper 10.135.128.39:2181
查看指定topic的详细信息
bin/kafka-topics.sh --describe --zookeeper 10.135.128.39:2181
生产消息
bin/kafka-console-producer.sh --broker-list 10.135.128.39:9092 --topic test
Kafka 使用一个简单的命令行producer,从文件中或者从标准输入中读取消息并发送到服务端。默认的每条命令将发送一条消息。ctrl+c可以退出发送。
消费消息
bin/kafka-console-consumer.sh --bootstrap-server 10.135.128.39:9092 --topic test --from-beginning
–from-beginning:是从producer开始的位置开始拿数据的。
在一个终端中运行consumer命令行,另一个终端中运行producer命令行,就可以在一个终端输入消息,另一个终端读取消息。
这两个命令都有自己的可选参数,可以在运行的时候不加任何参数可以看到帮助信息。
搭建一个多个broker的集群
刚才只是启动了单个broker,现在启动有3个broker组成的集群,这些broker节点也都是在本机上的:
首先为每个节点编写配置文件:
cp config/server.properties config/server-1.properties
cp config/server.properties config/server-2.properties
修改复制的配置文件:
server-1.properties中修改添加以下配置:
broker.id=1
port=9093
log.dirs=/my/kafka/log/kafka_9093
server-2.properties:
broker.id=2
port=9094
log.dirs=/my/kafka/log/kafka_9094
broker.id在集群中唯一的标注一个节点,因为在同一个机器上,所以必须制定不同的端口和日志文件,避免数据被覆盖。
启动broker
> bin/kafka-server-start.sh config/server-1.properties &
> bin/kafka-server-start.sh config/server-2.properties &
创建一个拥有3个副本的topic:
> bin/kafka-topics.sh --create --zookeeper 10.135.128.39:2181 --replication-factor 3 --partitions 1 --topic my-topic
查看详情:
bin/kafka-topics.sh --describe --zookeeper 10.135.128.39:2181 --topic my-topic
发送消息 接收消息 :
这里无论生产者连接9092 还是9093 消费者连接任意都可以读取消息, 这里我们就建立了集群,操作的都是同一个topic
我们再测试下集群的容错能力,目前leader 是briker 1 所以咱们kill 掉它
ps -ef|grep server-1.properties
再次查看详情 broker 2成为了 leader
再次测试消息发送 与 消费 仍然可以 只是9093 所在的broker 1 不能使用了而已。
三、集成SpringBoot
特别注意:kafka有很多版本的。各版本对应使用的springboot或者jar是不一样。请参考spring官网的说明:https://spring.io/projects/spring-kafka
这里我使用的kafka版本为2.3.0 springboot为2.2
首页我们启动服务端kafka
这里需要开启端口监听,修改 config/server.properties
启动9092,9093
bin/kafka-server-start.sh config/server.properties &
bin/kafka-server-start.sh config/server-1.properties &
创建一个topic
bin/kafka-topics.sh --create --zookeeper 10.135.128.39:2181 --replication-factor 1 --partitions 1 --topic test
然后我们开始SpringBoot 项目:
pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.0.BUILD-SNAPSHOT</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>SpringBootForKafka</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>SpringBootForKafka</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
<exclusion>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
</repository>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
</pluginRepository>
<pluginRepository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
</project>
application.yml:
server:
port: 8607
#============== kafka ===================
# 指定kafka 代理地址,可以多个
spring:
kafka:
bootstrap-servers: 10.135.128.39:9092,10.135.128.39:9093
producer:
# 指定消息key和消息体的编解码方式
key-serializer: org.apache.kafka.common.serialization.StringSerializer
value-serializer: org.apache.kafka.common.serialization.StringSerializer
consumer:
group-id: test #消费者组
enable-auto-commit: true #是否自动应答
auto-commit-interval: 100
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
实体类message:
public class Message {
private Long id; //id
private String msg; //消息
private Date sendTime; //时间戳
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public Date getSendTime() {
return sendTime;
}
public void setSendTime(Date sendTime) {
this.sendTime = sendTime;
}
}
消息发送者:
@Component
public class KafkaSender {
private final org.slf4j.Logger log = LoggerFactory.getLogger(getClass());
@Autowired
private KafkaTemplate<String, String> kafkaTemplate;
/* Gson提供了fromJson() 和toJson() 两个直接用于解析和生成的方法,
实现反序列化,后者实现了序列化;同时每个方法都提供了重载
*/
private Gson gson = new GsonBuilder().create();
//发送消息方法
public void send() {
Message message = new Message();
message.setId(System.currentTimeMillis());
message.setMsg(UUID.randomUUID().toString());
message.setSendTime(new Date());
log.info("+++++++++++++++++++++ message = {}", gson.toJson(message));
kafkaTemplate.send("test", gson.toJson(message));
}
}
消息消费者:
@Component
public class KafkaReceiver {
private final org.slf4j.Logger log = LoggerFactory.getLogger(getClass());
/**
* 监听消息 并消费
* @param record
*/
@KafkaListener(topics = {"test"})
public void listen(ConsumerRecord<?, ?> record) {
Optional<?> kafkaMessage = Optional.ofNullable(record.value());
if (kafkaMessage.isPresent()) {
Object message = kafkaMessage.get();
log.info("----------------- record =" + record);
log.info("------------------ message =" + message);
}
}
}
注意使用的topic一致,且是服务端存在的topic ,如果不存在会报错。
启动类:
@SpringBootApplication
public class SpringBootForKafkaApplication {
public static void main(String[] args) {
//SpringApplication.run(SpringBootForKafkaApplication.class, args);
ConfigurableApplicationContext context = SpringApplication.run(SpringBootForKafkaApplication.class, args);
KafkaSender sender = context.getBean(KafkaSender.class);
for (int i = 0; i < 3; i++) {
//调用消息发送类中的消息发送方法
sender.send();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
此致,一个简单的SpringBoot 集成kafka 的demo就完成了。
注:如果项目启动 报kafka地址连接错误, 请检查 监听端口的配置是否开启,防火墙端口是否开放。