はじめに
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(¶virt_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のあれこれ
-
コンテナの場合にどうなるのかは(まだ)調べていない。ただ、コンテナはOSの論理分割なので、物理環境上のコンテナであれば %steal は0になるような気がする。間違っていたら指摘してほしい。 ↩