性能调优07-Tomcat网络通信模型与应用解析

一、Tomcat组件介绍

1、简介

​ Tomcat是一个基于JAVA的WEB容器,其实现了JAVA EE中的 Servlet 与 jsp 规范,与Nginx(一般用于反向代理、负载均衡,屏蔽内部细节)、apache 服务器不同在于一般用于动态请求处理。在架构设计上采用面向组件的方式设计。即整体功能是通过组件的方式拼装完成。另外每个组件都可以被替换以保证灵活性。

​ 那么是哪些组件组成了Tomcat呢?

图片

2、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 (管道中的阀)

image-20211229224310865

3、Tomcat server.xml 配置

**server  **

​ root元素——server 的顶级配置
主要属性:

  • port:执行关闭命令的端口号
  • shutdown:关闭命令
1
2
3
#基于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 指定当所有可以使用的处理请求的线程数都被使用时,可以放到处理队列中的请求数,超过这个数的请求将不予处理
  • connectionTimeout 指定超时的时间数(以毫秒为单位)
  • SSLEnabled 是否开启 sll 验证,在Https 访问时需要开启。
1
2
3
4
5
6
7
8
9
10
11
12
 <!-- 演示配置Connector -->
 <Connector port="8860" protocol="org.apache.coyote.http11.Http11NioProtocol"
                connectionTimeout="20000"
                redirectPort="8862"
                URIEncoding="UTF-8"
                useBodyEncodingForURI="true"
                compression="on" compressionMinSize="2048"
compressableMimeType="text/html,text/xml,text/plain,text/javascript,text/css,application/x-json,application/json,application/x-javascript"
                maxThreads="1024" minSpareThreads="200"
                acceptCount="800"
                enableLookups="false"
        />

Engine

​ 引擎:用于处理连接的执行器,默认的引擎是catalina。一个service 中只能配置一个Engine
主要属性:

  • name 引擎名称
  • defaultHost 默认host

Host

​ 虚拟机:基于域名匹配至指定虚拟机。类似于nginx 当中的server,默认的虚拟机是localhost
主要属性:

1
2
3
4
5
<Host name="www.luban.com"  appBase="/usr/www/luban"
            unpackWARs="true" autoDeploy="true">
<Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"               prefix="www.luban.com.access_log" suffix=".txt"
               pattern="%h %l %u %t &quot;%r&quot; %s %b" />
</Host>

Context

应用上下文:一个host 下可以配置多个Context ,每个Context 都有其独立的classPath。相互隔离,以免造成ClassPath 冲突。
主要属性:

1
<Context docBase="hello" path="/h" reloadable="true"/>

Valve
阀门:可以理解成过滤器,具体配置要基于具体的Valve 接口的子类。以下即为一个访问日志的Valve.

1
2
3
 <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
prefix="www.luban.com.access_log" suffix=".txt"
pattern="%h %l %u %t &quot;%r&quot; %s %b" />

4、Tomcat 自动部署脚本编写


Tomcat启动参数说明

我们平时启动Tomcat过程是怎么样的?

  1. 复制WAR包至Tomcat webapp 目录。
  2. 执行starut.bat 脚本启动。
  3. 启动过程中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

可以编写一个脚本 来实现自定义配置:

更新 启动 脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#!/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

自动部署脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#!/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.147/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 '<?xml version="1.0" encoding="UTF-8" ?>
<Context docBase="'$target_ln'" allowLinking="false">
</Context>' > conf/Catalina/localhost/ROOT.xml
#重启Tomcat服务
./tomcat.sh restart
}
deploy_war

二、Tomcat通信模型原理与源码

2.1、tomcat支持的4种IO模型

1、什么是IO?

​ IO是指为数据传输所提供的输入输出流,其输入输出对象可以是:文件、网络服务、内存等。

2、什么是IO模型

​ 通常情况下IO操作是比较耗时的,所以为了高效的使用硬件,应用程序可以用一个专门线程进行IO操作,而另外一个线程则利用CPU的空闲去做其它计算。这种为提高应用执行效率而采用的IO操作方法即为IO模型。

3、各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(asynchronous I/O) 异步非阻塞式IO,jdk1.7后之支持 。与nio不同在于不需要多路复用选择器,而是请求处理线程执行完程进行回调调知,以继续执行后续操作。Tomcat 8之后支持。优点是适合连接数目大且连接时间较短的场景

4、Tomcat使用指定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“

2.2、Tomcat BIO、NIO实现过程源码解析

BIO(JioEndPoint)

3个组成成分都实现了runnable

Acceptor:接受所有连接,分配给SocketProcessor处理

SocketProcessor:接受连接,分配线程池线程

image-20220128233200081

NIO(NioEndPoint)

