目录
0,Tomcat 的下载与安装
到 Tomcat 官网下载:
点击 Tomcat8 如下:
下载之后解压即可使用。
1,Tomcat 目录结构
目录结构如下:
重要的几个程序,用于启动和停止服务:
重要的配置文件:server.xml
。
在启动 Tomcat 之前,要确保 Java 环境已存在,因为 Tomcat 是 Java 语言开发的。
1,Servlet 与 Web 容器
随着互联网的发展,我们已经不满足于仅仅浏览静态页面,还希望通过一些交互操作,来获取动态结果,因此也就需要一些扩展机制能够让 HTTP 服务器调用服务端程序。
于是 Sun 公司推出了 Servlet 技术,Servlet 技术是 Web 开发的原点。Servlet 可以简单理解为运行在服务端的 Java 小程序,但是 Servlet 没有 main 方法,不能独立运行,因此必须把它部署到 Servlet 容器中,由容器来实例化并调用 Servlet。
而 Tomcat 和 Jetty 就是一个 Servlet 容器。为了方便使用,它们也具有 HTTP 服务器的功能,因此 Tomcat 或者 Jetty 就是一个“HTTP 服务器 + Servlet 容器”,我们也叫它们 Web 容器。
SpringMVC 框架就是对 Servlet 的封装,Spring 应用本身就是一个 Servlet。在 JavaWeb 中,Servlet(javax.servlet.Servlet
) 也是一个接口,我们把实现了 Servlet 接口的业务类叫作 Servlet。
Servlet 容器用来加载和管理业务类。HTTP 服务器不直接跟业务类打交道,而是把请求交给 Servlet 容器去处理,Servlet 容器会将请求转发到具体的 Servlet,如果这个 Servlet 还没创建,就加载并实例化这个 Servlet,然后调用这个 Servlet 的接口方法。
因此 Servlet 接口其实是 Servlet 容器跟具体业务类之间的接口,Servlet 接口和 Servlet 容器的出现,达到了 HTTP 服务器与业务类解耦的目的。
1,HTTP 请求处理过程
- 当客户请求某个资源时,HTTP 服务器会用一个 ServletRequest 对象把客户的请求信息封装起来,然后调用 Servlet 容器的 service 方法
- Servlet 容器拿到请求后,根据请求的 URL 和 Servlet 的映射关系,找到相应的 Servlet(如果 Servlet 还没有被加载,就用反射机制创建这个 Servlet,并调用 Servlet 的 init 方法来完成初始化)
- 接着调用 Servlet 的 service 方法来处理请求,把 ServletResponse 对象返回给 HTTP 服务器
- HTTP 服务器会把响应发送给客户端
2,Web 应用
根据 Servlet 规范,Web 应用程序有一定的目录结构,在这个目录下分别放置了 Servlet 的类文件、配置文件以及静态资源,Servlet 容器通过读取配置文件,就能找到并加载 Servlet。
Web 应用的目录结构:
| - MyWebApp
| - WEB-INF/web.xml -- 配置文件,用来配置Servlet等
| - WEB-INF/lib/ -- 存放Web应用所需各种JAR包
| - WEB-INF/classes/ -- 存放你的应用类,比如Servlet类
| - META-INF/ -- 目录存放工程的一些信息
Servlet 规范里定义了 ServletContext 这个接口来对应一个 Web 应用。Web 应用部署好后,Servlet 容器在启动时会加载 Web 应用,并为每个 Web 应用创建唯一的 ServletContext 对象。
你可以把 ServletContext 看成是一个全局对象,一个 Web 应用可能有多个 Servlet。由于 ServletContext 持有所有 Servlet 实例,你还可以通过它来实现 Servlet 请求的转发。
3,Filter 与 Listener
Servlet 规范提供了两种扩展机制:Filter 和 Listener。
Filter 是过滤器,这个接口允许你对请求和响应做一些统一的定制化处理。过滤器的工作原理是这样的:Web 应用部署完成后,Servlet 容器需要实例化 Filter 并把 Filter 链接成一个 FilterChain。当请求进来时,获取第一个 Filter 并调用 doFilter 方法,doFilter 方法负责调用这个 FilterChain 中的下一个 Filter。
Listener 是监听器。当 Web 应用在 Servlet 容器中运行时,Servlet 容器内部会不断的发生各种事件,如 Web 应用的启动和停止、用户请求到达等。 Servlet 容器提供了一些默认的监听器来监听这些事件,当事件发生时,Servlet 容器会负责调用监听器的方法。
当然,你可以定义自己的监听器去监听你感兴趣的事件,将监听器配置在web.xml中。比如 Spring 就实现了自己的监听器,来监听 ServletContext 的启动事件,目的是当 Servlet 容器启动时,创建并初始化全局的 Spring 容器。
2,Tomcat 系统架构
Tomcat 的两个核心组件及功能:
- 连接器(Connector):处理 Socket 连接,负责网络字节流与 Request 和 Response 对象的转化。
- 每个连接器都监听不同的端口,比如 Tomcat 默认的 HTTP 连接器监听 8080 端口、默认的 AJP 连接器监听 8009 端口
- 容器(Container):加载和管理 Servlet,以及具体处理 Request 请求。
Tomcat 支持的 I/O 模型有:
- NIO:非阻塞 I/O,采用 Java NIO 类库实现。
- NIO.2:异步 I/O,采用 JDK 7 最新的 NIO.2 类库实现。
- APR:采用 Apache 可移植运行库实现,是 C/C++ 编写的本地库。
Tomcat 支持的应用层协议有:
- HTTP/1.1:这是大部分 Web 应用采用的访问协议。
- AJP:用于和 Web 服务器集成(如 Apache)。
- HTTP/2:HTTP 2.0 大幅度的提升了 Web 性能。
一个 Server 中有一个或者多个 Service(通过在 Tomcat 中配置多个 Service,可以实现通过不同的端口号来访问同一台机器上部署的不同应用)。
一个 Service 中有多个连接器和一个容器(一个容器可对接多个连接器,就好比一个房间有多个门)。连接器与容器之间通过标准的 ServletRequest 和 ServletResponse 通信。
1,连接器
连接器的功能如下:
- 监听网络端口。接受网络连接请求。读取网络请求字节流。
- 根据具体应用层协议(HTTP/AJP)解析字节流,生成统一的 Tomcat Request 对象。
- 将 Tomcat Request 对象转成标准的 ServletRequest。
- 调用 Servlet 容器,得到 ServletResponse。
- 将 ServletResponse 转成 Tomcat Response 对象。
- 将 Tomcat Response 转成网络字节流。
- 将响应字节流写回给浏览器。
连接器中的三个核心组件(其中 Endpoint 和 Processor 放在一起抽象成了 ProtocolHandler 组件):
- Endpoint:负责提供字节流给 Processor
- Endpoint 是通信端点,即通信监听的接口,Endpoint 在 Java 代码中是一个接口
- Acceptor 用于监听 Socket 连接请求
- SocketProcessor 用于处理接收到的 Socket 请求,它实现 Runnable 接口,在 run 方法里调用协议处理组件 Processor 进行处理
- 为了提高处理能力,SocketProcessor 被提交到线程池来执行,这个线程池叫作执行器(Executor)
- Processor:负责提供 Tomcat Request 对象给 Adapter
- Adapter:负责提供 ServletRequest 对象给容器
2,容器
Tomcat 通过一种分层的架构,使得 Servlet 容器具有很好的灵活性。
Tomcat 设计了 4 种容器,分别是 Engine、Host、Context 和 Wrapper,它们的关系如下:
Tomcat 用组合模式来管理这些容器,所有容器组件都实现了 Container 接口。
说明:
- Engine 表示引擎,用来管理多个虚拟站点,一个 Service 最多只能有一个 Engine
- Host 代表的是一个虚拟主机(站点),可以给 Tomcat 配置多个虚拟主机地址,而一个虚拟主机下可以部署多个 Web 应用程序
- Context 表示一个 Web 应用程序
- Wrapper 表示一个 Servlet,一个 Web 应用程序中可能会有多个 Servlet
Tomcat 配置文件的结构:
Tomcat 用 Mapper 组件将用户请求的 URL 定位到一个 Servlet,Mapper 组件里保存了容器组件与访问路径的映射关系。
类图关系如下:
每一层容器都是一个 Pipeline-Valve 管道,Valve 表示一个处理点,每个管道有多个处理点,处理点之间用链表连接。每个管道的第一个处理点加 First,最后一个处理点叫 Basic。上层容器的 Basic 会调用下层容器的 First。
Pipeline-Valve 是责任链模式,责任链模式是指在一个请求处理的过程中有很多处理者依次对请求进行处理,每个处理者负责做自己相应的处理,处理完之后将再调用下一个处理者继续处理。
Wrapper 容器的最后一个 Valve 会创建一个 Filter 链,并调用 doFilter 方法,最终会调到 Servlet 的 service 方法。
最后到业务层的 Controller 是这样调用的:Wrapper -> Filter -> DispatcherServlet -> Controller。
3,优化 Tomcat 启动速度
可优化项:
-
清理你的 Tomcat
- 清理不必要的 Web 应用。即删除掉 webapps 文件夹下不需要的工程,一般是 host-manager、example、doc 等这些默认的工程
- 清理 XML 配置文件。Tomcat 在启动的时候会解析所有的 XML 配置文件,保持配置文件的简洁,需要解析的东西越少,速度就会越快。
- 清理不必要的 JAR 文件。
- 清理其他文件。删除 logs 文件夹下不需要的日志文件。还有 work 文件夹下的 catalina 文件夹,它其实是 Tomcat 把 JSP 转换为 Class 文件的工作目录。
-
禁止 Tomcat TLD 扫描
- Tomcat 为了支持 JSP,在应用启动的时候会扫描 JAR 包里面的 TLD 文件,加载里面定义的标签库
- 如果你的项目没有使用 JSP 作为 Web 页面模板,而是使用 Velocity 之类的模板引擎,你完全可以把 TLD 扫描禁止掉
- 方法是在 conf/ 目录下的context.xml文件里的 Context 标签下,加上 JarScanner 和 JarScanFilter 子标签
-
关闭 WebSocket 支持
- Tomcat 会扫描 WebSocket 注解的 API 实现,如果用不到,可将其关闭
- 方法是在 conf/ 目录下的 context.xml文件,给 Context 标签加一个 containerSciFilter 的属性
- 同时可以把 Tomcat lib 目录下的websocket-api.jar和tomcat-websocket.jar这两个 JAR 文件删除掉
-
关闭 JSP 支持
- 如果想把 WebSocket 和 JSP 都关闭,那就这样配置:
-
禁止 Servlet 注解扫描
- Servlet 3.0 引入了注解 Servlet,Tomcat 为了支持这个特性,会在 Web 应用启动时扫描你的类文件。
- 因此如果你没有使用 Servlet 注解这个功能,可以告诉 Tomcat 不要去扫描 Servlet 注解。
- 方法是,在你的 Web 应用的web.xml文件中,设置
<web-app>
元素的属性metadata-complete="true"
如果你是用嵌入式的方式运行 Tomcat,比如 Spring Boot,你也可以通过 Spring Boot 的方式去修改 Tomcat 的参数,调优的原理都是一样的。
4,如何监控 Tomcat 性能
Tomcat 的关键指标有吞吐量、响应时间、错误数、线程池、CPU 以及 JVM 内存。
Tomcat 可以通过 JMX 将上述指标暴露出来的。JMX(Java Management Extensions,即 Java 管理扩展)是一个为应用程序、设备、系统等植入监控管理功能的框架。
首先需要开启 JMX 端口,然后通过 JConsole 监控 Tomcat 的各种指标。
可以使用 JMeter 来对服务性能进行测试
在 Linux 系统中还可以使用下面的命令来查看 Tomcat 的情况:
> top -p 进程ID
# 查看端口8080 的连接列表
> netstat -na | grep 8080
5,Tomcat I/O 和线程池的并发调优
所谓的 I/O 调优指的是选择 NIO、NIO.2 还是 APR,而线程池调优指的是给 Tomcat 的线程池设置合适的参数,使得 Tomcat 能够又快又好地处理请求。
I/O 模型:默认都是 NIO
- NIO:如果在 Linux 平台上,建议使用 NIO
- NIO.2:如果在 Windows 平台上,并且 HTTP 请求的数据量比较大,可以考虑 NIO.2
- APR:如果你的 Web 应用用到了 TLS 加密传输,而且对性能要求极高,这个时候可以考虑 APR
Connector 配置:
Executor 线程池中的关键参数:
其中最核心的是如何确定 maxThreads 的值:
- 如果这个参数设置小了,Tomcat 会发生线程饥饿,并且请求的处理会在队列中排队等待,导致响应时间变长
- 如果 maxThreads 参数值过大,线程数太多会导致线程在 CPU 上来回切换,耗费大量的切换开销
那么在实际情况下,线程池的个数如何确定呢?这是一个迭代的过程,可以先用默认值,再反复压测调整,从而达到最优。