LoginSignup
133
114

More than 5 years have passed since last update.

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

Last updated at Posted at 2017-03-04

はじめに

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構造体へのポインタを返す。また、ここは個別のハイパーバイザ非依存な、準仮想化環境におけるCPU時間stealへの対応処理である。
(追記: 2017/03/06: 記述を修正しました。 @satoru_takeuchi さん、コメントありがとうございました!)

上記の通り、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を立ち上げる…といった運用をしているとのことである。(すごい、そこまでやるのか...)

まとめ

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

  • 課題

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

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

133
114
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
133
114