3个组成成分都实现了runnable

Acceptor:接受所有连接,注册给Poller处理

Poller:监听、分配任务给SocketProcessor

SocketProcessor:接受连接,分配线程池线程

image-20220128233207289

2.3、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 模式下 最小线闲置线程数

2.4、类加载

1、什么是类加载

​ 类加载是加载Class文件进入JVM。它负责将 Class 的字节码形式转换成内存形式的 Class 对象。字节码可以来自于磁盘文件 *.class,也可以是 jar 包里的 *.class,也可以来自远程服务器提供的字节流,字节码的本质就是一个字节数组 []byte,它有特定的复杂的内部格式。

2、JVM的类加载器

​ JVM 运行实例中会存在多个 ClassLoader,不同的 ClassLoader 会从不同的地方加载字节码文件。它可以从不同的文件目录加载,也可以从不同的 jar 文件中加载,也可以从网络上不同的静态文件服务器来下载字节码再加载。JVM中类加载器层次结构如下

img

1、启动类加载器:Bootstrap ClassLoader,用于加载JVM提供的基础运行类,即位于**%JAVA_HOME%/jre/lib目录下的核心类库**;

2、扩展类加载器:Extension ClassLoader, Java提供的一个标准的扩展机制用于加载除核心类库外的Jar包,即只要复制到指定的扩展目录(可以多个)下的Jar, JVM会自动加载(不需要通过-classpath指定)。默认的扩展目录是%JAVA_HOME%/jre/lib/ext。典型的应用场景就是,Java使用该类加载器加载JVM默认提供的但是不属于核心类库的Jar。不推荐将应用程序依赖的类库放置到扩展目录下,因为该目录下的类库对所有基于该JVM运行的应用程序可见;

3、应用程序类加载器:Application ClassLoader ,用于加载环境变量CLASSPATH (不推荐使用)指定目录下的或者-classpath运行参数指定的Jar包。System类加载器通常用于加载应用程序Jar包及其启动入口类(Tomcat 的Bootstrap类即由System类加载器加载)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public abstract class ClassLoader {
// 每个类加载器都有一个父加载器
private final ClassLoader parent;
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
// 如果没有加载过
if (c == null) {
if (parent != null) {
// 先委托给父加载器去加载,注意这是个递归调用
c = parent.loadClass(name, false);
} else {
// 如果父加载器为空,查找 Bootstrap 加载器是不是加载过了
c = findBootstrapClassOrNull(name);
}

// 如果父加载器没加载成功,调用自己的 findClass 去加载
if (c == null) {
c = findClass(name);
}
}

return c;
}

}
//ClassLoader 中findClass方式需要被子类覆盖,下面这段代码就是对应代码
protected Class<?> findClass(String name){
//1. 根据传入的类名 name,到在特定目录下去寻找类文件,把.class 文件读入内存
...
//2. 调用 defineClass 将字节数组转成 Class 对象
return defineClass(buf, off, len);
}
// 将字节码数组解析成一个 Class 对象,用 native 方法实现
protected final Class<?> defineClass(byte[] b, int off, int len){

}

}

3、Tomcat 的类加载器

下图不表示类加载顺序,只代表结构

img

  • 引导类加载器 和 扩展类加载器 的作⽤不变

  • 系统类加载器正常情况下加载的是 CLASSPATH 下的类,但是 Tomcat 的启动脚本并未使⽤该变量,⽽是加载tomcat启动的类,⽐如bootstrap.jar,通常在catalina.bat或者catalina.sh中指定。位于CATALINA_HOME/bin下

  • Common 通⽤类加载器加载Tomcat使⽤以及应⽤通⽤的⼀些类,位于CATALINA_HOME/lib下,⽐如servlet-api.jar

  • Catalina ClassLoader ⽤于加载服务器内部可⻅类,这些类应⽤程序不能访问

  • Shared ClassLoader ⽤于加载应⽤程序共享类,这些类服务器不会依赖

  • Webapp ClassLoader,每个应⽤程序都会有⼀个独⼀⽆⼆的Webapp ClassLoader,他⽤来加载本应⽤程序 /WEB-INF/classes 和 /WEB-INF/lib 下的类。

2.5、Tomcat如何双亲委派机制

1、什么是双亲委派机制

​ 当某个类加载器需要加载某个.class⽂件时,它⾸先把这个任务委托给他的上级类加载器,递归这个操作,如果上级的类加载器没有加载,⾃⼰才会去加载这个类。JVM的类加载就是严格按照该机制进行的

2、双亲委派机制的作用

