【Kafka】生产者发送消息(一)

437 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第8天,点击查看活动详情

partition写入与消费.gif

Producer 生产者发送消息流程如下:

  1. 首先指定消息发送到哪个 Topic

  2. 选择一个 Topic 的分区 partitiion,默认是轮询来负载均衡

    也可以指定一个分区 key,根据 keyhash 值来分发到指定的分区。

    也可以自定义 partition 来实现分区策略。

  3. 找到这个分区的 leader partition

  4. 与所在机器的 Brokersocket 建立通信

  5. 发送 Kafka 自定义协议格式的请求(包含携带的消息)

发送 API 如下:

// 方式一:默认分区策略
producer.send(msg);
​
// 方式二:指定 key,根据 key 的 hash值去分发到某个分区上
producer.send(key, msg);

Producer 内部实现原理,如图:

producer发送.png

(1)分区策略

Kafka 的消息组织方式实际上是三级结构:主题 - 分区 - 消息。

所谓分区策略是决定生产者将消息发送到哪个分区的算法。

  • 轮询策略
  • 随机策略
  • 按消息键保序策略
  • 其他分区策略:自定义

发送 API 如下:

// 方式一:默认分区策略
producer.send(msg);
​
// 方式二:指定 key,根据 key 的 hash值去分发到某个分区上
producer.send(key, msg);

1. 轮询策略

也称 Round-robin 策略,即顺序分配。 Kafka Java 生产者 API 默认提供的分区策略。

举个栗子:一个主题 Topic 下有 3 个分区:

  • 第一条被发送到分区 0
  • 第二条被发送到分区 1
  • 第三条被发送到分区 2,以此类推。
  • 当生产第 4 条消息时又会重新开始,即将其分配到分区 0

2022-06-0114-35-44.png

2. 随机策略

所谓随机就是我们随意地将消息放置到任意一个分区上:

2022-06-0114-39-20.png

List partitions = cluster.partitionsForTopic(topic);
return ThreadLocalRandom.current().nextInt(partitions.size());

3. 按消息键保序策略

一旦消息被定义了 Key,那么就可以保证同一个 Key 的所有消息都进入到相同的分区里面,由于每个分区下的消息处理都是有顺序的,

2022-06-0114-57-31.png

List partitions = cluster.partitionsForTopic(topic);
​
return Math.abs(key.hashCode()) % partitions.size();

问:如何保证消息全局顺序一致?

一个 Topic 只有一个分区。

问:如何保证相关业务顺序处理?

  1. 发送方保证业务顺序性。
  2. 发送的消息根据 key,发送到同一个分区下。

4. 其他分区策略:自定义

根据 Broker 所在的 IP 地址实现定制化的分区策略:

List<PartitionInfo> partitions = cluster.partitionsForTopic(topic);
​
return partitions.stream().filter(p -> isSouth(p.leader().host())).map(PartitionInfo::partition).findAny().get();

(2)压缩

为了减少网络开销以及磁盘存储,一般都会使用压缩。

压缩解压缩流程:Producer 端压缩 -> Broker 端保持 -> Consumer 端解压缩。

何时压缩?Producer 端

 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);

何时解压缩?Consumer 端

各种压缩算法对比:

  • 吞吐量方面:LZ4 > Snappy > zstdGZIP
  • 压缩比方面:zstd > LZ4 > GZIP > Snappy

2022-06-0115-10-10.png

最佳实践:

  • 建议开启 zstd 压缩。

    如果客户端机器 CPU 资源有很多富余,强烈建议开启 zstd 压缩,这样能极大地节省网络资源消耗。

(3)TCP 建立连接

采用 TCP 而不是 HTTP 作为所有请求通信的底层协议的原因:

  • 可以利用 TCP 本身提供的一些高级功能,比如多路复用请求、同时轮询多个连接。
  • HTTP 库在很多编程语言中都略显简陋。

Kafka 生产者使用流程:

  1. 构造生产者对象所需的参数对象
  2. 利用第 1 步的参数对象,创建 KafkaProducer 对象实例
  3. 使用 KafkaProducersend 方法发送消息
  4. 调用 KafkaProducerclose 方法关闭生产者并释放各种系统资源
Properties props = new Properties();
props.put("bootstrap.servers", "192.168.226.150:9092");
props.put("acks", "all");
props.put("retries", 1);
props.put("batch.size", 16384);
props.put("key.serializer", StringSerializer.class.getName());
props.put("value.serializer", StringSerializer.class.getName());
KafkaProducer<String, String> producer producer 
    = new KafkaProducer<String, String>(props);
producer.send(new ProducerRecord<String, String>(record.topic(),
                                                     record.value().toString()));
producer.close();

1)何时创建 TCP 连接?

  1. 在创建 KafkaProducer 实例时:

    • 生产者应用会在后台创建并启动一个名为 Sender 的线程,该 Sender 线程开始运行时,首先会创建与 Broker 的连接。

    • 此时不知道要连接哪个 Brokerkafka 会通过 METADATA 请求获取集群的元数据,连接所有的 Broker

    不建议把集群中所有的 Broker 信息都配置到 bootstrap.servers 中,通常指定 3~4 台就足以了。

因为 Producer 一旦连接到集群中的任一台 Broker,就能拿到整个集群的 Broker 信息,故没必要为 bootstrap.servers 指定所有的 Broker

  1. 还可能在更新元数据后,或在消息发送时:

因为这两个地方并非总是创建 TCP 连接。 当 Producer 更新了集群的元数据信息之后,如果发现与某些 Broker 当前没有连接,那么它就会创建一个 TCP 连接。 同样地,当要发送消息时,Producer 发现尚不存在与目标 Broker 的连接,也会创建一个。

2)何时关闭 TCP 连接?

Producer 端关闭 TCP 连接的方式有两种:

  1. 用户主动关闭
// 1. 主动断开
producer.close();
​
// 2. 暴力删除
kill -9
  1. kafka 自动关闭
// 自动关闭跟 producer 端参数有关, 默认 9 分钟内没有任何请求通过,就会关闭
connection.max.idles.ms
​
// TCP 连接是在 Broker 端被关闭的,但这个关闭连接请求是客户端发起的,对 TCP 而言这是被动的关闭,被动关闭会产生大量的CLOSE_WAIT连接。

(4)发送给 Broker 遇到异常,则重试

发送不管是异步还是同步,都可能让你处理异常,常见异常如下:

  1. LeaderNotAvailableException 异常:leader分区不可用了

    • 产生原因:某个机器挂了,要等待选出新 leader分区,才能继续写入。

      平时重启 Broker 进程,肯定会导致 leader 切换,会导致写入报错。

    • 解决方案:重试发送即可。

  2. NotControllerException 异常:Controller 所在 Broker 挂了

    • 产生原因:某个机器挂了,等待 Controller 重新选举。
    • 解决方案:重试发送即可。
  3. NetworkException异常:网络异常

    • 产生原因:断网、网络分区、丢包等等。
    • 解决方案:重试发送即可。

综上,如果重试几次之后还不行,那么就要交给人工处理。