Javaのパフォーマンスを大幅に向上させる方法は?


23

LMAXのチームは、1ミリ秒未満のレイテンシで100k TPSを実行する方法についてプレゼンテーションを行いました。彼らは、ブログテクニカルペーパー(PDF)、およびソースコード自体を使用して、そのプレゼンテーションをバックアップしています。

最近、Martin Fowler氏は、公開LMAXアーキテクチャ上の優れた論文を、チームがパフォーマンスに大きさの別の注文を上がるために取ったステップのいくつかを、彼らは今、毎秒600万件の注文を処理することができることを言及し、ハイライト。

これまで、ビジネスロジックプロセッサの速度の鍵は、すべてをメモリ内で順番に実行することであると説明してきました。これを実行するだけで(本当に愚かではありません)、開発者は10K TPSを処理できるコードを作成できます。

その後、優れたコードの単純な要素に集中することで、これを100K TPSの範囲に引き上げることができることがわかりました。これには、適切にコード化されたコードと小さなメソッドが必要です-基本的に、これによりHotspotは最適化のより良い仕事をすることができ、実行中のコードをより効率的にキャッシュすることができます。

もう1桁上るには、もう少し賢くなりました。LMAXチームがそこにたどり着くのに役立つとわかったことがいくつかあります。1つは、キャッシュに対応し、ゴミに注意するように設計されたJavaコレクションのカスタム実装を記述することでした。

その最高レベルのパフォーマンスに到達する別の手法は、パフォーマンステストに注意を払うことです。私は長い間、人々がパフォーマンスを改善するためのテクニックについて多くのことを話していることに気づきましたが、実際に違いを生むのはそれをテストすることです

ファウラーは、発見されたいくつかの事柄があると述べましたが、彼はいくつかだけに言及しました。

このようなレベルのパフォーマンスを達成するのに役立つ他のアーキテクチャ、ライブラリ、テクニック、または「もの」はありますか?


11
「このようなレベルのパフォーマンスを達成するのに役立つ他のアーキテクチャ、ライブラリ、テクニック、または「もの」は何ですか?」何で質問する?その引用は、ある決定的なリスト。他にもたくさんのものがありますが、どれもそのリストのアイテムのような影響はありません。誰でも名前を付けられるものは、そのリストほど有用ではありません。これまでに作成された最高の最適化リストの1つを引用したのに、なぜ悪いアイデアを求めるのですか?
-S.Lott

生成されたコードがシステム上でどのように実行されるかを見るためにどのツールを使用したかを知ることは素晴らしいことです。

1
私は、人々があらゆる種類のテクニックで誓うことを聞いたことがあります。私が最も効果的だと思ったのは、システムレベルのプロファイリングです。プログラムとワークロードがシステムを実行する方法のボトルネックを示すことができます。後で簡単に調整できるように、パフォーマンスに関するよく知られているガイドラインを遵守し、モジュラーコードを記述することをお勧めします。
リテッシュ

回答:


21

高性能なトランザクション処理にはあらゆる種類のテクニックがあり、ファウラーの記事にあるテクニックは最先端の多くのテクニックの1つにすぎません。誰の状況にも当てはまるかもしれないし、当てはまらないかもしれない多くのテクニックをリストするよりも、基本原則とLMAXがそれらの多くに対処する方法を議論する方が良いと思います。

大規模なトランザクション処理システムの場合、次のすべてを可能な限り実行する必要があります。

  1. 最も遅いストレージ層で費やされる時間を最小限にします。最新のサーバーで最速から最速まで:CPU / L1-> L2-> L3-> RAM->ディスク/ LAN-> WAN。最速の最新の磁気ディスクから最も遅いRAMへのジャンプは、シーケンシャルアクセスで1000倍以上です。ランダムアクセスはさらに悪化します。

  2. 待機に費やす時間を最小化または排除します。これは、できるだけ少ない状態を共有することを意味し、状態を共有する必要がある場合は、可能な限り明示的なロックを回避します。

  3. ワークロードを分散します。CPUは、過去数年でかなり速くもらっていないが、彼らはしている小さい得て、そして8つのコアは、サーバー上でかなり一般的です。さらに、作業を複数のマシンに分散させることもできます。これはGoogleのアプローチです。これの素晴らしいところは、I / Oを含むすべてをスケーリングできることです。

ファウラーによると、LMAXはこれらのそれぞれに対して次のアプローチを取ります。

  1. キープすべてのメモリの状態をすべての回。データベース全体がメモリに収まる場合、ほとんどのデータベースエンジンは実際にこれを実際に実行しますが、リアルタイムトレーディングプラットフォームで理解できるチャンスに任せたくありません。大量のリスクを追加せずにこれを実現するために、彼らは軽量のバックアップおよびフェールオーバーインフラストラクチャの束を構築する必要がありました。

  2. 入力イベントのストリームにロックフリーキュー(「ディスラプター」)を使用します。確実にロックフリーではなく、実際には通常、非常に遅い分散トランザクションを伴う従来の永続メッセージキューとは対照的です。

  3. あまりない。LMAXは、ワークロードが相互に依存していることに基づいて、これをバスの下にスローします。1つの結果が他のパラメーターを変更します。これは重要な警告であり、ファウラーが明示的に呼び出しています。フェイルオーバー機能を提供するために並行性をある程度利用してますが、すべてのビジネスロジックは単一のスレッドで処理されます

