CPU Steal Time 入門

CPU Steal Time に関しては、多くの誤解が広がっているように思えます。例えば、同一ホスト上で稼働する他の VM が、本来自分に割り当てられるべきCPUリソースを盗んだ量を示すメトリクスだとか、CPUリソースの取り合いが発生していることを示すメトリクスだとか、など。設定などによっては、仮想環境側が CPU リソースを過剰に割り当ててしまう傾向にあるのは事実なのですが、CPU Steal Time を見るだけでは端的に "盗まれた" などという結論にいたることはできません。

CPU Steal Time とは、仮想マシンが割り当てられる CPU リソース以上のパフォーマンスを発揮しようとしている分を 計上したメトリクスであると言えます。本来であれば、Steal Timeという名前ではなくて、kernel/sched/cputime.cにのコメントにあるように、involuntary wait timeとでも呼ぶべきメトリクスです。

/proc/stat

まず、top(1)vmstat(8)に確認できる steal time のメトリクス%stがどこからやってくるのか確認しましょう。実は、このメトリクスは /proc/stat から情報を取得して計算しているにすぎません。以下の部分でしょうか。

fs/proc/stat.clinux/stat.c at master · torvalds/linux · GitHub:


		steal		+= cpustat[CPUTIME_STEAL];

どうもcpustat[CPUTIME_STEAL]の値をもらってきているだけのようです。 では、cpustat[CPUTIME_STEAL]がどこで計上されているのでしょうか? 確認してみると下記のソースに行き当たりました

VM の CPU time

kernel/sched/cputime.cで計算しています。抜粋します。

linux/cputime.c at master · torvalds/linux · GitHub:


/*
 * Account for involuntary wait time.
 * @cputime: the CPU time spent in involuntary wait
 */

void account_steal_time(u64 cputime)
{
	u64 *cpustat = kcpustat_this_cpu->cpustat;
	cpustat[CPUTIME_STEAL] += cputime;
}
...
/*
 * When a guest is interrupted for a longer amount of time, missed clock
 * ticks are not redelivered later. Due to that, this function may on
 * occasion account more time than the calling functions think elapsed.
 */
static __always_inline u64 steal_account_process_time(u64 maxtime)
{
#ifdef CONFIG_PARAVIRT
	if (static_key_false(&paravirt_steal_enabled)) {
		u64 steal;

		steal = paravirt_steal_clock(smp_processor_id());
		steal -= this_rq()->prev_steal_time;
		steal = min(steal, maxtime);
		account_steal_time(steal);
		this_rq()->prev_steal_time += steal;

		return steal;
	}
#endif
	return 0;
}

account_steal_time()は計測された steal time を収めているだけなのがわかります。実際の値の取得はsteal_account_process_time()の中で呼ばれているparavirt_steal_clock()が行っているようですね。

なお、Xenでも KVM でも guest VM として稼働する場合には、通常は HVM / PV に関係なく、CONFIG_PARAVIRT=y になり、#ifdef CONFIG_PARAVIRT中には入るので心配しなくても大丈夫です。HVMといえど多くの場合 paravirtual な機能を使用しないことはないので、これはむしろ自然だと思えます。ちなみに、CONFIG_PARAVIRT=y は、正確には paravirtualization code を有効にするだけのオプションであり、仮想化有無には関係ありません。Kconfig の help 内容を以下に抜粋しておきます。
linux/Kconfig at master · torvalds/linux · GitHub:


config PARAVIRT
	bool "Enable paravirtualization code"
	---help---
	  This changes the kernel so it can modify itself when it is run
	  under a hypervisor, potentially improving performance significantly
	  over full virtualization.  However, when run without a hypervisor
	  the kernel is theoretically slower and slightly larger.

さて、話がそれてしまいましたが、steal time を実際に計上するのは paravirt_steal_clock()でしたね。これを追ってみると、 linux/paravirt.h at master · torvalds/linux · GitHub にてpv_ops.time.steal_clock として抽象化されていることが分かります。以下その部分です:


static inline u64 paravirt_steal_clock(int cpu)
{
	return PVOP_CALL1(u64, time.steal_clock, cpu);
}

マクロがあるので、何かと思って混乱してしまうかもしれませんが、このマクロを展開してみればtime.steal_clockからpv_ops.time.steal_clockが得られます。つまり、大元の正体は、pv_ops.time.steal_clockであると分かります。

それでは、pv_ops.time.steal_clock として何が登録されているのか、それぞれ Xen と KVM の場合を見ておきましょう。

Xen の場合

Xenの場合はdrivers/xen/time.cに定義されています。
linux/time.c at master · torvalds/linux · GitHub:


	pv_ops.time.steal_clock = xen_steal_clock;

実態は以下です:


u64 xen_steal_clock(int cpu)
{
	struct vcpu_runstate_info state;

	xen_get_runstate_snapshot_cpu(&state, cpu);
	return state.time[RUNSTATE_runnable] + state.time[RUNSTATE_offline];
}

どうやら Xen の場合は、VCPU のステートから steal 時間を計測するようです。 なお、xen_get_runstate_snapshot_cpu()はその時の VCPU の状態のスナップショットを取得します。この関数も同じソースファイル内に定義されているので気になる場合はそちらを見てください。RUNSTATE_runnableRUNSTATE_offlineがそれぞれどういう意味かについては、include/xen/interface/vcpu.hに記載があります。

linux/vcpu.h at master · torvalds/linux · GitHub:


/* VCPU is currently running on a physical CPU. */
#define RUNSTATE_running  0

/* VCPU is runnable, but not currently scheduled on any physical CPU. */
#define RUNSTATE_runnable 1

