
※このブログは2022年9月25日に公開された英語ブログ「Using Streaming, Pipelining,and Parallelization to Build High Throughput Apps (Part I)」の拙訳です。
著者 : Cong Liu (PingCAP TiDB Cloudエンジニア) 、Phoebe He (デベロッパーリレーションズマネージャー) 編集者:Fendy Feng、Tom Dewan、Rick Golba
Microsoft社のCEOであるSatya Nadella氏は、かつて「すべての企業は今やソフトウェア企業である」と言いました。システムアーキテクトとして、私はこの言葉に大賛成です。特に、パンデミック以降、より多くの企業やユーザーがインターネットを利用するようになり、そのすべての活動が最終的にサーバー、アプリケーション、システムのワークロードを増やしつつあります。
スケーラビリティ (拡張性) が課題になります。サーバーに十分なリソースがなく、アプリケーションの負荷の増加に対応できないとき、あなたは通常どうしますか?RAMを買い足し、CPUコアを追加し、ディスクを追加するのでしょうか?このような解決策は、垂直スケールまたはスケールアップとして知られています。しかし、現在のリソースを最大限に活用しても、システムがワークロードを処理しきれない場合はどうでしょうか。一般的には、マシンを追加してシステムをさらにスケールアウトさせるという方法があります。
ここでは、より良い選択肢として、ハイブリッド・アプローチを紹介します。アプリケーションを設計する際、ストリーミング、パイプライン、並列化などを使って、1つのノードでリソースを最大限に活用することができます。その後、ハードウェアリソースを追加して、システムをスケールアウトまたはスケールアップすることができます。
ハードウェアリソースを追加することなくスループットを向上させる
システムの性能を表すには、入力と出力の両方を考慮する必要があります。
パフォーマンス=ワークロード/リソース消費量
一つはOPS (Operations per Second) 、もう一つはスループットに注目したシナリオです。この2つは、システム性能を測定する最も重要な方法です。OPS型は、一定の制約や最適化により、スループット型と同等にすることができます。今回は、より基本的なスループット型についてお話します。
例えば、システムの主な目標はより高いQueries per Second (QPS) かもしれませんが、スループットはQPSと同等である可能性があります。これは多くのオンライントランザクション処理 (OLTP) システムとAPIベースのサービスに当てはまり、各トランザクションの平均データ負荷はほぼ同じになります。オンライン分析処理 (OLAP) や巨大なデータ負荷を扱うように設計されたその他のシステムでは、より高いスループットが焦点となります。また、高いQPSと高いスループットの両方をサポートするHTAP (Hybrid Transactional and Analytical Processing) データベースのように、混合負荷を必要とするシステムも増えている傾向が見られます。スループットを使って作業負荷を測定することは、そうしたシステムにも適用できます。
スケジューリングの改善によるシステムリソースの完全消費
リソースの有効活用を理解するために、料理をテーマにしたビデオゲームOvercookedを見てみましょう。このゲームのゴールは、お客さんに料理を提供することです。お客さんのオーダーを受け、調理し、料理を提供します。調理では、食材を切ったり、混ぜたり、鍋で煮込んだりする必要があります。この工程をもう少し詳しく見て、どうすればもっと早く提供できるかを見てみましょう。
例えば、食材を切るのに2秒、調理に4秒かかるとしましょう。一度に1つのオーダーを受けると、6秒かかります。3人のお客さんが同時に来店した場合、1人目のお客さんは6秒、2人目は12秒、3人目は18秒待つことになります。お客さんを長くお待たせすればするほど、お客さんは我慢の限界を超え、帰ってしまう可能性が高くなります。お金を払ってくれるお客さんが少なければ、ビジネスが成り立たなくなります。
ではどうすればスピードアップし、勝負に勝てるのでしょうか。複数のオーダーを同時に処理したいと考えます。オーダー1が調理台の上にある間に、オーダー2の食材を切ります。オーダー2を調理台に乗せたら、オーダー3を切り始めます。この新しいスケジューリング方法では、お客さん1はまだ6秒待ちますが、お客さん2とお客さん3はそれぞれ4秒少なく、それぞれ8秒と14秒待ちます。

