【kafka面试精华】--“精华篇“

1. 为什么需要kafka?

Kafka 是一个分布式的基于发布/订阅模式的消息队列。消息队列是一种先进先出的数据结构。

队列是一种先进先出的数据结构,分布式消息队列可以看做将这种数据结构部署到独立的服务器上,应用程序可以通过远程访问接口使用分布式消息队列,进行消息存取操作,进而实现分布式的异步调用。

消息生产者应用程序通过远程访问接口将消息推送给消息队列服务器,消息队列服务器将消息写入本地内存队列后立即返回成功后响应给消息生产者。消息队列服务器根据消息订阅列表查找订阅该消息的消息消费者应用程序,将消息队列中的消息按照先进先出的原则将消息通故宫远程通信接口发送给消息消费者程序。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6jAc6EQY-1653135100611)(assets/image-20200716121328500.png)]

常用的应用场景:业务服务模块解耦、超时业务和数据延迟处理、高并发限流削峰、异步通信等

1.1 业务模块解耦和异步通信

直接耦合解耦
在这里插入图片描述在这里插入图片描述

系统的耦合性越高,容错率就越低。如上左图,如果在处理的过程中发送邮件和发送短信业务逻辑出现异常,整个流程将会终止,很显然这种处理方式是不可行的。因此可以将相应的业务模块解耦出来,并使用消息中间件进行异步通信。由于消息发送者不需要等待消息的接收者处理数据就可以返回,因此系统具有更好的响应延迟;(注册功能不需要等待发送短信和发送邮件的响应结果,做到低延迟)

消息发送者和消息接收者并没有直接耦合,消息发送者将消息发送到分布式消息队列中即结束了对消息的处理,消息接收者从分布式消息队列中取消息进行后续操作。如果想新增业务模块,只要对该类消息感兴趣即可订阅该消息,对原有的系统没有任何影响,从而实现网站业务的可扩展设计。

比如下面的就是同步调用和异步调用

同步调用异步调用
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4PMg1Hce-1653135100616)(assets/image-20200716162945393.png)]在这里插入图片描述

1.2 高并发限流削峰

在不使用消息队列服务器的时候,用户的请求数据直接写入数据库,在高并发的情况下数据库压力剧增,使得响应速度变慢。但是在使用消息队列之后,用户的请求数据发送给消息队列之后立即返回,再由消息队列的消费者进程从消息队列中获取数据,异步写入数据库。

对于商品抢购这种某一时刻会有大量的请求到达服务端的情况,在这种高并发的情况下会给系统带来诸多问题如商品超卖,用户等待时间过长,数据不一致等等。因此在网站访问高峰,消息可以暂时存储在消息队列中等待接收者根据自身负载处理能力控制消息处理速度,减轻数据库等后端存储的压力。

1.3 业务延迟处理

实现消息的延时处理,比如一个用户下单,监测用户在30分钟付款了还是取消订单了,可以通过RabbitMQ延迟队列进行处理。

项目中使用kafka主要在于实现异步通信,用户点赞后,评论后,关注后生产者线程都会向该用户发送一个系统通知放入消息队列中,生活者便可以继续处理其他的任务了,接着消费者线程就会从消息队列中取出消息进行后续的处理,实现异步调用。如果一个贴子是热帖,那么生产者把消息发到消息队列中后,消费者线程慢慢处理即可,达到限流削峰的作用,同时也实现了生产者线程和消费者线程的解耦。

2. kafka的工作机制(执行过程)

  • 生产者会根据生产者的3条分区规则(指定partition;没有指明 partition 值但有 key;没有 partition 值又没有 key 值 )将消息追加到到特定主题topic的partition分区中对应的log日志文件尾端(为防止log日志数据追加过大,partition采用分片和索引机制,每次发送数据都会追加到segment分片的.log文件日志尾端)。
  • 记住,这些消息数据都会被放在partition分区的leader副本中,不过还需要等待ISR集合中的所有 follower 副本都同步完之后(kafka会更新partition分区leader副本的 HW高水位,其他follower 副本就是根据HW去同步消息的与leader保持一致),发送ack到生产者,才能被认为已经提交(默认ack=-1/all)。
  • 之后,消费者可以根据消费者的2个分区规则(RoundRobin消费者组轮循,Range主题分配)去消费分区leader副本HW高水位之前的消息,消费者只能拉取到HW之前的消息,之后每一次消费会根据消费者维护的offset偏移量去消费从上一次开始的那个offset位置。
    在这里插入图片描述

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

② partition分区:为了实现扩展性,一个非常大的 topic 可以分布到多个 broker(即服务器)上,一个 topic 可以分为多个 partition分区,每个 partition 是一个有序的队列;TopicA主题内有3个分区partition0~partition2分别分布在broker0~broker2上。 每个分区只能保证本分区的队列有序,即生产者和一个消费者对应一个partition分区。如果多个分区合一起并不能保证这些是有序的。

③ replica:为保证集群中的某个节点发生故障时, 该节点上的 partition 数据不丢失 , 且 kafka 仍然能够继续工作, kafka 提供了副本机制,一个 topic 的每个分区都有若干个副本,一个 leader 和若干个 follower,并且leader和follower需要分布在不同的服务器上。

④ leader和follower:生产者发送数据的对象,以及消费者消费数据的对象都是 leader,follower会实时的从 leader 中同步数据,保持和 leader 数据的同步。leader 发生故障时,某个 follower 会成为新的leader

⑤consumer group: 消费者组中由多个消费者组成,每个消费者负责消费不同分区内的数据, 同一个消费者组内的两个消费者不能消费同一分区内的内容。也就是说不同的消费者消费不同的分区消息。

⑥ 一个消息可以被多个消费者组消费,但是只能被一个消费者组里的一个消费者消费

3. Kafka提供的多分区多副本机制有何好处?

  • 1.kafka将某个topic主题划分多个partition分区,每个partition分区有一个leader副本和多个follower副本,但是leader和follower要放在不同的Broker服务器上。这样做的目的是多分区可以提高kafka负载均衡能力,不用只靠一个broker服务器的一个partition分区存放消息。同时也提高了生产消息和消费消息的并发度

  • 2.kafka从0.8版本开始为partition分区引入了多副本机制。指的是分布式系统对数据和服务提供的冗余方式。数据副本是指在不同的broker服务器上持久化同一份数据,当某一个broker服务器上存储的数据丢失时,可以从副本上读取该数据。

    • 副本是相对于分区而言的,即副本是特定分区的副本。一个partition分区包含一个leader副本和多个follower副本。生产者和消费者都是从leader副本去发送和获取消息。
    • 各个副本位于不同的broker服务器中,leader和follower要放在不同的Broker服务器上。只有leader副本对外提供服务,follower副本只负责数据同步。
    • 分区中的所有副本统称为 AR;而ISR 是指与leader 副本保持同步状态的follower副本集合,当然leader副本本身也是这个ISR集合中的一员。
    • LEO标识每个分区中每个副本的最后一个offset ,分区的每个副本都有自己的LEO;每个分区ISR中所有副本中最小的LEO ,即为HW,俗称高水位,消费者只能拉取到HW之前的消息。
  • 多副本机制是通过增加副本数量来提升数据容灾能力的。当leader崩溃后,kafka会选择该分区的一个follower副本成为新的leader副本,其他follower会根据新的leader的HW高水位去同步消息。即使原leader副本恢复后也会成为follower副本。

4. kafka的消息模型

传统的消息队列模型分为点对点模式和发布订阅模式,Kafka是基于发布订阅模式的。

① 点对点模式(队列模型):一对一,消费者主动拉取数据,消息收到后消息清除

