KafKa

1.0 简介

1.0.1 消息队列

消息队列是一种用来储存消息队列(先进先出)。消息队列,就是将需要传输的数据存放在队列中,实现管道作用。消息队列不是一个永久性的储存,是作为临时存储存在的(设定一个期限:设置消息在MQ中保存10天)。

1.0.2 消息中间键(消息队列的组件)

消息中间件就是用来储存消息的软件(组件),连接各个系统。

1.0.3 消息队列的应用场景

1.0.3.1 异步处理

消息队列缓存大小和占有率关系 消息队列存储_消息队列缓存大小和占有率关系

1.0.3.2 系统解耦

消息队列缓存大小和占有率关系 消息队列存储_消息队列缓存大小和占有率关系_02

1.0.3.3 流量削峰

消息队列缓存大小和占有率关系 消息队列存储_kafka_03

1.0.3.4 日志处理

大型电商网站(淘宝、京东、国美、苏宁…)、App(抖音、美团、滴滴等)等需要分析用户行为,要根据用户的访问行为来发现用户的喜好以及活跃情况,需要在页面上收集大量的用户访问信息。

消息队列缓存大小和占有率关系 消息队列存储_队列_04

使用消息队列,作为临时存储,或者一种通信管道。

1.0.4 消息队列的模型

1.0.4.1 生产者、消费者模式

Java服务器端开发的交互模型:

消息队列缓存大小和占有率关系 消息队列存储_队列_05

消息队列缓存大小和占有率关系 消息队列存储_消息队列缓存大小和占有率关系_06

生产者、消费者模型:

消息队列缓存大小和占有率关系 消息队列存储_kafka_07

  • 生产者可以生产消息到消息队列中,消费者可以把消息队列中的中的数据取出。
  • 消费者和生产者之间是解耦的

1.0.5 消息队列的两种模式

1.0.5.1 点对点模式

消息队列缓存大小和占有率关系 消息队列存储_kafka_08

消息发送者发送消息到队列中,然后消息接收者从消息队列中取出并消费消息。消息被消费以后,消息队列中不在存储,所以消息接受者不可能消费到已经被消费的消息

  • 每个消息只有一个接收者(一旦被消费,消息就不在消息队列中)
  • 发送者和接收者之间没有依赖性,发送者发送消息之后,不管有没有接收者在允许,动不会影响到发送者下次发送消息。
  • 接收者在成功接收消息之后需向队列应答成功,以便消息队列删除当前接收消息
1.0.5.2 发布订阅模式

消息队列缓存大小和占有率关系 消息队列存储_队列_09

  • 每个消息可以有多个订阅者
  • 发步骤和订阅者之间有在时间上的依赖。针对某个主题的订阅者,必须创建一个订阅者之后,才能消费发布者的消息
  • 为了消费信息,订阅者需要提前订阅该角色主题,并保存在线运行

1.1.1 Kafka 介绍

kafka是一个分布式流平台。

  • 发布和订阅流数据流,类似于消费队列或者式企业消息传递系统
  • 以容错的持久化存储数据流
  • 处理数据流(流处理)
1.1.1.1 应用场景
  • 建立实时的数据管道,以可靠地在系统或应用程序之间获取数据
  • 构建实时流应用程序,以转换或者响应数据流

消息队列缓存大小和占有率关系 消息队列存储_消息队列缓存大小和占有率关系_10

2 kafka 基础操作

2.1 创建topic

创建一个topic(主题)。Kafka中所有的消息都是保存在主题中,要生产消息到Kafka,必须要一个确定的主题。

# 创建test 主题
bin/kafka-topics -create --bootstrap-server node1 --topic test
# 产看当前Kafka中的主题
bin/kafka-topics --list --bootstrap-server node1:9092

2.2 生产消息到Kafka

使用Kafka内置的测试程序,生产一些消息到Kafka的test主题中。

