Kafka 服务中消费者创建详解

2020-05-20 0 By admin

与生产者对应的是消费者,应用程序可以通过 KafkaConsumer 来订阅主题,并从订阅的主题拉取消息。不过在使用 KafkaConsumer 之前需要了解消费者与消费组的概念,否则无法理解如何使用 KafkaConsumer。

一、消费者与消费组

消费者(Consumer)负责订阅主题(Topic),并从订阅的主题上拉取消息。与其他消息中间间不同的是,再Kafka消费者观念里还有一层消费组(Consumer Group)。当消息发送到主题后,主题只会把消息发送给订阅它的消费组内的一个消费者。

Kafka 消息队列服务消费者组
Kafka comsumer Group

按照Kafka的默认规则,
消费组A每个消费者分配到了 Kafka 这个主题的一个分区,
消费组B每个消费者分配到了 Kafka 这个主题的两个分区,两个消费组之间互不影响。即 Topic 中的一条消息,会被组A 和组B 分别消费一次。
每个消费者只能消费所分配到的分区中的消息。换言之,每一个分区只能被一个消费组中的一个消费者消费。

消费者和消费组间的关系
1、每个消费者都只有一个对应的消费组。
2、一个消费组可以包含一个或多个消费者。

主题和分区间的关系
1、一个主题可以有多个分区,也就是所有分区的消息集合就是主题的消息。

主题和消费组间的关系
1、一个主题可以被多个消费组订阅。
2、消费组中消费者的数目如果大于订阅的主题中分区的数目,则存在空闲的消费者(订阅不到分区)。

分区和消费者间的关系
1、每个分区中的消息只能被订阅其主题的消费组消费;分区中的消息只能被订阅得消费组中的一个消费者(组内)消费。
2、每个消费者只能消费所分配到的分区中的消息。

消费者和消费组的这种模型可以让整体的消费能力具备横向伸缩性,可以增加或减少消费者的个数来提高或减少整体消费能力。

分区分配策略可以通过消费者客户端的 partition.assignment.strategy 参数来设置消费者与订阅主题之间的分区分配策略。

对于消息中间件而已,一般有两种消息投递模式:
1、点对点(P2P,Point-to-Point)模式和发布/订阅(Pub/Sub)模式。点对点模式是基于队列的,生产者将消息发送到队列,消费者从队列接收消息。
2、发布/订阅模式定义了如何向一个内容节点发送和订阅消息,这个内容节点在Kafka中为主题(Topic)相当于一个中介,生产者发布消息到主题,而消费者消费所订阅的主题。

主题使消息的订阅者和发布者互相保持独立,不需要接触就可以进行消息的传递,发布/订阅模式在消息一对多广播时使用。Kafka同时支持这两种模式,正是得益于消费者和消费组模型的契合。

二、消费者客户端开发

一个正常的消费逻辑需要具备以下几个步骤:

  1. 配置消费者客户端参数及创建相应的消费者实例。
  2. 订阅主题
  3. 拉取消息并消费
  4. 提交消费位移
  5. 关闭消费者实例

默认配置是从订阅开始后,才开始消费新消息如果需要从起始位置消费那么需要修改消费者客户端参数。

2.1、必要的参数配置

在创建真正的消费者实例之前需要做参数配置,比如上节设置消费组名称、连接地址等。
1、bootstrap.servers:该参数的释义和生产者客户端 KafkaProducer 中的相同。
2、key.deserializer 和 value.deserializer:与生产者客户端 KafkaProducer 中的 key.serializer和value.serializer 参数对应。
3、group.id:消费者隶属的消费组的名称,默认为“”。如果设置为空会报异常。一般情况下,这个参数需要设置成具有一定的业务意义的名称。

2.2、订阅主题和分区

创建好消费者后,需要给该消费者订阅相应的主题。一个消费者可以订阅一个或多个主题。
对于消费者使用集合的方式来订阅主题而言,比较容易理解,订阅了什么主题就消费集合中的主题。如果两次订阅的主题不一样,那么以最后一次为准。
消费者不仅可以通过 KafkaConsumer.subscribe() 方法订阅主题,还可以指定订阅某些主题的特定分区。
既然有订阅,那么就有取消订阅,可以使用 KafkaConsumer 中的 unsubscribe() 方法取消主题的订阅。

