相信使用过 Kafka 的朋友都知道其吞吐量可以高达百万,但很少人理解其中的设计原理。认真读完这篇文章,你会对Kafka Broker请求处理流程和网络架构设计实现细节,有更加深刻的理解。
开篇三问: Kafka Broker 端网络架构和请求处理到底是使用了哪些高大上的技术?它到底解决了什么问题?究竟是怎么解决的?
只有了解了这些, 我们才能深刻掌握 Kafka 服务端设计精髓所在,更加深刻理解一个高并发、高性能服务端架构该如何设计。
相信使用过 Kafka 的朋友都知道其吞吐量可以高达百万,但很少人理解其中的设计原理。认真读完这篇文章,你会对Kafka Broker请求处理流程和网络架构设计实现细节,有更加深刻的理解。
这篇文章干货很多,希望大家可以耐心读完。

01 总体概述
要想理解 Kafka Broker 请求处理架构设计,我们需要从简单请求处理模型来说起。
对于日常系统开发,我们都知道是基于 Request/Response 的模式来实现的, 对于 Kafka 来说, 无论是 Producer 端、Consumer 端 还是 Broker 端,他们之间的请求交互也都是基于「Request/Response」模式来完成的。比如,客户端会通过网络发送消息生产请求给 Broker,而 Broker 处理完成后,会发送对应的响应给到客户端。
下面,我会从自我设计角度出发,如果是我们会如何设计,带你一步步演化出来「kafka Broker 的网络请求处理」架构。
在这个过程中,你会看到 Kafka 在处理请求的过程中会遇到哪些高性能和高并发问题,以及架构为什么要这样演进,从而理解 Kafka 这么设计的意义和精妙之处。
02 顺序处理模式
我们从最简单的网络编程思路处理方式讲起。
因为对于 Kafka Broker 来说就是用来接收生产者发送过来的请求,那这个时候最简单的实现大概是这样的:

如上述代码所示:我们可以理解 Kafka 每个服务器启动起来后就是一个 while 循环, 不断的 accept 生产者提交上来的请求, 然后进行处理并存储到磁盘上,这种方式实现最简单,也非常好理解,但是这种方式存在2个致命的缺陷?
1)请求阻塞:只能顺序处理每个请求,即每个请求都必须等待前一个请求处理完毕才能得到处理。
2)吞吐量非常差:由于只能顺序处理,无法并发,效率太低,所以吞吐量非常差,只适合请求发送非常不频繁的系统。
从上面来看很明显,如果你的 Kafka 系统请求并发量很大,意味着要处理的时间就会越久。那按照前面我们提到的 Kafka「吞吐量」的标准,这个方案远远无法满足我们对高性能、高并发的要求。
那有什么更好的方案可以快速处理请求吗?
接下来我们可以试着采取这个方案:独立线程异步处理模式。
03 多线程异步处理模式
既然同步方式会阻塞请求,吞吐量差, 我们可以尝试着使用独立线程异步方式进行处理, 即经典的 connection per thread 模型, 那这个时候的实现大概是这样的:

