记录Kafka的使用
什么是kafka
-
定义
-
Kafka是分布式的基于发布/订阅模式的消息队列,主要应用于大数据实时处理领域
-
消息队列
-
削峰:高峰期将任务缓存在消息队列中,持续处理
-
解耦:不需要两边的处理同时在线
-
可恢复性:当系统一部分组件失效时,不会影响到整个系统
-
缓冲:有助于控制和优化数据流经过系统的速度,解决生产消息和消费消息速度不一致的问题
-
异步通信:允许用户将一个消息放入队列,但并不立即处理它
-
消息队列的模式
-
1.点对点模式
特点:一对一,消费者主动获取消息,消息被消费后清除
消息生产者可以对应多个消费者,但是消息只能被一个消费者消费
- 2.发布/订阅模式
特点:一对多,生产者推送数据,消费者消费数据之后不会清除消息
消息生产者将消息发布到topic中,同时有多个消费者消费该消息
发布/订阅模式又分为
MQ主动的推送模式
消费者主动的获取模式
- kafka基础架构
- 1.Producer:消息生产者,就是向kafka broker发消息的客户端
- 2.Consumer:消息消费者,就是向kafka broker拉取消息的客户端
- 3.Consumer Group:多个Consumer组成一个group,消费者组内的每一个消费者负责消费不同分区的数据,一个分区只能由一个消费者消费,消费者组之间互不影响.消费者组是逻辑上的一个订阅者
- 4.Broker:一台kafka服务器就是一个broker,一个集群由多个broker组成.一个broker可以容纳多个topic
- 5.Topic:一个主题,生产者和消费者面向的都是一个主题
- 6.Partition:一个TOPIC可以分布在不同的BROKER中,由多个Partition组成,每个partition都是一个有序的队列
- 7.Replica:副本,Broker中维护一个TOPIC的某个partition(Leader)同时还维护了该TOPIC其他partition的follower
- 8.Leader:每个Topic都由一个若干个Partition组成,而每一个Partition又有1到brokers数量的副本,其中只有主副本(Leader)才能对外提供服务,其他的副本(Follower)只是用来保存数据
- 9.Follower:每个分区多个副本中的从,主要用于数据备份.当Leader发生故障时,某个follower会成为新的leader
- 10.注册中心,kafka依赖于zookeeper,作为注册中心
kafka快速入门
- 安装
https://www.cnblogs.com/zikai/p/9627736.html - 常用命令(命令行)
- 1.kafka-server-start:启动kafka
通常启动kafka需要指定配置文件
bin/kafka-server-start.sh -daemon config/server.properties
这里使用守护进程启动,使用jps指令可以看到正在执行的kafka
- 2.kafka-server-stop:停止kafuka服务
bin/kafka-server-stop.sh
- 3.kafka-topics :主题相关操作
1.kafka-topics --list:查看当前所有主题
./kafka-topics.sh --list --zookeeper 7.223.145.184:2181 注意必须跟zk的地址
2.kafka-topics --create:创建主题
./kafka-topics.sh --create --zookeeper 7.223.145.184:2181 --topic first --partitions 2 --replication-factor 1
创建一个名称为first的主题,该主题有2个partition,和1个副本
副本数量最多和Broker数量一样.副本都会存在kafka所在的磁盘log.dirs配置的位置
3.kafka-topics --delete:删除主题
./kafka-topics.sh --delete --zookeeper 7.223.145.184:2181 --topic first
4.kafka-topics --describe:查看主题详情
./kafka-topics.sh --describe --zookeeper 7.223.145.184:2181 --topic first
- 4.kafka-console-producer: 生产者
./kafka-console-producer.sh --topic first --broker-list 7.223.143.235:9092
创建一个生产者.链接到我们的集群(7.223.143.235:9092,此时我只有一个kafka,集群也是一样的,只需要链接到任意一台)
此时我们就可以输入消息了
- 5.kafka-console-consumer: 消费者
1.消费
./kafka-console-consumer.sh --topic first --bootstrap-server 7.223.143.235:9092
./kafka-console-consumer.sh --topic first --zookeeper 7.223.145.184:2181(上古版本可用,新版本已不支持)
为了能够实现续传,kafka记录了某个消费者group对某个主题的消费情况(消费到哪里了?)
kafka在上古时期是通过zookeeper来存储偏移量的,存放在/consumers/[group_id]/offsets/[topic]/[broker_id-part_id]节点下
新版本的kafka将这些消息存在kafka集群的consumer主题中,不再依赖zookeeper,该主题默认是50个分区1个副本
2.从头消费
./kafka-console-consumer.sh --topic first --bootstrap-server 7.223.143.235:9092 --from-beginning
我们利用--from-beginning参数可以从头消费
kafka架构深入
- kafka架构及工作过程
- kafka的架构如下图,作如下假设
- 1.当前的topic为A,有3个partition(0,1,2),Replica数为2,每个broker保存的副本如图所示
- 2.每个partition当中存在6/4/5条消息,途中的编号为消息的offset,此时我们的producer一共产生了15条消息,可见,每个partition都有单独的offset编号,从而我们也可以看到,kafka不能保证全局有序
- 3.Follower会自动备份Leader中的消息,这就是kafka的容灾策略
- 4.Consumer Group:正如图所示,每一个消费者对应一个分区,消费者每消费一条数据,就会记录下对应已消费的offset值(同样也是partition隔离的),以便于出错时可以从上次的位置继续消费
根据Kafka的规定
1.每一个partition只能被同一个组中的一个consumer消费
2.同一个组中的consumer可以消费多个partition
所以最理想的情况是一个partition对应一个consumer(同一个consumer group中),这就是我们的分区依据
比如我们生产的速度是10kB/s,消费者性能的极限速度是1kB/s.
那么我们需要10台consumer才能稳定消费,此时我们可以将topic设置为10个分区,同时将10个consumer组成一个consumer group
如果我们想要多个consumer消费一个partition,怎么办呢?
我们可以设定多个consumer group,来消费同一个partition
https://www.cnblogs.com/sa-dan/p/8080197.html
- kafka文件存储机制
- 由于生产者产生的消息会不断追加到log文件末尾,为了有效防止log文件过大导致数据定位效率低下,Kafka引入了分片和索引机制,将每个partition分为多个segment(默认是1GB),每个segment对应两个文件"xx.index"和"xx.log".其中前缀是当前segment的第一条消息的offset.
- 如下图所示,index存放的内容是每条消息offset和消息在Log文件中存储位置的映射关系
- kafka分区操作
- 本节阐明生产者如何让生产的消息选择分区(Java)
- 1.生产者可以直接指明选择的分区
ProducerRecord(@NotNull String topic,Integer partition,String key,String value)
- 2.如果值是key,value形式的,可以使用key的hash与分去数取余
ProducerRecord(@NotNull String topic,String key,String value)
- 3.如果只有value,则通过轮询
ProducerRecord(@NotNull String topic,Integer partition,String value)
- 数据可靠性
- 本节阐明如何确保生产者的消息成功传给kafka,保证数据不丢失
- 1.topic的每个partition收到produicer发送的数据之后,都需要向producer发送ack,如果producer收到ack,就会进行下一轮的发送,否则重新发送数据
- 2.ISR(in-sync relplica)
Kafka的同步机制
为了防止某个follower长时间不响应导致同步困难,kafka维护了一个ISR用来解决这个问题kafka在发送ack之前,要求全部的follower都同步完成 这种方案的缺点是延迟高.优点是容灾需要的节点少(选举新leader的时候,容忍n节点故障,需要N+1个副本) zk选用的方案是半数机制,这种机制的优点是延迟低,但容灾需要的节点多(选举新leader,容忍N节点故障,需要2*N+1个副本)
Leader维护了一个动态的ISR 当ISR中的follower长时间未向leader同步,则该follower将被踢出ISR 该时长由replica.lag.time.max.ms决定 Leader发生故障后会从ISR中选举新的Leader
- 3.ack应答机制
- kafka有三种ack机制
- 0:接收到信息直接ack
效率最高,容错率最低
- 1:接收到信息leader落地完成后ack
效率高.但也容易丢失数据
- -1:所有isr中的副本同步完成后ack
效率最低. 在极端情况下也会丢失数据:isr中只有leader并且down 在某些情况下会造成数据重复:数据同步完成后还未返回ack的时候leader挂掉
- 消费数据一致性
- 由于kafka有多种ack机制,在某些机制下同步还没有完成就接收到了新的数据,有可能出现数据不同步,此时如何对消费者保证数据的一致性
- 可见如果此时leader挂掉了,选择了follower1,则此时最大的OFFSET为15,会导致数据不一致
- 为此kafka引入了两个概念HW和LEO,consumer可以收到的OFFSET为HW
LEO:Log End Offset,每个副本中最大的OFFSET
HW:High Watermark,类似于木桶效应,ISR中最小的LEO
- 基于HW的故障处理(保证partition中的数据一致性)
1.follower故障:follwer从故障恢复后,会读取本地次胖记录的上次的HW,并将Log文件高于HW的部分丢弃,
从HW开始向leader重新同步,当follower的LEO>=该partition的HW时,Follwer就可以重新加入ISR了
2.leader故障:leader发生故障后,会从ISR中选出一个新的leader,为保证多个副本之间的数据一致性,
其余的follower会将各自的log文件高于HW的部分截掉,然后从新的leader同步数据
- Exactly Once
- 前面介绍了不同的ack机制,都存在不同的问题
ACK 0: at most once -> 数据可能丢失
ACK -1: at last once -> 数据可能重复
- kafka在0.11版本之后解决了数据重复的问题
幂等性开关开启(Producer的参数enable.idompotence = true)后,该producer在初始化时会被分配一个PID
发往同一个Partition的消息会附带Sequence Number,Broker会对<PID,Partition,SeqNumber>作缓存这就是Kafka去重的依据
注意:重启PID会变化,同时kafka只能保证同一个partition中的exactly once
消费者
- 消费方式
- 消费者采用pull(拉取)的方式从broker中获取数据
pull模式不足之处是,如果kafka没有数据,消费者可能会陷入循环中,一直返回空数据
针对这一点,kafka在消费数据时会传入一个市场参数(timeout)
如果没有数据可以消费,consumer会等待一段时间之后再返回,这段市场即为timeout
- 分区分配策略
- 一个consumer group中有多个consumer,一个topic有多个partition,所以必然会涉及到partiton的分配问题,即决定某个partition由哪个consumer消费’’
- Kafka有两种分配策略,一是RoundRobin,一是Range
== 加图==
- 1.RoundRobin:轮询分配.在轮询策略中,会将消费者组监听的所有主题的所有partition进行排序后轮询交付给consumber
优点:可以使得所有的consumer监听的partition个数差别最小
缺点:无法保证消费者消费的主题
- 2.Range(默认):范围分配是按照单个主题来划分
优点:保证消费者组的每一个消费者都能消费所有的主题
缺点:消费者监听partition个数不对等
Kafka API
Producer API
- 消息发送流程
- kafka的Producer发送消息采用的是异步发送的方式.在消息发送的过程中.涉及到两个线程–main和sender线程.以及两个线程的共享变量–RecordAccumulator,sender线程不断从RecordAccumulator中拉取消息发送到Kafka broker
== 加图==
- 相关参数
1.batch size:只有数据累积到batch.size之后,sender才会发送数据
2.linger.ms:如果数据迟迟未达到bath.size,sender等待linger.time之后就会发送数据
3.我们从ProducerConfig类中可以找到所有的配置
- 普通生产者
public class MyProducer {
public static void main(String[] args) {
//创建生产者的配置信息
Properties properties = new Properties();
properties.put("bootstrap.servers","7.223.143.235:9092");
//ack应答级别-1
properties.put("ack","all");
//重复次数
properties.put("retries",3);
//批次大小 16kb
properties.put("batch.size",16384);
//等待时间
properties.put("linger.ms",1);
//RecordAccumulator缓冲区大小
properties.put("buffer.memory",33554432);
//序列化类
properties.put("key.serializer","org.apache.kafka.common.serialization.StringSerializer");
properties.put("value.serializer","org.apache.kafka.common.serialization.StringSerializer");
//创建生产者
KafkaProducer<String, String> producer = new KafkaProducer<String, String>(properties);
//发送数据
for (int i = 0; i < 10; i++) {
producer.send(new ProducerRecord<String,String>("third","value--"+i));
System.out.println(i);
}
//关闭资源
producer.close();
}
}
- 带回调函数的生产者
public class CallBackProducer {
public static void main(String[] args) {
//创建生产者的配置信息
Properties properties = new Properties();
properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "7.223.143.235:9092");
//序列化类
properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");
properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");
//创建生产者
KafkaProducer<String, String> producer = new KafkaProducer<String, String>(properties);
//发送数据
for (int i = 0; i < 10; i++) {
producer.send(new ProducerRecord<String, String>("third", "value--" + i), (metadata, exception) -> {
if (exception == null) {
System.out.println(metadata.partition() + "--" + metadata.offset());
} else {
exception.printStackTrace();
}
});
}
//关闭资源
producer.close();
}
}
- 生产者分区策略
public class CallBackProducer {
public static void main(String[] args) {
//创建生产者的配置信息
Properties properties = new Properties();
properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "7.223.143.235:9092");
//序列化类
properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");
properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");
//创建生产者
KafkaProducer<String, String> producer = new KafkaProducer<String, String>(properties);
//发送数据
for (int i = 0; i < 10; i++) {
//指定内容都发送到0号分区
producer.send(new ProducerRecord<String, String>("third", 0,"key","value--" + i), (metadata, exception) -> {
if (exception == null) {
System.out.println(metadata.partition() + "--" + metadata.offset());
} else {
exception.printStackTrace();
}
});
//通过key的hash值确定分区
producer.send(new ProducerRecord<String, String>("third", i%3+"","value--" + i), (metadata, exception) -> {
if (exception == null) {
System.out.println(metadata.partition() + "--" + metadata.offset());
} else {
exception.printStackTrace();
}
});
}
//关闭资源
producer.close();
}
}
- 自定义分区器
public class MyPartitioner implements Partitioner {
@Override
public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
return key.toString().hashCode() % cluster.partitionCountForTopic(topic);
}
@Override
public void close() {
}
@Override
public void configure(Map<String, ?> map) {
}
}
public class PartitionProducer {
public static void main(String[] args) {
//创建生产者的配置信息
Properties properties = new Properties();
properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "7.223.143.235:9092");
//序列化类
properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");
properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");
//配置分区器
properties.put(ProducerConfig.PARTITIONER_CLASS_CONFIG,"xx.xx.xx.MyProducer");
//创建生产者
KafkaProducer<String, String> producer = new KafkaProducer<String, String>(properties);
//发送数据
for (int i = 0; i < 10; i++) {
producer.send(new ProducerRecord<String, String>("third", "value--" + i))
}
//关闭资源
producer.close();
}
}