MQ - 03 基础篇_网络模块
  jt6CnuRn5coj 2023年11月02日 48 0

@[toc]

MQ - 03 基础篇_网络模块_Java


导图

MQ - 03 基础篇_网络模块_Java_02


概述

对消息队列来说,网络模块是核心组件之一,网络模块的性能很大程度上决定了消息传输的能力和整体性能。

如果你是 Java 技术栈的开发人员,讲到网络模块的开发,大概率第一反应就是 Netty。Netty 作为 Java 网络编程中最出名的类库,几乎主宰了 Java 的网络编程。

那消息队列网络模块的选型,是不是直接用 Netty 就可以了呢?

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

接下来我们就围绕这三点来思考消息队列网络模块应该怎样设计。

首先我们先来分析一下网络模块的性能瓶颈可能在哪里。


网络模块的性能瓶颈分析

我们基于最基础的消息队列访问链路图分析。

MQ - 03 基础篇_网络模块_Java_03

单个请求

对于单个请求来说,请求流程是:客户端(生产者 / 消费者)构建请求后,向服务端发送请求包 -> 服务端接收包后,将包交给业务线程处理 -> 业务线程处理完成后,将结果返回给客户端。

其中可能消耗性能的有三个点。

  • 编解码的速度MQ - 02 基础篇_通讯协议
  • 网络延迟。也就是客户端到服务端的网络延迟,这一点在软件层面几乎无法优化,取决于网络链路的性能,跟网络模块无关。
  • 服务端 / 客户端网络模块的处理速度。发送 / 接收请求包后,包是否能及时被处理,比如当逻辑线程处理完成后,网络模块是否及时回包。这一点属于性能优化,是网络模块设计的核心工作,我们后续会细讲。

并发请求

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

主要包含三个方面。

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

大流量场景,某种意义上是高并发处理的一种子场景。

因为大流量分为

  • 单个请求包大并发小、
  • 单个请求包小并发大两种场景。

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

第二种场景是我们需要主要解决的。


知道了瓶颈在哪里,接下来我们来具体看一下如何设计出一个高性能的网络模块。


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

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


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

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


1. 单条 TCP 连接的复用 (不考虑)

这是在一条真实的 TCP 连接中,创建信道(channel,可以理解为虚拟连接)的概念。

通过编程手段,我们把信道当做一条 TCP 连接使用,做到 TCP 连接的复用,避免创建大量 TCP 连接导致系统资源消耗过多。

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

MQ - 03 基础篇_网络模块_客户端_04


2. IO 多路复用技术

主流的消息队列 Kakfa、RocketMQ、Pulsar 的网络模块都是基于 IO 多路复用的思路开发的。

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

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

MQ - 03 基础篇_网络模块_客户端_05

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

  • 第一个上限是操作系统的 FD 上限,如果连接数超过了 FD 的数量,连接会创建失败。
  • 第二个限制是系统资源的限制,主要是 CPU 和内存。频繁创建、删除或者创建过多连接会消耗大量的物理资源,导致系统负载过高。

所以你会发现,每个消息队列的配置中都会提到连接数的限制和系统 FD 上限调整。

Linux 中可以通过命令查看系统的 FD 信息。

//查看能打开FD的数量 
ulimit -n //用户级限制
cat /proc/sys/fs/file-max  //系统级限制
//临时修改最大数量 
ulimit -n 100000 //将最大值改为100000

解决了第一个问题连接处理,我们看第二个问题:如何快速处理高并发请求。


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

单个请求的处理

先看单个请求的处理。

我们知道,两点之间直线最短。对于单个请求来说,最快的处理方式就是客户端直接发出请求,服务端接收到包后,直接丢给后面的业务线程处理,当业务线程处理成功后,直接返回给客户端。

MQ - 03 基础篇_网络模块_客户端_06

这种处理模式是最快的,但是这里有两个问题需要解决。

  • 如何第一时间拿到包交给后端的业务逻辑处理?
  • 当业务逻辑处理完成后,如何立即拿到返回值返回给客户端?

