シリアライゼーションとデシリアライゼーションは、常に分散プログラミングで回避できないトピックでした。PowerJobは、完全な意味での分散システムであるため、ノード通信中に必然的にシリアル化の問題が避けられません。PowerJobはミドルウェアとして位置付けられているため、パフォーマンスを追求するより、シリアライゼーションに多くの時間を費やすのは当然です。以下は、プロセス全体でのいくつかの経験と共有です。それがすべての人に役立つことを願っています。
1.シリアライゼーションの新興企業:kryo
現在最速のシリアライゼーションフレームワークであるkryoは、当然私に好まれています。PowerJobでは、kryoは組み込みのデフォルトのシリアル化フレームワークです。kryoの使い方の紹介です。
1.1基本的な使用法
シリアライゼーションフレームワークの場合、APIは実際には同じです。結局のところ、入力パラメーターと出力パラメーターが定義されています(1つはシリアル化が必要なオブジェクトで、もう1つはバイト配列などのシリアル化された結果です)。以下では、kryoの基本的な使い方を簡単に紹介しますが、シリアライゼーションとデシリアライゼーションは類似しているため、以下のデモではシリアライゼーションを使用します。
Kryo kryo = new Kryo();
try (Output opt = new Output(1024, -1)) {
kryo.writeClassAndObject(opt, obj);
opt.flush();
return opt.getBuffer();
}
コードは非常に単純です。まず、KryoとOutputの2つのオブジェクトを作成する必要があります。その中で、Kryoはシリアライゼーションの主人公で、実際のシリアライゼーション/デシリアライゼーション作業を担当しています。出力は、kryoフレームワークによってカプセル化され、シリアル化されたバイナリデータを格納するために使用されるストリームオブジェクトです。2つのオブジェクトの準備ができたら、kryo.writeClassAndObject(opt, obj)
メソッドを呼び出して オブジェクトのシリアル化を完了し、最後getBuffer()
に出力ストリームオブジェクトのメソッドを呼び出して、バイナリ配列であるシリアル化結果を取得します 。
1.2スレッドは安全ではありません
誰もがfastjsonを使用していると思います。fastjsonに初めて触れると、そのシンプルなAPIにきっと惹かれます。一般的に使用されるシリアル化/非シリアル化は、たとえば1行のコードで実行されます JSON.toJSONString()
。一般的に言えば、静的メソッドを通じて公開されるこのAPIの背後にある設計と実装はスレッドセーフです。つまり、マルチスレッド環境では、シリアル化と逆シリアル化にfastjson静的メソッドを安全に使用できます。クリョは大丈夫ですか?
上記のコードから理解することは難しくありません、いいえ〜それ以外の場合、なぜ人々はあなたが一挙にオブジェクトを作成して使用コストを増やすことを望んでいるのですか?
同志王ジンシは、条件がない場合、条件が作成されると述べました。kryoは私たちが簡単に使用できる静的メソッドを公式に提供していないので、自分でパッケージ化してください〜
パフォーマンス要因を除けば、ツールクラスをカプセル化するのは非常に簡単です。結局のところ、私たちの目標は、kryoの同時実行性のセキュリティ問題を解決することであり、共有リソースがない場合は、同時実行性のセキュリティ問題は発生しません。次に、サンプルコードに静的メソッドを追加するだけで、最も簡単なkryoツールクラスパッケージを完成できます。コード例は次のとおりです。
public static byte[] serialize(Object obj) {
Kryo kryo = new Kryo();
try (Output opt = new Output(1024, -1)) {
kryo.writeClassAndObject(opt, obj);
opt.flush();
return opt.getBuffer();
}
}
セキュリティの問題は解決されましたが、多くの場合、それほど単純ではありません。このモードでは、2つの新しいオブジェクト(KryoとOutput)が呼び出しごとに繰り返し作成されるため、同時実行性が高い場合に多くのオーバーヘッドが発生します。パフォーマンスを向上させるには、オブジェクトの再利用を検討するのが自然です。オブジェクトの再利用には、オブジェクトプールとThreadLocalの2つの一般的なソリューションがあります。
1.3オブジェクトプール
プログラミングでは、「プール」という用語は誰にでもよく知られています。スレッドプールと接続プールは、並行プログラミングのすでに避けられない部分です。「プール」は、再利用のアイデアを再利用し、作成されたオブジェクトを繰り返し使用できるようにコンテナーに保存して、パフォーマンスを向上させます。これがKryoオブジェクトプールの原理です。Kryoフレームワークにはオブジェクトプールの実装が付属しているので、プールの作成、プールからのオブジェクトの取得、オブジェクトの返却にすぎず、使用方法は非常に簡単です。コード例は次のとおりです。
まず、Kryoオブジェクトプールを作成します。Poolインターフェースのcreateメソッドをオーバーライドすることで、カスタム構成のオブジェクトプールを作成できます。
private static final Pool<Kryo> kryoPool = new Pool<Kryo>(true, false, 512) {
@Override
protected Kryo create() {
Kryo kryo = new Kryo();
// 关闭序列化注册,会导致性能些许下降,但在分布式环境中,注册类生成ID不一致会导致错误
kryo.setRegistrationRequired(false);
// 支持循环引用,也会导致性能些许下降 T_T
kryo.setReferences(true);
return kryo;
}
};
kryoを使用する必要がある場合は、 kryoPool.obtain()
メソッドを呼び出し、kryoPool.free(kryo)
返されたオブジェクトを使用して、完全なリースを完了した後に呼び出し ます。
public static byte[] serialize(Object obj) {
Kryo kryo = kryoPool.obtain();
// 使用 Output 对象池会导致序列化重复的错误(getBuffer返回了Output对象的buffer引用)
try (Output opt = new Output(1024, -1)) {
kryo.writeClassAndObject(opt, obj);
opt.flush();
return opt.getBuffer();
}finally {
kryoPool.free(kryo);
}
}
オブジェクトプールテクノロジーは、すべての並行性セキュリティソリューションの中で最高のパフォーマンスを発揮します。オブジェクトプールのサイズが適切に評価されている限り、非常に小さなメモリスペースを占有しながら並行性セキュリティの問題を完全に解決できます。これは、PowerJobの初期の頃に使用されていたソリューションでもあります... PowerJobが正式にコンテナ機能を開始してから、完璧なソリューションを放棄する必要がありました。
コンテナーモードで、kryoオブジェクトプール計算の使用に関する問題は何ですか?みなさんへの簡単な説明ですが、分からない場合は運次第です〜
PowerJobコンテナー関数は、実行のために外部コードを動的にロードすることを指します。分離のため、PowerJobは別のクラスローダーを使用して、コンテナー内のクラスのロードを完了します。したがって、各powerjob-workerには複数のクラスローダーがあります。つまり、システムクラスローダー(プロジェクトの読み込みを担当)と各コンテナーの独自のクラスローダー(コンテナークラスの読み込み)です。シリアル化ツールクラスは当然powerjob-workerの一部であり、powerjob-workerの起動時に作成されます。kryoオブジェクトプールが作成されるときに使用されるクラスローダーは、システムクラスローダーです。したがって、コンテナー内のクラスをシリアル化/逆シリアル化する必要がある場合、kryoは独自のクラスローダーから関連するクラス情報を取得できず、ClassNotFoundErrorを適切にスローできません。
したがって、PowerJobがコンテナーテクノロジーを導入した後は、2番目にして2番目の同時実行の安全性メソッドであるThreadLocalを採用するしかありませんでした。
1.4 ThreadLocal
ThreadLocalは、同時実行の安全性と引き換えにスペースを犠牲にする一般的な方法であり、スレッドごとに個別のスレッド固有のkryoオブジェクトを作成します。各スレッドの各kryoオブジェクトについて、順次実行されるため、同時実行の安全性の問題は自然に回避されます。作成方法は以下のとおりです。
private static final ThreadLocal<Kryo> kryoLocal = ThreadLocal.withInitial(() -> {
Kryo kryo = new Kryo();
// 支持对象循环引用(否则会栈溢出),会导致性能些许下降 T_T
kryo.setReferences(true); //默认值就是 true,添加此行的目的是为了提醒维护者,不要改变这个配置
// 关闭序列化注册,会导致性能些许下降,但在分布式环境中,注册类生成ID不一致会导致错误
kryo.setRegistrationRequired(false);
// 设置类加载器为线程上下文类加载器(如果Processor来源于容器,必须使用容器的类加载器,否则妥妥的CNF)
kryo.setClassLoader(Thread.currentThread().getContextClassLoader());
return kryo;
});
その後は、*kryoLocal*.get()
メソッドを使用してスレッドコンテキストからオブジェクトを取得するだけで 使用できます。これもシンプルで使いやすいソリューションです。(理論上のパフォーマンスはオブジェクトプールよりもはるかに悪いですが)
2.古いフレーム:ジャクソン
誰もが有名なジャクソンについて聞いたことがあると思います。それはまた、多くのプロジェクトのクイーンJSONシリアライズ/デシリアライズフレームワークでもあります。PowerJobでは、ホイールを再作成しないという原則に基づいて、jackson-cborがakka通信層のデフォルトのシリアル化フレームワークとして使用されます。
「なぜ、パフォーマンスが良く、プロジェクトにすでに組み込まれているKryoを使用しないのですか?」
「もちろん、それは公式のakkaがkryoの正式な実装を提供しなかったからです...」
kryoを使用する場合、多くのコーデックを自分で実装する必要があります。これはnettyを書くように見えます...しかし、jackson-cborはどうですか?ほんの少しの設定が必要です〜
actor {
provider = remote
allow-java-serialization = off
serialization-bindings {
"com.github.kfcfans.powerjob.common.OmsSerializable" = jackson-cbor
}
}
組み込みのJavaシリアル化メソッドと比較して、絶対的なパフォーマンスはkryoほど良くない場合がありますが、パフォーマンスは10倍以上改善されており、ほとんどのシナリオでパフォーマンスのボトルネックにはなりません。だから〜拒否する理由〜
最後に、3
これでこの記事は終わりです。次の記事では、PowerJobのユニークな分散コンピューティング機能の背後にある原理の分析を紹介します。このような重い記事は、このコラムのフィナーレとしては適切ではありません〜