複数のオーダーを同時に処理する
調理台の段階で最も時間がかかるため、1つのオーダーを完了させるまでの時間を決定します。この部分が「ボトルネックリソース」です。上の図から、オーダー2とオーダー3は食材を切った後、調理段階に移るまでに2秒間待たなければならないことがわかります。調理台を常時使用し、ダウンタイムがなければ、最大数のオーダーを処理することができます。
コンピュータのスケジューリングもこれに似ています。実行されるコードはスループットであり、料理ゲームのオーダーのようなものです。システムリソースは、まな板と調理台のようなものです。理想的には、下図のように、実行時間中にすべてのリソースがフルに活用されている状態です。

すべてのリソースをフルに活用するのが理想的
調理台をグレードアップすれば実現できます。調理台で料理を作るのに、食材を切る時間と同じ2秒しかかからなくなるのです。この場合、みじん切りサービスにはアイドルタイムがありません。

調理時間を短縮するために、調理台をグレードアップする
しかし、このような効率性は滅多にありません。より現実的な目標は、アプリケーションの実行中に、ボトルネックとなるリソース (CPU) が100%使用されることです。他のリソースが100%使われなくても、それはそれでOKです。

ボトルネックとなるリソースをフル活用する
一方、設計が不十分なアプリケーションは、ボトルネックとなるリソースを常に100%使用するわけではありません。むしろ、まったく使わないかもしれません。

悪いアプリはボトルネックとなるリソースをフル活用しない
つまり、システムの性能は、固定されたシステムリソースで達成できるスループットによって評価されます。優れたアプリケーション設計では、ボトルネックとなるリソースを十分に活用することで、固定されたシステムリソースでより高いスループットを実現することができます。
次に、ストリーミング、パイプライン化、並列化がどのようにリソース活用の目標達成に役立つかを見ていきます。
リソースの最大活用を実現するために
ストリーミングで大きなタスクを分解する
ストリーミングは、文脈によって意味が異なります。例えば、イベントストリーミングは、川の流れのように次々と発生するイベントの集合を指します。この記事の文脈では、ストリーミングは、大きなタスクを小さなチャンクに分割して、順番に処理することを意味します。例えば、ウェブブラウザからファイルをダウンロードする場合、ファイルを小さなデータブロックに分割し、1つのデータブロックをダウンロードし、ローカルディスクシステムに書き込み、ファイル全体がダウンロードされるまでこのプロセスを繰り返します。
ストリーミングにはいくつかの利点があります。ストリーミング以前は、1つの大きなタスクがある期間にすべてのリソースを使用し、他の小さなタスクがリソースを使用するのをブロックすることがありました。自動車運転免許センターで行列に並ぶようなものです。私は事前に記入したフォームで運転免許証を更新するだけで、5分もかからなかったにもかかわらず、私の前にいた人が申請を終えるまで20分も待たされたのです。
ストリーミングは、ファイルをダウンロードしているときでも、自動車運転免許センターの列に並んでいるときでも、リソースのピーク使用量を削減することができます。さらに、ストリーミングは失敗したタスクのコストも削減できます。例えば、ダウンロードが途中で失敗しても、最初からやり直す必要はありません。失敗したところから復旧し、残りのデータブロックでダウンロードを続行します。
したがって、1つの大きなタスクを小さなチャンクに分解することで、スケジューリングが可能になり、リソースの利用を最適化する複数の方法への扉が開かれるのです。これは、次のステップであるパイプライン化の基礎となるものです。
パイプラインによる処理時間の短縮
あるアプリケーションがストリーミング設計なしで動作しており、2つのステップを踏んでいると仮定します。ステップ1はエンコード処理で、CPUリソースを消費し、100秒かかります。ステップ2は、ディスクへの書込みで、I/O帯域を消費し、70秒かかります。全体として、このアプリケーションの実行には100秒+70秒=170秒かかります。
ストリーミングでは、この1つのタスクを10個のアトミックなスライスに分割します。各スライスは同じ2つのステップを踏みますが、かかる時間は1/10になります。全体のタスクはまだ同じ時間がかかります: (10s + 7s) * 10=170s。しかしこのアプローチでは、ディスクに巨大なデータのチャンクを同時に保存しないので、メモリ使用量を減らすことができます。また、この方法は障害コストも削減できます。 (ここでは、トランザクションブレークダウンのオーバーヘッドを考慮していません。これについては実装のセクションで説明します。)

ストリーミングあり/なしのタスクの消費時間
この図にパイプラインを追加すると、スライス1がCPUを使い終わったら、スライス2はすぐにCPUを使い始められます。スライス2はスライス1のI/O消費終了を待つ必要もないのです。スライス2がCPUを使い終わると、スライス3がCPUを使い始められます。パイプライン化により、タスクの処理時間は63秒短縮され、元の時間の3分の1以上も短縮されたのです!

ストリーミングとパイプライン化により、消費時間が短縮できる
パイプライン化を利用するメリットは明らかです。時間の折りたたみは処理時間を大幅に短縮します。アプリケーションは同じ時間内に、より多くの処理を行い、より高いスループットを生み出すことができます。
予想される処理時間は、元のプロセスで最も時間のかかるステップとほぼ同程度に短縮されるはずです。上記の例では、ストリーミングとパイプライン化を使用しない場合、エンコードのステップに100秒かかり、それらの技術を使用した新しいフローでは107秒と、ほぼ同じレベルになっています。
また、ボトルネックリソースであるCPUの利用率を最大化しています。CPUは、I/Oステップが終了するのを待ってから次のスライスの処理を開始する必要がないのです。アイドルタイムは無駄な時間です。そのため、以下に示すように、パフォーマンス目標に一歩近づいたことになります。

ボトルネックとなるリソースをフル活用するのが理想的
下記の2つの図は、リソースの消費量がどのように変化したかを示しています。「CPUとI/Oが交互」から「CPUとI/Oの消費が重なる」になっています。

パイプライン化は、連続的に実行されるアプリケーションにも適用されます。時間の折りたたみを使えば、1つのリソースの消費曲線を平らにすることができます。そこから並列化戦略を適用して、さらに効率を上げることができます。
並列化でリソース消費を最大化する
ストリーミングやパイプライン化はタスクを小さなアクションに分解し、1つのリソースの消費曲線は100%には達しないものの、フラットに近い状態になります。そこで、並列化によってボトルネックとなるリソースをフル活用し、例えばCPU稼働率100%といった高いパフォーマンスを実現することができるのです。
料理の例に戻ると、ストリーミングは、料理を作ることを、肉を焼く、麺を茹でる、キノコを焼く、と分解していくようなものです。パイプライン化は、料理の段階を、肉を切り、肉を調理し、肉を調理している間に、キノコを切ることに分解するようなものです。で、並列化って何なんでしょう?その通りです。異なるプレイヤーに協力してもらい、複数のオーダーを同時に調理することを意味します。スケジューリングがうまくいけば、ボトルネックとなっている調理台というリソースを、異なるプレーヤーが協力してノンストップで稼働させることができるのです。
日常生活において、私たちのコンピュータは常に並列化を使用しています。例えば、ウェブページの読み込みは並列化を利用しています。アドレスを入力した直後、ブラウザはHTMLページを読み込みます。同時に、ブラウザはHTMLページ上のすべての画像を読み込むために複数のスレッドを起動します。
並列化は、すでにリソースの消費曲線がフラットになっているスレッドに適用するとより効果的です。1つのステップしか含まず、デフォルトでリソースをスムーズに消費しているシンプルなプロセスにも、ストリーミングやパイプラインを使用する複雑なプロセスにも適用できます。(このシリーズの次の記事では、異なる並列化戦略の実装方法について説明します)
「やるべきこと」についてはたくさん話してきました。しかし、「やってはいけないこと」を忘れがちです。リソースをスムーズに消費しないプロセスや、消費されるリソースをコントロールできないプロセスには、並列化を適用すべきではありません。このような場合、オペレーティングシステムにリソースの割り当てを決定する権限を与えてしまうことになります。

これで、ストリーミング、パイプライン化、並列化の基本が理解できたと思います。本シリーズの後編では、これらの方法をどのように実装するかについて説明します。また、お客様のニーズに合わせて、どのような手法の組み合わせが可能なのか、その戦略についても考えていきます。
アプリ開発について詳しく知りたい場合は、ぜひご意見をお聞かせください。TwitterやSlackチャンネルからお気軽にお問い合わせください。また、データ集約型アプリケーションのスーパーチャージに関するソリューションについては、当社のウェブサイトをご確認ください。
こちらも併せてお読みください:
ストリーミング、パイプライン、並列化を使って高スループットのアプリケーションを構築する (後編)
Spring BootとTiDBによるWebアプリケーションの構築
RetoolとTiDB Cloudを使って、30分でリアルタイムカンバンを作る
TiDB Cloud Dedicated
TiDB Cloudのエンタープライズ版。
専用VPC上に構築された専有DBaaSでAWSとGoogle Cloudで利用可能。
TiDB Cloud Serverless
TiDB Cloudのライト版。
TiDBの機能をフルマネージド環境で使用でき無料かつお客様の裁量で利用開始。