Day683.AprEndpoint组件:Tomcat APR提高I/O性能的秘密 -深入拆解 Tomcat & Jetty

      最后更新:2022-07-21 09:12:31 手机定位技术交流文章

      AprEndpoint组件:汤姆卡特APR改进的IT/O性能的秘诀

      Hi,阿昌来也今天的课是关于目标的Tomcat APR:提高I/O性能的秘诀,也就是AprEndpoint组件的实现。

      当使用Tomcat时,您将在启动日志中看到下列提示信息:

      The APR based Apache Tomcat Native library which allows optimal performance in production environments was not found on the java.library.path: ***

      这句话的意思就是推荐你去安装 APR 库,可以提高系统性能。那什么是APR呢?

      APR(Apache Portable Runtime Libraries)是 Apache 可移植运行时库,它是用C 语言实现它旨在为高级应用程序提供一个全平台的操作系统接口库。Tomcat可以用于处理文件和网络接口,从而提高性能。

      Tomcat 支持的连接器有 NIO、NIO.2 和 APR。跟 NioEndpoint 一样,AprEndpoint 也实现了非阻塞 I/O,它们的区别是:

      • NioEndpoint使用JavaNIO API实现非阻塞的输入/输出
      • AprEndpoint已通过JNI呼吁APR本地图书馆并且认识到非阻塞的输入/输出。

      那同样是非阻塞 I/O,为什么 Tomcat 会提示使用 APR 本地库的性能会更好呢?

      这是因为在某些场景下,比如需要频繁与操作系统进行交互,Socket 网络通信就是这样一个场景,特别是如果你的 Web 应用使用了 TLS 来加密传输,TLS 协议在握手过程中有许多网络交互,在这个例子中,Java和C编程之间仍然存在一定的差距,这就是APR的实力。

      Tomcat 本身是 Java 编写的,为了调用 C 语言编写的 APR,需要通过 JNI 方式来调用。

      JNI(Java Native Interface)是JDK提供的编程接口。它允许Java程序调用其他语言编写的程序或代码库,事实上,JDK本身的实现使用大量的JNI技术来调用本地C库。

      1.AprEndpoint工作过程

      AprEndpoint工作流程

      在这里插入图片描述

      它与NioEndpoint图类似,由左到右有 LimitLatch 、 Acceptor 、 Poller 、 SocketProcessor 和 Http11ProcessorAcceptorPoller的实现和 NioEndpoint 不同。

      1、Acceptor

      Accpetor的功能是监视连接,接收和建立连接,其本质是调用四个操作系统API: Socket, Bind, Listen, and Accept。

      Java语言如何直接称呼C语言API?

      答案是通过JNI。 具体来说,有两个步骤:

      先封装一个 Java 类,在里面定义一堆用 native 关键字修饰的方法,像下面这样。

      接着用C 代码实现这些方法,比如Bind 函数就是这样实现的:

      JNI细节, 你可以扩展阅读,获得更多信息和例子.

      我们要注意的是函数名字要符合 JNI 的规范,以及 Java 和 C 语言如何互相传递参数,比如在 C 语言有指针,Java 没有指针的概念,所以在 Java 中用 long 类型来表示指针。

      AprEndpoint 的 Acceptor 组件就是调用了 APR 实现的四个 API。

      2、Poller

      Acceptor 接收到一个新的 Socket 连接后,按照 NioEndpoint 的实现,它会把这个 Socket 交给 Poller 去查询 I/O 事件

      AprEndpoint也做了同样的事情,不过 AprEndpoint 的 Poller 并不是调用 Java NIO 里的 Selector 来查询 Socket 的状态,而是通过 JNI 调用 APR 中的 poll 方法,APR还调用操作系统epoll API来实现它。

      在ApEndpoint这里有一个特殊的位置,我们可以配置一个叫做 deferAccept的参数,它与TCP协议相符TCP_DEFER_ACCEPT,设置这个参数后,当 TCP 客户端有新的连接请求到达时,TCP 服务端先不建立连接,而是再等等,直到客户端有请求数据发过来时再建立连接。

      这样做的优点是,服务器不需要使用选择器来重新询问请求的数据是否准备好。

      这是一种 TCP 协议层的优化,不是每个操作系统内核都支持,因为 Java 作为一种跨平台语言,需要屏蔽各种操作系统的差异,因此并没有把这个参数提供给用户;

      但对于APR,它的目的是最大化性能,因此它暴露了这个参数给用户。

      APR绩效改善的秘诀

      APR 连接器之所以能提高 Tomcat 的性能,除了 APR 本身是 C 程序库之外,还有哪些提速的秘密呢?

      1,JVM堆栈VS本地存储

      Java 的类实例一般在 JVM 堆上分配,而 Java 是通过 JNI 调用 C 代码来实现 Socket 通信的,那么 C 代码在运行过程中需要的内存又是从哪里分配的呢?

      C代码可以直接操作Java堆栈吗?为了回答这些问题,首先我要谈谈JVM与用户进程之间的关系。

      如果你想运行Java类文件,你可以执行下列Java命令。

      这个命令行中的Java实际上是一个可执行的程序,它创建了一个JVM来ロード和运行你的Java类。

      操作系统创建一个执行Java可执行的进程,每个进程都有自己的虚拟地址空间,而JVM使用的内存(包括堆栈、堆栈和方法区域)则由进程的虚拟地址空间分配。

      请你注意的是,JVM内存只是进程空间的一部分,此外,进程空间中还有代码节、数据节、内存映射区、内核空间等。从VM的角度来看,JVM内存以外的部分称为本地内存,在运行C程序代码时使用的内存是本地内存中分配的。

      在这里插入图片描述
      Tomcat的Endpoint组件在接收网络数据时需要预先分配一个好的缓冲器。所谓的 Buffer 就是字节数组byte[],Java 通过 JNI 调用把这块 Buffer 的地址传给 C 代码,C 代码通过操作系统 API 读取 Socket 并把数据填充到这块 Buffer。

      Java NIO API 提供了两种 Buffer 来接收数据:HeapByteBuffer和DirectByteBuffer,下面的代码演示了如何创建两种 Buffer。

      创建好 Buffer 后直接传给 Channel 的 read 或者 write 函数,最终这块 Buffer 会通过 JNI 调用传递给 C 程序。

      HeapByteBuffer和DirectByteBuffer之间有什么区别?

      • HeapByteBuffer对象本身位于JVM 堆上分配,并且它持有的字节数组byte[]也是在 JVM 堆上分配。但是如果你使用HeapByteBuffer来接收网络数据,你需要先从内核复制数据到临时的本地内存,然后从临时本地内存复制到JVM堆栈,不是直接从内核到JVM堆栈。这是为什么呢?这是因为数据被从内核复制到JVM堆栈,JVM可能有GC,在GC过程中可以移动对象,也就是说,可以移动JVM堆栈上的字节数,在这种情况下,缓冲地址将丢失。如果中间通过局部记忆,在从本地内存复制到JVM堆栈的过程中,JVM可以确保没有GC。如果你使用HeapByteBuffer,你会发现,JVM堆栈和内核之间有许多交叉点,DirectByteBuffer被用来解决这个问题
      • DirectByteBuffer对象本身位于JVM堆栈上,但它所持有的块不是从JVM堆栈中分配的,而是从本地内存分配的。DirectByteBuffer 对象中有个 long 类型字段 address,记录本地存储器地址,所以当你收到数据时,直接将这个本地存储地址转移到C程序中,C程序将网络数据从内核复制到这个本地内存,JVM可以直接读取这个本地存储器,这个方法比HeapByteBuffer更少,因此,它通常比HeapByteBuffer快几倍。

      在Tomcat中,ApEndpoint通过DirectByteBuffer接收数据。而 NioEndpoint 和 Nio2Endpoint 是通过 HeapByteBuffer 来接收数据的。

      你可能会问为什么NioEndpoint和Nio2Endpoint都没有使用DirectByteBuffer。

      这是因为本地内存管理不善,泄漏难以定位,并且由于稳定性考虑,NioEndpoint和Nio2Endpoint没有承担这一风险。

      2、sendfile

      让我们考虑另一个网络通信的场景, 即静态文件的处理.

      浏览器通过Tomcat访问HTML文件,Tomcat的处理逻辑由两个步骤组成:

      1. 从磁盘读取HTML到内存.
      2. 通过插座发送此存储器的内容。

      但传统上,记忆有许多副本:

      • 当读取文件时,内核首先读取文件的内容到内核缓冲器。
      • 如果你使用HeapByteBuffer,文件数据从内核到 JVM 堆内存需要经过本地内存中转。
      • 类似地,在将文件内容推入网络时,从JVM堆栈到内核缓冲区的传输需要通过本地内存。
      • 最后,必须从内核缓冲器复制文件到网络卡缓冲器。

      从下面的图你会发现这个过程有 6 次内存拷贝,并且 read 和 write 等系统调用将导致进程从用户态到内核态的切换,会耗费大量的 CPU 和内存资源。

      在这里插入图片描述

      而 Tomcat 的 AprEndpoint 通过操作系统层面的 sendfile 特性解决了这个问题,sendfile 系统调用方式非常简洁。

      它有两个关键参数:插座和文件手柄。

      从磁盘到插座上写文件只有两个步骤:

      • 步骤1:读取文件内容到内核缓冲器。
      • 第二步:数据并没有从内核缓冲区复制到 Socket 关联的缓冲区,只有记录数据位置和长度的描述符被添加到 Socket 缓冲区中;接着把数据直接从内核缓冲区传递给网卡。

      这个过程可以看到下面的图表。

      在这里插入图片描述

      三、总结

      对于一些需要频繁与操作系统交互的场景,比如网络通信,在C语言中,Java的效率并不高,特别是,TLS协议的握手需要多个网络交互,在这种情况下,使用APR本地图书馆可以大大提高性能。除此之外,APR业绩提升的秘诀还有:

      • DirectByteBuffer避免在JVM堆栈和本地内存之间复制内存;

      • 发送文件属性避免在内核和应用程序之间复制内存,以及在用户和内核状态之间交换。

      其实很多高性能网络通信组件,比如 Netty,都是通过 DirectByteBuffer 来收发网络数据的。由于本地内存难于管理,Netty 采用了本地内存池技术,感兴趣的同学可以深入了解一下。

      本文由 在线网速测试 整理编辑,转载请注明出处,原文链接:https://www.wangsu123.cn/news/30216.html

          热门文章

          文章分类