1、防⽌重复加载同⼀个.class。通过委托去向上⾯问⼀问,加载过了,就不⽤再加载⼀遍。保证数据安全。
2、保证核⼼.class不能被篡改。通过委托⽅式,不会去篡改核⼼.class,即使篡改也不会去加载,即使加载也不会是同⼀个.class对象了。这样保证了class执⾏安全(如果⼦类加载器先加载,那么我们可以写⼀些与java.lang包中基础类同名的类, 然后再定义⼀个⼦类加载器,这样整个应⽤使⽤的基础类就都变成我们⾃⼰定义的类了。)如

3、何打破双亲委派

WebappClassLoaderBase重写loadClass(),delegate默认为false,因此打破了双亲委派机制
可以通过配置 不打破双亲委托

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
if (log.isDebugEnabled())
log.debug("loadClass(" + name + ", " + resolve + ")");
Class<?> clazz = null;
// Log access to stopped class loader
checkStateForClassLoading(name);
//从当前ClassLoader的本地缓存中加载类,如果找到则返回
clazz = findLoadedClass0(name);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Returning class from cache");
if (resolve)
resolveClass(clazz);
return clazz;
}
// 本地缓存没有的情况下,调用ClassLoader的findLoadedClass方法查看jvm是否已经加载过此类,如果已经加载则直接返回。
clazz = findLoadedClass(name);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Returning class from cache");
if (resolve)
resolveClass(clazz);
return clazz;
}
String resourceName = binaryNameToPath(name, false);
//此时的javaseClassLoader是扩展类加载器 是把扩展类加载器赋值给了javaseClassLoader
ClassLoader javaseLoader = getJavaseClassLoader();
boolean tryLoadingFromJavaseLoader;
try {
.....
//如果可以用getResource得到
//如果能用扩展类加载器的getResource得到就证明可以被扩展类加载器加载到接下来安排扩展类加载器加载
if (tryLoadingFromJavaseLoader) {
try {
//使用扩展类加载器进行加载
clazz = javaseLoader.loadClass(name);
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
}
// (0.5) Permission to access this class when using a SecurityManager
if (securityManager != null) {
int i = name.lastIndexOf('.');
if (i >= 0) {
try {
securityManager.checkPackageAccess(name.substring(0,i));
} catch (SecurityException se) {
String error = "Security Violation, attempt to use " +
"Restricted Class: " + name;
log.info(error, se);
throw new ClassNotFoundException(error, se);
}
}
}
boolean delegateLoad = delegate || filter(name, true);
// (1) Delegate to our parent if requested
//如果是true就是用父类加载器进行加载
if (delegateLoad) {
if (log.isDebugEnabled())
log.debug(" Delegating to parent classloader1 " + parent);
try {
clazz = Class.forName(name, false, parent);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Loading class from parent");
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
}
// (2) Search local repositories
if (log.isDebugEnabled())
log.debug(" Searching local repositories");
try {
// 本地进行加载
clazz = findClass(name);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Loading class from local repository");
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
// (3) Delegate to parent unconditionally
//到这里还是没有加载上再次尝试使用父类加载器进行加载
if (!delegateLoad) {
if (log.isDebugEnabled())
log.debug(" Delegating to parent classloader at end: " + parent);
try {
clazz = Class.forName(name, false, parent);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Loading class from parent");
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
}
}
throw new ClassNotFoundException(name);
}

4、Tomcat类加载顺序

默认情况下(打破双亲委派)

  • JVM 的 Bootstrap 类
  • Web 应用的 /WEB-INF/classes 类
  • Web 应用的 /WEB-INF/lib/*.jar 类
  • System 类加载器的类
  • Common 类加载器的类

遵循双亲委托

  • JVM 的 Bootstrap 类
  • System 类加载器的类
  • Common 类加载器的类
  • Web 应用的 /WEB-INF/classes 类
  • Web 应用的 /WEB-INF/lib/*.jar 类

4、为何打破后依旧先加载Bootstrap的类

​ jvm的一些基础类不允许重写,所以bootstrapClassLoader始终是最先加载的

5、不同的加载器加载同一个class的到的class对象也是不同的

三、Tomcat处理HTTP原理

1、请求的流程

HTTP请求 -> Ip + Port -> 操作系统 -> Tomcat(1) ->根据Http协议解析成Request-> Engine(1) -> Host(n) -> Context(n) -> Wrapper(n) ->doFilter -> service() -> doGet()/doPost()

image-20220126235959710

image-20220126220257506

image-20220126220403696

image-20220126224337153

2、Http请求字节流如何解析成HttpServletRequest

1、Http请求头格式

image-20220126225059436

image-20220126225922494

​ JIoEndPoint(InternalInputBuffer)就是按照Http协议请求头的格式去解析字节流,从而解析成HttpServletRequest对象(一般而言通过请求头的Content-Length表示请求体长度,或者分块传输——Transfer-Encoding为chunk)