如上述代码所示:同上还是一个 while 循环不断的 accept 生产者提交上来的请求,但是这时候 Kafka 系统会为每个请求都创建一个「单独的线程」来处理。
这个实现方案的好处就是:
1)吞吐量稍强:相对上面同步方式的方案,一定程度上极大地提高了服务器的吞吐量。
2)非阻塞:它是完全异步的,每个请求的处理都不会阻塞下一个请求。
但同样缺陷也同样很明显:即为每个请求都创建线程的做法开销很大,在某些高并发场景下会压垮整个服务。可见,这个方案也只适用于请求发送频率很低的业务场景。还是无法满足我们对高性能、高并发的要求。
既然这种方案还是不能满足, 那么我们究竟该使用什么方案来支撑高并发呢?
这个时候我们可以想想我们日常开发用到的7层负载Nginx或者Redis在处理高并发请求的时候是使用什么方案呢?
从上面启发你可以看出,提升系统 I/O 并发性能的关键思路就是:事件驱动。
想必大家已经猜到了,没错,就是「多路复用」。那么Kafka 是不是也是采用这种方案来实现呢?
这里我们先考虑采用基于「事件驱动」的设计方案,当有事件触发时,才会调用处理器进行数据处理。
04 Reactor 模式
在高性能网络编程领域,有一个非常著名的模式——Reactor模式。那么何为「Reactor模式」,首先它是基于事件驱动的,有一个或多个并发输入源,有一个Service Handler,有多个Request Handler;这个Service Handler会同步的将输入的请求轮询地分发给相应的Request Handler进行处理。
借助于 Doug Lea(就是那位让人无限景仰的大爷)的 "Scalable IO in Java" 中讲述的Reactor模式。
简单来说,Reactor 模式特别适合应用于处理多个客户端并发向服务器端发送请求的场景。这里借用大神 PDF 中的一幅图来说明 Reactor 架构:

从上面这张图中,我们可以看出多个客户端会发送请求给到 Reactor。Reactor 有个请求分发线程 Dispatcher,也就是图中的绿色的 Acceptor,它会将不同的请求下分发到多个工作线程中处理。
在这个架构中,Acceptor 线程只是用来进行请求分发,所以非常轻量级,因此会有很高的吞吐量。而这些工作线程可以根据实际系统负载情况动态调节系统负载能力,从而达到请求处理的平衡性。
基于上面的 Reactor 架构, 我们来看看如果是我们该如何设计 Kafka 服务端的架构?

1)这里我们采用多路复用方案,Reactor 设计模式,并引用 Java NIO 的方式可以更好的解决上面并发请求问题。
2)当 Client 端将请求发送到 Server 端的时候, 首先在 Server 端有个多路复用器(Selector),然后会启动一个 Accepter 线程将 OP_CONNECT 事件注册到多路复用器上, 主要用来监听连接事件到来。
3)当监听到连接事件后,就会在多路复用器上注册 OP_READ 事件, 这样 Cient 端发送过来的请求, 都会被接收到。如果请求特别多的话, 我们这里进行优化, 创建一个 Read HandlePool 线程池。
4)当 Read HandlePool 线程池接收到请求数据后,最终会交给 Handler ThreadPool 线程池进行后续处理。比如如果是生产者发送过来的请求,肯定会解析请求体,处理并最终存储到磁盘中,待处理完后要返回处理结果状态, 这时候就由它在多路复用器上注册 OP_WRITE 事件来完成。这样多路复用器遍历到OP_WRITE 事件后就会将请求返回到 Client 端。
5)在上图中我们看到在整个流程中还有一个 MessageQueue 的队列组件存在, 为什么要加这个组件呢? 我们可以想象一下, 如果请求量非常大,直接交给 Handler ThreadPool 线程池进行处理, 可能会出现该线程池处理不过来的情况发生,如果处理不过来,也会出现阻塞瓶颈。所以这里我们在 Server 端内部也设计一个消息队列, 起到一个缓冲的作用,Handler ThreadPool 线程池会根据自己的负载能力进行处理。
以上就是我们引用了「多路复用」的设计方案,但是 Kafka Broker 端就是这样的架构设计方案吗?如果我们是 Kafka 系统架构的设计者,采用这样的架构设计方案会不会还是有什么问题,有没有哪个环节会出现系统性能瓶颈呢?
这是个值得思考的问题, 很考验你的架构设计能力。
细心的读者可能会发现:对于 Kafka 这种超高并发系统来说,一个 Selector 多路复用器是 Hold 不住的,从上图可以得出,我们监听这些连接、接收请求、处理响应结果都是同一个 Selector 在进行处理,很容易成为系统性能瓶颈。
接下来,我们将进一步进行优化,为了减轻当前 Selector 的处理负担,引入另外一个Selector 处理队列,如下图所示:


最低0.47元/天 解锁文章
904

被折叠的 条评论
为什么被折叠?