/* VCPU is blocked (a.k.a. idle). It is therefore not runnable. */
#define RUNSTATE_blocked  2

/*
 * VCPU is not runnable, but it is not blocked.
 * This is a 'catch all' state for things like hotplug and pauses by the
 * system administrator (or for critical sections in the hypervisor).
 * RUNSTATE_blocked dominates this state (it is the preferred state).
 */
#define RUNSTATE_offline  3

上記みていただければ分かりますが、RUNSTATE_runnableRUNSTATE_offlineは他に取られたリソースの影響を反映したものではありません。 もし仮想マシン上で動作中のアプリケーションに必要なリソースが割り当てられているなら、VCPUはrunnable状態にはなりません。runningblocked、もしくはofflineとなるでしょう。 Xenでは余っているリソースを借りることはできますが、他のVMからリソースを奪うことはできないのです。

もし仮想マシンの VCPU が長時間runnable状態、かつtop/vmstatのSteal Timeが高いような場合は、その仮想マシンが使える以上の CPU リソースを要求しようとしていることを表しています。そうではなくて、突然 Steal TIme が跳ね上がったりしたら、何かがおかしくなってしまった可能性はあり得るかもしれません。

KVM の場合

KVMの場合は、steal の値を直接 MSR から読むことになっています。 arch/x86/kernel/kvm.cにその定義があります。

linux/kvm.c at master · torvalds/linux · GitHub:


static void __init kvm_guest_init(void) 
{
	int i;
	paravirt_ops_setup();
...
	if (kvm_para_has_feature(KVM_FEATURE_STEAL_TIME)) {
		has_steal_clock = 1;
		pv_ops.time.steal_clock = kvm_steal_clock;
	}

linux/kvm.c at master · torvalds/linux · GitHub:


static u64 kvm_steal_clock(int cpu)
{
    u64 steal;
    struct kvm_steal_time *src;
	int version;

	src = &per_cpu(steal_time, cpu);
	do {
		version = src->version;
		virt_rmb();
		steal = src->steal;
		virt_rmb();
	} while ((version & 1) || (version != src->version));

	return steal;
}

これは MSR (Model Specific Register)を読み込んでるだけです。幸いにもドキュメントに上記の定義についての記載がありました。

https://www.kernel.org/doc/Documentation/virtual/kvm/msr.txt

MSR_KVM_STEAL_TIME: 0x4b564d03
...
steal: the amount of time in which this vCPU did not run, in
nanoseconds. Time during which the vcpu is idle, will not be
reported as steal time.

idleを除いて VCPU が実行されなかった時間と書いてありますね。Xen の場合と似ています。 実際にどんな値が提供されているかについては下記の実装を読むとわかります。

linux/x86.c at master · torvalds/linux · GitHub:


static void record_steal_time(struct kvm_vcpu *vcpu)
{
    ...
    unsafe_get_user(steal, &st->steal, out);
	steal += current->sched_info.run_delay -
		vcpu->arch.st.last_steal;
	vcpu->arch.st.last_steal = current->sched_info.run_delay;
	unsafe_put_user(steal, &st->steal, out);

	version += 1;
	unsafe_put_user(version, &st->version, out);
    ...
}

run_delay の差分が steal になっていく様子です。run_delay は下記で、ランキューでの待ち時間のようですね。

linux/sched.h at master · torvalds/linux · GitHub:


struct sched_info {
...
	/* Time spent waiting on a runqueue: */
	unsigned long long		run_delay;

	/* Timestamps: */

	/* When did we last run on a CPU? */
	unsigned long long		last_arrival;

	/* When were we last queued to run? */
	unsigned long long		last_queued;

};

まとめ

以上で見てきたように Xen でも KVM でも steal time はハイパーバイザによって提供されており、一般的に、CPU steal time がカウントされる状況は、基盤となるホストの問題を示すものではありません。 steal time は、ゲストに変わって操作を行うハイパーバイザーで費やされた時間もカバーしているし、基本的には、実行する VM に与えられた許容値以上を超えて要求している分の時間を明らかにしているのです。

ゲストとホストのメトリックを注意深く確認し、適切な VM を選択する必要があります。 CPU steal time は適切な仮想マシンを選択するための一種の指標なのです。タスクを実行するために必要なCPU消費量を調べるとき、物理CPUの実行を待機する時間も差し引いて考えることができることが重要です。CPU steal time はそのために役に立つでしょう。

CPU steal time が上昇がつづくようなら、VM のスペックがワークロードにあっていないのかもしれません。

小ネタ

実はクラウドサービス提供者にもよりますが、環境によっては CPU steal time を正しく提供しているかしていないかに差があるようです。GCE では steal time は報告されていなかったようです。AWS で VP/Distinguished Engineer をされている Matthew S. Wilsonさん Twitter 経由で、前に google で GCE の Product Director をされていた Paul R. Nash さん Twitter が教えてくれました(現在は MS Azure にいるようですが)。

In both cases, steal time is reported from the hypervisor to the guest. In some cloud environments, this is accurately reported. In other cloud environments, it is not. In particular, see this response from @paulrnash ... https://googlecloudplatform.uservoice.com/forums/302595-compute-engine/suggestions/20590696-steal-time-in-linux-instances -- Matthew S. Wilson (@_msw_)

Hah, that's an oldie. As you can see, it was an extremely infrequent request (doesn't look like that has changed). -- Paul R. Nash (@paulrnash)

GCE では、KVM ベースのハイパーバイザーを利用していると聞いたことがあるので、MSR_KVM_STEAL_TIMEを有効にしていないのでしょうか。詳細は分かりません。