This commit is contained in:
louzefeng
2024-07-11 05:50:32 +00:00
parent bf99793fd0
commit d3828a7aee
6071 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,141 @@
<audio id="audio" title="34 | JVM GC原理及调优的基本思路" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/d8/18/d86ba52c6bbb5e4d5e369cea04ae4118.mp3"></audio>
和Web应用程序一样Tomcat作为一个Java程序也跑在JVM中因此如果我们要对Tomcat进行调优需要先了解JVM调优的原理。而对于JVM调优来说主要是JVM垃圾收集的优化一般来说是因为有问题才需要优化所以对于JVM GC来说如果你观察到Tomcat进程的CPU使用率比较高并且在GC日志中发现GC次数比较频繁、GC停顿时间长这表明你需要对GC进行优化了。
在对GC调优的过程中我们不仅需要知道GC的原理更重要的是要熟练使用各种监控和分析工具具备GC调优的实战能力。CMS和G1是时下使用率比较高的两款垃圾收集器从Java 9开始采用G1作为默认垃圾收集器而G1的目标也是逐步取代CMS。所以今天我们先来简单回顾一下两种垃圾收集器CMS和G1的区别接着通过一个例子帮你提高GC调优的实战能力。
## CMS vs G1
CMS收集器将Java堆分为**年轻代**Young或**年老代**Old。这主要是因为有研究表明超过90的对象在第一次GC时就被回收掉但是少数对象往往会存活较长的时间。
CMS还将年轻代内存空间分为**幸存者空间**Survivor和**伊甸园空间**Eden。新的对象始终在Eden空间上创建。一旦一个对象在一次垃圾收集后还幸存就会被移动到幸存者空间。当一个对象在多次垃圾收集之后还存活时它会移动到年老代。这样做的目的是在年轻代和年老代采用不同的收集算法以达到较高的收集效率比如在年轻代采用复制-整理算法,在年老代采用标记-清理算法。因此CMS将Java堆分成如下区域
<img src="https://static001.geekbang.org/resource/image/8a/7a/8a4e63a4dc5c7f1c0ba19afd748aee7a.png" alt="">
与CMS相比G1收集器有两大特点
- G1可以并发完成大部分GC的工作这期间不会“Stop-The-World”。
- G1使用**非连续空间**这使G1能够有效地处理非常大的堆。此外G1可以同时收集年轻代和年老代。G1并没有将Java堆分成三个空间Eden、Survivor和Old而是将堆分成许多通常是几百个非常小的区域。这些区域是固定大小的默认情况下大约为2MB。每个区域都分配给一个空间。 G1收集器的Java堆如下图所示
<img src="https://static001.geekbang.org/resource/image/14/9e/14fed64d57fc1e56bdcd472440444d9e.png" alt="">
图上的U表示“未分配”区域。G1将堆拆分成小的区域一个最大的好处是可以做局部区域的垃圾回收而不需要每次都回收整个区域比如年轻代和年老代这样回收的停顿时间会比较短。具体的收集过程是
- 将所有存活的对象将从**收集的区域**复制到**未分配的区域**比如收集的区域是Eden空间把Eden中的存活对象复制到未分配区域这个未分配区域就成了Survivor空间。理想情况下如果一个区域全是垃圾意味着一个存活的对象都没有则可以直接将该区域声明为“未分配”。
- 为了优化收集时间G1总是优先选择垃圾最多的区域从而最大限度地减少后续分配和释放堆空间所需的工作量。这也是G1收集器名字的由来——Garbage-First。
## GC调优原则
GC是有代价的因此我们调优的根本原则是**每一次GC都回收尽可能多的对象**也就是减少无用功。因此我们在做具体调优的时候针对CMS和G1两种垃圾收集器分别有一些相应的策略。
**CMS收集器**
对于CMS收集器来说最重要的是**合理地设置年轻代和年老代的大小**。年轻代太小的话会导致频繁的Minor GC并且很有可能存活期短的对象也不能被回收GC的效率就不高。而年老代太小的话容纳不下从年轻代过来的新对象会频繁触发单线程Full GC导致较长时间的GC暂停影响Web应用的响应时间。
**G1收集器**
对于G1收集器来说我不推荐直接设置年轻代的大小这一点跟CMS收集器不一样这是因为G1收集器会根据算法动态决定年轻代和年老代的大小。因此对于G1收集器我们需要关心的是Java堆的总大小`-Xmx`)。
此外G1还有一个较关键的参数是`-XX:MaxGCPauseMillis = n`这个参数是用来限制最大的GC暂停时间目的是尽量不影响请求处理的响应时间。G1将根据先前收集的信息以及检测到的垃圾量估计它可以立即收集的最大区域数量从而尽量保证GC时间不会超出这个限制。因此G1相对来说更加“智能”使用起来更加简单。
## 内存调优实战
下面我通过一个例子实战一下Java堆设置得过小导致频繁的GC我们将通过GC日志分析工具来观察GC活动并定位问题。
1.首先我们建立一个Spring Boot程序作为我们的调优对象代码如下
```
@RestController
public class GcTestController {
private Queue&lt;Greeting&gt; objCache = new ConcurrentLinkedDeque&lt;&gt;();
@RequestMapping(&quot;/greeting&quot;)
public Greeting greeting() {
Greeting greeting = new Greeting(&quot;Hello World!&quot;);
if (objCache.size() &gt;= 200000) {
objCache.clear();
} else {
objCache.add(greeting);
}
return greeting;
}
}
@Data
@AllArgsConstructor
class Greeting {
private String message;
}
```
上面的代码就是创建了一个对象池当对象池中的对象数到达200000时才清空一次用来模拟年老代对象。
2.用下面的命令启动测试程序:
```
java -Xmx32m -Xss256k -verbosegc -Xlog:gc*,gc+ref=debug,gc+heap=debug,gc+age=trace:file=gc-%p-%t.log:tags,uptime,time,level:filecount=2,filesize=100m -jar target/demo-0.0.1-SNAPSHOT.jar
```
我给程序设置的堆的大小为32MB目的是能让我们看到Full GC。除此之外我还打开了verbosegc日志请注意这里我使用的版本是Java 12默认的垃圾收集器是G1。
3.使用JMeter压测工具向程序发送测试请求访问的路径是`/greeting`
<img src="https://static001.geekbang.org/resource/image/bd/85/bd3a55b83f85b3c6a050cbe7aa288485.png" alt="">
4.使用GCViewer工具打开GC日志我们可以看到这样的图
<img src="https://static001.geekbang.org/resource/image/7a/a2/7aab9535570082e1dd19c158012e05a2.png" alt="">
我来解释一下这张图:
- 图中上部的蓝线表示已使用堆的大小我们看到它周期的上下震荡这是我们的对象池要扩展到200000才会清空。
- 图底部的绿线表示年轻代GC活动从图上看到当堆的使用率上去了会触发频繁的GC活动。
- 图中的竖线表示Full GC从图上看到伴随着Full GC蓝线会下降这说明Full GC收集了年老代中的对象。
基于上面的分析我们可以得出一个结论那就是Java堆的大小不够。我来解释一下为什么得出这个结论
- GC活动频繁年轻代GC绿色线和年老代GC黑色线都比较密集。这说明内存空间不够也就是Java堆的大小不够。
- Java的堆中对象在GC之后能够被回收说明不是内存泄漏。
我们通过GCViewer还发现累计GC暂停时间有55.57秒,如下图所示:
<img src="https://static001.geekbang.org/resource/image/2a/06/2a0dddc7e9fc5c61339e5d515c449806.png" alt="">
因此我们的解决方案是调大Java堆的大小像下面这样
```
java -Xmx2048m -Xss256k -verbosegc -Xlog:gc*,gc+ref=debug,gc+heap=debug,gc+age=trace:file=gc-%p-%t.log:tags,uptime,time,level:filecount=2,filesize=100m -jar target/demo-0.0.1-SNAPSHOT.jar
```
生成的新的GC log分析图如下
<img src="https://static001.geekbang.org/resource/image/30/99/3027354c1ae0b359dab025c53b297599.png" alt="">
你可以看到没有发生Full GC并且年轻代GC也没有那么频繁了并且累计GC暂停时间只有3.05秒。
<img src="https://static001.geekbang.org/resource/image/9f/1b/9f1b3655cebf6e8f40148dfa6d6c111b.png" alt="">
## 本期精华
今天我们首先回顾了CMS和G1两种垃圾收集器背后的设计思路以及它们的区别接着分析了GC调优的总体原则。
对于CMS来说我们要合理设置年轻代和年老代的大小。你可能会问该如何确定它们的大小呢这是一个迭代的过程可以先采用JVM的默认值然后通过压测分析GC日志。
如果我们看年轻代的内存使用率处在高位导致频繁的Minor GC而频繁GC的效率又不高说明对象没那么快能被回收这时年轻代可以适当调大一点。
如果我们看年老代的内存使用率处在高位导致频繁的Full GC这样分两种情况如果每次Full GC后年老代的内存占用率没有下来可以怀疑是内存泄漏如果Full GC后年老代的内存占用率下来了说明不是内存泄漏我们要考虑调大年老代。
对于G1收集器来说我们可以适当调大Java堆因为G1收集器采用了局部区域收集策略单次垃圾收集的时间可控可以管理较大的Java堆。
## 课后思考
如果把年轻代和年老代都设置得很大,会有什么问题?
不知道今天的内容你消化得如何?如果还有疑问,请大胆的在留言区提问,也欢迎你把你的课后思考和心得记录下来,与我和其他同学一起讨论。如果你觉得今天有所收获,欢迎你把它分享给你的朋友。

