ぱと隊長日誌

ブログ運用もエンジニアとしての生き方も模索中

Hyper-V のルートスケジューラでは複数 CPU 動作時に大きく性能劣化することがあるのかも

概要

Windows 10 の Hyper-V 上で稼働する仮想マシンにて、PostgreSQLベンチマークを測定していました。すると、1 CPU より 2 CPU で動作する時に大きく性能劣化してしまいました。これはハイパーバイザースケジューラである「ルートスケジューラ」に原因があるのでは…?と推測しています。検証結果をまとめます。

検証環境

ホスト

プロセッサ Intel Core i5-6600 CPU @ 3.30GHz
OS Windows 10 Pro バージョン 21H2

Hyper-V

プロセッサ 4個の仮想プロセッサ
OS Red Hat Enterprise Linux release 8.6

PostgreSQL

PostgreSQL 14.5 をソースコードからインストールしました。

事象

PostgreSQLベンチマークツールである pgbench で、"SELECT 1;" というシンプルなクエリを実行して TPS を測定しました。すると、PostgreSQL と pgbench を同一 CPU で動作させるか、異なる CPU で動作させるかによって TPS に顕著な違いが現れました。

-- select1.pgbench ファイルの中身
SELECT 1;
-- 同一 CPU で動作
$ taskset -c 0 pg_ctl start -D /usr/local/pgsql/data/
$ taskset -c 0 pgbench -f /home/postgres/select1.pgbench -c 1 -j 1 -n -T 180 testdb

-- 異なる CPU で動作
$ taskset -c 0 pg_ctl start -D /usr/local/pgsql/data/
$ taskset -c 1 pgbench -f /home/postgres/select1.pgbench -c 1 -j 1 -n -T 180 testdb
動作 CPU TPS
同一 6,507,144
異なる 1,787,641

PostgreSQL と pgbench を同一 CPU で動作させた方が高い TPS となりました。直感的には異なる CPU で動作させた方がパフォーマンスは良くなりそうですが、これに反する結果となりました。

調査と結論

パフォーマンスモニターで Hyper-V の CPU 使用率を調べることができます。
パフォーマンス・モニタでHyper-Vサーバの実際のCPU使用率を調査する − @IT

また、CPU 使用率以外にも様々なメトリクスを取得できます。今回は "Hyper-V Hypervisor Logical Processor" カテゴリの以下のカウンタに注目しました。

  • % Total Run Time
  • % Guest Run Time
  • % Hypervisor Run Time
  • Context Switches/sec

論理プロセッサ別・1秒間隔で測定を行いました。以下にグラフを示します。

DBベンチマーク同一CPU動作時の Total Run Time

DBベンチマーク異なるCPU動作時の Total Run Time

DBベンチマーク同一CPU動作時の Guest Run Time

DBベンチマーク異なるCPU動作時の Guest Run Time

DBベンチマーク同一CPU動作時の Hypervisor Run Time

DBベンチマーク異なるCPU動作時の Hypervisor Run Time

DBベンチマーク同一CPU動作時の Context Switches/sec

DBベンチマーク異なるCPU動作時の Context Switches/sec

同一 CPU で動作した時は論理プロセッサに対してまんべんなく分散されているようにみえます。Hypervisor Run Time は低く、コンテキストスイッチも少ないです。

一方、異なる CPU で動作させたときは論理プロセッサの負荷に偏りが見られます。負荷が集中した論理プロセッサの Hypervisor Run Time は高く、コンテキストスイッチも多くなっています。

論理プロセッサの負荷の偏りが異なる CPU で動作させたときに必ず起きる事象なのかを確認するため、CPU ベンチマークを実行して同様の測定を行いました。

taskset -c 0 stress-ng -c 1 -l 100
taskset -c 1 stress-ng -c 1 -l 100

測定結果のグラフを示します。

CPUベンチマーク異なるCPU動作時の Total Run Time

CPUベンチマーク異なるCPU動作時の Guest Run Time

CPUベンチマーク異なるCPU動作時の Hypervisor Run Time

CPUベンチマーク異なるCPU動作時の Context Switches/sec

これらのグラフからは以下のことが言えます。

  • 負荷は論理プロセッサに対してまんべんなく分散されている
  • Hypervisor Run Time は低い
  • コンテキストスイッチも少ない

ここまでの結果から、特定のワークロードにおいて論理プロセッサの負荷分散を上手くできないケースがあると推測しました。

Hyper-V Hypervisor のさまざまな種類のスケジューラを理解して使用する | Microsoft Learn

この記事を参考に Hyper-V のハイパーバイザースケジューラの種類を確認しました。

