gson 替换 fastjson 引起的线上问题分析

前言

Json 序列化框架存在的安全漏洞一直以来都是程序员们挂在嘴边调侃的一个话题,尤为是这两年 fastjson 因为被针对性研究,更是频频地的报出漏洞,出个漏洞没关系,可安全团队老是用邮件催着线上应用要进行依赖升级,这可就要命了,我相信不少小伙伴也是不胜其苦,考虑了使用其余序列化框架替换 fastjson。这不,最近咱们就有一个项目将 fastjson 替换为了 gson,引起了一个线上的问题。分享下此次的经历,以避免你们踩到一样的坑,在此警示你们,规范千万条,安全第一条,升级不规范,线上两行泪。java

问题描述

线上一个很是简单的逻辑,将对象序列化成 fastjson,再使用 HTTP 请求将字符串发送出去。本来工做的好好的,在将 fastjson 替换为 gson 以后,居然引起了线上的 OOM。通过内存 dump 分析,发现居然发送了一个 400 M+ 的报文,因为 HTTP 工具没有作发送大小的校验,强行进行了传输,直接致使了线上服务总体不可用。程序员

问题分析

为何一样是 JSON 序列化,fastjson 没出过问题,而换成 gson 以后立马就暴露了呢?经过分析内存 dump 的数据,发现不少字段的值都是重复的,再结合咱们业务数据的特色,一会儿定位到了问题 -- gson 序列化重复对象存在严重的缺陷。web

直接用一个简单的例子,来讲明当时的问题。模拟线上的数据特性,使用 List<Foo> 添加进同一个引用对象json

Foo foo = new Foo();
Bar bar = new Bar();
List<Foo> foos = new ArrayList<>();
for(int i=0;i<3;i++){
    foos.add(foo);
}
bar.setFoos(foos);

Gson gson = new Gson();
String gsonStr = gson.toJson(bar);
System.out.println(gsonStr);

String fastjsonStr = JSON.toJSONString(bar);
System.out.println(fastjsonStr);

观察打印结果:数组

gson:安全

{"foos":[{"a":"aaaaa"},{"a":"aaaaa"},{"a":"aaaaa"}]}

fastjson:微信

{"foos":[{"a":"aaaaa"},{"$ref":"$.foos[0]"},{"$ref":"$.foos[0]"}]}

能够发现 gson 处理重复对象,是对每一个对象都进行了序列化,而 fastjson 处理重复对象,是将除第一个对象外的其余对象使用引用符号 $ref 进行了标记。网络

当单个重复对象的数量很是多,以及单个对象的提交较大时,两种不一样的序列化策略会致使一个质变,咱们不妨来针对特殊的场景进行下对比。app

压缩比测试

  • 序列化对象:包含大量的属性。以模拟线上的业务数据。框架

  • 重复次数:200。即 List 中包含 200 个同一引用的对象,以模拟线上复杂的对象结构,扩大差别性。

  • 序列化方式:gson、fastjson、Java、Hessian2。额外引入了 Java 和 Hessian2 的对照组,方便咱们了解各个序列化框架在这个特殊场景下的表现。

  • 主要观察各个序列化方式压缩后的字节大小,由于这关系到网络传输时的大小;次要观察反序列后 List 中仍是不是同一个对象

public class Main {

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Foo foo = new Foo();
        Bar bar = new Bar();
        List<Foo> foos = new ArrayList<>();
        for(int i=0;i<200;i++){
            foos.add(foo);
        }
        bar.setFoos(foos);
        // gson
        Gson gson = new Gson();
        String gsonStr = gson.toJson(bar);
        System.out.println(gsonStr.length());
        Bar gsonBar = gson.fromJson(fastjsonStr, Bar.class);
        System.out.println(gsonBar.getFoos().get(0) == gsonBar.getFoos().get(1));  
        // fastjson
        String fastjsonStr = JSON.toJSONString(bar);
        System.out.println(fastjsonStr.length());
        Bar fastjsonBar = JSON.parseObject(fastjsonStr, Bar.class);
        System.out.println(fastjsonBar.getFoos().get(0) == fastjsonBar.getFoos().get(1));
        // java
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(byteArrayOutputStream);
        oos.writeObject(bar);
        oos.close();
        System.out.println(byteArrayOutputStream.toByteArray().length);
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(byteArrayOutputStream.toByteArray()));
        Bar javaBar = (Bar) ois.readObject();
        ois.close();
        System.out.println(javaBar.getFoos().get(0) == javaBar.getFoos().get(1));
        // hessian2
        ByteArrayOutputStream hessian2Baos = new ByteArrayOutputStream();
        Hessian2Output hessian2Output = new Hessian2Output(hessian2Baos);
        hessian2Output.writeObject(bar);
        hessian2Output.close();
        System.out.println(hessian2Baos.toByteArray().length);
        ByteArrayInputStream hessian2Bais = new ByteArrayInputStream(hessian2Baos.toByteArray());
        Hessian2Input hessian2Input = new Hessian2Input(hessian2Bais);
        Bar hessian2Bar = (Bar) hessian2Input.readObject();
        hessian2Input.close();
        System.out.println(hessian2Bar.getFoos().get(0) == hessian2Bar.getFoos().get(1));
    }

}

输出结果:

gson:
62810
false