View File

@@ -0,0 +1,181 @@
<audio id="audio" title="35 | 如何监控Tomcat的性能" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/a0/51/a0fe8c6f6ec246d1b3e9ba089e64ca51.mp3"></audio>
专栏上一期我们分析了JVM GC的基本原理以及监控和分析工具今天我们接着来聊如何监控Tomcat的各种指标因为只有我们掌握了这些指标和信息才能对Tomcat内部发生的事情一目了然让我们明白系统的瓶颈在哪里进而做出调优的决策。
在今天的文章里我们首先来看看到底都需要监控Tomcat哪些关键指标接着来具体学习如何通过JConsole来监控它们。如果系统没有暴露JMX接口我们还可以通过命令行来查看Tomcat的性能指标。
Web应用的响应时间是我们关注的一个重点最后我们通过一个实战案例来看看Web应用的下游服务响应时间比较长的情况下Tomcat的各项指标是什么样子的。
## Tomcat的关键指标
Tomcat的关键指标有**吞吐量、响应时间、错误数、线程池、CPU以及JVM内存**。
我来简单介绍一下这些指标背后的意义。其中前三个指标是我们最关心的业务指标Tomcat作为服务器就是要能够又快有好地处理请求因此吞吐量要大、响应时间要短并且错误数要少。
而后面三个指标是跟系统资源有关的当某个资源出现瓶颈就会影响前面的业务指标比如线程池中的线程数量不足会影响吞吐量和响应时间但是线程数太多会耗费大量CPU也会影响吞吐量当内存不足时会触发频繁地GC耗费CPU最后也会反映到业务指标上来。
那如何监控这些指标呢Tomcat可以通过JMX将上述指标暴露出来的。JMXJava Management Extensions即Java管理扩展是一个为应用程序、设备、系统等植入监控管理功能的框架。JMX使用管理MBean来监控业务资源这些MBean在JMX MBean服务器上注册代表JVM中运行的应用程序或服务。每个MBean都有一个属性列表。JMX客户端可以连接到MBean Server来读写MBean的属性值。你可以通过下面这张图来理解一下JMX的工作原理
<img src="https://static001.geekbang.org/resource/image/71/6b/714fa2e12380122599be077c10375a6b.png" alt="">
Tomcat定义了一系列MBean来对外暴露系统状态接下来我们来看看如何通过JConsole来监控这些指标。
## 通过JConsole监控Tomcat
首先我们需要开启JMX的远程监听端口具体来说就是设置若干JVM参数。我们可以在Tomcat的bin目录下新建一个名为`setenv.sh`的文件(或者`setenv.bat`,根据你的操作系统类型),然后输入下面的内容:
```
export JAVA_OPTS=&quot;${JAVA_OPTS} -Dcom.sun.management.jmxremote&quot;
export JAVA_OPTS=&quot;${JAVA_OPTS} -Dcom.sun.management.jmxremote.port=9001&quot;
export JAVA_OPTS=&quot;${JAVA_OPTS} -Djava.rmi.server.hostname=x.x.x.x&quot;
export JAVA_OPTS=&quot;${JAVA_OPTS} -Dcom.sun.management.jmxremote.ssl=false&quot;
export JAVA_OPTS=&quot;${JAVA_OPTS} -Dcom.sun.management.jmxremote.authenticate=false&quot;
```
重启Tomcat这样JMX的监听端口9001就开启了接下来通过JConsole来连接这个端口。
```
jconsole x.x.x.x:9001
```
我们可以看到JConsole的主界面
<img src="https://static001.geekbang.org/resource/image/80/d7/80f14c4bd4eead05f4d84937ed7726d7.png" alt="">
前面我提到的需要监控的关键指标有**吞吐量、响应时间、错误数、线程池、CPU以及JVM内存**接下来我们就来看看怎么在JConsole上找到这些指标。
**吞吐量、响应时间、错误数**
在MBeans标签页下选择GlobalRequestProcessor这里有Tomcat请求处理的统计信息。你会看到Tomcat中的各种连接器展开“http-nio-8080”你会看到这个连接器上的统计信息其中maxTime表示最长的响应时间processingTime表示平均响应时间requestCount表示吞吐量errorCount就是错误数。
<img src="https://static001.geekbang.org/resource/image/ff/9c/ff0a9163fbeeed8eb84ed89c3f71799c.png" alt="">
**线程池**
选择“线程”标签页可以看到当前Tomcat进程中有多少线程如下图所示
<img src="https://static001.geekbang.org/resource/image/78/b8/78858a3264107e1f1a0b6a78d43113b8.png" alt="">
图的左下方是线程列表右边是线程的运行栈这些都是非常有用的信息。如果大量线程阻塞通过观察线程栈能看到线程阻塞在哪个函数有可能是I/O等待或者是死锁。
**CPU**
在主界面可以找到CPU使用率指标请注意这里的CPU使用率指的是Tomcat进程占用的CPU不是主机总的CPU使用率。
<img src="https://static001.geekbang.org/resource/image/1b/9f/1bb88cc3d5f2b9a80377a29b0b80e19f.png" alt="">
**JVM内存**
选择“内存”标签页你能看到Tomcat进程的JVM内存使用情况。
<img src="https://static001.geekbang.org/resource/image/f2/02/f22eca547ca76eb5edba03b39082fa02.png" alt="">
你还可以查看JVM各内存区域的使用情况大的层面分堆区和非堆区。堆区里有分为Eden、Survivor和Old。选择“VM Summary”标签可以看到虚拟机内的详细信息。
<img src="https://static001.geekbang.org/resource/image/cc/a0/cc91bb5b7b8d8b0b46dad946617b01a0.png" alt="">
## 命令行查看Tomcat指标
极端情况下如果Web应用占用过多CPU或者内存又或者程序中发生了死锁导致Web应用对外没有响应监控系统上看不到数据这个时候需要我们登陆到目标机器通过命令行来查看各种指标。
1.首先我们通过ps命令找到Tomcat进程拿到进程ID。
<img src="https://static001.geekbang.org/resource/image/84/9a/8477832ebe079cf92e3cd58766754e9a.png" alt="">
2.接着查看进程状态的大致信息,通过`cat/proc/&lt;pid&gt;/status`命令:
<img src="https://static001.geekbang.org/resource/image/d8/b3/d812ac93be2ac882e689f77e5d8e12b3.png" alt="">
3.监控进程的CPU和内存资源使用情况
<img src="https://static001.geekbang.org/resource/image/6e/d6/6e7e0730e92e3d8846f37ea8a14973d6.png" alt="">
4.查看Tomcat的网络连接比如Tomcat在8080端口上监听连接请求通过下面的命令查看连接列表
<img src="https://static001.geekbang.org/resource/image/14/61/14ba365d585f0ec79543efc1a9b32961.png" alt="">
你还可以分别统计处在“已连接”状态和“TIME_WAIT”状态的连接数
<img src="https://static001.geekbang.org/resource/image/aa/2c/aaab0fb1156bb8ec92ee6c02b149cf2c.jpg" alt="">
5.通过ifstat来查看网络流量大致可以看出Tomcat当前的请求数和负载状况。
<img src="https://static001.geekbang.org/resource/image/67/b2/67a9a29b8bf071152ccc1bc108adc4b2.png" alt="">
## 实战案例
在这个实战案例中我们会创建一个Web应用根据传入的参数latency来休眠相应的秒数目的是模拟当前的Web应用在访问下游服务时遇到的延迟。然后用JMeter来压测这个服务通过JConsole来观察Tomcat的各项指标分析和定位问题。
主要的步骤有:
1.创建一个Spring Boot程序加入下面代码所示的一个RestController
```
@RestController
public class DownStreamLatency {
@RequestMapping(&quot;/greeting/latency/{seconds}&quot;)
public Greeting greeting(@PathVariable long seconds) {
try {
Thread.sleep(seconds * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Greeting greeting = new Greeting(&quot;Hello World!&quot;);
return greeting;
}
}
```
从上面的代码我们看到程序会读取URL传过来的seconds参数先休眠相应的秒数再返回请求。这样做的目的是客户端压测工具能够控制服务端的延迟。
为了方便观察Tomcat的线程数跟延迟之间的关系还需要加大Tomcat的最大线程数我们可以在`application.properties`文件中加入这样一行:
```
server.tomcat.max-threads=1000server.tomcat.max-threads=1000
```
2.启动JMeter开始压测这里我们将压测的线程数设置为100
<img src="https://static001.geekbang.org/resource/image/89/51/895c7c986d0c86b0a869a604c9c6af51.png" alt="">
请你注意的是我们还需要将客户端的Timeout设置为1000毫秒这是因为JMeter的测试线程在收到响应之前不会发出下一次请求这就意味我们没法按照固定的吞吐量向服务端加压。而加了Timeout以后JMeter会有固定的吞吐量向Tomcat发送请求。
<img src="https://static001.geekbang.org/resource/image/3e/a5/3e275c10132680995ac83460f29a1aa5.png" alt="">
3.开启测试这里分三个阶段第一个阶段将服务端休眠时间设为2秒然后暂停一段时间。第二和第三阶段分别将休眠时间设置成4秒和6秒。
<img src="https://static001.geekbang.org/resource/image/8b/0d/8b071d99cbf8875c138f14bd17f0ed0d.png" alt="">
4.最后我们通过JConsole来观察结果
<img src="https://static001.geekbang.org/resource/image/dd/b3/ddb8609042469ee73ddc3fb68f676eb3.png" alt="">
下面我们从线程数、内存和CPU这三个指标来分析Tomcat的性能问题。
- 首先看线程数在第一阶段时间之前线程数大概是40第一阶段压测开始后线程数增长到250。为什么是250呢这是因为JMeter每秒会发出100个请求每一个请求休眠2秒因此Tomcat需要200个工作线程来干活此外Tomcat还有一些其他线程用来处理网络通信和后台任务所以总数是250左右。第一阶段压测暂停后线程数又下降到40这是因为线程池会回收空闲线程。第二阶段测试开始后线程数涨到了420这是因为每个请求休眠了4秒同理我们看到第三阶段测试的线程数是620。
- 我们再来看CPU在三个阶段的测试中CPU的峰值始终比较稳定这是因为JMeter控制了总体的吞吐量因为服务端用来处理这些请求所需要消耗的CPU基本也是一样的。
- 各测试阶段的内存使用量略有增加,这是因为线程数增加了,创建线程也需要消耗内存。
从上面的测试结果我们可以得出一个结论对于一个Web应用来说下游服务的延迟越大Tomcat所需要的线程数越多但是CPU保持稳定。所以如果你在实际工作碰到线程数飙升但是CPU没有增加的情况这个时候你需要怀疑你的Web应用所依赖的下游服务是不是出了问题响应时间是否变长了。
## 本期精华
今天我们学习了Tomcat中的关键的性能指标以及如何监控这些指标主要有**吞吐量、响应时间、错误数、线程池、CPU以及JVM内存。**
在实际工作中我们需要通过观察这些指标来诊断系统遇到的性能问题找到性能瓶颈。如果我们监控到CPU上升这时我们可以看看吞吐量是不是也上升了如果是那说明正常如果不是的话可以看看GC的活动如果GC活动频繁并且内存居高不下基本可以断定是内存泄漏。
## 课后思考
请问工作中你如何监控Web应用的健康状态遇到性能问题的时候是如何做问题定位的呢
不知道今天的内容你消化得如何?如果还有疑问,请大胆的在留言区提问,也欢迎你把你的课后思考和心得记录下来,与我和其他同学一起讨论。如果你觉得今天有所收获,欢迎你把它分享给你的朋友。