PS > Get-WinEvent -FilterHashTable @{ProviderName="Microsoft-Windows-Hyper-V-Hypervisor"; ID=2} -MaxEvents 1

   ProviderName: Microsoft-Windows-Hyper-V-Hypervisor

TimeCreated                      Id LevelDisplayName Message
-----------                      -- ---------------- -------
2022/10/30 10:55:54               2 情報             Hypervisor scheduler type is 0x4.

今回の環境では「ルートスケジューラ」が選択されているとわかります。

ルートスケジューラについて下記の記載があります。

サーバー システムでのルート スケジューラの使用
現時点では、サーバー上の Hyper-V でのルート スケジューラの使用は推奨されていません。これは、多くのサーバー仮想化のデプロイで一般的なさまざまなワークロードに対応するためのパフォーマンス特性がまだ完全に特徴付けられたり調整されていないためです。

Hyper-V Hypervisor のさまざまな種類のスケジューラを理解して使用する | Microsoft Learn

このことから、ルートスケジューラの調整が不十分なために今回の事象が発生した可能性もあるのでは…?とみています。

問題の切り分けのためには他のスケジューラで検証したいところですが、下記の記載がありました。

クライアント システムでのルート スケジューラの使用
Windows 10 バージョン1803以降、クライアント システムではルート スケジューラが既定で使用されます。クライアント システムでは、ハイパーバイザーを有効にすると、仮想化ベースのセキュリティと WDAG ワークロードの分離がサポートされ、異種のコア アーキテクチャを使用する今後のシステムを適切に運用できます。 これは、クライアント システムでサポートされる唯一のハイパーバイザー スケジューラの構成です。 管理者は Windows 10 クライアント システムで既定のハイパーバイザー スケジューラの種類を上書きすべきではありません。

Hyper-V Hypervisor のさまざまな種類のスケジューラを理解して使用する | Microsoft Learn

手元で検証に利用可能な環境がなく、今回は見送ることにしました。確証を得られなかったことは残念です。

調査補足

ハイパーバイザースケジューラの問題ではないか、という結論に至るまでは試行錯誤がありました。その過程の一部をお見せすることは今回の事象を理解する参考になると考え、以下にまとめます。

今回調査するに至ったきっかけは、PostgreSQLベンチマークツールを動作させたとき、CPU に余計な負荷をかけた時の結果が良いと気づいたことでした。

$ pgbench -i -s 32 testdb
$ pgbench -c 1 -j 1 -S -T 180 testdb

-- CPU に余計な負荷をかける場合は以下のコマンドを実行する。
# stress-ng -c 3 -l 100
条件 TPS
負荷なし 7486.170541
負荷あり 15680.693977

仮想マシンに割り当てる CPU の数を変化させてみると、1 個で最も結果が良く、複数個では結果が悪くなりました。

$ pgbench -i -s 32 testdb
$ pgbench -c 1 -j 1 -S -T 180 testdb
CPU数 TPS
1 17841.700461
2 7485.412947
3 7522.999178
4 7486.170541

ベンチマークのシナリオに問題があるのではと考え、"SELECT 1;" というシンプルなクエリで試しましたが、PostgreSQL と pgbench への CPU の割り当て方で差が出たのは先述の通りです。

プロセスへの CPU の割り当て方でパフォーマンスが変わるのであれば、コンテキストスイッチが悪さをしているのではないかと推測しました。そこで、perf で調査を行いました。

-- 同一 CPU で動作
$ taskset -c 0 pg_ctl start -D /usr/local/pgsql/data/
$ taskset -c 0 pgbench -f /home/postgres/select1.pgbench -c 1 -j 1 -n -T 180 testdb

-- 異なる CPU で動作
$ taskset -c 0 pg_ctl start -D /usr/local/pgsql/data/
$ taskset -c 1 pgbench -f /home/postgres/select1.pgbench -c 1 -j 1 -n -T 180 testdb

-- バックエンドプロセスの pid を調べる
# ps aux | grep postgres

# perf stat -p <バックエンドプロセスの pid> sleep 10

perf stat の結果を示します。

-- 同一 CPU で動作
          5,971.10 msec task-clock                #    0.597 CPUs utilized
           179,280      context-switches          #   30.025 K/sec
                 0      cpu-migrations            #    0.000 /sec
                 0      page-faults               #    0.000 /sec
    17,564,682,664      cycles                    #    2.942 GHz
     8,420,378,120      instructions              #    0.48  insn per cycle
     1,790,710,601      branches                  #  299.896 M/sec
        21,018,291      branch-misses             #    1.17% of all branches

