第四章:producer 开发
Kafka封装了一套二进制通信协议,并且有多种语言的实现,以下探讨的都是Java版。
producer独立进行工作,相互之间没有关联。
producer工作流程:
1. 用户主线程将待发送的消息封装进ProducerRecord
2. 交给序列化器序列化
3. 交给partitioner确定分区,并发送到消息缓冲区。如果指定了key,则是根据key的hash值对分区数取模(联想:很常见的操作,与Java中的HashMap.put方法逻辑相同)得到partition,如果没有指定key,则轮询,确保均匀。
4. I/O发送线程实时地从缓冲区中提取出就绪的消息,并封装进一个batch,发送给broker
关于KafkaProducer.send()(即上述步骤234)
1. 序列化+计算目标分区。
2. 追加写入消息缓冲区。缓冲区保存待发送的消息,其中有一个关键的HashMap——消息批次信息,key是分区,value是batch集合。这步结束后send()就执行完毕了,后面用户主线程等待Sender线程发送消息并执行返回结果。
3. Sender线程预处理及消息发送。
- 不断轮询缓冲区,寻找已做好发送准备的分区。
- 将轮询获得的各个batch进行分组
- 将分组后的batch通过底层创建的Socket连接发送给各个服务器
- 等待服务器发送response
4. Sender线程处理response。按照消息发送顺序调用batch中的回调方法。
构造producer:
1. 构造 java.util.Properties 对象,必须指定bootstrap.servers即IP端口号(用于故障转移,producer可自动发送集群中的所有broker。)、key.serializer即key的序列化器、value.serializer即value的序列化器。
2. 使用上述对象构造KafkaProducer对象
3. 构造待发送的消息对象ProducerRecord
4. 调用KafkaProducer.send()。包括三种发送方式,同步、异步、fire and forget。默认为异步发送,提供了回调函数;同步发送会一直等待broker的结果,性能较差,一般不使用;fire and forget是直接发送不处理结果,一般不使用。
可重试异常:Leader换届选举时的LeaderNotAvailableException、controller不可用的NotControllerException、网络故障NetworkException。
不可重试异常:消息过大的RecordTooLargeException、序列化异常的SerializationException、其他的KafkaException等等
5. 调用KafkaProducer.close()。结束时必须关闭,来释放线程、内存、套接字等资源。
producer主要参数:
- acks
acks | 吞吐量 | 持久性 | 含义 |
0 | 高 | 低 | producer完全不关心Leader broker端的处理结果,发送消息后立即发送下一条。 (联想:与上文中的调用方式是否会冲突?如果配置all但是fire and forget,待测试) |
1 | 中 | 中 | 等待Leader写入本地日志 |
all或-1 | 低 | 高 | 等待Leader和Follower都写入本地日志 |
- buffer.memory。缓冲区的大小。
- compression.type。是否压缩消息,默认为none。
- retries。发送失败后重试次数,注意重试可能引起重复发送、乱序的问题。
- batch.size。批次的大小。
- linger.ms。消息发送延时。
- max.request.size。消息大小的上限值。
- request.timeout.ms。broker返回结果的超时时间。
自定义分区与自定义序列化:可实现定制化逻辑,如将指定topic放入固定分区,其他topic随机存放。
producer拦截器:可实现定制化逻辑,如在消息发送前或回调函数前修改消息。
消息压缩:I/O与CPU的平衡。Zstandard > LZ4 > Snappy > GZIP
多线程处理:多线程单KafkaProducer和多线程多KafkaProducer
方案 | 说明 | 优势 | 劣势 |
单实例 | 所有线程共用一个实例。适用于分区数少的场景。 | 实现简单,性能好 | 耦合,易雪崩 |
多实例 | 每个线程维护自己的实例。适用于分区数多的场景。 | 可进行细粒度的调优; 不耦合,不会雪崩 |
第五章:consumer 开发
语言 |
API包名 |
API类名 |
备注 |
|
新版本 |
Java |
org.apache.kafka.clients.consumeer.* |
KafkaConsumer |
颠覆位移的管理和保存 |
旧版本 |
Scala |
kafka.consumer.* |
ZookeeperConsumerConnector SimpleConsumer |
不使用消费组并且使用low-level consume时,需要自行实现错误处理和故障转移 |
消费者组:topic的每条消息都只会被发送到订阅它的消费者组的一个消费者实例上。是实现高伸缩性、高容错性的消费者机制。
所有消费者实例属于同一个group -> 使得Kafka基于队列模型 / 属于不同的group -> 使得Kafka基于发布订阅模型。
位移(offset):消费者实例消费了多少条消息。
许多消息引擎将位移保存在服务器端,优缺点(联想:有哪些?):
- 实现简单。
- 服务器带状态,增加同步成本,影响伸缩性。
- 需要引入应答机制来确认消费成功。
- 增加存储开销。
Kafka将位移保存在消费者组里,并引入检查点机制定期持久化。
关于位移提交 | 方式 | 备注 |
新版本 | 提交到Kafka内部topic(__consumer_offsets)上 | 该topic会有50个分区,group.id通过hash取模确定分区 消息类似于key=group.id+topic+partition value=offset Kafka定期对该topic进行压实(compact),控制大小。 默认5秒自动提交一次。 |
旧版本 | 定期提交到ZooKeeper的固定节点 /consumers/<group.id>/offsets/<topic>/<partitionId> |
ZooKeeper本质上实服务协调组件, 不擅长处理高并发的读写操作。 |
常见消息交付语义:
- 最多一次。消息可能丢失,但不会被重复处理。
- 最少一次。消息不回丢失,但可能被处理多次。
- 精确一次。消息一定且只会被处理一次。
如果消费者在消费消息之前就提交位移,则可实现最多一次;之后提交则可实现最少一次。(考虑消费者崩溃重启的情况)
提交方式 | 使用方法 | 优缺点 | 交付语义保证 | 使用场景 |
自动提交 | 1.默认 2.enable.auto.commit=true |
开发成本低; 无法精准控制,失败后不易处理 |
可能丢失消息 最多实现“最少一次” |
容忍一定的消息丢失 |
手动提交 | 1.enable.auto.commit=false 2.手动调用commitSync/commitAsync |
精准控制; 开发成本高,需要自己处理 |
易实现“最少一次”, 可实现”精准一次“ |
消息处理逻辑重,不允许消息丢失 |
幂等性生产者:
- 需要设置enable.idempotence=true
- 类似于TCP的工作方式。Kafka给发送到服务器的每批消息都加上序列号,从0开始递增用于消息去重,并将序列号保存在底层日志。Kafka给生产者赋予了id,这样通过生产者id、分区号、序列号就可以唯一确定消息。当收到序列号小于等于的消息则为重复消息。
构建consumer:
1.构建一个java.util.Properties对象,必须指定bootstrap.servers、key.deserializer、value.deserializer、group.id,参数含义可参考producer。
2.使用上述对象构造KafkaConsumer对象
3.调用KafkaConsumer.subscribe() 订阅consumer group的topic列表
4.循环调用KafkaConsumer.poll() 获取封装在ConsumerRecord的topic消息。关键方法,旧版本采用多线程,每个分区都创建线程处理,当指定线程数大于分区数时则会浪费;新版本consumer的poll方法类似Linux的select I/O 机制(所有的事件如重平衡、获取消息都发生在一个事件循环中,这样一个线程就能完成所有I/O操作)。(联想:Linux?一个循环就是一个poll吗?)
5.处理获取到的ConsumerRecord(消费速度慢:如果是poll返回消息慢,可考虑调节相应参数提升poll方法效率;如果是消息的业务处理慢,可考虑简化逻辑或者把逻辑放入单独的线程执行。)
6.关闭KafkaConsumer。释放线程资源、内存、Socket连接等。
consumer主要参数:
- session.timeout.ms。消费者组建协调者(coordinator)检测消费者崩溃的时间,默认为10秒。
- max.poll.interval.ms。消费者逻辑处理最大时间。如果消费者两次poll间隔大于该值,协调者会认为这个消费者已经跟不上其他消费者的消费速度,并将其踢出消费组。此时,不但需要重平衡,还会造成因未提交位移而导致的重复消费。
- auto.offset.reset。无位移信息或位移越界时Kafka的应对策略。可选参数earliest(从最早的位移开始消费)、latest(从最新的位移开始消费)、none(直接抛出异常,不常用)。
- enable.auto.commit。消费者是否自动提交位移。有“精准处理一次”需求的场景建议设置为false。
- fetch.max.bytes。消费者单次获取数据的最大字节数。
- max.poll.records。单次poll调用返回的最大消息数,默认为500。如果消息处理逻辑很轻量可适当增加该值。
- heartbeat.interval.ms。当协调者决定发起重平衡时,会以REBALANCE_IN_PROGRESS异常的形式放入消费者心跳请求的response中,消费者获取到response后便知道要参加下一次重平衡了。推荐设置一个较小值。该值必须小于session.timeout.ms,毕竟在这个时间点到达的时候协调者已经认为该消费者崩溃了,就没必要再发给它重平衡的请求了。
- connections.max.idle.ms。Kafka定期关闭空闲Socket连接的时间,默认9分钟。关闭后下次需要重新建立连向服务器端的Socket连接,可能会导致周期性处理时间飙升,可设置为-1表示不关闭。
订阅topic:
- 订阅列表。消费者订阅是延时生效的,即下次poll调用时生效。
- 正则表达式订阅。如果没有配置enable.auto.commit=true,则需要指定ConsumerRebalanceListener,通过实现这个回调接口来实现重平衡的逻辑。
consumer.subscribe(Arrays.asList("topic1","topic2","topic3")); // 消费组订阅方式
TopicPartition tp1 = new TopicPartition("topic1", 0); // 独立消费者订阅方式
TopicPartition tp2 = new TopicPartition("topic1", 1);
consumer.assign(Arrays.asLisst(tp1, tp2));
// 正则订阅
consumer.subscribe(Pattern.compile("kafka-.*"), new ConsumerRebalanceListener()...)
消息轮询:
- 新版本Java consumer是一个双线程的Java进程,创建KafkaConsumer的线程被称为用户主线程,同时consumer会在后台创建一个心跳线程。
- poll方法返回订阅的分区上的一组消息,如果分区没有准备好,则为空。
- poll方法返回条件:获取了足够多的数据 / 等待了足够长的时间。在有定时任务的业务场景中,如每10秒将消费情况记录到日志,需设置超时时间。
- 新版本Java consumer是线程不安全的,不要将同一个KafkaConsumer实例用在多线程中。
// 有定时任务的业务场景
volatile Boolean isRunning = true;
try {
while (isRunning){
ConsumerRecords<String, String> records = consumer.poll(1000);
for (ConsumerRecord<String, String> record : records){
// 获取record后 处理业务逻辑等
}
}
} finally {
isRunning = false;
consumer.close();
}
// 没有的场景
try {
while (true){
ConsumerRecords<String, String> records = consumer.poll(Long.MAX_VALUE);
for (ConsumerRecord<String, String> record : records){
// 获取record后 处理业务逻辑等
}
}
} catch (WakeupException e){
// 不做操作
// 需要在另一个线程中调用consumer.wakeup()来触发consumer的关闭 只有这个方法是线程安全的
} finally {
consumer.close();
}
消费组者重平衡:分配哪些消费者处理哪些分区的过程。
触发条件:消费者组成员变更、订阅topic数变更、订阅topic的分区变更。最常见为消费者组成员变更,当消费者无法在指定时间完成消息的处理,协调者会认为消费者已经崩溃,从而引发新一轮重平衡。(避免消息逻辑过重且多次重试,容易引发不断的重平衡,降低吞吐量)
分配策略:(如果消费者组里一个消费者的策略不同于其他消费者,则会被拒绝加入组)
- range。顺序排列分区,依次分配。新版本默认策略。
- round-robin。顺序排列分区,轮询式分配。
- sticky。参考历史分配方案,有粘性的分配。
协议:
- JoinGroup。消费者加入组。
- SyncGroup。leader把分配方案同步更新到组内成员。
- Heartbeat。消费者定期向协调者汇报心跳。协调者响应时会告知是否开启新一轮重平衡。
- LeaveGroup。消费者告知协调者自己将离开组。
- DescribeGroup。查看组的所有信息,包括成员、协议、分配方案、订阅等信息。
重平衡流程:
- 加入组。所有消费者向协调者发送JoinGroup,收集好后协调者选择一个消费者作为leader,并把所有成员信息、订阅信息发送给leader。
- 同步更新分配方案。leader制定方案,分配完成后将方案放进SyncGroup发送给协调者,协调者收到后通过SyncGroup的响应分别发给每个消费者。
Kafka将分配放在客户端的好处:用户可以自行实现分配方案。例如Hadoop的机架感知(rack-aware)方案,同一个机架的分区数据被分配给相同机架上的消费者,减少网络传输开销。同时,变更策略也不需要重启服务器,重启消费者客户端即可。
多线程消费实例:
- 每个线程维护一个KafkaConsumer。优缺点:实现简单,无线程切换开销,方便位移管理,易维护分区间的顺序消费;Socket连接开销大,消费者数受限于分区数导致扩展性差,服务器端处理请求负载高,重平衡触发概率高。
- 单KafkaConsumer实例+多worker线程。解耦消息的获取与处理,将后者放入单独的worker线程中。优缺点:消息的获取与处理解耦,可独立扩展消费者和worker;实现复杂,难维护分区内的消费顺序,处理链路变长导致不易维护位移,worker线程异常可能导致消费数据丢失。
旧版本消费者:
- high-level consumer。消费者组依赖ZooKeeper来实现组管理、错误检测、故障转移、负载均衡、重平衡、位移提交。
- low-level consumer。简化了消费者的消费和消费者组的管理。使用场景:需要重复读取历史数据,只想消费部分分区数据,实现“精准一次”。缺点:需要自己实现位移提交、leader选举。(Apache Storm的storm-kafka便是基于此实现。)
第六章:Kafka 设计原理
服务器架构设计(核心组件,承担了持久化消息、传输消息队列中的消息等职责)
消息设计:
- 使用Java NIO的ByteBuffer来保存消息,同时依赖文件系统提供的页缓存机制。ByteBuffer是紧凑的二进制字节结构,不需要字段重排,省去了很多的对象开销。
- Kafka 从0.10之前,到0.10,到0.11,消息的数据结构不断优化,仅凭数据结构的变化,节省了更多的磁盘空间,并在支持事务、幂等性的同时减少网络I/O和磁盘I/O的开销。
集群管理:待学习。
副本与ISR机制:待学习。
日志存储:
- 传统的日志为方便人们阅读,常常是结构松散的请求日志、错误日志或其他数据。Kafka的日志专供程序阅读,类似于关系型数据库中的记录,将消息和一些元数据封装成记录,即消息集合或消息批次。
- Kafka的日志以分区为单位,即每个分区都有自己的文件夹,自己的日志。对于每个日志来说,又细分为日志段文件(.log)和索引文件(.index .timeindex)。
- 日志段文件默认大小1GB,填满后则会创建新的日志段文件和索引文件。非当前正在使用的日志文件只会以“只读”模式打开,当前正在使用的日志文件不会受到Kafka后台任务的影响,比如定期清除任务和定期日志压实任务。
- Kafka日志索引文件属于稀疏索引文件,待写入默认4KB的数据后才增加一个索引项;它们都是升序排列,方便使用二分查找。.index为位移索引文件,用于定位记录所在的物理文件位置,包括4字节的相对位移(与索引文件名中的起始位移的差值)和4字节的物理位置;.timeindex为时间戳索引文件,用于定位给定时间戳的位移信息,包括8字节的时间戳和4字节的相对位移。
- Kafka日志清除策略分为基于时间(如清除7天前的)和基于大小(日志大于10GB不再保存,默认不会开启这个策略)。
- Kafka日志压实机制保证了topic每个分区下每条具有相同key的消息都至少保存了最新value的消息。Kafka Cleaner组件负责从日志中移除已废弃的消息,即一条消息的key在日志中已存在,并且位移小于已存在消息的位移,则认为已废弃。(联想:和GC中的标记-压缩算法大同小异)
请求处理协议:待学习。
controller设计:待学习。
producer设计-数据结构:
- ProducerRecord(待发送的消息):topic、partition、key、value、timestamp。
- RecordMetadata(Kafka服务器返回给客户端的消息):offset、timestamp、topic/partition、checksum、serializedKeySize、serializedValueSize。
consumer设计-组状态机:
- Empty。组下没有活跃消费者,但可能包含位移信息。
- PreparingRebalance。正在准备重平衡。
- AwaitingSync。所有成员加入组,并等待leader发送分配方案。
- Stable。正常消费状态。
- Dead。组下没有活跃消费者并且所有元数据已删除。