APP下载

京东年薪60W架构师带你深入拆解Tomcat Endpoint

消息来源:baojiabao.com 作者: 发布时间:2024-05-18

报价宝综合消息京东年薪60W架构师带你深入拆解Tomcat Endpoint

一、NioEndpoint 元件:Tomcat 如何实现非阻塞 I/O

UNIX 系统下的 I/O 模型有 5 种:同步阻塞 I/O、同步非阻塞 I/O、I/O 多路复用、讯号驱动 I/O 和异步 I/O。

I/O 就是计算机内存与外部装置之间拷贝资料的过程。

1,同步阻塞 I/O

使用者执行绪发起 read 呼叫后就阻塞了,让出 CPU。核心等待网络卡资料到来,把资料从网络卡拷贝到核心空间,接着把资料拷贝到使用者空间,再把使用者执行绪叫醒。

2,同步非阻塞 I/O

使用者执行绪不断的发起 read 呼叫,资料没到核心空间时,每次都返回失败,直到资料到了核心空间,这一次 read 呼叫后,在等待资料从核心空间拷贝到使用者空间这段时间里,执行绪还是阻塞的,等资料到了使用者空间再把执行绪叫醒。

3,I/O 多路复用

使用者执行绪的读取操作分成两步了,执行绪先发起 select 呼叫,目的是问核心资料准备好了吗?等核心把资料准备好了,使用者执行绪再发起 read 呼叫。在等待资料从核心空间拷贝到使用者空间这段时间里,执行绪还是阻塞的。那为什么叫 I/O 多路复用呢?因为一次 select 呼叫可以向核心查多个数据通道(Channel)的状态,所以叫多路复用。

4,异步 I/O

使用者执行绪发起 read 呼叫的同时注册一个回拨函式,read 立即返回,等核心将资料准备好后,再呼叫指定的回拨函式完成处理。在这个过程中,使用者执行绪一直没有阻塞。

Tomcat 的 NioEndPoint 元件实现了 I/O 多路复用模型,它一共包含 LimitLatch、Acceptor、Poller、SocketProcessor 和 Executor 共 5 个元件。

LimitLatch 是连线控制器,它负责控制最大连线数,NIO 模式下预设是 10000,达到这个阈值后,连线请求被拒绝。

Acceptor 跑在一个单独的执行绪里,它在一个死循环里呼叫 accept 方法来接收新连线,一旦有新的连线请求到来,accept 方法返回一个 Channel 物件,接着把 Channel 物件交给 Poller 去处理。

Poller 的本质是一个 Selector,也跑在单独执行绪里。Poller 在内部维护一个 Channel 阵列,它在一个死循环里不断检测 Channel 的资料就绪状态,一旦有 Channel 可读,就生成一个 SocketProcessor 任务物件扔给 Executor 去处理。每个 Poller 执行绪都有自己的 Queue。每个 Poller 执行绪可能同时被多个 Acceptor 执行绪呼叫来注册 PollerEvent。Poller 不断的通过内部的 Selector 物件向核心查询 Channel 的状态,一旦可读就生成任务类 SocketProcessor 交给 Executor 去处理。Poller 的另一个重要任务是循环遍历检查自己所管理的 SocketChannel 是否已经超时,如果有超时就关闭这个 SocketChannel。

Executor 就是执行绪池,负责执行 SocketProcessor 任务类,SocketProcessor 的 run 方法会呼叫 Http11Processor 来读取和解析请求资料。ServerSocketChannel 通过 accept() 接受新的连线,accept() 方法返回获得 SocketChannel 物件,然后将 SocketChannel 物件封装在一个 PollerEvent 物件中,并将 PollerEvent 物件压入 Poller 的 Queue 里,这是个典型的生产者 - 消费者模式,Acceptor 与 Poller 执行绪之间通过 Queue 通讯。

高并发就是能快速地处理大量的请求,需要合理设计执行绪模型让 CPU 忙起来,尽量不要让执行绪阻塞,因为一阻塞,CPU 就闲下来了。另外就是有多少任务,就用相应规模的执行绪数去处理。我们注意到 NioEndpoint 要完成三件事情:接收连线、检测 I/O 事件以及处理请求,那么最核心的就是把这三件事情分开,用不同规模的执行绪去处理,

