Kafka之深入理解服务端

1,567 阅读9分钟

文章内容参考《深入理解Kafka:核心设计与实践原理》,欢迎大家购买阅读。

协议设计

Kafka自定义了一组基于TCP的二进制协议,只要遵守这组协议格式,就能向Kafka发送消息。Kafka包含了多种协议类型,每种协议类型都有对应的请求(Request)和响应(Response)。每种类型的Request都包含相同结构的协议请求头(RequestHeader)和不同结构的协议请求体(RequestBody)

image.png

协议请求头中包含4个域(field):api-key、api-version、correlation_id、client-id,含义如下:

域(field)描述
api-keyAPI标识,比如PRODUCEFETCH分别表示发送消息和拉取消息的请求
api-versionAPI版本号
correlation_id请求唯一标识,在Reponse中会写入同样的correlation_id
client-id客户端id

每种类型的Response也包含了相同结构的协议响应头(ResponseHeader)和不同结构的响应体(ResponseBody)

image.png

协议响应头中只有一个correlation_id。接下来,我们以最常见的消息发送协议类型为例进行详细讲解。 首先是消息发送协议类型,即ProduceRequest/ProduceResponse,对应api_key =0。ProduceRequest的结构如下图所示。

image.png

域(field)描述
transaction_id事务id
acks对应客户端参数acks
timeout请求超时时间,对应客户端参数request.timeout.ms
topic_dataProduceRequest中要发送的数据集合。数组
topic主题名称
data与主题对应的数据,数组
partition分区编号
records与分区对应的数据

客户端会将消息按照topic、partion归纳好之后按照ProduceRequest的格式进行发送。如果acks的值非0,那么生产者在发送ProduceRequest之后就需要异步等待服务端响应ProduceResponse了,ProduceResponse结构如下图所示。

image.png

域(field)描述
throttle_time_ms如果超过了配额(quota)限制则需要延迟该请求处理时间,如果没有配置配额,该字段为0
responses返回的数据集合,数组
topic主题名称
partition_reponses主题中所有分区的响应数据,数组
partition分区编号
error_code错误码
base_offset消息集的起始偏移量
log_append_time写入broker端的时间
log_start_offset所在分区的起始偏移量

时间轮

Kafka中存在大量的延时操作,比如延时生产、延时拉取和延时删除等等。Kafka基于时间轮的概念自定义实现了一个用于延时功能的定时器,将插入和删除操作的时间复杂度降低为O(1)

image.png

如上图所示,Kafka中的时间轮(TimingWheel)是一个存储定时任务的环形队列,底层采用数组实现,数组中的每个元素可以存放定时任务列表(TimerTaskList)。TimerTaskList是一个环形双向链表,链表中的每一项表示的都是定时任务项(TimerTaskEntry),其中封装了真正的定时任务(TimerTask)

时间轮由多个时间格组成,每个时间格代表当前时间轮的基本时间跨度(tickMs)。时间轮的时间格个数是固定的,可用wheelSize表示,整个时间轮的总时间跨度(interval)等于tickMs x wheelSize。时间轮还有一个表盘指针(currentTime),用来表示时间轮当前所处的时间,currentTime是tickMs的整数倍。currentTime可以将整个时间轮划分为到期部分和未到期部分,currentTime当前指向的时间格也属于到期部分,表示刚好到期,需要处理此时间格所对应的所有TimerTaskList中的所有任务

如果时间轮的tickMs=1mswheelSize=20,那么总时间跨度就是20ms。初始化时currentTime=0ms,如果此时有一个定时为2ms的任务插入进来就会存放到时间格为2的TimerTaskList中。随着时间推移,当currentTime=2ms时,就会将时间格2对应的所有任务执行掉。

实际上,上面介绍的只是简单的时间轮,它只能应付到期时间小于总时间跨度的定时任务。当到期时间大于总时间跨度时,上述实现就不能满足要求了。为了解决这个问题,Kafka引入了层级时间轮当任务到期时间大于当前时间轮的总时间跨度时,就会将它添加到上层时间轮中

image.png

如上图所示,层级时间轮就是上一层时间轮的tickMs等于当前时间轮的总时间跨度interval,每层时间轮的格数保持一致。层级时间轮跟钟表类似,当前时间轮走完一圈,上一层时间轮走一格。就像秒钟走完一圈,分钟走一格,分钟走完一圈,时钟走一格。

还是以上面的例子来说明,第一层时间轮的时间跨度时1ms x 20 = 20ms,第二层是400ms,第三场是8000ms,以此类推。假设此时有一个350ms的定时任务,很明显第一层无法满足要求,它会升级插入到第二层时间轮时间格为17的位置。

