最近有用户反馈思享汇论坛图片加载速度太慢,自从因为权限控制要求把 CDN 下了,这种问题完全在预料之中,每次从应用服务器加载几 MB 的图片,能不慢吗?但是这次用户说加载图片要一两分钟,慢得有点离谱,虽然已经不负责思享汇了,还是跟同事一起排查下系统问题。
前端页面
打开首页看下,好家伙,图片请求的次数和体积都把我吓到了。

按道理来说这接口一共就返回十五个帖子,每个帖子最多预览四张图片,极限情况也就六十个图片请求,再加上一些头像之类的,最多也就一百个左右吧。

Network 看下图片的网络请求,发现隐藏在 +20 的图片,也发起了请求。打开 Elements 选中一看,前端处理有问题,虽然是个图片预览,但是实际上 +20 内所有图片的元素全都加载到 DOM 了,只是没有显示出来,没有 Code Review 的代码有点任性。

另外这里还有个历史遗留问题,这四张图片看上去是 120×120 的缩略图,实际和我这网站一样,首页的缩略图加载的全是原图。缩略图需求还没做项目的功能开发就暂停了,一直搁置着这个事情。这两项一叠加,等于每个帖子都加载了数量不等的原图,共计一百多张。
前端图片全部加载这是个 BUG,也相对好改,准备让前端修复;缩略图功能因为改动较大,数据结构和前后端都有调整内容,而后台唯一的同事准备离职,这项目也是处于维护状态,就先不做了。
查看图片加载情况,发现下了 CDN 以后连客户端缓存都没有做,导致每次刷新页面,图片全都要到服务端重新 IO 一次,这个压力可想而知。

服务器状态
页面看完还要看下服务有问题没有,毕竟这个所有图片都要用 Java IO 的实现方式确实让人对性能心里没底。
今年系统换到容器化部署了,进到 Rancher 控制台发现有 4 个 pod,看了下日志,刷得飞快,主要就是文件读取的请求。选取其中一个打开命令行,top
命令看一下,瞬时 120 的 CPU 使用率,最低也是 90+。

看了下逻辑 CPU 有 16 个,应该是共享宿主机的 CPU,由于不清楚 CPU 限制了多少,不一定是满载情况。转到 Grafana 控制台,看到 CPU 限制就是 1 个,确实已经达到上限了,分配的参数可见官方文档。

回到容器,用 top -Hp 201
找线程,看到有几个线程 CPU 使用率居高不下:

但是比较奇怪的,读取文件是 IO 密集型操作,为啥 CPU 使用率这么高了,内存使用并没有突然上涨。
用 jstack 看下使用率最高的几个线程号,堆栈信息全都是一样的。
"http-nio-8080-exec-91" #186 daemon prio=5 os_prio=0 tid=0x00007fbbac053000 nid=0x191 runnable [0x00007fbb45c89000] java.lang.Thread.State: RUNNABLE at java.net.SocketInputStream.socketRead0(Native Method) at java.net.SocketInputStream.socketRead(SocketInputStream.java:116) at java.net.SocketInputStream.read(SocketInputStream.java:171) at java.net.SocketInputStream.read(SocketInputStream.java:141) at org.apache.http.impl.io.SessionInputBufferImpl.streamRead(SessionInputBufferImpl.java:137) at org.apache.http.impl.io.SessionInputBufferImpl.fillBuffer(SessionInputBufferImpl.java:153) at org.apache.http.impl.io.SessionInputBufferImpl.readLine(SessionInputBufferImpl.java:280) at org.apache.http.impl.conn.DefaultHttpResponseParser.parseHead(DefaultHttpResponseParser.java:138) at org.apache.http.impl.conn.DefaultHttpResponseParser.parseHead(DefaultHttpResponseParser.java:56) at org.apache.http.impl.io.AbstractMessageParser.parse(AbstractMessageParser.java:259) at org.apache.http.impl.DefaultBHttpClientConnection.receiveResponseHeader(DefaultBHttpClientConnection.java:163) at org.apache.http.impl.conn.CPoolProxy.receiveResponseHeader(CPoolProxy.java:157) at org.apache.http.protocol.HttpRequestExecutor.doReceiveResponse(HttpRequestExecutor.java:273) at com.amazonaws.http.protocol.SdkHttpRequestExecutor.doReceiveResponse(SdkHttpRequestExecutor.java:82) at org.apache.http.protocol.HttpRequestExecutor.execute(HttpRequestExecutor.java:125) at org.apache.http.impl.execchain.MainClientExec.execute(MainClientExec.java:272) at org.apache.http.impl.execchain.ProtocolExec.execute(ProtocolExec.java:186) at org.apache.http.impl.client.InternalHttpClient.doExecute(InternalHttpClient.java:185) at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:83) at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:56) at com.amazonaws.http.apache.client.impl.SdkHttpClient.execute(SdkHttpClient.java:72) at com.amazonaws.http.AmazonHttpClient$RequestExecutor.executeOneRequest(AmazonHttpClient.java:1297) at com.amazonaws.http.AmazonHttpClient$RequestExecutor.executeHelper(AmazonHttpClient.java:1113) at com.amazonaws.http.AmazonHttpClient$RequestExecutor.doExecute(AmazonHttpClient.java:770) at com.amazonaws.http.AmazonHttpClient$RequestExecutor.executeWithTimer(AmazonHttpClient.java:744) at com.amazonaws.http.AmazonHttpClient$RequestExecutor.execute(AmazonHttpClient.java:726) at com.amazonaws.http.AmazonHttpClient$RequestExecutor.access$500(AmazonHttpClient.java:686) at com.amazonaws.http.AmazonHttpClient$RequestExecutionBuilderImpl.execute(AmazonHttpClient.java:668) at com.amazonaws.http.AmazonHttpClient.execute(AmazonHttpClient.java:532) at com.amazonaws.http.AmazonHttpClient.execute(AmazonHttpClient.java:512) at com.amazonaws.services.s3.AmazonS3Client.invoke(AmazonS3Client.java:4926) at com.amazonaws.services.s3.AmazonS3Client.invoke(AmazonS3Client.java:4872) at com.amazonaws.services.s3.AmazonS3Client.getObject(AmazonS3Client.java:1472) at com.cmskframework.modules.system.service.impl.AptFileServiceImpl.getObject(AptFileServiceImpl.java:132) at com.cmskframework.modules.system.service.impl.AptFileServiceImpl\$\$FastClassBySpringCGLIB$$69fbc6f0.invoke(<generated>) at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218) at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:684) at com.cmskframework.modules.system.service.impl.AptFileServiceImpl\$\$EnhancerBySpringCGLIB$$dab8ff39.getObject(<generated>) at com.cmskframework.modules.system.controller.FileController.getFiles(FileController.java:142) ...
堆栈中的 FileController.getFiles
,就是系统从对象存储读取文件的入口,看下相关代码吧。
后台代码
找到文件读取的代码:
/** * 基于objectKey获取object */ @Override public S3Object getObject(String key) throws IOException { S3Object s3Object = s3Client.getObject(new GetObjectRequest(bucketName, key)); return s3Object; }
代码的问题就出在 getObject
方法,但是这个方法是 AWS 的 SDK 提供的,是不是传输的文件有问题,导致了读取 BUG?
在 tomcat 日志找到导致 "http-nio-8080-exec-91"
出问题的的文件 key,访问正常返回了图片,不是文件问题。
找到 s3Client
的创建方法,发现初始化的时候设置了超时时间,而且 ClientConfiguration
的构造方法也设置了 socketTimeout
和 connectionTimeout
。
但是这段代码也还是有个问题,以我们这个请求量,maxConnections
设置成 10 确实太保守了,默认的 50 都不太够用。
/** * 初始化Amazon S3 Client */ @PostConstruct public void initClint() { endPoint = environment.getProperty("cmsk.endPoint"); accessKey = environment.getProperty("cmsk.accessKey"); secretKey = environment.getProperty("cmsk.secretKey"); bucketName = environment.getProperty("cmsk.bucketName"); AWSCredentials awsCredentials = new BasicAWSCredentials(accessKey, secretKey); ClientConfiguration clientConfig = new ClientConfiguration(); clientConfig.setMaxErrorRetry(3); clientConfig.setConnectionTimeout(20 * 1000); clientConfig.setMaxConnections(10); clientConfig.setSignerOverride("S3SignerType"); clientConfig.setProtocol(Protocol.HTTP); s3Client = new AmazonS3Client(awsCredentials, clientConfig); s3Client.setEndpoint(endPoint); s3Client.setS3ClientOptions(S3ClientOptions.builder().setPathStyleAccess(true).build()); log.info("s3Client初始化完成-----> endPoint:{}", endPoint); }
这么看还是 AWS 源代码的问题,本质上是用 Apache HttpClient
发起了一个 HTTP 请求,最后在 java.net.SocketInputStream.socketRead0
这个本地方法出了问题,没法正常读取,也没有超时。
在网上一番搜索,发现有大量相同问题的反馈,Java 官方 8075484、8233660、8143992,Stackoverflow1、Stackoverflow2。
大多数给出的解决方案是设置超时时间,但是我们的超时时间并未起作用。第二种方案是定时清理 Httpclient 连接,这个实现比较复杂。第三种方案是使用非阻塞版本的 Apache HttpClient
,这需要修改 AWS 的 SDK。
查看 SDK
找到 AWS 的官方 github,想看下有没有相似的 Issue,意外发现了这个:
A version 2.x of the SDK is generally available. It is a major rewrite of the 1.x code base, built on top of Java 8+ and adds several frequently requested features. These include support for non-blocking I/O, improved start-up performance, automatic iteration over paginated responses and the ability to plug in a different HTTP implementation at run time.
查看 V2 版本的 SDK 文档,用 Netty 实现了非阻塞 IO,要求只有 Java 1.8+,完美符合我们的需求。
修复方案
最终确定了几个需要解决的问题:
- 前端不再加载不需要展示的图片。
- 加大 S3 的最大连接数。
- 文件读取 SDK 替换成非阻塞的实现方案。
- 代码增加缓存控制头,避免每次都重新发送图片请求。
发表回复