概述

最近线上的日志处理服务偶尔会出现Out Of Memory的问题,从Exception的call stack中顺藤摸瓜,最终定位到是thrift反序列化的问题。

发现问题

先交代一下问题现场:

  • thirft版本: 0.5.0,很久远的版本,但是公司统一使用的版本;
  • 反序列化使用的协议:TCompactProtocol协议;
  • 出错的call stack:

    Exception in thread "pool-10-thread-1" java.lang.RuntimeException: java.lang.OutOfMemoryError: Requested array size exceeds VM limit
    at com.lmax.disruptor.FatalExceptionHandler.handleEventException(FatalExceptionHandler.java:45)
    at com.lmax.disruptor.BatchEventProcessor.run(BatchEventProcessor.java:152)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
    at java.lang.Thread.run(Thread.java:745)
    Caused by: java.lang.OutOfMemoryError: Requested array size exceeds VM limit
    at org.apache.thrift.protocol.TCompactProtocol.readBinary(TCompactProtocol.java:651)
    at org.apache.thrift.protocol.TCompactProtocol.readString(TCompactProtocol.java:626)
    at com.xiaomi.data.spec.log.push.XmPushMessageInfo.read(XmPushMessageInfo.java:2779)

寻找原因

从上面的call stack可以看出,问题出在thrift TCompactProtocol类的651行,看看这一行干了些什么:

/**
* Read a byte[] of a known length from the wire.
*/
private byte[] readBinary(int length) throws TException {
if (length == 0) return new byte[0]; byte[] buf = new byte[length]; // TCompactProtocol第651行
trans_.readAll(buf, 0, length);
return buf;
}

从上面代码可以看到,TCompactProtocol类的651行申请了一个长度为length的byte数组,而此时可用内存已经不足以分配这么大的空间,所以报了java.lang.OutOfMemoryError错误,导致程序异常退出。

这个length是怎么来的呢?还是从call stack寻找答案,看看TCompactProtocol类的626行:


/**
* Reads a byte[] (via readBinary), and then UTF-8 decodes it.
*/
public String readString() throws TException {
int length = readVarint32(); if (length == 0) {
return "";
} try {
if (trans_.getBytesRemainingInBuffer() >= length) {
String str = new String(trans_.getBuffer(), trans_.getBufferPosition(), length, "UTF-8");
trans_.consumeBuffer(length);
return str;
} else {
return new String(readBinary(length), "UTF-8"); // 626行
}
} catch (UnsupportedEncodingException e) {
throw new TException("UTF-8 not supported!");
}
}

从上面的代码可以看到,length是通过 readVarint32 这个函数读到的一个int型数字,然后thrift使用这个数字来申请内存。

这种方式在正常情况下是没有问题的,但是如果源binary数据被写坏了,或者网络传输过程中出现了差错,就有可能导致 readVarint32 读到的是一个非常大的数字(可能达到10多亿),这种情况下申请内存必然会OOM。

解决问题

问题原因找到了,但是怎么解决呢? 
下面是一个比较简单的解决方案:

每次读取到length之后都做一下长度的check,如果这个长度超过一定的长度,则直接抛出异常,不要再申请内存。

thrift中需要check读取到的langth的地方有以下几个地方(如果使用其他Protocol也类似):

/** 
* Read a map header off the wire. If the size is zero, skip reading the key
* and value type. This means that 0-length maps will yield TMaps without the
* "correct" types.
*/
public TMap readMapBegin() throws TException {
int size = readVarint32(); //此处需要check size
byte keyAndValueType = size == 0 ? 0 : readByte();
return new TMap(getTType((byte)(keyAndValueType >> 4)), getTType((byte)(keyAndValueType & 0xf)), size);
}
/**
* Read a list header off the wire. If the list size is 0-14, the size will
* be packed into the element type header. If it's a longer list, the 4 MSB
* of the element type header will be 0xF, and a varint will follow with the
* true size.
*/
public TList readListBegin() throws TException {
byte size_and_type = readByte();
int size = (size_and_type >> 4) & 0x0f;
if (size == 15) {
size = readVarint32(); // 此处需要check size
}
byte type = getTType(size_and_type);
return new TList(type, size);
}
/**
* Reads a byte[] (via readBinary), and then UTF-8 decodes it.
*/
public String readString() throws TException {
int length = readVarint32(); // 此处需要check length if (length == 0) {
return "";
} try {
if (trans_.getBytesRemainingInBuffer() >= length) {
String str = new String(trans_.getBuffer(), trans_.getBufferPosition(), length, "UTF-8");
trans_.consumeBuffer(length);
return str;
} else {
return new String(readBinary(length), "UTF-8");
}
} catch (UnsupportedEncodingException e) {
throw new TException("UTF-8 not supported!");
}
}

以 readMapBegin 举例,可以这样修改:

/**
* Read a map header off the wire. If the size is zero, skip reading the key
* and value type. This means that 0-length maps will yield TMaps without the
* "correct" types.
*/
public TMap readMapBegin() throws TException {
int size = readVarint32();
if (size > trans_.getBytesRemainingInBuffer() || size > MAX_THRIFT_MAP_SIZE) {
throw new TPushProtocolException(TProtocolException.SIZE_LIMIT, "Thrift map size " + size + " out of range, remaining size = " + trans_.getBytesRemainingInBuffer());
}
byte keyAndValueType = size == 0 ? 0 : readByte();
return new TMap(getTType((byte)(keyAndValueType >> 4)), getTType((byte)(keyAndValueType & 0xf)), size);
}

其中的 MAX_THRIFT_MAP_SIZE 是一个常量,一个自定义的thrift map的最大size,此处是10000。

