记一次因为webmagic监测导致的OOM,从而导致的节点宕机

问题发现

问题是从监测云迭代一测试才发现的,测试发现监测云只要一开启比较多站点的监测之后一段时间,就会出现监测云所有功能卡顿,最后出现一直pending的情况,甚至直接显示网关错误(此时节点已经全部宕机)

问题重现

在监测云测试环境开启所有站点的监测任务,等待一段时间,发现测试环境所有功能开始变得卡顿,并且再一段时间之后发现所有功能pending的情况,此时进入服务器查看cpu以及内存使用信息,发现cpu飙满,内存也已经占满,并且已经有一个节点挂掉了,如下图。


image.png

问题定位

(1)出现上面情况第一反应,查看应用的日志,看一下现在工程在做什么,打开日志发现一直在刷下载的日志,此时可以确定确实是监测导致的系统卡顿以及pending的问题
(2)这时已经确定确实是监测导致的cpu和内存飙高,那么到底是项目中哪个位置的代码导致的呢?此时想到利用jstack命令可以看到打印出线程信息:
      命令:jstack -l -p 线程ID >>/temp/1.txt
但是通过查看jstack日志看到很多BLOCKED线程,基本都是spider的线程BLOCKED,而这里就是使用的父spider,根本获取不到其他信息


image.png

(3)通过查看jstack日志没有得到我想要的,那么这时想到,因为内存满了,所以可以去查看应用打出的GC日志,通过查看GC日志发现,在Full GC 之后,内存居然不减反增,那么这时可能有两个原因:
      a.内存泄露
      b.监测代码有使用软引用的地方
通过检查代码,确实是有地方是使用了软引用去缓存当前下载页面的父页面内容,虽然考虑到软引用应该不会导致应用内存OOM(在此时还只是猜测是内存溢出导致的应用挂掉。不能确定),但是为了确定是不是内存溢出导致的宕机,因此将代码中的软引用使用其他方式替代了。
(4)在修改软引用之后,重新做问题复现,通过查看GC日志发现Full GC之后,内存还是不减反增,并且结合如下两图可以确定确实是内存溢出导致的宕机(因此在内存满后不久,一个节点直接挂了)


image.png

image.png

(5)但是由于只是通过应用日志、GC日志、jstack日志也不能清楚的知道到底是什么对象内存导致内存溢出,因为jstack通过线程看,也只能定位在如下图位置代码的问题,而这个super实现是spider自己的实现,我根本不知道是他里面哪个位置导致的,这时我已经不知道该怎么办了,我寻求了帮助,第一次接触到了jmap这个东西,他和jstack一样都是jdk自带的分析工具,它能够记录下应用的OOM的时候内存对象的占用情况,并且能够定位到是具体哪个对象占用很大


image.png

(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/),界面如下:

image.png

(8)通过分析dump文件发现spider里面如下导致占用内存非常大:


image.png

image.png

image.png

image.png

这时发现spider会去下载mp4,并缓存这种大文件,那么需要找到缓存的代码:


image.png
image.png

image.png

image.png

(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("")
                        && !pageCheck.startsWith("")
                        && (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工具分析如下:

image.png

image.png

image.png

那么此时,就想找到到底是哪个页面会这么的大?
image.png

页面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是没有的,并且它是分块传输。


image.png

image.png

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)解决这个问题之后,发现自己有哪些提升?

你可能感兴趣的:(记一次因为webmagic监测导致的OOM,从而导致的节点宕机)