kafka生产者剖析

Kafka生产者程序概述

开发一个生产者的步骤:

  1. 构造生产者对象所需的参数对象;
  2. 利用第一步的参数对象,创建 KafkaProducer 对象实例;
  3. 使用 KafkaProducer 的 send 方法发送消息;
  4. 调用 KafkaProducer 的close方法关闭生产者并释放各种系统资源;
Properties props = new Properties ();
props.put(“参数 1, “参数 1 的值”);
props.put(“参数 2, “参数 2 的值”);
……
// try-with-resource,自动释放资源
try (Producer<String, String> producer = new KafkaProducer<>(props)) {
            producer.send(new ProducerRecord<String, String>(……), callback);
	……
}

生产者消息分区机制

Kafka 主题(Topic)是承载真实数据的逻辑容器,而在主题之下还分为若干个分区,也就是说 Kafka 的消息组织方式实际上是三级结构:主题 - 分区 - 消息。主题下的每条消息只会保存在某一个分区中,而不会在多个分区中被保存多份。而如何将大量的消息数据均匀的分到到Kafka的各个Broker上,就是一个比较重要的问题;

分区的作用

分区主要作用就是提供负载均衡的能力,或者说对数据进行分区的主要原因,就是为了实现系统的高伸缩性(Scalability)。不同的分区能够被放置到不同节点的机器上,而数据的读写操作也都是针对分区这个粒度而进行的,这样每个节点的机器都能独立地执行各自分区的读写请求处理。并且,我们还可以通过添加新的节点机器来增加整体系统的吞吐量。
另外,通过分区,也可以实现一些业务级别的需求;比如实现业务级别的消息顺序问题;

分区策略

分区策略:决定生产者将消息发送到哪个分区的算法。Kafka支持自定义分区策略;

自定义分区策略:
	1. 编写生产者程序时,编写一个实现 `org.apache.kafka.clients.producer.Partitioner` 接口的类;
		通常实现 partition()方法即可;
	int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster);
	2. 显式的配置生产者端参数:`partitioner.class`
  • 轮询策略:Round-robin策略,顺序分配;分区位置 = 消息offset % 分区数;轮询为Kafka Java生产者API默认提供的分区策略;
    轮询策略有非常优秀的负载均衡表现,它总是能保证消息最大限度地被平均分配到所有分区上,故默认情况下它是最合理的分区策略,也是我们最常用的分区策略之一。
  • 随机策略:Randomnesscelue;随机的将消息放置到任意分区;随机策略是老版本生产者使用的分区策略,新版本已改为轮询;
List<PartitionInfo> partitions = cluster.partitionsForTopic(topic);
return ThreadLocalRandom.current().nextInt(partitions.size());
  • 按消息键保序策略:Key-ordering策略;Kafka允许为每条消息定义消息键;可以保证具有相同消息键的一组消息被分配到同一个分区,由于每个分区下的消息处理都是有序的,故称之为按消息键保序策略;
List<PartitionInfo> partitions = cluster.partitionsForTopic(topic);
return Math.abs(key.hashCode()) % partitions.size();

Kafka 默认分区策略实际上同时实现了两种策略:如果指定了 Key,那么默认实现按消息键保序策略;如果没有指定 Key,则使用轮询策略。

  • 其他分区策略:地理位置分区策略(根据Broker所在IP地址分区)

生产者压缩算法

压缩(compression):秉承用时间换空间的经典trade-off思想,具体来说就是用 CPU 时间去换磁盘空间或网络 I/O 传输量,希望以较小的 CPU 开销带来更少的磁盘占用或更少的网络 I/O 传输。

Kafka压缩方式-消息格式

Kafka消息格式有两大类:社区分别称之为 V1 版本和 V2 版本。V2 版本是 Kafka 0.11.0.0 中正式引入的。V2 版本主要是针对 V1 版本的一些弊端做了修正,和消息压缩相关的修改就有
把消息的公共部分抽取出来放到外层消息集合里面,这样就不用每条消息都保存这些信息了。
V2 版本还有一个和压缩息息相关的改进,就是保存压缩消息的方法发生了变化。之前 V1 版本中保存压缩消息的方法是把多条消息进行压缩然后保存到外层消息的消息体字段中;而 V2 版本的做法是对整个消息集合进行压缩。显然后者应该比前者有更好的压缩效果。