使用队列(Queue)作为消息通信载体,消息生产者生产消息发送到Queue中, 然后消息消费者从Queue中取出并且消费消息。消息被消费以后, queue 中不再有存储,所以消息消费者不可能消费到已经被消费的消息。Queue 支持存在多个消费者,但是对一个消息而言,只会有一个消费者可以消费。 比如:我们生产者发送 100 条消息的话,两个消费者来消费一般情况下两个消费者会按照消息发送的顺序各自消费一半(也就是你一个我一个的消费。)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8hBZUj6o-1653135100620)(assets/image-20200719233931651.png)]

这种队列模型存在的问题:

假如我们存在这样一种情况:我们需要将生产者产生的消息分发给多个消费者,某一个消费者将消息消费后,该消息就会清除,那么其他消费者再想消费这个消息就不行了。

② 发布订阅模式(消费模型):一对多,消费者消费数据之后不会清除消息

消息生产者将消息发布到 topic 中,同时有多个消息消费者消费该消息。和点对点方式不同,发布到 topic 的消息会被所有订阅者消费。 也就是一次消息写入多次消费,但是前提是这些消费者位于不同的消费者组中。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DJuuaQzT-1653135100621)(assets/image-20200719233425556.png)]

发布订阅模型使用主题(Topic) 作为消息通信载体,类似于订阅模式;发布者发布一条消息,该消息通过主题传递给所有的订阅者,在一条消息广播之后才订阅的用户则是收不到该条消息的。

RocketMQ 的消息模型和 Kafka 基本是完全一样的。唯一的区别是Kafka中没有队列这个概念,与之对应的是 Partition(分区)。

5. kafka消息的存储机制(日志关系)

kafka的消息是存储在磁盘的,默认保存7天,所以数据不易丢失

(1)Log日志命名规则<topic>-<partition> (主题+分区序号):假设有一个名为“topic-log”的主题,此主题中具有 4 个分区,那么在实际物理存储上表现为“topic-log-0”“topic-log-1”“topic-log-2”“topic-log-3”这4个文件夹:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-59lcPz1G-1653135100622)(assets/image-20200724162911554.png)]

每个 partition分区 对应于一个 log日志文件,该 log日志文件中存储的就是 producer 生产的数据。 Producer 生产的数据会被不断追加到该log 文件末端,且每条数据都有自己的 offset。

消费者组中的每个消费者, 都会实时记录自己消费到了哪个 offset,以便出错恢复时,从上次的位置继续消费。 由于生产者生产的消息会不断追加到 log日志文件末尾, 为防止 log 文件过大导致数据定位效率低下, Kafka 采取了分片和索引机制,将每个 partition 分为多个 segment。 每个 segment对应两个文件——“.index”文件和“.log”文件。 .index文件中的元数据指向对应的.log文件中消息的物理偏移地址。

首先00000000000000000000.log是最早产生的文件,该文件达到1G后又产生了新的00000000000009084544.log文件,新的数据会写入到这个新的文件里面。这个文件到达1G后,数据又会写入到下一个文件中。也就是说它只会往文件的末尾追加数据,这就是顺序写的过程,生产者只会对每一个partition做数据的追加。

向Log 中追加消息时是顺序写入的,只有最后一个 Segment 才能执行写入操作,在此之前所有的Segment 都不能写入数据。为了方便描述,我们将最后一个Segment 称为“activeSegment”,即表示当前活跃的日志分段。随着消息的不断写入,当activeSegment满足一定的条件时,就需要创建新的activeSegment,之后追加的消息将写入新的activeSegment。

每个 partition (它会对应一个log文件,生产者生产的数据都会追加到og文件),当log文件过大,每个 partition 分为多个 segment,每个 segment再分为“.index”文件和“.log”文件。

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ofxniTPa-1653135100623)(assets/image-20200724154236204.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kxlvd19L-1653135100624)(assets/image-20200720095815490.png)]

存储日志关系:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KCTxb9yF-1653135100625)(assets/image-20200724163132969.png)]

(2)为了便于消息的检索,每个Segment中都对应两个文件:存储消息数据文件(以“.log”为文件后缀)和偏移量索引文件(以“.index”为文件后缀)日志文件和索引文件都是根据 文件名是上一组最后一条消息的offset+1命名的.比如第一个Segment的基准偏移量为0,对应的日志文件为00000000000000000000.log,索引文件00000000000000000000.index。从第二组开始, 文件名是上一组最后一条消息的offset+1。log文件是以追加的方式存储。index文件存储的是稀疏索引, 并没有存储所有日志的偏移量。

如果第2个Segment-1对应的基准位移是6,也说明了该Segment中的第一条消息的偏移量为6(Message-6)。同时可以反映出第1个Segment-0中共有6条消息(偏移量从0至5的消息),最后一条消息偏移量offset=5(Message-5)。则文件命名:00000000000000000006.log,00000000000000000006.index(上一组最后一条Message消息的offset+1)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bjoyTww2-1653135100627)(assets/image-20200724160416600.png)]

关于Offset的说明:在parition 内的每条消息都有一个有序的id号,这个id号被称为偏移(offset),它可以唯一确定每条消息在parition 内的位置。即offset表示partiion的第多少message

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Z10FJIRu-1653135100628)(assets/image-20200720103542962.png)]

6. kafka高效读写数据

① 顺序写入磁盘

Kafka在设计的时候,采用了文件追加的方式来写入消息,即只能在日志文件的尾部追加新的消息,并
且不允许修改已经写入的消息,这种方式属于典型的顺序写入此判断的操作,所以就算是Kafka使用磁
盘作为存储介质,所能实现的额吞吐量也非常可观。

所以我们看到的segment目录中的log文件都是顺序写入的,每条消息都有对应的offset。

② 页缓存

Kafka中大量使用页缓存,这页是Kafka实现高吞吐的重要因素之一。 所谓页缓存就是每次从磁盘中加载一页的数据到内存中可以减少IO的次数。

③ 零拷贝技术

“零拷贝技术”只用将磁盘文件的数据复制到页面缓存中一次,然后将数据从页面缓存直接发送到网络中(发送给不同的订阅者时,都可以使用同一个页面缓存),避免了重复复制操作。如果有10个消费者,传统方式下,数据复制次数为4*10=40次,而使用“零拷贝技术”只需要1+10=11次,一次为从磁盘复制到页面缓存,10次表示10个消费者各自读取一次页面缓存。

7. Kafka消息生产者流程

消息发送的过程中,涉及到两个线程协同工作,主线程首先将业务数据封装成ProducerRecord对象,之后调用send()方法将消息放入RecordAccumulator(消息收集器,也可以理解为主线程与Sender线程直接的缓冲区)中暂存Sender线程负责将消息信息构成请求,并最终执行网络I/O的线程,它从RecordAccumulator中取出消息并批量发送出去,需要注意的是,KafkaProducer是线程安全的,多个线程间可以共享使用同一个KafkaProducer对象

batch.size: 只有数据积累到 batch.size 之后, sender 才会发送数据到partition分区。
linger.ms: 如果数据迟迟未达到 batch.size, sender 等待 linger.time 之后就会发送数据到partition分区。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Q7hn0iEU-1653135100629)(assets/image-20200719223340509.png)]
public ProducerRecord(String topic, Integer partition, Long timestamp, K key, V value, Iterable<Header> headers) {
		    //省略参数校验.....
            this.topic = topic;
            this.partition = partition;
            this.key = key;
            this.value = value;
            this.timestamp = timestamp;
            this.headers = new RecordHeaders(headers);
        }
    }
}
  1. 首先要构造一个 ProducerRecord 对象,该对象可以声明主题Topic、分区Partition、键 Key以及
    值 Value,主题和值是必须要声明的,分区和键可以不用指定。
  2. 调用send() 方法进行消息发送。
  3. 因为消息要到网络上进行传输,所以必须进行序列化,序列化器的作用就是把消息的 key 和
    value对象序列化成字节数组。
  4. 接下来数据传到分区器,如果之间的 ProducerRecord 对象指定了分区,那么分区器将不再做
    任何事,直接把指定的分区返回;如果没有,那么分区器会根据 Key 来选择一个分区,选择好分区之
    后,生产者就知道该往哪个主题和分区发送消息了。
  5. 接着这条消息会被添加到一个消息批次(batch.size用来指定批次的大小,以字节为单位)里面,这个批次里所有的消息会被发送到相同的主题和分区。会有一个独立的线程来把这些消息批次发送到相应的 Broker 上。
  6. Broker成功接收到消息,表示发送成功,返回消息的元数据(包括主题和分区信息以及记录在
    分区里的偏移量)。发送失败,可以选择重试或者直接抛出异常。(ack返回确认机制)