I/O 模型是为了解决内存和外部装置速度差异的问题。我们平时说的阻塞或非阻塞是指应用程序在发起 I/O 操作时,是立即返回还是等待。而同步和异步,是指应用程序在与核心通讯时,资料从核心空间到应用空间的拷贝,是由核心主动发起还是由应用程序来触发。

二、Nio2Endpoint 元件:Tomcat 如何实现异步 I/O

1,Nio2Endpoint

Nio2Acceptor 扩充套件了 Acceptor,用异步 I/O 的方式来接收连线,跑在一个单独的执行绪里,也是一个执行绪组。Nio2Acceptor 接收新的连线后,得到一个 AsynchronousSocketChannel,Nio2Acceptor 把 AsynchronousSocketChannel 封装成一个 Nio2SocketWrapper,并建立一个 SocketProcessor 任务类交给执行绪池处理,并且 SocketProcessor 持有 Nio2SocketWrapper 物件。

Executor 在执行 SocketProcessor 时,SocketProcessor 的 run 方法会呼叫 Http11Processor 来处理请求,Http11Processor 会通过 Nio2SocketWrapper 读取和解析请求资料,请求经过容器处理后,再把响应通过 Nio2SocketWrapper 写出。

Nio2Endpoint 中没有 Poller 元件,也就是没有 Selector。因为在异步 I/O 模式下,Selector 的工作交给核心来做了。

2,Nio2Acceptor

Nio2Acceptor 的监听连线的过程不是在一个死循环里不断的调 accept 方法,而是通过回拨函式来完成的。

三、AprEndPoint 元件:Tomcat APR 提高 I/O 效能的秘密

APR(Apache Portable Runtime Libraries)是 Apache 可移植执行时库,它是用 C 语言实现的,其目的是向上层应用程序提供一个跨平台的操作系统界面库。Tomcat 可以用它来处理包括档案和网络 I/O,从而提升效能。

跟 NioEndpoint 一样,AprEndpoint 也实现了非阻塞 I/O,它们的区别是:NioEndpoint 通过呼叫 Java 的 NIO API 来实现非阻塞 I/O,而 AprEndpoint 是通过 JNI 呼叫 APR 本地库而实现非阻塞 I/O 的。

1,APR 提升效能的秘密

Tomcat 的 Endpoint 元件在接收网络资料时需要预先分配好一块 Buffer,所谓的 Buffer 就是字节阵列byte[],Java 通过 JNI 呼叫把这块 Buffer 的地址传给 C 程式码,C 程式码通过操作系统 API 读取 Socket 并把资料填充到这块 Buffer。Java NIO API 提供了两种 Buffer 来接收资料:HeapByteBuffer 和 DirectByteBuffer。

HeapByteBuffer 物件本身在 JVM 堆上分配,并且它持有的字节阵列byte[]也是在 JVM 堆上分配。但是如果用HeapByteBuffer来接收网络资料,需要把资料从核心先拷贝到一个临时的本地内存,再从临时本地内存拷贝到 JVM 堆,而不是直接从核心拷贝到 JVM 堆上。这是为什么呢?这是因为资料从核心拷贝到 JVM 堆的过程中,JVM 可能会发生 GC,GC 过程中物件可能会被移动,也就是说 JVM 堆上的字节阵列可能会被移动,这样的话 Buffer 地址就失效了。如果这中间经过本地内存中转,从本地内存到 JVM 堆的拷贝过程中 JVM 可以保证不做 GC。

DirectByteBuffer 物件本身在 JVM 堆上,但是它持有的字节阵列不是从 JVM 堆上分配的,而是从本地内存分配的。

DirectByteBuffer 物件本身在 JVM 堆上,但是它持有的字节阵列不是从 JVM 堆上分配的,而是从本地内存分配的。

2,sendfile

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

除此之外,APR 提升效能的秘密还有:通过 DirectByteBuffer 避免了 JVM 堆与本地内存之间的内存拷贝;通过 sendfile 特性避免了核心与应用之间的内存拷贝以及使用者态和核心态的切换。

粉丝福利,需获取HashMap、分散式、微服务、spring等最新相关架构资料

关注我+转发此文+私信回复关键词:架构

2019-11-18 10:51:00

相关文章