不论是哪个版本,Kafka 的消息层次都分为两层:消息集合(message set)以及消息(message)。
一个消息集合中包含若干条日志项(record item),而日志项才是真正封装消息的地方。
Kafka 底层的消息日志由一系列消息集合日志项组成。
Kafka 通常不会直接操作具体的一条条消息,它总是在消息集合这个层面上进行写入操作。

压缩时机

Kafka中,压缩可能发生在两个地方: 生产者端 和 Broker端。
compression.type: 生产者程序配置参数;表示启用指定类型的压缩算法。

 Properties props = new Properties();
 props.put("bootstrap.servers", "localhost:9092");
 props.put("acks", "all");
 props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
 props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
 // 开启 GZIP 压缩
 props.put("compression.type", "gzip");
 Producer<String, String> producer = new KafkaProducer<>(props);

Broker端可能压缩的场景:

  • Broker 端指定了和 Producer 端不同的压缩算法。
    在Broker端也有参数 compression.type,当Broker端和 Producer端配置不一致时就会出现预料之外的压缩/解压缩操作,通常表现为Broker端CPU使用率飙升;
    Broker端参数 compression.type 默认值为 producer,表示以Producer端压缩算法为准;
  • Broker 端发生了消息格式转换。
    在一个生产环境中,Kafka 集群中同时保存多种版本的消息格式非常常见。为了兼容老版本的格式,Broker 端会对新版本消息执行向老版本格式的转换。这个过程中会涉及消息的解压缩和重新压缩。一般情况下这种消息格式转换对性能是有很大影响的,除了这里的压缩之外,它还让 Kafka 丧失了引以为豪的 Zero Copy 特性。

解压时机

通常解压缩发生在消费者程序中;Kafka 会将启用了哪种压缩算法封装进消息集合中,这样当 Consumer 读取到消息集合时,它自然就知道了这些消息使用的是哪种压缩算法。
Producer 端压缩、Broker 端保持、Consumer 端解压缩。

在Broker端还会有另一种解压缩: 每个压缩过的消息集合在 Broker 端写入时都要发生解压缩操作,目的就是为了对消息执行各种验证。我们必须承认这种解压缩对 Broker 端性能是有一定影响的,特别是对 CPU 的使用率而言。

压缩算法

kafka 2.1.0之前
	1. GZIP
	2. Snappy
	3. LZ4
kafka 2.1.0
	4. Zstandard(zstd):facebook开源压缩算法,提供超高压缩比;

压缩算法优劣指标:压缩比 和 压缩/解压缩吞吐量
下面是下面这张表是 Facebook Zstandard 官网提供的一份压缩算法 benchmark 比较结果:
在这里插入图片描述
在实际使用中,GZIP、Snappy、LZ4 甚至是 zstd 的表现各有千秋。但对于 Kafka 而言,它们的性能测试结果却出奇得一致,即:
在吞吐量方面:LZ4 > Snappy > zstd 和 GZIP;
在压缩比方面:zstd > LZ4 > GZIP > Snappy。

具体到物理资源,使用 Snappy 算法占用的网络带宽最多,zstd 最少,这是合理的,毕竟 zstd 就是要提供超高的压缩比;在 CPU 使用率方面,各个算法表现得差不多,只是在压缩时 Snappy 算法使用的 CPU 较多一些,而在解压缩时 GZIP 算法则可能使用更多的 CPU。
建议:对于CPU资源充足 或者 宽带资源有限的环境,建议启用压缩。

TCP连接管理

Apache Kafka的所有通信都是基于TCP的,而不是基于HTTP或者其他协议。
开发客户端时,用到了TCP的高级功能:多路复用请求,同时轮询多个连接;

  • 多路复用请求:multiplexing request,指两个或多个数据流合并到底层单一物理连接中的过程。TCP的多路复用请求会在一条物理连接上创建若干个虚拟连接,每个虚拟连接负责流转各自对应的数据流;TCP严格意义上并不是多路复用,只是提供可靠的消息交付语义保证;eg: 自动重传丢失的报文。

