上篇文章我们讨论了生产端的高吞吐设计,这节课我们讨论服务端的高吞吐设计。
服务端针对高吞吐有几个设计特点:
- 网络层的 Reactor 设计模式、
- 顺序写、
- 页缓存
- 零拷贝。
接下来我会按照顺序分别为你详细讲解。
网络层的 Reactor 设计模式
网络层的设计图如下:
这里我就结合该图解释一下网络层的架构设计。
整个服务端的网络架构分为 4 个层次:
- Acceptor 线程构成的连接创建层,负责创建和客户端的连接;
- Processor 线程类构成的网络事件处理层;
- 由 RequestChannel 构成的请求和响应的缓冲层;
- 由 KafkaRequestHandler 和 KafkaApis 构成的真正的业务处理层。
这样的设计有什么优势呢?
第一,我们先思考为什么要把 Acceptor 线程和 Processor 线程分开。
如果不分开,网络读写的量很大势必造成大量线程阻塞,导致服务端对 OP_ACCEPT 事件响应不及时,进而连接失败。同时,如果服务端刚启动瞬时来了很多连接,大量的线程都去建立新的连接了,那么网络读写事件的处理就会慢下来,也会引起读写超时等问题。
Acceptor 线程和 Processor 线程分为两层这样的设计让连接的创建和网络读写事件的处理分开,同时还可以配置 Processor 线程的数量,这样做不会被极端场景影响到整体的响应时间,同时也是符合 Reactor 设计模式的。(Reactor 模式又被称为反应器模式或应答者模式,是基于事件驱动的设计模式,如有需要你可以查阅相关的资料来学习,这里就不过多赘述了。)
第二,Processor 线程解析完请求后并不是直接交给业务线程处理。
解析完请求后放入 RequestChannel 的请求队列里,这样做避免在高并发场景下业务线程(即调用底层组件的线程)工作过于饱和而造成超时的情况出现。
第三,KafkaRequestHandlerPool 线程池先消费 RequestChannel 类里的请求队列,然后通过调用 KafkaApis 实现对底层组件的调用。
这样做既可以实现网络处理和调用底层组件的解耦,也可以根据实际请求,随时调整 KafkaRequestHandlerPool 线程池的线程数,调整调用底层组件的能力。
第四,KafkaApis 类会把响应放入对应的 Processor 线程里的响应集合里。
而不是直接让 Processor 把响应发送给客户端,这样做实现了业务线程和网络操作线程的解耦,避免了高并发时线程工作过于饱和而造成的延迟问题。
顺序写
Kafka 写日志文件的时候用的是追加消息的形式,只在文件尾部顺序写消息,同时在文件头部顺序读取消息。消息队列不涉及修改消息,所以不需要随机写。这样的设计即使用的是传统的磁盘,吞吐量也会很大。主要原因是操作系统对于顺序写和顺序读有优化,具体采用的是后写(对于写消息优化)和预读(对于读消息优化)。生产环境上经过测试,顺序写比随机写快 3 个数量级。
页缓存
页缓存简单说就是把缓存当磁盘用,这样就避免了频繁地读写磁盘。
当一个进程要读取或写入磁盘文件的时候,系统会判断数据是否在内存中,如果在,就直接把内存中的数据返回给进程;如果不在,就读取磁盘文件,同时会多读一些连续的磁盘页放到内存中。这样下次再读取或写入时,系统会判断数据是否在内存中,只要是顺序地读写消息,命中率会很高的,大大减少了磁盘访问的次数,提高了服务端的吞吐量。
零拷贝
这里我们以消费者读取消息为例,服务端要从磁盘拷贝数据然后网络发送,如果不采用零拷贝的话,会发生什么样的事情呢?如下图所示:
非零拷贝过程图
首先,应用程序调用 read() 方法时需要从用户态切换到内核态,将数据从磁盘上取出来保存到内核缓冲区中;然后,内核缓冲区中的数据传输到应用程序,此时 read() 方法调用结束,从内核态切换到用户态。之后,应用程序执行 send() 方法,需要从用户态切换到内核态,将数据传输给 Socket Buffer;最后,内核会将 Socket Buffer 中的数据发送到网卡,再发送到远程节点,此时 send() 方法结束,从内核态切换到用户态。
可以看到,这个过程共涉及四次 CPU 上下文切换和四次数据复制,并且有两次复制操作是由 CPU 完成的,另外两次由 DMA 完成。在这个过程中,数据本身没有任何修改,仅仅是从磁盘复制到了网卡缓冲区中,于是会浪费大量的 CPU 周期。
那采用零拷贝又会发生什么呢?如下过程图:
零拷贝过程图
首先,应用程序调用 transferTo() 方法,从用户态转换为内核态,DMA 会将文件数据发送到内核缓冲区;然后,Socket Buffer 追加数据的描述信息;最后,DMA 将内核缓冲区的数据发送到网卡缓冲区,这样就完全解放了 CPU,实现了零拷贝。
也就是说,所谓“零拷贝”是 CPU 不参与拷贝数据的工作,可以节省大量的 CPU 周期,同时减少了两次 CPU 在用户态和内核态的切换。这样大大减少了 CPU 的负载,从而提升了吞吐量。
如果想详细学习请看 掘金小册《Kafka 源码精讲》 你将获得:
- 全面学 Kafka 各个组件,系统掌握 Kafka 的原理;
- 系统学习 Kafak 的轮子组件,如内存缓冲池,基于 NIO 的通信模块;
- Kafka 高可靠分布式设计;
- Kafka 底层文件存储设计;