bin/kafka-console-producer.sh --broker-list node1:9092 --topic test

2.3 从kafka消费信息

使用下面的命令来消费 test 主题中的消息

bin/kafka-console-consumer.sh --bootstrap-server node1:9092 --topic test --from-beginning

3. Kafka 架构

3.0 基本架构

消息队列缓存大小和占有率关系 消息队列存储_kafka_11

3.1 重要概念

3.1.1 Broker

消息队列缓存大小和占有率关系 消息队列存储_大数据_12

  • 一个kafka的集群通常由多个broker组成,这样才能实现负载均衡、以及容错
  • broker是无状态的,它们通过Zookeeper来维护集群
  • 一个kafka的broker每秒可以处理数十万读写,每个broker都可以处理TB消息而不影响性能
3.1.2 Zookpeer
  • ZK用来管理和协调broker,并且存储了Kafka的元数据(例如:有多少topic、partition、consumer)
  • ZK服务主要用于通知生产者和消费者Kafka集群中有新的broker加入、或者Kafka集群中出现故障的broker。
3.1.3 producer(生产者)
  • 生产者负责将数据推送给broker的topic
3.1.4 consumer(消费者)
  • 消费者从broker的topic中拉取数数据,并自己进行处理
3.1.5 consumer group(消费者组)

消息队列缓存大小和占有率关系 消息队列存储_消息队列缓存大小和占有率关系_13

  • consumer group 是kafka提供可扩展且具有容错性的消费者机制
  • 一个消费者组可以包含多个消费者
  • 一个消费者组由一个唯一ID(group id)
  • 组内的消费者一起消费主题的所有分区数据
3.1.6 partitions(分区)

消息队列缓存大小和占有率关系 消息队列存储_消息队列缓存大小和占有率关系_14

  • 在Kafka集群中,主题被分为多个分区
  • 在同一个分区中数据是有序的
  • 为了增加吞吐性
3.1.7 副本(Replicas)

消息队列缓存大小和占有率关系 消息队列存储_大数据_15

  • 副本可以确保某个服务器出现故障时,确保数据依然可用
  • 在Kafka中,一般都会设置副本的个数>1
3.1.8 主题(Topic)

消息队列缓存大小和占有率关系 消息队列存储_kafka_16

  • 主题时一个逻辑概念,用于生产者发布数据,消费者拿去数据
  • 主题必须有表示符,而且是唯一的,kafka中可以有任意数量的主题,没有数量上的限制
  • 在主题的消息是由结构的,一般一个主题包含某一类的消息
  • 一旦生产者发送到主题中,这些消息就不能被更改(跟新)
3.1.8 偏移量(offset)

消息队列缓存大小和占有率关系 消息队列存储_队列_17

  • offset 记录着下一条将要发送到consumer的消息序号
  • 默认将offset储存在ZooKeeper中
  • 在分区中,消息是由顺序的方式储存,每个在分区的消费者都有一个递增的id。就是偏移量
  • 偏移量在分区中才有意义。在分区之间,offset是没有任何意义的4.0 Kafka的架构

3.2 Kafka 工作流程及文件存储机制

3.2.1 Kafka 工作流程

消息队列缓存大小和占有率关系 消息队列存储_数据_18

Kafka中消息是以topic进行分类的,生产者生产消息,消费者消费消息,都是面向topic的。

topic是逻辑上的概念,而partition是物理上的概念每个partition对应一个log文件,该log文件中储存的就是producer生产的数据。Producer生产的数据会被不断的追加到该log文件末端,且每条数据都有自己的offest。消费者组中的每个消费者,都会实时记录自己消费到了那个offset,以便出错恢复时,从上次的位置继续消费。

消息队列缓存大小和占有率关系 消息队列存储_队列_19

