1. Qiita
  2. 投稿
  3. Linux

LinuxのCPU使用率の%stealについて

  • 9
    いいね
  • 0
    コメント

はじめに

Linux で採取できるCPU使用量(率)の情報として、%user や %sys 等に加えて %steal という量がある。これが追加されたのは、仮想化が広く使われはじめた10年くらい前だろうか。筆者は Xen を調べていて気づいたのだが、もっと前にs390のために追加されたのかもしれない。当時、ESXの場合も含めて調べていたのだが、最近、KVMの場合にどういう実装になっているのか、ふと気になって軽く調べてみたのでメモ。

CPU使用率の計算

まず最初に、sar や vmstat や mpstat 等、さまざまなツールでCPU使用率を取得することができるわけだが、どのような情報を元に、どのような計算を行って算出しているのか?

まず、kernel内ではboot以後の各種実行モードのCPU時間を分類して積算値として保持している。user モード、特権モード、割り込み処理に使った時間...等である。

この積算値のカウンタを使用して、たとえば、%user であれば、ある測定期間について(簡略化して書けば)

%user = user モードのCPU実行時間 / すべてのモードのCPU実行時間の和

という計算を行う。
上で、「簡略化して書けば」と書いた。SMPシステムの場合には、CPUごとにカウンタが存在するため、システム全体のCPU使用率を出すのであればCPU数分だけ分母、分子ともにたしあげる。また、100%で正規化するのか、正規化せずにnCPUであればn00%とするのかは表示プログラムの実装しだいである。もちろん、特定のCPUの使用率を算出することもできる。

もう一点大事な点を強調しておくと、この計算はuser landで行われる。前述したようなプログラムは、基本的に /proc/stat からCPUごとのカウンタを読み出して計算を行っているのである。

%steal とは何か?

さて、それでは %steal とは何を意味するのか?

ハイパーバイザ型の仮想環境の場合1、ゲストOSがプログラムを実行していたと思っているにもかかわらず、ホスト上で他のVMとのCPUの取り合い(競合)が発生し、実際にはハイパーバイザが物理CPUの時間を与えていないので、実世界では実行されていなかった…というようなことがおこる。

この、なんだか時間泥棒に盗まれたような(笑)時間を積算しているのが %steal に対応しているカウンタなのである。つまり %steal とは、時間泥棒さんに盗まれた時間の割合だと言える。

言い換えれば、仮想環境において、同じハイパーバイザの上で動いている他のVMとCPU時間を取り合って競合が発生している場合にこの数値が0より大きくなる。

ところで、%steal を処理しなかったらどうなるのか?
もちろん、CPUを使った時間が実際の値とはずれてくるため、上記の計算によって算出されるCPU使用率が狂うという影響がある。

Linux の CPU時間積算カウンタ

上述のように、Linuxの場合はCPUごとに各種実行モードのCPU時間のカウンタを持っている。以下では、手元にあった Ubuntu Xenial の linux-4.4.0-53-74 のソースツリーから引用する。

まず、関連する enum や構造体の定義/型宣言は include/kernel/kernel_stat.h の中にある。

enum cpu_usage_stat を見てわかるように、10個のモードに分類されていることがわかる。(しかし、CPUTIME_GUESTなんて追加されていたのね...)

 14 /*
 15  * 'kernel_stat.h' contains the definitions needed for doing
 16  * some kernel statistics (CPU usage, context switches ...),
 17  * used by rstatd/perfmeter
 18  */
 19
 20 enum cpu_usage_stat {
 21         CPUTIME_USER,
 22         CPUTIME_NICE,
 23         CPUTIME_SYSTEM,
 24         CPUTIME_SOFTIRQ,
 25         CPUTIME_IRQ,
 26         CPUTIME_IDLE,
 27         CPUTIME_IOWAIT,
 28         CPUTIME_STEAL,
 29         CPUTIME_GUEST,
 30         CPUTIME_GUEST_NICE,
 31         NR_STATS,
 32 };
 33
 34 struct kernel_cpustat {
 35         u64 cpustat[NR_STATS];
 36 };
 37
 38 struct kernel_stat {
 39         unsigned long irqs_sum;
 40         unsigned int softirqs[NR_SOFTIRQS];
 41 };
 42
 43 DECLARE_PER_CPU(struct kernel_stat, kstat);
 44 DECLARE_PER_CPU(struct kernel_cpustat, kernel_cpustat);

43行目と44行目がCPUごとに定義するためのC言語のマクロで、上記はヘッダなので宣言だけであって実体はここにはない。余談だが、このあたりは @satoru_takeuchi さんが最近書いて話題になった「linuxカーネルで学ぶC言語のマクロ」の良いサンプルではないかと思う。 :)

kernel_stat や kernel_cpustat の実体が定義されているのは kernel/sched/core.c である。こんな感じ。

2833 DEFINE_PER_CPU(struct kernel_stat, kstat);
2834 DEFINE_PER_CPU(struct kernel_cpustat, kernel_cpustat);

また別のマクロが出てきたが、深入りしないことにする。

KVMの場合の %steal の処理

さて、KVMの場合にはどう加算処理が行われるのか?
調べてみたところ、少なくとも Ubuntu Xenial の linux-4.4 系kernelでは、(CONFIG_PARAVIRT が define されてる場合)準仮想化用のハイパーバイザ呼び出しを使っているようだ。自分に割り当てられなかったCPU時間をハイパーバイザから取得し、%stealに対応するCPU時間のカウンタに足し込んでいる。

直接の足し込み処理をしているのは、kernel/sched/cputime.c にあるこのルーチンである。