2.3、反序列化

kafka 所提供的反序列化器有 ByteBufferDeserializer、ByteArrayDeserializer、BytesDeserializer、DoubleDeserializer、FloatDeserializer、IntegerDeserializer、LongDeserializer、ShortDeserializer、StringDeserializer,它们分别用于 ByteBuffer、ByteArray、Bytes、Double、Float、Integer、Long、Short 及 String 类型的反序列化,这些序列化器也都实现了 Deserializer 接口。

2.4、消息消费

Kafka 中的消费是基于拉取模式的。
消息的消费一般有两种模式:推送模式和拉取模式。推送模式是服务端主动将消息推送给消费者,而拉取模式是消费者从服务端拉取消息。

Kafka 中的消息消费就是一个不断轮询的过程,在轮询中重复调用 poll() 方法去拉取订阅的主题的消息。对于 poll() 方法而言,如果某些分区没有消息,那么此分区对应的消息拉取结果就为空;如果整个主题都没消息,那么poll() 方法的拉取结果是一个空的集合。
poll() 方法里还有一个超时时间参数 timeout,用来控制 poll() 方法的阻塞时间,在消费者的缓冲区里没有可用数据时会发生阻塞。
消费者消费到的每条消息类型为 ConsumerRecord(注意与 ConsumerRecords 的区别),这个和生产者发送的消息 类型 ProducerRecord 相对应。

2.5、移位提交

对于Kafka分区中的每条消息而言,都有一个 offset,用来表示消息在分区中对应的位置。对于消费者而言,也有一个 offset 概念,用来表示消费到分区中某个消息所在的位置。对于消息在分区中的位置,将 offset 称为“偏移量”,代表了分区储存层面。

在每次调用poll()方法的时候,返回的都是未被消费的消息集,要做到这一点就需要记录上一次消费时的消费位移。并且这个消费位移必须持久化,不能单单保存在内存中,否则消费者重启后就无法知晓之前的消费位移。并且,当有新的消费者加入时,那么必然会有再均衡动作,对于同一分区而言,它可能在再均衡动作之后分配给新的消费者,如果不持久化保存消费位移,新的消费者也就不会知道消费到哪里。

消费者客户端中,消费位移存储在Kafka内部的主题__consumer_offsets 中。这里把将消费位移存储起来的动作称之为“提交”,消费者客户端消费完消息后需要执行消费位移的提交。

在Kafka中默认的消费位移的提交方式是自动提交,这个由消费者客户端参数 enable.auto.commit 配置,默认为 true这个默认的自动提交不是每次消费都会提交,而是定期提交,时间由客户端参数 auto.commit.interval.ms 配置,默认值为5秒。

自动提交在正常情况下不会出现数据重复和数据丢失等情况,但是在编程里异常无可避免,且自动提交也无法做的精准的位移管理。
在Kafka中还提供了手动提交的方式,手动提交的方式可以让开发人员根据程序的逻辑在合适的地方进行位移提交,这样可以使开发人员对消费位移的管理控制更加灵活。开启手动提交的前提是消费者客户端参数 enable.auto.commit 配置为false

手动提交细分为同步提交和异步提交,对应KafkaConsumer中的 commitSync() 和 commintAsync() 两种类型方法。

2.6、控制或关闭消费

KafkaConsumer 提供了对消费速度进行控制的方法,在有些场景下我们可能需要暂停某个分区的消费先消费其他分区,等条件达到的时候在恢复。KafkaConsumer 中使用 pause()和resume() 方法来分别实现暂停某些分区向客户端返回数据和恢复某些分区向客户端返回数据。

2.7、指定位移消费

