前言
在 Java 应用中,常用的 Web 服务器一般由 tomcat、weblogic、jetty、undertwo等。但从 Java 2019和2020 生态使用报告可以看到,tomcat的用户量对比明显较大,当然这也基于它开源和免费的特点。
Java 2019 年生态圈使用报告
2020 Java 生态系统报告
从软件架构的发展角度来看,软件架构大致经历了如下几个阶段:
从 Java Web 角度来说,架构大致经历了:
从当前企业使用的架构角度来说,使用SSM架构项目比较多,SSH基本被淘汰(大部分是老项目维护),很大一部分企业转向微服务架构了。
基于Spring 生态来说,大部分中小型企业都基本使用SpringBoot,SpringBoot本身集成了 tomcat、jetty和undertwo 容器,那么我们为什么需要花时间来研究tomcat呢?
- 当前tomcat依然是主流java web容器,研究它符合java 技术生态发展;
- 在java web项目调优中,如ssm项目中,在优化项目时,jvm和tomcat同样重要,都需要优化;
- 尽管springboot内置了tomcat容器,且配置了默认的tomcat参数,但当默认的tomcat参数满足不了项目优化要求时,就需要优化人员手动进行相关的参数优化,因此研究tomcat非常必要;
- 熟悉tomcat架构,是后续进行项目优化的基础,也是必备条件。
Tomcat架构说明
知识点:
- Tomcat目录结构
- Tomcat简要架构
- Tomcat各组件及关系
- Tomcat server.xml配置详解
- Tomcat启动参数说明(启动脚本)
Tomcat
是一个基于JAVA的WEB容器,其实现了JAVA EE中的 Servlet 与 jsp 规范,与Nginx Apache 服务器不同在于一般用于动态请求处理。在架构设计上采用面向组件的方式设计。即整体功能是通过组件的方式拼装完成。另外每个组件都可以被替换以保证灵活性。
通过Tomcat官方可以看到,目前已经更新到Tomcat 10了,但当前大部分企业使用的Tomcat 为8或者9版本。
Tomcat 目录结构
- bin:可执行文件,.sh结尾的表示linux可执行文件,.bat结尾的表示windows可执行文件
- conf:配置文件
- lib:tomcat相关jar包
- temp:临时文件
- webapps:存放项目
- work:工作目录
bin目录
bin目录存放可执行文件,简要结束常用命令
这里主要解释如下通用的命令,其他命令就不一一介绍
- catalina.sh 真正启动Tomcat文件,可以在里面设置jvm参数
- startup.sh 程序项目命令文件
- version.sh 查看tomcat版本相关信息命令文件
- shutdown.sh 关闭程序命令
conf目录
conf文件夹用来存放tomcat相关配置文件
1.catalina.policy
项目安全文件,用来防止欺骗代码或JSP执行带有像System.exit(0)这样的命令的可能影响容器的破坏性代码. 只有当Tomcat用-security命令行参数启动时这个文件才会被使用,即启动tomcat时, startup.sh -security
。
上图中,tomcat容器下部署两个项目,项目1和项目2。由于项目1中有代码System.exit(0),当访问该代码时,该代码会导致整个tomcat停止,从而也导致项目2停止。
为了解决因项目1存在欺骗代码或不安全代码导致损害Tomcat容器,从而影响其他项目正常运行的问题,启动tomcat容器时,加上-security参数就,即startup.sh -security
,如此即使项目1中有代码System.exit(0),也只会仅仅停止项目1,而不会影响Tomcat容器,然而起作用的配置文件就是catalina.policy文件。
2.catalina.properties
配置tomcat启动相关信息文件
3.context.xml
监视并加载资源文件,当监视的文件发生发生变化时,自动加载
4.jaspic-providers.xml 和 jaspic-providers.xsd
这两个文件不常用
5.logging.properties
该文件为tomcat日志文件,包括配置tomcat输出格式,日志级别等
6.server.xml
tomcat核心架构主件文件,下面会详细解析。
7.tomcat-users.xml和tomcat-users.xsd
tomcat用户文件,如配置远程登陆账号
tomcat-users.xsd 为tomcat-users.xml描述和约束文件
8.web.xml
tomcat全局配置文件。
lib目录
lib文件夹主要用来存放tomcat依赖jar包,如下为 tomcat 的lib文件夹下的相关jar包。
每个jar包功能,这里就不讲解了,这里主要分析ecj-4.13.jar,这个jar包起到将.java编译成.class字节码作用。
假设要编译MyTest.java,那么jdk会执行两步:
-
第一步:将MyTest.java编译成MyTest.class
javac MyTest.java
-
第二步:执行MyTest.class
java MyTest.class
-
那么,使用ecj-4.13.jar如执行MyTest.java呢?
java -jar ecj-4.13.jar MyTest.java
logs目录
该文件夹表示tomcat日志文件,大致包括如下六类文件:
catalina.date.log | 表示tomcat启动文件 |
---|---|
catalina.out | 表示catalina.date.log日志汇总 |
host-manager.date.log | 表示访问webapps下host-manager项目日志,如访问 ip:8080/host-manager/html |
localhost.date.log | 表示tomcat在启动时,自身访问服务,这个日志只记录tomcat访问日志,而非业务项目日志 |
localhost_access_log.date.txt | 表示访问tomcat所有项目的日志记录,如下表示访问项目localhost,host-manager.html,manager.html和test/index.html四个项目日志记录 |
manager.date.log | 表示访问webapps下manager项目日志,如访问 ip:8080/manager/html |
temp目录
temp目录用户存放tomcat在运行过程中产生的临时文件。(清空不会对tomcat运行带来影响)。
webapps目录
webapps目录用来存放应用程序,当tomcat启动时会去加载webapps目录下的应用程序。可以以文件夹、war包、jar包的形式发布应用。
当然,你也可以把应用程序放置在磁盘的任意位置,在配置文件中映射好就行。
work目录
work目录用来存放tomcat在运行时的编译后文件,例如JSP编译后的文件。
清空work目录,然后重启tomcat,可以达到清除缓存的作用。
Tomcat 简要架构
Tomcat 各组件及关系
- Server 和 Service
- Connector 连接器
- HTTP 1.1
- SSL https
- AJP( Apache JServ Protocol) apache 私有协议,用于apache 反向代理Tomcat
- Container
- Engine 引擎 catalina
- Host 虚拟机 基于域名 分发请求
- Context 隔离各个WEB应用 每个Context的 ClassLoader都是独立
- Component
- Manager (管理器)
- logger (日志管理)
- loader (载入器)
- pipeline (管道)
- valve (管道中的阀)
Tomcat server.xml 配置详解
Server 的基本基本配置:
server
root元素:server 的顶级配置
主要属性:
port:执行关闭命令的端口号
shutdown:关闭命令
- 演示shutdown的用法
#基于telent 执行SHUTDOWN 命令即可关闭
telent 127.0.0.1 8005
SHUTDOWN
service
服务:将多个connector 与一个Engine组合成一个服务,可以配置多个服务。
Connector
连接器:用于接收 指定协议下的连接 并指定给唯一的Engine 进行处理。
主要属性:
- protocol 监听的协议,默认是http/1.1
- port 指定服务器端要创建的端口号
- minThread 服务器启动时创建的处理请求的线程数
- maxThread 最大可以创建的处理请求的线程数
- enableLookups 如果为true,则可以通过调用request.getRemoteHost()进行DNS查询来得到远程客户端的实际主机名,若为false则不进行DNS查询,而是返回其ip地址
- redirectPort 指定服务器正在处理http请求时收到了一个SSL传输请求后重定向的端口号
- acceptCount 指定当所有可以使用的处理请求的线程数都被使用时,可以放到处理队列中的请求数,超过这个数的请求将不予处理,默认100;
- address 绑定客户端特定地址,127.0.0.1
- bufferSize 每个请求的缓冲区大小 bufferSize * maxThreads
- compression 是否启用文档压缩
- compressionMinSize 文档压缩的最小大小
- compressableMimeTypes text/html,text/xml,text/plain
- connectionTimeout 客户端发起链接到服务端接收为止,指定超时的时间数(以毫秒为单位)
- connectionUploadTimeout upload情况下连接超时时间
- disableUploadTimeout 如果为true则使用 connectionTimeout
- keepAliveTimeout 当长链接闲置 指定时间主动关闭 链接 ,前提是客户端请求头 带上这个 head"connection" " keep-alive"
- maxKeepAliveRequests 最大的 长连接数 默认最大100
- maxSpareThreads BIO 模式下 最多线闲置线程数
- minSpareThreads BIO 模式下 最小线闲置线程数
- SSLEnabled 是否开启 sll 验证,在Https 访问时需要开启。
- 演示配置多个Connector
Engine
引擎:用于处理连接的执行器,默认的引擎是catalina。一个service 中只能配置一个Engine。
主要属性:name 引擎名称 defaultHost 默认host
Host
虚拟机:基于域名匹配至指定虚拟机。类似于nginx 当中的server,默认的虚拟机是localhost.
- 演示配置多个Host
Context
应用上下文:一个host 下可以配置多个Context ,每个Context 都有其独立的classPath。相互隔离,以免造成ClassPath 冲突。
- 演示配置多个Context
Valve
阀门:可以理解成request 的过滤器,具体配置要基于具体的Valve 接口的子类。以下即为一个访问日志的Valve.
Tomcat启动参数说明
我们平时启动Tomcat过程是怎么样的?
- 复制WAR包至Tomcat webapp 目录。
- 执行starut.bat 脚本启动。
- 启动过程中war 包会被自动解压装载。
但是我们在Eclipse 或idea 中启动WEB项目的时候 也是把War包复杂至webapps 目录解压吗?显然不是,其真正做法是在Tomcat程序文件之外创建了一个部署目录,在一般生产环境中也是这么做的 即:Tomcat 程序目录和部署目录分开 。
我们只需要在启动时指定CATALINA_HOME 与 CATALINA_BASE 参数即可实现。
启动参数 | 描述说明 |
---|---|
JAVA_OPTS | jvm 启动参数 , 设置内存 编码等 -Xms100m -Xmx200m -Dfile.encoding=UTF-8 |
JAVA_HOME | 指定jdk 目录,如果未设置从java 环境变量当中去找。 |
CATALINA_HOME | Tomcat 程序根目录 |
CATALINA_BASE | 应用部署目录,默认为$CATALINA_HOME |
CATALINA_OUT | 应用日志输出目录:默认$CATALINA_BASE/log |
CATALINA_TMPDIR | 应用临时目录:默认:$CATALINA_BASE/temp |
可以编写一个脚本 来实现自定义配置:
更新 启动 脚本:
#!/bin/bash
export JAVA_OPTS="-Xms100m -Xmx200m"
export JAVA_HOME=/root/svr/jdk/
export CATALINA_HOME=/root/svr/apache-tomcat-7.0.81
export CATALINA_BASE="`pwd`"
case $1 in
start)
$CATALINA_HOME/bin/catalina.sh start
echo start success!!
;;
stop)
$CATALINA_HOME/bin/catalina.sh stop
echo stop success!!
;;
restart)
$CATALINA_HOME/bin/catalina.sh stop
echo stop success!!
sleep 3
$CATALINA_HOME/bin/catalina.sh start
echo start success!!
;;
version)
$CATALINA_HOME/bin/catalina.sh version
;;
configtest)
$CATALINA_HOME/bin/catalina.sh configtest
;;
esac
exit 0
自动部署脚本:
#!/bin/bash -e
export now_time=$(date +%Y-%m-%d_%H-%M-%S)
echo "deploy time:$now_time"
app=$1
version=$2
mkdir -p war/
#从svn下载程序至 war目录
war=war/${app}_${version}.war
echo "$war"
svn export svn://192.168.0.253/release/${app}_${version}.war $war
deploy_war() {
#解压版本至当前目录
target_dir=war/${app}_${version}_${now_time}
unzip -q $war -d $target_dir
rm -f appwar
ln -sf $target_dir appwar
target_ln=`pwd`/appwar
echo '
' > conf/Catalina/localhost/ROOT.xml
#重启Tomcat服务
./tomcat.sh restart
}
deploy_war
Tomcat 网络通信模型剖析
Tomcat 支持四种线程模型介绍
什么是IO?
IO是指为数据传输所提供的输入输出流,其输入输出对象可以是:文件、网络服务、内存等。
什么是IO模型?
提问:
假设应用在从硬盘中读取一个大文件过程中,此时CPU会与硬盘一样出于高负荷状态么?
演示:
- 演示观察大文件的读写过程当中CPU 有没有发生大波动。
演示结果:CPU 没有太高的增涨
通常情况下IO操作是比较耗时的,所以为了高效的使用硬件,应用程序可以用一个专门线程进行IO操作,而另外一个线程则利用CPU的空闲去做其它计算。这种为提高应用执行效率而采用的IO操作方法即为IO模型。
各IO模型简要说明
描述 | |
---|---|
BIO | 阻塞式IO,即Tomcat使用传统的java.io进行操作。该模式下每个请求都会创建一个线程,对性能开销大,不适合高并发场景。优点是稳定,适合连接数目小且固定架构。 |
NIO | 非阻塞式IO,jdk1.4 之后实现的新IO。该模式基于多路复用选择器监测连接状态在通知线程处理,从而达到非阻塞的目的。比传统BIO能更好的支持并发性能。Tomcat 8.0之后默认采用该模式 |
APR | 全称是 Apache Portable Runtime/Apache可移植运行库),是Apache HTTP服务器的支持库。可以简单地理解为,Tomcat将以JNI的形式调用Apache HTTP服务器的核心动态链接库来处理文件读取或网络传输操作。使用需要编译安装APR 库 |
AIO | 异步非阻塞式IO,jdk1.7后之支持 。与nio不同在于不需要多路复用选择器,而是请求处理线程执行完程进行回调调知,已继续执行后续操作。Tomcat 8之后支持。 |
使用指定IO模型的配置方式:
配置 server.xml 文件当中的
默认配置 8.0 protocol=“HTTP/1.1” 8.0 之前是 BIO, 8.0 之后是 NIO
- BIO
protocol=“org.apache.coyote.http11.Http11Protocol”
- NIO
protocol=“org.apache.coyote.http11.Http11NioProtocol”
- AIO
protocol=“org.apache.coyote.http11.Http11Nio2Protocol”
- APR
protocol=“org.apache.coyote.http11.Http11AprProtocol”
Tomcat BIO、NIO实现过程源码解析
BIO 与NIO区别
分别演示在高并发场景下BIO与NIO的线程数的变化?
BIO 的配置
NIO配置
演示数据:
每秒提交数 | BIO执行线程 | NIO执行线程 | |
---|---|---|---|
预测 | 200 | 200线程 | 20线程 |
实验实际 | 200 | 55 wait个线程 | 23个线程 |
模拟生产环境 | 200 | 229个run线程 | 20个wait 线程 |
生成环境重要因素:
- 网络
- 程序执行业务用时
源代码地址:bit-bigdata-transmission
BIO 线程模型
BIO 源码
线程组:
Accept 线程组 acceptorThreadCount 默认1个
exec 线程组 maxThread
JIoEndpoint
Acceptor extends Runnable
SocketProcessor extends Runnable
NIO 线程模型
NIO 线程模型
Accept 线程组 默认两个轮询器
Poller Selector PollerEvent轮询线程状态
SocketProcessor
BIO
线程数量 会受到 客户端阻塞、网络延迟、业务处理慢===>线程数会更多。
NIO
线程数量 会受到业务处理慢===>线程数会更多。
Tomcat connector 并发参数解读
名称 | 描述 |
---|---|
acceptCount | 等待最大队列 |
address | 绑定客户端特定地址,127.0.0.1 |
bufferSize | 每个请求的缓冲区大小。bufferSize * maxThreads |
compression | 是否启用文档压缩 |
compressableMimeTypes | text/html,text/xml,text/plain |
connectionTimeout | 客户发起链接 到 服务端接收为止,中间最大的等待时间 |
connectionUploadTimeout | upload 情况下连接超时时间 |
disableUploadTimeout | true 则使用connectionTimeout |
enableLookups | 禁用DNS查询 true |
keepAliveTimeout | 当长链接闲置 指定时间主动关闭 链接 ,前提是客户端请求头 带上这个 head"connection" " keep-alive" |
maxKeepAliveRequests | 最大的 长连接数 |
maxHttpHeaderSize | |
maxSpareThreads | BIO 模式下 最多线闲置线程数 |
maxThreads(执行线程) | 最大执行线程数 |
minSpareThreads(初始线业务线程 10) | BIO 模式下 最小线闲置线程数 |
Tomcat 类加载机制源码解析
类加载的本质
是用来加载 Class 的。它负责将 Class 的字节码形式转换成内存形式的 Class 对象。字节码可以来自于磁盘文件 _.class,也可以是 jar 包里的 _.class,也可以来自远程服务器提供的字节流,字节码的本质就是一个字节数组 []byte,它有特定的复杂的内部格式。
JVM 运行实例中会存在多个 ClassLoader,不同的 ClassLoader 会从不同的地方加载字节码文件。它可以从不同的文件目录加载,也可以从不同的 jar 文件中加载,也可以从网络上不同的静态文件服务器来下载字节码再加载。
jvm里ClassLoader的层次结构
类加载器层次结构
BootstrapClassLoader(启动类加载器)
称为启动类加载器,是Java类加载层次中最顶层的类加载器,负责加载JDK中的核心类库,如:rt.jar、resources.jar、charsets.jar等,可通过如下程序获得该类加载器从哪些地方加载了相关的jar或class文件:
URL[] urLs = sun.misc.Launcher.getBootstrapClassPath().getURLs();
for (URL url : urLs) {
System.out.println(url.toExternalForm());
}
程序执行结果如下:
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/resources.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/rt.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/sunrsasign.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/jsse.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/jce.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/charsets.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/jfr.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/classes
从rt.jar中选择String类,看一下String类的类加载器是什么
ClassLoader classLoader = String.class.getClassLoader();
System.out.println(classLoader);
执行结果如下:
null
可知由于BootstrapClassLoader对Java不可见,所以返回了null,我们也可以通过某一个类的加载器是否为null来作为判断该类是不是使用BootstrapClassLoader进行加载的依据。
ExtensionClassLoader
ExtClassLoader称为扩展类加载器,主要负责加载Java的扩展类库,默认加载JAVA_HOME/jre/lib/ext/目录下的所有jar包或者由java.ext.dirs系统属性指定的jar包.放入这个目录下的jar包对AppClassLoader加载器都是可见的(因为ExtClassLoader是AppClassLoader的父加载器,并且Java类加载器采用了委托机制)。
ExtClassLoader的类扫描路径通过执行下面代码来看一下:
String extDirs = System.getProperty("java.ext.dirs");
for (String path : extDirs.split(";")) {
System.out.println(path);
}
执行结果如下(Mac系统):
/Users/hjh/Library/Java/Extensions:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/ext:/Library/Java/Extensions:/Network/Library/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java
jre/lib/ext路径下内容为:
从上面的路径中随意选择一个类,来看看他的类加载器是什么:
sun.misc.Launcher$ExtClassLoader@4439f31e
null
从上面的程序运行结果可知ExtClassLoader的父加载器为null,之前说过BootstrapClassLoader对Java不可见,所以返回了null。ExtClassLoader的父加载器返回的是null,那是否说明ExtClassLoader的父加载器是BootstrapClassLoader?
Bootstrap ClassLoader是由C/C++编写的,它本身是虚拟机的一部分,所以它并不是一个JAVA类,也就是无法在java代码中获取它的引用,JVM启动时通过Bootstrap类加载器加载rt.jar等核心jar包中的class文件,之前的int.class,String.class都是由它加载。然后呢,我们前面已经分析了,JVM初始化sun.misc.Launcher并创建Extension ClassLoader和AppClassLoader实例。并将ExtClassLoader设置为AppClassLoader的父加载器。Bootstrap没有父加载器,但是它却可以作用一个ClassLoader的父加载器。比如ExtClassLoader。这也可以解释之前通过ExtClassLoader的getParent方法获取为Null的现象
AppClassLoader
才是直接面向我们用户的加载器,它会加载 Classpath 环境变量里定义的路径中的 jar 包和目录。我们自己编写的代码以及使用的第三方 jar 包通常都是由它来加载的。
加载System.getProperty("java.class.path")所指定的路径或jar。在使用Java运行程序时,也可以加上-cp来覆盖原有的Classpath设置,例如: java -cp ./lavasoft/classes HelloWorld
public class AppClassLoaderTest {
public static void main(String[] args) {
System.out.println(ClassLoader.getSystemClassLoader());
}
}
输出结果如下:
sun.misc.Launcher$AppClassLoader@18b4aac2
以上结论说明调用ClassLoader.getSystemClassLoader()
可以获得AppClassLoader类加载器。
protected ClassLoader() {
this(checkCreateClassLoader(), getSystemClassLoader());
}
通过查看ClassLoader的源码发现并且在没有特定说明的情况下,用户自定义的任何类加载器都将该类加载器作为自定义类加载器的父加载器。
通过执行上面的代码即可获得classpath的加载路径。
在上面的main函数的类的加载就是使用AppClassLoader加载器进行加载的,可以通过执行下面的代码得出这个结论
public class AppClassLoaderTest {
public static void main(String[] args) {
ClassLoader classLoader = Test.class.getClassLoader();
System.out.println(classLoader);
System.out.println(classLoader.getParent());
}
private static class Test {
}
}
执行结果如下:
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@2d209079
从上面的运行结果可以得知AppClassLoader的父加载器是ExtClassLoader
Tomcat的 类加载顺序
在Tomcat中,默认的行为是先尝试在Bootstrap和Extension中进行类型加载,如果加载不到则在Webapp ClassLoader中进行加载,如果还是找不到则在Common中进行查找。
NoClassDefFoundError
NoClassDefFoundError是在开发JavaEE程序中常见的一种问题。该问题会随着你所使用的JavaEE中间件环境的复杂度以及应用本身的体量变得更加复杂,尤其是现在的JavaEE服务器具有大量的类加载器。
在JavaDoc中对NoClassDefFoundError的产生是由于JVM或者类加载器实例尝试加载类型的定义,但是该定义却没有找到,影响了执行路径。换句话说,在编译时这个类是能够被找到的,但是在执行时却没有找到。
这一刻IDE是没有出错提醒的,但是在运行时却出现了错误。
NoSuchMethodError
在另一个场景中,我们可能遇到了另一个错误,也就是NoSuchMethodError。
NoSuchMethodError代表这个类型确实存在,但是一个不正确的版本被加载了。
ClassCastException
ClassCastException,在一个类加载器的情况下,一般出现这种错误都会是在转型操作时,比如:A a = (A) method();,很容易判断出来method()方法返回的类型不是类型A,但是在 JavaEE 多个类加载器的环境下就会出现一些难以定位的情况。
部分图片来源于网络,版权归原作者,侵删。