View File

@@ -0,0 +1,89 @@
<audio id="audio" title="36 | Tomcat I/O和线程池的并发调优" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/aa/1c/aa656aaf89a325e2350892a86abbe11c.mp3"></audio>
上一期我们谈到了如何监控Tomcat的性能指标在这个基础上今天我们接着聊如何对Tomcat进行调优。
Tomcat的调优涉及I/O模型和线程池调优、JVM内存调优以及网络优化等今天我们来聊聊I/O模型和线程池调优由于Web应用程序跑在Tomcat的工作线程中因此Web应用对请求的处理时间也直接影响Tomcat整体的性能而Tomcat和Web应用在运行过程中所用到的资源都来自于操作系统因此调优需要将服务端看作是一个整体来考虑。
所谓的I/O调优指的是选择NIO、NIO.2还是APR而线程池调优指的是给Tomcat的线程池设置合适的参数使得Tomcat能够又快又好地处理请求。
## I/O模型的选择
I/O调优实际上是连接器类型的选择一般情况下默认都是NIO在绝大多数情况下都是够用的除非你的Web应用用到了TLS加密传输而且对性能要求极高这个时候可以考虑APR因为APR通过OpenSSL来处理TLS握手和加/解密。OpenSSL本身用C语言实现它还对TLS通信做了优化所以性能比Java要高。
那你可能会问那什么时候考虑选择NIO.2我的建议是如果你的Tomcat跑在Windows平台上并且HTTP请求的数据量比较大可以考虑NIO.2这是因为Windows从操作系统层面实现了真正意义上的异步I/O如果传输的数据量比较大异步I/O的效果就能显现出来。
如果你的Tomcat跑在Linux平台上建议使用NIO这是因为Linux内核没有很完善地支持异步I/O模型因此JVM并没有采用原生的Linux异步I/O而是在应用层面通过epoll模拟了异步I/O模型只是Java NIO的使用者感觉不到而已。因此可以这样理解在Linux平台上Java NIO和Java NIO.2底层都是通过epoll来实现的但是Java NIO更加简单高效。
## 线程池调优
跟I/O模型紧密相关的是线程池线程池的调优就是设置合理的线程池参数。我们先来看看Tomcat线程池中有哪些关键参数
<img src="https://static001.geekbang.org/resource/image/a4/e2/a48106c3893862fc826e7b7ffaa461e2.jpg" alt="">
这里面最核心的就是如何确定maxThreads的值如果这个参数设置小了Tomcat会发生线程饥饿并且请求的处理会在队列中排队等待导致响应时间变长如果maxThreads参数值过大同样也会有问题因为服务器的CPU的核数有限线程数太多会导致线程在CPU上来回切换耗费大量的切换开销。
那maxThreads设置成多少才算是合适呢为了理解清楚这个问题我们先来看看什么是利特尔法则Littles Law
**利特尔法则**
>
系统中的请求数 = 请求的到达速率 × 每个请求处理时间
其实这个公式很好理解,我举个我们身边的例子:我们去超市购物结账需要排队,但是你是如何估算一个队列有多长呢?队列中如果每个人都买很多东西,那么结账的时间就越长,队列也会越长;同理,短时间一下有很多人来收银台结账,队列也会变长。因此队列的长度等于新人加入队列的频率乘以平均每个人处理的时间。
**计算出了队列的长度,那么我们就创建相应数量的线程来处理请求,这样既能以最快的速度处理完所有请求,同时又没有额外的线程资源闲置和浪费。**
假设一个单核服务器在接收请求:
- 如果每秒10个请求到达平均处理一个请求需要1秒那么服务器任何时候都有10个请求在处理即需要10个线程。
- 如果每秒10个请求到达平均处理一个请求需要2秒那么服务器在每个时刻都有20个请求在处理因此需要20个线程。
- 如果每秒10000个请求到达平均处理一个请求需要1秒那么服务器在每个时刻都有10000个请求在处理因此需要10000个线程。
因此可以总结出一个公式:
**线程池大小 = 每秒请求数 × 平均请求处理时间**
这是理想的情况也就是说线程一直在忙着干活没有被阻塞在I/O等待上。实际上任务在执行中线程不可避免会发生阻塞比如阻塞在I/O等待上等待数据库或者下游服务的数据返回虽然通过非阻塞I/O模型可以减少线程的等待但是数据在用户空间和内核空间拷贝过程中线程还是阻塞的。线程一阻塞就会让出CPU线程闲置下来就好像工作人员不可能24小时不间断地处理客户的请求解决办法就是增加工作人员的数量一个人去休息另一个人再顶上。对应到线程池就是增加线程数量因此I/O密集型应用需要设置更多的线程。
**线程I/O时间与CPU时间**
至此我们又得到一个线程池个数的计算公式,假设服务器是单核的:
**线程池大小 = 线程I/O阻塞时间 + 线程CPU时间 / 线程CPU时间**
其中线程I/O阻塞时间 + 线程CPU时间 = 平均请求处理时间
对比一下两个公式,你会发现,**平均请求处理时间**在两个公式里都出现了,这说明请求时间越长,需要更多的线程是毫无疑问的。
不同的是第一个公式是用**每秒请求数**来乘以请求处理时间;而第二个公式用**请求处理时间**来除以**线程CPU时间**请注意CPU时间是小于请求处理时间的。
虽然这两个公式是从不同的角度来看待问题的,但都是理想情况,都有一定的前提条件。
1. 请求处理时间越长需要的线程数越多但前提是CPU核数要足够如果一个CPU来支撑10000 TPS并发创建10000个线程显然不合理会造成大量线程上下文切换。
1. 请求处理过程中I/O等待时间越长需要的线程数越多前提是CUP时间和I/O时间的比率要计算的足够准确。
1. 请求进来的速率越快需要的线程数越多前提是CPU核数也要跟上。
## 实际场景下如何确定线程数
那么在实际情况下,线程池的个数如何确定呢?这是一个迭代的过程,先用上面两个公式大概算出理想的线程数,再反复压测调整,从而达到最优。
一般来说如果系统的TPS要求足够大用第一个公式算出来的线程数往往会比公式二算出来的要大。我建议选取这两个值中间更靠近公式二的值。也就是先设置一个较小的线程数然后进行压测当达到系统极限时错误数增加或者响应时间大幅增加再逐步加大线程数当增加到某个值再增加线程数也无济于事甚至TPS反而下降那这个值可以认为是最佳线程数。
线程池中其他的参数最好就用默认值能不改就不改除非在压测的过程发现了瓶颈。如果发现了问题就需要调整比如maxQueueSize如果大量任务来不及处理都堆积在maxQueueSize中会导致内存耗尽这个时候就需要给maxQueueSize设一个限制。当然这是一个比较极端的情况了。
再比如minSpareThreads参数默认是25个线程如果你发现系统在闲的时候用不到25个线程就可以调小一点如果系统在大部分时间都比较忙线程池中的线程总是远远多于25个这个时候你就可以把这个参数调大一点因为这样线程池就不需要反复地创建和销毁线程了。
## 本期精华
今天我们学习了I/O调优也就是如何选择连接器的类型以及在选择过程中有哪些需要注意的地方。
后面还聊到Tomcat线程池的各种参数其中最重要的参数是最大线程数maxThreads。理论上我们可以通过利特尔法则或者CPU时间与I/O时间的比率计算出一个理想值这个值只具有指导意义因为它受到各种资源的限制实际场景中我们需要在理想值的基础上进行压测来获得最佳线程数。
## 课后思考
其实调优很多时候都是在找系统瓶颈假如有个状况系统响应比较慢但CPU的用率不高内存有所增加通过分析Heap Dump发现大量请求堆积在线程池的队列中请问这种情况下应该怎么办呢
不知道今天的内容你消化得如何?如果还有疑问,请大胆的在留言区提问,也欢迎你把你的课后思考和心得记录下来,与我和其他同学一起讨论。如果你觉得今天有所收获,欢迎你把它分享给你的朋友。

