一、简介
1、 是什么
是一个分布式、支持分区的(partition)、多副本的(replication),基于 zookeeper 协调的分布式消息系统,可以实时的处理大量数据.
2. 有哪些主流的消息队列 (中间件)
- RabbitMQ: 由 Erlang(二郎) 语言编写。吞吐量比较低,不容易进一步开发扩展。
- RocketMQ: 由 java 编写,阿里开发,社区活跃度低,万一不维护,需要自己公司研发。
- Redis: 用作消息队列时,数据量大小在 10k 以内速度快,数据量大时会非常慢
- Kafka:Apache 开发,由 Scala 和 java 编写,适合大数据实时计算以及日志收集
3. 为什么要使用消息队列
主要用来缓冲任务和削峰:上游数据有时会突发流量,下游可能扛不住,或者下游没有足够多的机器来保证处理,此时 kafka 在中间可以起到一个缓冲作用,把消息暂存在 kafka 集群中,下游服务就可以按照自己的节奏慢慢处理任务。
4. 消息队列的应用场景
用户管理中,当成功写入数据库后的回调信息中要作两件事:
- 发送注册邮件
- 发送注册短信
如果使用消息队列,则在收到异步通知后就直接响应用户,把后两件事放在队列中慢慢处理。
串行处理任务
并行处理任务(多线程问题)
kafka 消息队列处理
5. kafka 的分布式实现
NameServer 在 kafka 中使用的是 zookeeper
6.Kafka 特性
解耦、高吞吐量、低延迟、高并发、容错性、可扩展性、持久性和可靠性
- 解耦:使用 kafka 后,任务的处理者 (consumer) 与任务的发布者 (producer) 之间没有依赖关系。
- 高吞吐量、低延迟:kafka 每秒可处理几十万条消息,延迟可低到几毫秒。每个 topic 可以分多个 partition,consumer group 对分区可并行读取
- 可扩展性:kafka 集群支持热扩展
- 持久性和可靠性:
消息被持久化到本地磁盘, 并支持数据备份防止数据丢失。 - 容错性:允许集群中节点失败,只要还剩下一个就能正常工作
- 高并发:支持数千个客户端的读写
7. 消息的分类
- 点对点:一个队列可以有多个消费者一起消费,但一个消息只能被一个消费者消费。
- 发布与订阅
消息被持久化到一个 topic 中,消费者可消费该 topic 中所有数据,同一条数据可被多个消费者消费,数据被消费后不会删除。
二、kafka 整体架构
1.kafka 由哪些组件构成
topic:
- 是什么?就是一堆消息,由多个分区构成。
- 分区 partition, 为什么要分区:
- 从 producer 角度看,分区分布在不同 broker 上,方便容量扩展,同时也提高吞吐量和负载均衡
- 从 consumer 角度看,一个组内的某个消费者只能消费一个分区,分区后可以提高并发量,效率大提高。但要求组内消费者数量不能大于 topic 的分区数
- 副本 replication
副本是什么?
leader 副本:用来提供读写功能,一个分区只有一个 leader 副本
follower 副本:只是被动地备份leader 中数据,不提供读写功能,作用是为了提高分区的可用性
producer:
向 topic 中发布消息
consumer:
订阅 topic 中消息,并从 topic 中读取和处理消息
broker:
管理 topic 中消息存储的服务器
2 . ISR 和 AR 是什么,ISR 的伸缩又指什么?
- ISR:In-sync Replicas(副本同步队列),一个分区中,包含了 leader 和所有与 leader 保持同步的 follower 的 id, 该队列由控制器和 leader 维护,如果 follower 从 leader 中同步时间超过阈值,就会被从 ISR 中踢出,并把该 follower 的 id 存入 OSR(Outof-sync replicas) 列表中
- AR 是什么?
AR=ISR+OSR - ISR 伸缩:follower 副本跟 leader 同步超时会 ISR 中移除,当 follower 同步了所有 leader 数据后又加入 ISR 中
3. 控制器是什么?如何选举的
就是一个 Broker, 集群中第一个启动的 broker 会通过在 zookeeper 中创建临时节点 / controller 来试图让自己成为控制器,其他 broker 会在该节点消失时收到通知,然后分别再向 zk 中写 / controller 节点,只有一个能成功,其他节点只能监听该节点
作用:监听 ids 变化从而实现下面两个功能:
- topic 的分区副本分配:一个 topic 为了实现负载均衡,会被分成多个分区,这些分区信息及与 broker 的对应关系由 controller 维护
- 分区的 leader 选举
4. 分区 leader 选举?
某个作为 leader 的 broker 挂了,则 controller 会把其从 ISR 中移除,再从 ISR 列表中找跟当前 leader 保持最高同步的副本作为 leader, 如果都保持了完全同步,则按顺序从前向后选。
三、环境搭建
1.kafka 安装
- 网址下载软件:http://kafka.apache.org/downloads.html
- 解压:tar -zxvf kafka/kafka.tar.gz -C /opt/module/
- 在 kafka 目录中创建目录:mkdir logs,用来存放日志和消息数据
- 编辑配置文件:vim config/server.properties
- broker.id=0
- delete.topic.enable=true 删除注释,以便可以删除 topic
- log.dirs=/opt/module/kafka/logs
- zookeeper.connect=192.168.184.100:2181
- listeners = PLAINTEXT://192.168.184.100:9092
host.name=192.168.184.100, 这里的 ip 是当前虚拟机的 ip
- 克隆两台虚拟机并修改 ip 为 101 和 102
https://blog.csdn.net/wlxs32/article/details/105232268 注意要删除每台虚拟机 kafka 内部的 logs 目录
2. 启动
启动 zookeeper
/opt/module/zookeeper-3.4.10/bin/zkServer.sh start
启动与关闭 kafka 服务
在每台虚拟机上都执行下面命令
- 前台运行:/opt/module/kafka/bin/kafka-server-start.sh /opt/module/kafka/config/server.properties
- 后台运行:/opt/module/kafka/bin/kafka-server-start.sh -daemon /opt/module/kafka/config/server.properties
- 通过 jps 命令查看 java 进程,如果看到 kafka 表示启动成功
- 关闭服务:
- netstat -alnp | grep 9092,kill -9 进程号
- bin/kafka-server-stop.sh,如果权限不足,只可以 chmod 777 kafka-server-stop.sh
四、shell 命令使用
关闭:/opt/module/kafka/bin/kafka-server-stop.sh
启动:/opt/module/kafka/bin/kafka-server-start.sh -daemon /opt/module/kafka/config/server.properties
1. 创建 topic 命令
/opt/module/kafka/bin/kafka-topics.sh —create —zookeeper 192.168.184.100:2181 —partitions 2 —replication-factor 2 —topic first
会在 logs 目录中创建 topic,分别为 first-0 和 first-1
2. 查看集群中有哪些 topic
/opt/module/kafka/bin/kafka-topics.sh —list —zookeeper 192.168.184.100:2181
3. 查看指定 topic 的详细信息
/opt/module/kafka/bin/kafka-topics.sh —describe —zookeeper 192.168.184.100:2181 —topic first
- 没有 zookeeper 可以使用 kafka 吗?zk 有什么用
- 管理集群 broker 的上下线,如 broker 上线,会把 brokerId 写到 zk 的 / brokers/ids 节点下,下线就从 zk 中删除
- controller 选举:kafka 中某个 broker 会被 zk 选举为 controller
- controller 有什么用?
监听 ids 变化从而实现下面两个功能:- topic 的分区副本分配:一个 topic 为了实现负载均衡,会被分成多个分区,这些分区信息及与 broker 的对应关系由 controller 维护
- 分区的 leader 选举
- controller 有什么用?
4. 删除 topic
/opt/module/kafka/bin/kafka-topics.sh —delete —zookeeper 192.168.184.100:2181 —topic first
注意:必须所有分区所在的主机 kafka 服务处于开启状态才能正常完全删除。
5. 修改一个 topic 的分区数
bin/kafka-topics.sh —zookeeper 192.168.184.100:2181 —alter —partitions 3 —topic first
注意:分区数只能增加不能减少
6. producer 客户端连接
bin/kafka-console-producer.sh —broker-list 192.168.184.101:9092 —topic first
7.consumer 客户端连接
- 旧版本用法
bin/kafka-console-consumer.sh —zookeeper 192.168.184.100:2181 —from-beginning —topic first
问题:offset 的作用?
每个消息都有一个顺序 ID 号,叫偏移量 offset, 用来唯一地识别分区中的每条消息
- 新版本用法
bin/kafka-console-consumer.sh —bootstrap-server 192.168.184.102:9092 —from-beginning —topic first
offset 的值不再保存到 zk 中,而是保存在一个叫作**__consumer_offsets 的 topic 中**,可通过命令 bin/kafka-topics.sh —list —zookeeper 192.168.184.100:2181 查看
五、数据流程
1. 生产数据流程
- producer 从 broker-list 中获取某一 partition 的 leader
如 zookeeper 上获取 0 号分区的副本信息:get /brokers/topics/first/partitions/0/state - producer 把消息发送给某一个分区的 leader
- leader 将消息写入本地 logs 目录中
- 该分区的 follwer 从 leader 中 pull 消息,并写入 follower 本地 logs,然后再向 leader 发送 ack(acknowledge character)
- leader 收到所有 follower 的 ack 后再向 producer 发送 ack
2. 消费数据流程
- 采用 pull 模式消费消息,由消费者自己记录消费状态 (自已把 offset 写入 zk 或 kafka 中),每个消费者互相独立的顺序读取每个分区的消息。如果 broker 没数据,则会有一个超时等待时间,过了这段时间再返回。
- 分区消费
组内某个消费者只能同时消费一个分区,如果分区数大于组内消费者,则默认采用轮询方式依次消费数据。
3. 数据持久化
存储方式:
topic 存储在一个或多个 partition 中,每个分区对应一个文件夹,用来放消息和索引文件。
数据删除策略
无论消息是否被消费,kafka 都会保留所有消息,有两种策略可以删除旧数据
- 基于时间:log.retention.hours=168, 默认采用这种方式(7 天)
- 基于大小:log.retention.bytes=1073741824,1G, 如果分区大小超过 1G 就会在适当时机把一最老的 segment 数据段删除
数据的可靠性如何保证?
ack 为 all 的前提下采用幂等机制
通过控制丢失率、重复率和副本数据的一致性来实现。
- ack 的不同级别(3 个)保证控制数据的丢失率和重复率
- 0:生产者将数据发送出去后就不管了,不去等待任何回应,好处是数据不会重复,效率高,但不能保证数据一定能成功写入
- 1: 数据发送到 leader,并成功写入 leader 的 logs,但不会等待成功写入 follower 就会响应acks. 数据可能重复
如果 leader 在响应后,同步到 follower 前 leader 挂了,则会丢失数据。 - all: 会等待 leader 和所有副本成功写入,才响应acks, 否则就重发。
好处是数据绝对不丢失,缺点是会重复。- 为什么会重复?
如果数据已写入全部副本,但在响应 producer 前,leader 挂了,那么 producer 就认为写入失败,就要重写。 - 如何在 kafka 内去除重复?
- 用 acks=all 和幂等机制,也就是将 enable.indempotence 设置为 true 就能实现。
- 实现原理:kafka 会额外为 producer 分配一唯一的 id, 叫作 pid,producer 在发送消息时,生成消息的 id(pid + 消息的序列号),leader 在接收消息时会把每条消息的 id 存入缓存中,并对新接收的消息判断是否是重复的 id, 如果重复就丢弃。
- 为什么会重复?
- 如何保证副本数据的一致性
通过 LEO 和 HW 保证副本数据的一致性。- LEO:标识当前分区中下一条待写入消息的 offset
- HW: 高水位,实际上就是 ISR 中所有 broker 的 LEO 最小值,消费者只能获取 HW 这个 offset 之前的消息。
hw 有什么用?- follower 挂掉后,被移出 isr, 恢复后获取挂掉前的 hw, 把副本中所有大于等于这个 hw 的数据清除,从 leader 同步数据。
- 为什么要清除 hw 之后数据?因为 follower 恢复过程中,leader 可能挂掉,并重新选举了新 leader,而这个 leader 的 LEO 可能比较低,且随后又接收到一些新数据,该 follower 数据如果不删除一部分,就无法确定从哪个位置同步数据。
- leader 挂掉,会从 ISR 中选出一个新的 leader 之后,为保证多个副本之间的数据一致性,其余(不包括 leader)的 follower 会先将各自的 log 文件高于 HW 的部分截掉,然后从新的 leader 同步数据。
- follower 挂掉后,被移出 isr, 恢复后获取挂掉前的 hw, 把副本中所有大于等于这个 hw 的数据清除,从 leader 同步数据。
4. 写数据的高效性?
效率很高,因为采用顺序写,也就是在一个连续空间写数据,所以比写内存效率要高
5. 消费者获取消息的高效性如何保证
- 分区:在消费者组中有多个消费者并发读取不同的分区数据
- 分段:将数据文件分段,比如 100 条消息,它们的 offset 是从 0 到 99,假设将数据文件分成 5 段,第一段为 0-19 号消息,其文件名为 0.log,第二段 20-39,其文件名 20.log, 以此类推。
- 稀疏索引
为每个小文件中的部分消息建立索引文件,名字跟分段文件名相同,后缀为. index,其内容有两部分:
(1) 部分消息的相对 offset
(2) 部分消息的 poistion: 是相当于文件首地址的物理偏移量。
6.producer 发送消息到哪个分区?分区选择的原则是什么?
- 指定分区器:发送到分区器指定的分区中,p.put(“partitioner.class”,”xxx”)
- 没指定分区器,但指定了 key, 就会根据 key 的 hash 值跟分区数取模得到结果,就是要发送的分区 new ProducerRecord<String, String>(“first”,”abc”,Hello” + i), 这里 abc 就是 key
- key 和分区器都没指定,则默认采用轮询(旧版本)决定发送到哪个分区。
- 配置文件
<dependencies>
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka_2.12</artifactId>
<version>0.11.0.3</version>
</dependency>
</dependencies>
#cfg.properties
bootstrap.servers=192.168.184.100:9092
#一批数据的大小
batch.size=16384
#accumulator缓冲区大小
buffer.memory=33554432
#提交延时,当达到指定时间,即便是accumulator缓存不满也会提交数据
linger.ms=1
#生产者需要用序列化器(Serializer)将key和value序列化成字节数组才可以将消息传入Kafka。
#消费者需要用反序列化器(Deserializer)把从Kafka中收到的字节数组转化成相应的对象
#指定kv的序列化器的类型,值为其class路径。
key.serializer=org.apache.kafka.common.serialization.StringSerializer
value.serializer=org.apache.kafka.common.serialization.StringSerializer
partitioner.class=hy.MyPartition
retries=0
7. 生产者拦截器
用户在消息发送前以及 producer 回调逻辑前有机会对消息做一些定制化需求
如何实现?
- 定义类并实现接口:ProducerInterceptor
public class MyInterCeptor implements ProducerInterceptor<String,String>{
long sucNum;
long errNum;
//producer发送前会先调用该方法
public ProducerRecord<String, String> onSend(ProducerRecord<String, String> r) {
return new ProducerRecord<String,String>(r.topic(),r.partition(),r.timestamp(),r.key(),System.currentTimeMillis()+r.value(),r.headers());
}
//在kafka响应ack时会调用
public void onAcknowledgement(RecordMetadata recordMetadata, Exception e) {
if(e==null){
sucNum++;
}else{
errNum++;
}
}
//kfka的Producer关闭时会调用
public void close() {
System.out.println("本次共成功发送:"+sucNum+",失败:"+errNum);
}
//获取配置信息时调用
public void configure(Map<String, ?> map) {
System.out.println(map);
}
}
- 添加拦截器到配置文件中。
p.put("interceptor.classes","hzn.MyInterCeptor");
8. 消费者
public class MyConsumer {
public static void main(String[] args) {
Properties p=new Properties();
p.put("bootstrap.servers","192.168.184.100:9092");
p.put("group.id","gr_01");
p.put("enable.auto.commit","false");//关闭自动提交
p.put("key.deserializer", StringDeserializer.class.getName());
p.put("value.deserializer", StringDeserializer.class.getName());
p.put("auto.offset.reset","earliest");//从头开始消费
KafkaConsumer<String,String> consumer=new KafkaConsumer<String, String>(p);
while(true){
consumer.subscribe(Arrays.asList("first"));
ConsumerRecords<String,String> cr=consumer.poll(100);
for(ConsumerRecord<String,String> r:cr){
System.out.println("主题:"+r.topic()+",所属分区:"+r.partition()+",内容:"+r.value());
}
//手动提交
consumer.commitAsync();
}
/*
*异步:commitAsync,提交后就结束,不阻塞
*同步:commitSync,提交后会等待kafka确认,在收到确认前会阻塞
*/
}
}
9. 分区选择
public class MyPartition implements Partitioner{
Random random=new Random();
int p=-1;
@Override
public int partition(String s, Object o, byte[] bytes, Object o1, byte[] bytes1, Cluster cluster) {
System.out.println("s="+s);
int part=random.nextInt(2);
//return part;
p++;
if(p==2){
p=0;
}
return p;
}
@Override
public void close() {
}
@Override
public void configure(Map<String, ?> map) {
}
}
10. 生产者
public class MyProducer {
public static void main(String[] args) throws Exception{
Properties p = new Properties();
//p.put("bootstrap.servers","192.168.184.100:9092");
p.put("acks","all");
p.put("enable.indempotence",true);//幂等机制
p.load(MyProducer.class.getClassLoader().getResourceAsStream("cfg.properties"));
Producer<String, String> producer = new KafkaProducer<String, String>(p);
for (int i = 0; i < 10; i++) {
//producer.send(new ProducerRecord<String, String>("first","hello_"+i));
//producer.send(new ProducerRecord<String, String>("first","abc","hello_"+i));
producer.send(new ProducerRecord<String, String>("first", "abc", "hello_" + i), (m, e) -> {
//收到ack时调用,发送失败且不再重发也会调用该方法
if(e==null){
//发送成功
System.out.println("分区:"+m.partition()+",offset:"+m.offset());
}else {
e.printStackTrace();
System.out.println("发送失败");
}
}).get();//注意:调用get的后果是让发送方法阻塞;当收到ack并回调就解除阻塞
}
producer.close();
}
}
六、面试题
1.kafka 是怎么体现消息顺序性的?
在一个分区内能过 offset 来维护顺序性,不同分区无法保证。
2.kafka 新建的分区会在哪个目录中创建?
在配置文件指定的 logs 目录下创建: first-0,first-1
3.kafka 中的分区器、序列化器、拦截器是否了解?之间的顺序是什么?
- kafka 在发送消息前先经过拦截器处理
- 序列化器转换为字节数组
- 分区器指定分区
4. 什么情况会造成重复消费?什么情况会造成漏消费?
- 重复消费?
- 生产者:ack=all时生产者会重复发消息会重复消费,可通过幂等机制解决
- 消费者:消费者采用先消费后提交 offset的方式,如果在消费结束后提交 offset 时,失败了,则会重复消费同一条记录。可通过同步提交 offset 的方式解决。
- 漏消费?
- 生产者:ack=0 或 1 时,kafka 在生产者发送了数据后可能没存储成功,从而漏消费。
- 消费者:消费者采用先提交 offset, 后消费,可能会漏消费。可先消费后提交解决
5.kafka 内部有 topic 吗?有什么用?
__consumer_offsets, 用来记录所有 topic 的所有分区被消费者消费的 offset
6. 如何保证消息只能被消费一次?
- producer 端
- ack=all
- p.put(“enable.indempotence”,true);//幂等机制
- consumer 端
- 关闭自动提交功能
- 每消费一次数据后手动提交