目录

  • 四. KafKa API
  • 4.1 producer API
  • 4.1.1 消息发送流程
  • 4.1.2 普通生产者
  • 4.1.3 API指定生产者的分区分配策略
  • 4.1.4 自定义分区器
  • 4.1.5 同步发送消息的API
  • 4.1.6 异步发送消息的API
  • 4.2 consumer API
  • 4.2.1 普通消费者
  • 4.2.2 重置offset
  • 4.2.3 自动提交offset
  • 4.2.4 手动提交offset
  • 4.2.5 自定义存储offset
  • 4.3 自定义Interceptor
  • 4.3.1 拦截器原理
  • 4.3.2 拦截器案例
  • 五. KafKa监控
  • 5.1 介绍
  • 5.2 安装
  • 5.3 使用
  • 六. Flume对接Kafka
  • 七. KafKa Streams
  • 八. KafKa面试题


本文相关代码见本人github:https://github.com/localhost6379/KafkaDemo

四. KafKa API

4.1 producer API

4.1.1 消息发送流程

  • kfk的consumer发送消息是异步发送的, 一个main线程, 调用send()方法时启动一个sender线程发送
    消息. 所以并不是发送一批消息等待ack后发送下一批消息. 有另外的线程接收ack, 如果ack没接收到
    就重发.
  • 消息发送过程中涉及到两个线程和一个线程共享变量(行数计数器/累加器), 这个共享变量中放的就
    是待发送的数据.

kafka 流程图 kafka基础教程_API

  • 注意上述顺序: send() -> 拦截器 -> 序列化器 -> 分区器. 我们将会自定义拦截器和分区器.

4.1.2 普通生产者

  • 发送数据没有回调: cn.king.kfk01.producer.AProducer
  • 发送数据有回调: cn.king.kfk01.producer.BProducer

4.1.3 API指定生产者的分区分配策略

  • 发送数据指定分区: cn.king.kfk01.producer.CProducer

4.1.4 自定义分区器

  • 自定义分区器: cn.king.kfk01.partitioner.MyPartitioner
  • 使用自定义分区器的生产者: cn.king.kfk01.producer.DProducer

4.1.5 同步发送消息的API

  • 如果在调用send()方法时将main线程阻塞, 那么这就相当于是同步发送消息了.
  • 原理是用到了Future类的get()方法.
  • 同步发送了解即可. 在此不写demo.
  • 注意
  • 如果想保证全局消息有序, 光靠发送到同一个partition(分区)是不够的. 因为如果往partition-a发送 1 2 3 消息, 由于是异步发送, 那么无论partition-a是否收到消息, 都会立即发送 4 5 6 到partition-a中, 如果partition-a没收到 1 2 3, 那么生产者将消息 1 2 3重试发送到partition-a. 此时 1 2 3 消息在 4 5 6 消息之后.
  • 所以说如果想用kfk实现消息全局有序, 必须 往同一个partition发消息 + 同步发消息.

4.1.6 异步发送消息的API

  • 除了4.1.5 中介绍的, 其余全是异步发送. kfk默认就是异步发消息.

4.2 consumer API

kfk consumer有低级api和高级api.

  • 高级api只需要引入
<!--高级api. 版本尽量和kfk版本一致. 如果只使用高级api, 只需要引入这个依赖即可-->
<dependency>
    <groupId>org.apache.kafka</groupId>
    <artifactId>kafka-clients</artifactId>
    <version>0.11.0.0</version>
</dependency>
  • 低级api需要引入
<!--版本尽量和kfk版本一致. 如果只使用高级api, 只需要引入这个依赖即可-->
<dependency>
    <groupId>org.apache.kafka</groupId>
    <artifactId>kafka-clients</artifactId>
    <version>0.11.0.0</version>
</dependency>
<!--版本尽量和kfk版本一致-->
<dependency>
    <groupId>org.apache.kafka</groupId>
    <artifactId>kafka_2.12</artifactId>
    <version>0.11.0.0</version>
