作者: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个线程同时发送日志。

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

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

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

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

说明:

  • 一个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 <= 0) {
                    this.LOGGER.error("Read an invalid frame size of " + frameSize + ". 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 " + frameSize + ", which is bigger than the maximum allowable buffer size for ALL connections.");            return false;        }

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

        AbstractNonblockingServer.this.readBufferBytesAllocated.addAndGet((long)(frameSize + 4));        this.buffer_ = ByteBuffer.allocate(frameSize + 4);        this.buffer_.putInt(frameSize);        this.state_ = AbstractNonblockingServer.FrameBufferState.READING_FRAME;    }

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

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

说明:

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

  • 这个MAX_READ_BUFFER_BYTES是多少呢,thrift代码中给出了答案:

public abstract static class AbstractNonblockingServerArgs<T extends AbstractNonblockingServer.AbstractNonblockingServerArgs<T>> extends AbstractServerArgs<T> {<br>         public long maxReadBufferBytes = 9223372036854775807L;

    public AbstractNonblockingServerArgs(TNonblockingServerTransport transport) {        super(transport);        this.transportFactory(new Factory());    }}
  • 从上面源码可以看出,默认值居然给到了long的最大值9223372036854775807L。

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

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

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

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

步骤八:踏破铁鞋无觅处

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

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

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

可以做出假设是不是反序列化的问题,不是thrift IDL定义的不能正常的反序列化?

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

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

步骤九. 找到原因

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

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

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

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

问题解决方案

非常简单

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

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

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

2.服务端处理结果如下:

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

3.国外有人对thrift一些server做了压力测试,如下图所示:

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

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

所以是推荐使用TThreadedSelectorServer

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

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

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

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

  • 年轻代最大值 MaxNewSize:512M

  • 最大堆外内存(直接内存)MaxDirectMemorySize:128M

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

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

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

总结

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

长按订阅更多精彩▼

如有收获,点个在看,诚挚感谢

记一次内存溢出的分析经历相关推荐

  1. 记一次内存溢出的分析经历——使用thrift

    背景: 有一个项目做一个系统,分客户端和服务端,客户端用c++写的,用来收集信息然后传给服务端(客户端的数量还是比较多的,正常的有几千个), 服务端用Java写的(带管理页面),属于RPC模式,中间的 ...

  2. 生产内存溢出定位分析

    生产内存溢出定位分析 具体生产环境大多为linux系统,此处以linux系统为示例. 1.什么是oom? 英文全称"Out Of Memory",译为 "内存不足&quo ...

  3. JAVA内存分析:基于dump内存溢出快照分析

    JAVA内存分析 JAVA内存分析一:基于dump内存溢出快照分析 JAVA内存分析二:idea集成jprofiler查看JVM内存使用情况 JAVA内存分析三:使用JDK自带工具进行内存和CPU分析 ...

  4. 关于 android oom(内存溢出的分析)

    laozhu1124 android oom 全解析 Android oom 有时出现很频繁,这一般不是Android设计的问题,一般是我们的问题. 就我的经验而言,出现oom,无非主要是以下几个方面 ...

  5. Tomcat内存溢出原因分析

    Tomcat内存溢出的原因  在生产环境中tomcat内存设置不好很容易出现内存溢出.造成内存溢出是不一样的,当然处理方式也不一样. 这里根据平时遇到的情况和相关资料进行一个总结.常见的一般会有下面三 ...

  6. 生产环境 JVM 内存溢出案例分析

    点击上方蓝色"方志朋",选择"设为星标" 回复"666"获取独家整理的学习资料! 来源:blog.csdn.net/prestigeding ...

  7. java内存溢出怎么排查_【转】Java学习---内存溢出的排查经历

    前言 OutOfMemoryError 问题相信很多朋友都遇到过,相对于常见的业务异常(数组越界.空指针等)来说这类问题是很难定位和解决的. 本文以最近碰到的一次线上内存溢出的定位.解决问题的方式展开 ...

  8. Java常见内存溢出异常分析

    Java虚拟机规范规定JVM的内存分为了好几块,比如堆,栈,程序计数器,方法区等,而Hotspot jvm的实现中,将堆内存分为了三部分,新生代,老年代,持久带,其中持久带实现了规范中规定的方法区,而 ...

  9. 生产环境JVM内存溢出案例分析

    如果我们所在公司的业务量比较大,在生产环境经常会出现JVM内存溢出的现象,那我们该如何快速响应,快速定位,快速恢复问题呢? 本文将通过一个线上环境JVM内存溢出的案例向大家介绍一下处理思路与分析方法. ...

最新文章

  1. 从四点来探索监控设备系统的组成
  2. MySQL第3天:MySQL的架构介绍之linux版安装
  3. Eclipse设置项目默认编码和换行符类型
  4. Selenium自动化测试-6.鼠标键盘操作
  5. Ueditor使用以及遇到的问题
  6. SPHINX安装步骤
  7. java打印插件_java c/s项目中有没有好用的打印插件?
  8. php多线程扩展pthreads安装使用,swoole多进程,内置多进程
  9. (转)今天的人工智能比肩工业革命毫不夸张
  10. 15个磁性材料相关概念解释(基础版)
  11. Android:LayoutInflater(布局服务)的 简单介绍 使用方法解析
  12. 做的好不如说得好,老板为什么喜欢听过程汇报?
  13. Work Stealing
  14. php拼手速 抢红包,拼手速必备 | 这样设置,微信抢红包速度更快!
  15. 保护Android网络数据教程
  16. 电脑中文件名称字体变颜色怎么解决,由黑色变成蓝色了。。。。。。
  17. win10升级后删除自带的微软输入法
  18. UE 某图局部 展UV 图标 / CSS 精灵图(sprite) 好像
  19. gnuplot:散点图
  20. 5分钟教会学妹使用JavaScript栈解决问题(三)【JavaScript数据结构与算法系列】

热门文章

  1. 欧拉降幂 ---- 2020 杭电多校[E - Fibonacci Sum]+欧拉降幂+和式的调整+二次剩余+毒瘤卡常
  2. Codeforces Global Round 8 C. Even Picture 题解[巧妙法构造题]
  3. 附加 集合数据_浩辰3D软件新手教程:三维建模设计中如何重用CAD模型数据?
  4. 华为存储iscsi配置_网络+存储+虚拟化:三大要素构建新网络
  5. raid模式_基于网络的磁盘热备技术|网络RAID-1
  6. C++官方自带可持久化平衡树rope的3000行源码
  7. P2949 [USACO09OPEN]Work Scheduling (后悔法,贪心)难度⭐⭐⭐
  8. 我在兰亭这三年之AutoDiff自动化测试框架
  9. 怎样熟练使用一项技术
  10. python正则表达式(三)