Tomcat -- 整体架构
Posted neei
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Tomcat -- 整体架构相关的知识,希望对你有一定的参考价值。
整体架构
HTTP工作原理
● 规定浏览器和服务器之间的数据传输协议,基于TCP/IP协议传递数据,不涉及数据包的传输,主要规范了客户端与服务器端的通信格式
一个请求流程
其中tomcat主要参与接受连接,解析请求数据,处理请求,发送响应结果
- 用户通过浏览器进行操作,点击链接等,浏览器获取到这个事件
- 浏览器向服务器发出TCP链接请求
- 服务程序接收浏览器的请求,通过TCP三次握手建立连接(linux底层握手)
- 连接成功后,浏览器将请求数据打包成一个HTTP协议格式的数据包
- 浏览器将该数据包推入网络中,数据包经过网络传输,最终达到服务端程序
- 服务端程序拿到这个数据包,同样用HTTP协议格式解析,获取到客户端的操作意图(tomcat连接器,封装Request对象)
- 根据客户端的操作意图进行处理,提供数据
- 服务器将处理响应结果用HTTP再次打包
- 服务器将该数据包推入网络中,数据包经过网络传输,最终达到客户端浏览器
- 浏览器拿到数据包后,以http协议的格式解析,比如得到html
- 浏览器将html展示给用户
Tomcat整体架构
Http服务器请求处理
● http服务器不直接调用业务类,把请求交给Servlet容器来处理
● 容器通过servlet接口调用业务类,实现http服务器与业务类解耦
servlet容器工作过程
● 客户端请求某个资源的时候,http服务器会用一个ServletRequest对象把客户端的请求信息封装起来,调用Servlet容器的service方法
● Servlet容器拿到请求后,根据请求的URL和Servlet的映射关系,找到相应的处理的Servlet
● 如果此时Serlvet容器还没有被加载就会通过反射机制创建这个Servlet,并调用其init方法完成初始化
● 而后调用Servlet的service方法来处理请求
● service处理的结果会被封装到ServletResponse对象中返回给HTTP服务器
● http服务器将响应结果发送给客户端
tomcat两个核心
● 连接器(Connector):负责对外连接。处理Socket连接,负责网络字节流与Request和Response对象的转化
● 容器(Container):负责内部处理。加载和管理Servlet,处理具体Request请求
连接器Connector
Coyote
● tomcat中连接器框架,是tomcat服务器提供客户端访问的外部接口
● 客户端通过Coyote与服务器实现 建立连接、发送请求、接收响应
Coyote与Catalina容器的交互
● Coyote封装了底层网络通信(Socket),为Catalina容器提供统一的接口,使Catalina容器与具体的请求协议和IO操作完全解耦
● Coyote只负责具体协议和IO相关操作,与Servlet规范没有直接关系,Request和Response对象也是在Catalina中对其进一步封装为ServletRequest和ServletResponse
● Coyote将Socket输入的对象封装为Request对象,Servlet相关都交给Catalina处理,由Catalina容器进行请求处理,实现组件之间解耦
● 请求处理完成,Catalina通过Coyote提供的Response将结果写入输出流
支持的IO模型与协议
IO模型
IO模型 | 描述 |
---|---|
BIO | 阻塞IO,tomcat8.5之后移除了对的该IO模型的支持 |
NIO | 非阻塞IO,javaNIO类库实现 |
NIO2 | 异步IO,NIO2类库实现 |
APR | 使用Apache可移植运行库实现,需要单独安装APR库 |
应用层协议 | 描述 |
---|---|
HTTP/1.1 | 大部分web协议采用 |
HTTP/2.0 | tomcat8.5之后版本支持,大幅度提醒web性能 |
AJP | 用于与web服务器集成,实现对静态资源的优化及集群部署 |
连接器组件
组件 | 作用 |
---|---|
EndPoint | Endpoint是Coyote通信端点,即通信监听的接口,是具体socket接收和发送处理器,是对传输层的抽象; Endpoint用来实现TCP/IP协议 |
Processor | Processor是Coyote协议处理接口,用来实现Http协议;Processor接收来自EndPoint的Socket,读取字节流解析成的Tomcat Request和Response对象,并通过Adapter适配交给容器处理;Processor是对应应用层协议的抽象 |
ProtocolHandler | 是Coyote协议接口,通过Endpoint和Processor实现针对具体协议的处理能力;Tomcat按照协议和IO提供6个实现类: AjpNioProtocol, AjpAprProtocol, Http11NioProtocol, Http11Nio2Protocol, Http11AprProtocol |
Adapter | 由于协议不同,客户端发过来的请求信息不尽相同;tomcat定义了自己的Request类来封装这些请求信息;ProtocolHandler接口负责解析请求并生成Tomcat Request类,通过CoyoteAdapter适配为selvert容器可以解析的ServletRequest,执行对应的service方法 |
EndPoint
- 对传输对象层的抽象,用来实现TCP/IP协议
- Coyote通信端口,通信监听的接口,具体Socket的接收和发送处理器
- tomcat提供了抽象类AbstractEndPoint,定义了Acceptor和SocketProcessor
- Acceptor:用于监听Socket请求
- SocketProcessor:用于处理接收到的Socket请求,最终实现Runnable,在run/doRun方法里调用协议处理组件Processor进行处理,为提高性能,SocketProcessor被提交给Executor执行器线程池来管理
Processor
- 对应用层协议的抽象,EndPoint实现了TCP/IP协议,Processor实现了HTTP协议
- Coyote协议处理接口,接收EndPoint的Socket
- 读取字节流解析成Tomcat的Request和Response对象,并通过Adapter将其提交给容器处理
ProtocolHandler
- Coyote协议接口,同EndPoint和Processor实现针对具体协议的处理能力
Adapter
- 使用CoyoteAdapter类,适配Request/ServletRequest和Response/ServletRespose的适配器
- 由于协议不同,tomcat为了兼容,定义了Request类来接收请求信息
- ProtocolHandler负责解析请求生成Tomcat的Request对象
- CoyoteAdapter负责将tomcat的Request适配成Servlet可以解析的ServletRequest标准对象
- 从而调用Servlet的service方法,实现请求、响应能力
容器Container
Catalina
- tomcat本质是一个Servlet容器,容器的实现由Catalina,涉及到安全、会话、集群、管理等Servlet容器各个方面
- 通过松耦合方式集成Coyote,完成按照请求协议进行数据读写等
Catalina结构
- Catalina负责管理Server,Server表示整个服务器。
- Server下面有多个服务Service,每个Service包含多个多个连接器组件Connector(Coyote实现)和一个容器组件Container。
- tomcat启动的时候初始化一个Catalina实例
组件 | 功能 |
---|---|
Catalina | 负责解析tomcat配置文件,来创建服务器Server组件并对其进行管理 |
Server | 表示整个Catalina Servlet容器及组件,负责组装并启动Servlet引擎,tomcat连接器。通过实现Lifecycle接口,提供一种优雅的启动和关闭整个系统的方式 |
Service | 一个Server包含多个Service组件,它将多个Connector组件绑定到一个Container(Engine)上 |
Connector | 连接器,处理与客户端的通信,负责接收客户端请求,然后转给相关的容器处理,最终向客户端返回响应结果 |
Container | 容器,负责处理用户的servlet请求,返回对象给web用户 |
Container
Container结构
组件 | 功能 |
---|---|
Engine | 表示整个Catalina的Servlet引擎,用来管理多个host虚拟主机,一个Service最多就一个Engine,一个Engine包含多个Host |
Host | 代表一个虚拟主机,可以给tomcat配置多个虚拟主机地址,一个虚拟主机下面可以包含多个Context |
Context | 表示一个web应用程序,可包含多个Wrapper |
Wrapper | 表示一个(一种?)Servlet,容器最底层 |
Tomcat篇02-整体架构和I/O模型
参考技术A本文主要包括tomcat服务器的目录结构、工作模式、整体架构、I/O模型以及NIO、NIO2、APR三者的对比介绍。
我们先来看一下tomcat8.5和tomcat9中的home目录中的文件:
可以看到除掉一些说明文件之后,还有7个目录:
实际上除了主目录里有lib目录,在webapps目录下的web应用中的WEB-INF目录下也存在一个lib目录:
两者的区别在于:
● Tomcat主目录下的lib目录:存放的JAR文件 不仅能被Tomcat访问,还能被所有在Tomcat中发布的Java Web应用访问
● webapps目录下的Java Web应用的lib目录:存放的JAR文件 只能被当前Java Web应用访问
既然有多个lib目录,那么肯定就有使用的优先顺序,Tomcat类加载器的目录加载优先顺序如下:
Tomcat的类加载器负责为Tomcat本身以及Java Web应用加载相关的类。假如Tomcat的类加载器要为一个Java Web应用加载一个类,类加载器会按照以下优先顺序到各个目录中去查找该类的.class文件,直到找到为止,如果所有目录中都不存在该类的.class文件,则会抛出异常:
Tomcat不仅可以单独运行,还可以与其他的Web服务器集成,作为其他Web服务器的进程内或进程外的servlet容器。集成的意义在于:对于不支持运行Java Servlet的其他Web服务器,可通过集成Tomcat来提供运行Servlet的功能。
Tomcat有三种工作模式:
我们先从tomcat的源码目录来分析一下tomcat的整体架构,前面我们配置jsvc运行tomcat的时候,我们知道tomcat中启动运行的最主要的类是 org.apache.catalina.startup.Bootstrap ,那么我们在tomcat的源码中的java目录下的org目录的apache目录可以找到主要的源码的相对应的类。
图中的目录如果画成架构图,可以这样表示:
Tomcat 本质上就是一款Servlet 容器,因此 catalina 才是Tomcat的核心 ,其他模块都是为 catalina 提供支撑的。
单线程阻塞I/O模型是最简单的一种服务器I/O模型,单线程即同时只能处理一个客户端的请求,阻塞即该线程会一直等待,直到处理完成为止。对于多个客户端访问,必须要等到前一个客户端访问结束才能进行下一个访问的处理,请求一个一个排队,只提供一问一答服务。
如上图所示:这是一个同步阻塞服务器响应客户端访问的时间节点图。
这种模型的特点在于单线程和阻塞I/O。 单线程即服务器端只有一个线程处理客户端的所有请求,客户端连接与服务器端的处理线程比是 n:1 ,它无法同时处理多个连接,只能串行处理连接。而阻塞I/O是指服务器在读写数据时是阻塞的,读取客户端数据时要等待客户端发送数据并且把操作系统内核复制到用户进程中,这时才解除阻塞状态。写数据回客户端时要等待用户进程将数据写入内核并发送到客户端后才解除阻塞状态。 这种阻塞带来了一个问题,服务器必须要等到客户端成功接收才能继续往下处理另外一个客户端的请求,在此期间线程将无法响应任何客户端请求。
该模型的特点:它是最简单的服务器模型,整个运行过程都只有一个线程,只能支持同时处理一个客户端的请求(如果有多个客户端访问,就必须排队等待), 服务器系统资源消耗较小,但并发能力低,容错能力差。
多线程阻塞I/O模型在单线程阻塞I/O模型的基础上对其进行改进,加入多线程,提高并发能力,使其能够同时对多个客户端进行响应,多线程的核心就是利用多线程机制为每个客户端分配一个线程。
如上图所示,服务器端开始监听客户端的访问,假如有两个客户端同时发送请求过来,服务器端在接收到客户端请求后分别创建两个线程对它们进行处理,每条线程负责一个客户端连接,直到响应完成。 期间两个线程并发地为各自对应的客户端处理请求 ,包括读取客户端数据、处理客户端数据、写数据回客户端等操作。
这种模型的I/O操作也是阻塞的 ,因为每个线程执行到读取或写入操作时都将进入阻塞状态,直到读取到客户端的数据或数据成功写入客户端后才解除阻塞状态。尽管I/O操作阻塞,但这种模式比单线程处理的性能明显高了,它不用等到第一个请求处理完才处理第二个,而是并发地处理客户端请求,客户端连接与服务器端处理线程的比例是 1:1 。
多线程阻塞I/O模型的特点:支持对多个客户端并发响应,处理能力得到大幅提高,有较大的并发量,但服务器系统资源消耗量较大,而且如果线程数过多,多线程之间会产生较大的线程切换成本,同时拥有较复杂的结构。
在探讨单线程非阻塞I/O模型前必须要先了解非阻塞情况下套接字事件的检测机制,因为对于单线程非阻塞模型最重要的事情是检测哪些连接有感兴趣的事件发生。一般会有如下三种检测方式。
当多个客户端向服务器请求时,服务器端会保存一个套接字连接列表中,应用层线程对套接字列表轮询尝试读取或写入。如果成功则进行处理,如果失败则下次继续。这样不管有多少个套接字连接,它们都可以被一个线程管理,这很好地利用了阻塞的时间,处理能力得到提升。
但这种模型需要在应用程序中遍历所有的套接字列表,同时需要处理数据的拼接,连接空闲时可能也会占用较多CPU资源,不适合实际使用。
这种方式将套接字的遍历工作交给了操作系统内核,把对套接字遍历的结果组织成一系列的事件列表并返回应用层处理。对于应用层,它们需要处理的对象就是这些事件,这是一种事件驱动的非阻塞方式。
服务器端有多个客户端连接,应用层向内核请求读写事件列表。内核遍历所有套接字并生成对应的可读列表readList和可写列表writeList。readList和writeList则标明了每个套接字是否可读/可写。应用层遍历读写事件列表readList和writeList,做相应的读写操作。
内核遍历套接字时已经不用在应用层对所有套接字进行遍历,将遍历工作下移到内核层,这种方式有助于提高检测效率。 然而,它需要将所有连接的可读事件列表和可写事件列表传到应用层,假如套接字连接数量变大,列表从内核复制到应用层也是不小的开销。 另外,当活跃连接较少时, 内核与应用层之间存在很多无效的数据副本 ,因为它将活跃和不活跃的连接状态都复制到应用层中。
通过遍历的方式检测套接字是否可读可写是一种效率比较低的方式,不管是在应用层中遍历还是在内核中遍历。所以需要另外一种机制来优化遍历的方式,那就是 回调函数 。内核中的套接字都对应一个回调函数,当客户端往套接字发送数据时,内核从网卡接收数据后就会调用回调函数,在回调函数中维护事件列表,应用层获取此事件列表即可得到所有感兴趣的事件。
内核基于回调的事件检测方式有两种
第一种是用 可读列表readList 和 可写列表writeList 标记读写事件, 套接字的数量与 readList 和 writeList 两个列表的长度一样 。
上面两种方式由操作系统内核维护客户端的所有连接并通过回调函数不断更新事件列表,而应用层线程只要遍历这些事件列表即可知道可读取或可写入的连接,进而对这些连接进行读写操作,极大提高了检测效率,自然处理能力也更强。
单线程非阻塞I/O模型最重要的一个特点是,在调用读取或写入接口后立即返回,而不会进入阻塞状态。虽然只有一个线程,但是它通过把非阻塞读写操作与上面几种检测机制配合就可以实现对多个连接的及时处理,而不会因为某个连接的阻塞操作导致其他连接无法处理。在客户端连接大多数都保持活跃的情况下,这个线程会一直循环处理这些连接,它很好地利用了阻塞的时间,大大提高了这个线程的执行效率。
单线程非阻塞I/O模型的主要优势体现在对多个连接的管理,一般在同时需要处理多个连接的发场景中会使用非阻塞NIO模式,此模型下只通过一个线程去维护和处理连接,这样大大提高了机器的效率。一般服务器端才会使用NIO模式,而对于客户端,出于方便及习惯,可使用阻塞模式的套接字进行通信。
在多核的机器上可以通过多线程继续提高机器效率。最朴实、最自然的做法就是将客户端连接按组分配给若干线程,每个线程负责处理对应组内的连接。比如有4个客户端访问服务器,服务器将套接字1和套接字2交由线程1管理,而线程2则管理套接字3和套接字4,通过事件检测及非阻塞读写就可以让每个线程都能高效处理。
多线程非阻塞I/O模式让服务器端处理能力得到很大提高,它充分利用机器的CPU,适合用于处理高并发的场景,但它也让程序更复杂,更容易出现问题(死锁、数据不一致等经典并发问题)。
最经典的多线程非阻塞I/O模型方式是Reactor模式。首先看单线程下的Reactor,Reactor将服务器端的整个处理过程分成若干个事件,例如分为接收事件、读事件、写事件、执行事件等。Reactor通过事件检测机制将这些事件分发给不同处理器去处理。在整个过程中只要有待处理的事件存在,即可以让Reactor线程不断往下执行,而不会阻塞在某处,所以处理效率很高。
基于单线程Reactor模型,根据实际使用场景,把它改进成多线程模式。常见的有两种方式:一种是在耗时的process处理器中引入多线程,如使用线程池;另一种是直接使用多个Reactor实例,每个Reactor实例对应一个线程。
Reactor模式的一种改进方式如下图所示。其整体结构基本上与单线程的Reactor类似,只是引入了一个线程池。由于对连接的接收、对数据的读取和对数据的写入等操作基本上都耗时较少,因此把它们都放到Reactor线程中处理。然而,对于逻辑处理可能比较耗时的工作,可以在process处理器中引入线程池,process处理器自己不执行任务,而是交给线程池,从而在Reactor线程中避免了耗时的操作。将耗时的操作转移到线程池中后,尽管Reactor只有一个线程,它也能保证Reactor的高效。
Reactor模式的另一种改进方式如下图所示。其中有多个Reactor实例,每个Reactor实例对应一个线程。因为接收事件是相对于服务器端而言的,所以客户端的连接接收工作统一由一个accept处理器负责,accept处理器会将接收的客户端连接均匀分配给所有Reactor实例,每个Reactor实例负责处理分配到该Reactor上的客户端连接,包括连接的读数据、写数据和逻辑处理。这就是多Reactor实例的原理。
Tomcat支持的I/O模型如下表(自8.5/9.0 版本起,Tomcat移除了对BIO的支持),在 8.0 之前 , Tomcat 默认采用的I/O方式为 BIO , 之后改为 NIO。 无论 NIO、NIO2 还是 APR, 在性能方面均优于以往的BIO。
Tomcat中的NIO模型是使用的JAVA的NIO类库,其内部的IO实现是同步的(也就是在用户态和内核态之间的数据交换上是同步机制),采用基于selector实现的异步事件驱动机制(这里的异步指的是selector这个实现模型是使用的异步机制)。 而对于Java来说,非阻塞I/O的实现完全是基于操作系统内核的非阻塞I/O,它将操作系统的非阻塞I/O的差异屏蔽并提供统一的API,让我们不必关心操作系统。JDK会帮我们选择非阻塞I/O的实现方式。
NIO2和前者相比的最大不同就在于引入了异步通道来实现异步IO操作,因此也叫AIO(Asynchronous I/O)。NIO.2 的异步通道 APIs 提供方便的、平台独立的执行异步操作的标准方法。这使得应用程序开发人员能够以更清晰的方式来编写程序,而不必定义自己的 Java 线程,此外,还可通过使用底层 OS 所支持的异步功能来提高性能。如同其他 Java API 一样,API 可利用的 OS 自有异步功能的数量取决于其对该平台的支持程度。
异步通道提供支持连接、读取、以及写入之类非锁定操作的连接,并提供对已启动操作的控制机制。Java 7 中用于 Java Platform(NIO.2)的 More New I/O APIs,通过在 java.nio.channels 包中增加四个异步通道类,从而增强了 Java 1.4 中的 New I/O APIs(NIO),这些类在风格上与 NIO 通道 API 很相似。他们共享相同的方法与参数结构体,并且大多数对于 NIO 通道类可用的参数,对于新的异步版本仍然可用。主要区别在于新通道可使一些操作异步执行。
异步通道 API 提供两种对已启动异步操作的监测与控制机制。第一种是通过返回一个 java.util.concurrent.Future 对象来实现,它将会建模一个挂起操作,并可用于查询其状态以及获取结果。第二种是通过传递给操作一个新类的对象, java.nio.channels.CompletionHandler ,来完成,它会定义在操作完毕后所执行的处理程序方法。每个异步通道类为每个操作定义 API 副本,这样可采用任一机制。
Apache可移植运行时(Apache Portable Runtime,APR) 是Apache HTTP服务器的支持库,最初,APR是作为Apache HTTP服务器的一部分而存在的,后来成为一个单独的项目。其他的应用程序可以使用APR来实现平台无关性(跨平台)。APR提供了一组映射到下层操作系统的API,如果操作系统不支持某个特定的功能,APR将提供一个模拟的实现。这样程序员使用APR编写真正可在不同平台上移植的程序。
顺利安装完成后会显示apr的lib库路径,一般都是 /usr/local/apr/lib
安装完成之后我们还需要修改环境变量和配置参数
这里我们使用的是systemd调用jsvc来启动tomcat,所以我们直接在systemd对应的tomcat的unit文件中的 ExecStart 中添加一个路径参数 -Djava.library.path=/usr/local/apr/lib 指向apr库的路径:
然后我们在tomcat的home目录下的conf子目录中对server.xml文件进行修改
把8080端口对应的配置修改成apr:(其他端口配置也类似)
重启tomcat服务我们从tomcat的日志中就可以看到协议已经从默认的NIO变成了apr。
NIO性能是最差的这是毋庸置疑的,如果是考虑到高并发的情况,显然异步非阻塞I/O模式的NIO2和APR库在性能上更有优势,实际上NIO2的性能表现也和APR不相上下,但是NIO2要求Tomcat的版本要在8.0以上,而APR只需要5.5以上即可,但是APR需要额外配置库环境,相对于内置集成的NIO2来说APR这个操作比较麻烦,两者各有优劣。具体使用哪个还是需要结合实际业务需求和环境进行测试才能决定。
以上是关于Tomcat -- 整体架构的主要内容,如果未能解决你的问题,请参考以下文章