衝撃的なほどシンプル: Momento JavaScriptキャッシュクライアントのチューニング

node.js gRPCクライアントを最適化しました。

この投稿は、Momentoが開発者の体験をどのように考えているかを詳しく説明するシリーズの一部です。Momentoのサーバーレスキャッシュを使い始めるのは驚くほど簡単ですが、それは物語の半分に過ぎません。

私たちは、楽しいユーザー・エクスペリエンスには、キャッシュとやりとりするコードを書いて実行することも含まれると信じており、私たちのクライアント・ライブラリを衝撃的にシンプルにするには、意図的な努力が必要です。別の言い方をすれば、私たちのキャッシュ・クライアントは第一級の関心事なのです。

まだこのシリーズの最初の投稿、Shockingly simpleをご覧になっていない方は、こちらをお読みください。私たちのクライアントサイドのユーザー・エクスペリエンス哲学の詳細をご紹介しています。

Momento JavaScript クライアント

このページでは、Momento JavaScript クライアント・ライブラリをチューニングし、優れたユーザー・エクスペリエンスを提供するための舞台裏を紹介します。これには、パフォーマンス(レイテンシとスループット)、回復力、デバッグ性などが含まれます。多くのデータベースやキャッシュのクライアントライブラリは、このような重要なチューニングを読者のための練習として残しています。私たちのクライアント・ライブラリの開発・保守のアプローチはそれとは異なります(そして率直に言って、より優れています)。しかし、”ソーセージがどのように作られるのか “についても少し紹介します。

JavaScriptやTypeScriptでMomentoを使い始めることに興味があるだけなら、朗報です!この記事の続きを読む必要はありません。JavaScriptのサンプルを見て、数行のコードでMomentoキャッシュとどのようにやり取りできるかを確認してください。

旅よりも目的地に興味がある方は、記事末尾のtl;drセクションで調査結果の要約をご覧ください。

冒険好きな方は、この先をお読みください!

gRPC

MomentoはgRPCで構築されています。私たちは、あなたがこのことを知ったり気にしたりする必要がないことを望み、私たちのクライアントライブラリがあなたにそれを要求しないようにするために多くの努力を払っています。しかし、この記事の目的では、チューニングや実験のほとんどが gRPC の設定に関係するため、言及する価値があります。

gRPCをご存じない方のために、ハイライトをいくつかご紹介します:

グーグルが開発したリモート・プロシージャ・コールのフレームワークです。
・HTTP/2プロトコルをベースにしており、HTTP/1.1を拡張し、リクエストを多重化してパフォーマンスを向上させるなどの機能を備えています。
・Googleのプロトコルバッファを使用してバイナリワイヤフォーマットを定義し、JSONのような冗長なフォーマットと比較してネットワークI/Oを削減します。
・HTTP/1.1とJSONを使ってRESTサービスを構築する代替手段として考えることができます。
・これはCloud Native Computing Foundationのインキュベーション・プロジェクトです。

Node.js and CPU utilization

Momentoは、一般的なプログラミング言語のクライアント・ライブラリを提供しています。JavaScript(node.js)クライアントのパフォーマンスを調査する場合、C#、Java、Go、Rustなどの他のクライアント言語との重要な違いがあります。node.jsプロセスのデフォルトの実行環境では、1つのCPUコアでしかユーザーコードを実行できません。

Node.jsは、追加のCPUを利用するために選択できるメカニズム(複数プロセスのスポーンやプロセス間通信の使用など)を提供しますが、それらは少なくとも数ステップの道から外れており、ユーザーが自分の環境にうまく適合しないかもしれない特定の実行ストラテジーに適応することを強いることなく、汎用ライブラリやSDKに統合することは容易ではありません。そのため、Momentoのクライアント・コードを1つのCPUで実行し、1つのプロセスからできる限りのパフォーマンスを引き出すことに注力しました。

この練習で興味深いのは、CPUがパフォーマンスのボトルネックになるのは、他の言語よりもずっと早い可能性が高いことだ。

Momento JavaScriptクライアントのチューニング

それでは、JavaScriptクライアントをチューニングするために行った最近の作業から、いくつかの気づきを得ていきましょう!

Hello Laptop!

ほとんどの開発者にとって、Momentoクライアント・ライブラリを最初に使うのはラップトップからでしょう。そのため、サンプルコードとすぐに使える設定が、あなたの開発環境でうまく機能するようにしたいのです。

これにはいくつかの意味があります。まず第一に、ネットワークレイテンシーがほとんどのプロダクション環境よりも少し高くなる可能性があるということです。

次に、”Hello World!”の例をクリアしたら、次に何をしたいかは分かっています。キャッシュのgetとsetを行う非同期関数コールを大量に生成するループを書き、その速度とスループットを確認します。

さて、私たちは皆、ループの中でできるだけ速く非同期リクエストの束をスパミングすることが、本番コードでやりたいことではないことを知っている。何か、何か、競合、何か、バックプレッシャー、BLAH BLAH BLAH。しかし、それはまだ私たちが最初に書きたいコードです。ほとんど毎回だ…そうですよね?)

そこで、パフォーマンスを見るために、いろいろ試してみました。設定可能な同時リクエスト数をループで処理するコードを書き、10から10,000までのさまざまな値でテストを行いました。パフォーマンスはまあまあでしたが、もっと良くなることはわかっていました。

1つのCPUコアで何千もの同時リクエストの危険性

先に述べたように、1コアしか使えない言語で計算量の多いことをやっていると、CPUに縛られやすくなります。Momento SDKは、暗号化とシリアライズを少し行うだけで、それ以外はほとんどネットワークI/Oで、計算コストのかかることはしていません。

それでも、もし5,000の同時ループがスパミング・リクエストを発射したら、node.jsのイベント・ループに多くの仕事を積み重ねることになります。キューに入れられたタスクのほとんどは飢餓状態になり、サーバーからのレスポンスが実際にあった後、コールバックが呼ばれるのはかなり先になるかもしれません。そのため、クライアント側のレイテンシは、サーバーが報告しているレイテンシに比べて非常に大きくなっているように見えます。

物事を改善するために採用できる戦略がいくつかあります:
1.JavaScriptコードの最適化を探して、JavaScriptコードの作業を減らす。
2.ノードのランタイムが基礎となるI/Oレイヤーに委譲できる作業量を最大化し、JavaScriptコードにより多くのCPUを使えるようにする。
3.希少なリソース(CPU)のために多くのタスク・コールバックが競合することがないように、一度にキューに入れる作業量をもっと制限する。

項目1については、Momentoやクライアント・ライブラリ特有のものではないので、細かい説明は省略します。スループットを向上させるために、gRPCレイヤでできる微調整があるかどうかを確認します。スループットを最大化し、遅延を最小化するための同時リクエスト数のスイートスポットを見つけることができるかどうかを確認します。

I/Oの最大化: 複数のgRPCチャンネル

gRPC サーバー (および HTTP/2 サーバー全般) は、接続ごとの同時ストリーム数の上限を設定します。この設定の一般的なデフォルト値は100であり、Momentoのキャッシュサービスが使用しているのもこの値です。

この知識があれば、JSコードで5,000のリクエストが同時に実行されたときに何が起こるかを推論することができます。Momentoクライアントは、サーバーにリクエストを送信するために単一のgRPCチャネルを使用していたので、これは事実上、任意の時点でそのチャネルのスポットを待っているリクエストの4,900がブロックされることを保証していました。

チャンネル数を変えても、レイテンシーもスループットも(誤差を考慮すると)改善されませんでした。ありがたいことに、この実験の直後に、問題はgrpc.use_local_subchannel_poolの設定にあることが判明しました。ドキュメントによると、これが設定されている場合、gRPCは「チャネル内のローカル・サブチャネル・プールを使用」しあす。そうでない場合はグローバル・サブチャネル・プールを使用します。

この設定は、nodejsのgrpc-jsライブラリにとって重要なものであることが判明した。この設定をオンにすると、はるかに良い結果が得られました。

ここで、1チャンネルから5チャンネルに移行した場合、p50のレイテンシは43分の1に減少し、スループットは2.6分の1に増加したことがわかります。これらは明らかに大きな改善です。

我々のテスト環境では、チャンネル数を5以上に増やすと、収穫は減少しました。ノードプロセスは、このような多数の同時リクエストに対して依然としてCPUに負荷がかかっているため、I/Oレベルでリクエスト容量を追加しても、それ以上の改善は得られませんでした。

しかし、ここで注目すべき重要な点は、サーバー側のレイテンシがテスト期間中一貫して3ms以下であったことです。明らかに、ラップトップとサーバー間のクライアント側のレイテンシを測定する際に、若干のネットワークレイテンシが加わりますが、クライアント側とサーバー側の測定で観察されるレイテンシのなんと20倍のデルタ(クライアント側60ms、サーバー側3ms)を説明するものではありません。同時リクエスト数が多いため、まだCPUに負荷がかかっています。

同時リクエスト数の最適化

次に、どの程度の同時リクエスト数が最高のパフォーマンスをもたらすかを実験することに移りました。今のところ、これはユーザーが目標とすべき同時リクエストの最大量に関するガイダンスを提供する手段を与えてくれます。クライアント・ライブラリの今後のリリースでは、ユーザーが理想的な値を超えた場合に、同時リクエストの上限を提供するための作業キューを設置する予定です。

以下は、異なる同時実行レベルでのパフォーマンスを示すグラフです:

アプリを100に制限した場合と比較して、200の同時リクエストを許可した場合、p50と特にp99.9のレイテンシに明らかなスパイクがあることに注意してください。全体的なスループットは少し高くなりますが、p99.9のレイテンシを39msから292msに増加させるという犠牲を払えば、アプリケーションにとってそれが正しい選択であることはほとんどありません。これらのチャートから、この環境での最大同時リクエスト数のスイートスポットは約100であることがわかります。その結果、p50とp99.9のレイテンシは、10、20、50リクエストで測定したレイテンシにほぼ近くなり、一方でスループットはほぼ毎秒5,000リクエストに増加しました。

他にもいくつかの設定を試してみたが、パフォーマンスに最も顕著な影響を与えたのはこれらの設定だったため、詳しい説明は割愛します。さて、ラップトップランドでの話はここまでにして、本番に近い環境で最新の設定を試してみることにしましょう!

ラップトップからクラウドへ

サービスと同じリージョンとアベイラビリティゾーンで動作するAWS EC2インスタンスからパフォーマンスがどのように見えるか見てみよう。これにより、ネットワークレイテンシの大部分が除去されるため、クライアント側の数値はサーバー側のレイテンシ数値にかなり近くなるはずです(c6i.4xlargeインスタンスタイプを使用しているのは、このクラスでは小さいインスタンスよりも安定したネットワークパフォーマンスが観測されているためです)。

チューニングの目標

ラップトップ環境での実験が終わったので、本番環境でのチューニングの目標をより具体的にする必要があります。クライアント・アプリケーションのパフォーマンスをチューニングする場合、多くの場合、レイテンシの最小化とスループットの最大化の間でトレードオフを行う必要があります。あるコンフィギュレーションは、より高いスループットを提供するかもしれません。しかし、クライアントのリソース競合が多くなると、クライアント側のレイテンシが高くなるかもしれません。別のコンフィギュレーションでは、リソースの競合を確実に回避し、可能な限り低いレイテンシを達成することを好むかもしれません。

このチューニング演習では、2つの異なるレイテンシー目標を選び、それぞれのスループットを最大化する方法を探ります:

クライアント側のp99.9のレイテンシは20ms:これは、計算コストが非常に高いデータをキャッシュするアプリケーション(複雑なリレーショナルデータベーススキーマの大きなJOINクエリなど)や、アプリケーション自身のレイテンシ要件が甘い場合の妥当な目標値です。

クライアント側のp99.9のレイテンシは5ms:これは、独自のレイテンシ要件が絶対的に重要なアプリケーションにとっては、より良い目標です。

同時リクエスト数の再検討

妥当なレイテンシーとスループットが得られる値の範囲については、大体の見当がついています。ラップトップ環境では、5つのgRPCチャンネルが最良の値でした。

以下は、5つのgRPCチャネルと異なる同時リクエスト数で実行した結果です:

これらのチャートから最も顕著に観察されるのは、200の同時リクエストと500の同時リクエストを比較した場合、p99.9のレイテンシに大きな影響があるということです。p50のレイテンシも劇的に増加し、スループットも悪化します。ですから、この構成を除外して、残りの構成にズームインすることができます:

これらのチャートから、50の同時リクエストで、p99.9のレイテンシ20ms以下という最初の目標を達成できたことがわかります。1つのnode.jsプロセスで1秒あたり8300リクエストのスループットを達成することができました。

第二の目標であるp99.9のレイテンシを5ms未満にするためには、同時リクエスト数を2に減らす必要がある。こうすることでスループットも1900に減少する(4倍の減少)が、これはレイテンシに敏感なアプリケーションにとっては正しいトレードオフである(複数のnode.jsプロセスやクライアントノードにスケールアウトすることで、全体的なキャパシティはまだ改善できることを覚えておいてほしい)。

gRPCチャンネル数の再検討

gRPCチャンネル数を変え、最大同時リクエスト数を50にした場合の様子を確認してみましょう:

このデータから、低レイテンシの本番環境では、チャンネル数を増やしても意味のあるレイテンシの改善は得られないことがわかります。レイテンシは十分に低く、I/OボトルネックよりもシングルコアCPUのボトルネックに早くぶつかるようです。一般的に言って、少なくとも2つのチャンネルをオープンにしておくことは、そのうちの1つのTCP接続に何らかの問題が発生した場合にフォールバックができるようにするための良いアイデアです。

tl;dr

さて、私たちは今日ここで何を学んだのでしょうか? 以下がその要点です:

・node.js/grpc.jsで複数のgRPCチャネルを使用するメリットを得るには、grpc.use_local_subchannel_pool設定を必ず設定する必要があります。

・5つのgRPCチャネルを使用することで、レイテンシが高く、同時リクエスト数が多い環境で大きな違いが得られます(テストシナリオではレイテンシが40倍改善されました!)。

・同時リクエスト数の上限を設定することは、レイテンシーとスループットの両方に大きな影響を与えます。
・最大50の同時リクエストで、単一のnode.jsプロセスから、1秒あたり8300以上のスループットで、20ミリ秒未満のp99.9クライアント側レイテンシを達成することができます。

・最大2つの同時リクエストで、1つのnode.jsプロセスから毎秒1900リクエストのスループットで、5ミリ秒未満のp99.9クライアント側レイテンシを達成することができます。

・低レイテンシ環境(EC2インリージョンなど)では、複数のgRPCチャネルを使用してもパフォーマンスに測定可能な影響はありません。

これらの発見により、開発環境とプロダクツ環境用のビルド済み設定を定義するための強固な基礎ができました。近日公開予定のリリースでは、PreBuiltConfigs.Laptop、PreBuiltConfigs.InRegion.PrioritizeThroughput、PreBuildConfigs.InRegion.PrioritizeLatencyのようなビルド済みの設定を選択できるようになります。もちろん、適切と思われる設定を手動で調整することもできますが、その必要がないように、90%のケースをカバーすることが私たちの目標です!

Next up: Python

このシリーズの次の投稿では、Pythonクライアントのチューニング作業を取り上げます。node.jsとPythonには、この演習に関連するいくつかの類似点がありますが(特に、デフォルトの実行環境がシングルCPUコアをターゲットにしているという事実)、両者の理想的なgRPCコンフィギュレーション設定の違いを見て驚くかもしれません。

あるいは…Momentoを使えば、このようなことを心配する必要はありません。その場合は、入門ガイドをチェックして、ほんの数分で簡単にキャッシュを作成できることを確認してください!

次回まで…シンプルでいてください、友よ!