读书笔记-《Apache Kafka实战》-4~6章

第四章: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。组下没有活跃消费者并且所有元数据已删除。

发布了25 篇原创文章 · 获赞 12 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/qq_25498677/article/details/87182748