虚拟机性能监控
215
notes/JVM_性能监控之可视化工具.md
Normal file
@ -0,0 +1,215 @@
|
||||
# JVM 性能监控之可视化工具
|
||||
|
||||
## 一、简介
|
||||
|
||||
在 JDK 安装目录的 `bin` 文件夹下,除了提供有命令行监控工具外,还提供了几种可视化的监控工具,以方便用户直观地了解虚拟机的运行状态。常用的可视化监控工具如下:
|
||||
|
||||
|
||||
|
||||
## 二、JConsole
|
||||
|
||||
### 2.1 简介
|
||||
|
||||
JConsole(Java Monitoring and Management Console)是一款基于 JMX(Java Manage-ment Extensions)的可视化监视工具。它的主要功能是通过 JMX 的 MBean(Managed Bean)对系统信息进行收集和动态调整系统参数。JMX(Java Management Extensions)是一个为应用程序、设备、系统等植入管理功能的框架,通常用于监控系统的运行状态或管理系统的部分功能。
|
||||
|
||||
### 2.2 使用
|
||||
|
||||
打开位于 bin 目录下的 `jconsole` 程序后,它会自动扫描当前主机上的所有 JVM 进程:
|
||||
|
||||

|
||||
|
||||
选中需要监控的进程后,点击连接,即可进入监控界面。监控界面包含了 *概览*、*内存*、*线程*、*类*、*VM 概要*、*MBean* 六个选项卡。其中概览界面显示的是 *内存*、*线程*、*类* 等三个选项卡界面的概览信息,如下所示:
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
而内存界面主要用于显示堆和非堆上各个区域的使用量:
|
||||
|
||||
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
线程界面内主要显示各个线程的堆栈信息,最下角有一个 **检测死锁** 按钮,点击后如果检测到死锁存在,则在下部的线程选项卡旁边会出现死锁选项卡:
|
||||
|
||||
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
点击死锁选项卡则可以看到造成死锁的线程:
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
最后的 **类** 选项卡主要用于显示当前已加载和已卸载的类的数量。而 **VM 概要** 选项卡则主要用于显示虚拟机的相关参数,如下所示:
|
||||
|
||||
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
## 三、VisualVM
|
||||
|
||||
### 3.1 简介
|
||||
|
||||
VisualVM(All-in-One Java Troubleshooting Tool)是 Oracle 提供的功能最强大的运行监视和故障处理程序之一, 它除了支持常规的运行监视、故障处理等功能外,还能用于性能分析(Profiling)。同时因为 VisualVM 是基于 NetBeans 平台的开发工具,所以它还支持通过插件来进行功能的拓展。VisualVM 的主要功能如下:
|
||||
|
||||
- 显示虚拟机进程及其配置信息、环境信息(与 jps、jinfo 功能类似);
|
||||
- 监视应用程序的处理器、垃圾收集、堆、方法区以及线程的信息(与 jstat、jstack 功能类似);
|
||||
- dump以及分析堆转储快照(与 jmap、jhat 功能类似);
|
||||
- 方法级的程序运行性能分析,找出被调用最多、运行时间最长的方法;
|
||||
- 离线程序快照:可以收集程序的运行时配置、线程 dump、内存 dump 等信息来建立快照。
|
||||
|
||||
### 3.2 使用
|
||||
|
||||
打开位于 bin 目录下的 `jvisualvm` 程序, 它会自动扫描当前主机上的所有 JVM 进程:
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
点击需要监控的进程后,右侧即会显示相关的监控信息:
|
||||
|
||||
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
**1. 堆 Dump**
|
||||
|
||||
在监控界面点击按钮可以 **执行垃圾回收** 或者 **堆 Dump** 。进行堆 Dump 后,还会显示其分析结果:
|
||||
|
||||
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
**2. 线程 Dump**
|
||||
|
||||
在线程界面可以查看所有线程的状态,如果出现死锁,该界面还会进行提示:
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
此时可以进行 **线程 Dump** 来获取具体的线程信息,效果和 jstack 命令类似:
|
||||
|
||||
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
**3. 性能分析**
|
||||
|
||||
在 Profiler 界面,可以进行 CPU 和 内存的性能分析。要开始性能分析,需要先选择 **CPU** 或 **内存** 按钮中的一个,VisualVM 将会开始记录应用程序执行过的所有方法:如果是进行的是 CPU 执行时间分析,将会统计每个方法的执行次数、执行耗时;如果是内存分析,则会统计每个方法关联的对象数以及这些对象所占的空间。想要结束性能分析,点击停止按钮即可:
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
**4. Visual GC**
|
||||
|
||||
Visual GC 面板默认是不显示的,需要通过插件进行扩展。它会实时监控虚拟机的状态,在功能上类似于 jstat 命令:
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
### 3.3 安装插件
|
||||
|
||||
在主界面,点击 **工具 => 插件** ,可以打开插件面板。右击插件选项或者点击安装按钮即可完成对应插件的安装:
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
需要注意的是,安装插件前需要按照自己 JVM 的版本来配置插件中心,否则会抛出 ”无法连接到插件中心“ 的异常。每个版本对应的插件中心可以在该网址上查看:https://visualvm.github.io/pluginscenters.html,界面如下:
|
||||
|
||||
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
之后需要将正确的插件中心的地址配置到程序中:
|
||||
|
||||
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
## 四、连接远程进程
|
||||
|
||||
以上演示 JConsole 和 VisualVM 时,我们都是用的本地进程,但在实际开发中,我们更多需要监控的是服务器上的远程进程。想要监控远程主机上的进程,需要进行 JMX 的相关配置,根据连接时是否需要用户名和密码,可以分为以下两种配置方式:
|
||||
|
||||
### 4.1 不使用安全凭证
|
||||
|
||||
启动服务器上的 Java 进程时增加以下参数:
|
||||
|
||||
```shell
|
||||
java -Dcom.sun.management.jmxremote.port=12345 #jmx远程连接的端口号
|
||||
-Dcom.sun.management.jmxremote.ssl=false
|
||||
-Dcom.sun.management.jmxremote.authenticate=false
|
||||
-jar springboot.jar
|
||||
```
|
||||
|
||||
此时只需要知道主机地址和端口号就可以连接,不需要使用用户名和密码,所以安全性比较低。
|
||||
|
||||
### 4.2 使用安全凭证
|
||||
|
||||
启动服务器上的 Java 进程时增加以下参数:
|
||||
|
||||
```shell
|
||||
java -Dcom.sun.management.jmxremote.port=12345
|
||||
-Dcom.sun.management.jmxremote.ssl=false
|
||||
-Dcom.sun.management.jmxremote.authenticate=true
|
||||
-Dcom.sun.management.jmxremote.access.file=/usr/local/jmxremote.access
|
||||
-Dcom.sun.management.jmxremote.password.file=/usr/local/jmxremote.password
|
||||
-jar springboot.jar
|
||||
```
|
||||
|
||||
其中 `jmxremote.access ` 的内容如下,其中 admin 为用户名,readwrite 表示可读可写,也可以设置为 readonly(只读):
|
||||
|
||||
```shell
|
||||
admin readwrite
|
||||
```
|
||||
|
||||
`jmxremote.password` 的内容如下,其中 admin 为用户名,123456 为密码:
|
||||
|
||||
```shell
|
||||
admin 123456
|
||||
```
|
||||
|
||||
两个文件创建好后,还需要赋予其执行权限:
|
||||
|
||||
```shell
|
||||
chmod 600 /usr/local/jmxremote.access
|
||||
chmod 600 /usr/local/jmxremote.password
|
||||
chown root:root /usr/local/jmxremote.access
|
||||
chown root:root /usr/local/jmxremote.password
|
||||
```
|
||||
|
||||
之后在使用 VisualVM 进行远程连接时,配置如下:
|
||||
|
||||