后记

看了一下thrift 0.9.3版本的源码,这个版本中已经加上了类似的check逻辑。

http://outofmemory.cn/java/thrift-desearialize-outOfMemory

Thrift反序列化导致OOM(转)的更多相关文章

  1. 转载--Typecho install.php 反序列化导致任意代码执行

    转载--Typecho install.php 反序列化导致任意代码执行 原文链接(http://p0sec.net/index.php/archives/114/) 0x00 前言 漏洞公布已经过去 ...

  2. Typecho反序列化导致前台 getshell 漏洞复现

    Typecho反序列化导致前台 getshell 漏洞复现 漏洞描述: Typecho是一款快速建博客的程序,外观简洁,应用广泛.这次的漏洞通过install.php安装程序页面的反序列化函数,造成了 ...

  3. Typecho V1.1反序列化导致代码执行分析

    0x00  前言     今天在Seebug的公众号看到了Typecho的一个前台getshell分析的文章,然后自己也想来学习一下.保持对行内的关注,了解最新的漏洞很重要. 0x01  什么是反序列 ...

  4. Android RecyclerView利用Glide加载大量图片into(Target)导致OOM异常

    学过android的人应该都知道Glide是一个无比强大的图片加载库,它内部已经提供了很好的缓存机制供我们选择,我们只需一个参数调用即可(DiskCacheStrategy()),而不必像Univer ...

  5. Mysql中使用JDBC流式查询避免数据量过大导致OOM

    一.前言 java 中MySQL JDBC 封装了流式查询操作,通过设置几个参数,就可以避免一次返回数据过大导致 OOM. 二.如何使用 2.1 之前查询 public void selectData ...

  6. Android 导致OOM的常见原因

    OOM主要有两种原因导致: 1. 加载大图片: 2. 内存泄漏: 一.加载大图片 在Android应用中加载Bitmap的操作是需要特别小心处理的,因为Bitmap会消耗很多内存.比如,Galaxy ...

  7. 不停的实例化对象导致OOM

    使用axis调用webService,系统运行一段时间后,出现了 OOM,还好日志中 记下了错误信息. Exception in thread "Thread-1301" java ...

  8. 线上故障排查——drools规则引擎使用不当导致oom

    事件回溯 1.7月26日上午11:34,告警邮件提示:tomcat内存使用率连续多次超过90%: 2.开发人员介入排查问题,11:40定位到存在oom问题,申请运维拉取线上tomcat 内存快照dum ...

  9. Android引导页过多导致OOM内存泄漏

    摘要:前几天推广我们APP的时候,有些手机加载引导页的时候会闪退或崩溃,在Bugly显示是OOM异常.    然后Bugly上面显示的解决方案是: 该异常表示未能成功分配字节内存,通常是因为内存不足导 ...

随机推荐

  1. perl学习(10) 字符串处理函数和排序

    1.1.index Perl 查找子串第一次在大字符串中出现的地方,返回第一个字符的位置. . . my $stuff = “Howordy world!”; my $where3 = index($ ...

  2. 获取DOM元素位置和尺寸大小

    JavaScript获取DOM元素位置和尺寸大小 在一些复杂的页面中经常会用JavaScript处理一些DOM元素的动态效果,这种时候我们经常会用到一些元素位置和尺寸的计算,浏览器兼容性问题也是不可忽 ...

  3. Bandwidth内存带宽測试工具

    本博文为原创,遵循CC3.0协议,转载请注明出处:http://blog.csdn.net/lux_veritas/article/details/24766015 ----------------- ...

  4. 真机測试时的错误:No matching provisioning profiles found

    1.出现错误的原因是这种---- 公司接收一个外包项目,原来做真机測试的时候,用的是公司申请的苹果开发人员账号.如今项目结束了,准备上线,但客户要求使用客户自己的苹果开发人员是账号上线,于是就用客户的 ...

  5. Single Image Haze Removal Using Dark Channel Prior翻译

    这段时间在回顾以前做的去雾,顺便就把这篇文章翻译了一下,看看有没有同行交流.由于是用LaTex排版的,所以只能转为图片传上来了.另外,英语水平有限,大部分都是靠词典,所以只能勉强着看了.

  6. MapReduce调度与执行原理之作业提交

    前言 :本文旨在理清在Hadoop中一个MapReduce作业(Job)在提交到框架后的整个生命周期过程,权作总结和日后参考,如有问题,请不吝赐教.本文不涉及Hadoop的架构设计,如有兴趣请参考相关 ...

  7. 基于visual Studio2013解决C语言竞赛题之1052求根

       题目 解决代码及点评 /* 功能:用简单迭代法解方程 e^x - x - 2 = 0 它有两个根(如图),其迭代公式为: 1) x[n+1]= e^x*n-2 (初值X<0时) ...

  8. [每日一题] OCP1z0-047 :2013-08-29 NULL............................................................168

    转载请注明出处:http://blog.csdn.net/guoyjoe/article/details/10558305 正确答案:B 用函数可以针对各种数据类型时行操作,包括NULL值在内.其中有 ...

  9. cocos2dx3.2 异步载入和动态载入

    半个月没有更新博客,从这个项目開始学习了非常多细节的东西,都不太成系统.可是却是开发上线中必须经历的东西.比方超级玛丽系列(一)中的正确的异步载入,正确的分层.正确的合成和载入plist.及时的移除未 ...

  10. 简化ui文件转换写法

    在命令行敲一串长的命令.枯燥麻烦. #coding:utf-8 import sys import os import subprocess if len(sys.argv) == 2: #节省输入, ...