消息队列是需要满足高吞吐、高可靠、低延时,并支持多语言访问的基础软件,网络模块最需要解决的是性能稳定性、开发成本三个问题。

网络模块的性能瓶颈分析

消息队列的访问链路图如下:

image-20230717205443205

对于单个请求来说,请求流程是:客户端构建请求,发送给服务端,服务端收到后交由业务线程处理,业务线程处理完后返回给客户端。

该流程性能消耗有三个点:

  • 编解码的速度。即序列化与反序列化的速度。
  • 网络延迟。这点几乎无法优化,与网络传输有关。
  • 服务端 / 客户端网络模块的处理速度。发送 / 接收请求包后,包是否能及时被处理。

对于并发请求来说,在单个请求维度的问题的基础上,还需要处理高并发、高 QPS、高流量等场景带来的性能问题。主要包含三个方面。

  • 高效的连接管理:当客户端和服务端之间的 TCP 连接数很多,如何高效处理、管理连接。
  • 快速处理高并发请求:当客户端和服务端之间的 QPS 很高,如何快速处理(接收、返回)请求。
  • 大流量场景:当客户端和服务端之间的流量很高,如何快速吞吐(读、写)数据。

大流量场景分为两类,单个请求包大,但是并发小,单个请求包小,但是并发大。

第一种的瓶颈主要在于数据拷贝、垃圾回收、CPU 占用等方面,主要依赖语言层面的编码技巧来解决。

我们这里主要看第二种。

高性能网络模块的设计实现

从技术上来看,高性能网络模块的设计可以分为如何高效管理大量的 TCP 连接、如何快速处理高并发的请求、如何提高稳定性和降低开发成本等三个方面。

基于多路复用技术管理 TCP 连接

从技术原理来看,高效处理大量 TCP 连接,在消息队列中主要有单条 TCP 连接的复用多路复用两种技术思路。

1. 单条 TCP 连接的复用

如下图,在一条真实的 TCP 连接中,创建信道(channel,可以理解为虚拟连接)的概念。通过编程手段,我们把信道当做一条 TCP 连接使用,做到 TCP 连接的复用,避免创建大量 TCP 连接导致系统资源消耗过多。

缺点是在协议设计和编码实现的时候有额外开发工作量,而且近年随着异步 IO、IO 多路复用技术的发展,这种方案有点多余。

image-20230717210315458

2. IO 多路复用技术

IO 多路复用技术,是指通过把多个 IO 的阻塞复用到同一个 selector 的阻塞上,让系统在单线程的情况下可以同时处理多个客户端请求。最大的优势是系统开销小,系统不需要创建额外的进程或者线程,降低了维护的工作量,也节省了资源。

目前支持 IO 多路复用的系统调用有 Select、Poll、Epoll 等,Java NIO 库底层就是基于 Epoll 机制实现的。

image-20230717210436640

即使用了这两种技术,单机能处理的连接数还是有上限的

第一个上限是操作系统的 FD 上限,如果连接数超过了 FD 的数量,连接会创建失败。

第二个限制是系统资源的限制,主要是 CPU 和内存。频繁创建、删除或者创建过多连接会消耗大量的物理资源,导致系统负载过高。

基于 Reactor 模型处理高并发请求

对于单个请求来说,最快的处理方式就是客户端直接发出请求,服务端接收到包后,直接丢给后面的业务线程处理,当业务线程处理成功后,直接返回给客户端。但存在以下两个问题:

  • 如何第一时间拿到包交给后端的业务逻辑处理?

  • 当业务逻辑处理完成后,如何立即拿到返回值返回给客户端?

最直观的思路是阻塞等待模型,不断轮询等待请求拿到包,业务逻辑处理完后返回给客户端。但是阻塞等待模型是穿行机制,下一个请求需要等到上一个请求处理完才能处理,效率低。

所以,单个请求,最合理的方式就是异步的事件驱动模型,可以通过 Epoll 和异步编程来解决。

在高并发的情况下会有很多连接、请求需要处理,核心思路就是并行、多线程处理,需要用到Reactor模型。

Reactor 模型是一种处理并发服务请求的事件设计模式,当主流程收到请求后,通过多路分离处理的方式,把请求分发给相应的请求处理器处理。

如下图所示,Reactor 模式包含 Reactor、Acceptor、Handler 三个角色。

image-20230717211239869
  • Reactor:负责监听和分配事件。收到事件后分派给对应的 Handler 处理,事件包括连接建立就绪、读就绪、写就绪等。
  • Acceptor:负责处理客户端新连接。Reactor 接收到客户端的连接事件后,会转发给 Acceptor,Acceptor 接收客户端的连接,然后创建对应的 Handler,并向 Reactor 注册此 Handler。
  • Handler:请求处理器,负责业务逻辑的处理,即业务处理线程。

从技术上看,Reactor 模型一般有三种实现模式。

  • 单 Reactor 单线程模型(单 Reactor 单线程)
  • 单 Reactor 多线程模型 (单 Reactor 多线程)
  • 主从 Reactor 多线程模型 (多 Reactor 多线程)

1、单 Reactor 单线程模型

特点是 Reactor 和 Handler 都是单线程的串行处理。

image-20230717212026725

优点是所有处理逻辑放在单线程中实现,没有上下文切换、线程竞争、进程通信等问题。缺点是在性能与可靠性方面存在比较严重的问题。

性能上,因为是单线程处理,无法充分利用 CPU 资源,并且业务逻辑 Handler 的处理是同步的,容易造成阻塞,出现性能瓶颈。所以单 Reactor 单进程模型不适用于计算密集型的场景,只适用于业务处理非常快速的场景

2、单 Reactor 多线程模型

业务逻辑处理 Handler 变成了多线程,也就是说,获取到 IO 读写事件之后,业务逻辑是一批线程在处理。

image-20230717212143234

优点是 Handler 收到响应后通过 send 把响应结果返回给客户端,降低 Reactor 的性能开销,提升整个应用的吞吐。而且 Handler 使用多线程模式,可以充分利用 CPU 的性能,提高了业务逻辑的处理速度。

缺点是 Handler 使用多线程模式,带来了多线程竞争资源的开销,同时涉及共享数据的互斥和保护机制,实现比较复杂。另外,单个 Reactor 承担所有事件的监听、分发和响应,对于高并发场景,容易造成性能瓶颈。

3、主从 Reactor 多线程模型

该模型让 Reactor 也变为了多线程。

image-20230717212515159

这种方案,优点是 Reactor 的主线程和子线程分工明确,主线程只负责接收新连接,子线程负责完成后续的业务处理。主线程接收到主线程的连接后,只需要交由后续业务处理即可,不需要关注主线程,可以直接在子线程把处理结果返回给客户端。

缺点是Acceptor 是一个单线程,如果挂了,如何处理客户端新连接是一个风险点。

优化后,将Acceptor改为多线程

image-20230717212849187

参考

《深入拆解消息队列 47 讲》