View File

@@ -0,0 +1,169 @@
<audio id="audio" title="37 | Tomcat内存溢出的原因分析及调优" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/6c/09/6c9f55f61bef8b7996c62c960bc66b09.mp3"></audio>
作为Java程序员我们几乎都会碰到java.lang.OutOfMemoryError异常但是你知道有哪些原因可能导致JVM抛出OutOfMemoryError异常吗
JVM在抛出java.lang.OutOfMemoryError时除了会打印出一行描述信息还会打印堆栈跟踪因此我们可以通过这些信息来找到导致异常的原因。在寻找原因前我们先来看看有哪些因素会导致OutOfMemoryError其中内存泄漏是导致OutOfMemoryError的一个比较常见的原因最后我们通过一个实战案例来定位内存泄漏。
## 内存溢出场景及方案
**java.lang.OutOfMemoryError: Java heap space**
JVM无法在堆中分配对象时会抛出这个异常导致这个异常的原因可能有三种
1. 内存泄漏。Java应用程序一直持有Java对象的引用导致对象无法被GC回收比如对象池和内存池中的对象无法被GC回收。
1. 配置问题。有可能是我们通过JVM参数指定的堆大小或者未指定的默认大小对于应用程序来说是不够的。解决办法是通过JVM参数加大堆的大小。
1. finalize方法的过度使用。如果我们想在Java类实例被GC之前执行一些逻辑比如清理对象持有的资源可以在Java类中定义finalize方法这样JVM GC不会立即回收这些对象实例而是将对象实例添加到一个叫“`java.lang.ref.Finalizer.ReferenceQueue`”的队列中执行对象的finalize方法之后才会回收这些对象。Finalizer线程会和主线程竞争CPU资源但由于优先级低所以处理速度跟不上主线程创建对象的速度因此ReferenceQueue队列中的对象就越来越多最终会抛出OutOfMemoryError。解决办法是尽量不要给Java类定义finalize方法。
**java.lang.OutOfMemoryError: GC overhead limit exceeded**
出现这种OutOfMemoryError的原因是垃圾收集器一直在运行但是GC效率很低比如Java进程花费超过98的CPU时间来进行一次GC但是回收的内存少于2的JVM堆并且连续5次GC都是这种情况就会抛出OutOfMemoryError。
解决办法是查看GC日志或者生成Heap Dump确认一下是不是内存泄漏如果不是内存泄漏可以考虑增加Java堆的大小。当然你还可以通过参数配置来告诉JVM无论如何也不要抛出这个异常方法是配置`-XX:-UseGCOverheadLimit`但是我并不推荐这么做因为这只是延迟了OutOfMemoryError的出现。
**java.lang.OutOfMemoryError: Requested array size exceeds VM limit**
从错误消息我们也能猜到抛出这种异常的原因是“请求的数组大小超过JVM限制”应用程序尝试分配一个超大的数组。比如应用程序尝试分配512MB的数组但最大堆大小为256MB则将抛出OutOfMemoryError并且请求的数组大小超过VM限制。
通常这也是一个配置问题JVM堆太小或者是应用程序的一个Bug比如程序错误地计算了数组的大小导致尝试创建一个大小为1GB的数组。
**java.lang.OutOfMemoryError: MetaSpace**
如果JVM的元空间用尽则会抛出这个异常。我们知道JVM元空间的内存在本地内存中分配但是它的大小受参数MaxMetaSpaceSize的限制。当元空间大小超过MaxMetaSpaceSize时JVM将抛出带有MetaSpace字样的OutOfMemoryError。解决办法是加大MaxMetaSpaceSize参数的值。
**java.lang.OutOfMemoryError: Request size bytes for reason. Out of swap space**
当本地堆内存分配失败或者本地内存快要耗尽时Java HotSpot VM代码会抛出这个异常VM会触发“致命错误处理机制”它会生成“致命错误”日志文件其中包含崩溃时线程、进程和操作系统的有用信息。如果碰到此类型的OutOfMemoryError你需要根据JVM抛出的错误信息来进行诊断或者使用操作系统提供的DTrace工具来跟踪系统调用看看是什么样的程序代码在不断地分配本地内存。
**java.lang.OutOfMemoryError: Unable to create native threads**
抛出这个异常的过程大概是这样的:
1. Java程序向JVM请求创建一个新的Java线程。
1. JVM本地代码Native Code代理该请求通过调用操作系统API去创建一个操作系统级别的线程Native Thread。
1. 操作系统尝试创建一个新的Native Thread需要同时分配一些内存给该线程每一个Native Thread都有一个线程栈线程栈的大小由JVM参数`-Xss`决定。
1. 由于各种原因,操作系统创建新的线程可能会失败,下面会详细谈到。
1. JVM抛出“java.lang.OutOfMemoryError: Unable to create new native thread”错误。
因此关键在于第四步线程创建失败JVM就会抛出OutOfMemoryError那具体有哪些因素会导致线程创建失败呢
1.内存大小限制我前面提到Java创建一个线程需要消耗一定的栈空间并通过`-Xss`参数指定。请你注意的是栈空间如果过小可能会导致StackOverflowError尤其是在递归调用的情况下但是栈空间过大会占用过多内存而对于一个32位Java应用来说用户进程空间是4GB内核占用1GB那么用户空间就剩下3GB因此它能创建的线程数大致可以通过这个公式算出来
```
Max memory3GB = [-Xmx] + [-XX:MaxMetaSpaceSize] + number_of_threads * [-Xss]
```
不过对于64位的应用由于虚拟进程空间近乎无限大因此不会因为线程栈过大而耗尽虚拟地址空间。但是请你注意64位的Java进程能分配的最大内存数仍然受物理内存大小的限制。
**2. ulimit限制**在Linux下执行`ulimit -a`你会看到ulimit对各种资源的限制。
<img src="https://static001.geekbang.org/resource/image/4d/4b/4d79133fc4f8795e44ea3a48a7293a4b.png" alt="">
其中的“max user processes”就是一个进程能创建的最大线程数我们可以修改这个参数
<img src="https://static001.geekbang.org/resource/image/73/77/736fbbb5fadb35f9ae0cc47182938a77.png" alt="">
**3. 参数`sys.kernel.threads-max`限制**。这个参数限制操作系统全局的线程数,通过下面的命令可以查看它的值。
<img src="https://static001.geekbang.org/resource/image/2e/58/2e9f281f4f0d89be1983314713a70258.png" alt="">
这表明当前系统能创建的总的线程是63752。当然我们调整这个参数具体办法是
`/etc/sysctl.conf`配置文件中,加入`sys.kernel.threads-max = 999999`
**4. 参数`sys.kernel.pid_max`限制**这个参数表示系统全局的PID号数值的限制每一个线程都有IDID的值超过这个数线程就会创建失败。跟`sys.kernel.threads-max`参数一样,我们也可以将`sys.kernel.pid_max`调大,方法是在`/etc/sysctl.conf`配置文件中,加入`sys.kernel.pid_max = 999999`
对于线程创建失败的OutOfMemoryError除了调整各种参数我们还需要从程序本身找找原因看看是否真的需要这么多线程有可能是程序的Bug导致创建过多的线程。
## 内存泄漏定位实战
我们先创建一个Web应用不断地new新对象放到一个List中来模拟Web应用中的内存泄漏。然后通过各种工具来观察GC的行为最后通过生成Heap Dump来找到泄漏点。
内存泄漏模拟程序比较简单创建一个Spring Boot应用定义如下所示的类
```
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.LinkedList;
import java.util.List;
@Component
public class MemLeaker {
private List&lt;Object&gt; objs = new LinkedList&lt;&gt;();
@Scheduled(fixedRate = 1000)
public void run() {
for (int i = 0; i &lt; 50000; i++) {
objs.add(new Object());
}
}
}
```
这个程序做的事情就是每隔1秒向一个List中添加50000个对象。接下来运行并通过工具观察它的GC行为
1.运行程序并打开verbosegc将GC的日志输出到gc.log文件中。
```
java -verbose:gc -Xloggc:gc.log -XX:+PrintGCDetails -jar mem-0.0.1-SNAPSHOT.jar
```
2.使用`jstat`命令观察GC的过程
```
jstat -gc 94223 2000 1000
```
94223是程序的进程ID2000表示每隔2秒执行一次1000表示持续执行1000次。下面是命令的输出
<img src="https://static001.geekbang.org/resource/image/6f/c2/6fe71c885aa0917d6aaf634cdb1b19c2.png" alt="">
其中每一列的含义是:
- S0C第一个Survivor区总的大小
- S1C第二个Survivor区总的大小
- S0U第一个Survivor区已使用内存的大小
- S1U第二个Survivor区已使用内存的大小。
后面的列相信从名字你也能猜出是什么意思了其中E代表EdenO代表OldM代表MetadataYGC表示Minor GC的总时间YGCT表示Minor GC的次数FGC表示Full GC。
通过这个工具你能大概看到各个内存区域的大小、已经GC的次数和所花的时间。verbosegc参数对程序的影响比较小因此很适合在生产环境现场使用。
3.通过GCViewer工具查看GC日志用GCViewer打开第一步产生的gc.log会看到这样的图
<img src="https://static001.geekbang.org/resource/image/d2/3b/d281cba5576304f0c1407aa16b01353b.png" alt="">
图中红色的线表示年老代占用的内存你会看到它一直在增加而黑色的竖线表示一次Full GC。你可以看到后期JVM在频繁地Full GC但是年老代的内存并没有降下来这是典型的内存泄漏的特征。
除了内存泄漏我们还可以通过GCViewer来观察Minor GC和Full GC的频次已及每次的内存回收量。
4.为了找到内存泄漏点我们通过jmap工具生成Heap Dump
```
jmap -dump:live,format=b,file=94223.bin 94223
```
5.用Eclipse Memory Analyzer打开Dump文件通过内存泄漏分析得到这样一个分析报告
<img src="https://static001.geekbang.org/resource/image/5b/4d/5b64390e688a0f49c19b110106ee334d.png" alt="">
从报告中可以看到JVM内存中有一个长度为4000万的List至此我们也就找到了泄漏点。
## 本期精华
今天我讲解了常见的OutOfMemoryError的场景以及解决办法我们在实际工作中要根据具体的错误信息去分析背后的原因尤其是Java堆内存不够时需要生成Heap Dump来分析看是不是内存泄漏排除内存泄漏之后我们再调整各种JVM参数否则根本的问题原因没有解决的话调整JVM参数也无济于事。
## 课后思考
请你分享一下平时在工作中遇到了什么样的OutOfMemoryError以及你是怎么解决的。
不知道今天的内容你消化得如何?如果还有疑问,请大胆的在留言区提问,也欢迎你把你的课后思考和心得记录下来,与我和其他同学一起讨论。如果你觉得今天有所收获,欢迎你把它分享给你的朋友。