生产者分区原则:

  1. 如果指明 partition 的情况下,直接将指明的值直接作为 partiton 值 。
  2. 没有指明 partition 值但有 key 的情况下,Kafka根据传递消息的key来进行分区的分配,即hash(key) % numPartitions。将 key 的 hash 值与 topic 的 partition数进行取余得到 partition 值;
  3. 既没有 partition 值又没有 key 值的情况下,第一次调用时随机生成一个整数(后面每次调用在这个整数上自增),将这个值与 topic 可用的 partition 总数取余得到 partition值。
/**
 * Kafka 消息生产者
 */
public class ProducerFastStart {
    // 主题 
    private static final String topic = "heima";
    public static void main(String[] args) {
        Properties properties = new Properties();
        // 设置key序列化器
        properties.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
        // 设置value序列化器
        properties.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
        // 设置重试次数
        properties.put(ProducerConfig.RETRIES_CONFIG, 10);
        // 设置kafka服务器地址
        properties.put("bootstrap.servers", "192.168.157.130:9092");
        
        //创建ProducerRecord对象
        ProducerRecord<String, String> record = new ProducerRecord<>(topic, "Kafka-demo-001", "hello, Kafka!");
        //创建KafkaProducer对象,发送消息
        KafkaProducer<String, String> producer = new KafkaProducer<>(properties);
        try {
            producer.send(record);
        } catch (Exception e) {
            e.printStackTrace();
        }
        producer.close();
    }
}

8. kafka发送消息的三种方式

① 发送即忘记:这种方式是不管发送成功与否,客户端都会返回成功,不管发送结果,存在数据丢失的风险

public class CustomProducer {
    public static void main(String[] args) throws ExecutionException,InterruptedException {
        Properties props = new Properties();
        props.put("bootstrap.servers", "192.168.157.130:9092");//kafka 集群, broker-list
        props.put("acks", "all");
        props.put("retries", 1); //重试次数
        props.put("batch.size", 16384);//批次大小
        props.put("linger.ms", 1); //等待时间
        props.put("buffer.memory", 33554432); //RecordAccumulator 缓冲区大小
        props.put("key.serializer","org.apache.kafka.common.serialization.StringSerializer");
        props.put("value.serializer","org.apache.kafka.common.serialization.StringSerializer");
        Producer<String, String> producer = new
        KafkaProducer<>(props);
        for (int i = 0; i < 100; i++) {
            producer.send(new ProducerRecord<String, String>("first",
            Integer.toString(i), Integer.toString(i)));
        }
            producer.close();
        }
	}
}

② 同步发送:通过send()发送完消息后返回一个Future对象,然后调用Future对象的get()方法等待kafka响应,如果kafka正常响应,返回一个RecordMetadata对象,该对象存储消息的偏移量, 如果kafka发生错误,无法正常响应,就会抛出异常,我们便可以进行异常处 。

同步发送的意思就是,一条消息发送之后,会阻塞当前线程, 直至返回 ack。由于 send 方法返回的是一个 Future 对象,根据 Futrue 对象的特点,我们也可以实现同步发送的效果,只需在调用 Future 对象的 get 方发即可。

public static void main(String[] args) throws ExecutionException,InterruptedException {
    Properties props = new Properties();
    props.put("bootstrap.servers", "192.168.157.130:9092");//kafka 集群, broker-list
    props.put("acks", "all");
    props.put("retries", 1);//重试次数
    props.put("batch.size", 16384);//批次大小
    props.put("linger.ms", 1);//等待时间
    props.put("buffer.memory", 33554432);//RecordAccumulator 缓冲区大小
    props.put("key.serializer","org.apache.kafka.common.serialization.StringSerializer");
    props.put("value.serializer","org.apache.kafka.common.serialization.StringSerializer");
    Producer<String, String> producer = new KafkaProducer<>(props);
    for (int i = 0; i < 100; i++) {
        producer.send(new ProducerRecord<String, String>("first",
        Integer.toString(i), Integer.toString(i))).get();
    }
        producer.close();
    }
}

③ 异步发送:回调函数会在 producer 收到 ack 时调用,为异步调用, 该方法有两个参数,分别是RecordMetadata 和 Exception,如果 Exception 为 null,说明消息发送成功,如果Exception 不为 null,说明消息发送失败。

带 new Callback()返回值函数的异步调用,可以不添加也行。

注意:消息发送失败会自动重试,不需要我们在回调函数中手动重试。

public class CustomProducer {
    public static void main(String[] args) throws ExecutionException,InterruptedException {
        Properties props = new Properties();
        props.put("bootstrap.servers", "hadoop102:9092");//kafka 集群, broker-list
        props.put("acks", "all");
        props.put("retries", 1);//重试次数
        props.put("batch.size", 16384);//批次大小
        props.put("linger.ms", 1);//等待时间
        props.put("buffer.memory", 33554432);//RecordAccumulator 缓冲区大小
        props.put("key.serializer","org.apache.kafka.common.serialization.StringSerializer");
        props.put("value.serializer","org.apache.kafka.common.serialization.StringSerializer");
        Producer<String, String> producer = new KafkaProducer<>(props);
        for (int i = 0; i < 100; i++) {
            producer.send(new ProducerRecord<String, String>("first",Integer.toString(i), Integer.toString(i)), new Callback() {
                //回调函数, 该方法会在 Producer 收到 ack 时调用,为异步调用
                @Override
                public void onCompletion(RecordMetadata metadata, Exception exception) {
                    if (exception == null) {
                        System.out.println("success->" +
                        metadata.offset());
                    } else {
                        exception.printStackTrace();
                    }
                }
            });
        }
        producer.close();
    }
}

9.为什么消费者用 pull 模式从 broker 中读取数据?

consumer 采用 pull(拉) 模式从 broker 中读取数据

  • push(推)模式很难适应消费速率不同的消费者**,因为消息发送速率是由 broker 决定的。它的目标是尽可能以最快速度传递消息,但是这样很容易造成 consumer 来不及处理消息, 典型的表现就是拒绝服务以及网络拥塞。
  • pull 模式则可以根据 consumer 的消费能力以适当的速率消费消息。pull 模式不足之处是,如果 kafka 没有数据,消费者可能会陷入循环中, 一直返回空数据。 针对这一点, Kafka 的消费者在消费数据时会传入一个时长参数 timeout,如果当前没有数据可供消费, consumer 会等待一段时间之后再返回,这段时长即为timeout。

10. Kafka如何保证消息不堆积?

我们从Kafka中读取消息,并且进行检查,最后产生结果数据。我们可以创建一个消费者实例去做这件事情,但如果生产者写入消息的速度比消费者读取的速度快怎么办呢?这样随着时间增长,消息堆积越来越严重。对于这种场景,我们需要增加多个消费者来进行水平扩展。

① T1中有四个分区Patition0~Patition3,而消费者组G1中只有一个消费者C1,那么消费者C1将会收到这4个分区的消息

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-r7YFvTYG-1653135100631)(assets/image-20200719220420235.png)]

② 如果我们向G1中再增加一个C2,那么每个消费者将会分别收到两个分区的消息

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DvAccYq6-1653135100632)(assets/image-20200719221045478.png)]

