LocalDateTimeの使用に関連したシリアル化操作時のパフォーマンスの問題


この記事では、アリババのエンジニアがシリアル化処理中に発生した、LocalDateTimeおよびInstant timeフォーマットの使用に関連したパフォーマンスの問題について説明します。

本ブログは英語版からの翻訳です。オリジナルはこちらからご確認いただけます。一部機械翻訳を使用しております。翻訳の間違いがありましたら、ご指摘いただけると幸いです。

Lv Renqi氏より

パフォーマンスの問題

Apache Dubbo の新バージョンで性能圧力テストを行った際、Transfer Object (TO) クラスの属性に関連する問題を発見しました。DateLocalDateTimeに変更すると、スループットが5万から2万に低下し、応答時間が9msから90msに増加しました。

これらの変更の中で、私たちが最も気になったのは応答時間の変更でした。パフォーマンス指標は、一定の応答時間レベルが確保されて初めて意味を持つため、応答時間は多くの点で優れたパフォーマンス数値の礎となります。ストレステストの場合、ギガビット・パー・セカンド(GPS)やトランザクション・パー・セカンド(TPS)の数値は、目標とする応答時間の数値が満たされた場合にのみ許容されます。純粋な理論上の数字は意味がありません。クラウド・コンピューティングでは、応答時間のすべてのビットが重要です。基盤となるサービスの応答時間が0.1ms増加しただけでも、全体のコストが10%増加することを意味します。

レイテンシーは、リモートユーザーのいるシステムのアキレス腱のようなものです。データパケットの遅延は100kmごとに1ミリ秒ずつ増加します。杭州-上海間の待ち時間は約5ミリ秒で、上海-深セン間の待ち時間は、距離がかなり大きくなるため、当然ながらさらに高くなります。レイテンシーの直接的な結果はレスポンスタイムの増加であり、これは全体的なユーザーエクスペリエンスを悪化させ、コストを膨らませます。

リクエストが異なる単位で同じ行のレコードを変更した場合、たとえ一貫性と整合性を維持できたとしても、コストは非常に高くなります。アリババ内で広く使われている分散型RPCサービスフレームワークであるリモート高速サービスフレームワーク(HSF)サービスや他のリモートデータベースに10回以上アクセスする必要があるリクエストで、1つのサービスが別のサービスを呼び出す場合、レイテンシはすぐに加算され、雪だるま式の効果をもたらします。

Javaにおける普遍性の重要性

時間を扱うことは、コンピュータサイエンスの世界ではどこにでもあることです。時間の厳密な概念がなければ、アプリケーションの99.99%は意味を失い、実用性を失います。特に、最近ではクラウド上のほとんどの監視システムで見られる時間指向のカスタム処理がそうです。

Java Development Kit 8(JDK 8)以前は、java.util.Dateが日付と時刻を記述するために使用され、java.util.Calendarが時間に関連したコンピューティングに使用されていました。JDK 8では、InstantLocalDateTimeOffsetDateTimeZonedDateTimeなど、より便利な時間クラスが導入されました。一般的に、これらのクラスのおかげで、時間処理がより便利になりました。

Instant は、協定世界時(UTC)形式でタイムスタンプを保存し、マシンに面した、または内部の時刻表示を提供します。これは、データベースストレージ、ビジネスロジック、データ交換、およびシリアライズのシナリオに適しています。LocalDateTimeOffsetDateTime、およびZonedDateTimeには、タイムゾーンまたは季節の情報が含まれており、また、ユーザーにデータを入出力するため時間表示を提供します。同じ時間が異なるユーザに出力される場合、その値は異なります。例えば、注文の発送時間は、買い手と売り手に異なる現地時間で表示されます。これら3つのクラスは、アプリケーションの内部作業部分ではなく、外部に向けたツールと考えることができます。

要するに、Instantはバックエンドのサービスやデータベースに向いていますが、LocalDateTimeとそのコホートはフロントエンドのサービスや表示に向いています。この2つは理論的には互換性がありますが、実際には異なる機能を果たしています。国際ビジネスチームは、この点について豊富な経験と考えを持っています。

DateInstantは、アリババ社内の高速サービスフレームワーク(HSF)とDubboを統合する際によく使われています。

パフォーマンス問題の再現

前に見たパフォーマンス問題の背景にあるものを正確に把握するために、その再現を試みることができます。しかし、その前に、簡単なデモを通して Instant のパフォーマンスの利点を考えてみましょう。そのためには、Date 形式で日付を定義し、その後に Instant 形式を使用するという一般的なシナリオを考えてみましょう。

    @Benchmark
    @BenchmarkMode(Mode.Throughput)
    public String date_format() {
        Date date = new Date();
        return new SimpleDateFormat("yyyyMMddhhmmss").format(date);
    }

    @Benchmark
    @BenchmarkMode(Mode.Throughput)
    public String instant_format() {
        return Instant.now().atZone(ZoneId.systemDefault()).format(DateTimeFormatter.ofPattern(
                "yyyyMMddhhmmss"));
    }