生产者生产的消息会不断的追加到log文件末尾,大文件顺序读写的时候比较快,然而为防止log文件过大导致定位效率低下,Kafla采取了分片索引机制,当文件很大的时候(根据配置文件的设置,默认为1gb,开始分割。默认log文件一星期清除一次),将每个partition分为多个segment。每个segment对应2个文件**.index(时间索引和位置索引)和一个log文件,该文件的命名规则为topic名称+分区序号

消息队列缓存大小和占有率关系 消息队列存储_kafka_20

“.index”文件存储大量的索引信息,“.log”文件存储大量的数据,索引文件中的元数据指向对应数据文件中message的物理偏移地址。

3.2.2 生产者(Kafka的数据也是K-V对)
3.2.2.1 分区策略
  • 1)分区原因:
    (1)方便在集群中扩展,每个partition可以通过调整以适应所在的机器,而一个topic又可以有多个Partition组成,因此整个集群就可以适应任意大小的数据了;
    (2)可以提高并发,因为可以以Partition为单位读写
  • 2)分区原则(k-v由生产者指定的
    将producer发送的数据封装成一个producerRecord对象
  • 指明 partition 的情况下,直接将指明的值直接作为 partition 值;
  • 没有指明 partition 值但有 key 的情况下,将 keyhash 值与topicpartition数进行取余得到 partition 值;
  • 既没有 partition 值又没有 key 值的情况下,第一次调用时随机生成一个整数(后面每次调用在这个整数上自增),将这个值与topic 可用的partition 总数取余得到 partition 值,也就是常说的 round-robin 算法。
3.2.2.2 数据可靠性保证

为保证product发送的数据,能可靠的发送到指定的topic,topic的每日个人partition收到producer发送的数据后,都需要向producer发送ack(acknowledgement确认收到),如果product收到ack,就会进行下一轮的发送,否则重新发送数据。

消息队列缓存大小和占有率关系 消息队列存储_大数据_21

1)副本数据同步策略(全部同步完成,才发送ack)

方案

优点

缺点

半数以上完成同步,就发送ack

延迟低

选举新的leader时,容忍n台节点的故障,需要2n+1个副本

全部完成同步,才发送ack

选举新的leader时,容忍n台节点的故障,需要n+1个副本

延迟高

Kafka选择了第二种方案,原因如下:

(1)同样为了容忍n台节点的故障,第一种方案需要2n+1个副本,而第二种方案只需要n+1个副本,而Kafka的每个分区都有大量的数据,第一种方案会造成大量数据的冗余。

(2)虽然第二种方案的网络延迟会比较高,但网络延迟对Kafka的影响较小

2)ISR

采用第二种方案之后,设想以下情景: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。

3)ack应答机制

对于某些不太重要的数据,对数据的可靠性要求不是很高,能够容忍数据的少量丢失,所以没必要等ISR中的follower全部接收成功。

所以Kafka为用户提供了三种可靠性级别,用户根据对可靠性和延迟的要求进行权衡,选择以下的配置。

acks参数配置:

acks:

0:producer不等待broker的ack,这一操作提供了一个最低的延迟,broker一接收到还没有写入磁盘就已经返回,当broker故障时有可能**丢失数据**;

1:producer等待broker的ack,partition的leader落盘成功后返回ack,如果在follower同步成功之前leader故障,那么将会丢失数据

消息队列缓存大小和占有率关系 消息队列存储_大数据_22

-1(all):producer等待broker的ack,partition的leader和follower全部落盘成功后才返回ack。但是如果在follower同步完成后,broker发送ack之前,leader发生故障,那么会造成数据重复

消息队列缓存大小和占有率关系 消息队列存储_数据_23

4)故障处理细节

(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同步数据

消息队列缓存大小和占有率关系 消息队列存储_kafka_24

3.2.2.3 数据可靠性保证

消息队列缓存大小和占有率关系 消息队列存储_队列_25

在生产者生产消息是,如果出现retry时,有可能会一条消息发送了多次,如果Kafla不具备幂等性的,就有可能在partition中保存多条一摸一样的消息

1)幂等性原理