我们最直观的思路就是阻塞等待模型,不断轮询等待请求拿到包,业务逻辑处理完,直接返回结果给客户端。这种处理是最快的。但是阻塞等待模型因为是串行的处理机制,每个请求需要等待上一个请求处理完才能处理,处理效率会很低。

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


高并发请求的处理 ----> Reactor 模型

再看高并发请求的情况。

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

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

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

MQ - 03 基础篇_网络模块_消息队列_07

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

Reactor 模型的三种实现模式

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

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

我们具体分析一下,看消息队列更适合哪一种。

单 Reactor 单线程

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

MQ - 03 基础篇_网络模块_Java_08

优点是所有处理逻辑放在单线程中实现,没有上下文切换、线程竞争、进程通信等问题。

缺点是在性能与可靠性方面存在比较严重的问题。

  • 性能上,因为是单线程处理,无法充分利用 CPU 资源,并且业务逻辑 Handler 的处理是同步的,容易造成阻塞,出现性能瓶颈。
  • 可靠性主要是因为单 Reactor 是单线程的,如果出现异常不能处理请求,会导致整个系统通信模块不可用。

所以单 Reactor 单进程模型不适用于计算密集型的场景,只适用于业务处理非常快速的场景。


单 Reactor 多线程

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

MQ - 03 基础篇_网络模块_Java_09

优点是 Handler 收到响应后通过 send 把响应结果返回给客户端,降低 Reactor 的性能开销,提升整个应用的吞吐。

而且 Handler 使用多线程模式,可以充分利用 CPU 的性能,提高了业务逻辑的处理速度。

缺点是 Handler 使用多线程模式,带来了多线程竞争资源的开销,同时涉及共享数据的互斥和保护机制,实现比较复杂。

另外,单个 Reactor 承担所有事件的监听、分发和响应,对于高并发场景,容易造成性能瓶颈。


多 Reactor 多线程 【主从 Reactor 多线程模型】

在此基础上,主从 Reactor 多线程模型,是让 Reactor 也变为了多线程。

当前业界消息队列的网络模型,比如 Pulsar、Kafka、RocketMQ,为了保证性能,都是基于主从 Reactor 多线程模型开发的。

这种方案,优点是 Reactor 的主线程和子线程分工明确。

主线程只负责接收新连接,子线程负责完成后续的业务处理。同时主线程和子线程的交互也很简单,子线程接收主线程的连接后,只管业务处理即可,无须关注主线程,可以直接在子线程把处理结果返回给客户端。

所以,主从 Reactor 多线程模型适用于高并发场景,Netty 网络通信框架也采用了这种实现。

缺点是如果基于 NIO 从零开始开发,开发的复杂度和成本较高。另外,Acceptor 是一个单线程,如果挂了,如何处理客户端新连接是一个风险点。

为了解决 Acceptor 的单点问题,有些组件为了保证高可用性,会对主从 Reactor 多线程做一些优化,把 Acceptor 也变为多线程的形态。

MQ - 03 基础篇_网络模块_消息队列_10


说到这里,基于 IO 多路复用技术和 Reactor 模型,我们已经可以解决网络模块的性能问题了。接下来我们来看如何提高网络模块的稳定性和降低开发成本


基于成熟网络框架提高稳定性并降低开发成本 ----->Netty

这里的“稳定性”主要指代码的稳定性。因为网络模块的特点是编码非常复杂,要考虑的细节和边界条件非常多,一些异常情况的处理也很细节,需要经过长时间的打磨。但是一旦开发完成,稳定后,代码几乎不需要再改动,因为需求是相对固定的。

在 Java 中,网络编程的核心是一个基础的类库——Java NIO 库,它的底层是基于 Linux/Unix IO 复用模型 Epoll 实现的。

如果我们要基于 Java NIO 库开发一个 Server,需要处理网络的闪断、客户端的重复接入、连接管理、安全认证、编解码、心跳保持、半包读写、异常处理等等细节,工作量非常大。

