学习一个新东西最好的方式就是先看官方文档,或者源码.
消息队列已经逐渐成为企业IT系统内部通信的核心手段。它具有低耦合、可靠投递、广播、流量控制、最终一致性等一系列功能,成为异步RPC的主要手段之一。当今市面上有很多主流的消息中间件,如老牌的ActiveMQ、RabbitMQ,炙手可热的Kafka,阿里巴巴自主开发RocketMQ等。
消息服务器,作为server提供消息核心服务
消息生产者,业务的发起方,负责生产消息传输给broker
消息消费者,业务的处理方,负责从broker获取消息并进行业务逻辑处理
主题,发布订阅模式下的消息统一汇集地,不同生产者向topic发送消息,由MQ服务器分发到不同的订阅者,实现消息的 广播
队列,PTP模式下,特定生产者向特定queue发送消息,消费者订阅特定的queue完成指定消息的接收
消息体,根据不同通信协议定义的固定格式进行编码的数据包,来封装业务数据,实现消息的传输
3.1 点对点(p2p) 通过queue作为消息的载体
说明: 消息生产者生产消息发送到queue中,然后消息消费者从queue中取出并且消费消息。 消息被消费以后,queue中不再存储,所以消息消费者不可能消费到已经被消费的消息。 Queue支持存在多个消费者,但是对一个消息而言,只会有一个消费者可以消费。
Pub/Sub发布订阅(广播):使用topic作为通信载体
说明: 消息生产者(发布)将消息发布到topic中,同时有多个消息消费者(订阅)消费该消息。和点对点方式不同,发布到topic的消息会被所有订阅者消费。
两者不同:
queue实现了负载均衡,将producer生产的消息发送到消息队列中,由多个消费者消费。但一个消息只能被一个消费者接受,当没有消费者可用时,这个消息会被保存直到有一个可用的消费者。 topic实现了发布和订阅,当你发布一个消息,所有订阅这个topic的服务都能得到这个消息,所以从1到N个订阅者都能得到一个消息的拷贝。
四 kafka优势及缺点通过消息传送系统之间不用直接通信
主要业务同步,其他业务利用消息
深入卡夫卡之前,必须了解主题,经纪人,制作人和消费者等主要术语。下图说明了主要术语,表格详细描述了图表组件
在上图中,主题被配置为三个分区。分区1具有两个偏移因子0和1.分区2具有四个偏移因子0,1,2和3.分区3具有一个偏移因子0.复制品的id与托管它的服务器的id相同。
1 | Topics 属于特定类别的消息流被称为主题。数据存储在主题中。 主题分为多个分区。对于每个主题,卡夫卡都保留一个分区的最小范围。每个这样的分区都以不可变的有序顺序包含消息。分区被实现为一组相同大小的段文件。 |
2 | Partition 主题可能有很多分区,所以它可以处理任意数量的数据。 |
3 | Partition offset 每个分区消息都有一个称为偏移量的唯一序列标识。 |
4 | Replicas of partition 副本只是分区的备份。副本从不读取或写入数据。它们用于防止数据丢失。 |
5 | Brokers 负责维护公布的数据。每个代理可能每个主题有零个或多个分区。假设,如果一个主题和N个代理中有N个分区,则每个代理将有一个分区。 假设某个主题中有N个分区并且N个代理(n + m)多于N个,则第一个N代理将拥有一个分区,下一个M代理将不会拥有该特定主题的任何分区。 假设某个主题中有N个分区,且N个代理的数量少于N个(nm),则每个代理将拥有一个或多个分区共享。由于代理人之间的负载分配不均衡,不推荐这种情况。 |
6 | Kafka Cluster 卡夫卡拥有多个经纪人称为卡夫卡集群。Kafka集群可以在无需停机的情况下进行扩展。这些集群用于管理消息数据的持久性和复制。 |
7 | Producers 生产者可以将数据发布到所选择的topic(主题)中。生产者负责将记录分配到topic的哪一个 partition(分区)中。可以使用循环的方式来简单地实现负载均衡,也可以根据某些语义分区函数(例如:记录中的key)来完成。下面会介绍更多关于分区的使用。 |
8 | Consumers 消费者使用一个 消费组 名称来进行标识,发布到topic中的每条记录被分配给订阅消费组中的一个消费者实例.消费者实例可以分布在多个进程中或者多个机器上。 如果所有的消费者实例在同一消费组中,消息记录会负载平衡到每一个消费者实例. 如果所有的消费者实例在不同的消费组中,每条消息记录会广播到所有的消费者进程. 如图,这个 Kafka 集群有两台 server 的,四个分区(p0-p3)和两个消费者组。消费组A有两个消费者,消费组B有四个消费者。 通常情况下,每个 topic 都会有一些消费组,一个消费组对应一个"逻辑订阅者"。一个消费组由许多消费者实例组成,便于扩展和容错。这就是发布和订阅的概念,只不过订阅者是一组消费者而不是单个的进程。 在Kafka中实现消费的方式是将日志中的分区划分到每一个消费者实例上,以便在任何时间,每个实例都是分区唯一的消费者。维护消费组中的消费关系由Kafka协议动态处理。如果新的实例加入组,他们将从组中其他成员处接管一些 partition 分区;如果一个实例消失,拥有的分区将被分发到剩余的实例。 Kafka 只保证分区内的记录是有序的,而不保证主题中不同分区的顺序。每个 partition 分区按照key值排序足以满足大多数应用程序的需求。但如果你需要总记录在所有记录的上面,可使用仅有一个分区的主题来实现,这意味着每个消费者组只有一个消费者进程。 |
9 | Leader Leader是负责所有分区读写的节点。每个分区都有一台服务器充当领导者。 |
10 | Follower 遵循领导指示的节点称为追随者。如果领导失败,其中一个追随者将自动成为新领导。追随者扮演正常的消费者角色,拉动消息并更新自己的数据存储。 |
Kafka的优势在于每个topic都有以下特性—可以扩展处理并且允许多订阅者模式—不需要只选择其中一个.
Kafka相比于传统消息队列还具有更严格的顺序保证
传统队列在服务器上保存有序的记录,如果多个消费者消费队列中的数据, 服务器将按照存储顺序输出记录。 虽然服务器按顺序输出记录,但是记录被异步传递给消费者,因此记录可能会无序的到达不同的消费者。这意味着在并行消耗的情况下, 记录的顺序是丢失的。因此消息系统通常使用“唯一消费者”的概念,即只让一个进程从队列中消费, 但这就意味着不能够并行地处理数据。
Kafka 设计的更好。topic中的partition是一个并行的概念。Kafka能够为一个消费者池提供顺序保证和负载平衡,是通过将topic中的partition分配给消费者组中的消费者来实现的, 以便每个分区由消费组中的一个消费者消耗。通过这样,我们能够确保消费者是该分区的唯一读者,并按顺序消费数据。众多分区保证了多个消费者实例间的负载均衡。但请注意,消费者组中的消费者实例个数不能超过分区的数量。
数据写入Kafka后被写到磁盘,并且进行备份以便容错。直到完全备份,Kafka才让生产者认为完成写入,即使写入失败Kafka也会确保继续写入
Kafka使用磁盘结构,具有很好的扩展性—50kb和50TB的数据在server上表现一致。
可以存储大量数据,并且可通过客户端控制它读取数据的位置,您可认为Kafka是一种高性能、低延迟、具备日志存储、备份和传播功能的分布式文件系统。
Kafka 流处理不仅仅用来读写和存储流式数据,它最终的目的是为了能够进行实时的流处理。
在Kafka中,流处理器不断地从输入的topic获取流数据,处理数据后,再不断生产流数据到输出的topic中去。
例如,零售应用程序可能会接收销售和出货的输入流,经过价格调整计算后,再输出一串流式数据。
简单的数据处理可以直接用生产者和消费者的API。对于复杂的数据变换,Kafka提供了Streams API。Stream API 允许应用做一些复杂的处理,比如将流数据聚合或者join。
这一功能有助于解决以下这种应用程序所面临的问题:处理无序数据,当消费端代码变更后重新处理输入,执行有状态计算等。
Streams API建立在Kafka的核心之上:它使用Producer和Consumer API作为输入,使用Kafka进行有状态的存储, 并在流处理器实例之间使用相同的消费组机制来实现容错。
以下引用自官方文档
它可以用于两大类别的应用:
1. 构造实时流数据管道,它可以在系统或应用之间可靠地获取数据。 (相当于message queue)
2. 构建实时流式应用程序,对这些流数据进行转换或者影响。 (就是流处理,通过kafka stream topic和topic之间内部进行变化)
为了理解Kafka是如何做到以上所说的功能,从下面开始,我们将深入探索Kafka的特性。
实际中的运用
消息,跟踪网站活动,度量,日志处理(聚合,处理),再处理
首先是一些概念:
· Kafka作为一个集群,运行在一台或者多台服务器上.
· Kafka 通过 topic 对存储的流数据进行分类。
· 每条记录中包含一个key,一个value和一个timestamp(时间戳)。
Kafka有四个核心的API:
· The Producer API 允许一个应用程序发布一串流式的数据到一个或者多个Kafka topic。
· The Consumer API 允许一个应用程序订阅一个或多个 topic ,并且对发布给他们的流式数据进行处理。
· The Streams API 允许一个应用程序作为一个流处理器,消费一个或者多个topic产生的输入流,然后生产一个输出流到一个或多个topic中去,在输入输出流中进行有效的转换。
· The Connector API 允许构建并运行可重用的生产者或者消费者,将Kafka topics连接到已存在的应用程序或者数据系统。比如,连接到一个关系型数据库,捕捉表(table)的所有变更内容。
·
Topic 就是数据主题,是数据记录发布的地方,可以用来区分业务系统。Kafka中的Topics总是多订阅者模式,一个topic可以拥有一个或者多个消费者来订阅它的数据。
对于每一个topic, Kafka集群都会维持一个分区日志,如下所示:
每个分区都是有序且顺序不可变的记录集,并且不断地追加到结构化的commit log文件。分区中的每一个记录都会分配一个id号来表示顺序,我们称之为offset,offset用来唯一的标识分区中每一条记录。
Kafka 集群保留所有发布的记录—无论他们是否已被消费—并通过一个可配置的参数——保留期限来控制. 举个例子, 如果保留策略设置为2天,一条记录发布后两天内,可以随时被消费,两天过后这条记录会被抛弃并释放磁盘空间。Kafka的性能和数据大小无关,所以长时间存储数据没有什么问题.
在实际中消费者保存的是在分区日中偏移量,偏移量是由消费者控制,消费者的增加或者减少对其他的消费者不会产生影响
日志中的分区有几个作用
1. 可扩展topic主题
2. 分布式
日志的分区partition (分布)在Kafka集群的服务器上。每个服务器在处理数据和请求时,共享这些分区。每一个分区都会在已配置的服务器上进行备份,确保容错性.
每个分区都有一台 server 作为 “leader”,零台或者多台server作为 follwers 。leader server 处理一切对 partition (分区)的读写请求,而follwers只需被动的同步leader上的数据。当leader宕机了,followers 中的一台服务器会自动成为新的 leader。每台 server 都会成为某些分区的 leader 和某些分区的 follower,因此集群的负载是平衡的。
入门安装直接官方文档
安装注意两个参数
listeners=PLAINTEXT://192.168.64.129:9092 监听
advertised.listeners=PLAINTEXT://192.168.64.129:9092
代码贴上
依赖:
配置文件
kafka.consumer.servers=192.168.64.129:9092,192.168.64.129:9093,192.168.64.129:9094kafka.consumer.enable.auto.commit=truekafka.consumer.session.timeout=20000kafka.consumer.auto.commit.interval=100kafka.consumer.auto.offset.reset=latestkafka.consumer.topic=my-replicated-topickafka.consumer.group.id=testkafka.consumer.concurrency=10#协议转换后存储kafkakafka.producer.servers=192.168.64.129:9092,192.168.64.129:9093,192.168.64.129:9094kafka.producer.topic=my-replicated-topickafka.producer.retries=0kafka.producer.batch.size=4096kafka.producer.linger=1kafka.producer.buffer.memory=40960
配置类
生产者
import org.apache.kafka.clients.producer.ProducerConfig;import org.apache.kafka.common.serialization.StringSerializer;import org.springframework.beans.factory.annotation.Value;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.kafka.annotation.EnableKafka;import org.springframework.kafka.core.DefaultKafkaProducerFactory;import org.springframework.kafka.core.KafkaTemplate;import org.springframework.kafka.core.ProducerFactory;import java.util.HashMap;import java.util.Map;&#64;Configuration&#64;EnableKafkapublic class KafkaProducerConfig { &#64;Value("${kafka.producer.servers}") private String servers; &#64;Value("${kafka.producer.retries}") private int retries; &#64;Value("${kafka.producer.batch.size}") private int batchSize; &#64;Value("${kafka.producer.linger}") private int linger; &#64;Value("${kafka.producer.buffer.memory}") private int bufferMemory; public Map producerConfigs() { Map props &#61; new HashMap<>(); props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, servers); props.put(ProducerConfig.RETRIES_CONFIG, retries); props.put(ProducerConfig.BATCH_SIZE_CONFIG, batchSize); props.put(ProducerConfig.LINGER_MS_CONFIG, linger); props.put(ProducerConfig.BUFFER_MEMORY_CONFIG, bufferMemory); props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class); props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class); return props; } public ProducerFactory producerFactory() { return new DefaultKafkaProducerFactory<>(producerConfigs()); } &#64;Bean public KafkaTemplate kafkaTemplate() { return new KafkaTemplate(producerFactory()); }}
生产者测试
import org.springframework.beans.factory.annotation.Autowired;import org.springframework.kafka.core.KafkaTemplate;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.ResponseBody;&#64;Controllerpublic class TestController { &#64;Autowired KafkaTemplate kafkaTemplate; public static Integer i&#61;1; &#64;RequestMapping("/send") &#64;ResponseBody public String sendMessage(){ try { i&#43;&#43;; kafkaTemplate.send("my-replicated-topic",i.toString()); Thread.sleep(1000L); i&#43;&#43;; kafkaTemplate.send("my-replicated-topic",i.toString()); } catch (InterruptedException e) { e.printStackTrace(); } return "success"; }}
消费者
package com.example.demo.web;import org.apache.kafka.clients.consumer.ConsumerConfig;import org.apache.kafka.common.serialization.StringDeserializer;import org.springframework.beans.factory.annotation.Value;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.kafka.annotation.EnableKafka;import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory;import org.springframework.kafka.config.KafkaListenerContainerFactory;import org.springframework.kafka.core.ConsumerFactory;import org.springframework.kafka.core.DefaultKafkaConsumerFactory;import org.springframework.kafka.listener.ConcurrentMessageListenerContainer; import java.util.HashMap;import java.util.Map; /** * kafka消费者配置 * &#64;author Lvjiapeng * */&#64;Configuration&#64;EnableKafkapublic class KafkaConsumerConfig { &#64;Value("${kafka.consumer.servers}") private String servers; &#64;Value("${kafka.consumer.enable.auto.commit}") private boolean enableAutoCommit; &#64;Value("${kafka.consumer.session.timeout}") private String sessionTimeout; &#64;Value("${kafka.consumer.auto.commit.interval}") private String autoCommitInterval; &#64;Value("${kafka.consumer.group.id}") private String groupId; &#64;Value("${kafka.consumer.auto.offset.reset}") private String autoOffsetReset; &#64;Value("${kafka.consumer.concurrency}") private int concurrency; &#64;Bean public KafkaListenerContainerFactory> kafkaListenerContainerFactory() { ConcurrentKafkaListenerContainerFactory factory &#61; new ConcurrentKafkaListenerContainerFactory<>(); factory.setConsumerFactory(consumerFactory()); factory.setConcurrency(concurrency); factory.getContainerProperties().setPollTimeout(1500); return factory; } public ConsumerFactory consumerFactory() { return new DefaultKafkaConsumerFactory<>(consumerConfigs()); } public Map consumerConfigs() { Map propsMap &#61; new HashMap<>(); propsMap.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, servers); propsMap.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, enableAutoCommit); propsMap.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, autoCommitInterval); propsMap.put(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG, sessionTimeout); propsMap.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); propsMap.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); propsMap.put(ConsumerConfig.GROUP_ID_CONFIG, groupId); propsMap.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, autoOffsetReset); return propsMap; } /** * kafka监听 * &#64;return */ &#64;Bean public RawDataListener listener() { return new RawDataListener(); } }
消费者测试
import org.apache.kafka.clients.consumer.ConsumerRecord;import org.springframework.kafka.annotation.KafkaListener;import org.springframework.stereotype.Component;import java.io.IOException; /** * kafka监听 * &#64;author Lvjiapeng * */&#64;Componentpublic class RawDataListener { /* * 实时获取kafka数据(生产一条&#xff0c;监听生产topic自动消费一条) * &#64;param record * &#64;throws IOException */ &#64;KafkaListener(topics &#61; {"${kafka.consumer.topic}"}) public void listen(ConsumerRecord, ?> record) throws IOException { String value &#61; (String) record.value(); System.out.println(value); } }
在集群环境中如何写入数据,kakfa是直接写到主分区上,因为所有的 kafka 服务器节点都能响应这样的元数据请求&#xff1a;哪些服务器是活着的&#xff0c;主题的哪些分区是主分区&#xff0c;分配在哪个服务器上&#xff0c;这样生产者就能适当地直接发送它的请求到服务器上。
或者kafka在push的时候提供了api,让用于根据指定的键值进行hash分区(当然也有选项可以重写分区函数)&#xff0c;例如&#xff0c;如果使用用户ID作为key&#xff0c;则用户相关的所有数据都会被分发到同一个分区上。这允许消费者在消费数据时做一些特定的本地化处理。这样的分区风格经常被设计用于一些本地处理比较敏感的消费者。
为什么要进行批处理
很简单,批处理为了减少io,增加压缩率
如何设置
kafka 生产者会尝试在内存中汇总数据&#xff0c;并用一次请求批次提交信息。批处理&#xff0c;不仅仅可以配置指定的消息数量&#xff0c;也可以指定等待特定的延迟时间(如64k 或10ms)&#xff0c;这允许汇总更多的数据后再发送&#xff0c;在服务器端也会减少更多的IO操作。该缓冲是可配置的&#xff0c;并给出了一个机制&#xff0c;通过权衡少量额外的延迟时间获取更好的吞吐量。
把消息发送给服务器&#xff0c;但并不关心它是否正常到达。大多数情况下&#xff0c;消息会正常到达&#xff0c;因为 Kafka 是高可用的&#xff0c;而且生产者会自动尝试重发。不过&#xff0c;使用这种方式有时候也会丢失一些消息。
package com.example.demo.web;import org.apache.kafka.clients.producer.KafkaProducer;import org.apache.kafka.clients.producer.Producer;import org.apache.kafka.clients.producer.ProducerRecord;import java.util.Properties;/** * &#64;Title Producer01.java * &#64;Description Kafka 生产者发送消息的第一种方式&#xff1a;发送并忘记 * &#64;Author lph */public class Producer01 { public static void main(String[] args) throws Exception { Properties props &#61; new Properties(); props.put("bootstrap.servers", "192.168.64.129:9092,192.168.64.129:9093,192.168.64.129:9094"); props.put("acks", "1"); props.put("retries", 3); props.put("batch.size", 16384); // 16K props.put("linger.ms", 1); props.put("buffer.memory", 33554432); // 32M props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer"); props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer"); Producer producer &#61; new KafkaProducer<>(props); // 创建 ProducerRecord 可以指定 topic、partition、key、value&#xff0c;其中 partition 和 key 是可选的 // ProducerRecord record &#61; new ProducerRecord<>("my-replicated-topic", 0, "key", line); // ProducerRecord record &#61; new ProducerRecord<>("my-replicated-topic", "key", line); ProducerRecord record &#61; new ProducerRecord<>("my-replicated-topic", "1111111111111"); // 只管发送消息&#xff0c;不管是否发送成功 producer.send(record); Thread.sleep(100); producer.close(); }}
package com.example.demo.web;import org.apache.kafka.clients.producer.KafkaProducer;import org.apache.kafka.clients.producer.Producer;import org.apache.kafka.clients.producer.ProducerRecord;import org.apache.kafka.clients.producer.RecordMetadata;import java.io.BufferedReader;import java.io.FileReader;import java.util.Properties;/** * &#64;Title Producer02.java * &#64;Description Kafka 生产者发送消息的第二种方式&#xff1a;同步发送 * &#64;Author lph */public class Producer02 { public static void main(String[] args) throws Exception { Properties props &#61; new Properties(); props.put("bootstrap.servers", "192.168.64.129:9092,192.168.64.129:9093,192.168.64.129:9094"); props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer"); props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer"); Producer
日志:
record [1111111111111] has been sent successfully!
send to partition 0, offset &#61; 18
record [22222222222222] has been sent successfully!
send to partition 0, offset &#61; 19
record [2333333333333333333] has been sent successfully!
send to partition 0, offset &#61; 20
record [44444444444444444444] has been sent successfully!
send to partition 0, offset &#61; 21
record [55555555555555555] has been sent successfully!
send to partition 0, offset &#61; 22
record [666666666666666666666] has been sent successfully!
send to partition 0, offset &#61; 23
record [7777777777777777777777] has been sent successfully!
send to partition 0, offset &#61; 24
大多数时候&#xff0c;我们并不需要等待响应——kafka会把偏移信息发送回来但是对客户端来说是不必要的,所以我们调用send()的方法,指定一个回调函数
package com.example.demo.web;import org.apache.kafka.clients.producer.*;import java.io.BufferedReader;import java.io.FileReader;import java.util.Properties;/** * &#64;Title Producer03.java * &#64;Description Kafka 生产者发送消息的第三种方式&#xff1a;异步发送 * &#64;Author lph */public class Producer03 { public static void main(String[] args) throws Exception { Properties props &#61; new Properties(); props.put("bootstrap.servers", "192.168.64.129:9092,192.168.64.129:9093,192.168.64.129:9094"); props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer"); props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer"); Producer
类比于传统的消息中间件
Kafka 在这方面采取了一种较为传统的设计方式&#xff0c;也是大多数的消息系统所共享的方式&#xff1a;即 producer 把数据 push 到 broker&#xff0c;然后 consumer 从 broker 中 pull 数据
简单的 pull-based 系统的不足之处在于&#xff1a;如果 broker 中没有数据&#xff0c;consumer 可能会在一个紧密的循环中结束轮询&#xff0c;实际上 busy-waiting 直到数据到来。为了避免 busy-waiting&#xff0c;我们在 pull 请求中加入参数&#xff0c;使得 consumer 在一个“long pull”中阻塞等待&#xff0c;直到数据到来(还可以选择等待给定字节长度的数据来确保传输长度)。
现在大多数消息系统对已经消费的消息的处理都有关注和处理
传统的中间件在处理消息的时候,当消息发送到消费者的时候,消息标记为发送,消息被消费这处理完成的时候提交确认然后该消息才删除,但是问题来了,如果该消息已经发送出去了,但是在消息处理完成发送确认请求的时候出错了,那么这个消息就会被处理两次,另外维护这些消息的状态也是性能开销
我们来看kafka如何解决这个问题的
Kafka的 topic 被分割成了一组完全有序的 partition&#xff0c;其中每一个 partition 在任意给定的时间内只能被每个订阅了这个 topic 的 consumer 组中的一个 consumer 消费。这意味着 partition 中 每一个 consumer 的位置仅仅是一个数字&#xff0c;即下一条要消费的消息的offset。这使得被消费的消息的状态信息相当少&#xff0c;每个 partition 只需要一个数字。这个状态信息还可以作为周期性的 checkpoint。这以非常低的代价实现了和消息确认机制等同的效果。
这种方式还有一个附加的好处。consumer 可以回退到之前的 offset 来再次消费之前的数据&#xff0c;这个操作违反了队列的基本原则&#xff0c;但事实证明对大多数 consumer 来说这是一个必不可少的特性。例如&#xff0c;如果 consumer 的代码有 bug&#xff0c;并且在 bug 被发现前已经有一部分数据被消费了&#xff0c; 那么 consumer 可以在 bug 修复后通过回退到之前的 offset 来再次消费这些数据。
目前自己还没有应用留坑
消费组简单的定义从下面三个方面来理解
1.Consumer Group 下可以有一个或多个 Consumer 实例。这里的实例可以是一个单独的进程&#xff0c;也可以是同一进程下的线程。在实际场景中&#xff0c;使用进程更为常见一些。
2.Group ID 是一个字符串&#xff0c;在一个 Kafka 集群中&#xff0c;它标识唯一的一个 Consumer Group。
3.Consumer Group 下所有实例订阅的主题的单个分区&#xff0c;只能分配给组内的某个 Consumer 实例消费。这个分区当然也可以被其他的 Group 消费。
由上面可以看到 Consumer Group 是 Kafka 提供的可扩展且具有容错性的消费者机制&#xff0c;因此在新加入的消费组实例也就是Consumer Instance的时候&#xff0c;kafka会进行reblance
9.4.1 Rebalance
我们就要说一说Rebalance
首先&#xff0c;Rebalance 过程对 Consumer Group 消费过程有极大的影响&#xff0c;在 Rebalance 过程中&#xff0c;所有 Consumer 实例都会停止消费&#xff0c;等待 Rebalance 完成。
其次&#xff0c;目前 Rebalance 的设计是所有 Consumer 实例共同参与&#xff0c;全部重新分配所有分区。其实更高效的做法是尽量减少分配方案的变动。例如实例 A 之前负责消费分区 1、2、3&#xff0c;那么 Rebalance 之后&#xff0c;如果可能的话&#xff0c;最好还是让实例 A 继续消费分区 1、2、3&#xff0c;而不是被重新分配其他的分区。这样的话&#xff0c;实例 A 连接这些分区所在 Broker 的 TCP 连接就可以继续用&#xff0c;不用重新创建连接其他 Broker 的 Socket 资源。
这也就造成了kafka消费端不能保障只消费一次的原因
9.4.2 Offsets
__consumer_offsets 在 Kafka 源码中有个更为正式的名字&#xff0c;叫位移主题&#xff0c;即 Offsets Topic
新版本 Consumer 的位移管理机制其实也很简单&#xff0c;就是将 Consumer 的位移数据作为一条条普通的 Kafka 消息&#xff0c;提交到 __consumer_offsets 中。可以这么说&#xff0c;__consumer_offsets 的主要作用是保存 Kafka 消费者的位移信息
这个主题存的到底是什么格式的消息呢&#xff1f;所谓的消息格式&#xff0c;你可以简单地理解为是一个 KV 对。Key 和 Value 分别表示消息的键值和消息体&#xff0c;在 Kafka 中它们就是字节数组而已
位移主题的 Key 中应该保存 3 部分内容&#xff1a;(稍后分析源码)
当 Kafka 集群中的第一个 Consumer 程序启动时&#xff0c;Kafka 会自动创建位移主题
果位移主题是 Kafka 自动创建的&#xff0c;那么该主题的分区数是 50&#xff0c;副本数是 3。
9.4.3 Coordinator
所谓协调者&#xff0c;在Kafka中对应的术语是Coordinator&#xff0c;它专门为Consumer Group 服务负责为 Group执行Rebalance以及提供位移管理和组成员管理等。
具体来讲Consumer端应用程序在提交位移时其实是向Coordinator所在的 Broker提交位移。同样地&#xff0c;当Consumer应用启动时&#xff0c;也是向Coordinator所在的Broker发送各种请求&#xff0c;然后由Coordinator负责执行消费者组的注册、成员管理记录等元数据管理操作。
目前Kafka为某个Consumer Group确定Coordinator所在的Broker的算法有 2 个步骤。
第1步&#xff1a;确定由位移主题的哪个分区来保存该Group数据。
第 2 步&#xff1a;找出该分区 Leader 副本所在的 Broker&#xff0c;该 Broker 即为对应的 Coordinator。
Kafka的消息交付语义有三种
· At most once——消息可能会丢失但绝不重传。
· At least once——消息可以重传但绝不丢失。
· Exactly once——这正是人们想要的, 每一条消息只被传递一次.
分开来看就是两个问题 1.消息本身的保证 2.消费的保障
先来看只消费一次
其他的中间件消费一次的定义,上面讨论过消费一次的案例,某个环节失败
首先从生产者角度来看 传统的中间件有可能出现,在kafka上没有该模式模式,发布消息时&#xff0c;我们会有一个消息的概念被“committed”到 log 中。一旦消息被提交&#xff0c;只要有一个 broker 备份了该消息写入的 partition&#xff0c;并且保持“alive”状态&#xff0c;该消息就不会丢失。
在0.11.0.0版本之前&#xff0c;Kafka producer默认提供的是at least once语义。设想一下这个场景&#xff1a;如当producer向broker发送新消息后&#xff0c;分区leader副本所在的broker成功地将该消息写入本地磁盘&#xff0c;然后发送响应给producer&#xff0c;此时假设网络出现了故障导致响应没有发送成功&#xff0c;那么未接收到响应的producer会认为消息请求失败而重新发送&#xff0c;若网络恢复之后&#xff0c;那么同一条消息被写入日志两次&#xff0c;在极端的条件下&#xff0c;同一消息可能会被发送多次。 如图
0.11.0.0版本之后是恰好一次,通过两个方面来保正只有一次
幂等性producer是0.11.0.0版本用于实现EOS(Exactly-Once semantics)的第一个利器。若一个操作执行多次的结果与只运行一次的结果是相同的&#xff0c;那么我们称该操作为幂等操作。
0.11.0.0版本引入的幂等性producer表示它的发送操作是幂等的&#xff0c;同一条消息被producer发送多次&#xff0c;但在broker端这条消息只会被写入日志一次&#xff0c;如果要启用幂等性producer以及获取其提供的EOS语义&#xff0c;用户需要显示设置producer端参数enable.idempotence为true
// 此时ack默认被设置为-1(all)props.put("enable.idempotence",true);
发送到broker端的每批消息都会被赋予一个序列号(sequence number)用于消息去重。kafka会把它们保存到底层日志&#xff0c;这样即使分区leader副本挂掉&#xff0c;新选出来的leader broker也能执行消息去重的工作。
kafka还会为每个producer实例分配一个producer id(PID)&#xff0c;消息要被发送到每个分区都有对应的序列号值&#xff0c;它们总是从0开始单调增加&#xff0c;对于PID、分区、序列号三者的关系&#xff0c;可以设想为一个map&#xff0c;key就是(PID&#xff0c;分区)&#xff0c;value就是序列号&#xff0c;即每对(PID&#xff0c;分区)都有一个特定的序列号(seqID)&#xff0c;如果发送消息的seqID小于等于broker端保存的seqID&#xff0c;那么broker会拒绝接收这一条消息。
在单会话幂等性中介绍&#xff0c;kafka通过引入pid和seq来实现单会话幂等性&#xff0c;但正是引入了pid&#xff0c;当应用重启时&#xff0c;新的producer并没有old producer的状态数据。可能重复保存。
设计思路如下图
源码分析
org.apache.kafka.clients.producer.internals.Sender
对事务的支持是kafka实现EOS的第二个利器&#xff0c;引入事务使得clients端程序能够将一组消息放入一个原子性单元统一处理。
kafka事务属性是指一系列的生产者生产消息和消费者提交偏移量的操作在一个事务&#xff0c;或者说是是一个原子操作)&#xff0c;同时成功或者失败。
kafka为实现事务要求应用程序必须提供一个唯一的id表征事务&#xff0c;这个id被称为事务id&#xff0c;他必须在应用正序所有的会话上是唯一的。transactionID和Pid是不同的&#xff0c;前者是用户显式提供的&#xff0c;后者是producer自行分配的。
源码分析
org.apache.kafka.clients.producer.Producer
在一个原子操作中&#xff0c;根据包含的操作类型&#xff0c;可以分为三种情况&#xff0c;前两种情况是事务引入的场景&#xff0c;最后一种情况没有使用价值。
· 只有Producer生产消息&#xff1b;
· 消费消息和生产消息并存&#xff0c;这个是事务场景中最常用的情况&#xff0c;就是我们常说的“consume-transform-produce ”模式
· 只有consumer消费消息&#xff0c;这种操作其实没有什么意义&#xff0c;跟使用手动提交效果一样&#xff0c;而且也不是事务属性引入的目的&#xff0c;所以一般不会使用这种情况
使用kafka的事务api时的一些注意事项&#xff1a;
· 需要消费者的自动模式设置为false,并且不能子再手动的进行执行consumer#commitSync或者consumer#commitAsyc
· 生产者配置transaction.id属性
· 生产者不需要再配置enable.idempotence&#xff0c;因为如果配置了transaction.id&#xff0c;则此时enable.idempotence会被设置为true
· 消费者需要配置Isolation.level。在consume-trnasform-produce模式下使用事务时&#xff0c;必须设置为READ_COMMITTED。
在这个事务操作中&#xff0c;只有生成消息操作。代码如下&#xff1a;
public void onlyProduceInTransaction() { // 创建生成者&#xff0c;代码如下,需要: // 配置transactional.id属性 // 配置enable.idempotence属性 Producer producer &#61; new MockProducer(); // 1.初始化事务 producer.initTransactions(); // 2.开启事务 producer.beginTransaction(); try { // 3.kafka写操作集合 // 3.1 do业务逻辑 // 3.2 发送消息 producer.send(new ProducerRecord
/** * 在一个事务内,即有生产消息又有消费消息 */public void consumeTransferProduce() { // 1.构建上产者 Producer producer &#61; buildProducer(); // 2.初始化事务(生成productId),对于一个生产者,只能执行一次初始化事务操作 producer.initTransactions(); // 3.构建消费者和订阅主题 // 创建消费者代码&#xff0c;需要&#xff1a; // 将配置中的自动提交属性(auto.commit)进行关闭 // 而且在代码里面也不能使用手动提交commitSync( )或者commitAsync( ) // 设置isolation.level Consumer consumer &#61; buildConsumer(); consumer.subscribe(Arrays.asList("test")); while (true) { // 4.开启事务 producer.beginTransaction(); // 5.1 接受消息 ConsumerRecords
Consumer 读取到消息之后&#xff0c;先进行offset提交&#xff0c;然后再处理消息&#xff0c;如果消息处理到一半失败了&#xff0c;那这条消息就再也不会被消费了&#xff0c;这对应于at-most-once的语义。
Consumer 读取到消息之后&#xff0c;先处理消息&#xff0c;最后再offset提交。这样如果处理消息成功&#xff0c;在offset提交之前服务崩溃了&#xff0c;那么重启之后这条消息会再次被消费到&#xff0c;这对应于at-least-once的语义。
如果要Exactly once语义&#xff0c;则可以使用如下手段&#xff1a;
消费处理失败指的是业务失败或者操作db失败。
消费处理成功指的是业务成功或者操作db成功。
1&#xff0c;如果消费处理失败的话需要额外记录此条消息的offset&#xff0c;对于有顺序要求的消费来说&#xff0c;此时还得停止消费。下次再统一去消费这些处理失败的offset的消息。
2&#xff0c;同样消费处理失败&#xff0c;也可以利用producer事务来保证&#xff0c;比如提交offset并且把offset发送到另一个topic中&#xff0c;来保证这一系列的原子性&#xff0c;消费处理失败了&#xff0c;则中断事务&#xff0c;offset就不会被发送到topic中&#xff0c;topic中保存的还是上次那个offset。
3&#xff0c;如果消费处理成功&#xff0c;需要额外保存最新提交的offset到文件系统中&#xff0c;然后提交offset。这样不管offset提交成功还是失败&#xff0c;重启之后都可以从文件中拿到最新的offset。
4&#xff0c;或者&#xff0c;消费处理成功的同时&#xff0c;比如db操作成功的同时&#xff0c;把offset写到db中&#xff0c;意思就是consumer将offset存储和其输出在相同的位置。然后提交offset。这样我的输出位置保存的也是最新最准确的offset。
Kafka 允许 topic 的 partition 拥有若干副本&#xff0c;你可以在server端配置partition 的副本数量。当集群中的节点出现故障时&#xff0c;能自动进行故障转移&#xff0c;保证数据的可用性。
与大多数分布式系统一样&#xff0c;自动处理故障需要精确定义节点 “alive” 的概念。Kafka 判断节点是否存活有两种方式。
1. 节点必须可以维护和 ZooKeeper 的连接&#xff0c;Zookeeper 通过心跳机制检查每个节点的连接。
2. 如果节点是个 follower &#xff0c;它必须能及时的同步 leader 的写操作&#xff0c;并且延时不能太久。
就简单讲到这里 后面再续