本文从一次线上图片服务的OOM说起,通过分析当时的gc日志来分析其原因。
记录
由于Markdown导入图片的方式并不是很友好,本文又包含大量的截图,所以我将相关GC的图片和信息记录到了Evernote里, 可以访问这里。
分析
该服务提供对图片的下载功能,由于客户端升级后下载图片尺的 width
和 height
发生了变化,导致和之前预存储的图片不一致,服务器此时会去下载原图(8M)然后对该图进行压缩处理(按照客户端上传的尺寸)后返回,这是一个非常消耗CPU
以及需要大量的堆内存来存储字节数组的操作。短时间内的高并发请求图一张图片导致了JVM报错 OOM
。
分析出问题的原因后优化方案就很清楚了:
- 对特殊尺寸的图片进行了上传时预存储一份缩略尺寸,大大减少下载时读取数据miss导致二次读取原图+压缩的可能性。
- 同时考虑到上传时压缩存储可能失败,所以做了兜底逻辑:在第一次下载原图并压缩后将会再次进行缩略图存储,尽可能减少其他miss请求再次执行相同的高CPU和内存的消耗操作。
压力测试
改进完毕后如何评估我们的效果和性能呢?
对Web服务测试首选Apache Bench的 ab
命令,它会创建很多的并发访问线程,模拟多个访问者同时对某一URL地址进行访问。它的测试目标是基于URL的,因此,既可以用来测试Apache
的负载压力,也可以测试Nginx、Lighthttp、Tomcat、IIS等其它Web服务器的压力。
ab
命令对发出负载的计算机要求很低,既不会占用很高CPU,也不会占用很多内存,但却会给目标服务器造成巨大的负载,其原理类似CC攻击。自己测试使用也须注意,否则一次上太多的负载,可能造成目标服务器因资源耗完,严重时甚至导致死机。
在Linux服务器下使用如下命令进行安装:
sudo yum -y install httpd-tools
ab
命令的参数很多,一般我们用 -c
和 -n
参数就可以了通过100个并发请求7000次,并设置 keepalive
属性,ab -k -n 7000 -c 100
url,多轮测试后QPS可以最高可达3000+。
下面是对一张50K的图片进行某一轮压测时的简要信息(删除了敏感部分):
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
Concurrency Level: 100
Time taken for tests: 2.084 seconds
Complete requests: 7000
Failed requests: 0
Write errors: 0
Keep-Alive requests: 7000
Total transferred: 382123000 bytes
HTML transferred: 379099000 bytes
Requests per second: 3359.59 [#/sec] (mean)
Time per request: 29.766 [ms] (mean)
Time per request: 0.298 [ms] (mean, across all concurrent requests)
Transfer rate: 179098.09 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.3 0 2
Processing: 11 27 66.5 18 1026
Waiting: 9 24 66.5 16 1023
Total: 11 27 66.5 18 1026
Percentage of the requests served within a certain time (ms)
50% 18
66% 19
75% 20
80% 22
90% 31
95% 39
98% 75
99% 264
100% 1026 (longest request)
在测试时发现办公网络压测的效果并不理想,只能达到几百QPS,原因是网络延迟很大,所以选择了和服务器同网段的一台服务器进行压测,可以达到3000+,
如果选择和Web服务同一台机器进行压测的话,效果最高可以达到4600+,服务器没有发生
FullGC
(因为读到数据后直接返回给客户端,没有驻留内存),CPU占用率最高打到1000%,压测的效果达到了,数据也很不错(如果使用单台办公网机器压测CPU只能压到50%)。
再压
由于对于缩略图的压测效果基本达到预期,但原图并未达到预期,所以让原图下载尽可能的高成了一个新的课题,同时也发现了之前测试的不足:
- 使用单客户端压测并未能完全压榨服务器的性能
- 使用了公网域名进行压测,导致网络延迟严重影响测试效果
- 堆内存并未和线上对齐,导致测试数据并不完全有说服力
在解决了上述问题后,使用多个客户端进行压测,最终50KB的图片可以压到 1w QPS
。
当原图为 2M
时最多只能压到 1100QPS
,无论如何调整参数,包括堆内存、Tomcat线程数以及压测客户端数目都不能有明显突破,数据基本保持比较稳定的水准,下图是其中一个客户端的数据:
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
Concurrency Level: 100
Time taken for tests: 13.712 seconds
Complete requests: 7000
Failed requests: 0
Write errors: 0
Keep-Alive requests: 7000
Total transferred: 14657258000 bytes
HTML transferred: 14656334000 bytes
Requests per second: 510.51 [#/sec] (mean)
Time per request: 195.881 [ms] (mean)
Time per request: 1.959 [ms] (mean, across all concurrent requests)
Transfer rate: 1043909.08 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.1 0 2
Processing: 41 193 63.6 179 785
Waiting: 35 186 64.5 174 777
Total: 41 193 63.6 179 785
Percentage of the requests served within a certain time (ms)
50% 179
66% 195
75% 207
80% 215
90% 242
95% 293
98% 414
99% 493
100% 785 (longest request)
它的瓶颈在哪里?
此时服务器的GC压力并不大,没有FullGC,YoungGC处于基本可控的频率,CPU最高压到2400%左右。
分析一下 Transfer rate
这个参数,它标识平均每秒网络上的流量,可以帮助排除是否存在网络流量过大导致响应时间延长的问题,单位是千字节每秒,这里是 1043909 [Kbytes/sec]
。因为我们有两个客户端,所以加和之后
再转换为 G
单位,结果是 2
,这里和我们的网卡带宽基本上是一致的。也就是说网卡被打满了,瓶颈在网卡。
如何看我们的网卡带宽?
首先查看有多少个网卡,使用命令:ifconfig -a
其次使用ethtool命令,参数是网卡名,如ethtool eth0,需要关注里面的Speed指标,我的服务器显示20000Mb/s,即20w的Mbit per sec,注意这里是比特
所以这里的20w换算成字节是多少呢?20000/8/1024=2.44GB
关于不使用公网域名测试的原因
例如客户端在北京,服务端在上海,两地直线距离为1300公里,那么1次RTT时间=1300×2/(300000×2/3)=13毫秒(光在真空中传输速度为每秒30万公里, 这里假设光纤的速度为光速的2/3),那么客户端在1秒内大约只能执行80次左右的命令。
References
- Java 9 - The (G1) GC Awakens!
- Garbage First Garbage Collector Tuning
- 详解 JVM Garbage First(G1) 垃圾收集器
- G1 Garbage Collector in Action
- Web性能压力测试工具之ApacheBench(ab)详解
- 初步诊断你的GC
- 频繁GC (Allocation Failure)及young gc时间过长分析
- iostat 监视I/O子系统
本文首次发布于 LiuShuo’s Blog, 转载请保留原文链接.