Kafka Producer 是 Kafka 客户端中的一种,用于投递消息到 kafka Topic 中
消息投递时的负载均衡
我们知道,一个 Topic 被 Kafka 拆分成多个 Partition 存储在不同的 broker ,为了更好的利用 Partition 特性来提高 Kafka 性能,很容易想到,消息投递时,最好把消息均匀的投递到各个 Partition 上。
Producer 消息投递策略
Producer 会根据消息投递是的情况,选择不同的投递策略
-
消息指定了 Partition :这种情况下,直接将消息投递到指定的 Partition 中
-
消息没有指定 Partition 但设置了 key :这种情况下,将 key 的 hash 值与 topic 的 partition 数量进行取余,得到 partition 的值
-
消息既没有指定 partition 又没有设置 key 时 :第一次调用时随机生成一个整数,后面每次调用在这个整数上自增,将这个值与 topic 的 partition 数取余得到 partition 值,也就是常说的 round-robin (轮询) 算法
可以看出,在不手动指定 Partition 的情况下,Producer 的消息投递都是负载均衡的
异步(批量)发送消息
Producer 不仅可以做到消息投递时的负载均衡,还通过批量发送实现更高的吞吐量
kafka 0.11.x 之前 Producer 都是同步发送消息的,即生产了一条,投递一条。这种方式虽然及时性最高,但也存在一个问题,一条消息就发送一次,网络开销太大了。
kafka 0.11.x 开始 Producer 提供了异步发送消息的功能,即,将生产的消息先缓存起来,等满足一定的条件后,再批量发送
批格式的消息
Producer 会将这一批数据合并成一条"大消息",发送出去,这条"大消息"其实就是 kafka 0.11.x 新增加的批格式的消息
异步(批量)发送消息相关的配置
上面说到异步(批量)发送消息需要满足一定的条件,具体有哪些条件呢? Producer 提供了一些配置可供选择
批大小
一批消息的大小,单位字节,默认值16384,也就是 16K
batch.size=16384
复制代码
Producer 会为每一个 Partition 在内存中一个指定批大小的缓冲区,只有发往某一 Partition 的消息累计到了指定大小,才会开始投递。
该值不宜过大或过小。过小会降低吞吐量,为零将完全禁用批处理,过大可能会浪费内存
批暂存时长
设置批大小后,如果某一 Partition 缓冲区中的消息迟迟没有达到指定大小,消息会留在 Producer 端,造成消息延迟过大,为了解决这一问题,需要为设置缓冲区设置一个暂存时间,单位毫秒,默认值为0
linger.ms=0
复制代码
当到达这个时间后,不管消息大小有没有达到指定大小,都发送,防止延迟过大。
acks
为了确保消息到达了服务端, Producer 发送消息后,需要等待 broker 的 ack 响应, Producer 为消息提供了一个 acks 的属性来控制这一行为,它有三个值
-
acks=0
: 默认发送成功,Producer 不会等待服务器的任何确认,每个记录返回的偏移量将始终设置为-1。该配置相当于最多一次语义 -
acks=1
: 默认值, Leader replication 将记录写入其本地日志后响应 ack,无需等待 ISR 中的其它副本确认。如果此时 Leader replication 宕机了,那么消息还是会丢失
acks=-1
OR acks=all
: Leader replication 等待 ISR 中的所有副本回复确认信息后响应 ack。这种情况下,即使 Leader replication 宕机消息也不会丢失,但代价就是延迟最高,效率最低
注意,
acks=-1
ORacks=all
是 Leader replication 等待 ISR 中的所有副本而不是半数以上副本,回复确认信息后响应 ack。 有官方文档为证。
(面试时要敢于指出面试官的错误)
ack 超时与消息重投
不管是将 acks
设置成 1 还是 -1 都面临着响应超时的问题,当消息发送之后,久久没有收到 ack 回复,就需要考虑消息可能丢失了,需要重新投递
Producer 通过 request.timeout.ms
来控制请求超时时间,单位为毫秒,默认值为 30000 也就是 30 秒
request.timeout.ms=30000
复制代码
超过该时间还没有收到响应,则会重新发送
Producer 幂等性
消息重投之后必然会带来消息重复的问题,好在从 Kafka 0.11.0 开始,提供了幂等处理来进行消息去重。 要启用幂等性,需要将 Producer 的 enable.idempotence
参数设置为 true
。
开启幂等性的 Producer 在初始化的时候会被分配一个 PID,对于每个 PID,消息发送到的每一个分区都有对应的序列号,这些序列号从 0 开始单调递增。生产者每发送一条消息就会将 <PID, 分区> 对应的序列号值加 1。
Broker 端在内存中为每一对 <PID, 分区> 维护一个序列号 SN_old,针对生产者发送来的每一条消息,对其序列号 SN_new 进行判断,并作相应处理。
- 当 SN_new < SN_old + 1时,说明此消息已被写入,broker 直接丢弃该条消息
- 当 SN_new = SN_old + 1 时, broker 才会接受这条消息
- 当 SN_new > SN_old + 1时,说明中间有数据尚未写入,出现了消息乱序,可能存在消息丢失的现象,对应的 Producer 会抛出
OutOfOrderSequenceException
异常
Broker 通过消息的序列号和 <PID, 分区> ,保证了消息的顺序写入和去重
注意:序列号只针对 <PID, 分区> ,这意味着幂等性只能保证单个主题的单一分区内消息不重复。Producer 重启 PID 就会变化,如果将一次 Producer 的生命周期看作一次会话,那么它只能实现单会话上的幂等性。
Producer 事务
为了实现跨分区跨会话的幂等以及防止 Producer 重启 造成的 PID 变化,需要为 Topic 引入一个全局唯一的 Transaction ID,并将 Producer 获得的 PID 和 Transaction ID 绑定。这样当 Producer 重启后就可以通过正在进行的 TransactionID 获得原来的 PID。
为了管理 Transaction,Kafka 引入了一个新的组件 Transaction Coordinator。Producer 就是通过和 Transaction Coordinator 交互获得 Transaction ID 对应的 PID 的 。TransactionCoordinator
还负责将所有事务写入 Kafka 的一个内部 Topic,这样即使整个服务重启,由于事务状态得到保存,进行中的事务状态可以得到恢复,从而继续进行。
Producer Exactly-Once 语义
Producer 如要实现不重不漏,精确一次的 Exactly-Once 语义,需要启用幂等 enable.idempotence=true
(默认为false )。如果禁用,Producer 将不会在请求中设置 PID 字段。
当启用幂等性时,Producer 强制 acks=all
、retries > 1
、max.inflight.requests.per.connection=1
,如果没有配置就不能保证幂等性。如果应用程序未明确覆盖这些设置,则当启用幂等性时,Producer 将自动设置 。
但要实现跨分区跨会话的幂等,还需要开启 Producer 事务。
注意,必须启用幂等性才能使用事务。