概述

最近线上的日志处理服务偶尔会出现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. iOS开发--in house发布和安装(ipa重新签名)

    in house从字面意思理解就是‘内部的’,in house版本的ipa就是一个用于公司内部使用或测试的一个苹果应用程序安装包. 作为一个app应用程序开发者,在app应用程序在苹果商店上架前总需要 ...

  2. perl 类里的函数调用其他类的函数

    perl 类里的函数调用其他类的函数: package Horse; use base qw(Critter); sub new { my $invocant = shift; my $class = ...

  3. 基于visual Studio2013解决面试题之1001去除数字

     题目

  4. EasyUI - 一般处理程序 返回 Json值

    使用 一般处理程序(ashx)返回Json值. 原始Json数组的格式: [ { ", "name":"张三", "sex":&q ...

  5. 【linux】linux启动流程

    欢迎转载,转载时请保留作者信息,谢谢. 邮箱:tangzhongp@163.com 博客园地址:http://www.cnblogs.com/embedded-tzp Csdn博客地址:http:// ...

  6. 动手学Javascript(1)——PopStar

    PopStar是一款很流行的手机游戏.它的基本规则是在某个方块上单击,如果该方块周围有和它颜色一样的方块,那么这些方块都被选中.之后在选中方块的某一个上再次单击,所有选中的方块就会消失. 如下图所示, ...

  7. span设置固定宽度

    <span> 标签是被用来组合文档中的行内元素.相信对一般的网页设计师来讲是非常熟悉的朋友了,使用相当频繁,但我们往往很少对SPAN设定样式,一般也没什么必要,大多数都留给DIV老朋友了. ...

  8. Android SDK 环境变量配置(Windows)

    Android 开发需要进行adb的配置, 这里使用的是 adt-bundle-windows-x86_64-20140321, 里面捆绑的有 eclipse ,不需要再进行 其他的配置,直接下载解压 ...

  9. okHttp封装使用

    package com.zhy.utils.http.okhttp; import android.graphics.Bitmap; import android.graphics.BitmapFac ...

  10. uva 12096

    优先队列,主要是STL应用所以复制一下 #include <iostream> #include <cstdio> #include <cstdlib> #incl ...