horry's camp

ちょっと煽り気味のタイトルですが、CPU がマルチコアになり 2個、4個と増えていく中 Linux の負荷の指針になるロードアベレージをどう読むべきか、という話です。気になったところを少し調べたのでそのまとめを。

http://d.hatena.ne.jp/naoya/20070222/1172116665 でも書いたとおり、Linuxロードアベレージは「ロードアベレージは過去1分、5分、15分の間の実行待ちプロセス数の平均数 = 実行したくても他のプロセスが実行中で実行できないプロセスが平均で何個ぐらい存在してるか」を示す値です。ボトルネックが CPU、メモリ、ディスク等々どこにあるかは関係なく、仕事の実行までにどれぐらい待たされているかを示す値なので、システムのスループットを計測する指標の入り口になる値です。

このロードアベレージですが、実装を見るとランキュー(待ち行列)に溜まったタスク (task_struct構造体 ≒ プロセス)の数を、ハードウェアクロックの割り込み毎に数え上げて時間単位での平均値を出しているのがわかります。

ところでこのランキュー、Linux カーネル 2.6 の実装では CPU ごとに用意されています。CPU が一個のときは一つのランキューに溜まったタスクを数え上げるだけでよいところ、CPU が2個になるとロードアベレージの値はどうなるのでしょう。2コアが主流な昨今です。また、これから先 4 コア、8 コアと増えていった場合ロードアベレージが示す値はこれまでどおりの読みでよいのか、違う観点で見る必要があるのか、気になりますね。ということで実装を深追いします。カーネル 2.6.20 のコードから抜粋します。

まずは先のエントリーでも抜粋した kernel/timer.c。

static inline void calc_load(unsigned long ticks)
{
        unsigned long active_tasks; /* fixed-point */
        static int count = LOAD_FREQ;

        count -= ticks;
        if (unlikely(count < 0)) {
                active_tasks = count_active_tasks();
                do {
                        CALC_LOAD(avenrun[0], EXP_1, active_tasks);
                        CALC_LOAD(avenrun[1], EXP_5, active_tasks);
                        CALC_LOAD(avenrun[2], EXP_15, active_tasks);
                        count += LOAD_FREQ;
                } while (count < 0);
        }
}

static unsigned long count_active_tasks(void)
{
        return nr_active() * FIXED_1;
}

ここの calc_load() → count_active_tasks() がタイマー割り込み毎に呼び出されて CALC_LOAD マクロでロードアベレージが計算されているのでした。アクティブなタスクの数を求める nr_active() の実装は kernel/sched.c にあります。

unsigned long nr_active(void)
{
        unsigned long i, running = 0, uninterruptible = 0;

        for_each_online_cpu(i) {
                running += cpu_rq(i)->nr_running;
                uninterruptible += cpu_rq(i)->nr_uninterruptible;
        }

        if (unlikely((long)uninterruptible < 0))
                uninterruptible = 0;

        return running + uninterruptible;
}

この nr_active() の実装がポイントで、foreach_online_cpu で全CPUを舐めて、そのループ中で各 CPU に紐づいたキューの TASK_RUNNING と TASK_UNINTERUPPTIBLE なタスクを足しこんでいます。CPU が複数あるとランキューその同数になりますが、CPU ごとにアクティブタスク数を求めるのではなく、すべて合計しているのがここで分かります。

少し戻って、sched.c で使われているロードアベレージの計算マクロである CALC_LOAD を sched.h から。

extern unsigned long avenrun[];         /* Load averages */

#define FSHIFT          11              /* nr of bits of precision */
#define FIXED_1         (1<<FSHIFT)     /* 1.0 as fixed-point */
#define LOAD_FREQ       (5*HZ)          /* 5 sec intervals */
#define EXP_1           1884            /* 1/exp(5sec/1min) as fixed-point */
#define EXP_5           2014            /* 1/exp(5sec/5min) */
#define EXP_15          2037            /* 1/exp(5sec/15min) */

#define CALC_LOAD(load,exp,n) \
        load *= exp; \
        load += n*(FIXED_1-exp); \
        load >>= FSHIFT;

ここでは特に CPU の数に依存した演算は行っていません。

計算したロードアベレージの値は、カーネル内グローバルな配列である avenrun に保存されます。この avenrun に保存された値は

% cat /proc/loadavg
0.01 0.02 0.00 2/69 4279

と /proc ファイルシステムの /proc/loadavg から取得できます。top や sar、uptime などはここから値を読み取っています。実際、

% strace -e open uptime 2>&1 | grep loadavg
open("/proc/loadavg", O_RDONLY)         = 4

と trace を取るとはっきり分かります。この /proc/loadavg に値を出力している箇所を見てみます。/proc はファイルシステムなので実装コードは fs/proc 以下にあります。fs/proc/proc_misc.c の loadavg_read_proc() が該当箇所ですね。

static int loadavg_read_proc(char *page, char **start, off_t off,
                                 int count, int *eof, void *data)
{
        int a, b, c;
        int len;

        a = avenrun[0] + (FIXED_1/200);
        b = avenrun[1] + (FIXED_1/200);
        c = avenrun[2] + (FIXED_1/200);
        len = sprintf(page,"%d.%02d %d.%02d %d.%02d %ld/%d %d\n",
                LOAD_INT(a), LOAD_FRAC(a),
                LOAD_INT(b), LOAD_FRAC(b),
                LOAD_INT(c), LOAD_FRAC(c),
                nr_running(), nr_threads, current->nsproxy->pid_ns->last_pid);
        return proc_calc_metrics(page, start, off, count, eof, len);
}

と avenrun[] から 1分、5分、15分の値をそれぞれ取得して整形し出力しているのがわかります。と、ここまででロードアベレージのデータを取得して整形出力する一連の流れが分かりました。注目するのは、待ちタスク数を数え上げるのにすべてのキューを見て合計する一方、その後出力するまでに CPU の数でその値を割ったりはしていない、という点です。つまり、CPU の数が増えてもロードアベレージは CPU の数でタスク数を平均したりはせず、純粋に「待ち行列にたまって待たされたタスクの数」を示しているのがここではっきりします。

さて、一方CPU 使用率の方はどうでしょう。これはカーネルの実装を見る必要もなく top や sar の値は割り算をしているのがコマンド結果から分かります。例えば sar では sar -P ALL とすると、CPU が複数ある場合にそれぞれ別に表示することができます。以下はデュアルコアCPU x 2 = CPU 4 つのサーバーでの sar -P ALL です。

% sar -P ALL | head
Linux 2.6.19.2-4.hatena.centos44smp (kiratachi.hatena.ne.jp)    05/18/07

00:00:01          CPU     %user     %nice   %system   %iowait     %idle
00:10:01          all     22.70      0.00      3.29      0.01     74.00
00:10:01            0     49.14      0.00      2.85      0.01     48.00
00:10:01            1      7.04      0.00      1.11      0.01     91.84
00:10:01            2     16.63      0.00      6.31      0.01     77.05
00:10:01            3     17.97      0.00      2.91      0.00     79.12

all が示す CPU 使用率は 0 ~ 3 番の CPU 使用率を足して割ったものであるのが見て取れます。sar や top は特にオプションなどを指定しない場合、CPU 数の数に関係ないシステム全体のCPU 使用率を示しますが、実際には各 CPU ごとに使用率(実際にはプロセスがそのCPUを使った時間)を取って保持しているのがここから分かります。(これらデータはカーネル内部で CPU 毎に用意された cpu_usage_stat 構造体のインスタンスに保持されています。)

なお、sar や top は /proc/stat から CPU 使用時間の情報を取得しています。

% cat /proc/stat | head -5
cpu  319538758 24136 11757832 1603696315 131407 489623 4303234 0
cpu0 190160191 2820 6287055 284134618 81551 489611 3829506 0
cpu1 21211171 5318 1591735 462090674 14624 4 71798 0
cpu2 40233011 6341 1806675 442794419 18733 4 126131 0
cpu3 67934384 9654 2072365 414676603 16498 4 275798 0

これらの数字から計算して %user %system 等各項目の数値を出している、という仕組みになっています。(proc_misc.c の show_stat() で整形して出力しています。)

ここで簡単にまとめておきます。

というのが結論ですね。

  1. wtakuopetapetaからリブログしました
  2. katakoriladi0sからリブログしました
  3. katakoriladi0sからリブログしました
  4. katakoriladi0sからリブログしました
  5. kazzxzpetapetaからリブログしました
  6. wizardbluepetapetaからリブログしました
  7. fukutumipodstyleからリブログしました
  8. takamasapetapetaからリブログしました
  9. ryusoulpetapetaからリブログしました
  10. yoichi13petapetaからリブログしました
  11. nakaryoseapomeranianからリブログしました
  12. ladi0spetapetaからリブログしました
  13. ipodstylepetapetaからリブログしました
  14. horrypetapetaからリブログしました
  15. higyskkurinoからリブログしました
  16. nashi-kyopetapetaからリブログしました
  17. seapomeranianatm09tdからリブログしました
  18. atm09tdpetapetaからリブログしました
  19. kurinopetapetaからリブログしました
  20. poochinpetapetaからリブログしました
  21. petapetaの投稿です