-- 異なる CPU で動作
          7,456.07 msec task-clock                #    0.745 CPUs utilized
           113,125      context-switches          #   15.172 K/sec
                 0      cpu-migrations            #    0.000 /sec
                 0      page-faults               #    0.000 /sec
    13,385,326,996      cycles                    #    1.795 GHz
     5,310,518,873      instructions              #    0.40  insn per cycle
     1,128,762,018      branches                  #  151.388 M/sec
        34,475,930      branch-misses             #    3.05% of all branches

コンテキストスイッチの頻度だけを見れば、異なる CPU のほうが少ない結果となりました。ただ、cycles の値も小さいのが気になりました。

perf record / report での分析も行いましたが、ここからは差異を見出すことができませんでした。

CPU 毎の使用率を調べてみました。

-- 同一 CPU で動作
$ taskset -c 0 pg_ctl start -D /usr/local/pgsql/data/
$ taskset -c 0 pgbench -f /home/postgres/select1.pgbench -c 1 -j 1 -n -T 180 testdb

-- 異なる CPU で動作
$ taskset -c 0 pg_ctl start -D /usr/local/pgsql/data/
$ taskset -c 1 pgbench -f /home/postgres/select1.pgbench -c 1 -j 1 -n -T 180 testdb

# mpstat -P ALL 1
-- 同一 CPU で動作
CPU    %usr   %nice    %sys %iowait    %irq   %soft  %steal  %guest  %gnice   %idle
all   14.50    0.00   10.25    0.00    0.25    0.00    0.00    0.00    0.00   75.00
  0   58.00    0.00   41.00    0.00    1.00    0.00    0.00    0.00    0.00    0.00
  1    0.00    0.00    0.00    0.00    0.00    0.00    0.00    0.00    0.00  100.00
  2    0.00    0.00    0.00    0.00    0.00    0.00    0.00    0.00    0.00  100.00
  3    0.00    0.00    0.00    0.00    0.00    0.00    0.00    0.00    0.00  100.00

-- 異なる CPU で動作
CPU    %usr   %nice    %sys %iowait    %irq   %soft  %steal  %guest  %gnice   %idle
all    8.88    0.00   17.26    0.00    0.00    0.00    0.00    0.00    0.00   73.86
  0   26.80    0.00   36.08    0.00    0.00    0.00    0.00    0.00    0.00   37.11
  1    9.38    0.00   33.33    0.00    0.00    0.00    0.00    0.00    0.00   57.29
  2    0.00    0.00    0.00    0.00    0.00    0.00    0.00    0.00    0.00  100.00
  3    0.00    0.00    0.99    0.00    0.00    0.00    0.00    0.00    0.00   99.01

いずれの場合にも CPU には余裕があるように見えます。そこで、PostgreSQL の待機イベントに違いが見いだせないかを調べました。待機イベントの調査には pg_wait_sampling (GitHub - postgrespro/pg_wait_sampling: Sampling based statistics of wait events) を利用しました。

$ taskset -c 0 pg_ctl start -D /usr/local/pgsql/data/

$ psql -d testdb -c 'SELECT pg_wait_sampling_reset_profile();'

-- 同一 CPU で動作
$ taskset -c 0 pgbench -f /home/postgres/select1.pgbench -c 1 -j 1 -n -T 180 testdb
-- 異なる CPU で動作
$ taskset -c 1 pgbench -f /home/postgres/select1.pgbench -c 1 -j 1 -n -T 180 testdb

$ psql --csv -d testdb -c "SELECT event_type, event, SUM(count) AS sum_event FROM pg_wait_sampling_profile WHERE event IS NOT NULL GROUP BY event_type, event ORDER BY sum_event desc;"
-- 同一 CPU で動作
event_type,event,sum_event
Activity,LogicalLauncherMain,19491
Activity,CheckpointerMain,19491
Activity,WalWriterMain,19489
Client,ClientRead,17852
Activity,BgWriterMain,11355
Activity,BgWriterHibernate,8136
IO,DataFileRead,2
IO,WALSync,1

-- 異なる CPU で動作
event_type,event,sum_event
Activity,CheckpointerMain,19171
Activity,LogicalLauncherMain,19171
Activity,WalWriterMain,19170
Activity,BgWriterHibernate,18798
Client,ClientRead,17823
Activity,BgWriterMain,373
IO,WALSync,1

BgWriterMain, BgWriterHibernate で差異があるように見えますが、どちらもバックグラウンドライタプロセスの待機イベントであることを踏まえ、足し合わせた値で比較すると、差異がほとんどないとわかります。

その他、様々な試行錯誤を行いましたが、仮想マシン上で取得できる情報から原因を見出すことができませんでした。そして、最終的にパフォーマンスモニタで Hyper-V の情報を取得できることに気づき、調査して先述の結論にたどり着きました。