如果当一个新的消费组建立的时候,根本没有可以查找的消费位移。或者消费组内的一个新消费者订阅了一个新的主题,它也没有可以查找的消费位移。(同一个分区的消息,对同一个消费组来说只能消费一次。所以当新的消费组建立或者消费者订阅了新的消费组,也就代表了该消费组在这个分区中没有位移信息。)当_consumer_offsets 主题中有关这个消费组的位移信息过期而被删除后,它也没有可以查找的消费位移。
在Kafka中,当消费者找不到所记录的消费位移时,就会根据消费者客户端参数 auto.offset.reset 的配置来决定从何处开始消费,如果值是“earliest” 那么从起始处开始,如果值是“latest”那么从末尾开始,默认值是“latest”(就是下一条要写入消息的位置)。除了查找不到消费位移,位移越界也会触发 auto.offset.reset 参数的执行。

2.8、再均衡

再均衡是指分区的所属权从一个消费者转移到另一个消费者的行为,它为消费组的高可用性和伸缩性提供保障,使消费组可以方便安全的删除或者添加组内的消费者。不过在再均衡发生期间,消费组内的消费者是无法读取信息的。也就是说,在再均衡发生期间的这一小段时间内,消费组会变的不可用。

另外,当一个分区被重新分配给另外一个消费者时,消费者当前的状态也会丢失。比如消费者消费完某个分区中的一部分信息,还没有提交消费位移就分配给了另外一个消费者,此时这个消费者会重新消费一遍,也就造成了重复消费,所以应该尽量避免不必要的在均衡发生。

2,9、消费者拦截器

消费者拦截器主要在消费到消息或者在提交消费位移的时候进行一些定制化操作。

2.10、消费者多线程实现

KafkaProducer 是线程安全的,而 KafkaConsumer 却是非线程安全的。KafkaConsumer中定义了一个 acquire() 方法用来检查当前是否只有一个线程在操作,如果有其他线程在操作那么就会抛出 ConcurrentModifcationException 异常。
KafkaConsumer 所有公用方法在执行之前都会调用 acquire() 方法,只有 wakeup() 方法例外。

消费者多线程的实现方式

KafkaConsumer 非线程安全并不意味着消费的时候只能以单线程的方式进行。如果生产者发送消息的速度大于消费者处理消息的速度,那么就会有越来越多的消息不能及时消费,消费就会有一定的延迟。除此之外,由于Kafka中消息保留机制的作用,很可能造成有些消息还未被消费就被清理,从而造成消息丢失。我们可以通过多线程的方式来实现消息消费,多线程的目的就是为了提高整体消费能力。

第一种也是常见的一种:线程封闭,也就是为每个线程实例化一个 KafkaConsumer对象。
一个线程对应一个 KafkaConsumer示例,称之为消费线程。一个消费线程可以消费一个或多个分区中的消息,所有的消费线程都属于一个消费组。这种实现方式的并发度取决于分区数量,根据之前介绍的消费者与分区数的关系,当消费线程的数量大于分区数的时候就会有部分消费线程一直处于空闲状态。

第二种:多个消费者同时消费同一个分区,这个通过 assign() –订阅特定主题的某个分区、seek() –指定位移消费 等方法实现,这样可以打破原有的消费线程数不能超过分区数的限制,进一步提高消费能力。不过这这种实现方式对于位移提交和顺序控制的处理会变的非常复杂,实际应用中使用的极少,并不推荐使用。

第三种 将处理消息模块改成多线程实现方式,如果对消息的处理非常迅速,那么poll()拉取的频次也会同步提高,整体消费能力也会提示;
相反,如果消息处理慢,比如进行一个事务性操作或者等待一个RPC的同步响应,那么poll()拉取的频次也会随之下降,那么整体消费能力就会下降。
其实,poll()拉取是非常快的,消费速度的瓶颈就在于消费处理的速度。

三、重要的消费者参数

1.fetch.min.bytes
Consumer 在一次拉取请求中能从 Kafka中拉取的最小数据量,默认值1(B)。
kafka在收到 Consumer 的拉取请求时,如果返回给 Consumer的数据量小于这个设置,那么就会等待,直达数据量达到配置的大小。可以适当调整这个参数的值以提高吞吐量,不过也会造成额外的延迟。

