分散型アーキテクチャにおける一般的なシリアル化技術

シリアル化の進化

分散型アーキテクチャやマイクロサービスアーキテクチャが普及するにつれて、サービス同士の通信が基本的なニーズとなりました。このとき、通信のパフォーマンスだけでなく、言語の多様性問題にも注意を払う必要があります。したがって、シリアル化においては、シリアル化のパフォーマンスを向上させ、クロスランゲージ対応を解決することが重要な課題となります。

Javaのシリアル化と逆シリアル化:

@Data
public class User implements Serializable {

    private String name;

    private int age;
}


public class JavaSerializer {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        User user = new User();
        user.setName("snail");
        user.setAge(18);
        byte[] bytes = serialToFile(user);
        System.out.println(bytes);
        System.out.println(bytes.length);
        User desUser = deserializeToFromFile(bytes);
        System.out.println(desUser);
    }

    private static byte[] serialToFile(User user) throws IOException {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bos);
        oos.writeObject(user);
        return bos.toByteArray();
    }

    private static <T> T deserializeToFromFile(byte[] data) throws IOException, ClassNotFoundException {
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(data));
        return (T)ois.readObject();
    }
}

Javaが提供するシリアル化には2つの問題があります:

  1. シリアル化されたデータが大きく、転送効率が低い

  2. 他の言語では認識および接続できない

これにより、長い間XML形式のオブジェクトシリアル化メカニズムが主流となりました。これは、マルチランゲージ互換性を解決し、バイナリよりも理解しやすいためです。XMLベースのSOAPプロトコルおよび関連するWebServiceフレームワークが長期間にわたり主要な開発言語の必須技術として使用されました。

その後、JSONベースの単純なテキスト形式でHTTP RESTインターフェースが、複雑なWebServiceインターフェースをほぼ置き換えることになりました。これは、分散型アーキテクチャにおけるリモート通信の主要選択肢となりました。しかし、JSONシリアル化は空間コストが大きく、パフォーマンスが低いなどの問題があり、モバイルクライアントアプリケーションではより効率的なデータ転送が必要です。

このような状況で、言語に依存しない高効率なバイナリエンコードプロトコルが注目されるようになりました。最初に登場したオープンソースのバイナリシリアル化フレームワークはMessagePackであり、Protocol Buffersより前に登場しました。

さまざまなシリアル化技術

XMLシリアル化フレームワークについて

XMLシリアル化の利点は読みやすさであり、デバッグが容易です。ただし、シリアル化後のバイトコードファイルが大きく、効率が低いです。これは、パフォーマンスが高くなく、QPSが低い企業内の内部システム間のデータ交換に適しています。また、XMLは言語に依存しないため、異種システム間のデータ交換やプロトコルに適しています。たとえば、WebServiceはXML形式でデータをシリアル化します。XMLシリアル化/逆シリアル化の実装方法はいくつかあり、XStreamとJavaの標準的なシリアル化/逆シリアル化が知られています。

XMLシリアル化と逆シリアル化:

public class XmlSerializer {

    public static void main(String[] args) {
        User user = new User();
        user.setName("snail");
        user.setAge(18);
        String xml = serialToFile(user);
        System.out.println(xml);
        System.out.println(xml.length());
        User desUser = deserializeToFromFile(xml);
        System.out.println(desUser);
    }

    private static String serialToFile(User user) {
        return new XStream(new DomDriver()).toXML(user);
    }

    private static User deserializeToFromFile(String xml) {

        XStream xStream = new XStream(new DomDriver());
        xStream.addPermission(AnyTypePermission.ANY);
        return (User) xStream.fromXML(xml);
    }
}

JSONシリアル化フレームワーク

JSONは軽量なデータ交換形式であり、XMLと比較してバイトストリームが小さく、読みやすさも良いです。現在、JSONデータ形式は企業内で最も一般的です。

JSONシリアル化でよく使われるオープンソースツール:

これらのJSONシリアル化ツールの中で、JacksonとfastjsonはGSONよりもパフォーマンスが良いですが、JacksonとGSONの安定性はFastjsonよりも優れています。一方で、Fastjsonの利点は提供するAPIが簡単で使いやすいことです。

