本篇笔记在学习《Kafka权威指南》一书时记录
一、Kafka基本介绍
官方定义:
Kafka® 用于构建实时的数据管道和流式的app.它可以水平扩展,高可用,速度快,并且已经运行在数千家公司的生产环境。
Kafka是一个分布式、流式处理平台
图片来源网络1
分布式很好理解,即kafka能够分布在多个计算节点上处理同一批数据。同时具备HA、弹性扩容等能力。
什么是流式平台呢? 流式平台有以下几种特性:
1、可以发布或订阅流式记录,类似MQ或消息系统。
2、可以存储流式记录,并有较好的容错性。
3、可以实时处理流式记录。
什么又是流式数据?
1、流数据是指由数千个数据源持续生成的数据,通常也同时以数据记录的形式发送,规模较小(约几千字节)。
2、流数据(或数据流)是指在时间分布和数量上无限的一系列动态数据集合体,数据的价值随着时间的流逝而降低,因此必须实时计算给出秒级响应。
Kafka应用场景
1、构造实时数据流管道,在不同系统或应用之间搭建起数据桥梁,此时类似与其它MQ系统。
2、构建实时流式处理程序,对流式数据进行处理转换,此时比MQ系统多了一步处理流程。
Kafka四大核心API
1、The Producer API:允许一个应用程序发布一串流式的数据到一个或者多个Kafka topic。
2、The Consumer API: 允许一个应用程序订阅一个或多个 topic,并且对发布给他们的流式数据进行处理。
3、The Streams API: 允许一个应用程序作为一个流处理器,消费一个或者多个topic产生的输入流,然后生产一个输出流到一个或多个topic中去,在输入输出流中进行有效的转换。
4、The Connector API: 允许构建并运行可重用的生产者或者消费者,将Kafka topics连接到已存在的应用程序或者数据系统。比如,连接到一个关系型数据库,捕捉表(table)的所有变更内容。
二、Kafka基本组件
1、消息(Message)
1、Kafka的数据单元被称为消息。
2、Kafka消息由键值对{key: value}
组成。
- 键为字节数组,是消息的标识。
- 常用在确定消息的所属分区,确保消息的顺序性。消息进入分区前,取键值的hash值对分区数取模,保证相同键值数据进入统一分区中。
3、Kafka的消息是分批次写入的,即在传送前端做缓冲,缓冲区满了后才发送消息。这样可以提高传输效率,减小网络开销。
2、模式(Schema)
1、模式可以简单理解为Kafka传输的数据格式。如json、xml等。
2、Kafka使用Apache Avro:
- Avro 提供了一种紧凑的序列化格式,模式和消息体是分开的,当模式发生变化时,不需要重新生成代码。
- 它还支持强类型和模式进化,其版本既向前兼容,也向后兼容。
3、主题(topic)和分区(partition)
1、Kafka的消息通过 topic 进行分类,类似数据库表,或文件系统文件夹。 常见的操作如:一个数据源对应一个topic;一个应用对应一个topic。
2、主题内部分为多个 partition ,一个分区就是一个日志提交队列。在单个分区中能够保证数据的顺序性,而在整个主题中无法保证数据的顺序性
3、Kafka通过分区实现数据冗余和伸缩性,分区可以分布在不同服务器上,也就是说一个主题可以横跨多个服务器
4、生产者(producer)和消费者(consumer)
1、生产者创建消息,可以指定或不指定消息传输的分区。不指定分区则均匀的散列到所有分区。
2、消费者读取消息,可以订阅一个或多个主题,并按照消息生成的顺序读取他们。
-
消费者通过检查消息的偏移量来区分消息是否已读。
-
偏移量是一种元数据,是不断递增的整数值。在创建消息时,Kafka会将其添加进消息里。
-
每个消息的偏移量是唯一的,消费者把偏移量保存在Zookeeper或Kafka上,保证读取状态不丢失。
-
消费者是消费者群组的一部分,Kafka会保证每个分区都对应一个消费者。一个消费者可以对应多个分区,他们之间的映射关系就是所有权关系。
5、Kafka服务器(broker)和集群
1、一个独立Kafka服务器被称为broker。
2、broker 接收来自生产者的消息,为消息设置偏移量,并提交消息到磁盘保存。
3、broker 为消费者提供服务,对读取分区的请求作出响应,返回已经提交到磁盘上的消息
4、单个broker号称可以处理数千个分区以及秒级百万消息数量
5、broker的保留消息策略是保留一段时间或保留一定大小。消息达到上限时,就会删除保留消息。
6、为什么选择Kafka
1、高性能:Kafka可以无缝支持多个生产者,消费者。
2、持久化:Kafka支持消息的实时/非实时(消息持久化)处理。
3、容错:Kafka集群是灵活伸缩且容错的。
4、高性能:Kafka能够保证消息的亚秒级延迟。
三、Kafka常规配置
broker.id
集群中broker的唯一标识符,可以为任意整数,推荐设置为与机器名相关的整数,便于区分。
port
Kafka监听端口,默认为9092。
zookeeper.connect
1、指定zk地址,格式为:ip:port/path;ip:port/path;ip:port/path
。/path
为保存的zk路径,可选。如果不指定则默认为/
2、最佳实践:最好在指定kafka集群时添加/path
,因为zk集群不仅提供给一个Kafka集群,通过/path
能够很好的进行集群隔离。
log.dirs
指定Kafka持久化目录。是一组通过,
分隔的文件路径。如果指定了多个路径,则Kafka会将同一个分区的日志保存在一个目录下。
num.recovery.threads.per.data.dir
指定每个目录配置的线程数。
在如下几种情况下,这些线程会排上用场:
- 服务器正常启动,用于打开每个分区的日志片段
- 服务器崩溃后重启,用于检查和截短每个分区的日志片段
- 服务器正常关闭,用于关闭日志片段
默认情况下,一个目录只需要一个线程即可。但由于是在服务器关闭或启动时使用,因此可以配置大量线程以减小启动与关闭时间。
auto.create.topics.enable
指定是否自动创建topic。
如下情况会自动创建topic:
- 当一个生产者开始往topic写入消息时
- 当一个消费者开始从topic读取消息时
- 当任意一个客户端向topic发送元数据请求时
该配置是为非正常情况兜底的。topic未创建,则无法知道是否存在,因此就没有正常途径写入或消费消息。
四、topic的默认配置
num.partitions
新创建的topic将包含的分区数,默认为1。
设置了该值后,我们后面可以增加分区个数,但不能减小,想要设置分区数小于设置值的broker,必须手动创建。
log.retention.ms/hours/minutes
指定数据的保留时间,通常使用 log.retention.hours
,默认168小时/一周。
如果指定了不止一个参数,kafka默认使用最小的那个。
log.retention.bytes
指定数据的保留大小,作用范围为分区。如果该值设置为1G,一个主题有8个分区,则总共可以保留8G的数据。
如果同时设置了log.retention.ms/hours/minutes
和log.retention.bytes
,则任意一个要求达到时,消息都会被删除。
log.segment.bytes
指定消息片段的大小,消息被写入分区时,如果消息大于该值,则会新创建片段再写入消息。
这里就会有效率和容量的权衡问题:
1、如果片段设置的太大,比如1G,每天只有100MB的数据写入,则10天内片段不会被关闭,此时消息不会过期。因为消息的过期是从片段关闭开始计算的。
2、如果片段设置的太小,就会陷入频繁关闭片段和分配新片段情况,此时会影响消息的写入效率。
log.segment.ms
指定日志片段的关闭时间。只要片段时间和片段大小一个参数满足后就会关闭片段。
message.max.bytes
指定单个消息压缩后的最大容量,默认为1MB。如果尝试写入超过该值的消息,则消息被丢弃,同时还会接收broker返回的错误信息。
对性能的影响:该值越大,则负责处理网络连接和请求的线程就需要花越多的时间来处理这些请求,同时大容量消息同时会影响IO的吞吐量。
五、Kafka生产者
生产者写入消息的步骤:
1、创建ProducerRecord对象,该对象将必要信息(dest-topic,dest-partition等)与消息主题包装起来。
2、序列化器将ProducerRecord对象序列化为字节数组。
3、分区器处理,如果record对象包含分区信息则使用,否则按照kafka默认分区规则。
4、record被添加到一个批次里,这个批次里的所有消息都被发送到同一个topic和partition中。
5、消息发送成功返回RecordMetaData对象,如果发送失败则尝试retry,多次try后仍失败则抛出异常。
1、producer创建与发送消息
创建生产者有三个必选属性:
- bootstrap.servers:指定broker地址,只需要设置其中一个节点即可,生产者会查找到其它节点信息。
- key.serializer:record中键的序列化方式
- value.serializer:record中值的序列化方式
Properties kafkaProp = new Properties();
kafkaProp.put("bootstrap.servers", "localhost:9092");
kafkaProp.put("key.serializer", "StringSerializer");
kafkaProp.put("value.serializer", "StringSerializer");
KafkaProducer<String, String> producer = new KafkaProducer<String, String>(kafkaProp);
KafkaProducer有三种模式发送消息:
- 发送并忘记(fire-and-forget):发送消息但不关心是否到达,生产者会自动重发。
- 同步发送(sync):使用
send()
方法发送,返回Future
对象,通过Future.get()
阻塞进程,等待结果返回。 - 异步发送(async):调用
send()
方法,同时设置一个回调函数处理返回结果。
1)、直接发送消息
ProducerRecord<String, String> record = new ProducerRecord<>("topicName", "key", "value");
try {
producer.send(record);
} catch (Exception e) {
e.printStackTrace();
}
最简单的发送消息如上所示,包装好 ProducerRecord 后直接使用 send() 方法进行发送。无需关注是否发送成功。
即使如此,在发送前还有可能抛出一些异常:
- SerializationException(序列化消息失败)
- BufferExhaustedException / TimeoutException(缓冲区已满)
- InterruptException(线程发送中断)
2)、同步发送消息
ProducerRecord<String, String> record = new ProducerRecord<>("topicName", "key", "value");
try {
producer.send(record).get();
} catch (Exception e) {
e.printStackTrace();
}
send() 方法会返回一个 Future 对象,可以使用其 get() 方法阻塞线程,同步等待返回结果 RecordMetadata 对象。可以通过该对象获取消息偏移量等信息。
kafka在发送消息时一般会发生两类错误:
- 其中一类可以通过重试机制解决,发生该异常后kafka会触发重试机制,在重试多次后仍然失败,则抛出重试异常。
- 另一类无法通过重试解决,此时kafka会直接抛出异常。
3)、异步发送消息
private class DemoProducerCallback implements Callback {
@Override
public void onCompletion(RecordMetadata recordMetadata, Exception e) {
if (e != null) {
e.printStackTrace();
}
}
}
ProducerRecord<String, String> record = new ProducerRecord<>("topicName", "key", "value");
producer.send(record, new DemoProducerCallback());
异步发送消息的处理过程:
- 编写一个实现 Callback 接口的类,重写 onCompletion() 方法。
- 如果kafka返回一个错误,则 onCompletion() 方法则会携带一个非空(not null)异常,应用可以根据自行需要进行处理。
- 在发送消息时传入该对象。
2、producer的配置
1)、acks
acks 参数指定了必须要有多少个分区副本收到消息,生产者才会认为消息写入是成功的。
- acks=0:生产者在写入消息后立即认为写入成功,不用等待服务器响应结果。如果写入失败,生产者也无从得知。但此时吞吐量最高。
- acks=1:只要集群首领节点接收到消息后,就会返回生产者写入成功的响应。如果首领节点未接收到消息,则返回生产者错误响应,触发生产者重试机制。
- acks=all:只有集群中所有节点接收到消息后,才会返回写入成功的响应。此时吞吐量最低。
2)、buffer.memory
指定生产者缓冲区大小。
如果生产消息的速度大发送到服务器的速度,会导致生产者空间不足。此时根据 block.on.buffer.full
参数决定是直接抛出异常还是阻塞一段时间。
3)、compression.type
指定消息被发送前采用哪种压缩算法压缩消息。默认不压缩。
4)、retries
决定了生产者可以重发消息的次数,如果达到这个次数,生产者会放弃重试并返回错误。
默认情况下,重试间隔为100ms,但可以通过 retry.backoff.ms
设置。
5)、batch.size
指定同一个批次所占的内存大小。当批次未达到该值时 producer 也可能发送一个批次。
6)、linger.ms
指定生产者在发送批次前等待更多消息加入的时间。producer 会在 batch.size 被占满或者达到 linger.ms 的等待时间时,就会发送一个批次的消息。
7)、client.id
表示客户端ID,表示消息来源。
8)、max.in.flight.requests.per.connection
指定在收到服务器响应前可以发送的消息数量。如果设置该值 max.in.flight.requests.per.connection=1
,则可以保证消息是按照顺序写入分区的,即使发生了重试。
9)、timeout.ms / request.timeout.ms / metadata.fetch.timeout.ms
request.timeout.ms:producer在发送数据时等待服务器响应的时间。
metadata.fetch.timeout.ms:producer在获取元数据时等待服务器响应的时间。
以上两个参数:如果等待响应超时,那么生产者要么重试发送数据,要么返回一个错误(抛出异常或执行回调)。
timeout.ms:指定了broker 等待同步副本返回消息确认的时间。
10)、max.block.ms
当生产者缓冲区已满,send() 方法就会阻塞,如果阻塞超过一定时长,生产者会抛出超时异常。
11)、max.request.size
用于指定 producer 发送的请求大小,可以表示单个消息的最大值,也可以表示单个请求中所有消息的大小。
12)、receive.buffer.bytes / send.buffer.bytes
指定TCP socket 发送和接收数据包的缓冲区大小。如果设置为-1,则使用系统默认值。
3、序列化器
kafka sdk 带了很多序列化器,如字符串、整形、字节数组等。
这里主要介绍如何自定义一个序列化器:
// 首先创建一个 people pojo
class People {
String name;
Integer age;
// 省略 get/set
}
class PeopleSerializer implements Serializer<People> {
@Override
public void configure(Map configs, boolean isKey) {
// 不做任何配置
}
@Override
public byte[] serialize(String topic, People people) {
byte[] serializedName;
int stringSize;
if(people == null) return null;
else {
if(people.getName() != null) {
serializedName = people.getName().getBytes("UTF-8");
}
stringSize = serializedName.length;
ByteBuffer buffer = ByteBuffer.allocate(4 + 4 + stringSize);
buffer.putInt(people.getAge());
buffer.putInt(stringSize);
buffer.put(serializedName);
return buffer.array();
}
}
@Override
public void close() {
// 不需要关闭任何东西
}
}
4、分区
- 如果 producer 在发送消息时未指定 key,则默认分区器按照轮训算法将数据均匀的发送到各个分区上。
- 如果指定了key,则使用 kafka 内置的散列算法对键值hash到不同分区上。
- 如果指定了分区,则发送到指定分区上。
- kafka还支持自定义分区策略,通过实现 Partitioner 接口即可。
六、Kafka消费者
1、消费者和消费者组
- kafka consumer 从属于 consumer group。一个消费者组订阅一个主题,其中的消费者订阅该主题中的部分分区。
- 一个分区只对应一个消费者,一个消费者可对应多个分区。因此当消费者数量大于分区数时,部分消费者被闲置。
- 多个消费者组也可以订阅同一个主题。
- 再均衡:当分区的所有权从一个消费者转移到另一个消费者时发生的动作
- 主题中分区被添加或删除时会发生再均衡
- 消费者组中消费者被添加或删除时会发生再均衡
- 再均衡会导致整个消费者组的暂时不可用
- 由于分区被重置,消费者当前的读取状态会因为再均衡而丢失
2、创建消费者
消费者创建与生产者创建类似,都必填bootstrap.servers
、key.serializer
、value.serializer
三个属性,group.id
非必填,其能够标识消费者从属哪个消费者组。
Properties kafkaProp = new Properties();
kafkaProp.put("bootstrap.servers", "localhost:9092");
kafkaProp.put("group.id", "xu123");
kafkaProp.put("key.serializer", "StringSerializer");
kafkaProp.put("value.serializer", "StringSerializer");
KafkaConsumer<String, String> producer = new KafkaConsumer<String, String>(kafkaProp);
3、订阅主题
消费者通过 consumer.subscribe()
方法订阅主题,其接收一个主题列表作为参数。同时也可以接收正则表达式。
4、轮询消费
consumer通过 poll(timeout)
方法开启轮询,其封装了查找消费者组,接收分配分区,再均衡,接收消息,发送心跳等所有过程。用户只需要通过这个简单的API获取数据,然后完成自己的业务即可。
try{
while(true) {
ConsumerRecords<String, String> records = consumer.poll(100);
// records为消费的消息,开发人员接下来可以实现自己的业务。
// ...
}
} finally {
// 记得关闭连接
consumer.close();
}
tips:一个消费者使用一个线程,最好是把消费者的逻辑封装在自己的对象里,然后使用 Java 的 ExecutorService 启动多个线程,使每个消费者运行在自己的线程上。
5、消费者配置
1)、fetch.min.bytes
指定消费者从服务器获取记录的最小字节数。broker会等到有超过该值的可用数据时,才会将数据给消费者。
2)、fetch.max.wait.ms
指定broker等待多长时间将数据返回消费者。如果想要减少时延,可以减小该值。该值与 fetch.min.bytes
任何一值满足要求,broker将数据返回给消费者。
3)、max.partition.fetch.bytes
指定了服务器从每个分区里返回给消费者的最大字节数。默认值是1MB。设置的值的计算方式为 分区数 / 消费者
MB。在设置时可以设置比该值稍大,这样可以防止再分区时容量不够的情况。
4)、session.timeout.ms
指定消费者与broker断开的最长时间,超过这个时间则认为消费者已经死亡。该值与 heartbeat.interval.ms
值相关,heartbeat.interval.ms
表示消费者发送心跳间隔,该值必须设置比 session.timeout.ms
小,一般为 1/3 。
5)、auto.offset.reset
auto.offset.reset = latest:如果消费者读取的分区没有偏移量,则从最近的消息开始读起
auto.offset.reset = earliest:如果消费者读取的分区没有偏移量,则从最早的消息开始读起
6)、enable.auto.commit
指定是否自动提交偏移量,默认值为true。为了尽量避免出现重复数据和数据丢失,可以将其设置为false。
7)、partition.assignment.strategy
指定分区分配策略。
- Range:broker会将连续的分区分配至一个消费者上。
- RoundRobin:broker将分区轮训分配至所有消费者上。
8)、client.id
标识消费者的id号。
9)、max.poll.records
设置单词 pool() 返回的记录量。
10)、receive.buffer.bytes / send.buffer.bytes
socket 在读取时的TCP缓冲区大小。如果设置为-1,则使用系统默认值。
6、提交和偏移量
kafka分区中的消息并不会在消费者读取完后就丢失,而是一直保存在那里。分区上存在多个游标对应不同消费者。游标走过的位置表示消费者已经读取过的消息,未走过的位置表示将要读取的消息。
消费者更新其在分区上的位置叫做提交。
发生再均衡时,消费者需要被重新分配分区,消费者将会从该分区的上一次偏移量开始消费消息。此时便会出现两种情况:
- 提交的偏移量小于客户端处理的最后一个消息的偏移量:此时会有部分消息被重复读取。
- 提交的偏移量大于客户端处理的最后一个消息的偏移量:此时会有部分数据被丢失。
因此,如何处理偏移量对消费者有很大影响。
1)、自动提交
设置 enable.auto.commit=true
。消费者会自动提交从 poll()
方法获得的偏移量。
但是自动提交是有时间频率的,如果在时间频率内发生再均衡,则会存在消息读取错乱现象。
在这种情况下,消费者会在调用 poll() 方法时将上次一调用返回的最大偏移量上传,此时最好确保所有消息已经消费完毕。否则会存在消息丢失问题。
2)、提交当前偏移量
通过 commitSync()
方法手动提交由 poll()
获取的最新偏移量。
每次 consumer 消费数据后,在完成业务数据处理后,调用该方法确保偏移量被正确提交。提交失败会抛出异常,用户可以根据特定处理方式处理异常。
while(true) {
ConsumerRecords<String, String> records = consumer.poll(100);
// records为消费的消息,开发人员接下来可以实现自己的业务。
// ...
}
try {
consumer.commitSync();
} catch (Exception e) {
// 处理异常
}
}
3)、异步提交偏移量
commitSync()
方法在broker返回前会一直阻塞,并且在成功提交前一直重试。
而 commitAsync()
不会,其只会提交一次偏移量。不重复提交的原因是在两次异步提交间,消费者可能会多次提交偏移量,导致重复消息。
while(true) {
ConsumerRecords<String, String> records = consumer.poll(100);
// records为消费的消息,开发人员接下来可以实现自己的业务。
// ...
}
commitAsync();
}
commitAsync()
支持回调,如果提交失败,我们可以在回调中处理失败的情况。
4)、同步和异步组合提交
try {
while(true) {
ConsumerRecords<String, String> records = consumer.poll(100);
// records为消费的消息,开发人员接下来可以实现自己的业务。
// ...
}
commitAsync();
}
} catch (Exception e) {
} finally {
try {
consumer.commitSync();
} finally {
consumer.close();
}
}
5)、提交指定的偏移量
commitSync()
和 commitAsync()
方法支持在提交时传入< 提交分区,偏移量 > 的map,允许用户在处理数据间提交偏移量。
6)、再均衡监听器
在消费者订阅某个主题时,用户可以传入一个 ConsumerRebalanceListener
实例,令消费者发生再均衡时处理一些事务,比如:提交偏移量、处理缓冲区等。
ConsumerRebalanceListener
实例需要实现两个方法:
- public void onPartitionsRevoked(Collection< TopicPartition > partitions):该方法会在再均衡开始之前和消费者停止读取消息之后被调用。如果在这里提交偏移量,下一个接管分区的消费者就知道该从哪里开始读取了。
- public void onPartitionsAssigned(Collection< TopicPartition > partitions): 方法会在重新分配分区之后和消费者开始读取消息之前被调用。
7)、退出轮询
如果确定要退出 poll() 循环,用户可以在另一个线程中调用 consumer.wakeup()
方法。如果循环运行在主线程中,用户可以在shutdownHook中调用该方法。但记住,在退出轮询前,先执行 consumer.close()
方法,提交任何还未被提交的东西,通知broke准备退出了,触发再均衡。
8、独立的消费者
消费者除了订阅主题,还可以直接订阅分区,这样就不存在再均衡等情况。
用户可以首先通过 consumer.partitionsFor("topic")
方式请求指定主题可用的分区,然后通过 assign(partitions);
方法订阅分区列表。
但主题中新增分区后不会通知消费者,消费者需要周期调用 partitionsFor 方法校验是否有新分区加入。
http://kafka.apache.org/ ↩︎