概述

最近线上的日志处理服务偶尔会出现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. JSP页面小脚本实现日期比較,Java同理,精简过后的,可能在效率上不太好,有大大能够给优化下就更好了

    <%         java.text.SimpleDateFormat formatter = new java.text.SimpleDateFormat("yyyy-MM-dd ...

  2. Apache服务器学习笔记

    Apache服务器知识 首先我们要知道一共有那几个程序在监听网络端口,即与网络保持活跃连接,打开CMD命令窗口 输入: netstat  –an 指令就能显示出所有与网络保持连接的程序,输入net s ...

  3. 代码格式化工具Astyle配置

    Astyle是一个很好的代码格式化工具,其他不多说,下面介绍一下我在VS 2010的配置 1. http://sourceforge.net/projects/astyle,这是该插件的网站,下载后把 ...

  4. Windows Azure入门教学系列 (二):部署第一个Web Role程序

    本文是Windows Azure入门教学的第二篇文章. 在第一篇教学中,我们已经创建了第一个Web Role程序.在这篇教学中,我们将学习如何把该Web Role程序部署到云端. 注意:您需要购买Wi ...

  5. 九度OnlineJudge之1020:最小长方形

    题目描述:     给定一系列2维平面点的坐标(x, y),其中x和y均为整数,要求用一个最小的长方形框将所有点框在内.长方形框的边分别平行于x和y坐标轴,点落在边上也算是被框在内. 输入:      ...

  6. python3语法小记(二)列表 和 元组

    列表(list): 列表就像一个线性容器,但是比C++的 lis t扩展多得多 列表里的元素可以是相同类型,也可以包含各种类型,比如列表里嵌套另一个列表 >>> L1 = [1,2, ...

  7. Entity - 使用EF框架进行增删改查 - 模型先行

    模型先行:先创建数据库实体模型,然后再进行数据库的增删改查. 基本步骤是不变的,可参照 <Entity - 使用EF框架进行增删改查 - 数据库先行> 其中的不同是,在创建数据库实体模型的 ...

  8. 使用JDBC处理数据库大容量数据类型

    在本文将介绍如何使用JDBC操作MySQL数据库对于大容量数据类型的读取.在之前的博客中已经介绍了如何使用JDBC来操作数据库对各种数据的增删改查,那么大容量数据类型的数据操作又为何不同呢. 原因在于 ...

  9. UVA - 12119 The Bells are Ringing (枚举)

    Perhaps you all have heard the mythical story about Tower of Hanoi (The details of this story is not ...

  10. java大牛list

    1 Java的未来 Java能干什么.不能干什么,一开始就要搞清楚.这对于成为一个纯种的Java程序猿至关重要. 2 构建Java运行环境 Java运行在服务器,服务器都是Linux系统,对于真正程序 ...