③ 如果增加到4个消费者,那么每个消费者将会分别收到一个分区的消息

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TCAX2m7y-1653135100633)(assets/image-20200719221116897.png)]

④ 但如果我们继续增加消费者到这个消费组,剩余的消费者将会空闲,不会收到任何消息,因为消费者组内的每个消费者负责消费不同分区的消息,同一个消费者组内的两个消费者不能消费同一个分区的内容。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-smzAHpCW-1653135100633)(assets/image-20200719221147141.png)]

Kafka一个很重要的特性就是,只需写入一次消息,可以支持任意多的应用(不同的消费者组)读取这个消息。换句话说,每个应用都可以读到全量的消息。但是这些应用需要有不同的消费组。假如我们新增了一个新的消费组,而这个消费组有两个消费者,那么会是这样的:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Fm7wvI7O-1653135100634)(assets/image-20200719224045248.png)]

在这个场景中,消费组G1和消费组G2都能收到T1主题的全量消息,在逻辑意义上来说它们属于不同的应用。

kafka消息队列中,消费者是以消费者组消费消息的,所以只需写入一次消息,不同的消费者组都可以获取到主题下partition分区的消息。然后也是以消费者组内的消费者进行分区分配策略的

11. Kafka消费者组再均衡

再均衡是指分区的所属从一个消费者转移到另外一个消费者的行为,它为消费组具备了高可用性和伸缩性提供了保障,使得我们既方便又安全地删除消费组内的消费者或者往消费组内添加消费者。不过再均衡发生期间,消费者是无法拉取消息的 。

换句话说:当我们向消费者组内添加一个消费者,或者从消费者组内移除一个消费者时,都需要经过再均衡,这样就会导致分区的所属从一个消费者转移到了另一个消费者。如图:partition-0一开始是被Consumer 1消费的,当消费者内再加入一个消费者Consumer 2时,由于再均衡,partition-0便由Consumer 2 消费了。

一个消费者两个消费者
在这里插入图片描述在这里插入图片描述

12. kafka消费者提交offset方式

对于Kafka中的partition分区而言,它的每条消息都有唯一的offset,用来表示消息在分区中的位置(segment分片.index文件存储的索引就对应每一个.log文件中消息的位置),也保证了每个分区消息的局部有序性,但是多个分区做不到全局有序性 。

对于消费者而言,也有一个offset的概念,用来表示消费到分区中某个消息所在的位置。在每次调用poll()方法时,它返回的是还没有被消费过的消息集,因此需要知道上一次消费时的消费位移(offset),并且这个offset必须做持久化保存,而不是单单的保存在内存中,否则消费者重启之后就无法知道之前的消费位移了。再考虑一种情况,当有新的消费者加入时,那么必然会有再均衡的动作,对于同一分区而言,它可能在再均衡动作之后分配给新的消费者,如果不持久化保存消费位移,那么这个新的消费者也无法知晓之前的消费位移。

在旧消费者客户端中,消费位移是存储在ZooKeeper中的。而在新消费者客户端中,消费位移存储在Kafka内部的主题__consumer_offsets中。这里把将消费位移存储起来(持久化)的动作称为“提交”,消费者在消费完消息之后需要执行消费位移的提交。

① 自动提交Offset:自动提交 offset 的相关参数

在 Kafka 中默认的消费位移的提交方式是自动提交,这个由消费者客户端参数enable.auto.commit 配置,默认值为 true。当然这个默认的自动提交不是每消费一条消息就提交一次,而是定期提交,这个定期的周期时间由客户端参数auto.commit.interval.ms配置,默认值为5秒,此参数生效的前提是enable.auto.commit参数为true。在代码清单3-1中并没有展示出这两个参数,说明使用的正是默认值。

enable.auto.commit: 是否开启自动提交 offset 功能
auto.commit.interval.ms: 自动提交 offset 的时间间隔

public class CustomConsumer {
    public static void main(String[] args) {
        Properties props = new Properties();
        props.put("bootstrap.servers", "192.168.157.130:9092");
        //消费者隶属于的消费组,默认为空,如果设置为空,则会抛出异常,这个参数要设置成具有一定业务含义的名称
        props.put("group.id", "test");
        //自动提交
        props.put("enable.auto.commit", "true");
        props.put("auto.commit.interval.ms", "1000");
        //反序列化
        props.put("key.deserializer",
		"org.apache.kafka.common.serialization.StringDeserializer");
        props.put("value.deserializer",
        "org.apache.kafka.common.serialization.StringDeserializer");
        KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
        //创建完消费者后我们便可以订阅主题了,只需要通过调用subscribe()方法即可,这个方法接收一个主题列表
        consumer.subscribe(Arrays.asList("first"));
        while (true) {
            //返回值表示一次拉取操作锁获得的消息集
       	 	ConsumerRecords<String, String> records =consumer.poll(100);
        	for (ConsumerRecord<String, String> record : records)
        	System.out.printf("offset = %d, key = %s, value = %s%n", record.offset(), record.key(), record.value());
   	 	}
	}
}

② 手动提交:

手动提交 offset 的方法有两种:分别是 commitSync(同步提交) 和 commitAsync(异步提交) 。两者的相同点是,都会将本次 poll 的一批数据最高的偏移量提交;不同点是,commitSync 阻塞当前线程,一直到提交成功,并且会自动失败重试(由不可控因素导致,也会出现提交失败);而 commitAsync 则没有失败重试机制,故有可能提交失败。

同步提交方式:

public class CustomComsumer {
    public static void main(String[] args) {
        Properties props = new Properties();
        //Kafka 集群
        props.put("bootstrap.servers", "hadoop102:9092");
        //消费者组,只要 group.id 相同,就属于同一个消费者组
        props.put("group.id", "test");
        props.put("enable.auto.commit", "false");//关闭自动提交 offset
        props.put("key.deserializer",
        "org.apache.kafka.common.serialization.StringDeserializer");
        props.put("value.deserializer",
        "org.apache.kafka.common.serialization.StringDeserializer");
        KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
        consumer.subscribe(Arrays.asList("first"));//消费者订阅主题
        while (true) {
            //消费者拉取数据
            ConsumerRecords<String, String> records = consumer.poll(100);
            for (ConsumerRecord<String, String> record : records) {
            	System.out.printf("offset = %d, key = %s, value = %s%n", record.offset(), record.key(), record.value());
            }
            //同步提交,当前线程会阻塞直到 offset 提交成功
            consumer.commitSync();
        }
    }
}

异步提交方式:

虽然同步提交 offset 更可靠一些,但是由于其会阻塞当前线程,直到提交成功。因此吞吐量会收到很大的影响。因此更多的情况下,会选用异步提交 offset 的方式。

public class CustomConsumer {
    public static void main(String[] args) {
        Properties props = new Properties();
        //Kafka 集群
        props.put("bootstrap.servers", "hadoop102:9092");
        //消费者组,只要 group.id 相同,就属于同一个消费者组
        props.put("group.id", "test");
        //关闭自动提交 offset
        props.put("enable.auto.commit", "false");
        props.put("key.deserializer",
                  "org.apache.kafka.common.serialization.StringDeserializer");
        props.put("value.deserializer",
                  "org.apache.kafka.common.serialization.StringDeserializer");
        KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
        //消费者订阅主题
        consumer.subscribe(Arrays.asList("first"));
        while (true) {
            //消费者拉取数据
            ConsumerRecords<String, String> records = consumer.poll(100);
            for (ConsumerRecord<String, String> record : records) {
                System.out.printf("offset = %d, key = %s, value = %s%n", record.offset(), record.key(), record.value());
            }
            //异步提交
            consumer.commitAsync(new OffsetCommitCallback() {
                @Override
                public void onComplete(Map<TopicPartition,OffsetAndMetadata> offsets, Exception exception) {
                    if (exception != null) {
                        System.err.println("Commit failed for" +offsets);
                    }
                }    
            });
        }
    }
}

