1. MQ产品技术对比
ActiveMQ | RabbitMQ | RocketMQ | Kafka | EMQ | |
---|---|---|---|---|---|
公司/社区 | Apache | Rabbit(https://www.rabbitmq.com/) | 阿里(https://rocketmq.apache.org/) | Apache(http://kafka.apache.org/ ) | EMQ X(https://www.emqx.cn/) |
开发语言 | Java | Erlang(二郎神,高并发语言) | Java | Scala&Java | Erlang/OTP |
协议支持 | OpenWire,STOMP,REST,XMPP,AMQP | AMQP,XMPP,SMTP,STOMP | 自定义协议 | 自定义协议,社区封装了http协议支持 | MQTT、MQTT-SN、CoAP、WebSocket 或私有协议支持 |
可用性 | 一般(主从) | 高(主从) | 非常高(分布式) | 非常高(分布式) | — |
单机吞吐量 | 万级 | 万级 | 十万级 | 百万级 | 单机支持百万连接,集群支持千万级连接;毫秒级消息转发 |
消息延迟 | 毫秒级(ms) | 微秒级(us) | 毫秒级(ms) | 毫秒级以内(ms级以内) | 毫秒级(ms) |
消息可靠性 | 一般 | 高 | 高 | 高(数据副本) | 高 |
功能特性 | 老牌产品,成熟度高,文档较多,支持各种协议 | 并发能力强,性能好,延迟低,社区活跃,管理界面丰富 | MQ功能比较完善,扩展性最佳 | 只支持主要的MQ功能,主要应用于大数据领域(如 大数据领域的实时计算、日志采集等场景) | 主要服务于物联网领域(如 车联网、能源电力、充电桩、智能售货机、智能家居、工业互联网等等) |
使用场景:
- Kafka,追求高吞吐量,适合产生大量数据的互联网服务的数据收集业务;
- RocketMQ, 可靠性要求很高的金融互联网领域,稳定性高,经历了多次阿里双11考验;
- RabbitMQ ,性能较好,社区活跃度高,如果数据量没有那么大,优先选择功能比较完备的RabbitMQ。
2. Kafka概述
Kafka官网:http://kafka.apache.org/
中文教程:https://www.orchome.com/kafka/index
Kafka是一个分布式流处理平台,它可以处理消息数据流,由Scala和Java编写。Kafka最初由LinkedIn开发,现在是Apache软件基金会的一部分。Kafka的主要目的是在高性能、高吞吐量的情况下处理消息,是一个可扩展、可靠、容错的消息流平台。Kafka的基本组件包括producer、broker、以及consumer。生产者producer将消息发送到Kafka集群,消息经过broker中转,消费者consumer从broker中读取消息。Kafka适用于处理海量的实时数据,比如日志、事件信息、度量信息等等。
Kafka名词解释:
-
producer:发布消息的对象称之为主题生产者(Kafka topic producer);(客户端)
-
topic:Kafka将消息分门别类,每一类的消息称之为一个主题(Topic);(取代原来rabbitmq的交换机、路由key和队列)
-
consumer:订阅消息并处理发布的消息的对象称之为主题消费者(consumers);(客户端)
-
broker:已发布的消息保存在一组服务器中,称之为Kafka集群。集群中的每一个服务器都是一个代理(Broker)。 消费者可以订阅一个或多个主题(topic),并从Broker拉数据,从而消费这些已发布的消息。(相当于存数据的容器)
3. Kafka安装与配置
基于Docker部署kafka环境。
kafka对于zookeeper是强依赖,保存kafka相关的节点数据,所以安装Kafka之前必须先安装zookeeper(注册中心服务)。
3.1 安装zookeeper
# 下载zookeeper镜像
docker pull zookeeper:3.4.14
# 启动容器
docker run -d --name zookeeper -p 2181:2181 zookeeper:3.4.14
然后使用 docker logs -f zookeeper
查看启动日志,如下图出现2181端口表示zookeeper服务已经成功启动了:
3.2 安装kafka
# 下载镜像
docker pull wurstmeister/kafka:2.12-2.3.1
# 启动容器
docker run -d --name kafka \
--env KAFKA_ADVERTISED_HOST_NAME=192.168.200.130 \
--env KAFKA_ZOOKEEPER_CONNECT=192.168.200.130:2181 \
--env KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://192.168.200.130:9092 \
--env KAFKA_LISTENERS=PLAINTEXT://0.0.0.0:9092 \
--env KAFKA_HEAP_OPTS="-Xmx256M -Xms256M" \
--net=host wurstmeister/kafka:2.12-2.3.1
参数介绍:
- KAFKA_ADVERTISED_HOST_NAME:设置当前主机ip地址;(如果是云主机使用公网ip地址)
- KAFKA_ZOOKEEPER_CONNECT:zookeeper的连接地址;
- KAFKA_ADVERTISED_LISTENERS:kafka发布到zookeeper,供客户端使用的服务地址。
- KAFKA_LISTENERS:允许使用PLAINTEXT侦听器;(kafka对外监听的端口)
- KAFKA_HEAP_OPTS:限制内存的使用,用于性能调优;
- –net=host:使用宿主机的和端口。(如果是云主机的话此处使用
-p 9092:9092
指定端口)
docker run -d --name kafka \
--env KAFKA_ADVERTISED_HOST_NAME=192.168.200.130 \
--env KAFKA_ZOOKEEPER_CONNECT=192.168.200.130:2181 \
--env KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://192.168.200.130:9092 \
--env KAFKA_LISTENERS=PLAINTEXT://0.0.0.0:9092 \
--env KAFKA_HEAP_OPTS="-Xmx256M -Xms256M" \
--p 9092:9092 wurstmeister/kafka:2.12-2.3.1
3.3 容器启动顺序
注:启动容器的时候,要先启动zookeeper,然后再启动kafka。
docker start zookeeper
docker start kafka
或者在执行启动容器命令的时候,拼接--restart=always
参数,每次用完就不用手动启动了。
4. Kafka快速入门
测试案例:
- 生产者发送消息,多个消费者只能有一个消费者接收到消息;
- 生产者发送消息,多个消费者都可以接收到消息。
4.1 使用SpringBoot快速整合
测试:一个生产者和一个消费者场景。
1、创建一个springboot工程
2、导入依赖
<dependencies>
<!-- springboot web启动器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- springboot kafka启动器 -->
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
</dependencies>
3、添加配置到application.yml中:
server:
port: 9991
spring:
application:
name: kafka-demo
# kafka配置
kafka:
bootstrap-servers: 192.168.200.130:9092 # kafka服务器地址
# 生产者配置
producer:
# key和value的序列化方式
key-serializer: org.apache.kafka.common.serialization.StringSerializer #string类型
value-serializer: org.apache.kafka.common.serialization.StringSerializer
# 消费者配置
consumer:
# 消费组的id
group-id: ${spring.application.name}-test
# key和value的反序列化方式
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
4、编写生产者代码
package com.baidou.kafkademo.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
/**
* 生产者controller
*/
@RestController
public class ProducerController {
@Autowired
private KafkaTemplate kafkaTemplate;
/**
* 生产消息的方法
*/
@GetMapping("/sendKeyValue/{key}/{value}")
public String sendKeyValue(@PathVariable("key") String key,@PathVariable("value") String value){
// 发送消息
kafkaTemplate.send("testTopic",key, value);
return "ok";
}
}
5、编写消费者代码
package com.baidou.kafkademo.controller;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Component;
import java.util.Optional;
/**
* kafka主题消费者
*/
@Component
public class ConsumerListener {
/**
* 消费消息的方法
*
* @param consumerRecord<String,String> 消费者记录对象(这里kv是string类型的)
*/
@KafkaListener(topics = "testTopic") //监听主题 testTopic
public void receiveMsg(ConsumerRecord<String, String> consumerRecord) {
if (consumerRecord != null) {
System.out.println(consumerRecord.key() + "==>" + consumerRecord.value());
}
/*
// 使用Java8的Optional对指定的对象进行非空判断(简化上面的if语句)
Optional<ConsumerRecord<String, String>> optional = Optional.ofNullable(consumerRecord);
// 如果对象不为空,则执行ifPresent()方法
optional.ifPresent(x->{
// 输出消息内容
System.out.println(x.key() +"==>" + x.value());
});
*/
}
}
6、启动服务、测试
调用主题生产者接口发送消息:http://localhost:9991/sendKeyValue/phone/13912341234
然后主题消费者会监听testTopic的消息并消费掉:
4.2 消费组测试
模拟一个生产者和多个消费者场景。
测试1:同一个消费组场景下,只能有一个消费者收到消息。
1、修改application.yml中的服务端口配置:
server:
port: ${port:9991}
2、修改JVM运行时参数:(通过配置不同的端口号,实现消费者多实例运行。如果不指定端口默认使用大括号内的缺省值)
消费者01:9991
消费者02:9992
3、向kafka发送消息:http://localhost:9991/sendKeyValue/hello/Java
4、查看消费者监听器的日志信息,发现只有消费者1收到消息。
结论:如果多个消费者都在同一个群组内,并且消费同一个主题(topic)的消息时,只能有一个消费者收到消息。
测试2:不同消费组的场景下,每个组内起码一个消费者能收到消息(1对多,广播效果)
1、修改消费者01的消费组名,并重新启动消费者01的服务
2、修改消费者02的消费组名,并重新启动消费者02的服务
3、向kafka发送消息:http://localhost:9991/sendKeyValue/hello/Java
4、查看控制台打印结果:
结论:在不同消费组的场景下,所有消费者都可以收到消息。
5. Kafka分区和消费相关问题
5.1 分区机制
-
同一个Topic包含不同的Partition(分区)存储在不同机器,一个分区就是一个提交日志。消息以追加的方式写入分区,然后以先进先出的顺序读取。
-
Partition分区的好处是可以并行读和写,保证kafka高吞吐、高性能、高可用(交叉备份,实现数据高可用);
-
主题分区的数量至少为1个。(跟kafka集群数量有关,例如我们当前本地只有一台kafka,所以他集群数量是1,分区数量也是1)
-
每个Partition针对每一个消费组设计了独立的
偏移量(offset)
。
查看并配置主题分区数量:
在kafka容器内/opt/kafka_2.12-2.3.1/config/server.properties中通过配置项num.partitions来指定新建Topic的默认Partition数量,默认数量是1。
1、进入kafka容器:docker exec -it kafka /bin/bash
2、修改分区数量:vi /opt/kafka_2.12-2.3.1/config/server.properties
在命令模式下,输入 ?num.partitions
,查找指定内容位置:
5.2 不同消费组可以重复消费如何实现的
多个消费者在不同消费组内,可以重复消费同一个主题消息的原因?
原因是我们有记录消费者群组消费进度的日志文件,并且分区针对消费组具有独立的偏移量(offset)设计。
offset:任何发布到partition(分区)的消息会被追加到log日志文件尾部( 由partition的leader记录,并由partition的follower同步),每条消息在文件中的位置就被称为offset(偏移量),offset是一个long型数字,从0开始,是按照1自增的,它唯一标记一条消息(类似雪花算法,数字唯一有效)。消费者通过(offset、partition、topic)跟踪记录。可在配置文件server.properties配置log.dirs指定log日志文件目录。
topic_consumer_offsets : 消费者组的位移提交到自带的topic_consumer_offsets里面,当有消费者第一次消费kafka数据时就会自动创建,它的副本数不受集群配置的topic副本数限制,分区数默认50(可以配置),默认压缩策略为compact。 当Consumer重启后,自动读取topic__consumer_offsets中位移数据,从而在上次消费截止的地方继续消费。
5.3 命令行查看kafka信息
5.3.1 查看Kafka的消息log日志文件
1、进入kafka容器:docker exec -it kafka /bin/bash
2、使用vi编辑配置文件,并查看日志文件存储目录:vi /opt/kafka_2.12-2.3.1/config/server.properties
3、查看日志目录文件内容:ls /kafka/kafka-logs-itcast/
_consumer开头的表示消费进度日志文件,原始消息日志目录testTopic-0(格式:主题名-分区编号)。
4、查看原始消息日志文件目录内容:(下面的xxx.log日志文件只能通过kafka命令打开)
5、生产多条消息,然后查看原始消息日志文件:
/opt/kafka_2.12-2.3.1/bin/kafka-run-class.sh kafka.tools.DumpLogSegments --files /kafka/kafka-logs-itcast/testTopic-0/00000000000000000000.log --print-data-log
5.3.2 查看Kafka的消息者偏移量信息
# 注意最后一个值 kafka-demo-test11 是消费者群组名
/opt/kafka_2.12-2.3.1/bin/kafka-consumer-groups.sh --bootstrap-server 192.168.200.130:9092 --describe --group kafka-demo-test11
5.3.3 在Zookeeper中查看Kafka状态信息
zookeeper为kafka提供注册中心的功能。
1、进入zookeeper容器:docker exec –it zookeeper /bin/bash
2、使用ZK客户端命令连接2181端口:/zookeeper-3.4.14/bin/zkCli.sh
3、使用ls命令查看broker(代理)、topic、consumer等状态信息
5.4 kafka能否保证消息有序性
单机有序,集群无序。
**单机环境:一个主题对应一个分区。**单个分区内的消息写入是有序的(先进先出),所以针对单个分区的消费是可以保证消费顺序的。
**集群环境:一个主题对应多个分区。**例如连续进行如下5个操作,通过Kafka生产5条消息,由于Kafka是并行写消息并已追加的方式写入到多个不同分区,所以在整个主题范围内无法保证写的顺序,最后消费端数据消费也是无法保证顺序的。
如何保证集群环境消息有序性:集群环境下,生产者生产消息和消费消费消息都指向同一个分区。
具体代码逻辑如下:(生产和消费都指定同一个分区)
5.5 分区策略
分区策略:决定生产者将消息发送到哪个分区的算法。
分区策略 | 说明 |
---|---|
指定分区策略 | 给定了分区号,直接将数据发送到指定的分区里面去(会影响写的效率,可以保证消息有序性) |
按key分区策略 | 没有给定分区号,给定数据的key值,通过key取上hashCode和机器相关信息分配 |
轮询策略 | 默认分区策略是轮询,既没有给定分区号,也没有给定key值,能够保证消息最大限度的被平均分配到所有分区。(平均分配,使用最多) |
代码如下:
package com.baidou.kafkademo.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
/**
* 生产者controller
*/
@RestController
public class ProducerController {
@Autowired
private KafkaTemplate kafkaTemplate;
/**
* 生产消息的方法(按key分区策略)
*/
@GetMapping("/sendKeyValue/{key}/{value}")
public String sendKeyValue(@PathVariable("key") String key,@PathVariable("value") String value){
// 发送消息
kafkaTemplate.send("testTopic11",key, value);
return "ok";
}
/**
* 生产消息的方法(轮询策略)
*/
@GetMapping("/sendKeyValue/{value}")
public String sendValue(@PathVariable("value") String value){
// 发送消息
kafkaTemplate.send("testTopic11", value);
return "ok";
}
/**
* 生产消息的方法(指定分区策略)
*/
@GetMapping("/sendKeyValue/{value}")
public String sendValueMsg(@PathVariable("value") String value){
// 发送消息
kafkaTemplate.send("testTopic11", 0,value);
return "ok";
}
}
小节:
1、kafka分区设计好处?
- 可以并行读和写,保证kafka高吞吐、高性能、高可用;
2、kafka集群环境下默认分区策略?
- 轮询策略
3、kafka不同消费组可以重复消费原因?
- kafka分区针对不同的消费者群组都有一个专门记录消费者群组进度的日志文件,这个进度日志文件会维护最新消费偏移量。
4、kafka如何保证顺序消费?
- 单机有序,集群无序;
- 集群保证有序:指定生产和消费者使用同一个分区。
6. Kafka高可用设计
6.1 生产端:acks和重试机制
6.1.1 生产端发送消息类型
- 发送并忘记:不关心消息是否正常到达,对返回结果不做任何判断处理(效率高,无法保证消息可靠性)。
- 同步发送:使用send()方法发送,它会返回一个Future对象,调用get()方法进行等待kafka响应,就可以知道消息是否发送成功。
- 异步发送:调用send()方法,并指定一个回调函数,服务器在返回响应时调用函数。没有阻塞等待效果,效率更高。(使用最多)
6.1.2 生产端acks(Acknowledgments)确认应答机制
在kafka生产者配置中指定:
确认机制 | 说明 |
---|---|
acks=0 | 生产者不需要等待服务器任务响应,不保证数据可靠性(可能丢失数据),性能吞吐最高 |
acks=1(默认值) | 当leader节点持久化消息后,生产者才会得到服务器响应,可靠性和性能吞吐适中(使用最多) |
acls=all | 所有ISR(In-Sync Replicas)节点持久完消息后,生产者才会得到服务器响应,数据可靠性最高,性能吞吐会降低 |
6.1.3 生产者重试机制
生产者从服务器收到的错误有可能是临时性错误,在这种情况下,retries参数的值决定了生产者可以重发消息的次数,如果达到这个次数,生产者会放弃重试返回错误,默认情况下,生产者会在每次重试之间等待100ms。
6.2 服务端:主从备份机制和顺序写
6.2.1 主从备份机制(Replication)
为什么需要主从?主从可以备份数据,保证数据高可用。
一主多从。leader对外提供读写,follower负责同步leader数据和参与选举。
kafka 为了提高 partition 的可靠性而提供了副本的概念(Replica),通过副本机制来实现冗余备份。每个分区可以有多个副本,并且在副本集合中会存在一个leader 的副本,所有的读写请求都是由 leader 副本来进行处理。
Kafka 定义了两类副本:
- 领导者副本(Leader Replica)
- 追随者副本(Follower Replica)
6.2.2 备份机制(Replication)-ISR 主从同步备份机制
领导者宕机了,优先从ISR中选择新的主节点。
-
我们可以认为,副本集会存在一主多从的关系。一般情况下,同一个分区的多个副本会被均匀分配到集群中的不同 broker 上,当 leader 副本所在的 broker 出现故障后,可以重新选举新的 leader 副本继续对外提供服务。通过这样的副本机制来提高 kafka 集群的可用性。
-
所有与leader保持一定程度同步的副本(包括Leader)组成ISR(In-Sync Replicas)
-
如果leader失效后,需要选出新的leader,选举的原则如下:
- 第一:选举时优先从ISR中选定,因为这个列表中follower的数据是与leader同步的
- 第二:如果ISR列表中的follower都不行了,就只能从其他follower中选取(极端情况)
6.2.3 顺序写磁盘
顺序写相对于随机写性能提高百倍以上,是kafka高可用的重要保障。
当broker接收到producer发送过来的消息时,需要根据消息的主题和分区信息,将该消息写入到该分区当前最后的segment文件中,文件的写入方式是追加写。
由于是对segment文件追加写,故实现了对磁盘文件的顺序写,避免磁盘随机写时的磁盘寻道的开销,同时由于是追加写,故写入速度与磁盘文件大小无关,具体如图下所示。
6.3 消费端:如何解决重复消息问题
6.3.1 消费者重复消费产生原因
1、生产者如果网络抖动相关问题,可能重复发送消息。
2、消费者宕机、重启或者被强行kill进程,导致消费者消费的offset没有提交。
3、kafka消费端会每隔5秒自动提交消费偏移量(auto.commit.interval.ms)如果网络问题没来及提交,其他消费者会重复消费消息。
4、kafka消费者会每隔10秒向服务端发送心跳(session.timeout.ms)表明还活着,否则服务端认为消费者离组会触发重平衡,重平衡rebalance也会造成消息重复消费。
6.3.2 重平衡Rebalance造成重复消费
消费者收到消息没有提交偏移量造成重复消费问题。
kafka不会像其他JMS队列那样需要得到消费者的确认,消费者可以使用kafka来追踪消息在分区的位置(偏移量)
消费者会往一个叫做_consumer_offset
的特殊主题发送消息,消息里包含了每个分区的偏移量。如果消费者发生崩溃或有新的消费者加入群组,就会触发重平衡。
6.3.3 消费者消息重复如何解决
1、kafak可以手工处理偏移量(不推荐)
- 当
enable.auto.commit
被设置为true,提交方式就是让消费者自动提交偏移量,每隔5秒消费者会自动把从poll()方法接收的最大偏移量提交上去。
2、从业务角度处理(实环情况不管采用任何消息中间件,重复消费都避免不了)
-
**生产端:**发送的消息添加唯一id(雪花算法,或者纳秒时间戳) 【注意默认不使用messageID,需要显式配置】
消费端:
- 方案1 利用去重表去重判断。
- 方案2 利用redis去重判断(redis分布式锁 setnx)。(推荐使用)
http://www.redis.cn/commands/setnx.html
6.4 优化:Kafka吞吐优化设计
如何保证生产端生产数据速度足够快?①增加分区数量、②异步发送消息、③消息压缩支持、④批量发送。
默认情况下, 消息发送时不会被压缩。
生产者开启消息压缩配置如下:
压缩算法 | 说明 |
---|---|
snappy | 占用较少的 CPU, 却能提供较好的性能和相当可观的压缩比, 如果看重性能和网络带宽,建议采用/ |
lz4 | 占用较少的 CPU, 压缩和解压缩速度较快,压缩比也很客观。 |
gzip | 占用较多的 CPU,但会提供更高的压缩比,网络带宽有限,可以使用这种算法。 |
使用压缩可以降低网络传输开销和存储开销,而这往往是向 Kafka 发送消息的瓶颈所在。
批量发送
批量发送可以提高Kafka的吞吐。
参数 | 说明 |
---|---|
batch-size | Kafka的生产者发送数据到服务器,不是来一条就发一条,而是经过缓冲的,默认批量发送数据依次16K。 |
buffer-memory | 缓存池大小,默认32M。 |
7. Kafka消息删除机制
-
对于传统的message queue而言,一般会删除已经被消费的消息,而Kafka集群会保留所有的消息,无论其被消费与否。当然,因为磁盘限制,不可能永久保留所有数据(实际上也没必要)。
-
两种策略删除旧数据。一是基于时间,二是基于Partition文件大小。
-
例如可以通过配置$KAFKA_HOME/config/server.properties,让Kafka删除一周前的数据,也可在Partition文件超过1GB时删除旧数据。比如如下默认删除策略: