コンテキストスイッチング:マルチタスクの隠されたコスト

📖 8min read

サーバーが遅い場合はスレッドを増やしますか?

入社初期、私が引き受けたSpring Boot APIサーバーがトラフィックが集まる度に遅くなる現象があった。原因がわからないので、私はGoogleから始めました。

“Spring Boot server slow response” “Tomcat performance tuning”

検索結果、ブログやコミュニティで最も多く見られるアドバイスは単純だった。 「トムキャット(Tomcat)のスレッドプール(Thread Pool)サイズを増やしてください。労働者が足りなくて要求が待っています。

私は「ああ、労働者が足りません!」と単純に考えた。すぐにapplication.yml設定を開き、デフォルト値200個だったスレッドを2,000個に増やしてしまった。私の計算通りなら労働者が10倍増えたので処理速度も早くなければならなかった。

しかし、デプロイ後にモニタリング画面を見た私は凍りついた。サーバーはむしろより太く動いており、CPU使用率は高騰したが、いち早く処理される要求数は減った。まるで作業者が仕事をしないで虚空にシャベルだけしているようだった。

一体なぜ労働者を増やしたのに工場はもっと遅くなったのか?その理由を掘り下げて、私はオペレーティングシステムの最も高価なコスト、「コンテキストスイッチング」に直面しました。

労働者が多いといいわけではない。それらを交代させる費用がより高いかもしれません。

復習:スレッド、覚えていますか?

この記事に初めて触れる人のために、または最後の記事(第4編:プロセスとスレッド)の内容が寛大な人のためにしばらく記憶を巻き戻してみましょう。

私たちの「デジタル物流センター」世界観で:

スプリングブートは基本的にマルチスレッドです。要求(Request)が入るたびに働き手(スレッド)を一つずつ割り当てて仕事をさせる。だから私は単に「労働者が多ければ、より多くの要求を同時に処理しますか?」と思ったのです。

しかし、私が見落としたことがありました。私たちの工場のコアワーカー、 CPUコアの数です。

デジタル物流センターの交代勤務

実際にコンピュータのコアワーカーである CPUコアは、一度に1つだけ1つのことできます。 (シングルコア基準) ところで私たちは歌も聞き、コーディングもして、カトクも同時にする。どうすれば可能ですか?

工場(OS)が作業者(CPU)に信じられないほど速い速度で「作業交代」をさせるからだ。 「0.001秒間曲を再生し、停止します。次の0.001秒間カトクを転送して停止します。」

これが「時分割」であり、この過程で作業者がツールを置き、新しいツールを拾うプロセスが「コンテキスト切り替え」です。

作業遷移の対価:服の着替え時間

私がスレッドを2,000に増やしたときに起こったことはこれです。

CPUは体がひとつなのに、2,000人のスレッド労働者が「削除処理してください!」とアウソンチン。 CPUは公平に仕事を処理するために2,000人を休むことなく交互に会います。

問題は、A労働者の仕事をしてB労働者の仕事に移るときに「準備時間」が必要であることです。

が「記録して消して読み取る時間」を「コンテキストスイッチングコスト(Overhead)」という。労働者が適しているときは、この費用は無視できます。しかし、労働者が多すぎると? CPUは一日中作業者の帳簿だけを整理しているが、本当の「実際の仕事(計算)」は一つもできない状況に陥る。これが私のサーバーが遅くなった本当の理由でした。

作業を変える時間の間、工場は停止している。

[Code Verification]スレッドが多いと無条件に高速ですか?

ペクムンが不余一見だ。コードで証明してみましょう。同じ量の加算操作を実行します。スレッド1つにするときとスレッド100万個に分割するときの速度を比較しましょう。常識的には100万個が早くなければならないようだが、現実は違う。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class ContextSwitchingTest {
    private static final int TASK_COUNT = 1_000_000;

    public static void main(String[] args) throws InterruptedException {
        // 1. シングルスレッドで処理(交代勤務なし)
        long start = System.currentTimeMillis();
        for (int i = 0; i < TASK_COUNT; i++) {
            simpleTask();
        }
        System.out.println("シングルスレッド時間: " + (System.currentTimeMillis() - start) + "ms");

        // 2. 大量のスレッドで処理(コンテキストスイッチングを誘発)
        // スレッドプールを無制限に生成(注意:コンピュータが止まる可能性)
        ExecutorService executor = Executors.newCachedThreadPool();
        start = System.currentTimeMillis();
        for (int i = 0; i < TASK_COUNT; i++) {
            executor.submit(() -> simpleTask());
        }
        executor.shutdown();
        executor.awaitTermination(1, TimeUnit.HOURS);
        System.out.println("マルチスレッド時間: " + (System.currentTimeMillis() - start) + "ms");
    }

    private static void simpleTask() {
        int a = 1 + 1; // 非常に軽い作業
    }
}

結果の例(環境によって異なります):

分析:作業自体( 1 + 1 )はとても簡単で、目を覚ます鳥で終わります。しかし、マルチスレッド方式では、100万個のスレッドを作成し、オペレーティングシステムがこれらの間を行き来したりする(スイッチング)コストが作業時間より数千倍多く入ったのだ。 船よりも腹が大きい状況です。

実務におけるレッスン:適正線を探す

それでは、Spring Boot Serverのスレッドはいくつありますか?正解は「サーバーが何をするのか」によって異なります。

しかし、私が経験した状況のように2,000個に無作為に増やすのは過油不給だ。スレッドが増えるほどメモリ(Stack)も多くつかみ、コンテキストスイッチングコストのためにCPUが過負荷になるためだ。

最近は、この問題を解決するために、 Node.js やSpringのWebFluxなどの非同期(Non-blocking)技術が注目されています。皆さんは「スレッドを増やさず、一人が休まないで早く処理しよう」という戦略を書く。

無条件の正解はない。私のサービスが「シフト」が多いのか、「疾走」が必要かによって選択する必要があります。

終了:無料のランチはありません

私たちはしばしば「同時に処理すると早い」と勘違いする。しかし、コンピュータの世界で「同時」とは、実際には目つきに近い超高速交代勤務にすぎない。

コンテキスト切り替えを理解すると、なぜサーバーのチューニングが単に「数字を増やす」のではないのかがわかります。膨らんだスレッドは、むしろサーバーの息苦しさを締める毒になる可能性があります。

さて、もうコンピュータ内部(CPU、RAM、Process)工場はかなりよく戻っているようだ。それでは、今工場のドアを開けて外に出ましょう。私たちの工場で作成したデータを他の工場(顧客)に送信するにはどうすればよいですか?

次回は、ネットワークとHTTP、その見えない道路網について話しましょう。

コメントする