为了实现生产者的幂等性,Kafka引入了 Producer ID(PID)Sequence Number的概念。

  • PID:每个Producer在初始化时,都会分配一个唯一的PID,这个PID对用户来说,是透明的。
  • Sequence Number:针对每个生产者(对应PID)发送到指定主题分区的消息都对应一个从0开始递增的Sequence Number。

消息队列缓存大小和占有率关系 消息队列存储_kafka_26

要启用幂等性,只需要将Producer的参数中enable.idompotence设置为true即可。Kafka的幂等性实现其实就是将原来下游需要做的去重放在了数据上游。开启幂等性的Producer在初始化的时候会被分配一个PID,发往同一Partition的消息会附带Sequence Number。而Broker端会对<PID, Partition, SeqNumber>做缓存,当具有相同主键的消息提交时,Broker只会持久化一条。

幂等性无法保证跨分区会话。

3.2.3 消费者
3.2.3.1 消费模式

push(推)模式很难适应消费速率不同的消费者,因为消息发送速率是由broker决定的。它的目标是尽可能以最快速度传递消息,但是这样很容易造成consumer来不及处理消息,典型的表现就是拒绝服务以及网络拥塞而pull模式则可以根据consumer的消费能力以适当的速率消费消息。

pull模式不足之处是,如果kafka没有数据,消费者可能会陷入循环中,一直返回空数据。针对这一点,Kafka的消费者在消费数据时会传入一个时长参数timeout,如果当前没有数据可供消费,consumer会等待一段时间之后再返回,这段时长即为timeout。

3.2.3.2 分区分配策略

一个consumer group 中有多个comsumer,一个topic有多个partition。

Kafka有分配策略:roundrobin,range,Stricky(rebalance时启用)。(cousumer数量比partition少的时候)

1)roundrobin(轮询)

将消费组内所有消费者以及消费者所订阅的所有topic的partition按照字典序排序(topic和分区的hashcode进行排序),然后通过轮询方式逐个将分区以此分配给每个消费者。

消息队列缓存大小和占有率关系 消息队列存储_kafka_27

2)range范围分配策略是Kafka默认的分配策略,它可以确保每个消费者消费的分区数量是均衡的。

消息队列缓存大小和占有率关系 消息队列存储_队列_28

消息队列缓存大小和占有率关系 消息队列存储_kafka_29

3)Stricky粘性分配

从Kafka 0.11.x开始,引入此类分配策略,为了:分区分配尽可能均匀;在发生rebalance的时候,分区的分配尽可能与上一次分配保持相同。

没有发生rebalance时,Striky粘性分配策略和RoundRobin分配策略类似:

消息队列缓存大小和占有率关系 消息队列存储_数据_30

上面如果consumer2崩溃了,此时需要进行rebalance。如果是Range分配和轮询分配都会重新进行分配

消息队列缓存大小和占有率关系 消息队列存储_数据_31

consumer0和consumer1原来消费的分区大多发生了改变。采用粘性分配策略。

消息队列缓存大小和占有率关系 消息队列存储_队列_32

Striky粘性分配策略,保留rebalance之前的分配结果。这样,只是将原先consumer2负责的两个分区再均匀分配给consumer0、consumer1,这样可以明显减少系统资源的浪费。

3.2.3.3 offset的维护

由于consumer在消费过程中有可能出现断电宕机的故障,consumer恢复后,需要从故障前的位置继续消费,所以counsumer需要实时记录自己消费到了那一个offset,以便故障恢复后继续消费。

0.9 版之前,consumer默认将offset保存在zookeeper中,0.9版本之后,consumer默认将offset保存在kafka哟个内置的topic中,该topic为__comsumer_offsets

3.2.4 Kafka 高效读写数据
3.2.2.1 顺序写磁盘

