WRY

Where Are You?
You are on the brave land,
To experience, to remember...

0%

《Kafka权威指南》要点记录

基本概念

Kafka是一个分布式的、可分区的、可复制的提交日志服务。

生产者

生产者的角色顾名思义,是将消息推送到Kafka中,Kafka的客户端官方是Java程序,但只要实现了Kafka的二进制传输协议,可以用任何其他语言再次实现客户端。生产者关心的问题主要有两个:发送的消息内容和发送消息的策略。

发送消息的内容

要发送消息的内容,包括下面四部分:

  • Topic,要发送到的主题名称
  • Partition,要发送到的主题分区
  • Key,消息键,可以理解是消息内容的一部分
  • Value,消息值,也是消息内容的一部分

其中Topic和Value是用户发送消息时必须要声明的。Key和Partition是可选的,其中Partition可以指定,也可以根据Key来生成:

  • 如果Key不为空,默认会使用默认的分区器,根据Key将其散列到partition上,因此,在partition的数量不变的情况下,相同Key的消息都会发送到相同的partition中。
  • 用户可以自定义分区器,从而实现不同的key分发到不同的partition中,通过给partition赋予不同的性能实现,从而实现对不同类型的消息在不同性能的消息流上传输。

此外因为消息在Kafka中是二进制的格式进行存储的,所以我们还需要提供Key和Value的序列化方法。

发送消息的策略

Kafka客户端在发送消息(使用send函数)之前会将消息暂存起来(根据topic和partition的不同分别暂存),然后批量发送个服务端,提升吞吐量。客户端可以从如下维度控制消息的发送:

  • 缓存消息的大小和消息最多暂存的时间,只要其中一个条件满足,就会将缓存中的消息批量发送出去。
  • 并行发送的数量,并行发送会导致服务端接受的顺序和实际顺序不一致,可以设此参数为1,来保证服务端接受的消息是顺序的
  • 最大重试次数、超时时长等等
  • 确认收到消息的模式:不需要服务端确认、master确认或者所有参与复制的节点都收到消息再确认。
  • 压缩算法,客户端提供消息压缩支持
  • 内存缓存大小和阻塞send函数的时长,这个主要是在send的速度高于实际发送的速度后,客户端最大缓存的积压消息的大小以及阻塞send函数多长时间之后返回发送失败的结果。

客户在使用send函数的时候支持三种发送模式:

  • 同步,等待结果发送
  • 异步,不关心结果发送
  • 异步,传入回调函数发送

消费者

基本概念

消费者比生产者的逻辑要复杂很多,因此书中也先介绍了一些基本概念,主要如下:

  • 消费者和消费者群组
    • 消费者属于消费者群组,一个群组里的消费者订阅同一个主题,每个消费者接受主题一部分分区的信息。
    • 分区和消费者之间的对应关系:只要一个分区不被多个消费者消费就可以。
  • 消费者群组和分区再均衡
    • 当消费者发生变化(例如退出或者崩溃)或者主题发生变化(例如增加了新的分区)时,触发分区再均衡。
    • 再均衡是指,原来的分区从一个消费者转移到另一个消费者来消费。
    • 再均衡期间,群组会有短暂的时间不可用,且对应关系发生变化的消费者当前的读取状态会丢失,需要重新刷缓存。
    • 消费者通过向被指派为群组协调器的broker发送心跳来维持他们和群组之间的从属关系以及他们对分区所有权的关系。
    • 在0.10.1版本里,心跳和轮询有两个独立线程负责,从而将发送心跳的频率和消息轮训的频率独立开(之前实习的时候,kafka运维同学在排查问题的时候就沟通过消息处理的时长,判断是否是因为轮询周期过长导致频繁的再均衡,当时的kafka版本应该比较低)
  • 分配分区的过程
    • 当消费者加入群组的时候,会向群组协调器发送一个JoinGroup的请求,第一个加入群组的消费者将成为群主,群主会从协调器获取群组里的成员信息,并负责给每个成员分配分区。
    • 群主将分配结果反馈给群组协调器,群组协调器把这些消息发送给所有消费者,每个消费者只能看到自己的分区信息。
    • 具体分配策略,默认官方提供两种
      • Range:在每个主题的范围内将分区分配给消费者,有可能会造成分配比较不均衡的情况发生。
      • RoundRobin:把主题的所有分区逐个分配给消费者,这种策略会更加均衡。

在消费组中消费消息

创建消费者

每个消费者创建时都必须携带三个必须信息:kafka的服务地址、key的反序列化方法和value的反序列化方法。因为要在群组里面进行消费,所以还需要group.id,但也可以创建一个不属于任何群组的消费者。

订阅主题

在订阅主题时,可以订阅单个topic,也可以使用正则表达式,订阅多个topic。在使用正则表达式进行订阅的时候,若新的topic名称和正则表达式相匹配会立即进入一次再均衡。

轮询

消费者通过轮询的方式,查询是否有新的可以消费的数据,轮询也是保证消费者存活的一种方式(区分于心跳信号,长时间不轮询会被认为是活锁)。在轮询中可以指定阻塞的时长,在指定时间内没有获取到新消息就会直接返回,返回的内容是一个消息列表。

在消费者第一次执行轮询时,会负责查找群组协调器,加入群组,接受分配的分区。此外在轮询中也会完成再均衡的过程。

