基于kafka 2.11版本
kafka-clients 2.2.2
由此可以看到kafka体系架构的组成有如下几部分:
1.producer生产者,发送消息到kafka cluster
2.kafka cluster是由broker组成的集群
3.consumer消费者,从kafka cluster中pull拉取消息进行消费
4.zookeeper cluster,用于保存kafka集群的元数据
由图可见:
整个生产者客户端由两个线程协调运行,这两个线程分别为主线程和 Sender 线程(发送线程)。
作用:
生产者拦截器既可以用来在消息发送前做一些准备工作,比如按照某个规则过滤不符合要求的消息、修改消息的内容等,也可以用来在发送回调逻辑前做一些定制化的需求,比如统计类工作。
使用方式:
主要是自定义实现 org.apache.kafka.clients.producer. ProducerInterceptor 接口。
ProducerInterceptor 接口中包含3个方法:
public ProducerRecord onSend(ProducerRecord record);
public void onAcknowledgement(RecordMetadata metadata, Exception exception);
public void close();
KafkaProducer 在将消息序列化和计算分区之前会调用生产者拦截器的 onSend() 方法来对消息进行相应的定制化操作。一般来说最好不要修改消息 ProducerRecord 的 topic、key 和 partition 等信息。
KafkaProducer 会在消息被应答(Acknowledgement)之前或消息发送失败时调用生产者拦截器的 onAcknowledgement() 方法,优先于用户设定的 Callback 之前执行。这个方法运行在 Producer 的I/O线程中,所以这个方法中实现的代码逻辑越简单越好,否则会影响消息的发送速度。
close() 方法主要用于在关闭拦截器时执行一些资源的清理工作。
作用:
把java对象转成字节数组用于在网络中传输
使用方式:
自定义实现org.apache.kafka.common.serialization.Serializer 接口,此接口有3个方法:
public void configure(Map configs, boolean isKey)
public byte[] serialize(String topic, T data)
public void close()
configure() 方法用来配置当前类
serialize() 方法用来执行序列化操作
close() 方法用来关闭当前的序列化器
作用:
确定该消息发送到broker上的哪个分区。(分区的概念将会在讲broker的文章中专门讲到)
使用方式:
如果消息 ProducerRecord 中指定了 partition 字段,那么就不需要分区器的作用,因为 partition 字段代表的就是所要发往的分区号。
如果消息 ProducerRecord 中没有指定 partition 字段,那么就需要用分区器,根据 key 这个字段来计算 partition 的值。
Kafka 中提供的默认分区器是 org.apache.kafka.clients.producer.internals.DefaultPartitioner,它实现了 org.apache.kafka.clients.producer.Partitioner 接口,这个接口中定义了2个方法,具体如下所示。
public int partition(String topic, Object key, byte[] keyBytes,
Object value, byte[] valueBytes, Cluster cluster);
public void close();
partition() 方法用来计算分区号,返回值为 int 类型。partition() 方法中的参数分别表示主题、键、序列化后的键、值、序列化后的值,以及集群的元数据信息,通过这些信息可以实现功能丰富的分区器。
close() 方法在关闭分区器的时候用来回收一些资源。
默认分区器 DefaultPartitioner 的实现:
close() 是空方法
partition() 方法中定义了主要的分区分配逻辑:
如果 key 不为 null,那么默认的分区器会对 key 进行哈希,最终根据得到的哈希值来计算分区号,拥有相同 key 的消息会被写入同一个分区。(这里其实就是Key-ordering 按消息键顺序策略)
如果 key 为 null,会调用nextValue(topic)方法获取一个自增的值,如果topic存在可用分区那么将nextValue转成正数之后对可用分区数进行取余,如果topic不存在可用分区那么就从所有不可用的分区中通过取余的方式返回一个不可用的分区。(这里其实就是Round-robin轮询策略)
自定义的分区器,只需同 DefaultPartitioner 一样实现 Partitioner 接口即可
作用:
主要用来缓存消息以便 Sender 线程可以批量发送,进而减少网络传输的资源消耗以提升性能。
在 RecordAccumulator 的内部为每个分区都维护了一个双端队列。
Sender 从 RecordAccumulator 中获取缓存的消息之后,会进一步将原本<分区, Deque< ProducerBatch>> 的保存形式转变成
KafkaProducer 要将此消息追加到指定主题的某个分区所对应的 leader 副本之前,首先需要知道主题的分区数量,然后经过计算得出(或者直接指定)目标分区,之后 KafkaProducer 需要知道目标分区的 leader 副本所在的 broker 节点的地址、端口等信息才能建立连接,最终才能将消息发送到 Kafka。
所以这里需要一个转换,对于网络连接来说,生产者客户端是与具体的 broker 节点建立的连接,也就是向具体的 broker 节点发送消息,而并不关心消息属于哪一个分区。
换句话说生产者也有一份broker集群的元素据,才知道某个分区的leader副本所在broker节点的地址、端口等信息,通过zk的watch机制实现broker元素据变更时通知生产者
请求在从 Sender 线程发往 Kafka 之前还会保存到 InFlightRequests 中,InFlightRequests 保存对象的具体形式为 Map
生产者的ack机制说的是怎么才认为发送成功
在生产者的配置中配置ACK对应的值,不同的值是不同的策略
只管发送,不论有不有分区收到了,不论是否已经保存到了副本中,发了就认为发送成功
broker中对应的分区收到了,至少该分区有一个副本(leader副本)已收到落盘了,不过其他follower是否同步且落盘了这条消息,这样就认为发送成功。
该配置下可能会丢数据的场景:
生产者发送了,leader也落盘了,就认为发送成功了,但是follower还没来得及同步,leader就挂了,此时从follower选一个当leader,这个新的leader是没有这条数据的
broker中对应的分区收到了,该分区所有副本(leader 和 follower)都收到落盘了,这样才认为发送成功
由此可见ack机制会直接影响到Kafka集群的吞吐量和消息可靠性。而吞吐量和可靠性就像硬币的两面,两者不可兼得,只能平衡。
那么只要把ack位置为all就一定不会丢数据吗?
不是!如果一个分区只有一个副本,那ack设置为all也会丢数据,所以建议一个分区至少有3个副本(1个leader,2个follower)
消息重复:
生产者发送数据,leader收到落盘,leader挂了,follower还没来得及同步,follower变成新leader,旧leader重启变成新follower(已有该条数据),生产者认为发送失败,生产者重发成功,新follower保存了两条该数据