View File

@@ -0,0 +1,114 @@
<audio id="audio" title="38 | Tomcat拒绝连接原因分析及网络优化" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/80/9b/808812b8479ad8a3404366ca2540469b.mp3"></audio>
专栏上一期我们分析各种JVM OutOfMemory错误的原因和解决办法今天我们来看看网络通信中可能会碰到的各种错误。网络通信方面的错误和异常也是我们在实际工作中经常碰到的需要理解异常背后的原理才能更快更精准地定位问题从而找到解决办法。
下面我会先讲讲Java Socket网络编程常见的异常有哪些然后通过一个实验来重现其中的Connection reset异常并且通过配置Tomcat的参数来解决这个问题。
## 常见异常
**java.net.SocketTimeoutException**
指超时错误。超时分为**连接超时**和**读取超时**连接超时是指在调用Socket.connect方法的时候超时而读取超时是调用Socket.read方法时超时。请你注意的是连接超时往往是由于网络不稳定造成的但是读取超时不一定是网络延迟造成的很有可能是下游服务的响应时间过长。
**java.net.BindException: Address already in use: JVM_Bind**
指端口被占用。当服务器端调用new ServerSocket(port)或者Socket.bind函数时如果端口已经被占用就会抛出这个异常。我们可以用`netstat an`命令来查看端口被谁占用了,换一个没有被占用的端口就能解决。
**java.net.ConnectException: Connection refused: connect**
指连接被拒绝。当客户端调用new Socket(ip, port)或者Socket.connect函数时可能会抛出这个异常。原因是指定IP地址的机器没有找到或者是机器存在但这个机器上没有开启指定的监听端口。
解决办法是从客户端机器ping一下服务端IP假如ping不通可以看看IP是不是写错了假如能ping通需要确认服务端的服务是不是崩溃了。
**java.net.SocketException: Socket is closed**
指连接已关闭。出现这个异常的原因是通信的一方主动关闭了Socket连接调用了Socket的close方法接着又对Socket连接进行了读写操作这时操作系统会报“Socket连接已关闭”的错误。
**java.net.SocketException: Connection reset/Connect reset by peer: Socket write error**
指连接被重置。这里有两种情况分别对应两种错误第一种情况是通信的一方已经将Socket关闭可能是主动关闭或者是因为异常退出这时如果通信的另一方还在写数据就会触发这个异常Connect reset by peer如果对方还在尝试从TCP连接中读数据则会抛出Connection reset异常。
为了避免这些异常发生,在编写网络通信程序时要确保:
- 程序退出前要主动关闭所有的网络连接。
- 检测通信的另一方的关闭连接操作,当发现另一方关闭连接后自己也要关闭该连接。
**java.net.SocketException: Broken pipe**
指通信管道已坏。发生这个异常的场景是通信的一方在收到“Connect reset by peer: Socket write error”后如果再继续写数据则会抛出Broken pipe异常解决方法同上。
**java.net.SocketException: Too many open files**
指进程打开文件句柄数超过限制。当并发用户数比较大时服务器可能会报这个异常。这是因为每创建一个Socket连接就需要一个文件句柄此外服务端程序在处理请求时可能也需要打开一些文件。
你可以通过`lsof -p pid`命令查看进程打开了哪些文件是不是有资源泄露也就是说进程打开的这些文件本应该被关闭但由于程序的Bug而没有被关闭。
如果没有资源泄露,可以通过设置增加最大文件句柄数。具体方法是通过`ulimit -a`来查看系统目前资源限制,通过`ulimit -n 10240`修改最大文件数。
## Tomcat网络参数
接下来我们看看Tomcat两个比较关键的参数maxConnections和acceptCount。在解释这个参数之前先简单回顾下TCP连接的建立过程客户端向服务端发送SYN包服务端回复SYNACK同时将这个处于SYN_RECV状态的连接保存到**半连接队列**。客户端返回ACK包完成三次握手服务端将ESTABLISHED状态的连接移入**accept队列**等待应用程序Tomcat调用accept方法将连接取走。这里涉及两个队列
- **半连接队列**保存SYN_RECV状态的连接。队列长度由`net.ipv4.tcp_max_syn_backlog`设置。
- **accept队列**保存ESTABLISHED状态的连接。队列长度为`min(net.core.somaxconnbacklog)`。其中backlog是我们创建ServerSocket时指定的参数最终会传递给listen方法
```
int listen(int sockfd, int backlog);
```
如果我们设置的backlog大于`net.core.somaxconn`accept队列的长度将被设置为`net.core.somaxconn`而这个backlog参数就是Tomcat中的**acceptCount**参数默认值是100但请注意`net.core.somaxconn`的默认值是128。你可以想象在高并发情况下当Tomcat来不及处理新的连接时这些连接都被堆积在accept队列中而**acceptCount**参数可以控制accept队列的长度超过这个长度时内核会向客户端发送RST这样客户端会触发上文提到的“Connection reset”异常。
而Tomcat中的**maxConnections**是指Tomcat在任意时刻接收和处理的最大连接数。当Tomcat接收的连接数达到maxConnections时Acceptor线程不会再从accept队列中取走连接这时accept队列中的连接会越积越多。
maxConnections的默认值与连接器类型有关NIO的默认值是10000APR默认是8192。
所以你会发现Tomcat的最大并发连接数等于**maxConnections + acceptCount**。如果acceptCount设置得过大请求等待时间会比较长如果acceptCount设置过小高并发情况下客户端会立即触发Connection reset异常。
## Tomcat网络调优实战
接下来我们通过一个直观的例子来加深对上面两个参数的理解。我们先重现流量高峰时accept队列堆积的情况这样会导致客户端触发“Connection reset”异常然后通过调整参数解决这个问题。主要步骤有
1.下载和安装压测工具[JMeter](http://jmeter.apache.org/download_jmeter.cgi)。解压后打开,我们需要创建一个测试计划、一个线程组、一个请求和,如下图所示。
**测试计划**
<img src="https://static001.geekbang.org/resource/image/a6/4d/a6ad806b55cab54098f2b179c2cf874d.png" alt="">
**线程组**线程数这里设置为1000模拟大流量
<img src="https://static001.geekbang.org/resource/image/59/3a/590569c1b516d10af6e6ca9ee99f6a3a.png" alt="">
**请求**请求的路径是Tomcat自带的例子程序
<img src="https://static001.geekbang.org/resource/image/9e/3b/9efa851e885448e457b4883c8927ac3b.png" alt="">
2.启动Tomcat。
3.开启JMeter测试在View Results Tree中会看到大量失败的请求请求的响应里有“Connection reset”异常也就是前面提到的当accept队列溢出时服务端的内核发送了RST给客户端使得客户端抛出了这个异常。
<img src="https://static001.geekbang.org/resource/image/26/9c/267b3808cdc2673418f9b3ac44a59b9c.png" alt="">
4.修改内核参数,在`/etc/sysctl.conf`中增加一行`net.core.somaxconn=2048`,然后执行命令`sysctl -p`
5.修改Tomcat参数acceptCount为2048重启Tomcat。
<img src="https://static001.geekbang.org/resource/image/d1/0b/d12ea2188bddf803b62613fd59d8af0b.png" alt="">
6.再次启动JMeter测试这一次所有的请求会成功也看不到异常了。我们可以通过下面的命令看到系统中ESTABLISHED的连接数增大了这是因为我们加大了accept队列的长度。
<img src="https://static001.geekbang.org/resource/image/c6/e6/c6f610d4311c433a149ea9d3d4b5ade6.png" alt="">
## 本期精华
在Socket网络通信过程中我们不可避免地会碰到各种Java异常了解这些异常产生的原因非常关键通过这些信息我们大概知道问题出在哪里如果一时找不到问题代码我们还可以通过网络抓包工具来分析数据包。
在这个基础上我们还分析了Tomcat中两个比较重要的参数acceptCount和maxConnections。acceptCount用来控制内核的TCP连接队列长度maxConnections用于控制Tomcat层面的最大连接数。在实战环节我们通过调整acceptCount和相关的内核参数`somaxconn`,增加了系统的并发度。
## 课后思考
在上面的实验中,我们通过`netstat`命令发现有大量的TCP连接处在TIME_WAIT状态请问这是为什么它可能会带来什么样的问题呢
不知道今天的内容你消化得如何?如果还有疑问,请大胆的在留言区提问,也欢迎你把你的课后思考和心得记录下来,与我和其他同学一起讨论。如果你觉得今天有所收获,欢迎你把它分享给你的朋友。

View File

@@ -0,0 +1,136 @@
<audio id="audio" title="39 | Tomcat进程占用CPU过高怎么办" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/9f/b5/9fe8938a0e92a040a20aad313d1f28b5.mp3"></audio>
在性能优化这个主题里前面我们聊过了Tomcat的内存问题和网络相关的问题接下来我们看一下CPU的问题。CPU资源经常会成为系统性能的一个瓶颈这其中的原因是多方面的可能是内存泄露导致频繁GC进而引起CPU使用率过高又可能是代码中的Bug创建了大量的线程导致CPU上下文切换开销。
今天我们就来聊聊Tomcat进程的CPU使用率过高怎么办以及怎样一步一步找到问题的根因。
## “Java进程CPU使用率高”的解决思路是什么
通常我们所说的CPU使用率过高这里面其实隐含着一个用来比较高与低的基准值比如JVM在峰值负载下的平均CPU利用率为40如果CPU使用率飙到80%就可以被认为是不正常的。
典型的JVM进程包含多个Java线程其中一些在等待工作另一些则正在执行任务。在单个Java程序的情况下线程数可以非常低而对于处理大量并发事务的互联网后台来说线程数可能会比较高。
对于CPU的问题最重要的是要找到是**哪些线程在消耗CPU**通过线程栈定位到问题代码如果没有找到个别线程的CPU使用率特别高我们要怀疑到是不是线程上下文切换导致了CPU使用率过高。下面我们通过一个实例来学习CPU问题定位的过程。
## 定位高CPU使用率的线程和代码
1.写一个模拟程序来模拟CPU使用率过高的问题这个程序会在线程池中创建4096个线程。代码如下
```
@SpringBootApplication
@EnableScheduling
public class DemoApplication {
//创建线程池其中有4096个线程。
private ExecutorService executor = Executors.newFixedThreadPool(4096);
//全局变量,访问它需要加锁。
private int count;
//以固定的速率向线程池中加入任务
@Scheduled(fixedRate = 10)
public void lockContention() {
IntStream.range(0, 1000000)
.forEach(i -&gt; executor.submit(this::incrementSync));
}
//具体任务就是将count数加一
private synchronized void incrementSync() {
count = (count + 1) % 10000000;
}
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
```
2.在Linux环境下启动程序
```
java -Xss256k -jar demo-0.0.1-SNAPSHOT.jar
```
请注意这里我将线程栈大小指定为256KB。对于测试程序来说操作系统默认值8192KB过大因为我们需要创建4096个线程。
3.使用top命令我们看到Java进程的CPU使用率达到了262.3%注意到进程ID是4361。
<img src="https://static001.geekbang.org/resource/image/e0/50/e0db4c399cbbf83924a505a9cd619150.png" alt="">
4.接着我们用更精细化的top命令查看这个Java进程中各线程使用CPU的情况
```
#top -H -p 4361
```
<img src="https://static001.geekbang.org/resource/image/4a/8d/4a52b5335daf5bfe0b60128a1c13558d.png" alt="">
从图上我们可以看到有个叫“scheduling-1”的线程占用了较多的CPU达到了42.5%。因此下一步我们要找出这个线程在做什么事情。
5.为了找出线程在做什么事情我们需要用jstack命令生成线程快照具体方法是
```
jstack 4361
```
jstack的输出比较大你可以将输出写入文件
```
jstack 4361 &gt; 4361.log
```
然后我们打开4361.log定位到第4步中找到的名为“scheduling-1”的线程发现它的线程栈如下
<img src="https://static001.geekbang.org/resource/image/da/8e/dae7a6f02563051a1d4dd3752d9f5e8e.png" alt="">
从线程栈中我们看到了`AbstractExecutorService.submit`这个函数调用说明它是Spring Boot启动的周期性任务线程向线程池中提交任务这个线程消耗了大量CPU。
## 进一步分析上下文切换开销
一般来说通过上面的过程我们就能定位到大量消耗CPU的线程以及有问题的代码比如死循环。但是对于这个实例的问题你是否发现这样一个情况Java进程占用的CPU是262.3% 而“scheduling-1”线程只占用了42.5%的CPU那还有将近220%的CPU被谁占用了呢
不知道你注意到没有我们在第4步用`top -H -p 4361`命令看到的线程列表中还有许多名为“pool-1-thread-x”的线程它们单个的CPU使用率不高但是似乎数量比较多。你可能已经猜到这些就是线程池中干活的线程。那剩下的220%的CPU是不是被这些线程消耗了呢
要弄清楚这个问题我们还需要看jstack的输出结果主要是看这些线程池中的线程是不是真的在干活还是在“休息”呢
<img src="https://static001.geekbang.org/resource/image/68/bf/68bb91e5c1405940b470c08851d13cbf.png" alt="">
通过上面的图我们发现这些“pool-1-thread-x”线程基本都处于WAITING的状态那什么是WAITING状态呢或者说Java线程都有哪些状态呢你可以通过下面的图来理解一下
<img src="https://static001.geekbang.org/resource/image/0e/43/0e2336814a4b9fc39bcdf991949a7e43.png" alt="">
从图上我们看到“Blocking”和“Waiting”是两个不同的状态我们要注意它们的区别
- Blocking指的是一个线程因为等待临界区的锁Lock或者synchronized关键字而被阻塞的状态请你注意的是处于这个状态的线程**还没有拿到锁。**
- Waiting指的是一个线程拿到了锁但是需要等待其他线程执行某些操作。比如调用了Object.wait、Thread.join或者LockSupport.park方法时进入Waiting状态。**前提是这个线程已经拿到锁了**并且在进入Waiting状态前操作系统层面会自动释放锁当等待条件满足外部调用了Object.notify或者LockSupport.unpark方法线程会重新竞争锁成功获得锁后才能进入到Runnable状态继续执行。
回到我们的“pool-1-thread-x”线程这些线程都处在“Waiting”状态从线程栈我们看到这些线程“等待”在getTask方法调用上线程尝试从线程池的队列中取任务但是队列为空所以通过LockSupport.park调用进到了“Waiting”状态。那“pool-1-thread-x”线程有多少个呢通过下面这个命令来统计一下结果是4096正好跟线程池中的线程数相等。
<img src="https://static001.geekbang.org/resource/image/f7/3d/f7b4611b87a8bd65fa25a2c4c7228b3d.png" alt="">
你可能好奇了那剩下的220%的CPU到底被谁消耗了呢分析到这里我们应该怀疑CPU的上下文切换开销了因为我们看到Java进程中的线程数比较多。下面我们通过vmstat命令来查看一下操作系统层面的线程上下文切换活动
<img src="https://static001.geekbang.org/resource/image/07/c4/07cccbe33337df20a2544947281c71c4.png" alt="">
如果你还不太熟悉vmstat可以在[这里](https://linux.die.net/man/8/vmstat)学习如何使用vmstat和查看结果。其中cs那一栏表示线程上下文切换次数in表示CPU中断次数我们发现这两个数字非常高基本证实了我们的猜测线程上下文切切换消耗了大量CPU。那么问题来了具体是哪个进程导致的呢
我们停止Spring Boot测试程序再次运行vmstat命令会看到in和cs都大幅下降了这样就证实了引起线程上下文切换开销的Java进程正是4361。
<img src="https://static001.geekbang.org/resource/image/5f/fa/5f0a5dadc0659da607fd6e5f0c96dffa.png" alt="">
## 本期精华
当我们遇到CPU过高的问题时首先要定位是哪个进程的导致的之后可以通过`top -H -p pid`命令定位到具体的线程。其次还要通jstack查看线程的状态看看线程的个数或者线程的状态如果线程数过多可以怀疑是线程上下文切换的开销我们可以通过vmstat和pidstat这两个工具进行确认。
## 课后思考
哪些情况可能导致程序中的线程数失控,产生大量线程呢?
不知道今天的内容你消化得如何?如果还有疑问,请大胆的在留言区提问,也欢迎你把你的课后思考和心得记录下来,与我和其他同学一起讨论。如果你觉得今天有所收获,欢迎你把它分享给你的朋友。

View File

@@ -0,0 +1,200 @@
<audio id="audio" title="40 | 谈谈Jetty性能调优的思路" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/1d/41/1d672e12e7055ecef1acb50198278f41.mp3"></audio>
关于Tomcat的性能调优前面我主要谈了工作经常会遇到的有关JVM GC、监控、I/O和线程池以及CPU的问题定位和调优今天我们来看看Jetty有哪些调优的思路。
关于Jetty的性能调优官网上给出了一些很好的建议分为操作系统层面和Jetty本身的调优我们将分别来看一看它们具体是怎么做的最后再通过一个实战案例来学习一下如何确定Jetty的最佳线程数。
## 操作系统层面调优
对于Linux操作系统调优来说我们需要加大一些默认的限制值这些参数主要可以在`/etc/security/limits.conf`中或通过`sysctl`命令进行配置其实这些配置对于Tomcat来说也是适用的下面我来详细介绍一下这些参数。
**TCP缓冲区大小**
TCP的发送和接收缓冲区最好加大到16MB可以通过下面的命令配置
```
sysctl -w net.core.rmem_max = 16777216
sysctl -w net.core.wmem_max = 16777216
sysctl -w net.ipv4.tcp_rmem =“4096 87380 16777216”
sysctl -w net.ipv4.tcp_wmem =“4096 16384 16777216”
```
**TCP队列大小**
`net.core.somaxconn`控制TCP连接队列的大小默认值为128在高并发情况下明显不够用会出现拒绝连接的错误。但是这个值也不能调得过高因为过多积压的TCP连接会消耗服务端的资源并且会造成请求处理的延迟给用户带来不好的体验。因此我建议适当调大推荐设置为4096。
```
sysctl -w net.core.somaxconn = 4096
```
`net.core.netdev_max_backlog`用来控制Java程序传入数据包队列的大小可以适当调大。
```
sysctl -w net.core.netdev_max_backlog = 16384
sysctl -w net.ipv4.tcp_max_syn_backlog = 8192
sysctl -w net.ipv4.tcp_syncookies = 1
```
**端口**
如果Web应用程序作为客户端向远程服务器建立了很多TCP连接可能会出现TCP端口不足的情况。因此最好增加使用的端口范围并允许在TIME_WAIT中重用套接字
```
sysctl -w net.ipv4.ip_local_port_range =“1024 65535”
sysctl -w net.ipv4.tcp_tw_recycle = 1
```
**文件句柄数**
高负载服务器的文件句柄数很容易耗尽,这是因为系统默认值通常比较低,我们可以在`/etc/security/limits.conf`中为特定用户增加文件句柄数:
```
用户名 hard nofile 40000
用户名 soft nofile 40000
```
**拥塞控制**
Linux内核支持可插拔的拥塞控制算法如果要获取内核可用的拥塞控制算法列表可以通过下面的命令
```
sysctl net.ipv4.tcp_available_congestion_control
```
这里我推荐将拥塞控制算法设置为cubic
```
sysctl -w net.ipv4.tcp_congestion_control = cubic
```
## Jetty本身的调优
Jetty本身的调优主要是设置不同类型的线程的数量包括Acceptor和Thread Pool。
**Acceptors**
Acceptor的个数accepts应该设置为大于等于1并且小于等于CPU核数。
**Thread Pool**
限制Jetty的任务队列非常重要。默认情况下队列是无限的因此如果在高负载下超过Web应用的处理能力Jetty将在队列上积压大量待处理的请求。并且即使负载高峰过去了Jetty也不能正常响应新的请求这是因为仍然有很多请求在队列等着被处理。
因此对于一个高可靠性的系统我们应该通过使用有界队列立即拒绝过多的请求也叫快速失败。那队列的长度设置成多大呢应该根据Web应用的处理速度而定。比如如果Web应用每秒可以处理100个请求当负载高峰到来我们允许一个请求可以在队列积压60秒那么我们就可以把队列长度设置为60 × 100 = 6000。如果设置得太低Jetty将很快拒绝请求无法处理正常的高峰负载以下是配置示例
```
&lt;Configure id=&quot;Server&quot; class=&quot;org.eclipse.jetty.server.Server&quot;&gt;
&lt;Set name=&quot;ThreadPool&quot;&gt;
&lt;New class=&quot;org.eclipse.jetty.util.thread.QueuedThreadPool&quot;&gt;
&lt;!-- specify a bounded queue --&gt;
&lt;Arg&gt;
&lt;New class=&quot;java.util.concurrent.ArrayBlockingQueue&quot;&gt;
&lt;Arg type=&quot;int&quot;&gt;6000&lt;/Arg&gt;
&lt;/New&gt;
&lt;/Arg&gt;
&lt;Set name=&quot;minThreads&quot;&gt;10&lt;/Set&gt;
&lt;Set name=&quot;maxThreads&quot;&gt;200&lt;/Set&gt;
&lt;Set name=&quot;detailedDump&quot;&gt;false&lt;/Set&gt;
&lt;/New&gt;
&lt;/Set&gt;
&lt;/Configure&gt;
```
那如何配置Jetty的线程池中的线程数呢跟Tomcat一样你可以根据实际压测如果I/O越密集线程阻塞越严重那么线程数就可以配置多一些。通常情况增加线程数需要更多的内存因此内存的最大值也要跟着调整所以一般来说Jetty的最大线程数应该在50到500之间。
## Jetty性能测试
接下来我们通过一个实验来测试一下Jetty的性能。我们可以在[这里](https://repo1.maven.org/maven2/org/eclipse/jetty/aggregate/jetty-all/9.4.19.v20190610/jetty-all-9.4.19.v20190610-uber.jar)下载Jetty的JAR包。
<img src="https://static001.geekbang.org/resource/image/2d/c9/2d78b4d2b8eca5912ed5899aa57b73c9.png" alt="">
第二步我们创建一个Handler这个Handler用来向客户端返回“Hello World”并实现一个main方法根据传入的参数创建相应数量的线程池。
```
public class HelloWorld extends AbstractHandler {
@Override
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
response.setContentType(&quot;text/html; charset=utf-8&quot;);
response.setStatus(HttpServletResponse.SC_OK);
response.getWriter().println(&quot;&lt;h1&gt;Hello World&lt;/h1&gt;&quot;);
baseRequest.setHandled(true);
}
public static void main(String[] args) throws Exception {
//根据传入的参数控制线程池中最大线程数的大小
int maxThreads = Integer.parseInt(args[0]);
System.out.println(&quot;maxThreads:&quot; + maxThreads);
//创建线程池
QueuedThreadPool threadPool = new QueuedThreadPool();
threadPool.setMaxThreads(maxThreads);
Server server = new Server(threadPool);
ServerConnector http = new ServerConnector(server,
new HttpConnectionFactory(new HttpConfiguration()));
http.setPort(8000);
server.addConnector(http);
server.start();
server.join();
}
}
```
第三步我们编译这个Handler得到HelloWorld.class。
```
javac -cp jetty.jar HelloWorld.java
```
第四步启动Jetty server并且指定最大线程数为4。
```
java -cp .:jetty.jar HelloWorld 4
```
第五步启动压测工具Apache Bench。关于Apache Bench的使用你可以参考[这里](https://httpd.apache.org/docs/current/programs/ab.html)。
```
ab -n 200000 -c 100 http://localhost:8000/
```
上面命令的意思是向Jetty server发出20万个请求开启100个线程同时发送。
经过多次压测测试结果稳定以后在Linux 4核机器上得到的结果是这样的
<img src="https://static001.geekbang.org/resource/image/33/04/33b63aca5bc1e92864c01d1b5c11f804.png" alt="">
从上面的测试结果我们可以看到20万个请求在9.99秒内处理完成RPS达到了20020。 不知道你是否好奇为什么我把最大线程数设置为4呢是不是有点小
别着急接下来我们就试着逐步加大最大线程数直到找到最佳值。下面这个表格显示了在其他条件不变的情况下只调整线程数对RPS的影响。
<img src="https://static001.geekbang.org/resource/image/78/db/782ab4f927e8e27b762f7fcb48c48cdb.jpg" alt="">
我们发现一个有意思的现象线程数从4增加到6RPS确实增加了。但是线程数从6开始继续增加RPS不但没有跟着上升反而下降了而且线程数越多RPS越低。
发生这个现象的原因是测试机器的CPU只有4核而我们测试的程序做得事情比较简单没有I/O阻塞属于CPU密集型程序。对于这种程序最大线程数可以设置为比CPU核心稍微大一点点。那具体设置成多少是最佳值呢我们需要根据实验里的步骤反复测试。你可以看到在我们这个实验中当最大线程数为6也就CPU核数的1.5倍时,性能达到最佳。
## 本期精华
今天我们首先学习了Jetty调优的基本思路主要分为操作系统级别的调优和Jetty本身的调优其中操作系统级别也适用于Tomcat。接着我们通过一个实例来寻找Jetty的最佳线程数在测试中我们发现对于CPU密集型应用将最大线程数设置CPU核数的1.5倍是最佳的。因此,在我们的实际工作中,切勿将线程池直接设置得很大,因为程序所需要的线程数可能会比我们想象的要小。
## 课后思考
我在今天文章前面提到Jetty的最大线程数应该在50到500之间。但是我们的实验中测试发现最大线程数为6时最佳这是不是矛盾了
不知道今天的内容你消化得如何?如果还有疑问,请大胆的在留言区提问,也欢迎你把你的课后思考和心得记录下来,与我和其他同学一起讨论。如果你觉得今天有所收获,欢迎你把它分享给你的朋友。

View File

@@ -0,0 +1,74 @@
<audio id="audio" title="41 | 热点问题答疑4 Tomcat和Jetty有哪些不同" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/0a/04/0ae0712b99d5bcc08dbf1dcb43cec604.mp3"></audio>
作为专栏最后一个模块的答疑文章我想是时候总结一下Tomcat和Jetty的区别了。专栏里也有同学给我留言询问有关Tomcat和Jetty在系统选型时需要考虑的地方今天我也会通过一个实战案例来比较一下Tomcat和Jetty在实际场景下的表现帮你在做选型时有更深的理解。
我先来概括一下Tomcat和Jetty两者最大的区别。大体来说Tomcat的核心竞争力是**成熟稳定**因为它经过了多年的市场考验应用也相当广泛对于比较复杂的企业级应用支持得更加全面。也因为如此Tomcat在整体结构上比Jetty更加复杂功能扩展方面可能不如Jetty那么方便。
而Jetty比较年轻设计上更加**简洁小巧**配置也比较简单功能也支持方便地扩展和裁剪比如我们可以把Jetty的SessionHandler去掉以节省内存资源因此Jetty还可以运行在小型的嵌入式设备中比如手机和机顶盒。当然我们也可以自己开发一个Handler加入Handler链中用来扩展Jetty的功能。值得一提的是Hadoop和Solr都嵌入了Jetty作为Web服务器。
从设计的角度来看Tomcat的架构基于一种多级容器的模式这些容器组件具有父子关系所有组件依附于这个骨架而且这个骨架是不变的我们在扩展Tomcat的功能时也需要基于这个骨架因此Tomcat在设计上相对来说比较复杂。当然Tomcat也提供了较好的扩展机制比如我们可以自定义一个Valve但相对来说学习成本还是比较大的。而Jetty采用Handler责任链模式。由于Handler之间的关系比较松散Jetty提供HandlerCollection可以帮助开发者方便地构建一个Handler链同时也提供了ScopeHandler帮助开发者控制Handler链的访问顺序。关于这部分内容你可以回忆一下专栏里讲的回溯方式的责任链模式。
说了一堆理论你可能觉得还是有点抽象接下来我们通过一个实例来压测一下Tomcat和Jetty看看在同等流量压力下Tomcat和Jetty分别表现如何。需要说明的是通常我们从吞吐量、延迟和错误率这三个方面来比较结果。
测试的计划是这样的,我们还是用[专栏第36期](http://time.geekbang.org/column/article/112271)中的Spring Boot应用程序。首先用Spring Boot默认的Tomcat作为内嵌式Web容器经过一轮压测后将内嵌式的Web容器换成Jetty再做一轮测试然后比较结果。为了方便观察各种指标我在本地开发机器上做这个实验。
我们会在每个请求的处理过程中休眠1秒适当地模拟Web应用的I/O等待时间。JMeter客户端的线程数为100压测持续10分钟。在JMeter中创建一个Summary Report在这个页面上可以看到各种统计指标。
<img src="https://static001.geekbang.org/resource/image/88/b6/888ff5dc207d7bc746663c2be2d3dbb6.png" alt="">
第一步压测Tomcat。启动Spring Boot程序和JMeter持续10分钟以下是测试结果结果分为两部分
**吞吐量、延迟和错误率**
<img src="https://static001.geekbang.org/resource/image/eb/c9/eb8f119a6106da2ada200a86436df8c9.png" alt="">
**资源使用情况**
<img src="https://static001.geekbang.org/resource/image/8a/c6/8ad606fd374a375ccedae71c5eaadcc6.png" alt="">
第二步我们将Spring Boot的Web容器替换成Jetty具体步骤是在pom.xml文件中的spring-boot-starter-web依赖修改下面这样
```
&lt;dependency&gt;
&lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
&lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt;
&lt;exclusions&gt;
&lt;exclusion&gt;
&lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
&lt;artifactId&gt;spring-boot-starter-tomcat&lt;/artifactId&gt;
&lt;/exclusion&gt;
&lt;/exclusions&gt;
&lt;/dependency&gt;
&lt;dependency&gt;
&lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
&lt;artifactId&gt;spring-boot-starter-jetty&lt;/artifactId&gt;
&lt;/dependency&gt;
```
编译打包启动Spring Boot再启动JMeter压测以下是测试结果
**吞吐量、延迟和错误率**
<img src="https://static001.geekbang.org/resource/image/16/c6/1613eca65656b08e467eae63238252c6.png" alt="">
**资源使用情况**
<img src="https://static001.geekbang.org/resource/image/60/9f/6039f999094dd390bb7a60ec63c5b19f.png" alt="">
下面我们通过一个表格来对比Tomcat和Jetty
<img src="https://static001.geekbang.org/resource/image/82/4f/824b67dbdb1cd7205427ab67a3ab864f.jpg" alt="">
从表格中的数据我们可以看到:
- **Jetty在吞吐量和响应速度方面稍有优势并且Jetty消耗的线程和内存资源明显比Tomcat要少这也恰好说明了Jetty在设计上更加小巧和轻量级的特点。**
- **但是Jetty有2.45%的错误率而Tomcat没有任何错误并且我经过多次测试都是这个结果。因此我们可以认为Tomcat比Jetty更加成熟和稳定。**
当然由于测试场景的限制以上数据并不能完全反映Tomcat和Jetty的真实能力。但是它可以在我们做选型的时候提供一些参考如果系统的目标是资源消耗尽量少并且对稳定性要求没有那么高可以选择轻量级的Jetty如果你的系统是比较关键的企业级应用建议还是选择Tomcat比较稳妥。
最后用一句话总结Tomcat和Jetty的区别**Tomcat好比是一位工作多年比较成熟的工程师轻易不会出错、不会掉链子但是他有自己的想法不会轻易做出改变。而Jetty更像是一位年轻的后起之秀脑子转得很快可塑性也很强但有时候也会犯一点小错误。**
不知道今天的内容你消化得如何?如果还有疑问,请大胆的在留言区提问,也欢迎你把你的课后思考和心得记录下来,与我和其他同学一起讨论。如果你觉得今天有所收获,欢迎你把它分享给你的朋友。