无论是同步提交还是异步提交 offset,都有可能会造成数据的漏消费或者重复消费。先提交 offset 后消费,有可能造成数据的漏消费;而先消费后提交 offset,有可能会造成数据的重复消费。

13. kafka如何保证消息的顺序消费?

比如说生产者生产了0到100个商品,消费者在消费的时候按照0到100这个从小到大的顺序消费,kafka如何保证这种有序性呢?

其实kafka集群是无法做到消费者在消费消息的时候全局有序,因为我们在创建topic时,会设置不同的分区并存储在不同的服务器上,生产者生产的消息是有序的,但是经过分区策略后会被分到不同的partition中,尽管partition中的数据时顺序存储的,但是并不是全局有序的,只能走到局部有序。

比如生产者生产出100条数据之后,通过一定的分组策略存储到broker的partition中的时候,0到10这10条消息被存到了partition-0中,10到20这10条消息被存到了partition-1中,这样当消费者消费的时候从partition中取出消息并消费时便是无序的。

那么能否做到消费者在消费消息的时候全局有序呢?

① 在大多数情况下是做不到全局有序的。但在某些情况下是可以做到的。如我的partition只有一个,这种情况下是可以全局有序的。可是只有一个partition的话,哪里来的分布式呢?哪里来的负载均衡呢?所以说,全局有序是一个伪命题!全局有序根本没有办法在kafka要实现的大数据的场景来做到。但是我们只能保证当前这个partition内部消息消费的有序性。

② 在发送消息的时候指定key和partition,那么这些消息就会被发送到指定的partition中,从而保证了有序性。

结论:一个partition中的数据是有序的吗?回答:间隔有序,不连续。针对一个topic里面的数据,只能做到partition内部有序,不能做到全局有序。

消息在被追加到 Partition(分区)的时候都会分配一个特定的偏移量(offset)。Kafka 通过偏移量(offset)来保证消息在分区内的顺序性。

14.数据可靠性的保证

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

在这里插入图片描述

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PyFzIqzH-1653135100638)(assets/image-20200726141001164.png)]

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 就会给 follower 发送 ack。如果 follower长 时 间 未 向 leader 同 步 数 据 , 则 该 follower 将 被 踢 出 ISR , 该 时 间 阈 值 由replica.lag.time.max.ms 参数设定。 Leader 发生故障之后,就会从 ISR 中选举新的 leader。

14.1 kafka如何保证消息不丢失?

1. 生产者

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

我们讨论了消息发送的3种模式,即发后即忘、同步和异步。对于发后即忘的模式,不管消息有没有被成功写入分区,生产者都不会收到通知,那么即使消息写入失败也无从得知,因此发后即忘的模式不适合高可靠性要求的场景。如果要提升可靠性,那么生产者可以采用同步或异步的模式,在出现异常情况时可以及时获得通知,以便可以做相应的补救措施,比如选择重试发送(可能会引起消息重复)。

问题1:什么是acks?

这个参数用来指定分区中必须有多少个副本收到这条消息,之后生产者才会认为这条消息时写入成功的。acks是生产者客户端中非常重要的一个参数,它涉及到消息的可靠性和吞吐量之间的权衡。

问题2:何时发送ack?

确保leader和follower同步完成,才能给producer发送ack,这样才能保证leader挂掉之后能从新的follower中选出新的leader。

问题3:多少个follower同步完成后发送ack?

全部完成同步(ISR中的follower副本,该集合也包括leader),才发送 ack,选举新的 leader 时, 容忍 n 台 节点的故障,需要 n+1 个副 本。

生产者方面 ack应答机制:

对于某些不太重要的数据,对数据的可靠性要求不是很高,能够容忍数据的少量丢失,所以没必要等 ISR 中的 follower 全部接收成功。所以 Kafka 为用户提供了三种可靠性级别,用户根据对可靠性和延迟的要求进行权衡,
选择以下的配置。

  1. ack=0:生产者给partition分区发送消息后,不会等待broker服务器的ack确认机制返回(生产者发完消息就不再接收ack,不管事了)。如果出现问题生产者是感知不到的,消息就丢失了(可能服务器还没有写入磁盘就宕机了,就会丢失数据生产者也不会再重发丢失的消息)。不过因为生产者不需要等待服务器响应,所以它可以以网络能够支持的最大速度发送消息,从而达到很高的吞吐量。 记住,生产者不会再重新发送消息
  2. ack=1:生产者将消息发送到leader副本,leader副本在成功写入本地log日志之后会告知生产者已经成功提交(producer 会等待 partition 的 leader 写入成功后返回的 ack),如果此时ISR集合的follower副本还没来得及拉取到leader中新写入的消息,leader就宕机了,那么此次发送就会丢失数据(ISR的follower副本会竞选leader副本,但是新的leader数据是缺失的)。记住,当数据丢失后producer会对leader重新发送该数据,但是也不能保证数据丢失。
  3. ack=-1:producer 会等待partition 的 leader 和 follower副本全部落盘成功后才返回 ack(即ISR中的follower副本全部同步leader成功,返回ack)。但是如果在 follower 同步完成后, broker 发送 ack 之前, leader 发生故障,那么会造成数据重复(follower副本竞选新的leader,然后返回ack到生产者,之后producer会重新发送故障的数据,造成数据重复) 。ack=-1可以最大程度地提高消息的可靠性,但是数据可能重复。记住,leader故障,生产者会重新发送数据。
2. 消费者

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HUWwuYJG-1653135100638)(assets/image-20200720164812811.png)]

先提交offset再消费:当前一次poll()操作所拉取的消息集为[x+2,x+7],x+2代表上一次提交的消费位移,说明已经完成了x+1之前(包括x+1在内)的所有消息的消费,x+5表示当前正在处理的位置。如果拉取到消息之后就进行了位移提交,即提交了x+8,那么当前消费x+5的时候遇到了异常,在故障恢复之后,我们重新拉取的消息是从x+8开始的。也就是说,x+5至x+7之间的消息并未能被消费,如此便发生了消息丢失的现象。

先消费再提交offset:再考虑另外一种情形,位移提交的动作是在消费完所有拉取到的消息之后才执行的,那么当消费x+5的时候遇到了异常(在消息处理之后且在位移提交之前消费者宕机了),在故障恢复之后,我们重新拉取的消息是从上一次提交的消费位移 x+2开始的,而不是异常的x+5,因为该消费位移还未来得及提交。也就是说,x+2至x+4之间的消息又重新消费了一遍,故而又发生了重复消费的现象。

但是如果我们只是想保证消息不丢失,可以采用先消费再提交offset,可以将提交Offset的方式改成手动提价的方式,即每次消费完拉取的消息之后再进行提交Offset,这样可以避免因为拉取的消息还未消费便出先异常,从而导致消息丢失的情况。

14.2 kafka如何保证消息不重复消费?

什么情况下会产生重复消费的情形?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PHClTA5p-1653135100639)(assets/image-20200720164812811.png)]

自动提交offset造成重复消费:

因为自动提交并不是每消费一条消息便提交一次Offset,而是定期提交,这个定期的周期时间由客户端参数auto.commit.interval.ms配置,默认值为5秒,也就是说,假设刚刚提交完一次消费位移,然后拉取一批消息进行消费,在下一次自动提交消费位移之前,消费者崩溃了,那么又得从上一次位移提交的地方重新开始消费,这样便发生了重复消费的现象。

手动提交offset造成重复消费:

这种情况是因为异步提交Offset的原因,commitAsync()提交的时候会有失败的情况发生,此时就会进行重试。如果某一次异步提交的消费位移为 x,但是提交失败了,然后下一次又异步提交了消费位移为 x+y,这次成功了。如果这里引入了重试机制,前一次的异步提交的消费位移在重试的时候提交成功了,那么此时的消费位移又变为了 x。如果此时发生异常,那么恢复之后的消费者就会从x处开始消费消息,这样就发生了重复消费的问题。