JSONシリアル化と逆シリアル化:

public class JsonSerializer {

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        User user = new User();
        user.setName("snail");
        user.setAge(18);
        String json = serialToFile(user);
        System.out.println(json);
        System.out.println(json.length());
        User desUser = deserializeToFromFile(json);
        System.out.println(desUser);
    }

    private static String serialToFile(User user){
        return JSON.toJSONString(user);
    }

    private static User deserializeToFromFile(String json){
        return JSON.parseObject(json,User.class);
    }
}

Hessianシリアル化フレームワーク

Hessianは複数の言語間での転送をサポートするバイナリシリアル化プロトコルであり、Javaのデフォルトのシリアル化メカニズムに比べてパフォーマンスと使い勝手が良く、複数の言語をサポートしています。実際、DubboはHessianのシリアル化を使用していますが、DubboはHessianを再構築しており、パフォーマンスがさらに向上しています。

Hessianシリアル化と逆シリアル化:

public class HessianSerializer {

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        User user = new User();
        user.setName("snail");
        user.setAge(18);
        byte[] bytes = serialToFile(user);
        System.out.println(bytes);
        System.out.println(bytes.length);
        User desUser = deserializeToFromFile(bytes);
        System.out.println(desUser);
    }

    private static byte[] serialToFile(User user) throws IOException {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        HessianOutput ho = new HessianOutput(bos);
        ho.writeObject(user);
        return bos.toByteArray();
    }

    private static User deserializeToFromFile(byte[] bytes) throws IOException {
        ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
        HessianInput hi = new HessianInput(bis);
        return (User)hi.readObject();
    }
}

Avroシリアル化

リンク:https://avro.apache.org/

Avroは大規模なデータ交換アプリケーションに設計されたデータシリアル化システムです。Hadoopの創設者であるDoug Cutting(Lucene、Nutchなどプロジェクトの創設者)によって開発されました。特徴:バイナリシリアル化方式で、大量データの処理が簡単で高速です。動的言語に優しく、Avroは動的言語がAvroデータを処理しやすくする仕組みを提供します。

  • Person.avscファイルの定義
{
  "namespace": "com.example.serialexample",
  "type":"record",
  "name":"Person",
  "fields": [
    {"name":"name","type":"string"},
    {"name":"age","type":"int"}
  ]
}

  • maven
<dependency>
    <groupId>org.apache.avro</groupId>
    <artifactId>avro</artifactId>
    <version>1.8.2</version>
</dependency>
<dependency>
    <groupId>org.apache.avro</groupId>
    <artifactId>avro-ipc</artifactId>
    <version>1.8.2</version>
</dependency>

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>

        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
        <plugin>
            <groupId>org.apache.avro</groupId>
            <artifactId>avro-maven-plugin</artifactId>
            <version>1.8.2</version>
            <executions>
                <execution>
                    <id>schemas</id>
                    <phase>generate-sources</phase>
                    <goals>
                        <goal>schema</goal>
                    </goals>
                    <configuration>
                        <sourceDirectory>${project.basedir}/src/main/avro</sourceDirectory>
                        <outputDirectory>${project.basedir}/src/main/java</outputDirectory>
                    </configuration>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

maven installを実行するとPerson.javaが生成されます。

Kryoシリアル化フレームワーク

リンク:https://github.com/EsotericSoftware/kryo/blob/master/README.md

Kryoは非常に成熟したシリアル化実装であり、Hive、Stormなどで広く使用されていますが、クロスランゲージをサポートしていません。現在、dubboは2.6バージョンでKryoのシリアル化メカニズムをサポートしています。そのパフォーマンスは以前のHessian2より優れています。

Protocol Buffersシリアル化フレームワーク

Protocol BuffersはGoogleのデータ交換形式であり、言語やプラットフォームに依存しません。GoogleはJava、C、Go、Pythonなど、さまざまな言語で実装しています。各実装には、該当する言語用のコンパイラとライブラリが含まれています。

Protocol Buffersは純粋なプレゼンテーション層プロトコルであり、さまざまなトランスポート層プロトコルと併用できます。