これを行った後、ローカルの4つのコンカレントスレッドで30秒間ストレステストを実行します。結果は以下のようになります。

Benchmark                            Mode  Cnt        Score   Error  Units
DateBenchmark.date_format           thrpt       4101298.589          ops/s
DateBenchmark.instant_format        thrpt       6816922.578          ops/s

これらの結果から、フォーマット性能の面ではInstantが有利であると結論づけられます。実際、他の操作に関しても、Instantは性能面で優位性を持っています。例えば、日付と時刻の足し算と引き算の演算において、Instantは有望な性能を示していることがわかりました。

シリアライゼーション操作中のインスタントの落とし穴

次に、上で見た問題のレプリケーションとして、JavaとHessian(タオバオ向けに最適化されている)でそれぞれシリアライズとデシリアライズ操作時のパフォーマンスの変化を見るためのストレステストも行いました。

HessianはHSF 2.2とDubboのデフォルトのシリアライズスキーム:

    @Benchmark
    @BenchmarkMode(Mode.Throughput)
    public Date date_Hessian() throws Exception {
        Date date = new Date();
        byte[] bytes = dateSerializer.serialize(date);
        return dateSerializer.deserialize(bytes);
    }

    @Benchmark
    @BenchmarkMode(Mode.Throughput)
    public Instant instant_Hessian() throws Exception {
        Instant instant = Instant.now();
        byte[] bytes = instantSerializer.serialize(instant);
        return instantSerializer.deserialize(bytes);
    }

    @Benchmark
    @BenchmarkMode(Mode.Throughput)
    public LocalDateTime localDate_Hessian() throws Exception {
        LocalDateTime date = LocalDateTime.now();
        byte[] bytes = localDateTimeSerializer.serialize(date);
        return localDateTimeSerializer.deserialize(bytes);
    }

結果は以下の通りでした。ヘシアンプロトコルを使用することで、Instant形式とLocalDateTime形式を使用した場合には、スループットが急激に低下しました。実際には、Date形式を使用した場合に比べて100倍もスループットが低下しています。さらに調べてみると、Dateのシリアル化バイトストリームは6バイトであるのに対し、LocalDateTimeのストリームは256バイトであることがわかりました。また、送信のためのネットワーク帯域幅のコストも大きくなっています。Javaのビルトインシリアライズソリューションでは、若干の低下が見られますが、実質的な違いはありません。

Benchmark                         Mode  Cnt        Score   Error  Units
DateBenchmark.date_Hessian       thrpt       2084363.861          ops/s
DateBenchmark.localDate_Hessian  thrpt         17827.662          ops/s
DateBenchmark.instant_Hessian    thrpt         22492.539          ops/s
DateBenchmark.instant_Java       thrpt       1484884.452          ops/s
DateBenchmark.date_Java          thrpt       1500580.192          ops/s
DateBenchmark.localDate_Java     thrpt       1389041.578          ops/s

課題分析

我々の分析は以下の通りです。Dateはヘシアンオブジェクトのシリアライズの8つの原始型のうちの1つである。

次に、Instantはシリアライズとデシリアライズの両方でClass.forNameを経由しなければならないため、スループットと応答時間の急激な低下を引き起こしました。したがって、Dateの方が有利です。

最後の感想

Instantなどのクラスにcom.alibaba.com.caucho.hessian.io.Serializerを拡張機能を介して実装し、SerializerFactoryに登録することでHessianをアップグレードして最適化できることがわかりましたので、この記事で取り上げた問題を解消することができます。ただし、それ以前のバージョンや今後のバージョンとの互換性の問題が出てきます。これは深刻な問題です。Alibabaのかなり複雑な依存関係がこれを不可能にしています。この問題を考えると、私たちができる唯一の推奨事項は、TOクラスの好ましい時間属性としてDateを使用することです。

技術的には、HSFのRPCプロトコルはセッション層のプロトコルであり、バージョン認識もここで行われます。しかし、サービスデータのプレゼンテーション層は、Hessianのような自己記述的シリアライズフレームワークで実装されており、バージョン認識には欠けています。そのため、バージョンアップが非常に困難になります。

アリババクラウドは日本に2つのデータセンターを有し、世界で60を超えるアベラビリティーゾーンを有するアジア太平洋地域No.1(2019ガートナー)のクラウドインフラ事業者です。
アリババクラウドの詳細は、こちらからご覧ください。
アリババクラウドジャパン公式ページ