如何解决?

单个会话单个Partition分区:可以采用Exactly Once 精准唯一性 语义,保证数据不重复也不丢失。At Least Once + 幂等性 = Exactly Once ,开启幂等性的 Producer 在初始化的时候会被分配一个 PID,发往同一 Partition 的消息会附带 Sequence Number序列号。而Broker 端会对<PID, Partition, SeqNumber>做缓存,当具有相同Partition主键的消息提交时, Broker 只会**持久化一条。**但是 PID 重启就会变化,同时不同的 Partition 也具有不同主键,所以幂等性无法保证跨分区跨会话的 Exactly Once。

跨分区跨会话:可以采用事务保证唯一性(原子性)。

15. broker宕机后如何保证副本间数据的一致性?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bmTSjZe1-1653135100640)(assets/image-20200726153509262.png)]

LEO:指的是分区中每个副本最大的 offset;
HW:指的是消费者能见到的最大的 offset, ISR 队列中所有副本最小的 LEO

(1) follower 故障
follower 发生故障后会被临时踢出 ISR,待该 follower 恢复后, follower 会读取本地磁盘记录的上次的 HW,并将 log 文件高于 HW 的部分截取掉,从 HW 开始向 leader 进行同步。等该 follower 的 LEO 大于等于该 Partition 的 HW,即 follower 追上 leader 之后,就可以重新加入 ISR 了。

(2) leader 故障
leader 发生故障之后,会从 ISR 中选出一个新的 leader,之后,为保证多个副本之间的数据一致性, 其余的 follower 会先将各自的 log 文件高于 HW 的部分截掉,然后从新的 leader同步数据。“多退少补”

注意: 这只能保证副本之间的数据一致性,并不能保证数据不丢失或者不重复。

16.事务

16.1 消息传输保障

消息中间件的消息传输保障有3个层级,分别如下。

(1)at most once:至多一次。消息可能会丢失,但绝对不会重复传输。同服务器 ACK 级别设置为 0, 保证生产者每条消息只会被发送一次,即 At Most Once 语义 .

(2)at least once:最少一次。消息绝不会丢失,但可能会重复传输。同服务器的 ACK 级别设置为-1,

(3)exactly once:恰好一次。每条消息肯定会被传输一次且仅传输一次。精准唯一性At Least Once + 幂等性 = Exactly Once .

生产者:Kafka 的消息传输保障机制非常直观。当生产者向 Kafka 发送消息时,一旦消息被成功提交到日志文件,由于多副本机制的存在,这条消息就不会丢失。如果生产者发送消息到 Kafka之后,遇到了网络问题而造成通信中断,那么生产者就无法判断该消息是否已经提交。虽然Kafka无法确定网络故障期间发生了什么,但生产者可以进行多次重试来确保消息已经写入 Kafka,这个重试的过程中有可能会造成消息的重复写入,所以这里 Kafka 提供的消息传输保障为 at least once。ack=-1,数据重复

消费者:消费者处理消息和提交消费位移的顺序在很大程度上决定了消费者提供哪一种消息传输保障。

如果消费者在拉取完消息之后,应用逻辑先处理消息后提交消费位移,那么在消息处理之后且在位移提交之前消费者宕机了,待它重新上线之后,会从上一次位移提交的位置拉取(而本次消费位移还未提交,又得重新消费这些消息),这样就出现了重复消费,因为有部分消息已经处理过了只是还没来得及提交消费位移,此时就对应at least once。

如果消费者在拉完消息之后,应用逻辑先提交消费位移后进行消息处理,那么在位移提交之后且在消息处理完成之前消费者宕机了,待它重新上线之后,会从已经提交的位移处开始重新消费,但之前尚有部分消息未进行消费(当前offset位移x+2,提交消费位移offset=x+8,消费消息到offset=x+5处还未消费到x+8,消费者宕机了,下一次直接从提交的消费位移x+8处开始消费),如此就会发生数据丢失,此时就对应at most once。

Kafka从0.11.0.0版本开始引入了幂等和事务这两个特性,以此来实现exactly once,精准唯一性。

16.2 幂等(单分区单个生产者会话)

生产者在进行重试的时候有可能会重复写入消息,而使用Kafka的幂等性功能之后就可以避免这种情况。

  • 为了实现生产者的幂等性,Kafka为此引入了producer id(以下简称PID)和序列号(sequence number)这两个概念。每个新的生产者实例在初始化的时候都会被分配一个PID。对于每个PID,消息发送到的每一个分区都有对应的序列号,这些序列号从0开始单调递增。生产者每发送一条消息到Partition分区就会将<PID, Partition, SeqNumber>对应的SeqNumber序列号的值加1。

  • broker端会在内存中为每一对<PID,分区>维护一个序列号。对于收到的每一条消息,只有当它的序列号的值(SN_new)比broker端中维护的对应的序列号的值(SN_old)大1(即SN_new=SN_old+1)时,broker才会接收它。如果SN_new<SN_old+1,那么说明消息被重复写入,broker可以直接将其丢弃。如果 SN_new>SN_old+1,那么说明中间有数据尚未写入,出现了乱序,暗示可能有 消息丢失

  • 引入序列号来实现幂等也只是针对每一对<PID,分区>而言的,也就是说,Kafka的幂等只能保证单个生产者会话(session)中单分区的幂等,当分区具有相同的partition主键时,就会持久化一条数据。

16.3 事务(跨分区跨会话)

幂等性并不能跨多个分区运作,而事务可以弥补这个缺陷。事务可以保证对多个分区写入操作的原子性。操作的原子性是指多个操作要么全部成功,要么全部失败,不存在部分成功、部分失败的可能。

  • Producer 事务

为了实现跨分区跨会话的事务,需要引入一个全局唯一的 Transaction ID,并将 Producer获得的PID 和Transaction ID 绑定。这样当Producer 重启后就可以通过正在进行的 TransactionID 获得原来的 PID。
为了管理 Transaction, Kafka 引入了一个新的组件 Transaction Coordinator。 Producer 就是通过和 Transaction Coordinator 交互获得 Transaction ID 对应的任务状态。 Transaction Coordinator 还负责将事务所有写入 Kafka 的一个内部 Topic,这样即使整个服务重启,由于事务状态得到保存,进行中的事务状态可以得到恢复,从而继续进行。

  • Consumer 事务

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

17. zookeeper在Kafka中的作用?

Broker 注册 :在 Zookeeper 上会有一个专门用来进行 Broker 服务器列表记录的节点。每个 Broker 在启动时,都会到 Zookeeper 上进行注册,即到/brokers/ids 下创建属于自己的节点。每个 Broker 就会将自己的 IP 地址和端口等信息记录到该节点中去

Topic 注册 :在 Kafka 中,同一个Topic 的消息会被分成多个分区并将其分布在多个 Broker 上,这些分区信息及与 Broker 的对应关系也都是由 Zookeeper 在维护。比如我创建了一个名字为 my-topic 的主题并且它有两个分区,对应到 zookeeper 中会创建这些文件夹:/brokers/topics/my-topic/partions/0/brokers/topics/my-topic/partions/1

负载均衡 :上面也说过了 Kafka 通过给特定 Topic 指定多个 Partition, 而各个 Partition 可以分布在不同的 Broker 上, 这样便能提供比较好的并发能力。对于同一个 Topic 的不同 Partition,Kafka 会尽力将这些 Partition 分布到不同的 Broker 服务器上。当生产者产生消息后也会尽量投递到不同 Broker 的 Partition 里面。当 Consumer 消费的时候,Zookeeper 可以根据当前的 Partition 数量以及 Consumer 数量来实现动态负载均衡。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3Z9uAoZG-1653135100641)(assets/image-20200729084713525.png)]

