使用方式

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. Python中函数是否能使用全局变量?

    Python函数中的变量,既可以使用局部变量(本地名字空间的变量),也可以使用全局变量(全局名字空间的变量),函数在执行查找变量只读时,先在局部变量中查找,找不到再查到全局变量中查找.因此当局部变量和 ...

  2. 第14.16节 爬虫实战2:赠人玫瑰,手留余香! request+BeautifulSoup实现csdn博文自动点赞

    写在前面:本文仅供参考学习,请勿用作它途,禁止转载! 在<第14.14节 爬虫实战准备:csdn博文点赞过程http请求和响应信息分析>老猿分析了csdn博文点赞处理的http请求和响应报 ...

  3. PyQt(Python+Qt)学习随笔:Qt Designer中建立CommandLinkButton信号与Action的槽函数连接

    在Qt Designer中,通过F4进行信号和槽函数连接编辑时,接收信号的对象不能是Action对象,但在右侧的编辑界面,可以选择将一个界面对象的信号与Action对象的槽函数连接起来. 如图: 上图 ...

  4. DVWA学习笔记

    原来装的DVWA没有认认真真地做一遍,靶场环境也有点问题了,到github上面重新下载了一遍:https://github.com/ethicalhack3r/DVWA 复习常见的高危漏洞,产生,利用 ...

  5. LeetCode初级算法之数组:66 加一

    加一 题目地址:https://leetcode-cn.com/problems/plus-one/ 给定一个由整数组成的非空数组所表示的非负整数,在该数的基础上加一.最高位数字存放在数组的首位, 数 ...

  6. 团队作业三——需求改进&系统设计

    需求改进&系统设计 一. 需求&原型改进 1. 针对课堂讨论环节老师和其他组的问题及建议,对修改选题及需求进行修改 老师及其他组的同学在课堂讨论时尚未提出问题及修改意见,但是课后我们有 ...

  7. 半夜删你代码队 Day5冲刺

    一.每日站立式会议 1.站立式会议 成员 昨日完成工作 今日计划工作 遇到的困难 陈惠霖 跟进作业 完成注册界面 相关界面设计内容知识不充足 侯晓龙 开始双人合作 开始与数据库结合 无 周楚池 初步完 ...

  8. 换种思路写Mock,让单元测试更简单

    开篇引入 单元测试中的Mock方法,通常是为了绕开那些依赖外部资源或无关功能的方法调用,使得测试重点能够集中在需要验证和保障的代码逻辑上.在定义Mock方法时,开发者真正关心的只有一件事:" ...

  9. 职场中究竟什么是ownership,你是一个有ownership的人吗?

    在互联网行业,我们经常用一个标准去评价一个人,这个标准就是ownership.一个有ownership的员工往往会被认为是出色的,被委以重任,从此升职加薪.而一个被打上了没有ownership的人,往 ...

  10. 【Pyhton 】 装饰器

    # -*- coding:utf8 -*-# Author : Mr·Yang''' 装饰器,带参数装饰器,装饰器中带参数''' import time# 普通装饰器'''def time_decor ...