LMAXは、大規模OLTPへの唯一のアプローチではありません。また、それ自体は非常に優れていますが、そのレベルのパフォーマンスを引き出すために最先端の技術を使用する必要ありませ

上記のすべての原則の中で、ハードウェアは安価であるため、#3がおそらく最も重要かつ最も効果的です。半ダースのコアと数ダースのマシンにワークロードを適切に分割できれば、空は従来の並列コンピューティング技術の限界になります。大量のメッセージキューとラウンドロビンディストリビューターだけで、どれほどのスループットを達成できるか驚くでしょう。明らかにLMAXほど効率的ではありません-実際には近いものでもありませんが、スループット、レイテンシ、費用対効果は別の懸念事項です。ここでは、スループットについて具体的に説明します。

LMAXと同じ種類の特別なニーズ(特に、急いでデザインを選択するのではなくビジネスの現実に対応する共有状態)がある場合は、そのコンポーネントを試してみることをお勧めします。それ以外は、それらの要件に適しています。しかし、単に高いスケーラビリティについて話している場合は、今日ほとんどの組織(Hadoopと関連プロジェクト、ESBと関連アーキテクチャ、FQlerが使用するCQRS言及など)。

SSDは、ゲームチェンジャーにもなります。おそらく、彼らはすでにそうです。RAMへのアクセス時間が同程度の永続ストレージを使用できるようになりました。サーバーグレードのSSDは依然として非常に高価ですが、採用率が上がると最終的に価格が下がります。 徹底的に研究されており、結果はかなり気が遠く、時間の経過とともに良くなるだけなので、「すべてをメモリに保存する」という概念は、以前ほど重要ではなくなりました。繰り返しになりますが、可能な限り並行性に焦点を当てようとします。


ファウラーの論文は、キャッシュ忘れアルゴリズムへの脚注参照持っていなかった場合を除き原則を議論するとの原則を根底されることは...素晴らしいですし、あなたのコメントが優れているとen.wikipedia.org/wiki/Cache-oblivious_algorithmにうまくフィット(あなたが上記のカテゴリー番号1)私は彼らにつまずいたことがなかっただろう。だから...上記の各カテゴリに関して、人が知っておくべき上位3つのことを知っていますか?
ダコタノース

@Dakotah:私もないだろう開始、私は完全に時間の大半は、アプリケーションの大半に待機に費やされているところであるディスクI / Oを、排除していないとまで、キャッシュの局所性を心配します。それとは別に、「人が知っておくべきトップ3」とはどういう意味ですか?トップ3何、何を知るために?
アーロンノート

RAMアクセスの待ち時間(〜10 ^ -9s)から磁気ディスクの待ち時間(〜10 ^ -3s平均ケース)へのジャンプは、1000xよりも数桁大きいです。SSDでさえ、数百マイクロ秒で測定されるアクセス時間を保持しています。
落ち着いたエイリアン

@Sedate:レイテンシはい。ただし、これは生のレイテンシよりもスループットの問題です。アクセス時間を過ぎて合計転送速度に達すると、ディスクはそれほど悪くありません。そのため、ランダムアクセスとシーケンシャルアクセスを区別しました。ランダムアクセスのシナリオでは、主に遅延の問題になります。
アーロンノート

@Aaronaught:読み直すと、あなたは正しいと思います。おそらく、すべてのデータアクセスは可能な限りシーケンシャルにすべきであるという点を指摘する必要があります。RAMから順番にデータにアクセスする場合にも、大きな利点が得られます。
落ち着いたエイリアン

10

これから学ぶための最大の教訓は、基本から始める必要があるということです。

  • 優れたアルゴリズム、適切なデータ構造、および「本当に愚かな」ことは何もしません
  • よく考慮されたコード
  • 性能試験

パフォーマンステスト中に、コードのプロファイルを作成し、ボトルネックを見つけて、それらを1つずつ修正します。

あまりにも多くの人が「1つずつ修正」の部分にジャンプします。彼らは「javaコレクションのカスタム実装」を書くのに多くの時間を費やしています。彼らのシステムが遅い理由はキャッシュミスのためだと知っているからです。それが一因かもしれませんが、そのような低レベルのコードを微調整すると、LinkedListを使用する必要があるときにArrayListを使用するという大きな問題を見逃す可能性が高くなります。遅いのは、ORMがエンティティの子を遅延読み込みしているため、リクエストごとにデータベースに400回別々にアクセスするためです。


7

LMAXコードについては特に説明しませんが、これは十分に説明されていると思いますが、ここに大幅な測定可能なパフォーマンスの改善をもたらしたいくつかの例を示します。