18. kafka基础结构讲解

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mFiCMpsu-1653135100642)(assets/20200627182638826.png)]

1) Producer : 消息生产者,就是向 kafka broker 发消息的客户端;
2) Consumer : 消息消费者,向 kafka broker 取消息的客户端;
3) Consumer Group (CG): 消费者组,由多个 consumer 组成。 消费者组内每个消费者负责消费不同Partition分区的数据一个Partition分区只能由一个组内消费者消费但是其他组的消费者可以消费,消费者组之间互不影响。 所有的消费者都属于某个消费者组,即消费者组是逻辑上的一个订阅者。
4) Broker : 一台 kafka 服务器就是一个 broker。一个集群由多个 broker 组成。一个 broker可以容纳多个 topic。
5) Topic 主题: 可以理解为一个队列, 生产者和消费者面向的都是一个 topic;
6) Partition分区: 为了实现扩展性,一个非常大的 topic 可以分布到多个 broker(即服务器)上,一个 topic 可以分为多个 partition分区,每个 partition 是一个有序的队列;FIFO
7) Replica: 副本,为保证集群中的某个节点发生故障时, 该节点上的 partition 数据不丢失,且 kafka 仍然能够继续工作, kafka 提供了副本机制,一个 topic 的
每个Partition分区都有若干个副本
一个 leader 和若干个 follower。(注意:leader是主副本,follower是从副本)
8) leader: 每个 Partition分区多个副本的“主副本” leader Replica,生产者发送数据的对象,以及消费者消费数据的对象都是 leader。
9) follower: 每个 Partition分区多个副本中的“从副本”follower Replica,实时从 leader 中同步数据(备份leader数据),保持和 leader 数据的同步。 leader 发生故障时,某个 follower 会成为新的 leader。


(1)为什么kafka已经有 Broker 服务器了,还要加上Topic主题

如果Broker 服务器中不规划主题topic,那么每个Broker 服务器中的消息很杂乱,什么消息都会往Broker 服务器中放,没有归类,消费者取消息时会从各个Broker 服务器中取,因为不知道消息放在哪个Broker 服务器,耗费时间。

(2)为什么一个topic要放在多个Partition分区(分区1和2)?

如:topic A(Partition 0)leader在分区1,topic A(Partition 1)leader在分区2.

主要是提高了kafka集群负载均衡能力和并发度。当生产者生产消息时,会在一个Broker服务器放一个消息,其他Broker服务器放另一个消息,是一种交替轮循的模式,不至于把所有主题消息都放在一个Broker服务器。

(3) leader是基于topic使用的,所以可以放在多个Partition分区,不是基于Broker1,Broker2这样的集群,生产者发送数据的对象,以及消费者消费数据的对象都是 leader。图中topic A(Partition 0)leader在分区1,topic A(Partition 1)leader在分区2.

(4)有leader必须有follower,follower相当于备份,也是基于topic,可以放在多个Partition分区,以防一台Broker服务器宕机,还可以将follower备份升级为leader。图中topic A(Partition 1)follower在分区1,topic A(Partition 0)follower在分区2.

记住:leader和follower不可以出现在同一个Broker服务器,因为一台服务器宕机,可以将其他服务器的follower备份升级为leader.

(5)Replication: 副本。副本可以定义多个,主副本leader和多个从副本follower

19.消费者组消费消息规则

消费者对Partition分区:同一个消费者组只有一对多和一对一情况,禁止多对一

消费者组内每个消费者负责消费不同Partition分区的数据(组内一对多:一个消费者对多个分区)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tadU236T-1653135100643)(assets/1.png)]

一个Partition分区只能由一个组内消费者消费,这个组内其他消费者不可以再消费该分区(组内一对一:一个消费者对一个分区)但是其他组的消费者可以消费,消费者组之间互不影响

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aa2ps0Gp-1653135100644)(assets/2.png)]

20.kafka同一消费者组分区分配策略(消费者获得的分配消息)

一个 consumer group 中有多个 consumer,一个 topic 有多个 partition,所以必然会涉及到 partition 的分配问题,即确定那个 partition 由哪个 consumer 来消费
Kafka 同一消费者组有两种分配策略:一是 RoundRobin(采用轮循方式),一是 系统默认的Range(按照Topic主题分)。 记住针对的是同一个消费者组内的分区分配规则。不同消费者组独立。

(1)RoundRobin

分配策略的原理是将消费组内所有消费者及消费者订阅的所有主题的分区按照字典序排序,然后通过轮询方式逐个将分区依次分配给每个消费者。

情况一:如果同一个消费组内所有的消费者的订阅主题信息都是相同的,那么RoundRobin分配策略的分区分配会是均匀的。举个例子,假设消费组中有2个消费者C0和C1,都订阅了主题t0和t1,并且每个主题都有3个分区,那么订阅的所有分区可以标识为:t0p0、t0p1、t0p2、t1p0、t1p1、t1p2。最终的分配结果为:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lCdAKqcE-1653135100644)(assets/image-20200725110235675.png)]

情况二:如果同一个消费组内的消费者订阅的信息是不相同的,那么在执行分区分配的时候就不是完全的轮询分配,有可能导致分区分配得不均匀。如果某个消费者没有订阅消费组内的某个主题,那么在分配分区的时候此消费者将分配不到这个主题的任何分区

举个例子,假设消费组内有3个消费者(C0、C1和C2),它们共订阅了3个主题(t0、t1、t2),这3个主题分别有1、2、3个分区,即整个消费组订阅了t0p0、t1p0、t1p1、t2p0、t2p1、t2p2这6个分区。具体而言,消费者C0订阅的是主题t0(C0只轮循t0的分区),消费者C1订阅的是主题t0和t1(C1轮循t0和t1的分区,但是t0被C0轮循了,故没有),消费者C2订阅的是主题t0、t1和t2(C2轮循t0,t1,t2的partition分区,t0被分配完,但是t1还剩p1),那么最终的分配结果为:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aw4mmBz1-1653135100645)(assets/image-20200725110549588.png)]

可以看出轮循策略也不是十分完美,这样分配其实并不是最优解,因为完全可以将分区t1p1分配给消费者C1。

(2) 系统默认的Range

Range分配策略的原理是按照消费者总数和分区总数进行整除运算来获得一个跨度,然后将分区按照跨度进行平均分配,以保证分区尽可能均匀地分配给所有的消费者。对于每一个主题,Range策略会将同一消费组内所有订阅这个主题的消费者按照名称的字典序排序,然后为每个消费者划分固定的分区范围,如果不够平均分配,那么字典序靠前的消费者会被多分配一个分区。(按照主题topic划分)

有个分配计算公式:假设n=分区数/消费者数量,m=分区数%消费者数量,那么前m个消费者每个分配n+1个分区,后面的**(消费者数量-m)个消费者每个分配n个分区**。

情况一:假设消费组内有2个消费者C0和C1,都订阅了主题t0和t1,并且每个主题都有4个分区,那么订阅的所有分区可以标识为:t0p0、t0p1、t0p2、t0p3、t1p0、t1p1、t1p2、t1p3。最终的分配结果为:

按照计算规则:n=4(分区数)/2(消费者数)=2,m=4%2=0, 所以后面(2-0)个消费者每个分配2个分区。

即C0分配2个分区p0,p1; C1分配2个分区p2,p3. 分配均匀。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-w62lgdIa-1653135100646)(assets/image-20200725111716295.png)]

情况二:分配不均匀的情况。假设上面例子中2个主题都只有3个分区,那么订阅的所有分区可以标识为:t0p0、t0p1、t0p2、t1p0、t1p1、t1p2。最终的分配结果为:

按照计算规则:n=3/2=1 , m=3%2=1, 前1个消费者(m–C0)每个分配2个分区(1+1),后面的(2-1)=1个消费者(C1)每个分配1个分区。

