Kafka学习笔记(十三) --- 请求是怎么处理的

Apache Kafka定义了一组请求协议,用于实现各种各样的交互操作。如:PRODUCE请求用生产消息的,FETCH请求用于消费消息的,METADATA请求用于请求Kafka集群元数据的。目前,截止2.3版本,Kafka定义了45种请求格式,并且所有的请求都是通过TCP网络以Socket的方式进行通讯的。今天主要讨论Kafka Broker端处理请求的全流程。

怎样处理请求,很容易就想到两个:

1.顺序处理请求。这个方法实现简单,但是吞吐量太差。因为每个请求都要等待前面请求处理完毕才轮到它处理。

2.每个请求使用单独线程处理。也就是每个入站请求都创建一个线程来异步处理,这种做法开销极大,某些场景下甚至会压垮整个服务。

上面两种方案都只适用于发送频率很低的业务场景。Kafka真正的使用的是Reactor模式,简单来说,Reactor模式是事件驱动架构的一种实现方式,特别适合应用于处理多个客户端并发向服务器发送请求的场景。它的架构如下图所示:

                                             

从这张图可以看到,多个client向Reactor发送请求,Reactor有个请求分发线程Dispatcher,也就是图中的Acceptor,它会将不同的请求下发到多个工作线程中处理。Acceptor只是用于请求分发,没有具体的处理逻辑,非常轻量级,因此有很高的吞吐量的表现。而这些工作线程会根据业务处理需要,任意地增减,达到动态调节系统负载能力。

下面也为Kafka画一张类似的图:

                                             

显然,这两张图比较类似。Kafka的Broker端有个SocketServer组件,类似于Dispatcher,也有对应的Acceptor线程和工作线程池,即网络线程池。Kafka提供了Broker端参数num.network.threads,用于调整该网络线程池的线程数,默认值为3。

另外,Acceptor线程采用轮询的方式将入站请求公平地发到所有网络线程中,所以,这些线程都有相同的几率被分配到待处理请求,而且这种轮询策略实现简单,同时避免了请求处理倾斜,有利于实现叫公平的请求处理调度。

当一个请求被分发到一个网络线程后,并不是被顺序处理,这里Kafka还做了另外一层异步线程池的处理,且看下图:

                                     

当网络线程拿到请求后,直接将请求放入一个共享请求队列中。Broker端还有个IO线程池,负责从队列中取出请求并处理。如果是PRODUCE请求,则将消息写入底层磁盘日志中;如果是FETCH请求,则从磁盘或页缓存读取消息。Broker端有个参数num.io.threads,控制线程池中线程数,默认值为8。当IO线程处理完请求,会将生成的响应发送到网络线程池的响应队列中,最后由对应的网络线程负责将Response返还给客户端。注意,请求队列是所有网络线程共享的,而响应队列则是每个网络线程专属的。Dispatcher只负责请求分发,而不管响应回传,因此只能要求每个网络线程自己发送Response给客户端,座椅这些Response就没用必要放到公共的地方。

另外图中还有Purgatory组件,即 “炼狱”组件。用来缓存延时请求,即那些为满足条件不能立即处理的请求。比如,设置了acks=all的PRODUCE请求,那么该请求就必须等待ISR中所有副本都接受消息后才返回,此时处理该请求的IO线程就必须等待其他Broker的写入结果。一旦该请求满足一定条件后,IO线程就会继续处理该请求,并将Response放入对应网络线程的响应队列中。

以上所讲的请求处理流程适用用于Kafka中所有的请求类型,社区将Producer请求和FETCH请求统称为数据类请求,把其他的请求类型称为控制类请求。比如负责更新Leader副本、Follower副本及ISR集合的LeaderAndIsr请求,负责勒令副本下线的StopReplica请求等,这些控制类请求只负责执行特定Kafka内部动作的。事实上Kafka这种对所有请求一视同仁的处理方式是不合理的,因为控制类请求有一种能力,可以让数据类请求失效!

举个例子,假设某个主题只有1个分区,该分区配置了两个副本,其中Leader副本在Broker0上,Follower副本在Broker1上。将设Broker0积压了很多PRODUCE请求,如果使用Kafka命令强制将该主题分区的Leader副本和Follower互换角色,那么Controller组件会发送LeaderAndIsr请求给Broker0,显示告诉它,变更为Follower角色,而Broker1上Follower副本变更为新的Leader角色,停止从Broker0拉取消息。

这时候,一个尴尬的场面出现了:如果刚才积压的PRODUCE请求都设置了acks=all,那么这些在LeaderAndIsr发送之前的请求都将无法正常完成。而前面说过,这些被暂存在Purgatory中不断重试,知道最终请求超时返回给客户端。但是,设想一下,如果Kafka能够优先处理LeaderAndIsr请求,Broker0就会立刻跑出NOT_LEADER_FOR_PARTITION异常,快速标识这些积压的请求已失效,这样客户端就不用等到Purgatory中请求超时就立刻能感知到,从而降低了请求处理时间。

另外,即使acks不是all,积压的PRODUCE请求能成功写入Leader副本日志,但处理完LeaderAndIsr请求后,Broker0上的Leader变为了Follower副本,也要执行显示的日志截断,即原Leader副本称为Follower后,会将之前写入未提交的消息全部删除,所以之前的写入依然是无用功。

再举个例子,同样是在积压大量数据请求的Broker上,当你删除主题的时候,Kafka控制器向该Broker发送StopReplica请求。如果请求不能立即处理,主题删除操作会一直hang住,增加了删除主题延时。

基于这些问题,社区于2.3版本正式实现了数据类请求和控制类请求分离。具体是怎么做的呢?回看上面第三张图,Kafka Broker启动后,会在后台创建两套网络线程池和IO线程池,一套处理处理数据类请求,另一套控制类请求。至于所用的Socket端口,需要提供不同的listeners配置,显示指定哪套端口用于处理哪类请求。

标注:这个系列文章是本人在极客时间专栏---kafka核心技术与实战中的学习笔记

    https://time.geekbang.org/column/article/101171

发布了37 篇原创文章 · 获赞 20 · 访问量 4952

猜你喜欢

转载自blog.csdn.net/qq_24436765/article/details/102599905