いつものように、これらは問題がありパフォーマンスを改善する必要があることがわかったら適用すべきテクニックです。そうでなければ、早すぎる最適化を行っているだけです。

  • 適切なデータ構造を使用し、必要に応じてカスタム構造を作成します。データ構造の設計を修正すると、マイクロ最適化から得られる改善が小さくなります。最初にこれを実行してください。アルゴリズムが多くの高速O(1)ランダムアクセス読み取りのパフォーマンスに依存している場合、これをサポートするデータ構造があることを確認してください!これを正しくするためにいくつかのフープを飛び越える価値があります。たとえば、非常に高速なO(1)インデックス付き読み取りを活用するために配列でデータを表す方法を見つけることです。
  • CPUはメモリアクセスよりも高速です -メモリがL1 / L2キャッシュにない場合、1つのランダムメモリを読み取るのにかかる時間で、かなりの計算を行うことができます。メモリの読み取りを節約できる場合は、通常、計算する価値があります。
  • finalでJITコンパイラーを支援 -フィールド、メソッド、クラスをfinalにすることで、JITコンパイラーを本当に支援する特定の最適化が可能になります。具体例:

    • コンパイラは、最終クラスにサブクラスがないと想定できるため、仮想メソッド呼び出しを静的メソッド呼び出しに変換できます。
    • 特にコンパイル時に計算できる計算で定数が使用される場合、コンパイラは静的な最終フィールドを定数として扱い、パフォーマンスを向上させることができます。
    • Javaオブジェクトを含むフィールドがfinalとして初期化される場合、オプティマイザーはnullチェックと仮想メソッドのディスパッチの両方を排除できます。いいね
  • コレクションクラスを配列に置き換えます。これにより、コードが読みにくくなり、保守が難しくなりますが、多くの優れた配列アクセス最適化による間接化の層と利点が削除されるため、ほとんど常に高速です。通常、内部ループ/パフォーマンスに敏感なコードは、それをボトルネックとして特定した後は良い考えですが、読みやすくするために他の方法は避けてください!

  • 可能な限りプリミティブを使用します -プリミティブは、オブジェクトベースの同等のものより基本的に高速です。特に、ボクシングは大量のオーバーヘッドを追加し、GCの一時停止を引き起こす可能性があります。パフォーマンス/遅延を気にする場合は、プリミティブをボックス化しないでください。

  • 低レベルのロックを最小限に抑える -ロックは低レベルで非常に高価です。完全にロックしないようにする方法、または粗いレベルでロックする方法を見つけて、大きなデータブロックで頻繁にロックする必要がなく、ロックや同時実行の問題をまったく心配せずに低レベルのコードを続行できるようにします。

  • メモリの割り当てを回避する -JVMガベージコレクションは非常に効率的であるため、実際には全体的に速度が低下する可能性がありますが、非常に低いレイテンシに到達してGCの一時停止を最小限に抑える必要がある場合は非常に役立ちます。割り当てを回避するために使用できる特別なデータ構造があります。特にhttp://javolution.org/ライブラリはこれらに優れており、注目に値します。

方法を最終的にすることに私は同意しません。JITは、メソッドが決してオーバーライドされないことを把握できます。さらに、サブクラスが後でロードされる場合、最適化を取り消すことができます。また、「メモリの割り当てを避ける」ことはGCの仕事をより難しくし、それによってあなたを遅くするかもしれないことに注意してください-それで注意して使用してください。
maaartinus

@maaartinus:finalいくつかのJITはそれを理解するかもしれないが、他のJITは理解しないかもしれない。実装に依存します(多くのパフォーマンスチューニングのヒントと同様)。割り当てについて同意します-これをベンチマークする必要があります。通常、割り当てを削除する方が良いことがわかりましたが、YMMVです。
ミケラ

4

アーロンノートからの優れた回答ですでに述べられている以外に、そのようなコードは開発、理解、デバッグが非常に難しいかもしれないことに注意したいと思います。「非常に効率的ですが...簡単に台無しになります...」とLMAXブログで彼らの仲間が言及しました。

  • 従来のクエリとロックに慣れている開発者にとって、新しいアプローチのコーディングは野生の馬に乗っているように感じるかもしれません。少なくともそれは、LMAXテクニカルペーパーにその概念が記載されているPhaserを実験したときの私自身の経験でした。その意味で、このアプローチは開発者の脳の競合とロックの競合を交換すると言うでしょう。

上記のことから、Disruptorや同様のアプローチを選択した人は、ソリューションを維持するのに十分な開発リソースを確保していると思います。

全体として、ディスラプターのアプローチは私にとって非常に有望に見えます。あなたの会社は理由が上記のために例えば、それを利用し余裕がない場合であっても、(そしてそれを研究にいくつかの努力を「投資」へのあなたの管理を説得考えるSEDA一般的には) -そうでない場合は、そのチャンスがありますので、1日顧客は、4倍、8倍などの少ないサーバーを必要とする、より競争力のあるソリューションを好みます。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.