即C0分配2个分区p0,p1;C1分配1个分区p2。可以明显地看到这样的分配并不均匀,如果将类似的情形扩大,则有可能出现部分消费者过载的情况。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IAu2QUdJ-1653135100647)(assets/image-20200725112218423.png)]

21 .生产者消息分区分配策略

将 producer 发送的数据封装成一个 ProducerRecord 对象。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7piAoe2L-1653135100648)(assets/image-20200726114338972.png)]

  1. 如果指明 partition 的情况下,直接将指明的值直接作为 partiton 值 。
  2. 没有指明 partition 值但有 key 的情况下,Kafka根据传递消息的key来进行分区的分配,即hash(key) % numPartitions。将 key 的 hash 值与 topic 的 partition分区总数进行取余得到 partition 值;
  3. 既没有 partition 值又没有 key 值的情况下,第一次调用时随机生成一个整数(后面每次调用在这个整数上自增),每次将这个自增值与 topic 可用的 partition 总数取余得到 partition值,也就是常说的 round-robin轮循机制算法。

22.Kafka和RabbitMQ的区别

ActiveMQ、RabbitMQ、Kafka、RocketMQ

(1)语言不同:kafka是采用Scala语言开发,它主要用于处理活跃的流式数据,大数据量的数据处理上。RabbitMQ是由内在高并发的erlanng语言开发,用在实时的对可靠性要求比较高的消息传递上。

(2)结构不同:Kafka采用的结构有Broker服务器,基于topic主题的partition分区中的leader副本存放消息、获取消息。MQ采用用户限定的host虚拟机、queue队列存放消息、交换机

(3)消费方式不同:kafka采用pull拉取消息,MQ采用push(推)+pull(拉)

(4)事务:kafka支持事务;MQ不支持

(5)都支持持久化。

23.延时队列(时间轮)

Kafka中的时间轮(TimingWheel)是一个存储定时任务的环形队列,底层采用数组实现,数组中的每个元素可以存放一个定时任务列表(TimerTaskList)。TimerTaskList是一个环形的双向链表,链表中的每一项表示的都是定时任务项(TimerTaskEntry),其中封装了真正的定时任务(TimerTask)。

时间轮由多个时间格组成,每个时间格代表当前时间轮的基本时间跨度(tickMs)。时间轮的时间格个数是固定的,可用wheelSize来表示,那么整个时间轮的总体时间跨度(interval)可以通过公式 tickMs×wheelSize计算得出。时间轮还有一个表盘指针(currentTime),用来表示时间轮当前所处的时间,currentTime是tickMs的整数倍。currentTime可以将整个时间轮划分为到期部分和未到期部分,currentTime当前指向的时间格也属于到期部分,表示刚好到期,需要处理此时间格所对应的TimerTaskList中的所有任务。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2B2sucEC-1653135100649)(assets/image-20200726164736258.png)]

若时间轮的tickMs为1ms且wheelSize等于20,那么可以计算得出总体时间跨度interval为20ms。初始情况下表盘指针currentTime指向时间格0,此时有一个定时为2ms的任务插进来会存放到时间格为2的TimerTaskList中。随着时间的不断推移,指针currentTime不断向前推进,过了2ms之后,当到达时间格2时,就需要将时间格2对应的TimeTaskList中的任务进行相应的到期操作。此时若又有一个定时为 8ms 的任务插进来,则会存放到时间格 10 中,currentTime再过8ms后会指向时间格10。如果同时有一个定时为19ms的任务插进来怎么办?新来的TimerTaskEntry会复用原来的TimerTaskList,所以它会插入原本已经到期的时间格1。总之,整个时间轮的总体跨度是不变的,随着指针currentTime的不断推进,当前时间轮所能处理的时间段也在不断后移,总体时间范围在currentTime和currentTime+interval之间。

层级时间轮:

如果此时有一个定时为350ms的任务该如何处理?直接扩充wheelSize的大小?Kafka中不乏几万甚至几十万毫秒的定时任务,这个wheelSize的扩充没有底线,就算将所有的定时任务的到期时间都设定一个上限,比如100万毫秒,那么这个wheelSize为100万毫秒的时间轮不仅占用很大的内存空间,而且也会拉低效率。Kafka 为此引入了层级时间轮的概念,当任务的到期时间超过了当前时间轮所表示的时间范围时,就会尝试添加到上层时间轮中。

复用之前的案例,第一层的时间轮tickMs=1ms、wheelSize=20、interval=20ms。第二层的时间轮的tickMs为第一层时间轮的interval,即20ms。每一层时间轮的wheelSize是固定的,都是20,那么第二层的时间轮的总体时间跨度interval为400ms。以此类推,这个400ms也是第三层的tickMs的大小,第三层的时间轮的总体时间跨度为8000ms。

对于之前所说的350ms的定时任务,显然第一层时间轮不能满足条件,所以就升级到第二层时间轮中,最终被插入第二层时间轮中时间格17所对应的TimerTaskList。如果此时又有一个定时为450ms的任务,那么显然第二层时间轮也无法满足条件,所以又升级到第三层时间轮中,最终被插入第三层时间轮中时间格1的TimerTaskList。注意到在到期时间为[400ms,800ms)区间内的多个任务(比如446ms、455ms和473ms的定时任务)都会被放入第三层时间轮的时间格1,时间格1对应的TimerTaskList的超时时间为400ms。随着时间的流逝,当此TimerTaskList到期之时,原本定时为450ms的任务还剩下50ms的时间,还不能执行这个任务的到期操作。这里就有一个时间轮降级的操作,会将这个剩余时间为50ms 的定时任务重新提交到层级时间轮中,此时第一层时间轮的总体时间跨度不够,而第二层足够,所以该任务被放到第二层时间轮到期时间为[40ms,60ms)的时间格中。再经历40ms之后,此时这个任务又被“察觉”,不过还剩余10ms,还是不能立即执行到期操作。所以还要再有一次时间轮的降级,此任务被添加到第一层时间轮到期时间为[10ms,11ms)的时间格中,之后再经历 10ms 后,此任务真正到期,最终执行相应的到期操作。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TCisYvJ6-1653135100650)(assets/image-20200726164954578.png)]

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
kafka-connect-transform-kryptonite 是 Kafka Connect 的一个转换器插件。Kafka Connect 是一个可扩展的分布式数据集成工具,可用于在 Apache Kafka 和外部系统之间进行可靠和高效的数据流传输。而 Kafka Connect 转换器是用于对数据进行转换、过滤或处理的插件。 Kafka Connect 是一个开源的分布式数据集成框架,用于连接和处理来自各种数据源的数据,例如数据库、消息队列和文件系统等。它提供了一个统一的、可扩展的架构,允许用户将数据从多个来源导入到 Kafka 中,并将数据导出到多个目标系统。这为数据集成提供了更加灵活和可靠的方式,且能够满足各种实时数据处理的需求。 Kafka Connect 的一个关键特性是插件化的架构,使得用户可以根据自己的需求,选择和配置合适的插件。其中,kafka-connect-transform-kryptonite 插件就是其中之一。Kryptonite 可以理解为一种“解除”或“削弱”转换器,它可能采用一些特定的规则或算法,对输入的数据进行加工、转换或过滤。 使用 kafka-connect-transform-kryptonite 插件,我们可以根据具体的业务需求,对 Kafka 中的消息进行处理。例如,我们可以通过 Kryptonite 转换器,将消息中的某些字段进行加密,以保护敏感数据的安全;或者根据一些规则,对消息进行过滤和筛选,只保留我们关心的数据。 总之,kafka-connect-transform-kryptonite 是 Kafka Connect 提供的一个转换器插件,可以用于对数据进行加工、转换和过滤。通过这个插件,我们可以根据业务需求对 Kafka 中的消息进行定制化处理,以满足不同场景下的数据集成和处理需求。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值