fastjson:
4503
true

Java:
1540
true

Hessian2:
686
true

结论分析:因为单个对象序列化后的体积较大,采用引用表示的方式能够很好的缩小体积,能够发现 gson 并无采起这种序列化优化策略,致使体积膨胀。甚至一向不被看好的 Java 序列化都比其优秀的多,而 Hessian2 更是夸张,直接比 gson 优化了 2个数量级。而且反序列化后,gson 并不能将本来是同一引用的对象还原回去,而其余的序列化框架都可以实现这一点。

吞吐量测试

除了关注序列化以后数据量的大小,各个序列化的吞吐量也是咱们关心的一个点。使用基准测试能够精准地测试出各个序列化方式的吞吐量。

@BenchmarkMode({Mode.Throughput})
@State(Scope.Benchmark)
public class MicroBenchmark {

    private Bar bar;

    @Setup
    public void prepare() {
        Foo foo = new Foo();
        Bar bar = new Bar();
        List<Foo> foos = new ArrayList<>();
        for(int i=0;i<200;i++){
            foos.add(foo);
        }
        bar.setFoos(foos);
    }

    Gson gson = new Gson();

    @Benchmark
    public void gson(){
        String gsonStr = gson.toJson(bar);
        gson.fromJson(gsonStr, Bar.class);
    }

    @Benchmark
    public void fastjson(){
        String fastjsonStr = JSON.toJSONString(bar);
        JSON.parseObject(fastjsonStr, Bar.class);
    }

    @Benchmark
    public void java() throws Exception {
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(byteArrayOutputStream);
        oos.writeObject(bar);
        oos.close();

        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(byteArrayOutputStream.toByteArray()));
        Bar javaBar = (Bar) ois.readObject();
        ois.close();
    }

    @Benchmark
    public void hessian2() throws Exception {
        ByteArrayOutputStream hessian2Baos = new ByteArrayOutputStream();
        Hessian2Output hessian2Output = new Hessian2Output(hessian2Baos);
        hessian2Output.writeObject(bar);
        hessian2Output.close();


        ByteArrayInputStream hessian2Bais = new ByteArrayInputStream(hessian2Baos.toByteArray());
        Hessian2Input hessian2Input = new Hessian2Input(hessian2Bais);
        Bar hessian2Bar = (Bar) hessian2Input.readObject();
        hessian2Input.close();
    }

    public static void main(String[] args) throws RunnerException {
        Options opt = new OptionsBuilder()
            .include(MicroBenchmark.class.getSimpleName())
            .build();

        new Runner(opt).run();
    }

}

吞吐量报告:

Benchmark                 Mode  Cnt        Score         Error  Units
MicroBenchmark.fastjson  thrpt   25  6724809.416 ± 1542197.448  ops/s
MicroBenchmark.gson      thrpt   25  1508825.440 ±  194148.657  ops/s
MicroBenchmark.hessian2  thrpt   25   758643.567 ±  239754.709  ops/s
MicroBenchmark.java      thrpt   25   734624.615 ±   66892.728  ops/s

是否是有点出乎意料,fastjson 居然独领风骚,文本类序列化的吞吐量相比二进制序列化的吞吐量要高出一个数量级,分别是每秒百万级和每秒十万级的吞吐量。

总体测试结论

  • fastjson 序列化事后带有 $ 的引用标记也可以被 gson 正确的反序列化,但笔者并无找到让 gson 序列化时转换成引用的配置
  • fastjson、hessian、java 均支持循环引用的解析;gson 不支持
  • fastjson 能够设置 DisableCircularReferenceDetect,关闭循环引用和重复引用的检测
  • gson 反序列化以前的同一个引用的对象,在经历了序列化再反序列化回来以后,不会被认为是同一个对象,可能会致使内存对象数量的膨胀;而 fastjson、java、hessian2 等序列化方式因为记录的是引用标记,不存在该问题
  • 以笔者的测试 case 为例,hessian2 具备很是强大的序列化压缩比,适合大报文序列化后供网络传输的场景使用
  • 以笔者的测试 case 为例,fastjson 具备很是高的吞吐量,对得起它的 fast,适合须要高吞吐的场景使用
  • 序列化还须要考虑到是否支持循环引用,是否支持循环对象优化,是否支持枚举类型、集合、数组、子类、多态、内部类、泛型等综合场景,以及是否支持可视化等比较的场景,增删字段后的兼容性等等特性。综合来看,笔者比较推荐 hessian2 和 fastjson 两种序列化方式

总结

你们都知道 fastjson 为了快,作了相对一些较为 hack 的逻辑,这也致使其漏洞较多,但我认为编码都是在 trade off 之中进行的,若是有一个完美的框架,那其余竞品框架早就不会存在了。笔者对各个序列化框架的研究也不深,可能你会说 jackson 更加优秀,我只能说能解决你的场景遇到的问题,那就是合适的框架。

最后,想要替换序列化框架时必定要慎重,了解清楚替代框架的特性,可能原先框架解决的问题,新的框架不必定能很好的 cover。

END -

「技术分享」某种程度上,是让做者和读者,不那么孤独的东西。欢迎关注个人微信公众号:Kirito的技术分享」


本文分享自微信公众号 - Kirito的技术分享(cnkirito)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。

相关文章
相关标签/搜索