天天看點

rpc架構之 avro 學習 2 - 高效的序列化

rpc架構之 avro 學習 2 - 高效的序列化

同一類架構,後出現的總會吸收之前架構的優點,然後加以改進,avro在序列化方面相對thrift就是一個很好的例子。借用Apache Avro 與 Thrift 比較 一文中的幾張圖來說明一下,avro在序列化方面的改進:

1、無需強制生成目智語言代碼

rpc架構之 avro 學習 2 - 高效的序列化

avro提供了二種使用方式,一種稱之為Sepcific方式,這跟thrift基本一緻,都是寫定義IDL檔案,然後用編譯器(或插件)生成目标class,另一種方式是Generic,這種方式下,不用生成目标代碼,而是采用動态加載定義檔案的方式,将 FieldName - FieldValue,以Map<K,V>的方式存儲。

2、scheme/tag資訊不重複寫入二進制資料,存儲及傳輸更高效

rpc架構之 avro 學習 2 - 高效的序列化

上圖是thrift的存儲格式,每塊資料前都有一個tag用于辨別資料域的類型及編号(這部分tag資訊可以了解為資料域的meta資訊),如果傳輸一個List集合,集合中的每條記錄,這部分meta資訊實際是重複存儲的,多少有些浪費。

rpc架構之 avro 學習 2 - 高效的序列化

這是avro的改進,avro抛棄了對Filed編号的做法,而是直接在class的頭部,把所有schema中繼資料資訊包含在内(見下面的java代碼),這樣,client與server二端其實都已經知道資料的schema(架構模式)資訊,僅僅在client與server通訊初始化,首次傳輸即可,以後無需再傳遞這部分資訊,提升了網絡傳輸效率。類似剛才的List集合這種情況,這部分資訊也需要重複存儲到2進制資料中,反序列化時,也不需再關注schema的資訊,存儲空間更小。

package yjmyzz.avro.study.dto;

@SuppressWarnings("all")
@org.apache.avro.specific.AvroGenerated
public class QueryParameter extends org.apache.avro.specific.SpecificRecordBase implements org.apache.avro.specific.SpecificRecord {
    public static final org.apache.avro.Schema SCHEMA$ = new org.apache.avro.Schema.Parser().parse("{\"type\":\"record\",\"name\":\"QueryParameter\",\"namespace\":\"yjmyzz.avro.study.dto\",\"fields\":[{\"name\":\"ageStart\",\"type\":\"int\"},{\"name\":\"ageEnd\",\"type\":\"int\"}]}");

    public static org.apache.avro.Schema getClassSchema() {
        return SCHEMA$;
    }
  
    //...
}
      

這是avro生成的java代碼,從源代碼可以印證Schema确實已經包含在java代碼内。

關于avro的序列化,可以用下面的代碼測試一下:

package yjmyzz.avro.test;

import org.apache.avro.Schema;
import org.apache.avro.generic.GenericData;
import org.apache.avro.generic.GenericDatumReader;
import org.apache.avro.generic.GenericDatumWriter;
import org.apache.avro.generic.GenericRecord;
import org.apache.avro.io.*;
import org.apache.avro.specific.SpecificDatumReader;
import org.apache.avro.specific.SpecificDatumWriter;
import org.junit.Assert;
import org.junit.Test;
import yjmyzz.avro.study.dto.QueryParameter;
import java.io.ByteArrayOutputStream;
import java.io.IOException;

public class SerializeTest {

    @Test
    public void test() throws IOException {

        QueryParameter queryParameter = getQueryParameter();

        //****** 1 Specific 方式-序列化*******//

        ByteArrayOutputStream out1 = new ByteArrayOutputStream();
        DatumWriter<QueryParameter> writer1 = new SpecificDatumWriter<QueryParameter>(QueryParameter.class);
        BinaryEncoder encoder1 = EncoderFactory.get().binaryEncoder(out1, null);
        writer1.write(queryParameter, encoder1);
        encoder1.flush();
        out1.close();
        byte[] byte1 = out1.toByteArray();
        System.out.println("Avro Specific二進制序列後的byte數組長度:" + byte1.length);

        //反序列化
        DatumReader<QueryParameter> reader1 = new SpecificDatumReader<QueryParameter>(QueryParameter.class);
        Decoder decoder1 = DecoderFactory.get().binaryDecoder(out1.toByteArray(), null);
        QueryParameter result1 = reader1.read(null, decoder1);
        Assert.assertEquals(queryParameter.getAgeStart(), result1.getAgeStart());
        Assert.assertEquals(queryParameter.getAgeEnd(), result1.getAgeEnd());

        //**我是萬惡的分割線***//

        //****** 2 Genericy 方式-序列化*******//
        Schema.Parser parser = new Schema.Parser();
        //Schema schema = parser.parse(new File("/Users/jimmy/Work/Code/avro/avro-contract/src/main/avro/QueryParameter.avsc"));
        Schema schema = parser.parse(getClass().getResourceAsStream("/QueryParameter.avsc"));

        //根據schema建立一個record示例(跟反射的思想有點類似)
        GenericRecord datum = new GenericData.Record(schema);
        datum.put("ageStart", 1);
        datum.put("ageEnd", 5);

        //序列化
        ByteArrayOutputStream out2 = new ByteArrayOutputStream();
        DatumWriter<GenericRecord> writer2 = new GenericDatumWriter<GenericRecord>(schema);
        Encoder encoder2 = EncoderFactory.get().binaryEncoder(out2, null);
        writer2.write(datum, encoder2);
        encoder2.flush();
        out2.close();
        byte[] byte2 = out2.toByteArray();
        System.out.println("Avro Generic二進制序列後的byte數組長度:" + byte2.length);

        //反序列化
        DatumReader<GenericRecord> reader2 = new GenericDatumReader<GenericRecord>(schema);
        Decoder decoder2 = DecoderFactory.get().binaryDecoder(out2.toByteArray(), null);
        GenericRecord result2 = reader2.read(null, decoder2);
        Assert.assertEquals(datum.get("ageStart"), result2.get("ageStart"));
        Assert.assertEquals(datum.get("ageEnd"), result2.get("ageEnd"));
    }

    private QueryParameter getQueryParameter() {
        QueryParameter query = new QueryParameter();
        query.setAgeStart(1);
        query.setAgeEnd(5);
        return query;
    }
}
      

Avro Specific二進制序列後的byte數組長度:2

Avro Generic二進制序列後的byte數組長度:2

與前一篇thrift中的序列化結果相比,存儲占用的空間比thrift的TCompactProtocol還要小,确實在序列化方面avro做得更好。

但是,凡事總有二面性,雖然avro在序列化方面做了不少改進,但是其RPC的實作并沒有做出太多的創新,預設提供的HttpServer、NettyServer都是直接用的其它開源産品實作,不象Thrift自己提供了全新的實作,是以在RPC的性能方面,avro仍有很多可以優化的空間,預設情況下,從我自己測試的情況下,avro是不敵thrift的。但具體能優化到什麼程度,就看使用的人在網絡通訊、網絡協定方面的功底了,有朋友說avro使用c#語言開發Server與Client端,對源代碼優化後,可達到每秒20~30萬的處理數。

作者:菩提樹下的楊過

出處:http://yjmyzz.cnblogs.com

本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接配接,否則保留追究法律責任的權利。