TCP连接建立

在创建 KafkaProducer 实例时,生产者应用会在后台创建并启动一个名为 Sender 的线程, 该 Sender 线程开始运行时首先会创建与 Broker 的连接;
注:Producer 并不知道和哪个 Broker 建立连接, 而是会和 bootstrap.servers 参数指定的所有Broker建立连接。
建议:不建议将集群中所有的Broker 信息都配置到 bootstrap.servers中,通常配置3~4台即可。因为Producer一旦连接到集群中任意一台Broker(通过向一台Broker发送METADATA请求,及尝试获取集群的元数据信息),就能获取到整个集群的Broker信息。

KafkaProducer实例

KafkaProducer类是线程安全的;KafkaProducer实例创建的线程和Sender线程共享的可变数据结构只有 RecordAccumulator 类,故维护 RecordAccumulator类线程安全,即可实现KafkaProducer类的线程安全。
RecordAccumulator类数据结构:ConcurrentMap<TopicPartition, Deque>,TopicPartion 是Kafka用来表示主题分区的java对象,本身是不可变对象。而Deque使用处都有锁保护,所以基本可以认定RecordAccumulator类是线程安全的。

  • 纵然KafkaProducer 是线程安全的,也不赞同创建 KafkaProducer 实例时启动 Sender 线程的做法。写了《Java 并发编程实践》的那位布赖恩·格茨(Brian Goetz)大神,明确指出了这样做的风险:在对象构造器中启动线程会造成 this 指针的逃逸。理论上,Sender 线程完全能够观测到一个尚未构造完成的 KafkaProducer 实例。当然,在构造对象时创建线程没有任何问题,但最好是不要同时启动它。

TCP连接建立的时机

  • TCP连接在KafkaProducer实例创建的时候建立;
  • 更新元数据后,会建立TCP连接;当Producer更新集群的元数据信息之后,如果发现与某些Broker当前没有连接,那么他会创建一个TCP连接;
  • 消息发送时,会建立TCP连接;当要发送消息时,Producer发现尚不存在与目标Broker之间的连接会创建;
    更新集群元数据信息的场景:
  1. 场景一:当 Producer 尝试给一个不存在的主题发送消息时,Broker 会告诉 Producer 说这个主题不存在。此时 Producer 会发送 METADATA 请求给 Kafka 集群,去尝试获取最新的元数据信息。
  2. 场景二:Producer 通过 metadata.max.age.ms 参数定期地去更新元数据信息。该参数的默认值是 300000,即 5 分钟,也就是说不管集群那边是否有变化,Producer 每 5 分钟都会强制刷新一次元数据以保证它是最及时的数据。

TCP连接关闭的时机

  1. 用户主动关闭:主动关闭实际上是广义的主动关闭,甚至包括用户调用 kill -9 主动“杀掉”Producer 应用。当然最推荐的方式还是调用 producer.close() 方法来关闭。
  2. Kafka自动关闭
    这与 Producer 端参数 connections.max.idle.ms 的值有关。默认情况下该参数值是 9 分钟,即如果在 9 分钟内没有任何请求“流过”某个 TCP 连接,那么 Kafka 会主动帮你把该 TCP 连接关闭。用户可以在 Producer 端设置 connections.max.idle.ms=-1 禁掉这种机制。一旦被设置成 -1,TCP 连接将成为永久长连接。当然这只是软件层面的“长连接”机制,由于 Kafka 创建的这些 Socket 连接都开启了 keepalive,因此 keepalive 探活机制还是会遵守的。
    TCP 连接是在 Broker 端被关闭的,但其实这个 TCP 连接的发起方是客户端,因此在 TCP 看来,这属于被动关闭的场景,即 passive close。被动关闭的后果就是会产生大量的 CLOSE_WAIT 连接,因此 Producer 端或 Client 端没有机会显式地观测到此连接已被中断。—僵尸连接
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章