|
||||
|
||||
需要注意的是这里的端口号是配置的 `Dcom.sun.management.jmxremote.port` 的值,而不是 Java 程序的端口号。连接完成后,即可查看到对应进程的监控状态。
|
||||
|
||||
|
||||
|
||||
## 参考资料
|
||||
|
||||
1. 主要参考自:周志明 . 深入理解Java虚拟机(第3版). 机械工业出版社 , 2019-12 ,想要深入了解虚拟机的话,推荐阅读原书。
|
||||
2. visualvm 官方文档:https://visualvm.github.io/documentation.html
|
||||
3. [Java_jvisualvm使用JMX连接远程机器(实践)](https://www.cnblogs.com/gossip/p/6141941.html)
|
||||
4. [使用JMX透过防火墙远程监控tomcat服务](https://my.oschina.net/mye/blog/64879)
|
244
notes/JVM_性能监控之命令行工具.md
Normal file
@ -0,0 +1,244 @@
|
||||
# JVM 性能监控之命令行工具
|
||||
|
||||
## 一、简介
|
||||
|
||||
在 JDK 安装目录的 `bin` 文件夹下,除了提供有 `javac` 、`java` 这两个常用的编译和运行工具外,还提供了一系列命令行工具用于 JVM 的性能监控和故障诊断,常用的命令如下:
|
||||
|
||||
|
||||
|
||||
## 二、jps
|
||||
|
||||
jps(JVM Process Status Tool)用于列出正在运行的虚拟机进程的主类名称和 LVMID(Local Virtual Machine Identifier,本地虚拟机唯一标识),这里得到的 LVMID 是进行后续其它查询的基础。示例如下:
|
||||
|
||||
```shell
|
||||
C:\Users>jps
|
||||
10848 Main
|
||||
14560 Jps
|
||||
7040 Launcher
|
||||
11572
|
||||
9492 DeadLockTest
|
||||
7868 JConsole
|
||||
```
|
||||
|
||||
可选参数有 `-v` ,用于输出虚拟机进程启动时的 JVM 参数。
|
||||
|
||||
|
||||
|
||||
## 三、jstat
|
||||
|
||||
jstat(JVM Statistics Monitoring Tool)用于监视虚拟机的运行状态。使用格式如下:
|
||||
|
||||
```shell
|
||||
jstat -<option> [-t] [-h<lines>] <vmid> [<interval> [<count>]]
|
||||
```
|
||||
|
||||
其中 `option` 的所有可选值如下:
|
||||
|
||||
| 选项 | 作用 |
|
||||
| ----------------- | ------------------------------------------------------------ |
|
||||
| -class | 监视类加载、卸载数量、总空间以及类装载所耗费的时间 |
|
||||
| -gc | 监视 Java 堆状况,包括 Eden 区、2 个 Survivor 区、老年代的容量、已用空间、垃圾收集时间等信息 |
|
||||
| -gccapacity | 与 -gc 基本相同,但主要关注的是 Java 堆各个区域使用到的最大、最小空间 |
|
||||
| -gcutil | 与 -gc 基本相同,但主要关注的是已使用空间占总空间的百分比 |
|
||||
| -gccause | 与 -gcutil 基本相同,但是会额外输出上一次垃圾回收的原因 |
|
||||
| -gcnew | 监视新生代垃圾回收的状况 |
|
||||
| -gcnewcapacity | 与 -gcnew 基本相同,但主要关注的是使用到的最大、最小空间 |
|
||||
| -gcold | 监视老年代垃圾回收的状况 |
|
||||
| -gcoldcapacity | 与 -gcold 基本相同,但主要关注的是使用到的最大、最小空间 |
|
||||
| -compiler | 输出即时编译器编译过的方法、耗时等信息 |
|
||||
| -printcompilation | 输出已经被即时编译的方法 |
|
||||
|
||||
命令行中的 `interval` 表示监控的时间间隔,`count` 表示监控次数。示例如下:
|
||||
|
||||
```shell
|
||||
jstat -gc 9492 3s 5 # 每3s输出一次,一共输出5次
|
||||
```
|
||||
|
||||

|
||||
|
||||
输出信息中各个参数含义分别如下:
|
||||
|
||||
- **S0C**:survivor 0 的容量大小,单位 kB;
|
||||
- **S1C**:survivor 1 的容量大小,单位 kB;
|
||||
- **S0U**:survivor 0 已使用的空间大小,单位 kB;
|
||||
- **S1U**:survivor 1 已使用的空间大小,单位 kB;
|
||||
- **EC**:Eden 区的容量大小,单位 kB;
|
||||
- **EU**:Eden 区已使用的空间大小,单位 kB;
|
||||
- **OC**:老年代的容量大小,单位 kB;
|
||||
- **OU**:老年代已使用的空间大小,单位 kB;
|
||||
- **MC**:Metaspace 容量大小,单位 kB;
|
||||
- **MU**:Metaspace 已使用的空间大小,单位 kB;
|
||||
- **CCSC**:压缩类的空间大小,单位 kB;
|
||||
- **CCSU**:压缩类已使用的空间大小,单位 kB;
|
||||
- **YGC**:年轻代垃圾回收的次数;
|
||||
- **YGCT**: 年轻代垃圾回收所消耗的时间;
|
||||
- **FGC**:老年代垃圾回收的次数;
|
||||
- **FGCT**:老年代垃圾回收所消耗的时间;
|
||||
- **GCT**:垃圾回收所消耗的总时间。
|
||||
|
||||
以上是 option 为 `-gc` 时的输出结果,不同 option 的输出结果是不同的,所有输出结果及其参数解释可以参考官方文章: https://docs.oracle.com/javase/8/docs/technotes/tools/unix/jstat.html
|
||||
|
||||
|
||||
|
||||
## 四、jinfo
|
||||
|
||||
jinfo(Configuration Info for Java)的作用是实时查看和调整虚拟机的各项参数。使用格式如下:
|
||||
|
||||
```shell
|
||||
jinfo [option] <pid>
|
||||
```
|
||||
|
||||
其中 `option ` 支持以下可选项:
|
||||
|
||||
- **-flag name** :输出指定的虚拟机参数的值;
|
||||
- **-flag [+|-]name** :启用或禁用指定名称的虚拟机参数;
|
||||
- **-flag name=value** :设置虚拟机参数的值;
|
||||
- **-flags** :以键值对的方式输出 JVM 的相关属性;
|
||||
- **-sysprops**:以键值对的方式输出 Java 相关的系统属性。
|
||||
|
||||
示例如下:
|
||||
|
||||
```java
|
||||
jinfo -flags 13604
|
||||
jinfo -flag CMSInitiatingOccupancyFraction 13604
|
||||
```
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
## 五、jmap
|
||||
|
||||
jmap(Memory Map for Java)命令主要用于生成堆转储快照(一般称为 heapdump 或 dump文件)。除此之外,它还可以用来查询 finalize 执行队列、Java 堆和方法区的详细信息,如空间使用率、当前使用的收集器等。 使用格式如下:
|
||||
|
||||
```shell
|
||||
jmap [option] <pid>
|
||||
```
|
||||
|
||||
其中 `option` 支持以下可选项:
|
||||
|
||||
| 选项 | 作用 |
|
||||
| --------------------------- | ---- |
|
||||
| -dump:[live,]format=b,file= | 生成 Java 堆转储快照,其中 live 用于指明是否只 dump 出存活的对象 |
|
||||
| -finalizerinfo | 显示在 F-Queue 中等待 Finalizer 线程执行 finalize 方法的对象。只在 Linux/Solaris 平台下有效 |
|
||||
| -heap | 显示 Java 堆详细信息,如使用哪种回收器、参数配置、分代状况等。只在 Linux/Solaris 平台下有效 |
|
||||
| -histo[:live] | 显示堆中对象的统计信息,包括类、实例数量、合计容量 |
|
||||
| -permstat | 以 ClassLoader 为统计口径显示永久代内存状态。只在 Linux/Solaris 平台下有效 |
|
||||
| -F | 当虚拟机进程堆 -dump 选项没有响应时,可使用这个选项强制生成 dump 快照。<br/>只在 Linux/Solaris 平台下有效 |
|
||||
|
||||
示例如下:
|
||||
|
||||
```shell
|
||||
jmap -dump:format=b,file=test.bin 3260
|
||||
```
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
## 六、jhat
|
||||
|
||||
jhat(JVM Heap Analysis Tool)命令主要用来分析 jmap 生成的堆转储快照。 假设我们有如下一段程序:
|
||||
|
||||
```java
|
||||
public class StackOverFlowTest {
|
||||
|
||||
private static List<StackOverFlowTest> list = new ArrayList<>();
|
||||
|
||||
public static void main(String[] args) throws InterruptedException {
|
||||
while (true) {
|
||||
list.add(new StackOverFlowTest());
|
||||
Thread.sleep(10); //因为只是演示,所以休眠一下,避免生成的堆转储文件过大,导致分析时间过长
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
其最终会抛出 `java.lang.OutOfMemoryError: Java heap space` 异常,意味着在 JVM 堆上发生了内存溢出。在程序运行期间,我们可以使用上面的 jmap 命令生成堆转储快照,并使用 jhat 命令进行分析:
|
||||
|
||||

|
||||
|
||||
jhat 命令最终的分析结果会以网页的方式进行提供,端口为 7000,界面如下:
|
||||
|
||||

|
||||
|
||||
jhat 分析的结果并不够直观,因此我们还可以借助第三方工具来分析堆转储快照,这里以 JProfiler 为例,该软件可以直接从[官网](https://www.ej-technologies.com/products/jprofiler/overview.htm)下载并安装,安装完成后,点击 `session` 选项卡,并使用 `Open Snapshot` 打开 jmap 命令生成的堆转储快照:
|
||||
|
||||

|
||||
|
||||
之后程序会自动进行分析,分析结果如下:
|
||||
|
||||

|
||||
|
||||
通过以上可视化的统计结果,我们就可以很快定位到导致内存溢出的原因。
|
||||
|
||||
|
||||
|
||||
## 七、jstack
|
||||
|
||||
jstack(Stack Trace for Java)命令用于生成虚拟机的线程快照(一般称为 threaddump 或者 javacore 文件)。线程快照就是每一条线程正在执行的方法堆栈的集合,线程快照可以用于定位线程长时间停顿的原因,如死锁、死循环和长时间挂起等。其使用格式如下:
|
||||
|
||||
```shell
|
||||
jstack -F [-m] [-l] <pid>
|
||||
```
|
||||
|
||||
各选项的作用如下:
|
||||
|
||||
| 选项 | 作用 |
|
||||
| ---- | ----------------------------------------------- |
|
||||
| -F | 当正常输出的请求不被响应时,强制输出线程堆栈 |
|
||||
| -m | 除堆栈外,显示关于锁的附加信息 |
|
||||
| -l | 如果有调用本地方法的话,则可以显示 C/C++ 的堆栈 |
|
||||
|
||||
假设我们的程序中存在如下死锁:
|
||||
|
||||
```java
|
||||
public class DeadLockTest {
|
||||
|
||||
private static final String a = "a";
|
||||
private static final String b = "b";
|
||||
|
||||
public static void main(String[] args) {
|
||||
new DeadLockTest().deadlock();
|
||||
}
|
||||
|
||||
private void deadlock() {
|
||||
new Thread(() -> {
|
||||
synchronized (a) {
|
||||
try {
|
||||
Thread.sleep(1000);
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
synchronized (b) {
|
||||
}
|
||||
}
|
||||
}).start();
|
||||
new Thread(() -> {
|
||||
synchronized (b) {
|
||||
synchronized (a) {
|
||||
}
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
此时使用 jstack 分析就能很快的定位到问题所在,示例如下:
|
||||
|
||||
```shell
|
||||
jstack 8112
|
||||
```
|
||||
|
||||
输出结果如下:
|
||||
|
||||

|
||||
|
||||
从输出中结果中可以看出,出现了一个死锁,该死锁由线程 Thread-0 和 Thread-1 导致,原因是 Thread-0 锁住了对象 `<0x00000000d6d8d610>` ,并尝试获取 `<0x00000000d6d8d640>` 对象的锁;但是 Thread-0 却恰恰相反,锁住了对象 `<0x00000000d6d8d640>` ,并尝试获取 `<0x00000000d6d8d610>` 对象的锁,由此导致死锁。
|
||||
|
||||
|
||||
|
||||
## 参考资料
|
||||
|
||||
+ 主要参考自:周志明 . 深入理解Java虚拟机(第3版). 机械工业出版社 , 2019-12 ,想要深入了解虚拟机的话,推荐阅读原书。
|
||||
+ 官方文档:https://docs.oracle.com/javase/8/docs/technotes/tools/unix/s11-troubleshooting_tools.html#sthref327
|
@ -27,6 +27,8 @@ Mycat 是一个开源的数据库中间件,可以解决分布式数据库环
|
||||
- 支持使用 zookeeper 来协调主从切换、统一管理配置数据以及使用 zookeeper 来生成全局唯一 ID。( 1.6+ )
|
||||
- 提供了 Web 监控界面,来对 Mycat、MySQL 以及服务器的状态进行监控。
|
||||
|
||||
|
||||
|
||||
## 二、Mycat 核心概念
|
||||
|
||||
在引入 Mycat 后,所有的客户端请求需要经过中间件进行转发上,此时客户端直接面向的是 Mycat 上的逻辑库或逻辑表:
|
||||
@ -48,8 +50,26 @@ Mycat 是一个开源的数据库中间件,可以解决分布式数据库环
|
||||
|
||||
将表按照分片键进行分片后,一个表中的所有数据就会被分发到不同的数据库上,这些数据库节点就称为分片节点。
|
||||
|
||||
|
||||
|
||||
## 三、Mycat 安装
|
||||
|
||||
Mycat 所有版本的安装包都可以从 http://dl.mycat.io/ 上进行下载,下载后进行解压即可,其主要文件目录如下:
|
||||
|
||||
```shell
|
||||
mycat
|
||||
├── bin
|
||||
│ ├── mycat #mycat程序的执行脚本,如 mycat start 用于启动mycat服务,mycat stop用于停止mycat服务
|
||||
├── conf
|
||||
│ ├── rule.xml
|
||||
│ ├── schema.xml
|
||||
│ ├── server.xml
|
||||
├── logs #日志存放目录
|
||||
└── version.txt #版本信息
|
||||
```
|
||||
|
||||
|
||||
|
||||
## 四、Mycat 基本配置
|
||||
|
||||
在 Mycat 的安装目录的 `conf` 目录下,有以下三个核心配置文件:
|
||||
@ -187,6 +207,8 @@ rule.xml 文件中定义的是分片规则,主要包含以下标签:
|
||||
|
||||
Mycat 内置支持十几种分片算法,如 取模分片,枚举分片,范围分片,字符串 hash 分片,一致性 hash 分片,日期分片等。关于这些分片算法的详细说明可以参考官方文档:[Mycat 官方指南](http://www.mycat.io/document/mycat-definitive-guide.pdf)
|
||||
|
||||
|
||||
|
||||
## 五、Mycat 读写分离
|
||||
|
||||
Mycat 读写分离的配置非常简单,只需要通过配置 balance,writeHost 和 readHost 就可以实现,示例如下:
|
||||
@ -226,6 +248,8 @@ Mycat 读写分离的配置非常简单,只需要通过配置 balance,writeH
|
||||
|
||||
基于以上两个原因,如果想要实现高可用,并不建议配置多个 writeHost ,而是配置一个 writeHost ,但其指向的是虚拟的读 IP 地址,此时复制架构由 MMM 或者 MHA 架构来实现,并由它们来提供虚拟机的读 IP。
|
||||
|
||||
|
||||
|
||||
## 六、Mycat 分库分表
|
||||
|
||||
综合以上全部内容,这里给出一个分库分表的示例,其架构如下:
|
||||
|
312
notes/Tomcat_架构解析.md
Normal file
@ -0,0 +1,312 @@
|
||||
# Tomcat 架构解析
|
||||
|
||||
## 一、Tomcat 简介
|
||||
|
||||
Tomcat 是目前主流的基于 Java 语言的轻量级应用服务器,它是对是 Java Servlet,JavaServer Pages(JSP),Java Expression Language(EL 表达式)和 Java WebSocket 技术的开源实现。当前 Tomcat 共有四个版本:
|
||||
|
||||
+ **Tomcat 7**:支持 Servlet 3.0,JSP 2.2,EL 2.2 和 WebSocket 1.1 规范。
|
||||
+ **Tomcat 8.5**:支持 Servlet 3.1,JSP 2.3,EL 3.0 和 WebSocket 1.1 规范,并可以通过安装 Tomcat 原生库来支持 HTTP/2 。当前 Tomcat 8.5 已经完全取代了 Tomcat 8,Tomcat 8 已停止维护,并不再提供下载。
|
||||
+ **Tomcat 9**:是当前主要的发行版;它建立在 Tomcat 8.0.x 和 8.5.x 之上,并实现了 Servlet 4.0,JSP 2.3,EL 3.0,WebSocket 1.1 和 JASPIC 1.1 规范。
|
||||
+ **Tomcat 10 (alpha)** :是当前主要的开发版;它实现了 Servlet 5.0,JSP 3.0,EL 4.0 和 WebSocket 2.0 规范。
|
||||
|
||||
|
||||
|
||||
## 二、Tomcat 架构
|
||||
|
||||
Tomcat 的整体架构如下:
|
||||
|
||||

|
||||
|
||||
+ **Server**:表示整个 Servlet 容器,在整个 Tomcat 运行环境中只有唯一一个 Server 实例。一个 Server 包含多个 Service,每个 Service 互相独立,但共享一个 JVM 以及系统类库。
|
||||
+ **Service**:一个 Service 负责维护多个 Connector 和一个 Engine。其中 Connector 负责开启 Socket 并监听客户端请求,返回响应数据;Engine 负责具体的请求处理。
|
||||
+ **Connector**:连接器,用于监听并转换来自客户端 Socket 请求,然后将 Socket 请求交由 Container 处理,支持不同协议以及不同的 I/O 方式。
|
||||
+ **Engine**:表示整个 Servlet 引擎,在 Tomcat 中,Engine 为最高层级的容器对象。
|
||||
+ **Host**:表示 Engine 中的虚拟机,通常与一个服务器的网络名有关,如域名等。
|
||||
+ **Context**:表示 ServletContext ,在 Servlet 规范中,一个 ServletContext 即表示一个独立的 Web 应用。
|
||||
+ **Wrapper**:是对标准 Servlet 的封装。
|
||||
|
||||
以上各组件的详细介绍如下:
|
||||
|
||||
|
||||
|
||||
## 三、连接器
|
||||
|
||||
连接器的主要功能是将 Socket 的输入转换为 Request 对象,并交由容器进行处理;之后再将容器处理完成的 Response 对象写到输出流。连接器的内部组件如下:
|
||||
|
||||

|
||||
|
||||
### 3.1 ProtocolHandler
|
||||
|
||||
**1. Endpoint**
|
||||
|
||||
EndPoint 会启动线程来监听服务器端口,并负责处理来自客户端的 Socket 请求,是对传输层的抽象。它支持以下 IO 方式:
|
||||
|
||||
+ **BIO**:即最传统的 I/O 方式;
|
||||
+ **NIO**:采用 Java NIO 类库进行实现,Tomcat 8 之后默认该 I/O 方式,以替换原来的 BIO;
|
||||
+ **NIO2**:采用 JDK 7 最新的 NIO2 类库进行实现;
|
||||
+ **APR**:采用 APR (Apache 可移植运行库) 实现,APR 是使用 C/C++ 编写的本地库,需要单独进行安装。
|
||||
|
||||
**2. Processor**
|
||||
|
||||
负责构造 Request 和 Response 对象,并通过 Adapter 提交到容器进行处理,是对应用层的抽象。它支持以下应用层协议:
|
||||
|
||||
+ **HTTP / 1.1 协议**;
|
||||
+ **HTTP / 2.0 协议**:自 Tomcat 8.5 以及 9.0 版本后开始支持;
|
||||
+ **AJP 协议**:即定向包协议。
|
||||
|
||||
**3. ProtocolHandler**
|
||||
|
||||
ProtocolHandler 通过组合不同类型的 Endpoint 和 Processor 来实现针对具体协议的处理功能。按照不同的协议(HTTP 和 AJP)和不同的 I/O 方式(NIO,NIO2,AJP)进行组合,其有以下六个具体的实现类:
|
||||
|
||||

|
||||
|
||||
**4. 协议升级**
|
||||
|
||||
可以看到上面的 ProtocolHandler 只有对 HTTP 1.1 协议进行处理的实现类,并没有对 HTTP 2.0 进行处理的实现类,想要对 HTTP 2.0 进行处理,需要使用到协议升级:当 ProtocolHandler 收到的是一个 HTTP 2.0 请求时,它会根据请求创建一个用于升级处理的令牌 UpgradeToken,该令牌中包含了升级处理器 HttpUpgradeHandler(接口),对于 HTTP 2.0 而言,其实现类是 Http2UpgradeHandler。
|
||||
|
||||
### 3.2 Adapter
|
||||
|
||||
Tomcat 设计者希望连接器是一个单独的组件,能够脱离 Servlet 规范而独立存在,以便增加其使用场景,因此 Process 对输入流封装后得到的 Request 不是一个 Servlet Request,该 Request 的全限定命名为:org.apache.coyote.Request 。因此在这里需要使用适配器模式(具体实现类是 CoyoteAdapter)将其转换为 org.apache.catalina.connector.Request,它才是标准的 ServletRequest 的实现:
|
||||
|
||||

|
||||
|
||||
### 3.3 Mapper 和 MapperListener
|
||||
|
||||
由 Socket 输入流构建好标准的 ServletRequest 后,连接器还需要知道将 Request 发往哪一个容器,这需要通过 Mapper 来实现。Mapper 维护了请求路径与容器之间的映射信息。在 Tomcat 7 及之前的版本中 Mapper 由连接器自身维护,在 Tomcat 8 之后的版本中,Mapper 由 Service 进行维护。
|
||||
|
||||
MapperListener 实现了 ContainerListener 和 LifecycleListener 接口,用于在容器组件状态发生变更时,注册或取消对应容器的映射关系,这么做主要是为了支持 Tomcat 的热部署功能。
|
||||
|
||||
|
||||
|
||||
## 四、容器
|
||||
|
||||
### 4.1 Container 和 Lifecycle
|
||||
|
||||
Tomcat 中的所有容器都实现了 Container 接口,它定义了容器共同的属性与方法,而 Container 接口则继承自 Lifecycle 接口。Tomcat 中的大多数组件都实现了 Lifecycle 接口,它定义了与组件生命周期相关的公共方法,如 `init()`,`start()`,`stop()`,`destroy()` :
|
||||
|
||||
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
### 4.2 分层结构
|
||||
|
||||
Tomcat 之所以采用分层的结构,主要是为了更好的灵活性和可扩展性:
|
||||
|
||||
+ **Engine**:最顶层的容器,一个 Service 中只有一个 Engine;
|
||||
+ **Host**:代表一个虚拟主机,一个 Engine 可以包含多个虚拟主机;
|
||||
+ **Context**:表示一个具体的 Web 应用程序,一个虚拟主机可以包含多个 Context;
|
||||
+ **Wrapper**:是 Tomcat 对 Servlet 的包装,一个 Context 中可以有多个 Wrapper。
|
||||
|
||||

|
||||
|
||||
Tomcat 容器的分层结构在其 conf 目录下的 `server.xml` 配置文件中也有体现:
|
||||
|
||||
```xml
|
||||
<Server port="8005" shutdown="SHUTDOWN">
|
||||
<Service name="Catalina">
|
||||
<Connector port="8080" protocol="HTTP/1.1"connectionTimeout="20000"redirectPort="8443" />
|
||||
<Engine name="Catalina" defaultHost="localhost">
|
||||
<Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true">
|
||||
</Engine>
|
||||
</Service>
|
||||
</Server>
|
||||
```
|
||||
|
||||
这里的 appBase 代表我们应用程序所在父目录,我们部署的每一个应用程序就是一个独立的 Context 。
|
||||
|
||||
### 4.3 Pipeline 和 Valve
|
||||
|
||||

|
||||
|
||||
由连接器发过来的请求会最先发送到 Engine,最终逐层传递,直至我们编写的 Servlet,这种传递主要通过 Pipeline 和 Valve 来实现。每层容器都有自己的 Pipeline,Pipeline 相当于处理管道;每个 Pipeline 中有一个 Valve 链,每个 Valve 可以看做一个独立的处理单元,用于对请求进行处理。最基础的 Valve 叫做 Basic Valve,新增的 Valve 会位于已有的 Valve 之前。Pipeline 和 Valve 的接口定义如下:
|
||||
|
||||
```java
|
||||
public interface Pipeline extends Contained {
|
||||
|
||||
public Valve getBasic(); // 获得Basic Valve
|
||||
public void setBasic(Valve valve); // 设置Basic Valve
|
||||
public void addValve(Valve valve); // 新增Valve
|
||||
public Valve[] getValves(); // 获取所有Valve
|
||||
public void removeValve(Valve valve);// 移除Valve
|
||||
public Valve getFirst(); //获取第一个 Valve
|
||||
|
||||
public boolean isAsyncSupported();
|
||||
public void findNonAsyncValves(Set<String> result);
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
public interface Valve {
|
||||
|
||||
public Valve getNext();
|
||||
// 每一个Valve都持有其下一个Valve,这是标准的责任链模式
|
||||
public void setNext(Valve valve);
|
||||
// 对请求进行检查、处理或增强
|
||||
public void invoke(Request request, Response response) throws IOException, ServletException;
|
||||
public void backgroundProcess();
|
||||
public boolean isAsyncSupported();
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
通过 Pipeline 的 Valve 责任链模式,每一层容器都可以很方便地进行功能的扩展,来对请求进行检查、处理或增强。每一层处理完成后,就会传递到下一层的 First Valve,由下一层进行处理。以 Engine 容器为例,其实现类为 StandardEngine:
|
||||
|
||||
```java
|
||||
public class StandardEngine extends ContainerBase implements Engine {
|
||||
public StandardEngine() {
|
||||
super();
|
||||
pipeline.setBasic(new StandardEngineValve());
|
||||
....
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
在 StandardEngine 创建时就会为其 Pipeline 设置上一个名为 StandardEngineValve 的 Basic Valve,StandardEngineValve 的实现如下:
|
||||
|
||||
```java
|
||||
final class StandardEngineValve extends ValveBase {
|
||||
|
||||
public StandardEngineValve() {super(true);}
|
||||
|
||||
@Override
|
||||
public final void invoke(Request request, Response response) throws IOException, ServletException {
|
||||
|
||||
// 获取当前请求的Host
|
||||
Host host = request.getHost();
|
||||
if (host == null) {
|
||||
return;
|
||||
}
|
||||
if (request.isAsyncSupported()) {
|
||||
request.setAsyncSupported(host.getPipeline().isAsyncSupported());
|
||||
}
|
||||
|
||||
// 将请求传递给host的Pipeline的第一个Valve
|
||||
host.getPipeline().getFirst().invoke(request, response);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Engine 的 Basic Valve(即最后一个 Valve)在 `invoke` 方法中会获取到下一级容器(Host)的第一个 Valve,从而完成首尾相接。
|
||||
|
||||
### 4.4 FilterChain
|
||||
|
||||
通过 Pipeline 和 Valve 的传递,请求最终会传递到最内层容器 Wrapper 的 Basic Valve,其实现类为 StandardWrapperValve 。StandardWrapperValve 会在 `invoke` 方法中为该请求创建 FilterChain,依次执行请求对应的过滤器:
|
||||
|
||||
```java
|
||||
// 为该请求创建Filter Chain
|
||||
ApplicationFilterChain filterChain = ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);
|
||||
.....
|
||||
// 调用Filter Chain的doFilter方法
|
||||
filterChain.doFilter(request.getRequest(), response.getResponse());
|
||||
```
|
||||
|
||||
当到达执行链的末端后,会执行 servlet 的 service 方法:
|
||||
|
||||
```java
|
||||
servlet.service(request, response);
|
||||
```
|
||||
|
||||
以我们最常使用的 HttpServlet 为例,其最终的 service 方法如下:
|
||||
|
||||
```shell
|
||||
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
|
||||
String method = req.getMethod();
|
||||
|
||||
if (method.equals(METHOD_GET)) {
|
||||
long lastModified = getLastModified(req);
|
||||
if (lastModified == -1) {
|
||||
.....
|
||||
doGet(req, resp);
|
||||
} else {
|
||||
......
|
||||
}
|
||||
|
||||
} else if (method.equals(METHOD_HEAD)) {
|
||||
long lastModified = getLastModified(req);
|
||||
maybeSetLastModified(resp, lastModified);
|
||||
doHead(req, resp);
|
||||
|
||||
} else if (method.equals(METHOD_POST)) {
|
||||
doPost(req, resp);
|
||||
|
||||
} else if (method.equals(METHOD_PUT)) {
|
||||
doPut(req, resp);
|
||||
|
||||
} else if (method.equals(METHOD_DELETE)) {
|
||||
doDelete(req, resp);
|
||||
|
||||
} else if (method.equals(METHOD_OPTIONS)) {
|
||||
doOptions(req,resp);
|
||||
|
||||
} else if (method.equals(METHOD_TRACE)) {
|
||||
doTrace(req,resp);
|
||||
|
||||
} ......
|
||||
}
|
||||
```
|
||||
|
||||
至此,来自客户端的请求就逐步传递到我们编写的 doGet 或者 doPost 方法中。
|
||||
|
||||
|
||||
|
||||
## 五、请求流程
|
||||
|
||||
这里对前面的连接器和容器章节进行总结,Tomcat 对客户端请求的完整处理流程如下:
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
## 六、启动流程
|
||||
|
||||
Tomcat 整体的启动流程如下图所示:
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
#### 1. startup.sh & catalina.sh
|
||||
|
||||
`startup.sh` 是对 `catalina.sh` 的一层薄封装,主要用于检查 `catalina.sh` 是否存在以及调用它。 `catalina.sh` 负责启动一个 JVM 来运行 Tomcat 的启动类 Bootstrap 。
|
||||
|
||||
#### 2. Bootstrap
|
||||
|
||||
Bootstrap 独立于 Tomcat 结构之外,它以 JAR 包的形式存在于 `bin` 目录下,主要负责初始化 Tomcat 的类加载器,并通过反射来创建 Catalina。
|
||||
|
||||
#### 3. Catalina
|
||||
|
||||
Catalina 通过 Digester 解析 server.xml 来创建所有的服务组件。Digester 是一款能将 XML 转换为 Java 对象的事件驱动型工具,简而言之,它通过流读取 XML 文件,当识别出特定 XML 节点后,就会创建对应的组件。
|
||||
|
||||
|
||||
|
||||
## 七、类加载器
|
||||
|
||||
Tomcat 并没有完全沿用 JVM 默认的类加载机制,为了保证 Web 应用之间的隔离性和加载的灵活性,其采用了下图所示的类加载机制:
|
||||
|
||||

|
||||
|
||||
#### 1. Web App Class Loader
|
||||
|
||||
负责加载 `/WEB-INF/classes` 目录下的未压缩的 Class 和资源文件,以及 `/WEB-INF/lib` 目录下的 Jar 包。它只对当前的 Web 应用可见,对其它 Web 应用均不可见,因此它可以保证 Web 应用之间的彼此隔离。
|
||||
|
||||
#### 2. Shared Class Loader
|
||||
|
||||
是所有 Web 应用的父类加载器,它负责加载 Web 应用之间共享的类,从而避免资源的重复加载。
|
||||
|
||||
#### 3. Catalina Class Loader
|
||||
|
||||
用于加载 Tomcat 应用服务器的类加载器,从而保证 Tomcat 与 Web 应用程序之间的隔离。
|
||||
|
||||
#### 4. Common Class Loader
|
||||
|
||||
其作用和 Shared Class Loader 类似,当 Tomcat 与 Web 应用程序之间存在共同依赖时,可以使用其进行加载。再往上,流程就与 JVM 类加载的流程一致了。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## 参考资料
|
||||
|
||||
+ 刘光瑞 . Tomcat架构解析 . 人民邮电出版社 . 2017-05
|
BIN
pictures/jconsole-start.png
Normal file
After Width: | Height: | Size: 28 KiB |
BIN
pictures/jconsole-内存.png
Normal file
After Width: | Height: | Size: 31 KiB |
BIN
pictures/jconsole-检测死锁.png
Normal file
After Width: | Height: | Size: 31 KiB |
BIN
pictures/jconsole-概要.png
Normal file
After Width: | Height: | Size: 40 KiB |
BIN
pictures/jconsole-概览.png
Normal file
After Width: | Height: | Size: 30 KiB |
BIN
pictures/jconsole-死锁.png
Normal file
After Width: | Height: | Size: 25 KiB |
BIN
pictures/jconsole-类.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
pictures/jhat.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
pictures/jhat_web.png
Normal file
After Width: | Height: | Size: 54 KiB |
BIN
pictures/jinfo.png
Normal file
After Width: | Height: | Size: 39 KiB |
BIN
pictures/jmap.png
Normal file
After Width: | Height: | Size: 5.0 KiB |
BIN
pictures/jprofiler-1.png
Normal file
After Width: | Height: | Size: 23 KiB |
BIN
pictures/jprofiler-2.png
Normal file
After Width: | Height: | Size: 88 KiB |
BIN
pictures/jstack.png
Normal file
After Width: | Height: | Size: 48 KiB |
BIN
pictures/jstat_gc.png
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
pictures/jvisual-dump.png
Normal file
After Width: | Height: | Size: 42 KiB |
BIN
pictures/jvisual-gc.png
Normal file
After Width: | Height: | Size: 42 KiB |
BIN
pictures/jvisual-堆dump.png
Normal file
After Width: | Height: | Size: 47 KiB |
BIN
pictures/jvisual-性能分析.png
Normal file
After Width: | Height: | Size: 50 KiB |
BIN
pictures/jvisual-插件中心.png
Normal file
After Width: | Height: | Size: 72 KiB |
BIN
pictures/jvisual-插件安装.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
pictures/jvisual-监视.png
Normal file
After Width: | Height: | Size: 37 KiB |
BIN
pictures/jvisual-线程.png
Normal file
After Width: | Height: | Size: 40 KiB |
BIN
pictures/jvisual-连接远程主机.png
Normal file
After Width: | Height: | Size: 24 KiB |
BIN
pictures/jvisual-配置插件中心.png
Normal file
After Width: | Height: | Size: 27 KiB |
BIN
pictures/jvisual.png
Normal file
After Width: | Height: | Size: 19 KiB |
BIN
pictures/tomcat_AbstractProtocol.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
pictures/tomcat_container.png
Normal file
After Width: | Height: | Size: 6.5 KiB |
BIN
pictures/tomcat_request.png
Normal file
After Width: | Height: | Size: 5.4 KiB |
BIN
pictures/tomcat_分层结构.png
Normal file
After Width: | Height: | Size: 5.2 KiB |
BIN
pictures/tomcat_多层容器.jpg
Normal file
After Width: | Height: | Size: 68 KiB |
BIN
pictures/tomcat_架构.png
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
pictures/tomcat_类加载器.jpg
Normal file
After Width: | Height: | Size: 66 KiB |
BIN
pictures/tomcat启动流程.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
pictures/tomcat启动请求处理流程.jpg
Normal file
After Width: | Height: | Size: 164 KiB |
BIN
pictures/tomcat连接器组件.png
Normal file
After Width: | Height: | Size: 14 KiB |