</dependency>
  • 高级API
  • 优点
  • 书写简单.
  • 不需要自行管理offset, 系统通过zk自行管理.
  • 不需要管理分区, 副本等情况, 系统自动管理.
  • 消费者断线会自动根据上一次记录在zk中的offset去接着获取数据. (默认设置1分钟更新一下zookeeper中存的offset)
  • 可以使用group来区分对同一个topic 的不同程序访问分离开来. (不同的group记录不同的offset,这样不同程序读取同一个topic才不会因为offset互相影响)
  • 缺点
  • 不能自行控制offset. (对于某些特殊需求来说)
  • 不能细化控制如分区、副本、zk等.
  • 低级API
  • 优点
  • 能够让开发者自己控制offset, 想从哪里读取就从哪里读取.
  • 自行控制连接分区, 对分区自定义进行负载均衡.
  • 对zk的依赖性降低. (如: offset不一定非要靠zk存储, 自行存储offset即可, 比如存在文件或者内存中)
  • 缺点
  • 太过复杂. 需要自行控制offset, 连接哪个分区, 找到分区leader 等.
  • 本文代码全部使用高级API. 详见我的github: https://github.com/localhost6379/KafkaDemo

4.2.1 普通消费者

  • cn.king.kfk01.consumer.AConsumer

4.2.2 重置offset

即API实现控制台的 --first-beginning 的效果.

  • org.apache.kafka.clients.consumer.ConsumerConfig 类中有一个成员变量 public static final String AUTO_OFFSET_RESET_CONFIG = "auto.offset.reset"; 这个配置项并不是写上就会生效的. 必须在两个特定场景参会生效:
  • 当前消费者组第一次消费. (消费者换了个组, 即换了个组名, 这种情况下会生效)
  • 没换组名. 消费者组消费过数据, 但是消费的这个offset在集群中已经不存在了. (最常见的情况就是默认过了7天后数据已经被删除了)
  • 这个属性有两个值可以选择: earliest(最早的)latest(最新的. 默认). 即从头开始消费还是从最大的开始消费.
properties.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
  • 最早的偏移量不一定是0, 因为如果偏移量是0-1000并且7天之后数据删掉了, 那么最早的偏移量将会是1001.
  • 面试题: 如何重新消费某一个主题的数据? 答案: 换一个组, 同时将 auto.offset.reset 参数值设置为earliest.

4.2.3 自动提交offset

// 开启自动提交offset
properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, true);
enable.auto.commit: 是否开启自动提交offset功能. 
auto.commit.interval.ms: 自动提交offset的时间间隔.

4.2.4 手动提交offset

// 开启手动提交offset. cn.king.kfk01.consumer.AConsumer中有体现. 
properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);
// 自动提交的延迟. 1秒. 配置了手动提交之后, 自动提交的延迟这条配置就没有用了, 就算写了也不生效.
properties.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG,"1000");
  • 按理说如果consumer消费了90-100条数据, 如果不配置自动提交, 那么consumer不会将最新的offset(也就是100)写入kfk, 下次再消费也会从90开始消费, 消费的还是第90-100条数据.
  • 但是实际上, consumer内存中维护了一份offset, 即便是没有将offset提交到kfk, 那么consumer再次消费数据的时候, 会先去消费者内存中获取最新消费的offset. 即下次消费会从101开始消费. 但是offset始终没有写入到kfk, 那么kill掉消费者重启, 还是会从第90条消息开始消费.
  • 可见保存在kfk或者zk的offset只会在consumer启动的时候访问一次, consumer启动后访问的是consumer内存中维护的offset.
  • 自动提交offset的缺点
  • 自动提交是基于时间提交的(例如配置1s), 开发人员难以把握offset提交的时机. 因此可以使用手动提交offset; 例如配置了自动提交时间是1s, producer生产100条数据, consumer在没消费完100条数据之前就挂了, 此时offset提交了, 那么consumer重启之后offset=100开始消费, 这就产生了consumer丢数据. 所以自动提交不好, 推荐使用手动提交offset.
  • consumer手动提交offset有两种方式: 同步提交(commitSync) 异步提交(commitAsync)
  • 两者相同点: 都会将本次poll的一批数据的最高偏移量提交;
  • 两者不同点: commitSync阻塞当前线程, 一直到提交成功, 并且会自动失败重试(由不可控因素导致, 也会出现提交失败);
    commitAsync没有失败重试机制, 因此有可能提交失败.
  • 同步提交有失败重试机制, 因此更加可靠, 但是由于她会阻塞线程, 直到提交成功, 因此吞吐量会受到很大影响. 因此更多的情况下会选用异步提交offset的方式.
  • 同步提交: cn.king.kfk01.consumer.BConsumer
  • 异步提交: cn.king.kfk01.consumer.CConsumer
  • 无论是同步提交还是异步提交, 都可能出现数据漏消费或重复消费的问题.
  • 自动提交缺点
  • 如果设置的延迟时间过短会容易丢失数据. 上述已说明原因.
  • 如果设置的延迟时间过长会产生重复消费问题. 例如发送100条msg, 设置自动提交延迟为10s, 假设消费者5秒处理完了100条msg并且第6秒的时候消费者挂了, 那么等到10s的时候消费者才会提交offset, 那么第6秒时消费者还没提交offset, 那么重启消费者后, 因为offset还未提交, kfk中的offset还是消费这100条msg之前的offset, 那么从这100条mag之前开始消费, 这就是重复消费了.
  • 手动提交时, 使用异步提交也可能出现重复消费. cn.king.kfk01.consumer.CConsumer中的代码, 如果在调用commitAsync()方法时挂了, 那么offset没提交, 就会出现重复消费.
  • 由于无论是手动提交还是自动提交都可能出现重复消费, 所以kfk提供了一个自定义存储offset功能.