消费者的配置

  • 消费者从服务器中获取记录的最小字节数 & 消费者从服务器中获取记录的最长等待时间,这两个参数只要有一个满足了,就会返回。
  • 服务器从每个分区里返回的最大字节数。若该值为1M,同时有20个分区,5个消费者,则每个消费者需要有至少4M的缓存空间接受服务器的数据返回。若缓存空间过小的话,会造成消费者一直挂起重试。此外若该参数设置过大,也容易造成消费者的处理时间过长,导致长时间没有轮询,而被认为是脱离了群组。
  • 心跳的频率和最长不发送心跳的时间,心跳频率一般为最长不发送心跳时间的三分之一,通过这两个参数的配合,可以在快速响应与减少再均衡之间权衡。
  • 在消费者申请读取的偏移量无效的情况下,服务器重置偏移量的策略,从最早或者最新的地方开始读。
  • 偏移量提交策略,可以有自动提交,设置自动提交时间间隔等,也可以设置成手动提交。
  • 分区在消费者之间的分配策略
  • 客户端ID
  • 单次调用call函数可以返回的记录数量
  • 指定用到的发送和接收的缓冲区

提交和偏移量

每个分区的消费情况是由服务端进行记录的,每次消费者更新分区的偏移量的时候都是向一个_consumer_offset的特殊主题中发送消息。偏移量的提交策略有自动和手动两种:

  • 自动提交会在设置的时间间隔向服务端提交最后一次轮询返回的偏移量。
  • 手动提交偏移量的方式有同步提交和异步提交两种,其中同步提交会自动进行重试;而异步提交不会,异步提交如果进行重试的话,可能会将较小的偏移量覆盖掉后来提交的较大偏移量。
  • 手动提交通常会采用同步和异步将结合的方式,在程序正常运行的过程中使用异步提交,在程序关闭的时候使用同步提交。
  • 手动提交默认的偏移量是最后一次轮询返回的偏移量,但也可以指定偏移量,多用于一个批次里面的数据量比较大,存在消费了一部分数据之后程序意外退出风险的情况。

再均衡监听器

在发生再均衡之前,会进行一些清理工作,允许用户编写要发生再均衡时的处理函数和重新获得新分区时的处理函数。

  • 在发生再均衡之前,通常会执行提交最新偏移量的函数
  • 在重新获取新分区的时候,允许我们从指定的偏移量开始消费,而不是服务端记录的最新偏移量的地方开始消费。再一些严格要求每条消息执行且执行一次的场景,这就允许我们把消息存储到其他例如Mysql数据库的地方,保证一些事务的原子性。

退出

消费者的程序通常会是一个死循环一直循环消费消息,kafka客户端为我们提供了优雅的退出方式,在另一个线程里面执行consumer.wakeup()会让poll函数返回一个wakeupException的异常。从而退出循环,这个异常是不需要进行处理的,wakeup会保证这种退出的方式是安全的。

反序列化

和生产者的序列化方法相对应,在此不关心。

独立消费者

独立消费者可以为自己分配需要消费的分区。但是当有新的分区的时候,消费者并不会收到通知,只能自己周期性的查询。

Kafka 服务端

集群成员关系管理

Kafka使用ZooKeeper来维护集群成员的信息,每个broker有一个唯一ID注册到Zookeeper中。若broker出现异常,会断开和Zookeeper之间的连接,Zookeeper会通知监听broker列表的组件,该broker已经被移除了。

控制器

控制器本身就是一个broker,在具有broker的一般功能之外,还负责分区首领的选举。第一个加入的节点会在Zookeeper中注册一个临时节点/controller,让自己成为控制器。若某个承担了控制器身份的broker离线的话,Zookeeper会通知各个broker,broker在知道分区控制器失效的时候,会尝试在Zookeeper中注册临时节点/controller,让自己成为一个控制器。每个新选出的控制器通过Zookeeper的条件递增操作获得一个全新的、数值更大的controller epoch,若节点收到了包含之前较小epoch的消息时,会直接忽略掉。通过这个epoch,Kafka避免了脑裂情况的发生。脑裂是指同时有多个节点认为自己是控制器。

复制

复制是Kafka架构的核心,使得Kafka在部分节点失效的情况下,仍能保证高可用性和持久性。复制的对象是分区,每个分区都有多个副本,这些副本就保存在broker上。

副本有两种类型:

  • 首领副本,每个分区都会有一个首领副本,所有生产者和消费者的请求都会经过这个副本。
  • 跟随者副本,只负责从首领那里复制消息,保持与首领一致的状态。在首领发生崩溃的时候,跟随者会被提升为新首领。

跟随者副本分为同步的副本和不同步的副本,只有同步的副本可以被提升为首领。副本从首领中复制消息的过程和消费者消费分区的原理基本一致,但副本消费消息是有序的,所以可以设置成如下模式,只要副本请求了偏移量x,那就说明之前的偏移量已经同步了。判断同步与不同步的规则是,若副本规定时间内没有请求任何消息或者10秒内没有请求最新的数据,就会被认为是不同的;反之是同步的。

此外在跟随者中还会有一个首选首领,首选首领是根据各个broker之间的负载均衡算法计算得到的,这可以让首选首领成为首领之后,broker之间的负载仍是均衡的。但首选首领也需要满足同步副本的规则才能成为首领。首选首领就在副本列表中的第一个。

处理请求