24 geek-请求是怎么被处理的?

Kafka使用的是Reactor模式

Reactor模式是事件驱动架构的一种实现方式,特别适合应用于处理多个客户端并发向服务器端发送请求的场景

Reactor模式如图

从这张图中,我们可以发现,多个客户端会发送请求给到Reactor。Reactor有个请求分发线程Dispatcher,也就是图中的Acceptor,它会将不同的请求下发到多个工作线程中处理。

         Kafka提供了Broker端参数num.network.threads,用于调整该网络线程池的线程数。其默认值是3,表示每台Broker启动时会创建3个网络线程,专门处理客户端发送的请求

        Acceptor线程采用轮询的方式将入站请求公平地发到所有网络线程中,因此,在实际使用过程中,这些线程通常都有相同的几率被分配到待处理请求。这种轮询策略编写简单,同时也避免了请求处理的倾斜,有利于实现较为公平的请求处理调度。

        

        当网络线程拿到请求后,它不是自己处理,而是将请求放入到一个共享请求队列中。Broker端还有个IO线程池,负责从该队列中取出请求,执行真正的处理。如果是PRODUCE生产请求,则将消息写入到底层的磁盘日志中;如果是FETCH请求,则从磁盘或页缓存中读取消息。

        IO线程池中的线程才是执行请求逻辑的线程。Broker端参数num.io.threads控制了这个线程池中的线程数。目前该参数默认值是8,表示每台Broker启动后自动创建8个IO线程处理请求。你可以根据实际硬件条件设置此线程池的个数。

        比如,如果你的机器上CPU资源非常充裕,你完全可以调大该参数,允许更多的并发请求被同时处理。当IO线程处理完请求后,会将生成的响应发送到网络线程池的响应队列中,然后由对应的网络线程负责将Response返还给客户端。

细心的你一定发现了请求队列和响应队列的差别:请求队列是所有网络线程共享的,而响应队列则是每个网络线程专属的。这么设计的原因就在于,Dispatcher只是用于请求分发而不负责响应回传,因此只能让每个网络线程自己发送Response给客户端,所以这些Response也就没必要放在一个公共的地方。

我们再来看看刚刚的那张图,图中有一个叫Purgatory的组件,这是Kafka中著名的“炼狱”组件。它是用来缓存延时请求(Delayed Request)的。所谓延时请求,就是那些一时未满足条件不能立刻处理的请求。比如设置了acks=all的PRODUCE请求,一旦设置了acks=all,那么该请求就必须等待ISR中所有副本都接收了消息后才能返回,此时处理该请求的IO线程就必须等待其他Broker的写入结果。当请求不能立刻处理时,它就会暂存在Purgatory中。稍后一旦满足了完成条件,IO线程会继续处理该请求,并将Response放入对应网络线程的响应队列中。

讲到这里,Kafka请求流程解析的故事其实已经讲完了,我相信你应该已经了解了Kafka Broker是如何从头到尾处理请求的。但是我们不会现在就收尾,我要给今天的内容开个小灶,再说点不一样的东西。

到目前为止,我提及的请求处理流程对于所有请求都是适用的,也就是说,Kafka Broker对所有请求是一视同仁的。但是,在Kafka内部,除了客户端发送的PRODUCE请求和FETCH请求之外,还有很多执行其他操作的请求类型,比如负责更新Leader副本、Follower副本以及ISR集合的LeaderAndIsr请求,负责勒令副本下线的StopReplica请求等。与PRODUCE和FETCH请求相比,这些请求有个明显的不同:它们不是数据类的请求,而是控制类的请求。也就是说,它们并不是操作消息数据的,而是用来执行特定的Kafka内部动作的。

Kafka社区把PRODUCE和FETCH这类请求称为数据类请求,把LeaderAndIsr、StopReplica这类请求称为控制类请求。细究起来,当前这种一视同仁的处理方式对控制类请求是不合理的。为什么呢?因为控制类请求有这样一种能力:它可以直接令数据类请求失效!

我来举个例子说明一下。假设我们有个主题只有1个分区,该分区配置了两个副本,其中Leader副本保存在Broker 0上,Follower副本保存在Broker 1上。假设Broker 0这台机器积压了很多的PRODUCE请求,此时你如果使用Kafka命令强制将该主题分区的Leader、Follower角色互换,那么Kafka内部的控制器组件(Controller)会发送LeaderAndIsr请求给Broker 0,显式地告诉它,当前它不再是Leader,而是Follower了,而Broker 1上的Follower副本因为被选为新的Leader,因此停止向Broker 0拉取消息。

这时,一个尴尬的场面就出现了:如果刚才积压的PRODUCE请求都设置了acks=all,那么这些在LeaderAndIsr发送之前的请求就都无法正常完成了。就像前面说的,它们会被暂存在Purgatory中不断重试,直到最终请求超时返回给客户端。

设想一下,如果Kafka能够优先处理LeaderAndIsr请求,Broker 0就会立刻抛出NOT_LEADER_FOR_PARTITION异常,快速地标识这些积压PRODUCE请求已失败,这样客户端不用等到Purgatory中的请求超时就能立刻感知,从而降低了请求的处理时间。即使acks不是all,积压的PRODUCE请求能够成功写入Leader副本的日志,但处理LeaderAndIsr之后,Broker 0上的Leader变为了Follower副本,也要执行显式的日志截断(Log Truncation,即原Leader副本成为Follower后,会将之前写入但未提交的消息全部删除),依然做了很多无用功。

再举一个例子,同样是在积压大量数据类请求的Broker上,当你删除主题的时候,Kafka控制器(我会在专栏后面的内容中专门介绍它)向该Broker发送StopReplica请求。如果该请求不能及时处理,主题删除操作会一直hang住,从而增加了删除主题的延时。

基于这些问题,社区于2.3版本正式实现了数据类请求和控制类请求的分离。其实,在社区推出方案之前,我自己尝试过修改这个设计。当时我的想法是,在Broker中实现一个优先级队列,并赋予控制类请求更高的优先级。这是很自然的想法,所以我本以为社区也会这么实现的,但后来我这个方案被清晰地记录在“已拒绝方案”列表中。

究其原因,这个方案最大的问题在于,它无法处理请求队列已满的情形。当请求队列已经无法容纳任何新的请求时,纵然有优先级之分,它也无法处理新的控制类请求了。

那么,社区是如何解决的呢?很简单,你可以再看一遍今天的第三张图,社区完全拷贝了这张图中的一套组件,实现了两类请求的分离。也就是说,Kafka Broker启动后,会在后台分别创建两套网络线程池和IO线程池的组合,它们分别处理数据类请求和控制类请求。至于所用的Socket端口,自然是使用不同的端口了,你需要提供不同的listeners配置,显式地指定哪套端口用于处理哪类请求。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值