问题发现
问题是从监测云迭代一测试才发现的,测试发现监测云只要一开启比较多站点的监测之后一段时间,就会出现监测云所有功能卡顿,最后出现一直pending的情况,甚至直接显示网关错误(此时节点已经全部宕机)
问题重现
在监测云测试环境开启所有站点的监测任务,等待一段时间,发现测试环境所有功能开始变得卡顿,并且再一段时间之后发现所有功能pending的情况,此时进入服务器查看cpu以及内存使用信息,发现cpu飙满,内存也已经占满,并且已经有一个节点挂掉了,如下图。
问题定位
(1)出现上面情况第一反应,查看应用的日志,看一下现在工程在做什么,打开日志发现一直在刷下载的日志,此时可以确定确实是监测导致的系统卡顿以及pending的问题
(2)这时已经确定确实是监测导致的cpu和内存飙高,那么到底是项目中哪个位置的代码导致的呢?此时想到利用jstack命令可以看到打印出线程信息:
命令:jstack -l -p 线程ID >>/temp/1.txt
但是通过查看jstack日志看到很多BLOCKED线程,基本都是spider的线程BLOCKED,而这里就是使用的父spider,根本获取不到其他信息
(3)通过查看jstack日志没有得到我想要的,那么这时想到,因为内存满了,所以可以去查看应用打出的GC日志,通过查看GC日志发现,在Full GC 之后,内存居然不减反增,那么这时可能有两个原因:
a.内存泄露
b.监测代码有使用软引用的地方
通过检查代码,确实是有地方是使用了软引用去缓存当前下载页面的父页面内容,虽然考虑到软引用应该不会导致应用内存OOM(在此时还只是猜测是内存溢出导致的应用挂掉。不能确定),但是为了确定是不是内存溢出导致的宕机,因此将代码中的软引用使用其他方式替代了。
(4)在修改软引用之后,重新做问题复现,通过查看GC日志发现Full GC之后,内存还是不减反增,并且结合如下两图可以确定确实是内存溢出导致的宕机(因此在内存满后不久,一个节点直接挂了)
(5)但是由于只是通过应用日志、GC日志、jstack日志也不能清楚的知道到底是什么对象内存导致内存溢出,因为jstack通过线程看,也只能定位在如下图位置代码的问题,而这个super实现是spider自己的实现,我根本不知道是他里面哪个位置导致的,这时我已经不知道该怎么办了,我寻求了帮助,第一次接触到了jmap这个东西,他和jstack一样都是jdk自带的分析工具,它能够记录下应用的OOM的时候内存对象的占用情况,并且能够定位到是具体哪个对象占用很大
(6)那么我使用jmap命令输出dump文件:
命令:
jmap -dump:live,format=b,file=/upload/dumpwmcs_1.txt 9
解释:live表示在使用的,format=b表示二进制的
当然也可以直接在启动脚本配置如下参数:
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=$proj_dir/logs/java_pid_%p.hprof
(7)输出的dump文件拿到之后,因为文件很大并且比较特殊,所以需要用到专门的分析内存的软件来分析,推荐MAT(Eclipse Memory Analyzer,地址:https://www.eclipse.org/mat/),界面如下:
(8)通过分析dump文件发现spider里面如下导致占用内存非常大:
这时发现spider会去下载mp4,并缓存这种大文件,那么需要找到缓存的代码:
(9)既然找到了影响内存的代码,并且spider提供了handleResponse的重写,所以,重写这个方法:
@Override
protected Page handleResponse(Request request, String charset, HttpResponse httpResponse, Task task) throws IOException {
if (httpResponse.getEntity().getContentLength() > 10 * 1024 * 1024) {
log.warn("=================contentLength=" + httpResponse.getEntity().getContentLength() + ", url=" + new PlainText(request.getUrl()));
return new Page();
}
return super.handleResponse(request, charset, httpResponse, task);
}
问题反复
希望总是美好的,本以为重写handleResponse方法之后这个问题就能解决,结果发现处理之后,去测还是发现内存占满,直接宕机,通过分析发现还是存在大文件在内存中,直接导致OOM
处理阶段一:
在重写的spider的handleResponse方法中做如下处理:
@Override
protected Page handleResponse(Request request, String charset, HttpResponse httpResponse, Task task) throws IOException {
EnumUrlType urlType = WebPageUtil.getUrlType(request.getUrl());
if (urlType != EnumUrlType.HTML) {
Page page = new Page();
int statusCode = httpResponse.getStatusLine().getStatusCode();
long contentLength = httpResponse.getEntity().getContentLength();
if (contentLength > 0) {
page.setUrl(new PlainText(request.getUrl()));
page.setRequest(request);
page.setStatusCode(statusCode);
page.setDownloadSuccess(true);
log.warn("=================contentLength1=" + contentLength + ", url=" + new PlainText(request.getUrl()));
} else {
throw new IOException("ContentLength is zero, url=" + new PlainText(request.getUrl()));
}
return page;
}
if (httpResponse.getEntity().getContentLength() > 10 * 1024 * 1024) {
long contentLength = httpResponse.getEntity().getContentLength();
log.warn("=================contentLength2=" + contentLength + ", url=" + new PlainText(request.getUrl()));
return new Page();
}
测试阶段一:
通过测试发现仍然会导致节点内存占满,然后宕机,仔细检查代码发现,只是通过url后缀名判断url类型不准确,还是有大文件进入了spider,并且通过日志也是验证了这个问题,如下链接就是监测网站中某站点的两个下载附件的链接,但是后缀是jsp,被程序认为是html页面:
http://www.ms.gov.cn/system/resource/opinioncollection/download.jsp?id=63
http://www.ms.gov.cn/system/resource/opendata/download.jsp?opendataid=483&id=423&downloadSource=netizen
因此,我需要修改判断链接是文件还是html的方式:url后缀判断和通过内容结合来判断类型
处理阶段二:
修改判断链接是文件还是html的逻辑,在download之前添加:
private boolean isHtml(Request request) {
BufferedInputStream bis = null;
HttpURLConnection huc = null;
try {
URL urlObj = new URL(request.getUrl());
huc = (HttpURLConnection) urlObj.openConnection();
huc.setUseCaches(false);
huc.setConnectTimeout(10_000);
huc.connect();
bis = new BufferedInputStream(huc.getInputStream());
byte[] b = new byte[200];
bis.read(b);
String s = new String(b).trim();
String pageCheck = s.toLowerCase();
String contentType = HttpURLConnection.guessContentTypeFromStream(bis);
if (!pageCheck.startsWith("<!doctype html")
&& !pageCheck.startsWith("<html")
&& !pageCheck.contains("<html>")
&& !pageCheck.startsWith("<xml")
&& !pageCheck.contains("<xml>")
&& (StringUtil.isEmpty(contentType) || !contentType.contains("text/html"))) {
int responseCode = huc.getResponseCode();
if (responseCode == HttpStatus.SC_NOT_FOUND || (responseCode >= HttpStatus.SC_INTERNAL_SERVER_ERROR && responseCode <= HttpStatus.SC_INSUFFICIENT_STORAGE)) {
//排除首页
if (request.getUrl().equals(baseUrl)) {
return false;
}
log.warn("===========file=" + request.getUrl());
insertInvalidLink(request, responseCode);
}
} else {
return true;
}
} catch (MalformedURLException | SocketTimeoutException | UnknownHostException e) {
if (huc != null) {
huc.disconnect();
}
//排除首页
if (request.getUrl().equals(baseUrl)) {
return false;
}
log.warn("===========exception=" + request.getUrl());
insertInvalidLink(request, 0);
log.error("", e);
} catch (IOException e) {
log.error("", e);
} finally {
if (huc != null) {
huc.disconnect();
}
}
return false;
}
HttpURLConnection.guessContentTypeFromStream(bis)方法相对于通过url后缀判断更加准确,但是也有是html 页面但获取不到ContentType的时候,所以需要两者结合,参考链接:http://developer.51cto.com/art/201205/337516.htm
并且在handleResponse中添加的后缀判定保留,不是hmtl类型不要添加内容到page中,这样process也不会再去这个链接下获取链接(因为是文件)
测试阶段二:
测试发现,两个节点内存还是会飙满,并且在一段时间后,节点2宕机,这时将dump文件下载下来,使用MAT工具分析如下:
那么此时,就想找到到底是哪个页面会这么的大?
页面url:http://www.ms.gov.cn/xxgk/xxgk_content.jsp?urltype=news.NewsContentUrl&wbtreeid=4719&wbnewsid=1040956
经过确认,这个页面确实有352M,按照正常情况,一个页面不会如此之大,那么就要规避它。
为什么限制了长度的还是进入了这个父的handleResponse?
那么只有一个解释,httpResponse.getEntity().getContentLength()获取到的长度为0或者-1。测试发现这个页面没有返回ContentLength,没返回时默认为为-1,还是进入的父handleResponse。
处理阶段三:
(1)在webmagic中的site对象上设置header属性Accept-Encoding
Site site = Site.me().setRetryTimes(3).setSleepTime(10).setTimeOut(15000).addHeader("Accept-Encoding","identity");
(2)在handleResponse重写方法中,httpResponse.getEntity().getContentLength()的判断加上-1和0的判断。
后续还要尽可能的处理-1的情况,-1主要原因是http 响应头中的Transfer-Encoding: chunked属性,当http 响应头中有这个属性时,content-length是没有的,并且它是分块传输。
http协议有这样一段描述:“如果head中有Content-Length,那么这个Content-Length既表示实体长度,又表示传输长度。如果实体长度和传输长度不相等(比如说设置了Transfer-Encoding),那么则不能设置Content-Length。如果设置了Transfer-Encoding,那么Content-Length将被忽视”
测试阶段三:
在测试环境开启所有站点监测验证,未再出现宕机现象,内存和cpu也趋于正常
问题解决
(1)借用了:应用日志、gc日志、jstack日志、jmap日志、Eclipse Memory Analyzer(MAT)工具
(2)寻求了林哥、晨哥的帮助
问题引发的思考
(1)是什么导致花费了近八天的时间才解决这个问题?
(2)代码的严谨性是否有待商榷?
(3)对于用户的真实使用场景是否了解?
(4)对于webmagic的原理、jdk自带工具、外部工具mat的使用是否熟悉?
(5)对于线程、gc内存相关是否更加了解?
(6)解决这个问题之后,发现自己有哪些提升?