热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

记一次内存溢出的分析经历

作者:Jantihttps:www.cnblogs.comsuperfjp8474288.html说在前面的话朋友,你经历过部署好的服务突然内存溢出吗&

640?wx_fmt=gif

作者:Janti

https://www.cnblogs.com/superfj/p/8474288.html

说在前面的话

朋友,你经历过部署好的服务突然内存溢出吗?

你经历过没有看过Java虚拟机,来解决内存溢出的痛苦吗?

你经历过一个BUG,百思不得其解,头发一根一根脱落的烦恼吗?

我知道,你有过!

但是我还是要来说说我的故事.................. 


背景:

有一个项目做一个系统,分客户端和服务端,客户端用c++写的,用来收集信息然后传给服务端(客户端的数量还是比较多的,正常的有几千个),

服务端用Java写的(带管理页面),属于RPC模式,中间的通信框架使用的是thrift。

thrift很多优点就不多说了,它是facebook的开源的rpc框架,主要是它能够跨语言,序列化速度快,但是他有个不讨喜的地方就是它必须用自己IDL来定义接口

thrift版本:0.9.2.

问题定位与分析

步骤一.初步分析

客户端无法连接服务端,查看服务器的端口开启状况,服务端口并没有开启。于是启动服务端,启动几秒后,服务端崩溃,重复启动,服务端依旧在启动几秒后崩溃。

步骤二.查看服务端日志分析

分析得知是因为java.lang.OutOfMemoryError: Java heap space(堆内存溢出)导致的服务崩溃。

客户端搜集的主机信息,主机策略都是放在缓存中,可能是因为缓存较大造成的,但是通过日志可以看出是因为Thrift服务抛出的堆内存溢出异常与缓存大小无关。

步骤三.再次分析服务端日志

可以发现每次抛出异常的时候都会伴随着几十个客户端在向服务端发送日志,往往在发送几十条日志之后,服务崩溃。可以假设是不是堆内存设置的太小了?

查看启动参数配置,最大堆内存为256MB。修改启动配置,启动的时候分配更多的堆内存,改成java -server -Xms512m -Xmx768m。

结果是,能坚持多一点的时间,依旧会内存溢出服务崩溃。得出结论,一味的扩大内存是没有用的。

为了证明结论是正确的,做了这样的实验:

  • 内存设置为256MB,在公司服务器上部署了服务端,使用Java VisualVM远程监控服务器堆内存。

  • 模拟客户现场,注册3000个客户端,使用300个线程同时发送日志。

  • 结果和想象的一样,没有出现内存溢出的情况,如下图:

    640?wx_fmt=png

  • 上图是Java VisualVM远程监控,在压力测试的情况下,没有出现内存溢出的情况,256MB的内存肯定够用的。

步骤四.回到thrift源码中,查找关键问题

服务端采用的是Thrift框架中TThreadedSelectorServer这个类,这是一个NIO的服务。下图是thrift处理请求的模型:

640?wx_fmt=png

说明:

  • 一个AcceptThread执行accept客户端请求操作,将accept到的Transport交给SelectorThread线程, 

  • AcceptThread中有个balance均衡器分配到SelectorThread;SelectorThread执行read,write操作,

  • read到一个FrameBuffer(封装了方法名,参数,参数类型等数据,和读取写入,调用方法的操作)交给WorkerProcess线程池执行方法调用。

  • 内存溢出就是在read一个FrameBuffer产生的。

步骤五.细致一点描述thrift处理过程

1.服务端服务启动后,会listen()一直监听客户端的请求,当收到请求accept()后,交给线程池去处理这个请求

2.处理的方式是:首先获取客户端的编码协议getProtocol(),然后根据协议选取指定的工具进行反序列化,接着交给业务类处理process()

3.process的顺序是,先申请临时缓存读取这个请求数据,处理请求数据,执行业务代码,写响应数据,最后清除临时缓存

总结:thrift服务端处理请求的时候,会先反序列化数据,接着申请临时缓存读取请求数据,然后执行业务并返回响应数据,最后请求临时缓存

所以压力测试的时候,thrift性能很高,而且内存占用不高,是因为它有自负载调节,使用NIO模式缓存,并使用线程池处理业务,每次处理完请求之后及时清除缓存。