231 /*
232  * Account for involuntary wait time.
233  * @cputime: the cpu time spent in involuntary wait
234  */
235 void account_steal_time(cputime_t cputime)
236 {
237         u64 *cpustat = kcpustat_this_cpu->cpustat;
238
239         cpustat[CPUTIME_STEAL] += (__force u64) cputime;
240 }

kcpustat_this_cpu もマクロである。自分を実行中のCPUのkernel_cpustat構造体へのポインタを返す。また、ファイルのパス名を見てわかるとおり、実はこれはハイパーバイザ依存コードではない。KVMはLinuxの一部だからである(たぶん...)。

上記の通り、account_steal_time() は、渡された引数を前述のカウンタに足し込んでいるだけで、%steal 相当時間をハイパーバイザから取得してきてaccount_steal_time()を呼ぶ処理はkernel/sched/cputime.cにある。

257 static __always_inline bool steal_account_process_tick(void)
258 {
259 #ifdef CONFIG_PARAVIRT
260         if (static_key_false(&paravirt_steal_enabled)) {
261                 u64 steal;
262                 unsigned long steal_jiffies;
263
264                 steal = paravirt_steal_clock(smp_processor_id());
265                 steal -= this_rq()->prev_steal_time;
266
267                 /*
268                  * steal is in nsecs but our caller is expecting steal
269                  * time in jiffies. Lets cast the result to jiffies
270                  * granularity and account the rest on the next rounds.
271                  */
272                 steal_jiffies = nsecs_to_jiffies(steal);
273                 this_rq()->prev_steal_time += jiffies_to_nsecs(steal_jiffies    );
274
275                 account_steal_time(jiffies_to_cputime(steal_jiffies));
276                 return steal_jiffies;
277         }
278 #endif
279         return false;
280 }
281

264行目でparavirt_steal_clock()を使って取得し、適宜加工した後で 275行目でaccount_steal_time()が呼ばれているのが見てとれると思う。

なお、paravirt_steal_clock() は inline関数で、実体は arch/x86/include/asm/paravirt.h にある。

196 static inline u64 paravirt_steal_clock(int cpu)
197 {
198         return PVOP_CALL1(u64, pv_time_ops.steal_clock, cpu);
199 }
200

これがハイパーバイザ呼び出し(のマクロ)である。(が、ここでは深入りはしない)

さて、これらの処理はどんな契機で実行されているのか?
上記のsteal_account_steal_tick() は、基本的にはタイマ割り込みの処理の一環で呼び出される。直接呼び出す部分は以下の account_process_tick() で、たどっていくと、update_process_times()、 そしてtick の処理へさかのぼる。

459 /*
460  * Account a single tick of cpu time.
461  * @p: the process that the cpu time gets accounted to
462  * @user_tick: indicates if the tick is a user or a system tick
463  */
464 void account_process_tick(struct task_struct *p, int user_tick)
465 {
466         cputime_t one_jiffy_scaled = cputime_to_scaled(cputime_one_jiffy);
467         struct rq *rq = this_rq();
468
469         if (vtime_accounting_enabled())
470                 return;
471
472         if (sched_clock_irqtime) {
473                 irqtime_account_process_tick(p, user_tick, rq, 1);
474                 return;
475         }
476
477         if (steal_account_process_tick())
478                 return;
479
480         if (user_tick)
481                 account_user_time(p, cputime_one_jiffy, one_jiffy_scaled);
482         else if ((p != rq->idle) || (irq_count() != HARDIRQ_OFFSET))
483                 account_system_time(p, HARDIRQ_OFFSET, cputime_one_jiffy,
484                                     one_jiffy_scaled);
485         else
486                 account_idle_time(cputime_one_jiffy);
487 }
488

ところで、CPUの実行モードが変わるのはtimer割り込みだけではない。user land のプログラムがsystem callを呼ぶなど、各種の契機がある。これらの処理でCPU時間のaccountingがどうなっているのかは、時間の都合で調べ切れなかったので、課題としておきたい。

傾向と対策

さて、%steal が得られたとして何に使うのか?
利用方法としては、典型的には、仮想環境におけるVCPU pinning等のチューニングを行う指標にすることができる。
典型的にはこんな感じである。

  • 特定のVCPUを特定物理CPUに固定してしまう (1:1で固定)
  • VCPUを、物理CPUのグループにmappingする(virsh の cpusetを使う) (n:mで固定)

なお、この際NUMA topologyも意識すべきである。
つまり、メモリやI/O deviceとCPUの距離も意識しないと片手落ちになるので注意が必要である。(...が、詳細は本稿のスコープを超えるので、また別の機会としたい。)

ところで、以下の記事によれば、AWSの著名なユーザとしても知られているNetflixでは、%steal を監視して閾値を超えた場合、他のVMと競合が多いので効率が悪いと判断して、当該VMをshutdownし、他のVMを立ち上げる…といった運用をしているとのことである。(すごい、そこまでやるのか...)

http://blog.scoutapp.com/articles/2013/07/25/understanding-cpu-steal-time-when-should-you-be-worried

まとめ

  • 各種ツール(sar, vmstat, mpstat等)で得られるCPU使用率のうち、%steal は仮想化環境におけるCPU資源の競合具合(特に競合に負けて盗まれた(=stealされた)分)をあらわしている。
  • この数値に基づいて、各種のチューニングを行うことができる。

  • 課題

    • timer 処理以外のCPU時間のaccountingの調査
    • %steal に応じたtuningのあれこれ

  1. コンテナの場合にどうなるのかは(まだ)調べていない。ただ、コンテナはOSの論理分割なので、物理環境上のコンテナであれば %steal は0になるような気がする。間違っていたら指摘してほしい。