目录
一、Kafka概述
一般定义:Kafka是一种分布式的基于发布/订阅模式的消息队列,主要应用于大数据实时处理等领域。
什么是发布/订阅模式?什么是消息队列?
1.消息队列
传统的消息队列的主要应用场景包括:缓存/消峰、解耦和异步通信。Kafka当然也都具备。
- 缓存/消峰:使用消息队列作为中间件可以将流量的高峰保存在消息队列中,有助于控制和优化数据流经过系统的速度,解决生产者、消费者处理速度不一致情况。
- 解耦:允许独立的扩展或修改两边的处理过程,只要确保它们遵守同样的接口约束,防止某一个接口出现故障影响当前功能。
- 异步通信:区别于同步要求流程全部走完再返回结果,异步允许用户把一个消息放入队列,即可返回结果,但并不立即处理它,然后让非必要环节的其他业务处理接口从消息队列中拉取消费处理即可(在需要的时候再去处理它们)
消费队列共有2种消费模式(Kafka主要应用发布/订阅模式):
2.Kafka基本框架
- Producer:消息生产者,就是向 Kafka broker 发消息的客户端。
- Consumer:消息消费者,向 Kafka broker 取消息的客户端。
- Consumer Group(CG):消费者组,由多个 consumer 组成。消费者组内每个消 费者负责消费不同分区的数据,一个分区只能由一个组内消费者消费;消费者组之间互不 影响。所有的消费者都属于某个消费者组,即消费者组是逻辑上的一个订阅者。
- Broker:一台 Kafka 服务器就是一个 broker。一个集群由多个 broker 组成。一个 broker 可以容纳多个 topic。
- Topic:可以理解为一个队列,生产者和消费者面向的都是一个 topic。
- Partition:为了实现扩展性,一个非常大的 topic 可以分布到多个 broker(即服 务器)上,一个 topic 可以分为多个 partition,每个 partition 是一个有序的队列。
- Replica:副本。一个 topic 的每个分区都有若干个副本,一个 Leader 和若干个 Follower。
- Leader:每个分区多个副本的“主”,生产者发送数据的对象,以及消费者消费数 据的对象都是 Leader。
- Follower:每个分区多个副本中的“从”,实时从 Leader 中同步数据,保持和 Leader 数据的同步。Leader 发生故障时,某个 Follower 会成为新的 Leader。
3.命令行基本操作
Kafka安装目录下对应文件
- 主题——bin/kafka-topics.sh
- 生产者——bin/kafka-console-producer.sh
- 消费者——bin/kafka-console-consumer.sh
常用命令如下
参数 | 描述 |
--bootstrap-server | 连接的 Kafka Broker 主机名称和端口号 |
--topic | 操作的 topic 名称 |
--create | 创建主题 |
--delete | 删除主题 |
--alter | 修改主题 |
--list | 查看所有主题 |
--describe | 查看主题详细描述 |
--partitions | 设置分区数 |
--replication-factor | 设置分区副本 |
--config | 更新系统默认的配置 |
创建topic:
bin/kafka-topics.sh --bootstrap-server hadoop102:9092 --create --partitions 1 --replication-factor 3
注:分区数只能增多不能减少,因为已经被消费的分区无法相互合并
下例为生产者发送数据,消费者接受数据。(--from-beginning用来接收历史所有)
二、生产者
1.消息发送原理
外部数据通过生产者发到Kafka集群,在这个过程中,创建了一个Producer,调用send()方法,经过拦截器,序列化器、分区器发送到缓存队列,最后通过Sender线程将数据发送到对应的Kafka集群。发送到Kafka集群后,会对数据进行备份。如下图。Kafka的Producer发送消息采用的是异步发送的方式,在消息发送的过程中,涉及到了两个线程main线程和Sender线程,以及一个线程共享变量RecordAccumulator,main线程将消息发送给RecordAccumulator,Sender线程不断从RecordAccumulator中拉取消息发送到Kafka broker中。
2.不同发送模式及代码实现
- 普通异步发送
- 带回调函数的异步发送:回调函数会在 producer 收到 ack 时调用,为异步调用,该方法有两个参数,分别是元 数据信息(RecordMetadata)和异常信息(Exception),如果 Exception 为 null,说明消息发送成功,如果 Exception 不为 null,说明消息发送失败。
- 同步发送
代码实现:
(1)导入依赖
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>3.0.0</version>
</dependency>
(2)3种发送模式实现
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 org.apache.kafka.common.serialization.StringSerializer;
import java.util.Properties;
public class CustomProducer {
public static void main(String[] args) {
//0.配置基础属性
Properties properties = new Properties();
//连接集群 bootstrap.servers
properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"master:9092,slave1:9092");
//指定对应的key,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", "good morning" + i));
//异步带回调
/*
kafkaProducer.send(new ProducerRecord<>("first", "good morning" + i), new Callback() {
@Override
//recordMetadata 返回的元数据信息;e 异常信息
public void onCompletion(RecordMetadata recordMetadata, Exception e) {
if(e == null){
System.out.println("主题:" + recordMetadata.topic() + "分区:" + recordMetadata.partition());
}
}
});
*/
//同步
kafkaProducer.send(new ProducerRecord<>("first", "good morning" + i)).get();
}
//3.关闭资源
kafkaProducer.close();
}
}
(3)在虚拟机种通过消费者看是否接收到数据
3.分区策略
1.分区的原因
- 便于合理使用存储资源:每个Partition在一个Broker上存储,可以把海量的数据按照分区切割成一 块一块数据存储在多台Broker上。合理控制分区的任务,可以实现负载均衡的效果。
- 提高并行度:生产者可以以分区为单位发送数据;消费者可以以分区为单位进行消费数据。
2.分区的原则
- 指明partition(这里的指明是指第几个分区)的情况下,直接将指明的值作为partition的值。例如partition=0,所有数据写入 分区0
- 没有指明partition的情况下,但是存在值key,此时将key的hash值与topic的partition总数进行取余得到partition值。例如:key1的hash值=5, key2的hash值=6 ,topic的partition数=2,那 么key1 对应的value1写入1号分区,key2对应的value2写入0号分区。
- 值与partition均无的情况下,第一次调用时随机生成一个整数,后面每次调用在这个整数上自增,将这个值与topic可用的partition总数取余得到partition值,即round-robin算法。例如:第一次随机选择0号分区,等0号分区当前批次满了(默认16k)或者linger.ms设置的时间到, Kafka再随机一个分区进 行使用(如果还是0会继续随机)。
3.自定义分区(用于过滤等)
需求:若输入value包含"hello",则放入1号分区,否则放入0号分区
(1)定义类实现 Partitioner 接口。(2)重写 partition()方法。
public class MyPartitioner implements Partitioner {
@Override
public int partition(String s, Object o, byte[] bytes, Object o1, byte[] bytes1, Cluster cluster) {
String input = o1.toString();
int partition;
if(input.contains("hello")){
partition = 1;
}
else partition = 0;
return partition;
}
@Override
public void close() {
}
@Override
public void configure(Map<String, ?> map) {
}
}
(3)使用分区器的方法,在生产者的配置中添加分区器参数。
properties.put(ProducerConfig.PARTITIONER_CLASS_CONFIG,"com.atguigu.kafka.producer.MyPartitioner");
测试:
输出结果:
4.其他配置参数
1.数据吞吐量
主要调整batch.size批次大小 和linger.ms 等待时间(延迟时间,默认为0,即来一条数据,sender就发送数据给broker),两者不可同时兼容,若要提高吞吐量,应该提升批次大小,但会有相应的延迟时间。
也可调整compression.type压缩方式,同样16K的数据,压缩过后一批次能传递更多的数据。
// batch.size:批次大小,默认 16K
properties.put(ProducerConfig.BATCH_SIZE_CONFIG, 16384);
// linger.ms:等待时间,默认 0
properties.put(ProducerConfig.LINGER_MS_CONFIG, 1);
// RecordAccumulator:缓冲区大小,默认 32M:buffer.memory
properties.put(ProducerConfig.BUFFER_MEMORY_CONFIG, 33554432);
// compression.type:压缩,默认 none,可配置值 gzip、snappy、lz4 和 zstd
properties.put(ProducerConfig.COMPRESSION_TYPE_CONFIG,"snappy");
2.数据可靠性
- acks=0,生产者发送过来数据就不管了,Leader一旦崩掉了,也没有办法。可靠性差,效率高;
- acks=1,生产者发送过来数据Leader应答,如果应当完,Leader还没同步给Follower副本就挂了,新的Leader就没有办法收到原数据。可靠性中等,效率中等;
- acks=-1 or 'all',生产者发送过来数据Leader和ISR队列里面所有Follwer应答,可靠性高,效率低;(为了解决“Leader收到数据,所有Follower都开始同步数据,但有一 个Follower,因为某种故障,迟迟不能与Leader进行同步”的问题,in-sync replica set(ISR),意为和 Leader保持同步的Follower+Leader集合,比如broker0,1,2(leader:0,isr:0,1,2),如果Follower长时间未向Leader发送通信请求或同步数据,则该Follower将被踢出ISR。默认30s。例如2超时,(leader:0, isr:0,1)。)
如果分区副本设置为1个,或者ISR里应答的最小副本数量设置为1,和ack=1的效果是一 样的,仍然有丢数的风险(leader:0,isr:0)。
数据完全可靠条件 = ACK级别设置为-1 + 分区副本大于等于2 + ISR里应答的最小副本数量大于等于2
在生产环境中,acks=0很少使用;acks=1,一般用于传输普通日志,允许丢个别数据;acks=-1,一般用于传输和钱相关的数据, 对可靠性要求比较高的场景。
// 设置 acks
properties.put(ProducerConfig.ACKS_CONFIG, "all");
// 重试次数 retries,默认是 int 最大值,2147483647
properties.put(ProducerConfig.RETRIES_CONFIG, 3);
3.数据去重
(1)数据传递:
- 至少一次(At Least Once)= ACK级别设置为-1 + 分区副本大于等于2 + ISR里应答的最小副本数量大于等于2
- 最多一次(At Most Once)= ACK级别设置为0
At Least Once可以保证数据不丢失,但是不能保证数据不重复; At Most Once可以保证数据不重复,但是不能保证数据不丢失
但如果是金额这类既要满足数据不丢失,又要数据不重复的数据(精确一次),Kafka引入幂等性和事务。
(2)幂等性
幂等性就是指Producer不论向Broker发送多少次重复数据,Broker端都只会持久化一条,保证了不重复。
精确一次(Exactly Once) = 幂等性 + 至少一次( ack=-1 + 分区副本数>=2 + ISR最小副本数量>=2) 。
重复数据的判断标准:<PID,Partition,SeqNumber>相同主键的消息提交时,Broker只会持久化一条。其中PID是Kafka每次重启都会分配一个新的生产者ID;Partition 表示分区号;SeqNumber是单调自增的。
所以幂等性只能保证在单分区单会话内不重复(重新启动一次Kafka就是新的会话)。
若要使用,开启参数 enable.idempotence 默认为 true,false 关闭。
(3)生产者事务
kafka
从0.11
版本开始引入了事务支持,事务可以保证Kafka
在Exactly Once
语义的基础上,生产和消费可以跨分区的会话,要么全部成功,要么全部失败。
开启事务,必须开启幂等性。
4.数据乱序
1)kafka在1.x版本之前保证数据单分区有序,条件如下: max.in.flight.requests.per.connection=1(不需要考虑是否开启幂等性)。
2)kafka在1.x及以后版本保证数据单分区有序,条件如下:
(1)未开启幂等性 max.in.flight.requests.per.connection需要设置为1。
(2)开启幂等性 max.in.flight.requests.per.connection需要设置小于等于5。
原因说明:因为在kafka1.x以后,启用幂等后,kafka服务端会缓存producer发来的最近5个request的元数据, 故无论如何,都可以保证最近5个request的数据都是有序的。
数据可以在单分区有序,但一般无法保证分区间、多分区有序,是乱序的。例如下图集群接受Request3在Request5后,需要重新排序。
参考资料: