トランスクリプト
Lawrey氏: 私はJavaチャンピオンだ。Stack Overflowで13,000の回答を持っている。memory、file-io、concurrencyでは、どの言語でも最初の金メダルホルダーだ。これで私の興味がお分かりいただけると思う。これは私の最初のコンピューターで、40年近く前のものだ。メモリは128キロバイトだった。その間、何年も使ったが、128キロバイト全部を使う用途は見つからなかった。8インチのフロッピーには、それぞれ1メガバイトのストレージがあった。
クロニクルソフトウェア
会社として、私たちのビジョンは、トレーディングシステムが必要とする共通ライブラリやインフラの80%を提供することだ。そうすることで、顧客はビジネス上の価値がどこにあるかに集中することができる。会社にとって、お金を稼ぐという点で何が違いを生むのか、そのために多くの時間を割くことができ、その結果、開発者をより効率的に活用できる。私たちは16の銀行と取引所で利用されており、彼らは有料顧客だ。全世界で毎月20万を超えるIPアドレスからダウンロードされている。約80%の銀行がすでにオープンソース製品として、中核取引システムの少なくとも1つで利用している。
私たちは、それぞれ30年以上の経験を持つITスペシャリストの重要なチームを擁している。専門性の高いチームだ。トレーディングシステムの開発とサポートの両方で多くの経験を持っている。クロニクルに在籍している間だけでも、5年から10年にわたりサポートしてきた顧客システムもある。Chronicle Queueは当社の主力製品で、毎月8万回ダウンロードされている。申し上げたように、オープンソースなので多くの人が使っている。オープンソースの製品で低レイテンシー部分を示す。私たちの重要な価値は、他の選択肢の多くと連携しやすかったことだ。低レイテンシー部分に着目すると、すべてが細かく調整されているため、作業しにくいことがよくある。そうなると、市場投入までに時間がかかる。修正が難しくなり、メンテナンスも難しくなる。私たちは、高性能であるにもかかわらず、作業や開発、テストが非常に簡単であることに重点を置いている。それを実現する方法のひとつが、スライディングスケールだ。まず、そこそこ高性能で、かつ作業が非常に簡単なものを用意する。システムが安定したら、さらにチューニングして必要なレイテンシーまで下げるオプションがある。
私たちが提供しているのは、Chronicle QueueやQueue Enterpriseといった耐久性のあるメッセージングだ。無料版はJavaだが、C++、Python、Rust版もある。マイクロサービス向けのリスタート戦略や高可用性もある。商業的に最も大きな製品のひとつは、fixed engineだ。これは、私たちがどのような会社で、私がどこから来たのか、その背景を説明するものだ。私と同じように30年以上の経験を持つ開発者チームが働いている。
スコープ
今日の話では、トレーディングシステムにおける効率とパフォーマンスの最大化について話す。しかし、こうしたことの多くはトレーディングシステムに限ったことではない。これらのことがより重要であるため、より目立つのだ。これらは広く役立つテクニックだ。トレーディング全般に特化したものではない。ユーザーの多くはゲームや、単に高いスループットを追求するのではなく、低レイテンシーを必要とするトランザクション型の情報に携わっている。オブジェクトの割り当てがGCのコストよりも最大80倍も 上回る理由について話す。レイテンシーの99%は、本質的な複雑さではなく、偶発的な複雑さかもしれない。64ビットのタイムスタンプをクラスタ全体で一意にする軽量な手法で、一意のIDが得られる。信頼できる情報源のレイテンシーを10分の1以下にする。最後に、データドリブンテストの90%を生成するテクニックを持つことで、すべてのテストを自分で作成・管理する必要がなくなる。Grace Hopper氏の言葉に、「英語で最も危険な言葉は、"We have always done it that way. "である」というものがある。彼女は実際に、初期のCOBOLコンパイラを開発するチームを率いていた。彼女は、コンパイル後にアプリケーションをリンクする技術を開発したことで知られている。コンパイル段階を経て、後でリンクさせる。彼女は米陸軍で提督の地位にあった。
スケーリング
リソースがスケールに失敗する可能性はどのように考えればいいのだろうか、一つの例として、オフィスから始めてみよう。少数の部屋があるオフィスを想像してみよう。例えるなら、これらの部屋のひとつひとつがCPUだ。仕事をするため、あるいは会議に出るために、人々はこれらの部屋を行き来できる必要がある。ある部屋から別の部屋へ移動するのに多くの時間を費やしているとしたら、それはオーバーヘッドであるだけでなく、ボトルネックにもなる。あなたが望むのは、廊下や廊下へのドアといった共有リソースがあなたの制約にならないように、それぞれの部屋でできるだけ多くの時間を費やすことだ。各CPUが独立して非常に効率的に動作していれば、共有リソースで競合することはない。問題のひとつは、世の中の多くのテストがシングルスレッドだということだ。例えばJMHはシングルスレッド・テストだ。これは要するに、ある人があるオフィスから別のオフィスに行くのにどれくらいの時間がかかるかをテストするものだ。最も単純なテストだ。これはベースラインだ。このテストでは、オフィスが本当に混雑しているときにどのような挙動を示すかはわからない。スレッドが1つしかないときにはとてもうまくいくことが、スレッドがたくさんあるときにはあまりうまくいかないということはないだろうか。
これはオープンソースのベンチマークで、マイクロサービスがどのように構成されているかを示している。入ってくるメッセージ用のインターフェースがある。出て行くメッセージやイベントのための別のインターフェースがある。このベンチマークでは、16のクライアントが同時に接続しており、32コアのマシンの論理コアをすべて使用している。この小さなオブジェクトのコピーを取るというフラグがある。わずか44バイトだ。それ自体は重要ではなく、スループットの点で、メッセージあたりのアロケーションレートが小さいだけでも、どれほどの違いがあるかを示すためのものだ。少ないコネクション数、あるいは使用されているスレッド数であれば、非常にうまくスケールする。オブジェクトの割り当てにかかるレイテンシーは18ナノ秒程度だ。これは大したことではないように思える。JMHのようなものが、特に小さい場合、オブジェクトの割り当てが本当に非常に安価であることを示すものだ。少数のスレッドが同時にアロケートしようとする場合でも、まだ問題はない。しかし、規模が大きくなり、システムでより多くのコアを使おうとすると、システムのすべてのコアを使おうとすると、特にL3キャッシュやメモリバスで競合が始まる。なぜならこれはすべてのコアで共有されるリソースだからだ。本当に必要なのは、すべてのコアを可能な限り使用しないことだ。明らかに、Javaではこれをほとんどコントロールできない。あなたがコントロールできる主なことは、どれだけ早く割り当てられるかだ。というのも、割り当てをするのは短命のオブジェクトだけで、文字通りゴミを作り出し、キャッシュをゴミで満たし、有用なデータを強制的にメモリーから追い出してしまうからだ。このことはボトルネックを作り出すことにもなる。
オブジェクトを割り当てるスレッドが増えると、どのようにスケールするのか、これは単純なテストで、私がやっているのはオブジェクト単体としての割り当てだけだ。スレッド数が4つくらいまでは、15ナノ秒から18ナノ秒にしかならない。あまり多くはなさそうだ。しかし、オブジェクトを割り当てしようとするスレッドが増えるにつれて、割り当て時間が長くなっているのがわかるだろう。実際、1秒あたりに生成されるオブジェクトの数は増えていない。なぜなら、閾値に達したからだ。別の視点から見てみると、異なるサイズのオブジェクトの場合、最も小さいオブジェクトでも150ナノ秒程度であることがわかる。そのことを言及したのは、他のベンチマークでも出てくるからだ。1秒あたりのギガバイト数で見ると、1秒あたりのギガバイト数は飽和していることがわかる。1秒間に何ギガバイト生成できるかという点では、オブジェクトサイズに違いがある。一般的なサーバーの場合、割り当て率は8ギガバイトから18ギガバイトの間だ。先ほどのベンチマークでは、TCPで毎秒6700万イベントを達成ができた。これはエコーサービスに対するものだ。エコーサービスにメッセージを送信しているのだが、エコーサービスが行っているのは、ただメッセージを送り返すことだけだ。これにはシリアライズも含まれており、明らかにエコーサービスは実作業はしていないが、1秒間に約40億のメッセージを受信しており、これはかなり高い数字だ。メッセージごとに1つのオブジェクトを割り当てると、約25%遅くなることがわかる。実際、全体に見られる平均は、先ほどのスタンドアロンベンチマークで見られた150ナノ秒をほぼ少し超える程度だ。
少なくともこのベンチマークでは、Java 11はJava 8より少し優れているようだ。結果はさまざまだ。良くなることもあれば、そうでないこともある。悪くなったことはない。確かに、パフォーマンスの観点からは、Java 11以上にするのは良いアイデアだ。比較のために、GCに費やされた時間はどうだったろうか。GCに費やされた時間は、非常に短命なオブジェクトが多かったため、クリーンアップは非常に簡単だ。GCに費やされた時間はわずか0.3%だった。GCログを見れば、非常に短い間だとわかるだろう。あまり頻繁ではないので、実際GCオーバーヘッドは非常に少ない。実際には、GCの割り当てや最初の作成で失われた時間の方がずっと大きい。実際、およそ80倍だ。これは自分で実行できるベンチマークだ。このような分析をしなくても、何か経験則があるのだろうか。以前は1秒間に約10ギガバイトが目安だった。最近のマシンは毎秒12ギガバイトくらいだろうか。マシン上のすべてのJVMの割り当て率を合計すると先述のようになるが、これは物理的なマシンのため、仮想化を利用していて、うるさい隣人がいる場合、まったく役に立たない。もしそれらを合計して、1秒あたり12ギガバイト前後なら、おそらくこの閾値に達している。おそらく最も簡単なうちの1つは、アプリの1つを別のマシンに再デプロイすることだ。アプリケーションをチューニングできる状態であれば、割り当て率を下げれば、システムが達成できるスループットはほぼ確実に向上する。
実際にいくつかのプロジェクトでそうしてきた。あるプロジェクトでは、メッセージあたりの割り当て率を10%下げるたびに、スループットが10%向上した。実際には、割り当て率は一定ではなかった。割り当て率の低下を確認し始める前に、割り当てを40%減らさなければならなかった。最適化の後、再実行するたびにスループットが上がっていたからだ。割り当てはちょうど飽和点であり、これ以上進まないようにしていたのだ。このノートパソコンでは、スレッド数が少ないうちは、割り当てしようがしまいが、あまり関係ないという、非常によく似た挙動が見られる。マシンをもっと使おうとすると、割り当てがボトルネックになり、それ以上スループットを上げたり、スレッドを増やしたりすることができなくなる。というのも、スレッド数を増やすことでアドバンテージを得ているわけではないからだ。割り当て率を下げれば、より多くのスレッドを使うことでスループットを向上できる。
偶発的な複雑性
もうひとつの名言、"完璧とは、これ以上付け加えるものがないときに達成されるものではなく、むしろ、奪うものがないときに達成されるものである"。これはAntoine de Saint-Exupery氏の言葉である。彼はフランス空軍の司令官であり、大西洋横断郵便のパイオニアでもある。著書も多数ある。これは、単に可能な限りのものを追加しようとするのではなく、実際には本当に必要でないものはすべて取り除くという工学的考え方の例としてよく引用される。確かに、多くの低遅延トレーディングシステムではそのような考え方が採用されている。より少ない労力でより速くする。あなたが探している大きな分野のひとつは、偶発的な複雑さである。本質的な複雑さとは、目の前の問題を解決するために必要な複雑さである。これ以上単純に問題を解決することはできない。なぜなら、それが達成しようとしている問題の本質だからだ。しかし、私たちはしばしば多くの偶発的な複雑さを抱えている。通常、抽象度や練習方法、問題を解決するための方法などだ。これはソリューションの特徴だ。これは解決方法の特徴なのだ。同じ問題を別の方法で解決することで、偶発的な複雑さを大幅に減らすことができる。偶発的な複雑さを測定するのは非常に難しい。実際にやってみないと、どれだけ速くできるかわからないからだ。ひとつのアプローチは、同じ問題を解決する複数の方法を比較することだ。もしそれらがあなたの要求も満たしているなら、最速の解決策と今やっていることの違いは、おそらく偶発的なものだろう。なぜなら、より少ないもので同じ結果が得られるからだ。
偶発的な複雑さの例として、ロンドンのセントポールからグローブ座までのドライブが挙げられる。特にロンドンはあまり論理的に配置されていない。このレイアウトは、もともと紀元1世紀か2世紀頃にローマ人によって設定されたものだ。実は17世紀ごろに大火事があって、実際にすべて碁盤目状に設計し直す計画があった。その後、問題が発生し、時間がかかったため、その頃には再建が始まっていた。レイアウトは変わっていない。建物の幅のいくつかは、ローマ時代の標準的な幅と同じである。このアプローチがいかに時代錯誤であるか、おわかりいただけるだろう。その結果、ロンドンでは一般的に車の運転は直線ではない。AからBに行くためには、このようにかなり複雑なルートを通らなければならない。これは、ソフトウェアでも実際に得られることの一例で、AからBに行くためには、できるのだが、まったく必要以上に多くの作業をしなければならないということだ。それは、レイアウトのようには見えないし、人間の尺度では見えないから、あなたにはわからない。ただ歩くだけならもっと短い。それでも直行とまではいかないが、より多くのルートを自由に使えるので、はるかに短くなる。
同じような問題がソフトウェアにもある。Kafkaを使ったラウンドトリップ・レイテンシーだ。メッセージがマイクロサービスに送られ、そのレスポンスがキューを経由して戻ってくる。これは、実際に目に見えるものよりも10倍速いので、評価するのは難しい。仮にこのレイテンシーが10倍だったとしても、まだ見ることはできないだろう。古典的な映画のフレームレートは約40ヘルツだ。映画の中でスライドが次のスライドに変わるのを見ることはできない。これは約25ミリ秒だ。これも見えない。これは10倍速い。本当に速いと思うだろう。人間のスケールじゃない。ニューヘイブンとワシントンの間に信号を送れる距離でもあり、これはかなり長い距離だ。比較のために言っておくと、これは私が見たKafkaの99パーセンタイルのベンチマークの半分くらいだ。Kafkaのベンチマークでは5ミリ秒程度と言われている。通常、私たちはミリ秒と書かれたものは引用しない。それは私たちにとって恥ずかしいことだからだ。ミリ秒単位なら修正するか、公表しない。そのようなことには触れない。
フライトレコーダーを使ってパフォーマンスプロファイルをやってみた。Javaアプリケーションで見られる非常に標準的なパターンで、GCが非常に速く行われる。私はこれをGCing like madと呼んでいる。これは実はとても標準的なことだ。どういうわけか、フライトレコーダーは2秒ごとにバーを表示する。2秒分のアロケーションだが、実際には1秒間に約16.5ギガバイトを処理している。興味深いことに、これは問題の約半分だ。プロファイリングしていないブローカーもある。おそらく、ほぼ同じ割り当て率になる。これらを足すと、1秒間に約13ギガバイトの割り当てになる。マシンのアロケーションの限界に達していると言えるだろう。これをオープンソースのChronicle Queueと比較すると、同じ往復にかかる時間は約3.7マイクロ秒。これは、バッテリーを歩いて横切るのと同じくらいの距離だ。これはとても短い。もうひとつは、GCがより静かになったことだ。これはJava 8でのことだ。これが重要なのは、このゴミのほとんどがアプリケーションではなくフライトレコーダー自身だからだ。この時点では、1秒間に50万件のメッセージを処理しているが、実際には表示されない。実際、Java 17ではフライトレコーダーが改良され、まったく何も表示されなくなった。
このスライドをここに残したのは、実際の低遅延アプリケーションでは、このような現象がよく見られ、これが典型的な例だからだ。ゴミのようなものでなければ、それほど多くはないだろうか、実際に見ることができるものだ。おそらくこの程度だろう。これは1秒間に約300キロバイトのゴミを処理している。毎秒300キロバイトというと、1時間に1ギガバイトだ。1時間に1ギガバイトということは、例えば24ギガバイトのEden領域があれば、Eden領域を埋めるのに24時間かかるという素晴らしい機能がある。一晩中GCを行うメンテナンスタスクがある場合、GCの計画を立てることができる。例えば、朝の4時にマイナークリックを1回、フルコレクションを1回行い、それがその日のGCとなる。そして翌日、Eden領域が徐々に埋まっていくのを見ることができる。この割り当て率で、一日中GCしない取引システムも可能だ。GCの休止時間は問題ではない。なぜならそれは発生せず、無関係になるからだ。これが、割り当て率が低いことの利点の一つだ。もちろん、それ以上に低くする必要は特にない。もしそうすることができたとしても、実際には必ずしも大きな成果は得られないだろう。どうせGCしないのだから、GCする頻度は変わらないだろう。また、どのガベージコレクタを選択するかはそれほど重要ではない。というのも、繰り返しになるが、通常の動作モードではコレクションをトリガーしていないからだ。
Chronicle Queueがかなり速いといっても、それはオープンソース版だ。この場合は緑色の線だ。C++をベースにしたクローズドソースバージョンもあり、そちらはさらにレイテンシーが低い。より一貫して、より要領を得たものだ。スリーナイン、フォーナイン、ファイブナインはまだ非常に低い。ファイブナインでは、約0.5マイクロ秒になる。これは、あるプロセスから別のプロセスへ、永続化されたキューを経由してメッセージを送信する場合だ。高いパーセンタイルでは、もっと低くなる。要件としてそのようなユースケースがあるかどうかによる。多くのJavaアプリケーションでは、99パーセンタイルが2マイクロ秒以下であれば、通常はかなり良い。要するに、これはさまざまなパーセンタイルにおけるレイテンシーの比率である。もしあなたが使っているものに100倍速い別のソリューションがあるのなら、関係するすべてのレイテンシの99%は、あなたの要件とはまったく偶然のものである可能性が高い。これは意外に高いと思われるかもしれないが、チューニングによって10%の改善、20%の改善ができるかもしれない。そう思うような立場ではないかもしれないが、実際に、別のアプローチや別のテクノロジーを使うだけで、100倍速くできるかもしれない。それが何を意味し、何になるかはユースケースによる。これは実際に起こっている。私たちはパフォーマンスコンサルティングを依頼されることが多いので、これは何度も目にすることだ。私たちは問題を解決する別の方法を見つけることができ、より迅速な解決策を得ることができる。
特に難しいクライアントがいた。1ヶ月の猶予があるから、99パーセンタイルのスピードアップをしてほしいが、ハードウェアもソフトウェアも変えてほしくない、と言われたんだ。彼らは当初、私がどれだけ改善できるかを保証するよう求めていた。私はそれにノーと言った。「いや、現時点では何も保証できない」と言ったんだ。何かを変えなければならないかもしれないが、最小限にとどめよう、と思ったんだ。その結果、本当に必要だったのは分析だけだった。遅延の穴はどこから来ているのか、分析が必要だったのは、それぞれのケースで、誰かがすでに、場合によっては3年前にソリューションを書いていたからだ。しかし、なぜ乗り換えるべきなのか、その理由を提示することができなかった。というのも、設計が優れているとか、より優れた技術を使用しているとか、そういうデータを提供することができなかったからだ。また、銀行は明らかにリスクを嫌い、今はうまくいっている。乗り換える正当な理由が示されない限りは乗り換えない。私は彼らに分析を提供できた。そして、一連の設定を変更し、すでに問題を解決している実装を切り替えることで、コードを追加することなく、3ナインを25分の1に減らすことができた。このようなことが起きたのは今回が初めてだ。通常は、コードも変更しなければならない。
分散されたユニークタイムスタンプ
「コードはユーモアのようなもの。説明しなければならない時点で、それは悪いことなんだ」とCory House氏は言った。分散されたユニークタイムスタンプ、分散されたユニークIDは、かなり一般的なパターンだ。システムのクラスタがあり、特定のイベントや注文、特定のトランザクションをアプリケーション全体でトレースできるIDを持ちたい。今でも使われている手法のひとつに、IDを配布するマイクロサービスがある。これは多くの場合、IDの塊のようなものを取得するデータベースによって支えられており、その後、それらは漁られる。問題は、これには約100マイクロ秒かかるということで、特に低遅延を求めるクライアントにとっては驚くほど長い時間だ。もっと簡単な方法は、ユニークUUIDを使うことだ。これは内蔵されている。余計な作業をする必要はない。統計的に一意である。まったく同じIDが2つできる可能性は極めて低い。事実上、ユニークなIDとして使うことができる。わずか0.3マイクロ秒しかかからない。より軽量だ。ネットワークは関係ない。中央でサービスを運営する必要もない。しかし、かなり不透明であるという問題がある。タイムスタンプがあっても、それを簡単に読み取ることはできない。おそらく、別のタイムスタンプも作らなければならなくなり、少しオーバーヘッドが増える。ちょっと無駄なことだ。
私たちがとった解決策は、下2桁にホストIDを持つタイムスタンプを持つことだ。ホストの数が100以下であり、システム間で一意であることを望む限り、ナノ秒分解能のタイムスタンプを持つことができ、すべてのシステムで一意であることができる。単調増加するように強制することで、2つのタイムスタンプが極めて近くても、人為的に「実は違う時間があるんだ」と言うことができる。そんなことはめったに起こらないので、起こらないことを保証すればいいのだ。これで64ビットの値ができ、人間が読めるものに変換するのが非常に簡単になり、ちょっとした情報が得られ、タイムスタンプを追加する必要はない。共有メモリーを使えばそれができる。Javaで共有メモリを使うことは、私たちの製品の多くで非常に中心的な役割を担っている。これはオープンソースの製品であるBytesを使っている。基本的に、これはメモリ・マップ・ファイルに格納された長い値を取得するものだ。このメモリマップファイルは、マシン上のすべてのプロセスで共有されるため、ホストIDのタイムスタンプを単調増加させることができる。そして、マシン間の一意性を保証するために、各マシンは異なるホストIDを設定しなければならない。見ての通り、コードは非常にシンプルだ。次にループがある。楽観的なパスがあり、問題がなく競合がないことを望んでいる。もし競合があれば、最終的に以前のどのIDよりも大きいIDを取得するまでループする。
Javaの共有メモリであるオフヒープメモリを使っているとはいえ、コードはそれほど複雑ではない。また、非常に軽量で、約40ナノ秒かかる。ユニークIDを使うより約8倍速い。タイムスタンプも必要ない。また、任意の8ビット値で保存できるので、長ければIDとして使用できる。オブジェクトの作成も必要ない。他にも多くの利点がある。これは、別のテクニックを使って、より効率的で、この場合ではより使いやすいソリューションを生み出した例だ。"良いハードウェアがあれば、ソフトウェアは天から降ってくる" Ken Olsen氏は、初期の64ビット・マイクロプロセッサのひとつを開発したDEC Computing社の創設者のひとりである。私は大学時代、そしてそれ以降も、そのアーキテクチャに魅了され、Alphaについてかなり研究した。私は大ファンだった。残念ながら、最終的には勝ち残ることはできなかった。それでも、私はいつも先駆的な製品だと思っていた。
信頼できる情報源
これまで話してきたこととは裏腹に、最大の難関の多くは、何を信頼できる情報源とすべきかを決定することだ。なぜなら、多くの場合、信頼できる情報源がボトルネックとなり、レイテンシーの最大の原因となるからだ。データを入れ替えるのは簡単ではない。データベースが必要か、耐久性のあるメッセージングが必要か、冗長性のあるメッセージングが必要なのか、それとも単にディスクへの永続化が必要なのか、これらは、どのようなシステムにおいても、レイテンシーの最大の原因を決定する質問である。それがあなたのユースケースにとって適切かどうかを確認することは、しばしばあなたが下すことのできる最大の決断となる。リトルの法則を参照すると、システムが必要とする並行性の量は、タスクを実行する平均時間に平均到着率を掛けたもの、つまりスループットである。平均レイテンシーとスループットを掛け合わせたものが、システムが必要とする同時実行性だ。逸話として、あるクライアントのコンサルティングをしていたとき、そのクライアントは非常に分散したマルチスレッド・アプリケーションを持っていた。管理とデプロイが非常に複雑だった。平均レイテンシーはどれくらいだろうか。彼は、ミリ秒くらいだと答えた。スループットはどれくらいだろうか。1000だ。1000×1ミリ秒だ。平均すると、一度に1つのことがシステムを通過することになる。同時並行性はそれほど必要ないかもしれない。それどころか、さらに速く走ることを妨げるオーバーヘッドを生み出しているかもしれない。彼らがその答えを気に入ったかどうかはわからない。しかし、アイデアを得るには有益なことだ。あなたのシステムで実際に達成されている並行性のレベルはどれくらいだろうか。平均レイテンシに標準的なスループットを掛ければ、実際にどれだけのスループットを得ているかがわかる。あなたのシステムでは、同時にいくつのことが進行しているだろうか。
なぜそれが重要かというと、レイテンシーが高ければ、同じスループットを達成するために必要な同時処理量が増えるからだ。レイテンシーが10倍高ければ、同じスループットを達成するためには10倍の同時実行性が必要になる。逆に言えば、レイテンシーを下げれば、同時並行性の低い、よりシンプルなシステムで同じスループットを達成できる。これが信頼できる情報源にとって特に重要なのは、多くの場合、同時並行性が高くないからである。信頼できる情報源はシングルスレッドであったり、同時実行性が非常に低い場合が多い。というのも、それは往々にしてトランザクショナルだからである。イベントの発生順序は重要である。そうなると、基本的にシングルスレッドになる。取引所はこの非常に一般的な例だ。取引所では、一度にたくさんの異なるシンボルで取引を行う。しかし実際には、1日の終わりには1つのシンボルが他のすべてのシンボルを支配するのが普通だ。たとえば、ロンドンの債券ショートでは、3ヵ月物債券ショートの1つのシンボルが全出来高の90%を占めている。他は全部合わせても10%しかない。現実には、チューニングしなければならないのはこの1つのシンボルだけで、マルチスレッドにはできない。したがって、高いスループットを達成しようとするなら、低レイテンシーにする必要がある。多くの取引所がこのような状況だ。何千ものシンボルがあるかもしれないが、実際にはどの瞬間にも、すべての取引量を支配している特定のシンボルがある。
耐久性の保証
異なるタイプの保証を見る1つの方法は、分散システムのどこから保証を得れば、保証されていると感じたり、ビジネス要件を満たしていると感じたりできるのか、ということだ。別のサーバーや冗長サーバーにコピーを置く必要があるのか、それともディスク上に置く必要があるのか、このような決定は、あなたのために行われる。特定のデータベースを使用する場合、そのデータベースはこれを行うための特定の方法を持っている。大まかに言えば、これはあなたが解決しようとしている一般的な問題だ。冗長コピーが必要なのか、それともディスク上にあるべきなのか、あるいはその両方なのか。しかし、私たちが発見したのは、適切なネットワークがある限り、セカンドマシンに冗長コピーを持つことは、ディスクに強制的にコピーするよりもはるかに高速であることが多いということだ。これもオープンソースでサポートされている。シンプルなインターフェイスがあり、どの時点でも同期コールをトリガーできる。また、キューにレコードが作成されるので、その時点で同期が呼び出されたことが読者にわかる。その時点で、その時点までのすべてが書き込まれているという確信を持つことができる。
これは、スループットにどれほどの違いがあるかを示している。一番上の青いグラフはM.2ドライブを使用している。多くの銀行ではまだM.2ドライブを使用しておらず、SSDは約5倍遅い。今後2、3年のうちに、M.2スタイルのドライブやこのパフォーマンスプロファイルを持つドライブが銀行で使用されるようになると思う。主な利点は、多くの場合、大量のIOPSが可能であること、あるいは高いスループットが可能であることだ。システムがディスクへの同期に依存している場合、最も重要なのはディスクへの書き込み時間だ。例えば、データベースの実行速度を決定できる。一般的なレイテンシーは約2ミリ秒だ。99パーセンタイルは約20ミリ秒だ。各レコードをディスクに書き込まなければならないのであれば、これくらいがちょうどいい。しかし、私たちが提唱するのは、タイミングを基準にするのではなく、プログラム的に行うことを検討する。製品によってはすでにそうなっているものもある。nメッセージごと、n秒ごと、OSが30秒後にディスクに書き込む等、調整可能である。
問題は、それがITの指標だということだ。ビジネスの指標ではない。ビジネスの観点からは、何メガバイト失うかではなく、何百万失うかなのだ。彼らが気にするのは金銭的なリスクだけだ。ディスク容量を気にすることはない。プログラムに基づき、5,000万ドルの取引があり、見積もり依頼があれば、それがディスクに同期されていることを知りたい。もし見積もり依頼がなくなったら、その見積もり依頼はそれほど重要ではないかもしれない。この例では、1秒間に10回、絶対にディスクに同期させなければならないメッセージが入ってくると仮定している。典型的なレイテンシーはこの影響をあまり受けないが、明らかに高いパーセンタイルでは影響を受ける。ディスクに同期させなければならないメッセージが時折ある場合でも、ビジネスリスクに基づいてプログラム的に行うことで、より最適なソリューションが得られる。
BDDとAI(データ・ドリブン・テストの生成)
「コードはパズルのように読むのではなく、物語のように読むべきだ」Venkat Subramaniam氏。私たちにはイベント駆動型システムがある。それが私たちが使っているモデルだ。テストするために、YAMLテストを作成し、YAMLの結果を出力する。これらのイベントやメッセージはそれぞれメソッド呼び出しに変換される。RPCのようなものだ。データファイルに表示されるものと、出力として生成されるものを、とてもシンプルに変換している。この利点のひとつは、プログラムで操作できることだ。例えば、非常に複雑な出力であっても、期待される結果に対して出力を比較すれば、それは単なる複数行のテキスト比較だ。そのユーザー設定がどのようなものかはすぐにわかる。単純なことだが、もし結果が気に入れば、これは意図的な変更なので、実際の結果をコピーして、予想された結果を含むファイルを上書きすればいい。
自動で行うこともできるし、ツールが代わりにやってくれることもある。ビヘイビア駆動開発では、テスト内容と期待される結果を一緒に記述するのが一般的だが、これではメンテナンスが非常に難しくなる。なぜなら多くのテスト、場合によっては何十万ものテストに目を通し、変更しなければならない場合、その作業は非常に退屈なものになりかねないからだ。それを手作業でやらなければならない。プログラムでそれを行う簡単な方法はない。一方、出力が独立したファイルに保存されている場合は、出力を上書きできる。そのためのシステムプロパティがある。私たちはリグレステストと呼んでいる。単なるデータなので、非常に簡単なテキストの変更によってプログラムで操作できる。フィールドが欠けていたら、フィールドが間違っていたら、メッセージの順番が狂っていたらどうなるか、それらを再調整するだけだ。そうすれば、これらの組み合わせはすべて有用かもしれないが、おそらくそうではない。フィルタリングして、新しいメッセージや、他のテストがすでに生成していないメッセージが生成されなければ、それを削除する。この例では、単純なものには、考慮すべき可能性のあるバリエーションがかなりあり、それはすべて自動的に生成される。他のものはそうではない。これらはオリジナルのメッセージを生成した唯一の例だ。
それは、うまくいかない可能性のあるすべてのことを探るのに適している。ハッピーパステストを増やすにはどうすれば良いか、というのも、ハッピーパステストを受けるということがベースになっているからだ。それを壊して、ある方法で代替する。物事がうまくいかない可能性をすべて調べるのだ。新しいハッピーパステストはどうやって作るのか、今回はChatGPT-4を使った。テンプレートからかなり良い仕事をしてくれた。私は次の例に基づいて、複数のアカウントを作成するテストを生成するように言った。別のテストスイートが生成された。すばらしいことの1つは YAML でコメントを使うため、コメントが直接出力にコピーされることだ。出力を見ているとき、それぞれのメッセージをチェックするための参照フレームを得ることができる。実際に、メッセージを反映するためにコメントを更新し、一貫性を保つことができた。あるケースでは、コメントが間違っていると思ったのだが、実は私がタイプミスをしていて、コメントが正しかったことがわかった。私が間違えたのだ。YAMLだけでなく英語でも書かれていたことで、それを拾い上げることができた。
さらに一歩踏み込んで、あと20アカウントと20の送金を作ってくれないかと言ったんだ。実際かなり退屈に聞こえるので実行するためのスクリプトを書こうと思った。私はYAMLテンプレートを使ったので、その中にアカウントを作成するためのforループのテンプレートが生成され、もうひとつは送金を行うためのforループが生成された。そんなことは考えもしなかったから、これには感心した。コードを私に渡すという提案も出てきた。その両方を使うことで、それぞれバリエーションを持った11のテストができた。自分で書いた3つの基本テストから、今では55のテストになった。比較のためにBardにもやってもらった。可能ではあったが、もっと大変だった。AIが期待した答えと違う答えを出すことがあるので、それを修正しなければならないし、プロンプトをより具体的に記述しなければならない。Bardでは一般的にその傾向が強いと思う。もっと頑張らないといけないし、たいてい最初の試技では満足できないけど、3回目か4回目の試技では満足できる結果になるんだ。今のところ、ChatGPT-4はこの種のことでは有利だと思う。
DTOをチェックするために他のありふれたテストをいくつか使用することで、ほとんど労力をかけずに非常に高いコードカバレッジを達成することができた。AIを使ってテストを生成する一般的なアプローチは、より一般的なものになるだろう。特に作成されないことが多い本当に平凡なテストをすべて維持する。より現実的な例として、これはBookBuilderであるEFX取引システムのものである。これは、さまざまな情報源からの市場データを統合したものだ。私は57のテストを特定した。これはすべてのテストではないが、これに適していたものだ。すべての組み合わせを探索したところ、約4000まで上がったが、問題は6秒ではなく約14分かかったことだ。サービスがとても軽量なので、起動と終了がとても速く、たくさんのテストを素早く実行できることにかなり満足している。私たちはそれを維持したいと思っている。他のテストでは生成されない、そのテスト独自のメッセージを探すフィルタリングを行った結果、426件まで減少した。約10分の1の削減だ。生成されるテストを削減するための非常にシンプルで効果的なテクニックだ。それでも34秒しかかからなかったのだから十分だ。
RPCのためのクリオニクルチャンネル
リモートRPC APIもある。これもオープンソースのワイヤーで、TCPでメッセージを送受信することができ、コンフィギュレーションを通して、共有メモリーの使用に切り替えることができる。実際、これは前回のベンチマークで使用したライブラリで、1秒間に40億メッセージを送る。典型的なレイテンシーは、TCPホップあたり約8マイクロ秒、共有メモリーホップあたり約2マイクロ秒だ。これは最高レベルのAPIだ。これが最悪の結果になるだろう。より低く、一貫したレイテンシーを実現するために、さまざまな最適化の選択肢がある。大きく書き換える必要はなく、チューニングが必要なものに集中すればいい。これが我々の一般的なアプローチだ。