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源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索