消息队列是一种用来储存消息的队列(先进先出)。消息队列,就是将需要传输的数据存放在队列中,实现管道作用。消息队列不是一个永久性的储存,是作为临时存储存在的(设定一个期限:设置消息在MQ中保存10天)。
消息中间件就是用来储存消息的软件(组件),连接各个系统。
大型电商网站(淘宝、京东、国美、苏宁…)、App(抖音、美团、滴滴等)等需要分析用户行为,要根据用户的访问行为来发现用户的喜好以及活跃情况,需要在页面上收集大量的用户访问信息。
使用消息队列,作为临时存储,或者一种通信管道。
Java服务器端开发的交互模型:
生产者、消费者模型
:
消息发送者发送消息到队列中,然后消息接收者从消息队列中取出并消费消息。消息被消费以后,消息队列中不在存储,所以消息接受者不可能消费到已经被消费的消息
kafka是一个分布式流平台。
发布和订阅
流数据流,类似于消费队列或者式企业消息传递系统- 以容错的持久化
存储
数据流- 处理数据流(
流处理
)
创建一个topic(主题)。Kafka中所有的消息都是保存在主题中,要生产消息到Kafka,必须要一个确定的主题。
# 创建test 主题
bin/kafka-topics -create --bootstrap-server node1 --topic test
# 产看当前Kafka中的主题
bin/kafka-topics --list --bootstrap-server node1:9092
使用Kafka内置的测试程序,生产一些消息到Kafka的test主题中。
bin/kafka-console-producer.sh --broker-list node1:9092 --topic test
使用下面的命令来消费 test 主题中的消息
bin/kafka-console-consumer.sh --bootstrap-server node1:9092 --topic test --from-beginning
Kafka中消息是以topic
进行分类的,生产者生产消息,消费者消费消息,都是面向topic的。
topic是逻辑上的概念,而partition
是物理上的概念每个partition对应一个log
文件,该log文件中储存的就是producer
生产的数据。Producer生产的数据会被不断的追加到该log文件末端,且每条数据都有自己的offest
。消费者组中的每个消费者,都会实时记录自己消费到了那个offset,以便出错恢复时,从上次的位置继续消费。
生产者生产的消息会不断的追加到log文件末尾,大文件顺序读写的时候比较快,然而为防止log文件过大导致定位效率低下,Kafla采取了分片
和索引
机制,当文件很大的时候(根据配置文件的设置,默认为1gb,开始分割。默认log文件一星期清除一次),将每个partition分为多个segment。每个segment
对应2个文件**.index(时间索引和位置索引)和一个log文件,该文件的命名规则为topic名称+分区序号
“.index”文件存储大量的索引信息,“.log”文件存储大量的数据,索引文件中的元数据指向对应数据文件中message的物理偏移地址。
1)分区原因:
(1)方便在集群中扩展
,每个partition可以通过调整以适应所在的机器,而一个topic又可以有多个Partition组成,因此整个集群就可以适应任意大小的数据了;
(2)可以提高并发
,因为可以以Partition为单位读写
2)分区原则(k-v由生产者指定的)
将producer发送的数据封装成一个producerRecord
对象
key
的hash
值与topic
的 partition
数进行取余得到 partition
值;topic
可用的partition
总数取余得到 partition
值,也就是常说的 round-robin
算法。为保证product发送的数据,能可靠的发送到指定的topic,topic的每日个人partition收到producer发送的数据后,都需要向producer发送ack(acknowledgement确认收到),如果product收到ack,就会进行下一轮的发送,否则重新发送数据。
方案 | 优点 | 缺点 |
---|---|---|
半数以上完成同步,就发送ack | 延迟低 | 选举新的leader时,容忍n台节点的故障,需要2n+1个副本 |
全部完成同步,才发送ack | 选举新的leader时,容忍n台节点的故障,需要n+1个副本 | 延迟高 |
Kafka选择了第二种方案,原因如下:
(1)同样为了容忍n台节点的故障,第一种方案需要2n+1个副本,而第二种方案只需要n+1个副本,而Kafka的每个分区都有大量的数据,第一种方案会造成大量数据的冗余。
(2)虽然第二种方案的网络延迟会比较高,但网络延迟对Kafka的影响较小
采用第二种方案之后,设想以下情景:leader收到数据,所有follower都开始同步数据,但有一个follower,因为某种故障,迟迟不能与leader进行同步,那leader就要一直等下去,直到它完成同步,才能发送ack。这个问题怎么解决呢?
Leader维护了一个动态的in-sync replica set (ISR),意为和leader保持同步的follower集合。当ISR中的follower完成数据的同步之后,leader就会给producer发送ack。如果follower长时间未向leader同步数据,则该follower将被踢出ISR,该时间阈值由replica.lag.time.max.ms参数设定。Leader发生故障之后,就会从ISR中选举新的leader。
对于某些不太重要的数据,对数据的可靠性要求不是很高,能够容忍数据的少量丢失,所以没必要等ISR中的follower全部接收成功。
所以Kafka为用户提供了三种可靠性级别,用户根据对可靠性和延迟的要求进行权衡,选择以下的配置。
acks参数配置:
acks:
0:
producer不等待broker的ack,这一操作提供了一个最低的延迟,broker一接收到还没有写入磁盘就已经返回,当broker故障时有可能**丢失数据
**;
1:
producer等待broker的ack,partition的leader落盘成功后返回ack,如果在follower同步成功之前leader故障,那么将会丢失数据;
-1(all)
:producer等待broker的ack,partition的leader和follower全部落盘成功后才返回ack。但是如果在follower同步完成后,broker发送ack之前,leader发生故障,那么会造成数据重复
。
(1)follower故障
fllower发生故障后被临时踢出ISR
,待该follower恢复后,follower会读取本地磁盘记录的上次的HW,并将log文件高于HW的部分截取掉,从HW开始向leader进行同步。等该floower的LEO大于Partition的HW
,及follower追上leader之后,就可以重复加入ISR。
(2)Leader故障
leader发生故障后,会从ISR中选取出一个新的leader,之后,为保证多个副本之间的数据一致性,其余的follower会将各自log高于HW的部分截掉,然后从新的leader同步数据
在生产者生产消息是,如果出现retry时,有可能会一条消息发送了多次,如果Kafla不具备幂等性的,就有可能在partition中保存多条一摸一样的消息
1)幂等性原理
为了实现生产者的幂等性,Kafka引入了 Producer ID(PID)
和 Sequence Number
的概念。
PID
:每个Producer在初始化时,都会分配一个唯一的PID,这个PID对用户来说,是透明的。Sequence Number
:针对每个生产者(对应PID)发送到指定主题分区的消息都对应一个从0开始递增的Sequence Number。要启用幂等性,只需要将Producer
的参数中enable.idompotence
设置为true
即可。Kafka的幂等性实现其实就是将原来下游需要做的去重放在了数据上游。开启幂等性的Producer在初始化的时候会被分配一个PID
,发往同一Partition
的消息会附带Sequence Number
。而Broker端会对
做缓存,当具有相同主键的消息提交时,Broker只会持久化一条。
幂等性无法保证跨分区会话。
push
(推)模式很难适应消费速率不同的消费者,因为消息发送速率是由broker
决定的。它的目标是尽可能以最快速度传递消息,但是这样很容易造成consumer来不及处理消息
,典型的表现就是拒绝服务
以及网络拥塞
。而pull模式则可以根据consumer的消费能力以适当的速率消费消息。
pull模式不足之处是,如果kafka没有数据,消费者可能会陷入循环中,一直返回空数据。
针对这一点,Kafka的消费者在消费数据时会传入一个时长参数timeout
,如果当前没有数据可供消费,consumer会等待一段时间之后再返回,这段时长即为timeout。
一个consumer group 中有多个comsumer,一个topic有多个partition。
Kafka
有分配策略:roundrobin
,range
,Stricky
(rebalance时启用)。(cousumer数量比partition少的时候)
1)roundrobin
(轮询)
将消费组内所有消费者以及消费者所订阅的所有topic的partition按照字典序排序
(topic和分区的hashcode进行排序),然后通过轮询方式逐个将分区以此分配给每个消费者。
2)range
范围分配策略是Kafka默认的分配策略,它可以确保每个消费者消费的分区数量是均衡的。
3)Stricky粘性分配
从Kafka 0.11.x开始,引入此类分配策略,为了:分区分配尽可能均匀;在发生rebalance的时候,分区的分配尽可能与上一次分配保持相同。
没有发生rebalance
时,Striky粘性分配策略和RoundRobin分配策略类似:
上面如果consumer2崩溃了,此时需要进行rebalance
。如果是Range分配和轮询
分配都会重新进行分配
,
consumer0和consumer1原来消费的分区大多发生了改变。采用粘性分配策略。
Striky粘性分配策略,保留rebalance之前的分配结果。这样,只是将原先consumer2负责的两个分区再均匀分配给consumer0、consumer1,这样可以明显减少系统资源的浪费。
由于consumer在消费过程中有可能出现断电宕机的故障,consumer恢复后,需要从故障前的位置继续消费,所以counsumer需要实时记录自己消费到了那一个offset,以便故障恢复后继续消费。
0.9 版之前,consumer默认将offset保存在zookeeper
中,0.9版本之后,consumer默认将offset保存在kafka哟个内置的topic中,该topic为__comsumer_offsets
。
kafkad的product生产数据,需要写入log中,写的过程是一直追加文件到文件末端,为顺序写,其中省去了大量的寻址时间
Kafka数据持久化是直接持久化到Pagecache,先将读写缓存在内存中,然后组装成顺序读写写(操作系统提供)
所有空闲内存(非 JVM 内存)
。如果使用应用层 Cache(即 JVM 堆内存),会增加 GC 负担持久化到Pagecache上可能会造成宕机丢失数据的情况,但这可以被Kafka的Replication机制解决
所有的缓存只发生了页面缓存,没有发生其他的拷贝。 操作系统提供的机制
Kakfa集群中有一个broker会被选举成为Controller(谁快,谁线在zookeeper中建立节点),负责管理集群broker的上下线,所有的topic的分区副本分片
和leader选举过程
。Controller的管理工作都是依赖于Zookeeper的
。Kafka事务指的是生产者生产消息以及消费者提交offset的操作可以在一个原子操作中,要么都成功,要么都失败。尤其是在生产者、消费者并存时,事务的保障尤其重要。(consumer-transform-producer模式)
为了实现跨分区
会话的事务,需要引入一个全局的唯一的Transaction ID
,并将Producer获得PID
和Transaction ID
绑定。这样当Producer重启后就可以通过正在进行的Transaction ID获取原来的PID
为了管理Transaction,Kafka引入了一个新的组件Transaction Coordinator
。Producer就是通过和Transaction Coordinator交互获得Transaction ID对应的任务状态。Transaction Coordinator还负责将事务所有写入Kafka的一个内部Topic,这样即使整个服务重启,由于事务状态得到保存,进行中的事务状态可以得到恢复,从而继续进行。
server对consumer没有控制能力
,consumer通常不考虑。如果控制消费行为得用户自己实现业务逻辑
事务机制主要是从Producer方面考虑,对于Consumer而言,事务的保证就会相对较弱,尤其时无法保证Commit的信息被精确消费。这是由于Consumer可以通过offset
访问任意信息,而且不同的Segment File生命周期不同,同一事务的消息可能会出现重启后被删除的情况。
如果想完成Consumer端的精准一次性消费,那么需要kafka消费端将消费过程和提交offset过程做原子绑定
。此时我们需要将kafka的offset保存到支持事务的自定义介质中(比如mysql)。这部分知识会在后续项目部分涉及。
Kafka的Producer发送消息采用的是异步发送
的方式。在消息发送的过程中,涉及到了两个线程——main线程
和Sender线程
(守护线程),以及一个线程共享变量——RecordAccumulator
。main线程将消息发送给RecordAccumulator,Sender线程不断从RecordAccumulator中拉取消息发送到Kafka broker。
参数:
batch.size:只有数据积累到batch.size之后,sender才会发送数据。
linger.ms:如果数据迟迟未达到batch.size,sender等待linger.time之后就会发送数据。
public class KafkaProducerTest {
/**
* todo-测试Kafka生产者
* 1. 创建用于连接Kafka的Properties配置
* 2. 创建一个生产者对象KafkaProducer,并且kafka配置信息
* 3. 调用send发送1-100消息到指定Topic test,并获取返回值Future,该对象封装了返回值
* 4. 再调用一个Future.get()方法等待响应
* 5. 关闭生产者
*/
@Test
public void testKafkaProduct() throws ExecutionException, InterruptedException {
//1. 创建用于连接Kafka的Properties配置
Properties props = new Properties();
props.setProperty("bootstrap.servers", "192.168.88.161:9092");
props.setProperty("ack","all");
props.setProperty("key.serializer","org.apache.kafka.common.serialization.StringSerializer");
props.setProperty("value.serializer","org.apache.kafka.common.serialization.StringSerializer");
// 2.创建一个生产者对象KafkaProducer,并且kafka配置信息
KafkaProducer<String, String> kafkaProducer = new KafkaProducer<>(props);
// 调用send发送1-100消息到指定Topic test,并获取返回值Future,该对象封装了返回值
for (int i = 0; i < 100; i++) {
// TODO 方式一使用同步等待的方式生产数据
//String topic, K key, V value
//ProducerRecord producerRecord = new ProducerRecord<>("test", null, i + "");
//Future future = kafkaProducer.send(producerRecord);
// 调用getFuture的get方法等待响应
//future.get();
// System.out.println("第"+i+"条消息写入成功");
// TODO 方式二 使用异步回调的方式发送消息
ProducerRecord<String, String> producerRecord = new ProducerRecord<>("test", null, i + "");
kafkaProducer.send(producerRecord, new Callback() {
@Override
public void onCompletion(RecordMetadata metadata, Exception exception) {
// 判断消息是否成功
// 在发送消息出现异常的时候,能够即使打印出异常信息
// 消息成功式,打印topic、分区id、offset
if (exception ==null){
// 发送成功
String topic = metadata.topic();
int partition = metadata.partition();
long offset = metadata.offset();
System.out.println("topic:"+topic+" partition:"+partition+" offset:"+offset);
}else {
// 发送出现错误
System.out.println("出现异常");
// 打印异常消息
System.out.println(exception.getMessage());
// 打印调用栈
System.out.println(Arrays.toString(exception.getStackTrace()));
}
}
});
}
kafkaProducer.close();
}
}
public class KafkaConsumerTest {
@Test
public void testKafkaConsumer() throws InterruptedException {
Properties props = new Properties();
props.setProperty("bootstrap.servers", "node1:9092");
//消费者组
props.setProperty("group.id", "test");
//自动提交offset
props.setProperty("enable.auto.commit", "true");
//offset提交间隔时间
props.setProperty("auto.commit.interval.ms", "1000");
//反序列化key的值
props.setProperty("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
//反序列化value的值
props.setProperty("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
// 创建消费者
KafkaConsumer<String, String> kafkaConsumer = new KafkaConsumer<>(props);
// 订阅要消费的主题
kafkaConsumer.subscribe(Arrays.asList("test"));
while (true) {
// 消费者一次拉取一批数据
ConsumerRecords<String, String> consumerRecords = kafkaConsumer.poll(Duration.ofSeconds(5));
//如果没有拉取到数据,就停30m
if (consumerRecords.count()==0){
Thread.sleep(30);
}
for (ConsumerRecord<String, String> consumerRecord : consumerRecords) {
// 主题
String topic = consumerRecord.topic();
// offset:这条消息处于kafka分区中那个位置
long offset = consumerRecord.offset();
// key/value
String key = consumerRecord.key();
String value = consumerRecord.value();
System.out.println("topic = " + topic + " ,offset = " + offset + " ,key = " + key + " ,value = " + value);
}
}
}
}
onsumerRecord : consumerRecords) {
// 主题
String topic = consumerRecord.topic();
// offset:这条消息处于kafka分区中那个位置
long offset = consumerRecord.offset();
// key/value
String key = consumerRecord.key();
String value = consumerRecord.value();
System.out.println("topic = " + topic + " ,offset = " + offset + " ,key = " + key + " ,value = " + value);
}
}
}
}