JDK调优系列1- 用好性能调优利器:火焰图,事半功倍
兵欲善其事,必先利其器。程序员在定位性能瓶颈的时候,要是有一个趁手的性能调优工具,能一针见血的指出程序的性能问题,可谓事半功倍。
我们常用的性能调优工具Perf(Linux系统原生提供的性能分析工具),能按出现的百分比降序打印CPU正在执行的函数名以及调用栈,
如命令:
perf record
perf report -n
可打印出:
这种结果的输出还是不直观的,Linux性能优化大师Brendan Gregg发明了火焰图(因整个图形看起来像燃烧的火焰而得名),以全局的方式来看各个函数的调用时间分布,以图形化的方式列出调用栈。
1 初识火焰图
火焰图是基于perf的结果生成的图形,我们先了解一下怎么去看火焰图。以下图为例:
X轴表示被抽样到的次数。理解X轴的含义,需先了解采样数据的原理。Perf是在指定时间段内,每隔一段时间采集一次数据,被采集到的次数越多,说明该函数的执行总时间长,可能的情况有调用次数多,或者单次执行时间长。因此,X轴的宽度不能简单的认为是运行时长。
Y轴表示调用栈。
如何从火焰图看出性能的瓶颈在哪里?最有理由怀疑的地方,顶层的“平顶”。下面是我们利用火焰图来定位问题的一次实战。
2 火焰图定位问题的实战
2.1 问题场景
问题发生的场景是客户端向服务器发起http请求,服务器返回数据给客户端。客户发现使用OracleJDK 8u_74的性能要远优于OracleJDK 8u_202的性能,图中体现为业务线统计的得到服务器响应的响应时长。
次数 |
JDK8u74响应时间(单位:秒) |
JDK8u202响应时间(单位:秒) |
1 |
0.030 |
0.834 |
2 |
0.036 |
1.088 |
3 |
0.030 |
0.332 |
4 |
0.033 |
0.597 |
5 |
0.018 |
0.581 |
6 |
0.049 |
0.850 |
7 |
0.041 |
0.355 |
8 |
0.021 |
0.711 |
9 |
0.148 |
0.854 |
10 |
0.080 |
0.754 |
11 |
0.025 |
1.176 |
12 |
0.032 |
0.459 |
13 |
0.046 |
0.443 |
14 |
0.025 |
0.135 |
15 |
0.059 |
0.485 |
16 |
0.077 |
1.093 |
17 |
0.123 |
1.173 |
18 |
0.115 |
0.945 |
19 |
0.058 |
0.384 |
20 |
0.035 |
1.061 |
平均时间 |
0.05405 |
0.7155 |
典型的性能问题,202使用CPU的情况是74的13倍之多,考虑使用火焰图来定位性能消耗的问题点。
2.2 火焰图定位
对比两张火焰图,使用74时ClientHandshaker.processMessage占比为1.15%,而在202中这个函数占比为23.98%,很明显在ClientHandshaker.processMessage带来了性能差异。
2.3 根因定位
两者在这个ClientHandshaker.processMessage上的cpu消耗差异很大,继续分析这个函数找到根因。
void processMessage(byte handshakeType, int length) throws IOException {
if(this.state >= handshakeType && handshakeType != 0) {
//... 异常
} else {
label105:
switch(handshakeType) {
case 0://hello_request
this.serverHelloRequest(new HelloRequest(this.input));
break;
//...
case 2://sever_hello
this.serverHello(new ServerHello(this.input, length));
break;
case 11:///certificate
this.serverCertificate(new CertificateMsg(this.input));
this.serverKey = this.session.getPeerCertificates()[0].getPublicKey();
break;
case 12://server_key_exchange 该消息并不是必须的,取决于协商出的key交换算法
//...
case 13: //certificate_request 客户端双向验证时需要
//...
case 14://server_hello_done
this.serverHelloDone(new ServerHelloDone(this.input));
break;
case 20://finished
this.serverFinished(new Finished(this.protocolVersion, this.input, this.cipherSuite));
}
if(this.state < handshakeType) {//握手状态
this.state = handshakeType;
}
}
}
processMessage()主要是通过不同的信息类型进行不同的握手消息的处理。而在火焰图中可以看到,74图中,主要消耗在serverFinished()和serverHello()上,而202主要消耗在serverHelloDone()和serverKeyExchange()。
在介绍火焰图的时候,我们有提到,X轴的长度是映射了被采样到的次数。因此需要进一步确定是一直卡在该函数上,还是因为调用频繁。可通过字节码插桩查看serverHelloDone()的调用次数及执行时间。
JDK8u202 数据
Execute count : 253
Execute count : 258
Execute count : 649
Execute count : 661
serverHelloDone execute time [1881195 ns]
Execute count : 1223
Execute count : 1234
Execute count : 1843
Execute count : 1852
serverHelloDone execute time [1665012 ns]
Execute count : 2446
Execute count : 2456
serverHelloDone execute time [1686206 ns]
JDK8u74 数据
Execute count : 56
Execute count : 56
Execute count : 56
Execute count : 56
Execute count : 56
Execute count : 56
Execute time是取了每1000次调用的平均值,Execute count每5000ms输出一次总执行次数。很明显使用JDK8u202时在不断调用serverHelloDone,而74在调用56次后没有再调用过这个函数。
初始化握手时,serverHelloDone方法中,客户端会根据服务端返回加密套件决定加密方式,构造不同的Client Key Exchange消息;服务器如果允许重用该会话,则通过在Server Hello消息中设置相同的会话ID来应答。这样,客户端和服务器就可以利用原有会话的密钥和加密套件,不必重新协商,也就不再走serverHelloDone方法。
从现象来看,OracleJDK8u202没有复用会话,而是建立的新的会话。
2.4 水落石出
查看161的release notes,添加了TLS会话散列和扩展主密钥扩展支持,找到引入的一个还未修复的issue,对于带有身份验证的TLS的客户端,支持UseExtendedMasterSecret会破坏TLS-Session的恢复,导致不使用现有的TLS-Session,而执行新的Handshake。
OracleJDK8u161之后的版本,包括161版本,若复用会话时不能成功恢复Session,而是创建新的会话,会造成较大性能消耗,且积压的大量的不可复用的session造成GC压力变大;如果业务场景存在不变更证书密钥,需要复用会话,且对性能有要求,可通过添加参数-Djdk.tls.useExtendedMasterSecret=false来解决这个问题。
- 点赞
- 收藏
- 关注作者
评论(0)