Protocol Buffersは広く使用されており、スペースのコストが少なく、パフォーマンスが良いため、会社内での高性能RPC呼び出しに適しています。

また、解析性能が高いので、シリアル化後のデータ量が相対的に少ないため、個別永続化のシナリオにも適用できます。

しかし、Protocol Buffersを使用するには多少面倒です。それは独自の文法とコンパイラがあるため、必要な場合はこの技術の学習にコストをかける必要があります。

Protocol Buffersには欠点があり、送信する各クラスの構造を対応するprotoファイルに生成しなければならず、クラスが変更された場合、該当するprotoファイルを再生成しなければなりません。

Protocol Buffersシリアル化の原理

Protocol Buffersを使用して開発する一般的なステップ:

1.開発環境を設定し、protocol compilerコードコンパイラをインストールします

2.protoファイルを記述し、シリアル化オブジェクトのデータ構造を定義します

3.記述した.protoファイルに基づいて、protocol compilerコンパイラを使用して対応するシリアル化/逆シリアル化のユーティリティクラスを生成します。

4.自動生成されたコードに基づいて、自分のシリアル化アプリケーションを記述します

Protocol Buffersのケーススタディ

Protocol Buffersツールのダウンロード: https://github.com/google/protobuf/releases

ここではMacのインストール方法を紹介します: https://www.jianshu.com/p/67f64307d268

protoファイルの作成:

syntax="proto2";
package com.demo.serial;
option java_package = "com.demo.serial";
option java_outer_classname = "UserProtos";

message User{
	required string name = 1;
	required int32 age = 2; //这里是正数
    //  required sint32 age = 2; //这里是负数
}


データタイプ:
string/bytes/bool/int32(4バイト)/int64/float/double/enum/messageカスタムクラス
修飾子
required 表示必須フィールド
optional 表示オプションフィールド
repeated 可反復、コレクションを表す
1、2、3、4は現在の範囲内で一意で、順序を表します

エンティティクラスの生成

【protoc --java_out=./ ./user.proto】

シリアル化の実装

<dependency>
    <groupId>com.google.protobuf</groupId>
    <artifactId>protobuf-java</artifactId>
    <version>3.13.0</version>
</dependency>

Protocol Buffersシリアル化の原理

シリアル化後のデータを出力できます

public class TestProto {
    public static void main(String[] args) {
        UserProtos.User user = UserProtos.User.newBuilder().setAge(300).setName("zhangsan").build();
        byte[] bytes = user.toByteArray();
        System.out.println(bytes.length);
        for (byte bt : bytes) {
            System.out.print(bt + " ");
        }
        System.out.println();
        UserProtos.User desUser = UserProtos.User.parseFrom(bytes);
        System.out.println(desUser);
    }
}
//length = 13
//10 8 122 104 97 110 103 115 97 110 16 -84 2

ここでは、シリアル化された数字は基本的に理解できませんが、シリアル化されたデータは確かに小さくなっています。

通常、最小限のシリアル化結果を得るには圧縮技術を使用する必要があります。Protocol Buffersではvarintとzigzagという2つの圧縮アルゴリズムが使用されています。

varint
最初に、age=300という数字がどのように圧縮されるかを見てみましょう

この2つのバイトはそれぞれ-84と2です。

-84はどのように計算されるのでしょうか?二進数で負数を表現する方法は、上位ビットを1に設定し、元の数の二進数を反転して補数表示(補数は反転+1)することです。

したがって、逆に計算するには:

  1. 【補数】10101100 -1 を取得して10101011を得ます

  2. 【反転】01010100は84になります。上位ビットが1なので、結果は-84です。

文字の符号化

"zhangsan"という文字はASCIIテーブルに基づいて数字に変換する必要があります。

z = 122、h = 104、a = 97、n = 110、g = 103、s = 115、a = 97、n = 110

したがって、結果は122 104 97 110 103 115 97 110です。

なぜこの結果がASCII値になるのでしょうか?なぜ圧縮されていないのでしょうか?

varintはバイトコードを圧縮しますが、この数字の2進数が1バイトで表現できる場合、最終的なエンコード結果は変化しません。

残りの2つの数字、8と16は何を意味しているのでしょうか?ここではProtocol Buffersの保存形式を理解する必要があります。

