KAFKA 海量吞吐低延迟技术解密:KafkaProducer

1、导读

笔者在生产交付的项目中使用了 KAFKA,为了更好地掌握 KAFKA,业余时间阅读了部分源码。KAFKA 生产者的代码中有很多的精妙绝伦的设计,非常值得借鉴学习。本文将探讨 KafkaProducer 的消息发送流程、高并发场景下消息的缓冲机制、缓冲机制是如何通过分段加锁和读写分离巧妙提升吞吐和并发的、为减少频繁 FGC 设计的内存池、消息重复发送和消息丢失的场景。笔者水平有限,若有不当之处,请不吝指正。

2、消息发送的流程

KAFKA 生产者客户端分别由主线程和 Sender 线程协调运行,主线程负责由 KafkaProducer 创建消息,经过拦截器、序列化和分区选择后缓存追加至 RecordAccumulator(消息累加器)中,Sender 线程负责从 RecordAccumulator 中批量取出消息并发送至 KAFKA 服务端。

RecordAccumulator(消息累加器)负责缓存生产者客户端产生的消息,按消息的目标分区进行攒批,攒批的大小可以通过生产者客户端参数batch.size配置(默认为 16KB),整个缓存的大小可以通过生产者客户端参数buffer.memory配置(默认为 32 MB)。RecordAccumulator(消息累加器)在内部设计上为每个分区都维护了一个 Deque(双端队列),队列中存放了每个分区的 ProducerBatch(消息攒批),消息是追加到 ProducerBatch 的尾部,因此消息是分区有序的;

消息发送的流程见下图:

※ 消息发送的各关键阶段:

① 用户使用生产者客户端发送消息;

② KafkaProducer 主线程唤醒 Sender 线程(Sender 线程在客户端初始化过程中启动并阻塞),Sender 线程向 KAFKA 服务端请求获取集群元数据并维护在客户端内存 Metadata 对象中;

③ 消息经过拦截器、序列化和分区后,追加至 RecordAccumulator(消息累加器) 中;

④ Sender 线程轮询 RecordAccumulator,待缓冲区数据批次就绪后,取出消息块;

⑤ Sender 线程构造消息发送的请求,并封装为 ClientRequest 后提交给 NetworkClient,准备发送;

⑥ NetworkClient 将请求放入 KafkaChannel 缓存中,执行网络 IO,发送至 KAFKA 服务端;

⑦ NIO Selector 收到请求响应,进行 TCP 拆包后解析成 NetworkReceive 对象放入 Deque 队列中,由 NetworkClient 经过一连串的请求响应处理器后,封装成 ClientResponse;

2.1、消息拦截器

消息拦截器可以在消息发送前对其进行拦截或修改,也可对ACK响应进行预处理,通常可用于监控埋点、消息审计等用途。用户如果自定义消息拦截器,需要实现接口org.apache.kafka.clients.producer.ProducerInterceptor,生产者会按照一个或多个自定义的拦截器的顺序,有序地作用于同一条消息从而形成一条拦截链。

public interface ProducerInterceptor<K, V> extends Configurable {
  	// 消息在发送前拦截,此拦截器返回的结果将传递至下一个消息拦截器
	public ProducerRecord<K, V> onSend(ProducerRecord<K, V> record);
  	// 消息在发送至KAFKA服务端结束后(成功或失败),在调用用户的Callback前进行拦截
    public void onAcknowledgement(RecordMetadata metadata, Exception exception);
  	// 消息拦截器的关闭
    public void close();
}

2.2、消息序列化

客户端和服务端的网络通信是基于 Java NIO 实现的,传输的数据类型是 Byte 数组。KAFKA 生产者在初始化时通过反射方式分别指定了 Key、Value 序列化器,KAFKA 客户端提供了针对各种数据类型的序列化器,包路径位于:org.apache.kafka.common.serialization.*,用户也可通过实现接口org.apache.kafka.common.serialization.Serializer和org.apache.kafka.common.serialization.Deserializer来自定义序列化器和反序列化器。

2.3、元数据加载

用户在使用客户端发送消息时,仅指定了 Topic、部分 Broker IP,而生产者最终是要将消息发送到指定 Topic 下某个分区的 Leader 副本所在的服务节点上的,因此需要知道 KAFKA 集群的元数据信息,包括 Topic 有多少分区、各分区的 Leader 副本分配在哪个服务节点上、Follower 副本分配在哪些服务节点上、哪些副本在ISR集合中、服务节点的地址和端口等信息,这些元数据会在某个合适的时机加载到客户端的 Metadata 对象中,主要成员如下:

public final class Metadata {
    // 两次元数据过期刷新之间的时间间隔
    private final long refreshBackoffMs;
    // 元数据的保留时长
    private final long metadataExpireMs;
    // 客户端内存中 KAFKA 集群元数据的版本号,每次过期刷新都自增
    private int version;
    // 上次刷新的时间戳(包括刷新失败的场景)
    private long lastRefreshMs;
    // 上次成功刷新的时间戳
    private long lastSuccessfulRefreshMs;
    // KAFKA 集群的元数据
    // 1、Broker 节点的ID、Host、IP地址、端口
    // 2、KafkaController 的节点信息
    // 3、Topic 下各个分区的ID、Leader副本所在的服务节点信息、所有副本所在的节点信息、ISR集合中所有副本所在的节点信息、OSR集合中所有副本所在的节点信息
    private Cluster cluster;
    // 是否需要强制刷新元数据
    private boolean needUpdate;
    // Topic 的元数据过期时间,默认5分钟
    private final Map<String, Long> topics;
    // 是否允许自动创建Topic(Broker配置了auto.create.topics.enable=true,且KAFKA集群不存在指定的Topic时自动创建同名Topic)
    private final boolean allowAutoTopicCreation;

  	...
}

2.3.1、懒加载

KAFKA 生产者客户端在初始化时并不会加载元数据,这是懒加载的编程思想,元数据是由 Sender 线程异步加载的。下图展示了生产者客户端发送消息时加载元数据的时序:

生产者客户端在初始化过程中,Sender 线程首先与 Broker 建立 TCP 连接,随后进入阻塞。加载元数据的关键步骤如下:

1)    【主线程】生产者客户端发送消息时,主线程会唤醒 Sender 线程,随后主线程阻塞,直到 Sender 线程完成元数据的加载。主线程和 Sender 线程的通信是基于 wait/notify 机制;

private ClusterAndWaitTime waitOnMetadata(String topic, Integer partition, long maxWaitMs) throws InterruptedException {
  	...
    do {
      // 唤醒Sender线程
      sender.wakeup();
      try {
        // 内部调用wait(),主线程阻塞
        metadata.awaitUpdate(version, remainingWaitMs);
      } catch (TimeoutException ex) {
        throw new TimeoutException("Failed to update metadata after " + maxWaitMs + " ms.");
      }
		...
}

2)    【Sender线程】构造获取元数据的请求,并发送到负载最小的服务端节点;

private long maybeUpdate(long now, Node node) {
	// 1、Broker是否可连接
  String nodeConnectionId = node.idString();
  if (canSendRequest(nodeConnectionId)) {
		...
    MetadataRequest.Builder metadataRequest;
    if (metadata.needMetadataForAllTopics())
      metadataRequest = MetadataRequest.Builder.allTopics();
    else
      metadataRequ
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值