プログラムの多くは、連続してやってくるリクエストを処理するという問題に対処しなくてはならない。この問題を並行処理により解決する最も単純な手法は、Thread-Per-Message デザインパターンを使用することである。このパターンでは、1つのリクエストの処理に対して1つの新規スレッドを割り当てる。[Lea 2000a] 以下のような特徴がある場合、各タスクを逐次実行するよりも、このデザインパターンを用いる方が好まれる。
- 処理に時間がかかる
- 入出力の制約を受ける
- セッション単位で処理を行う
- 相互の関連のない処理内容である
しかし、このパターンを使用すると、逐次実行では発生しないオーバーヘッドが発生する。たとえば、スレッド生成とスケジューリング、タスク管理、リソース割当てと割当て解除、頻繁なコンテキストスイッチング、といったオーバーヘッドである[Lea 2000a]。その上、攻撃者は、大量のリクエストを一斉送信し、システムをサービス運用妨害(DoS)状態にすることができる。攻撃を受けたシステムは、パフォーマンスが徐々に低下するのではなく、応答しなくなってしまう。システムの安全性という観点で考えると、断続的に発生するエラーの対処にすべてのリソースを使い果たしてしまい、結果としてその他のコンポーネントを飢餓状態に陥らせてしまうことが考えられる。
スレッドプールを使用することで、リクエストが殺到した場合にすべてのサービスを停止するのではなく、同時に処理するリクエストの最大値を無理なく処理できる数に制限することができる。スレッドプールはこれらの問題に対処するために、並行実行可能なワーカースレッドの最大数を制御する。スレッドプールを提供するオブジェクトは、RunnableあるいはCallable<T>タスクを受け取り、リソースが利用可能になるまでの間一時的なキューにタスクを保存する。スレッドプール内のスレッドは再利用可能であり、また効率的な追加と削除を行えるため、スレッドライフサイクルの管理に要するオーバーヘッドを最小限に抑えることができる。
複数のスレッドを使用してリクエストを処理するプログラムにおいては、トラフィックが突然増加した場合、規模を縮小してでもサービスを続けるべきである。DoS攻撃を受ける可能性のあるプログラムにおいて、これは必須である。
違反コード (Thread-Per-Message パターン)
以下の違反コードでは、Thread-Per-Messageデザインパターンを適用している。RequestHandlerクラスはpublic static宣言されたファクトリメソッドを提供している。呼出し元はRequestHandlerインスタンスを取得した後、handleRequest()メソッドを呼び出してリクエストを処理させる。handleRequest() メソッドでは、新たなスレッドが作られ、リクエストの処理が行われる。
class Helper { public void handle(Socket socket) { //... } } final class RequestHandler { private final Helper helper = new Helper(); private final ServerSocket server; private RequestHandler(int port) throws IOException { server = new ServerSocket(port); } public static RequestHandler newInstance() throws IOException { return new RequestHandler(0); // 次の利用可能なポートを選択する } public void handleRequest() { new Thread(new Runnable() { public void run() { try { helper.handle(server.accept()); } catch (IOException e) { // ハンドラへ処理を移す } } }).start(); } }
Thread-Per-Message パターンでは、急激なサービス低下を防ぐことはできない。一定のリソースが消費し尽くされるまで、スレッドは作成され続け、処理は通常通り続く。たとえば、リクエストに応じた数のスレッドを作成できる場合であっても、システムがオープンできるファイル記述子の数には上限があるかもしれない。あるいは、枯渇するリソースがメモリである場合、システムは突然停止し、サービス運用妨害につながるかもしれない。
適合コード (スレッドプール)
以下の適合コードでは、並行実行できるスレッド数の上限値が設定された、スレッド数固定のスレッドプールを使用している。スレッドプールに渡されたタスクは、内部のキューに保存される。こうすることで、システムがすべてのリクエストを処理しようとして過負荷状態に陥ることを回避し、一度に対応するクライアントの数を限定することにより急激なサービス低下を防いでいる[Tutorials 2008]。
// Helper クラスに変更はない final class RequestHandler { private final Helper helper = new Helper(); private final ServerSocket server; private final ExecutorService exec; private RequestHandler(int port, int poolSize) throws IOException { server = new ServerSocket(port); exec = Executors.newFixedThreadPool(poolSize); } public static RequestHandler newInstance(int poolSize) throws IOException { return new RequestHandler(0, poolSize); } public void handleRequest() { Future<?> future = exec.submit(new Runnable() { @Override public void run() { try { helper.handle(server.accept()); } catch (IOException e) { // ハンドラへの転送 } } }); } // ... スレッドプールの終了やタスクの取消し等の他のメソッドを定義する... }
Java API仕様にはExecutorインタフェースについて以下のように記述されている。[API 2006]
(Executor インタフェースは)送信された Runnable タスクを実行するオブジェクトである。このインタフェースは、タスク送信を各タスクの実行方式(スレッドの使用やスケジューリングの詳細などを含む)から分離する方法を提供する。通常、Executor は、明示的にスレッドを作成する代わりに使用される。
前述の適合コードで使用しているExecutorServiceインタフェースは、java.util.concurrent.Executorインタフェースから派生したものである。ExecutorService.submit()メソッドは、その呼出し元がFuture<V>クラスのオブジェクトを取得することを可能にする。このオブジェクトは、呼出し時点では結果が分からない非同期処理の結果をカプセル化するとともに、呼出し元がタスクの取消しのような処理を実行することを可能にする。
newFixedThreadPoolを選択することが必ずしも最適であるとは限らない。Java API仕様を参考に、実装要件を満たすスレッドプールを以下から選択すべきである。 [API 2006]
- newFixedThreadPool()
- newCachedThreadPool()
- newSingleThreadExecutor()
- newScheduledThreadPool()
リスク評価
単純な並行処理方式を使って無制限な数のリクエストを処理すると、深刻なパフォーマンスの低下、デッドロック、システムリソースの枯渇、サービス運用妨害といった結果を招く恐れがある。
ルール | 深刻度 | 可能性 | 修正コスト | 優先度 | レベル |
---|---|---|---|---|---|
TPS00-J | 低 | 中 | 高 | P2 | L3 |
関連ガイドライン
MITRE CWE | CWE-405. Asymmetric resource consumption (amplification) |
CWE-410. Insufficient resource pool |
参考文献
[API 2006] | Interface Executor |
[Lea 2000] | 4.1.3, Thread-Per-Message; 4.1.4, Worker Threads |
[Tutorials 2008] | Thread Pools |
[Goetz 2006] | Chapter 8, Applying Thread Pools |
翻訳元
これは以下のページを翻訳したものです。
TPS00-J. Use thread pools to enable graceful degradation of service during traffic bursts (revision 85)