记录一次线上图片服务的OOM分析

An OOM Experience in Prod Service

Posted by LiuShuo on October 13, 2019

本文从一次线上图片服务的OOM说起,通过分析当时的gc日志来分析其原因。

记录

由于Markdown导入图片的方式并不是很友好,本文又包含大量的截图,所以我将相关GC的图片和信息记录到了Evernote里, 可以访问这里

分析

该服务提供对图片的下载功能,由于客户端升级后下载图片尺的 widthheight 发生了变化,导致和之前预存储的图片不一致,服务器此时会去下载原图(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

本文首次发布于 LiuShuo’s Blog, 转载请保留原文链接.