步骤六.研读FrameBuffer的read方法代码

可以排除掉没有及时清除缓存的可能,方向明确,极大的可能是在申请NIO缓存的时候出现了问题,回到thrift框架,查看FrameBuffer的read方法代码:

public boolean read() {         // try to read the frame size completely 
            if (this.state_ == AbstractNonblockingServer.FrameBufferState.READING_FRAME_SIZE) {
                if (!this.internalRead()) {
                    return false;
                }
         // if the frame size has been read completely, then prepare to read the actual time
                if (this.buffer_.remaining() != 0) {
                    return true;
                }

                int frameSize = this.buffer_.getInt(0);
                if (frameSize <&#61; 0) {
                    this.LOGGER.error("Read an invalid frame size of " &#43; frameSize &#43; ". Are you using TFramedTransport on the client side?");
                    return false;
                }
          // if this frame will always be too large for this server, log the error and close the connection. 

if ((long)frameSize > AbstractNonblockingServer.this.MAX_READ_BUFFER_BYTES) {
            this.LOGGER.error("Read a frame size of " &#43; frameSize &#43; ", which is bigger than the maximum allowable buffer size for ALL connections.");
            return false;
        }

        if (AbstractNonblockingServer.this.readBufferBytesAllocated.get() &#43; (long)frameSize > AbstractNonblockingServer.this.MAX_READ_BUFFER_BYTES) {
            return true;
        }

        AbstractNonblockingServer.this.readBufferBytesAllocated.addAndGet((long)(frameSize &#43; 4));
        this.buffer_ &#61; ByteBuffer.allocate(frameSize &#43; 4);
        this.buffer_.putInt(frameSize);
        this.state_ &#61; AbstractNonblockingServer.FrameBufferState.READING_FRAME;
    }

    if (this.state_ &#61;&#61; AbstractNonblockingServer.FrameBufferState.READING_FRAME) {
        if (!this.internalRead()) {
            return false;
        } else {
            if (this.buffer_.remaining() &#61;&#61; 0) {
                this.selectionKey_.interestOps(0);
                this.state_ &#61; AbstractNonblockingServer.FrameBufferState.READ_FRAME_COMPLETE;
            }

            return true;
        }
    } else {
        this.LOGGER.error("Read was called but state is invalid (" &#43; this.state_ &#43; ")");
        return false;
    }
}

说明&#xff1a;

  • MAX_READ_BUFFER_BYTES这个值即为对读取的包的长度限制&#xff0c;如果超过长度限制&#xff0c;就不会再读了/

  • 这个MAX_READ_BUFFER_BYTES是多少呢&#xff0c;thrift代码中给出了答案&#xff1a;

public abstract static class AbstractNonblockingServerArgs<T extends AbstractNonblockingServer.AbstractNonblockingServerArgs<T>> extends AbstractServerArgs<T{
     
    public long maxReadBufferBytes &#61; 9223372036854775807L;

    public AbstractNonblockingServerArgs(TNonblockingServerTransport transport) {
        super(transport);
        this.transportFactory(new Factory());
    }
}

  • 从上面源码可以看出&#xff0c;默认值居然给到了long的最大值9223372036854775807L。

所以thrift的开发者是觉得使用thrift程序员不够觉得内存不够用吗&#xff0c;这个换算下来就是1045576TB&#xff0c;这个太夸张了&#xff0c;这等于没有限制啊&#xff0c;所以肯定不能用默认值的。

步骤七.通信数据抓包分析

需要可靠的证据证明一个客户端通信的数据包的大小。

640?wx_fmt&#61;png

这个是我抓到包最大的长度&#xff0c;最大一个包长度只有215B&#xff0c;所以需要限制一下读取大小

步骤八&#xff1a;踏破铁鞋无觅处

在论坛中&#xff0c;看到有人用http请求thrift服务端出现了内存溢出的情况&#xff0c;所以我抱着试试看的心态&#xff0c;在浏览器中发起了http请求&#xff0c;

果不其然&#xff0c;出现了内存溢出的错误&#xff0c;和客户现场出现的问题一摸一样。这个读取内存的时候数量过大&#xff0c;超过了256MB。

很明显的一个问题&#xff0c;正常的一个HTTP请求不会有256MB的&#xff0c;考虑到thrift在处理请求的时候有反序列化这个操作。

可以做出假设是不是反序列化的问题&#xff0c;不是thrift IDL定义的不能正常的反序列化&#xff1f;

验证这个假设&#xff0c;我用Java socket写了一个tcp客户端&#xff0c;向thrift服务端发送请求&#xff0c;果不其然&#xff01;java.lang.OutOfMemoryError: Java heap space。

这个假设是正确的,客户端请求数据不是用thrift IDL定义的话&#xff0c;无法正常序列化&#xff0c;序列化出来的数据会异常的大&#xff01;大到超过1个G的都有。

步骤九. 找到原因

某些客户端没有正常的序列化消息&#xff0c;导致服务端在处理请求的时候&#xff0c;序列化出来的数据特别大&#xff0c;读取该数据的时候出现的内存溢出。

查看维护记录&#xff0c;在别的客户那里也出现过内存溢出导致服务端崩溃的情况&#xff0c;通过重新安装客户端&#xff0c;就不再复现了。

所以可以确定&#xff0c;客户端存在着无法正常序列化消息的情况。考虑到&#xff0c;客户端量比较大&#xff0c;一个一个排除&#xff0c;再重新安装比较困难&#xff0c;工作量很大&#xff0c;所以可以从服务端的角度来解决问题&#xff0c;减少维护工作量。

最后可以确定解决方案了&#xff0c;真的是废了很大的劲&#xff0c;不过也是颇有收获

问题解决方案

非常简单

1.在构造TThreadedSelectorServer的时候&#xff0c;增加args.maxReadBufferBytes &#61; 1*1024 * 1024L;也就是说修改maxReadBufferBytes的大小&#xff0c;设置为1MB。

客户端与服务端通过thrift通信的数据包&#xff0c;最大十几K&#xff0c;所以设置最大1MB&#xff0c;是足够的。代码部分修改完成&#xff0c;版本不做改变。

修改完毕后&#xff0c;这次进行了异常流测试&#xff0c;发送了http请求&#xff0c;使服务端无法正常序列化。

2.服务端处理结果如下&#xff1a;

640?wx_fmt&#61;png

thrift会抛出错误日志&#xff0c;并直接没有读这个消息&#xff0c;返回false,不处理这样的请求&#xff0c;将其视为错误请求。

3.国外有人对thrift一些server做了压力测试&#xff0c;如下图所示&#xff1a;

640?wx_fmt&#61;jpeg

使用thrift中的TThreadedSelectorServer吞吐量达到18000以上

由于高性能&#xff0c;申请内存和清除内存的操作都是非常快的&#xff0c;平均3ms就处理了一个请求。

所以是推荐使用TThreadedSelectorServer

4.修改启动脚本&#xff0c;增大堆内存&#xff0c;分配单独的直接内存。

  • 修改为java -server -Xms512m -Xmx768m -XX:MaxPermSize&#61;256m -XX:NewSize&#61;256m -XX:MaxNewSize&#61;512m -XX:MaxDirectMemorySize&#61;128M。

  • 设置持久代最大值 MaxPermSize:256m

  • 设置年轻代大小 NewSize:256m

  • 年轻代最大值 MaxNewSize:512M

  • 最大堆外内存&#xff08;直接内存&#xff09;MaxDirectMemorySize&#xff1a;128M

5.综合论坛中&#xff0c;StackOverflow一些同僚的意见&#xff0c;在使用TThreadedSelectorServer时&#xff0c;将读取内存限制设置为1MB&#xff0c;最为合适&#xff0c;正常流和异常流的情况下不会有内存溢出的风险。

之前启动脚本给服务端分配的堆内存过小&#xff0c;考虑到是NIO&#xff0c;所以在启动服务端的时候&#xff0c;有必要单独分配一个直接内存供NIO使用.修改启动参数。

增加堆内存大小直接内存&#xff0c;防止因为服务端缓存太大&#xff0c;导致thrift服务没有内存可申请&#xff0c;无法处理请求。

总结

真的是一次非常酸爽的过程&#xff0c;特此发个博客记录一下&#xff0c;如果有说的不对的对方&#xff0c;欢迎批评斧正&#xff01;

长按订阅更多精彩▼

640?wx_fmt&#61;jpeg

如有收获&#xff0c;点个在看&#xff0c;诚挚感谢640?wx_fmt&#61;png




推荐阅读
  • 如何自行分析定位SAP BSP错误
    The“BSPtag”Imentionedintheblogtitlemeansforexamplethetagchtmlb:configCelleratorbelowwhichi ... [详细]
  • 本文介绍了使用PHP实现断点续传乱序合并文件的方法和源码。由于网络原因,文件需要分割成多个部分发送,因此无法按顺序接收。文章中提供了merge2.php的源码,通过使用shuffle函数打乱文件读取顺序,实现了乱序合并文件的功能。同时,还介绍了filesize、glob、unlink、fopen等相关函数的使用。阅读本文可以了解如何使用PHP实现断点续传乱序合并文件的具体步骤。 ... [详细]
  • 本文介绍了如何使用Express App提供静态文件,同时提到了一些不需要使用的文件,如package.json和/.ssh/known_hosts,并解释了为什么app.get('*')无法捕获所有请求以及为什么app.use(express.static(__dirname))可能会提供不需要的文件。 ... [详细]
  • Nginx使用(server参数配置)
    本文介绍了Nginx的使用,重点讲解了server参数配置,包括端口号、主机名、根目录等内容。同时,还介绍了Nginx的反向代理功能。 ... [详细]
  • 本文介绍了Oracle数据库中tnsnames.ora文件的作用和配置方法。tnsnames.ora文件在数据库启动过程中会被读取,用于解析LOCAL_LISTENER,并且与侦听无关。文章还提供了配置LOCAL_LISTENER和1522端口的示例,并展示了listener.ora文件的内容。 ... [详细]
  • 本文详细介绍了Linux中进程控制块PCBtask_struct结构体的结构和作用,包括进程状态、进程号、待处理信号、进程地址空间、调度标志、锁深度、基本时间片、调度策略以及内存管理信息等方面的内容。阅读本文可以更加深入地了解Linux进程管理的原理和机制。 ... [详细]
  • 本文介绍了计算机网络的定义和通信流程,包括客户端编译文件、二进制转换、三层路由设备等。同时,还介绍了计算机网络中常用的关键词,如MAC地址和IP地址。 ... [详细]
  • Linux如何安装Mongodb的详细步骤和注意事项
    本文介绍了Linux如何安装Mongodb的详细步骤和注意事项,同时介绍了Mongodb的特点和优势。Mongodb是一个开源的数据库,适用于各种规模的企业和各类应用程序。它具有灵活的数据模式和高性能的数据读写操作,能够提高企业的敏捷性和可扩展性。文章还提供了Mongodb的下载安装包地址。 ... [详细]
  • {moduleinfo:{card_count:[{count_phone:1,count:1}],search_count:[{count_phone:4 ... [详细]
  • 该楼层疑似违规已被系统折叠隐藏此楼查看此楼*madebyebhrz*#include#include#include#include#include#include#include ... [详细]
  • Nginx Buffer 机制引发的下载故障
    Nginx ... [详细]
  • {moduleinfo:{card_count:[{count_phone:1,count:1}],search_count:[{count_phone:4 ... [详细]
  • 本文介绍了为什么要使用多进程处理TCP服务端,多进程的好处包括可靠性高和处理大量数据时速度快。然而,多进程不能共享进程空间,因此有一些变量不能共享。文章还提供了使用多进程实现TCP服务端的代码,并对代码进行了详细注释。 ... [详细]
  • 解决nginx启动报错epoll_wait() reported that client prematurely closed connection的方法
    本文介绍了解决nginx启动报错epoll_wait() reported that client prematurely closed connection的方法,包括检查location配置是否正确、pass_proxy是否需要加“/”等。同时,还介绍了修改nginx的error.log日志级别为debug,以便查看详细日志信息。 ... [详细]
  • mac php错误日志配置方法及错误级别修改
    本文介绍了在mac环境下配置php错误日志的方法,包括修改php.ini文件和httpd.conf文件的操作步骤。同时还介绍了如何修改错误级别,以及相应的错误级别参考链接。 ... [详细]
author-avatar
balamark_466
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有