4.2.5 自定义存储offset

  • 提交offset提交到了kfk或者zk. 自定义存储offset的意思是我们可以将offset维护在mysql或者本地文件等地方.
  • 最好是将offset存储到mysql, 这样我们可以将 消息消费成功offset提交成功 搞一个事务.
  • offset的维护是很麻烦的, 因为涉及到 consumer的Rebalance(再平衡).
  • 当有新的consumer加入consumer group 或者 已有的consumer退出consumer group 或者 订阅的topic的partition发生变化, 就会触发partition的重新分配, 重新分配的过程叫做Rebalance.
  • consumer发生Rebalance之后, 每个consumer消费的partition就会发生变化. 因此consumer首先要获取到自己被重新分配到的partition, 并且定位到每个分区最近提交的offset位置继续消费.
  • 例如三个分区ABC的offset分别被3个consumer维护着, 每个分区的offset都写到mysql中, 如果维护A分区offset的消费者挂了, 那么就需要将A分区维护的offset交给B分区或者C分区维护. 这就叫再平衡.
  • 要实现自定义存储offset, 需要使用的类是 ConsumerRebalanceListener.
  • 如果要将offset存储到mysql, 那么这张表的列必须包含: consumer_group topic partition offset.
  • 希望consumer完全不丢失数据的时候需要使用自定义存储offset, 就是将消息的消费和偏移量的存储做一个事务.
  • 示例代码: cn.king.kfk01.consumer.DConsumer

4.3 自定义Interceptor

4.3.1 拦截器原理

  • 作用: msg发送前做一些定制化的需求, 比如修改msg等.
  • 一个producer可以指定多个拦截器形成一条拦截器链作用同一条消息.
  • 需要实现的接口: org.apache.kafka.clients.producer.ProducerInterceptor.
  • 拦截器是 kfk0.10 才有的.
  • 条消息都会经过一次拦截器链.
  • 拦截器中方法如下:
// 读取配置信息
public void configure(Map<String, ?> configs);
// 最核心的方法. 传进来一条生产者要发送的数据.
public ProducerRecord<K, V> onSend(ProducerRecord<K, V> record);
// 拦截器中也能收到ack. 即能判断消息发送是否成功.
// 如果metadata不为null, 说明发送成功
// 如果exception不为null, 说明发送失败
public void onAcknowledgement(RecordMetadata metadata, Exception exception);
// 
public void close();

4.3.2 拦截器案例

  • cn.king.kfk01.interceptor.AInterceptor: 在发送的消息前面加上时间戳.
  • cn.king.kfk01.interceptor.BInterceptor: 消息发送成功后会更新发送成功的消息数 或 发送失败的消息数.
  • cn.king.kfk01.producer.EProducer: 带拦截器的生产者.

五. KafKa监控

5.1 介绍

5.2 安装

5.3 使用

六. Flume对接Kafka

七. KafKa Streams

八. KafKa面试题