所以在消息队列的网络编程模型中,为了提高稳定性或者降低成本,选择现成的、成熟的 NIO 框架是一个更好的方案。

MQ - 03 基础篇_网络模块_Java_11

而 Netty 就是这样一个基于 Java NIO 封装的成熟框架。

所以我们一提到 Java 的网络编程,最先想到的就是 Netty。

当前业界

  • 主流消息队列 RocketMQ、Pulsar 也都是基于 Netty 开发的网络模块,
  • Kafka 因为历史原因是基于 Java NIO 实现的。

接下来我们以 RocketMQ 和 Kafka 的网络模型为例,来分析一下主流消息队列的网络模型的设计实现。


主流消息队列的网络模型实现

Kafka 网络模型

Kafka 的网络层没有用 Netty 作为底层的通信库,而是直接采用 Java NIO 实现网络通信。在网络模型中,也是参照 Reactor 多线程模型,采用多线程、多 Selector 的设计。

看整个网络层的结构图。

Processor 线程和 Handler 线程之间通过 RequestChannel 传递数据,RequestChannel 中包含一个 RequestQueue 队列和多个 ResponseQueues 队列。

每个 Processor 线程对应一个 ResponseQueue。

MQ - 03 基础篇_网络模块_Java_12

具体流程上:

  • 一个 Acceptor 接收客户端建立连接的请求,创建 Socket 连接并分配给 Processor 处理。
  • Processor 线程把读取到的请求存入 RequestQueue 中,Handler 线程从 RequestQueue 队列中取出请求进行处理。
  • Handler 线程处理请求产生的响应,会存放到 Processor 对应的 ResponseQueue 中,Processor 线程从其对应的 ResponseQueue 中取出响应信息,并返回给客户端。

RocketMQ 网络模型

RocketMQ 采用 Netty 组件作为底层通信库,遵循 Reactor 多线程模型,同时又在 Reactor 模型上做了一些扩展和优化。

所以它的网络模型是 Netty 的网络模型,Netty 底层采用的是主从 Reactor 多线程模型,模型的原理逻辑跟前面讲到的主从 Reactor 多线程模型是一样的。

MQ - 03 基础篇_网络模块_消息队列_13

在主从 Reactor 多线程模型的理论基础上,我们来分析一下 RocketMQ 中 NettyRemotingServer 的具体实现形式。

具体流程上:

  • 一个 Reactor 主线程负责监听 TCP 网络连接请求,建立好连接,创建 SocketChannel,并注册到 Selector 上。RocketMQ 的源码中会自动根据 OS 的类型选择 NIO 和 Epoll,也可以通过参数配置,监听真正的网络数据。
  • 接收到网络数据后,会把数据传递给 Reactor 线程池处理。
  • 真正执行业务逻辑之前,会进行 SSL 验证、编解码、空闲检查、网络连接管理,这些工作在 Worker 线程池处理(defaultEventExecutorGroup)。
  • 处理业务操作,放在业务 Processor 线程池中执行。

从 Kafka 和 RocketMQ 的网络模型的实现来看,网络模块既可以基于原生的 Java NIO,也可以基于 NIO 的框架(如 Netty)来完成开发,不过基本思想都是基于 IO 多路复用技术和 Reactor 模型来提高处理性能、完成具体的编码实现。

但是到这里还没有结束,NIO 编程属于 TCP 层网络编程,我们还需要进行协议设计、编解码、链路的建立 / 关闭等工作,才算完成一个完整的网络模块的开发。有没有更好的方案可以解决这些问题,减少我们的工作量呢?


NIO 编程和 RPC 框架

要想不关心底层的调用细节(如底层的网络协议和传输协议等),我们可以调用远端机器上的函数或方法来实现,也就是 RPC(Remote Procedure Call)远程过程调用。

MQ - 03 基础篇_网络模块_Java_14