还有一点要特别注意,上层时间轮每转动一次,并不会直接将对应时间格的任务直接执行,而是将该时间格对应任务按照剩余定时时间再向时间轮提交一次。此时,剩余时间肯定小于当前时间轮的基本时间跨度,因此任务肯定都会提交到下层甚至下下层时间轮,这个过程也叫作时间轮降级。

只有第一层时间轮的任务才会真正执行,其他层的任务最终都会降级到第一层时间轮中。

延时操作

如果客户端参数acks=-1,那么客户端发送消息就需要等待ISR集合中所有副本都确认收到消息之后才能收到响应结果,或者捕获超时异常。在将消息写入leader副本的本地日志文件之后,Kafka会创建一个延时的生产操作(DelayProduce),用来处理消息正常写入所有副本或超时的情况,以返回相应的响应结果给客户端

延时操作需要延时返回响应结果,首先它必须有一个超时时间(delayMs),如果在超时时间内没有完成既定任务,那么就需要强制完成以返回响应结果给客户端。延时操作不同于定时操作,定时操作是指在特定时间之后执行的操作,而延时操作可以在设定的超时时间之前完成,所以延时操作能够支持外部事件触发。就延时生产操作而言,它的外部事件是所要写入消息的某个分区的HW(高水位)增长,每一次增长都会检测是否能够完成此次延时生产操作,如果可以则返回结果给客户端;如果在超时时间内始终无法完成,则强制执行。

当follower副本已经拉取到leader副本的最新位置,此时又向leader副本发送拉取请求,而leader副本并没有新消息写入。此时,Kakka不会直接返回空结果给follower副本,而是会创建延时拉取操作来处理。Kafka在处理拉取请求时,会先读取一次日志,如果收集不到足够多的消息(由fetch.min.bytes参数控制,默认为1),那么就会创建一个延时拉取操作(DelayFetch)以等待拉取到足够的消息。延时拉取操作同样是由超时触发或者外部事件触发。

控制器

在Kafka集群中会有一个或多个broker,其中有一个broker会被选举为控制器,它负责管理整个集群中所有的分区和副本状态。当某个分区的leader副本出现故障时,由控制器负责为该分区选举新的leader副本。当检测到某个分区ISR集合发生变化时,由控制器负责通知所有broker更新元数据信息。

控制器的选举及异常恢复

Kafka中的控制器选举工作依赖于Zookeeper,成功竞选为控制器的broker会在Zookeeper中创建/controller临时节点。节点内容格式为:

{"version": 1, "brokerid": 0, "timestamp":"1529210278988"}

其中version固定为1,brokerid为成为控制器的broker的id编号。

在任意时刻,集群中有且只有一个控制器。每个broker在启动的时候都会尝试读取/controller节点的brokerid值,如果读取成功,则放弃竞选,否则就会尝试创建/controller节点。创建成功的broker即成为控制器。每个broker在内存中都会保存当前控制器的brokerid值。

Zookeeper中还有一个/controller_epoch的持久节点,用于记录当前是第几代控制器。controller_epoch的初始值为1,当控制器发生变更时,就会加1。每个和控制器交互的请求都会带上controller_epoch字段。

具备控制器身份的broker需要比其它普通的broker多一份职责,具体细节如下:

  • 监听分区相关变化。
  • 监听主题相关变化。
  • 监听broker相关变化。
  • 从Zookeeper中读取当前所有与主题、分区及broker有关的信息并进行相应的管理。
  • 启动并管理分区状态机和副本状态机。
  • 更新集群元数据信息。
  • 如果开启了自动均衡,会开启一个定时任务来维护优先副本均衡。

每个broker都会对/controller节点添加监听器,以此来监听此节点的数据变化。如果/controller节点中的brokerid发生变化,说明之前的控制器需要退位了,要关闭相应的资源。如果/controller被删除了,则会触发重新选举。

分区leader的选举

分区leader副本的选举是由控制器具体实施的。当创建分区或者分区上线的时候都需要执行leader的选举动作。选举策略的基本思路是:按照AR集合的副本顺序查找第一个存活的副本,并且这个副本在ISR集合中。一个分区的AR集合在分配的时候就被指定,并且只要不发生重分配,集合内部副本的顺序是不会变的,而分区ISR的副本顺序是会变化的。

当分区进行重分配的时候也需要执行leader选举,此时的选举策略是:从重分配的AR列表找到第一个存活的副本,并且这个副本在目前的ISR列表中。

当发生优先副本选举时,直接将优先副本设置成leader即可。