记一次因为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("<!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工具分析如下:

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343

推荐阅读更多精彩内容