使用方式

KafkaProducer 发送消息主要有以下 3 种方式:

    Properties properties = new Properties();
properties.setProperty("bootstrap.servers", "localhost:9092");
properties.setProperty("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
properties.setProperty("value.serializer", "org.apache.kafka.common.serialization.StringSerializer"); KafkaProducer<String, String> producer = new KafkaProducer<>(properties);
ProducerRecord<String, String> record = new ProducerRecord<>("topic", "key", "value"); // 发送并忘记(fire-and-forget)
producer.send(record); // 同步发送
Future<RecordMetadata> future = producer.send(record);
RecordMetadata metadata = future.get(); // 异步发送
producer.send(record, new Callback() {
@Override
public void onCompletion(RecordMetadata metadata, Exception exception) { }
}); producer.close();

具体的发送流程可以参考 KafkaProducer发送流程简析

KafkaProducer 是线程安全的,多个线程可以共享同一个 KafkaProducer 对象。

配置解析

client.id

 该参数可以是任意的字符串,broker 会用它来识别消息的来源,会在日志和监控指标里展示。

bootstrap.servers

 该属性指定 broker 的地址列表。

 清单里不需要包含所有的 broker 地址,生产者会从给定的 broker 里查找到其他 broker 的信息。

 不过建议至少要提供两个 broker 的信息,一旦其中一个宕机,生产者仍然能够连接到集群上。

key.serializer & value.serializer

 这两个属性必须被设置为一个实现了org.apache.kafka.common.serialization.Serializer接口的类。

 生产者会使用这个类把键值对象序列化成字节数组。

receive.buffer.bytes & send.buffer.bytes

 设置 socket 读写数据时用到的 TCP 缓冲区大小。如果它们被设为 -1,就使用操作系统的默认值。

 当生产者或消费者与 broker 处于不同的机房时,可以适当增大这些值

buffer.memory

 设置生产者内存缓冲区的大小,生产者用它缓冲要发送到服务器的消息。

 如果应用程序发送消息的速度超过发送到服务器的速度,会导致生产者空间不足。

 此时KafkaProducer.send()会阻塞等待内存释放,等待时间超过 max.block.ms 后会抛出超时异常。

compression.type

 该参数指定了消息被发送给 broker 之前,使用哪一种压缩算法(snappygziplz4)进行压缩。

 使用压缩可以降低网络传输开销和存储开销,而这往往是向 Kafka 发送消息的瓶颈所在。

batch.size

 该参数指定了一个批次可以使用的内存字节数(而不是消息个数)。

 消息批次ProducerBatch包含了一组将要发送至同个分区的消息,当批次被填满,批次里的所有消息会被立即发送出去。

 不过生产者并不一定都会等到批次被填满才发送,半满甚至只包含一个消息的批次也可能被发送。

 所以就算把批次大小设置得很大,也不会造成延迟,只是会占用更多的内存而已。

 但如果设置得太小,生产者会频繁地发送消息,会增加一些额外的网络开销。

linger.ms

 该参数指定了生产者在发送批次之前等待的时间。

 生产者会在批次填满或等待时间达到 linger.ms 时把批次发送出去。

 设置linger.ms>0会增加延迟,但也会提升吞吐量(一次性发送更多的消息,每个消息的开销就变小了)。

acks

 参数指定了必须要有多少个分区副本收到消息,生产者才会认为消息写入是成功的。

 这个参数决定令消息丢失的可能性:

  • acks=0 生产者发出消息后不等待来自服务器的响应
    如果当中出现了问题,导致服务器没有收到消息,那么生产者就无从得知,消息也就丢失了。
    不过,因为生产者不需要等待服务器的响应,所以它可以以网络能够支持的最大速度发送消息,从而达到很高的吞吐量。

  • acks=1 只要集群的 leader 节点收到消息,生产者就会收到一个来自服务器的成功响应
    如果消息无法到达 leader 节点(比如:leader节点崩溃,新的 leader 还没有被选举出来),生产者会收到一个错误响应。
    为了避免数据丢失,生产者会重发消息。不过,如果一个没有收到消息的节点成为新 leader,消息还是会丢失。

    这个时候的吞吐量取决于使用的是同步发送还是异步发送:

    • 发送端阻塞等待服务器的响应(通过调用 Future.get() 方法),显然会增加延迟(在网络上传输一个来回的延迟)
    • 发送端使用回调可以缓解延迟问题,不过吞吐量仍受在途消息数量的限制(比如:生产者在收到服务器响应之前可以发送多少个消息)
  • acks=all 只有当所有参与复制的节点全部收到消息时,生产者才会收到一个来自服务器的成功响应
    这种模式是最安全的,就算有服务器发生崩溃,数据也不会丢失。
    不过,它的延迟比 acks=1 时更高,因为我们要等待不只一个服务器节点接收消息。

retries

 该参数决定了生产者可以重发消息的次数(每次重试之间等待 retry.backoff.ms)。

 服务器返回临时性的错误(比如:分区找不到 leader)时,生产者会自动重试,没必要在代码逻辑里处理可重试的错误。

 作为开发者,只需要处理那些不可重试的错误(比如:消息字节数超过单个发送批次上限)或重试次数超出上限的情况即可。

max.in.flight.requests.per.connection

 该参数指定生产者,最多可以发送未响应在途消息批次数量。

 在途消息批次越多,会占用更多的内存,不过也会提升吞吐量。

 当retries > 0max.in.flight.requests.per.connection > 1时,可能出现消息乱序。

 如果第一个批次消息写入失败,而第二个批次写入成功,broker 会重试写入第一个批次。

 如果此时第一个批次也写入成功,那么两个批次的顺序就反过来了。

 一般不建议设置retries=0,而是令max.in.flight.requests.per.connection = 1来保证消息顺序。

 在生产者尝试发送第一批消息时,就不会有其他的消息发送给 broker,即使发生重试消息也不会乱序。

 不过这样会严重影响生产者的吞吐量,所以只有在对消息的顺序有严格要求的情况下才能这么做。

高级特性

幂等

当 broker 失效时生产者可能会自动重试,导致一条消息被重复写入多次。

为了避免这种情况,Kafka 在生产者端提供来幂等保证:同一条消息被生产者发送多次,但在 broker端这条消息只会被写入日志一次

在发送端设置 enable.idempotence = true 可以开启幂等性,此时配置同时满足以下条件:

  • max.in.flight.requests.per.connection ≤ 5
  • retries > 0
  • acks = all

其工作机制如下:

  • producer 在初始化时必须分配一个 PIDproducer id该过程对用户来说是完全透明的)
  • 发送到 broker 端的每批消息都会被赋予一个单调递增的 SNsequence number用于消息去重(每个分区都有独立的序列号)
  • 接收到消息的 broker 会将批次的(PID, SN)信息一同持久化到对应的分区日志中(保证 leader 切换后去重仍然生效)

