2023年09月21日
非同期モニタリング
(Asynchronous Monitoring)
サーバのアプリケーションプログラム環境の変化
本格的なインターネットサービス時代にサーバ系プログラムの大半はJVM環境の代表的な言語であるJAVAと.NET環境の代表言語のC#で開発されてきました。
しかし、最近は業務環境の変化に素早く対応するニーズと継続的なサーバ性能の向上に伴い、遅いという理由で避けられてきた動的言語、例えばPythonやJavaScriptなどがサーバ系プログラムにも適用されるようになりました。
これに伴い、サービスを処理する方式にも変化があります。MSA(Micro Service Architecture)が話題になって、軽くて独立的なアプリケーションプログラムが求められるようになり、高性能サーバの処理能力を前提にマルチスレッドを活用した同期方式の処理を行うことで、できる限り少ない数のスレッドでマルチリクエストを処理する非同期方式に変わることになりました。
このような変化には面白い相違点があります。
言語環境の変化は十分理解できます。一般的に開発担当者はできるだけ分かり易い言語で作成することを望むために、既存の制約が強い静的言語ではない動的言語を採択したと考えられます。
しかし、同期方式が非同期に変わることで、人間の脳は同期方式の処理は簡単に理解できますが、非同期方式の処理の理解は難しいです。
勿論、非同期への変化は言語の変化に比較すると、それ程速くはありません。未だ、多くのアプリケーションプログラムは同期方式で作成されています。
その変化の壁となる主な要因は、非同期方式が相対的に難しいからです。
では、この二つの方式の間にはどの位の差があるでしょうか。
同期方式と非同期方式を理解するために先ず、重要な要素である「スレッド」について理解する必要があります。
スレッド
スレッドはOSがサポートする処理単位で、CPUのコアの1つが、OSが作るスレッドの1つを実行する関係です。
もし、4コアのCPUであれば、OSの4つのスレッドが同時に実行できることを意味します。ここで重要なポイントは「同時実行」が4つまで可能であることで、スレッド数が最大4つである必要はありません。
実際にOSで実行するプログラムは基本的に1個のスレッドを生成して、必要に応じてスレッドを生成します。コアより多くのスレッドが生成し、実行できる理由は、OSが周期的にCPUで実行するスレッドを選択するためです。
下記の図はA~Hまでの8個のスレッドがあると仮定した場合、CPUでどのように実行されるかを表しています。
一般的にスレッドの交代はかなり速い速度で処理されるため、コンピュータユーザはスレッドの実行が遅いと認識することは難しいです。
スレッドには幾つかのデメリットがあります。
先ず、生成と削除の負荷が大きいことです。この問題を改善するために一部のアプリケーションプログラムは、既に生成済みのスレッドのPoolを構築して、必要なときにそのPoolからスレッドを借りて利用し、必要がなくなれば返還するという方式で運用をしています。
もう1つのデメリットはCPUで実行されるスレッドをOSが制御する作業の負荷が大きい点です。
64ビット時代の今、スレッドの最大数の制限はないですが、現実的には多過ぎるスレッドで運用すると、OS側のスレッドスケジューリングの負荷が大きくなる問題があります。これが酷くなるとCPUでユーザのコードを実行する時間より、スレッド運用のためのOSコードでより多くの時間を使うようになり、性能低下を招く恐れがあります。
この問題は前に説明したスレッドプール(Thread Pool)で最多スレッド数を制限することで解決できます。
まとめると、活性スレッド数が少ないと、CPUはその分より効率的に作業を行えます。この点を理解しているのに何故、同期処理から非同期処理に変更する必要があるのかについて次で確認します。
同期処理のためのスレッドの運用
これまで、Webアプリケーションプログラムでサービスリクエストの処理は一般的に同期方式で処理していました。同期方式とは単一スレッドがサービスリクエストの最初から最後まで処理することを意味します。
例えば、電子ブックサービスのWebサイトでユーザがe-book 1冊をダウンロード のリクエストをすると、サーバでは図2のような処理をすると仮定します。
スレッドの1つが、ユーザがリクエストしたWebページの機能を順次処理して終わるまで拘束されます。
問題はここからです。上記の過程でもし「ファイルの読込み」の処理が1秒だとどうなるでしょうか?
つまり、スレッドの1つが1秒間「使用中」の状態になります。そのため、同時に1,000名のユーザがダウンロードリクエストをすると、全部で1,000個のスレッドがあれば1秒でそのリクエストを全て処理することになります。
もし、「ファイルの読込み」が1秒から10秒になると、増えるユーザのリクエストとスレッドは1:1で連動して10秒まで継続的に累積されて、秒当たり1,000名のユーザが継続的にダウンロードリクエストをした場合、10秒では10,000個のスレッドが活性化することになります。
偶に、ユーザが急増すると、止まってしまうサイトがあります。
もし、特定のWebサーバの場合、Webリクエストのためのスレッドの最大値を5,000に設定すると、前記の状況では5秒以降に接続したユーザにはサービス拒否画面が表示されます。勿論、この場合にはサーバを増設や、高性能なサーバにアップグレードする方法で追加的なリクエストを処理できますが、最多ユーザに対する需要予測が外れると、例えば、ショッピングモールのサイトでは全て売上損失になります。
2つのタイプのスレッド同期処理
今まで確認した結果、スレッドを同期方式で運用する場合、処理時間をできる限り短縮すると同時に利用されるスレッド数を減らせることが確認できました。
Webサイトではユーザのリクエストを素早く処理する必要があります。大規模ユーザが利用する特定のWebサイトがリクエストを処理するときには、短い秒数であっても急にサービストラブルが発生する可能性があります。
では、どの作業に多くの処理時間がかかっているのでしょうか?
Webサイトのリクエストであれば、大きく2つの観点で処理時間を考えることができます。
① CPUがユーザのコードを実行するときにかける時間
1. //1からnまで足し算するコード
2. long sum = 0;
3. for (long i = 1; i <= n; i++)
4. {
5. sum += i;
6. }
② 他のサーバに遠隔リクエストをして結果を待つ時間
1. // HTTPリクエストする場合
2. WebClient wc = new WebClient();
3. wc.DownloadData(“http://www.nave.com”); //応答を受信までの所要時間
①は改善の余地はそれ程ありません。
作成されたユーザのコードを実行することで消費されたCPU時間であるため、その時間を短縮するためにユーザのコードを消すことはできません。しかし、ユーザコードが非効率な構造で作成されている場合は、より早く終わるコードに改善できます。例えば、前記の例は1~nまでを足すコードを次のように改善できます。
1. // 1からnまで足し算するコード
2. long sum = (n * (n + 1)) / 2; //nまでループをせず、1回の演算で計算
②はどうでしょうか?
ネットワークを少し早い回線に変更するか、対象サーバの性能を高めて、結果を速く変換するように改善できます。しかし、単独システム内の状況ではない外部の要因は制御の範囲を超えます。
結果として、現実的な理由で改善は難しいです。
しかし、②の状況では考慮する点が1つあります。
処理時間を短縮することは簡単ではありませんが、CPUは外部サーバで応答を受けるまで休むことができます。
比較のため①の状況を確認します。CPUは足し算のコードを全て実行するまで演算作業を継続します。しかし、②は図3で確認できるように、外部にリクエスト後に応答を受けるまでスレッドが止まるためCPUが行う作業がありません。
OSは「スレッド待機」の間、CPUが休まないように図4のように普通は他のスレッドの作業を実行するようにします。
「スレッド A」がネットワークからデータを受信するまでの待機中に、OSはCPUリソースを「スレッド B」に割当て、「作業 B」を実行します。次に、「スレッド A」はLANカードからネットワーク受信の完了信号を受けると、次の作業を実行します。
もし、「スレッド B」を利用することなく、「スレッド A」が「待機時間」中に待機モードではなく、「作業 B」を実行できるようにできればどうでしょうか?それができれば、これまで2つのスレッドでの作業が1つのスレッドだけで運用できます。
このような考え方を実装したのが非同期処理です。
スレッドの効率を向上させる非同期処理
非同期処理の重要な根幹は、コンピュータの入出力装置がデータの送受信の完了毎にアラートを送ることができることです。図4ではネットワークアダプターを例としましたが、コンピュータの代表的な入出力装置であるディスクも同様にこのようなアラート動作をサポートします。
ユーザコードのReadFile APIリクエストはディスクの読込み作業を行い、スレッド上の処理を中止します。するとCPUは他のスレッドの作業が可能で、その後にディスク装置が「読込み作業」の完了信号をCPUへ送ると、処理を中止していたスレッドで次のコードを実行できます。
この過程を非同期処理で改善します。
そのためには図6と同じくスレッドが実行するコードをI/O装置の利用による基準で作業を分けて、データの送受信毎に発生するスレッドの待機時間中に他の作業を選択して実行できるようにコーディングする必要があります。
この原則を基に図2で説明した電子ブックのダウンロードを処理するWebページのコードを次の2つの作業に分離できます。
- 作業 A:ユーザ認証、リクエストした本の購買確認、ブックファイルの読込み
- 作業 B:ファイルの転送
「作業 A」の場合、「ブックファイルの読込み」のためのディスクI/O時間は10秒ですが、その前のコードの実行は1ミリ秒(ms)と仮定します。
では、スレッドを担当したCPUは「作業 A」を実行してファイルの読込みを行い、待機している間には他のリクエストを1ms * 10,0000個まで処理できます。つまり、1つのスレッドでユーザのリクエストを10,0000個まで処理することになります。
もし、ファイル転送に該当する「作業 B」が同じく1msだと仮定して、もう1つのスレッドを割り当てて運用すると、理想的な環境で2つのスレッドだけで10,000個のリクエストを処理できる能力を持つことになります。
最近は、単一コンピュータでコンテナ環境を活用してマルチサービスを運用する環境に変わりつつありますが、少数のスレッドでマルチリクエストを処理できるこのような非同期処理はWebサービスの必須要件になりつつあります。
非同期処理の問題点
以上のような非同期処理を利用しない理由はありません。しかし、今まで一般的なWebサービスで、非同期処理でコードが作成されたでしょうか?
非同期処理の最大の問題点は、同期処理では単一で順次に処理されたコードが、非同期処理では作業単位に分かれて順序に従わず実行されることです。これをユーザ2名が電子ブックダウンロードをリクエストした場合に例えて説明します。まず、同期処理では次のように2つのスレッド(T1と T2)で処理できます。
- 作業 A:ユーザ認証、リクエストした本の購買確認、ブックファイルの読込み
- 作業 B:ファイルの転送
しかし、これを非同期に処理すると、次のように単一スレッド T1で複雑に順序に従わずに実行されます。
① スレッド T1 => 作業 Aを実行 (ユーザ1からのリクエスト)
② スレッド T1 => 作業 Aを実行 (ユーザ2からのリクエスト)
③ (…約 10秒後…)
④ スレッド T1 => 作業 Bを実行 (ユーザ 1の ReadFile API リクエストで発生した読込み完了時点)
⑤ スレッド T1 => 作業 Bを実行 (ユーザ 2の ReadFile API リクエストで発生した読込み完了時点)
上記の処理では非同期アラートを1つだけ受ける場合です。実際の業務では多数のI/O リクエストがあるので、順序に従わずに実行されるプロセスは複雑になるため、開発担当者が理解し難い構造になります。
最近のプログラミング言語は非同期処理のための特別文法を提供していて、複雑さを抽象化して開発担当者が簡単にコーディングできるようにサポートします。しかし、抽象化で内部動作が見えなくなるため、それを理解できない開発担当者が作成したコードにバグが発生する副作用も発生します。
非同期処理を導入すると、問題になるもう一つの問題はトラブルシューティングが難しい点です。
例えば、電子ブックのダウンロードのReadFileで10秒かかるサービスのトラブルを仮定します。トラブルが発生した時点でそのプロセスのダンプを採取すれば、事後分析をできますが、既存の同期処理では、このときシステムには約10,000個のスレッドが生成されています。
多数のスレッドでリクエストのスタックの最上段にReadFileがあります。そのため、該当APIリクエストが遅くなりトラブルが発生したことを比較的に簡単に推測することができます。
しかし、非同期処理ではどうでしょうか?
ReadFileに対する非同期リクエスト自体は素早く実行されるため、スレッドのコールスタックに残る瞬間をキャッチしてダンプファイルに残せる確率は高くありません。
また、2つ程度のスレッドで1つは「ユーザの認証、 リクエストしたブックの購買確認、ブックファイルの読込み」に該当するコードのどこかを指し、もう一つは「ファイル転送」コードの部分を指します。すると、該当のサービスがReadFileにより遅くなったのか断定できる根拠が無いため、システムの総合的な他の性能数値、この場合はディスク I/O 性能のデータまで同時に確認できれば問題の原因を推測できます。
現場で作られるWebサービスのコードに非同期 I/Oが1つだけ含まれる場合はほとんどないことを考えると、どのI/Oで問題が発生したのか原因を特定することは難しいです。
性能モニタリング製品での非同期処理
以上が同期処理と非同期処理の方式の差です。
非同期処理のデメリットは無視できないですがメリットもあるため、最近開発するWebサービスはプログラミング言語と開発フレームワークがサポートする機能を使って、非同期方式で実装する傾向があります。このような変化はそのサービスをモニタリングする製品にも影響を与えています。