因为 RPC 调用的是一个远端对象,调用者和被调用者处于不同的节点上,想完成调用,必须实现 4 个能力。

  • 网络传输协议:远端调用底层需要经过网络传输,所以需要选择网络通信协议,比如 TCP。
  • 应用通信协议:网络传输需要设计好应用层的通信协议,比如 HTTP2 或自定义协议。
  • 服务发现:调用的是远端对象,需要可以定位到调用的服务器地址以及调用的具体方法。
  • 序列化和反序列化:网络传输的是二进制数据,因此 RPC 框架需要自带序列化和反序列化的能力。

那 RPC 框架作为消息队列中的网络模块会有哪些优缺点呢?

我们以 gRPC 框架举例分析。gRPC 是 Google 推出的一个 RPC 框架,可以说是 RPC 框架中的典型代表。

主要有以下三个优点:

  • gRPC 内核已经很好地实现了服务发现、连接管理、编解码器等公共部分,我们可以把开发精力集中在消息队列本身,不需要在网络模块消耗太多精力。
  • gRPC 几乎支持所有主流编程语言,开发各个消息队列的 SDK 可以节省很多开发成本。
  • 很多云原生系统,比如 Service Mesh 都集成了 gRPC 协议,基于 HTTP2 的 gRPC 的消息队列很容易被云原生系统中的其他组件所访问,组件间的集成成本很低。

但是当前主流的消息队列都不支持 gRPC 框架,这是因为如果支持就要做很大的架构改动。而且,gRPC 底层默认是七层的 HTTP2 协议,在性能上,可能比直接基于 TCP 协议实现的方式差一些。但是 HTTP2 本身在性能上做了一些优化,从实际表现来看,性能损耗在大部分场景下是可以接受的。

所以如果是一个新设计的消息队列或者消息队列的新架构,通过成熟的 RPC 框架来实现网络模块是一个蛮不错的方案。比如 RocketMQ 5.0 中的 Proxy 就使用 gRPC 框架实现了网络模块。


小结

消息队列的网络模块主要解决的是性能、稳定性、成本三个方面的问题。

  • 性能问题,核心是通过 Reactor 模型、IO 多路复用技术解决的。Reactor 模式在 Java 网络编程中用得非常广泛,比如 Netty 就实现了 Reactor 多线程模型。即使不用 Netty 进行网络编程(比如 Kafka 直接基于 Java NIO 编程)的情况下,网络模块也大多是参考或基于 Reactor 模式实现的。因为 Reactor 模式可以结合多路复用、异步调用、多线程等技术解决高并发、大流量场景下的网络模块的性能问题。
  • 在 Java 技术栈下,网络编程的核心是 Java NIO。但为了解决稳定性和开发成本的问题,建议选择业界成熟的网络框架来实现网络模块,而不是基于原生的 Java NIO 来实现。成熟的框架分为成熟的 NIO 框架(如 Netty)和成熟的 RPC 框架(如 gRPC)

目前业界主流的消息队列都是基于 Java NIO 和 Netty 实现的。Netty 是我们网络模块编程的常用选型,大部分情况下,可能还是我们的最终选择。

但是 Netty 好用并不意味着所有的 Java 网络编程都必须选择 Java NIO 和 Netty。

当你需要构建一个组件的网络模块的时候,你要先知道这个组件的业务特点是什么,需要解决哪些问题,再来考虑使用什么技术。比如在客户端连接数不多、并发不高,流量也很小的场景,只需要一个简单的网络 Server 就够了,完全没必要选择 Java NIO 或 Netty 来实现你的网络模块。

随着技术架构的迭代,基于 RPC 框架的方案也是一个不错的选择。

MQ - 03 基础篇_网络模块_客户端_15

【版权声明】本文内容来自摩杜云社区用户原创、第三方投稿、转载,内容版权归原作者所有。本网站的目的在于传递更多信息,不拥有版权,亦不承担相应法律责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@moduyun.com

  1. 分享:
最后一次编辑于 2023年11月08日 0

暂无评论

jt6CnuRn5coj