若重试导致 broker 接收到小于或等于已知最大序列号的消息,broker 会拒绝写入这些消息,从而保证每条消息也只会被保存在日志中一次。

由于每个 producer 实例都会被分配不同的 PID,该机制只能保证单个 producer 实例的幂等性,无法实现协同多个 producer 实现幂等。

事务

Kafka 事务可以实现 producer 对多个主题和分区的原子写入,并且保证 consumer 不会读取到未提交的数据。

Kafka 要求应用程序必须提供一个全局唯一的 TIDtransactional id

初始化时,producer 首先要向 broker 集群注册其 TID,broker 会根据给定的 TID 检查是否存在未完成的事务。

如果某个 producer 实例失效,该机制能够保证下一个拥有相同 TID 的实例首先完成之前未完成的事务。

此外,broker 还会为自动每个 producer 分配一个epoch用于隔离fencing out失效但仍存活的 producer:

当 producer 参与事务时,broker 会检查是否存在相同的 TID 且 epoch 更大的活跃 producer。

如果存在,则认为当前 producer 是一个僵尸实例zombie instance并拒绝为其提供服务,防止其破坏事务的完整性。

下面是两个常见的应用场景:

实现跨主题原子写入

    Properties properties = new Properties();
properties.setProperty("bootstrap.servers", "localhost:9092");
properties.setProperty("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
properties.setProperty("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
properties.setProperty("enable.idempotence", "true"); // 开启幂等
properties.setProperty("transactional.id", "my-transaction-id"); // 设置事务ID KafkaProducer<String, String> producer = new KafkaProducer<>(properties); ProducerRecord<String, String> record1 = new ProducerRecord<>("topic1", "key1", "value1");
ProducerRecord<String, String> record2 = new ProducerRecord<>("topic2", "key2", "value2");
ProducerRecord<String, String> record3 = new ProducerRecord<>("topic3", "key3", "value3"); producer.initTransactions(); // 初始化事务(只需执行一次)
try {
producer.beginTransaction(); // 开始事务 // 向多个不同的 topic 写入消息
producer.send(record1);
producer.send(record2);
producer.send(record3); producer.commitTransaction(); // 提交事务
} catch (ProducerFencedException e) {
producer.close(); // 事务ID 已被占用
} catch (KafkaException e) {
producer.abortTransaction();
}

实现 read-process-write 模式

    final String groupID = "my-group-id";

    Properties producerProps = new Properties();
producerProps.setProperty("bootstrap.servers", "localhost:9092");
producerProps.setProperty("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
producerProps.setProperty("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
producerProps.setProperty("enable.idempotence", "true"); // 开启幂等
producerProps.setProperty("transactional.id", "my-transaction-id"); // 设置事务ID Properties consumerProps = new Properties();
consumerProps.setProperty("bootstrap.servers", "localhost:9092");
consumerProps.setProperty("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
consumerProps.setProperty("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
consumerProps.setProperty("isolation.level","read_committed"); // 设置隔离级别
consumerProps.setProperty("group.id", groupID); // 设置消费者组群ID KafkaProducer<String, String> producer = new KafkaProducer<>(producerProps);
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(consumerProps); producer.initTransactions();
consumer.subscribe(Collections.singletonList("ping")); while (true) { ConsumerRecords<String, String> records = consumer.poll(Long.MAX_VALUE); // 读取消息 try {
producer.beginTransaction(); // 开启事务 // 处理消息(可以是任意业务场景)
Map<TopicPartition, OffsetAndMetadata> offsets = new HashMap<>();
for(ConsumerRecord<String, String> record : records){
offsets.put(new TopicPartition(record.topic(), record.partition()), new OffsetAndMetadata(record.offset())); // 记录消费偏移量
producer.send(new ProducerRecord<>("pong", record.value())); // 发送消息
} producer.sendOffsetsToTransaction(offsets, groupID); // 提交消费偏移量
producer.commitTransaction(); // 事务提交
} catch (ProducerFencedException e) {
producer.close(); // 事务ID 已被占用
} catch (Exception e){
producer.abortTransaction(); // 回滚事务
}
}

参考资料

KafkaProducer 简析的更多相关文章

  1. 简析.NET Core 以及与 .NET Framework的关系

    简析.NET Core 以及与 .NET Framework的关系 一 .NET 的 Framework 们 二 .NET Core的到来 1. Runtime 2. Unified BCL 3. W ...

  2. 简析 .NET Core 构成体系

    简析 .NET Core 构成体系 Roslyn 编译器 RyuJIT 编译器 CoreCLR & CoreRT CoreFX(.NET Core Libraries) .NET Core 代 ...

  3. RecycleView + CardView 控件简析

    今天使用了V7包加入的RecycleView 和 CardView,写篇简析. 先上效果图: 原理图: 这是RecycleView的工作原理: 1.LayoutManager用来处理RecycleVi ...

  4. Java Android 注解(Annotation) 及几个常用开源项目注解原理简析

    不少开源库(ButterKnife.Retrofit.ActiveAndroid等等)都用到了注解的方式来简化代码提高开发效率. 本文简单介绍下 Annotation 示例.概念及作用.分类.自定义. ...

  5. PHP的错误报错级别设置原理简析

    原理简析 摘录php.ini文件的默认配置(php5.4): ; Common Values: ; E_ALL (Show all errors, warnings and notices inclu ...

  6. Android 启动过程简析

    首先我们先来看android构架图: android系统是构建在linux系统上面的. 所以android设备启动经历3个过程. Boot Loader,Linux Kernel & Andr ...

  7. Android RecycleView + CardView 控件简析

    今天使用了V7包加入的RecycleView 和 CardView,写篇简析. 先上效果图: 原理图: 这是RecycleView的工作原理: 1.LayoutManager用来处理RecycleVi ...

  8. Java Annotation 及几个常用开源项目注解原理简析

    PDF 版: Java Annotation.pdf, PPT 版:Java Annotation.pptx, Keynote 版:Java Annotation.key 一.Annotation 示 ...

  9. 【ACM/ICPC2013】POJ基础图论题简析(一)

    前言:昨天contest4的惨败经历让我懂得要想在ACM领域拿到好成绩,必须要真正的下苦功夫,不能再浪了!暑假还有一半,还有时间!今天找了POJ的分类题库,做了简单题目类型中的图论专题,还剩下二分图和 ...

随机推荐

  1. 【Usaco 2009 Gold 】JZOJ2020年9月19日提高B组T2 电视游戏问题

    [Usaco 2009 Gold ]JZOJ2020年9月19日提高B组T2 电视游戏问题 题目 Description 农夫约翰的奶牛们游戏成瘾!本来FJ是想要按照陶叫兽的做法拿她们去电击戒瘾的,可 ...

  2. 20201101_Python的虚拟环境问题

    虚拟环境使用总结: 1. 安装创建虚拟环境要使用的工具virtualenv  pip install virtualenv -i https://pypi.douban.com/simple/ #使用 ...

  3. PyQt学习随笔:自定义信号连接时报AttributeError: 'PyQt5.QtCore.pyqtSignal' object has no attribute 'connect'

    专栏:Python基础教程目录 专栏:使用PyQt开发图形界面Python应用 专栏:PyQt入门学习 老猿Python博文目录 如果使用自定义信号,一定要记得信号是类变量,必须在类中定义,不能在实例 ...

  4. 第15.25节 PyQt(Python+Qt)入门学习:Model/View开发实战--使用QTableView展示Excel文件内容

    老猿Python博文目录 专栏:使用PyQt开发图形界面Python应用 老猿Python博客地址 一.概述 在前面的订阅专栏<第十九章.Model/View开发:QTableView的功能及属 ...

  5. Android夜神模拟器

    夜神安卓模拟器 NOX,是一个可以让手机应用程序运行在电脑上的软件,也是电脑玩手游的新一代神器, 与传统安卓模拟器相比,基于基于Android5.1.1,兼容X86/AMD,在性能.稳定性.兼容性等方 ...

  6. SASRec 实践

    SASRec--2018,ICDM,论文<Self-Attentive Sequential Recommendation> 源代码链接:https://github.com/kang20 ...

  7. PC微信[多开+免扫码+防撤回撤回提示+转存语音+自动收款+远程命令]

    PC端微信玩出了新花样,主要技术为Hook技术 有兴趣的小伙伴可以去了解下hook钩子技术 版本介绍: 增加转存语音到MP3(保存你重要的语音数据) 增加表图转存; 增加自动收转账和自动回复; 增加远 ...

  8. ASP.NET 漂亮美观的验证码

    效果预览: 1.随机码和图片流生成 public class ValidateCode { /// <summary> /// 產生圖形驗證碼. /// </summary> ...

  9. c++11-17 模板核心知识(十四)—— 解析模板之依赖型模板名称(.template/->template/::template)

    tokenization与parsing 解析模板之类型的依赖名称 Dependent Names of Templates Example One Example Two Example Three ...

  10. 链判断运算符和Null 判断运算符

    链判断运算符 如果我们要获取一个对象的深层嵌套属性,例如获取文章标题res.data.article.title,然后为了安全起见,我们肯定不能直接这样获取,万一res对象没有article属性了呢, ...