kafkad的product生产数据,需要写入log中,写的过程是一直追加文件到文件末端,为顺序写,其中省去了大量的寻址时间

3.2.2.2 应用Pagecache(页面缓存)

Kafka数据持久化是直接持久化到Pagecache,先将读写缓存在内存中,然后组装成顺序读写写(操作系统提供)

  • I/O Scheduler 会将连续的小块写组装成大块的物理写从而提高性能
  • I/O Scheduler 会尝试将一些写操作重新按顺序排好,从而减少磁盘头的移动时间
  • 充分利用所有空闲内存(非 JVM 内存)。如果使用应用层 Cache(即 JVM 堆内存),会增加 GC 负担
  • 读操作可直接在 Page Cache 内进行。如果消费和生产速度相当,甚至不需要通过物理磁盘(直接通过 Page Cache)交换数据
  • 如果进程重启,JVM 内的 Cache 会失效,但 Page Cache 仍然可用

持久化到Pagecache上可能会造成宕机丢失数据的情况,但这可以被Kafka的Replication机制解决

3.2.2.3 零拷贝技术

消息队列缓存大小和占有率关系 消息队列存储_大数据_33

所有的缓存只发生了页面缓存,没有发生其他的拷贝。 操作系统提供的机制

3.2.5 Zookeeper 在kafka中的作用

Kakfa集群中有一个broker会被选举成为Controller(谁快,谁线在zookeeper中建立节点),负责管理集群broker的上下线,所有的topic的分区副本分片leader选举过程。Controller的管理工作都是依赖于Zookeeper的

消息队列缓存大小和占有率关系 消息队列存储_kafka_34

3.2.6 Kakfa事务

。Kafka事务指的是生产者生产消息以及消费者提交offset的操作可以在一个原子操作中,要么都成功,要么都失败。尤其是在生产者、消费者并存时,事务的保障尤其重要。(consumer-transform-producer模式)

消息队列缓存大小和占有率关系 消息队列存储_队列_35

3.2.6.1 Producer 事务

为了实现跨分区会话的事务,需要引入一个全局的唯一的Transaction ID,并将Producer获得PIDTransaction ID绑定。这样当Producer重启后就可以通过正在进行的Transaction ID获取原来的PID

为了管理Transaction,Kafka引入了一个新的组件Transaction Coordinator。Producer就是通过和Transaction Coordinator交互获得Transaction ID对应的任务状态。Transaction Coordinator还负责将事务所有写入Kafka的一个内部Topic,这样即使整个服务重启,由于事务状态得到保存,进行中的事务状态可以得到恢复,从而继续进行。

3.2.6.2 Consumer 事务

server对consumer没有控制能力,consumer通常不考虑。如果控制消费行为得用户自己实现业务逻辑

事务机制主要是从Producer方面考虑,对于Consumer而言,事务的保证就会相对较弱,尤其时无法保证Commit的信息被精确消费。这是由于Consumer可以通过offset访问任意信息,而且不同的Segment File生命周期不同,同一事务的消息可能会出现重启后被删除的情况。

如果想完成Consumer端的精准一次性消费,那么需要kafka消费端将消费过程和提交offset过程做原子绑定。此时我们需要将kafka的offset保存到支持事务的自定义介质中(比如mysql)。这部分知识会在后续项目部分涉及。

4 Kafka API

4.1 Producer API

Kafka的Producer发送消息采用的是异步发送的方式。在消息发送的过程中,涉及到了两个线程——main线程Sender线程(守护线程),以及一个线程共享变量——RecordAccumulator。main线程将消息发送给RecordAccumulator,Sender线程不断从RecordAccumulator中拉取消息发送到Kafka broker

消息队列缓存大小和占有率关系 消息队列存储_kafka_36

参数:

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<String, String> producerRecord = new ProducerRecord<>("test", null, i + "");
            //Future<RecordMetadata> 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();
    }
}
4.2 Consumer API
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);
            }
        }
    }
}