保存形式

データタイプ

##### tagの計算方法 ``` field_number(現在のフィールドの番号)<<3 |write_type

例えば、zhangsanのフィールド番号は1で、write_typeの値は2です。したがって、1<<3 |2 =10

zhangsanの長さは8です。

age=300のフィールド番号は2で、write_typeの値は0です。したがって、2 << 3 |0 = 16

int型の長さはここでは省略可能です。

最初の数字10はキーを表し、それ以降のすべては値です。


##### 負数の保存

コンピュータでは、負数は大きな整数として表現されます。コンピュータでは負数の符号ビットは数字の最高位であり、varintエンコードで負数を表現する場合、必ず5ビットが必要です。したがって、Protocol Buffersではsint32/sint64型を使用して負数を表現します。負数の処理はzigzagエンコード(符号付き数を符号なし数に変換)を使用し、varintエンコードを行います。

sint32: (n<<1)^(n>>31)

sint64: (n<<1)^(n>>63)

例えば、-300の値を保存する場合

-300

元のコード: 0001 0010 1100

反転: 1110 1101 0011

加算: 1110 1101 0100

n<<1: 左に1ビットシフト、右に0を追加 -> 1101 1010 1000

n>>31: 右に31ビットシフト、左に1を追加 -> 1111 1111 1111

n<<1 ^ n>>31

1101 1010 1000 ^ 1111 1111 1111 = 0010 0101 0111

十進数: 0010 0101 0111 = 599

varintアルゴリズム: 右から左に7ビットを選択し、最上位ビットに1/0を追加(バイト数に応じて)

2つのバイトを得ます

1101 01111 0000 0100

-41 4


その他のシリアル化フレームワーク: marshalling(Jbossで使用)、messagepack、Thrift

### 結論

Protocol bufferのパフォーマンスは、シリアル化後のデータサイズが小さく、シリアル化が速いため、最終的に転送効率が高くなります。その理由は以下の通りです:

1. エンコード/デコード方式が単純(単なる数学演算=シフト)

2. protocol buffer自身のフレームワークコードとコンパイラが共同で行う

シリアル化されたデータのサイズが小さい(つまりデータ圧縮効果が良い)理由:

1. 特異なエンコード方式、例えばvarint、zigzagエンコード方式を採用している

2. T-L-Vのデータ保存方式を採用:セパレータの使用を減らし、データ保存を密にしている

シリアル化の選定
------

1. シリアル化の空間コスト、つまりシリアル化結果のサイズ、これは転送性能に影響します;

2. シリアル化中に消費する時間、シリアル化時間が長すぎると業務の応答時間に影響します;

3. シリアル化プロトコルがクロスプラットフォーム、クロスランゲージをサポートしているかどうか。現在のアーキテクチャはより柔軟になっており、異種システム間の通信が必要な場合、これは考慮しなければならない;

4. 拡張性/互換性、実際のビジネスではシステムが迅速な需要の反復によって迅速に更新されなければならないため、採用するシリアル化プロトコルは良好な拡張性/互換性を持つ必要があります。例えば、現在のシリアル化データ構造に新しいビジネスフィールドを追加しても、既存のサービスに影響を与えないようにする必要があります。

5. 技術の流行度、より一般的な技術は使用する会社が多く、そのための技術ソリューションも相対的に成熟しています

6. 学習の難易度と使いやすさ

選定の推奨
----

パフォーマンスが高くないシナリオでは、XMLベースのSOAPプロトコルを使用することができます

2. パフォーマンスと中間性に高い要件があるシナリオでは、Hessian、Protocol Buffers、Thrift、Avroなどが利用可能です。

3. 前後端分離または独立した外部APIサービスの場合、JSONを選択するのは良い選択です。デバッグや可読性が非常に良いです。

4. Avroのデザイン思想は動的型言語に偏っているため、このようなシナリオではAvroを使用することは可能です。

さまざまなシリアル化技術のパフォーマンス比較
------------

異なるシリアル化技術のパフォーマンス比較: https://github.com/eishay/jvm-serializers/wiki

タグ: Java XML JSON Protocol Buffers Hessian

5月13日 07:11 投稿