2.fetch.max.bytes
与 fetch.min.bytes对应,配置 Consumer 在一次拉取请求中从 Kafka 中拉取的最大数据量,默认值为 52428800(B),也就是50MB。
如果这个参数设置的值比任何一条写入 Kafka 中的消息要小,那么会不会造成无法消费呢?很多资料对此参数的解读认为是无法消费的,比如一条消息的大小为10B,而这个参数的值是1(B),既然此参数设定的值是一次拉取请求中所能拉取的最大数据量,那么显然1B<10B,所以无法拉取。这个观点是错误的,该参数设定的不是绝对的最大值,如果在第一个非空分区中拉取的第一条消息大于该值,那么该条消息将仍然返回,以确保消费者继续工作。也就是说,在这种情况下只会返回这一条record。

与此相关的,Kafka 中所能接收的最大消息的大小通过服务端参数 message.max.bytes(对应于主题端参数 max.message.bytes)来设置。

3. fetch.max.wait.ms
这个参数也和 fetch.min.bytes 参数有关,如果 Kafka 仅仅参考 fetch.min.bytes 参数的要求,那么有可能会一直阻塞等待而无法发送响应给 Consumer,显然这是不合理的。
fetch.max.wait.ms 参数用于指定 Kafka 的等待时间,默认值为500(ms)。如果 Kafka 中没有足够多的消息而满足不了 fetch.min.bytes 参数的要求,那么最终会等待500ms。这个参数的设定和 Consumer 与 Kafka 之间的延迟也有关系,如果业务应用对延迟敏感,那么可以适当调小这个参数。

4. max.partition.fetch.bytes
这个参数用来配置从每个分区里返回给 Consumer 的最大数据量,默认值为1048576(B),即1MB。
这个参数与 fetch.max.bytes 参数相似,只不过前者用来限制一次拉取中每个分区的消息大小,而后者用来限制一次拉取中整体消息的大小。同样,如果这个参数设定的值比消息的大小要小,那么也不会造成无法消费,Kafka 为了保持消费逻辑的正常运转不会对此做强硬的限制。

5.max.poll.records
这个参数用来配置 Consumer 在一次拉取请求中拉取的最大消息数,默认值为500(条)。
如果消息的大小都比较小,则可以适当调大这个参数值来提升一定的消费速度。

6. connections.max.idle.ms
这个参数用来指定在多久之后关闭闲置的连接,默认值是540000(ms),即9分钟。

7. exclude.internal.topics
Kafka 中有两个内部的主题:consumer_offsets 和 __transaction_state。
exclude.internal.topics 用来指定 Kafka 中的内部主题是否可以向消费者公开,默认值为 true。如果设置为 true,那么只能使用 subscribe(Collection)的方式而不能使用 subscribe(Pattern)的方式来订阅内部主题,设置为 false 则没有这个限制。

8. receive.buffer.bytes
这个参数用来设置 Socket 接收消息缓冲区(SO_RECBUF)的大小,默认值为65536(B),即64KB。如果设置为-1,则使用操作系统的默认值。
如果 Consumer 与 Kafka 处于不同的机房,则可以适当调大这个参数值。

9. send.buffer.bytes
这个参数用来设置Socket发送消息缓冲区(SO_SNDBUF)的大小,默认值为131072(B),即128KB。
与receive.buffer.bytes参数一样,如果设置为-1,则使用操作系统的默认值。

10. request.timeout.ms
这个参数用来配置 Consumer 等待请求响应的最长时间,默认值为30000(ms)。

11. metadata.max.age.ms
这个参数用来配置元数据的过期时间,默认值为300000(ms),即5分钟。
如果元数据在此参数所限定的时间范围内没有进行更新,则会被强制更新,即使没有任何分区变化或有新的 broker 加入。

13. retry.backoff.ms
这个参数用来配置尝试重新发送失败的请求到指定的主题分区之前的等待(退避)时间,避免在某些故障情况下频繁地重复发送,默认值为100(ms)。

14. isolation.level
这个参数用来配置消费者的事务隔离级别。
字符串类型,有效值为“read_uncommitted”和“read_committed”,表示消费者所消费到的位置,如果设置为“read_committed”,那么消费者就会忽略事务未提交的消息,即只能消费到LSO(LastStableOffset)的位置,默认情况下为“read_uncommitted”,即可以消费到 HW(High Watermark)处的位置。