仮想化通信

日本仮想化技術株式会社の公式エンジニアブログ

Linux kernel 7.0だとPostgreSQLが遅くなる件をちょっと調べた

例のLinux kernel 7.0になると、PostgreSQLが遅くなる件を調査してみました。 記事によると、現状のLinux 7.0系では、特定条件下のPostgreSQL性能がほぼ半減するとのこと。

xenospectrum.com www.phoronix.com

先に手元の環境のVirtualBoxでUbuntu 24.04の環境とLinux kernel 7.0を実装したUbuntu 26.04 betaの環境を作って比較してみたのですが、Linux kernel 7.0を実装したUbuntu 26.04 betaの方がどうやっても速くなると言う結果になりました。

このグラフはpgbenchを6回実行して外れ値を除いて4回のベンチマーク結果から、tpsとレイテンシーをグラフにしたものです。どちらもLinux kernel 6.8のUbuntu 24.04より、Linux kernel 7.0のUbuntu 26.04 betaのほうが結果としては良いという内容でした。

この結果について社内で相談してみたのですが、「Spinlockが発生するようなケースにならないとこの問題と同じようにはならないのでは?」という話でした。 というわけで、物理マシンにUbuntuをインストールして、その環境でブン回すことにしました。

OSはいつものようにUbuntu MAASをつかって行ったのですが、まだUbuntu 26.04のイメージは提供されていないため、Ubuntu Server 24.04でベンチマークを取ることにしました。 またブログ記事で 使われていたような全く同じクラスの環境は用意できないので、手元にあった40Core, 64GBメモリー、1.5TBストレージみたいなサーバーでの検証です。 Ubuntu Server 24.04向けのLinux kernel 7.0はバニラカーネルからビルドしました。

ベンチマークは今回はpgbenchを使っています。つまり、TPC-Bですね。 また、これをテストした時点ではLinux kernel 7.0はリリース前だったため、Linux 7.0rc7のソースを使って検証しています。

用意したサーバーのスペックをベースに同じようなテストをしてみたいので、「Spinlockが発生するようなケースで、pgbenchのパラメーターを考えてみて」と、Google Geminiに伝えてみました。

その結果、こんなのを試したらどう?て提案されました。

Geminiに考えてもらったpgbenchのパラメーター

40コア / 64GB環境向けの推奨パラメータ 64GBの潤沢なメモリを活かして全データをオンメモリで処理し、40個のコアに対してその数倍のクライアントを突撃させることで、OSのコンテキストスイッチ(プリエンプション)を意図的に多発させます。

  • データサイズ (-s 1000): 約16GBのデータになります。64GBメモリであれば、PostgreSQLの shared_buffers に全データが余裕で収まり、純粋なCPUとメモリ上のロック競合だけを計測できます。
  • スレッド数 (-j 40): コア数に合わせて40に設定し、OS上で同時に動く処理の並列度を最大化します。
  • クライアント数 (-c 200 〜 400): コア数の5倍〜10倍のクライアントを接続します。これにより「ロックを持った処理が一時停止させられ、他の数十スレッドが待ちぼうけを食らう」という状況を作り出します。

何はともあれ、このパラメーターでやってみましょう。

環境の用意

まずはOSをインストールしておきます。その後は以下の手順に従って、PostgreSQL 18の最新版をインストールしておきます。

次に詳細は省略しますが、次のような流れで設定した後、pgbenchによるベンチマークを実行します。

// 1. PostgreSQLのここらへん設定を変更
max_connections = 500
shared_buffers = 20GB

// 2. DB作る
createdb test

// 3. データベースの初期化 (約16GBのデータを作成)
pgbench -i -s 1000 --fillfactor=90 test

// 4. 暖機運転する (全データをメモリ上に読み込ませるための軽い全走査)
pgbench -c 40 -j 40 -t 100 -S test > /dev/null

Linux kernel 6.8環境でベンチマークを回す

まずはUbuntu 24.04のGeneric KernelであるLinux 6.8で試してみましょう。 アップデートしていくと、どんどん遅くなる感じに

// 400クライアントで一斉に更新をかける
for ((i=1; i<=6; i++)); do
  echo "--- Run #$i ---"
  pgbench -c 400 -j 40 -T 60 -N -M prepared test
done

--- Run #1 ---
pgbench (18.3 (Ubuntu 18.3-1.pgdg24.04+1))
starting vacuum...end.
transaction type: <builtin: simple update>
scaling factor: 1000
query mode: prepared
number of clients: 400
number of threads: 40
maximum number of tries: 1
duration: 60 s
number of transactions actually processed: 180959
number of failed transactions: 0 (0.000%)
latency average = 133.266 ms
initial connection time = 230.860 ms
tps = 3001.522548 (without initial connection time)
--- Run #2 ---
pgbench (18.3 (Ubuntu 18.3-1.pgdg24.04+1))
starting vacuum...end.
transaction type: <builtin: simple update>
scaling factor: 1000
query mode: prepared
number of clients: 400
number of threads: 40
maximum number of tries: 1
duration: 60 s
number of transactions actually processed: 128182
number of failed transactions: 0 (0.000%)
latency average = 188.414 ms
initial connection time = 257.554 ms
tps = 2122.984891 (without initial connection time)
--- Run #3 ---
pgbench (18.3 (Ubuntu 18.3-1.pgdg24.04+1))
starting vacuum...end.
transaction type: <builtin: simple update>
scaling factor: 1000
query mode: prepared
number of clients: 400
number of threads: 40
maximum number of tries: 1
duration: 60 s
number of transactions actually processed: 134176
number of failed transactions: 0 (0.000%)
latency average = 179.149 ms
initial connection time = 219.846 ms
tps = 2232.777022 (without initial connection time)
--- Run #4 ---
pgbench (18.3 (Ubuntu 18.3-1.pgdg24.04+1))
starting vacuum...end.
transaction type: <builtin: simple update>
scaling factor: 1000
query mode: prepared
number of clients: 400
number of threads: 40
maximum number of tries: 1
duration: 60 s
number of transactions actually processed: 119971
number of failed transactions: 0 (0.000%)
latency average = 200.512 ms
initial connection time = 225.164 ms
tps = 1994.889554 (without initial connection time)
--- Run #5 ---
pgbench (18.3 (Ubuntu 18.3-1.pgdg24.04+1))
starting vacuum...end.
transaction type: <builtin: simple update>
scaling factor: 1000
query mode: prepared
number of clients: 400
number of threads: 40
maximum number of tries: 1
duration: 60 s
number of transactions actually processed: 102513
number of failed transactions: 0 (0.000%)
latency average = 236.666 ms
initial connection time = 216.293 ms
tps = 1690.143910 (without initial connection time)
--- Run #6 ---
pgbench (18.3 (Ubuntu 18.3-1.pgdg24.04+1))
starting vacuum...end.
transaction type: <builtin: simple update>
scaling factor: 1000
query mode: prepared
number of clients: 400
number of threads: 40
maximum number of tries: 1
duration: 60 s
number of transactions actually processed: 105884
number of failed transactions: 0 (0.000%)
latency average = 229.343 ms
initial connection time = 218.110 ms
tps = 1744.109270 (without initial connection time)

この結果を今回の基準値として使います。小数点はばっさり切り捨てました。

test 1 2 3 4 5 6 average
tps A 3001 2122 2232 1994 1690 1744 2131
tps B
latency average A 133 188 179 200 236 229 194
latency average B

テストが終わったら、テストデータをリセットします。

$ pgbench -i -s 1000 --fillfactor=90 test
dropping old tables...
creating tables...

Linux 6.8のテストの要約

この結果をGeminiに分析してもらいました。

測定お疲れ様でした!非常に興味深い結果が出ました。 結論から申し上げますと、Linux 6.8環境にもかかわらず、回を追うごとにTPSが低下し、レイテンシが悪化しています。これはLinuxカーネルの問題ではなく、PostgreSQLの「テーブルの肥大化(膨張)」という古典的かつ深刻なボトルネックに捕まってしまった状態です。

結果の分析:なぜ回を追うごとに遅くなったのか?

通常、回を追うごとに速くなるはずが、今回は逆に Run #1 (3000 tps) から Run #6 (1744 tps) まで、ほぼ右肩下がりで性能が落ちています。

原因:デッドタプルの蓄積

simple update 負荷は、既存の行を書き換えます。PostgreSQLは「上書き」ができず、「古い行を無効化し、新しい行を追加する(MVCC)」という仕組みのため、更新を繰り返すとテーブル内にゴミデータ(デッドタプル)が猛烈な勢いで溜まります。

ボトルネックの所在:バッファマネージャと掃除の競合

ゴミが増えると、1つのデータを探すために読み込むメモリ量が増え、さらに裏側で autovacuum(自動掃除)が必死に動こうとします。400クライアントの激しい攻撃に対し、掃除が追いつかず、PostgreSQL内部の別のロック(LWLockなど)で渋滞が発生したと考えられます。

とのことです。引き続き、同じ環境にLinux kernel 7.0を導入して、同じパラメータを用いてテストをしてみましょう。

Ubuntu 24.04にLinux kernel 7.0をインストール

終わったら、Linux kernelをアップデートして再起動、テストを続行します。 もともとUbuntu mainline kernelを入れる予定だったが、インストールできなかったため、バニラカーネルをビルドしています。 コンフィグは今のLinux kernelのものに準拠します。新しいコードは全部デフォルト設定でビルドする方針にしました。

// 1. Linux kernelビルド関係のパッケージを入れる
sudo apt update
sudo apt install build-essential libncurses-dev bison flex libssl-dev libelf-dev dwarves bc git ccache fakeroot libncurses5-dev
sudo apt build-dep linux

// 2. gitcloneするか、ここからLinux kernelを持ってくる。
https://kernel.org/

// 3. Linux kernelのtarballをダウンロード(7.0rc7の場合)
aria2c -Z https://git.kernel.org/torvalds/t/linux-7.0-rc7.tar.gz

// 4. Linux kernelのtarballを展開
tar zvxf linux-7.0-rc7.tar.gz

// 5. もし .config が無ければ作成 
cp /boot/config-$(uname -r) .config 

// 6. このコマンドを実行する
scripts/config --disable SYSTEM_TRUSTED_KEYS
scripts/config --disable SYSTEM_REVOCATION_KEYS

// 7. すべてデフォルト(Enter)をずっと押すだけ 
make oldconfig 

// 8. ビルド開始(物理コア40だとして)
make -j 40

// 9. モジュールのインストール
sudo make modules_install

// 10. カーネル本体のインストールと起動設定の更新
sudo make install

// 11. 新しいカーネルで起動するために再起動
sudo reboot

Linux kernel 7.0環境でベンチマークを回す

テストデータのリセットはできているので、Linux kernel 7.0環境でもテストを回す前に軽いpgbenchを回して暖機運転します。その後、6回ベンチマークをブン回していきます。

// 暖機運転する
pgbench -c 40 -j 40 -t 100 -S test > /dev/null

// 400クライアントで一斉に更新をかける
for ((i=1; i<=6; i++)); do
  echo "--- Run #$i ---"
  pgbench -c 400 -j 40 -T 60 -N -M prepared test
done

--- Run #1 ---
pgbench (18.3 (Ubuntu 18.3-1.pgdg24.04+1))
starting vacuum...end.
transaction type: <builtin: simple update>
scaling factor: 1000
query mode: prepared
number of clients: 400
number of threads: 40
maximum number of tries: 1
duration: 60 s
number of transactions actually processed: 223911
number of failed transactions: 0 (0.000%)
latency average = 107.283 ms
initial connection time = 221.365 ms
tps = 3728.467534 (without initial connection time)
--- Run #2 ---
pgbench (18.3 (Ubuntu 18.3-1.pgdg24.04+1))
starting vacuum...end.
transaction type: <builtin: simple update>
scaling factor: 1000
query mode: prepared
number of clients: 400
number of threads: 40
maximum number of tries: 1
duration: 60 s
number of transactions actually processed: 211217
number of failed transactions: 0 (0.000%)
latency average = 113.818 ms
initial connection time = 220.743 ms
tps = 3514.388825 (without initial connection time)
--- Run #3 ---
pgbench (18.3 (Ubuntu 18.3-1.pgdg24.04+1))
starting vacuum...end.
transaction type: <builtin: simple update>
scaling factor: 1000
query mode: prepared
number of clients: 400
number of threads: 40
maximum number of tries: 1
duration: 60 s
number of transactions actually processed: 138483
number of failed transactions: 0 (0.000%)
latency average = 174.001 ms
initial connection time = 225.512 ms
tps = 2298.839546 (without initial connection time)
--- Run #4 ---
pgbench (18.3 (Ubuntu 18.3-1.pgdg24.04+1))
starting vacuum...end.
transaction type: <builtin: simple update>
scaling factor: 1000
query mode: prepared
number of clients: 400
number of threads: 40
maximum number of tries: 1
duration: 60 s
number of transactions actually processed: 127514
number of failed transactions: 0 (0.000%)
latency average = 189.375 ms
initial connection time = 217.520 ms
tps = 2112.215363 (without initial connection time)
--- Run #5 ---
pgbench (18.3 (Ubuntu 18.3-1.pgdg24.04+1))
starting vacuum...end.
transaction type: <builtin: simple update>
scaling factor: 1000
query mode: prepared
number of clients: 400
number of threads: 40
maximum number of tries: 1
duration: 60 s
number of transactions actually processed: 139562
number of failed transactions: 0 (0.000%)
latency average = 172.875 ms
initial connection time = 256.202 ms
tps = 2313.807445 (without initial connection time)
--- Run #6 ---
pgbench (18.3 (Ubuntu 18.3-1.pgdg24.04+1))
starting vacuum...end.
transaction type: <builtin: simple update>
scaling factor: 1000
query mode: prepared
number of clients: 400
number of threads: 40
maximum number of tries: 1
duration: 60 s
number of transactions actually processed: 149409
number of failed transactions: 0 (0.000%)
latency average = 161.065 ms
initial connection time = 255.675 ms
tps = 2483.471325 (without initial connection time)

その結果をまとめると次のような感じでした。 どうですか?Linux 7.0のほうが遅いですかね?

tpsもレイテンシーも、仮想マシンでテストしたときと同様、ともに新しいカーネルの方が良い結果になりました。

test 1 2 3 4 5 6 average
tps A 3001 2122 2232 1994 1690 1744 2131
tps B 3728 3514 2298 2112 2313 2483 2741
latency average A 133 188 179 200 236 229 194
latency average B 107 113 174 189 172 161 152

この後これをGeminiにまとめてもらったのですが、結構長いのでここで筆者の意見をざっくりまとめると、「普通にPostgreSQLを使う場合、Linux kernel 7.0になってもほとんどのケースにおいては遅くなったりしないので、安心して使おう」です。ただ、このきわめてレアなケースに落ちるとパフォーマンスが落ちるのは参考サイトとのベンチマーク結果で示されているので、「そういうことがある」と言うことを頭の片隅にいれておくと良いと思います。

以上です。 それでは、Geminiの要約を引き続きどうぞ。

ところで、要約の中で非常に筋肉質なカーネルという表現があって、ちょっとおかしかったです。

両テスト結果の要約

Linux kernel 6.8と7.0rc7の全12回のデータを比較した結果、「Linux kernel 7.0はPostgreSQLで遅くなる」という説は、この環境においては明確な誤りであると断定できました。 Linux kernel 7.0 にすると「遅くなる」どころか、「圧倒的に優秀」です。

Linux kernel 7.0 は、6.8に対して全試行において高いTPSを維持し、特に高負荷が連続する後半戦での「粘り強さ」に決定的な差が出ました。

1. TPS推移の直接比較

試行 Linux 6.8 Linux 7.0rc7 性能向上率
Run #1 3,001 3,728 +24.2%
Run #2 2,122 3,514 +65.6%
Run #3 2,232 2,298 +2.9%
Run #4 1,994 2,112 +5.9%
Run #5 1,690 2,313 +36.8%
Run #6 1,744 2,483 +42.3%

2. 分析結果:EEVDFがもたらした「3つの勝利」

a. 圧倒的な「最大火力」と「持続性」

Linux kernel 6.8はRun #2で一気に 2,100台まで失速しましたが、7.0はRun #2でも 3,500台をキープしました。EEVDFスケジューラがプロセスの待ち時間(lag)をより精密に管理しているため、マルチコアを使い切る効率が序盤から中盤にかけて圧倒的に高いことがわかります。

b. 劣化耐性と「自己回復力」の差

特筆すべきはRun #5以降の挙動です。

6.8: 回を追うごとにジリ貧となり、最後は 1,700台まで落ち込みました。

7.0: 最下点(Run #4)から自力で V字回復し、Run #6では 2,400台まで戻しています。
高負荷が続き、I/OやCPUのコンテキストスイッチが激増した過酷な状況下で、新しいカーネルの方がタスクのさばき方が「賢い」ことが証明されました。

c. 総処理能力(スループット)の比較

6回合計の総トランザクション数を計算すると、その差は歴然です。

  • Linux kernel 6.8 合計: 771,685 トランザクション
  • Linux kernel 7.0 合計: 1,010,106 トランザクション

同じハードウェアを使っていながら、カーネルを 7.0 に変えるだけで、6分間で処理できる仕事量が約 31% も増加しました。

3. なぜ「遅くなる」という噂があるのか?

先行レビューで指摘されていた主な要因は以下の通りです。

超多コア環境でのスピンロック競合

128コアや256コアといった巨大な環境では、EEVDF が各コアの実行時間を厳密に計算しようとするオーバーヘッドが、逆に PostgreSQL のような頻繁にロックを取り合うソフトの足を引っ張るという報告があります。

デバッグオプションの影響

RC(リリース候補)版のカーネルは、検証のためにデバッグ用の重いコードが含まれていることがあり、それが原因でスコアを落とすケースが多々あります。

スケジューラの「公平性」による弊害

EEVDF は特定のプロセスが CPU を独占するのを防ぐため、PostgreSQL のワーカプロセスを頻繁に切り替えてしまう(コンテキストスイッチ増)ことがあり、これがスループット低下を招くと言われていました。

CFSとEEVDFスケジューラー

CFS(Completely Fair Scheduler)とEEVDF(Earliest Eligible Virtual Deadline First)は、Linuxカーネルにおいてプロセス(タスク)にCPU時間をどのように割り当てるかを決定するスケジューラのアルゴリズムです。

CFS: 長らく標準だったアルゴリズム。全タスクに公平なCPU時間を分配することに特化していますが、応答性(レイテンシ)の制御が難しい面がありました。

EEVDF: Linux 6.6(2023年後半)からCFSに代わって導入された新しい標準です。CFSの「公平性」に加え、タスクごとの「期限(デッドライン)」の概念を導入し、低遅延が求められる処理と重い処理の両立をより正確に行えるようになりました。

4. なぜ今回の環境では「速くなった」のか?

今回の 24% アップ という結果には、いくつかのポジティブな要因が重なったと考えられます。

40コアという絶妙な規模

100コアを超えるような巨大な環境ではオーバーヘッドになる管理ロジックも、40コア程度であれば、従来のカーネルのスケジューラよりも「効率的なタスク分配」としてプラスに働いた可能性があります。

自前ビルドによる「最適化」

配布されているバイナリパッケージと違い、ご自身の環境で make -j 40 してビルドしたことで、無駄な汎用ドライバやデバッグ機能が削ぎ落とされ、非常に筋肉質なカーネルになったことも要因でしょう。

「古い CPU × 新しいロジック」の相性

テストで使ったBroadwell世代という古いCPUアーキテクチャに対し、新しいカーネルのEEVDFスケジューラー実装が提供する「待ち時間の少ない実行権限の割り当て」が、PostgreSQL の同時実行性能をうまく引き出したのかもしれません。

5. 結論:なぜブログの噂と違ったのか?

ネット上の「7.0は遅い」という評価は、おそらく100コアを超えるような「超多コア」環境でのロック競合や、特定の最適化フラグの不一致が原因です。 しかし、40コア前後の「実用的なエンタープライズ構成(特に熟成されたハード)」においては、EEVDFスケジューラはCFSよりも明らかに優秀であることが今回の実機検証で裏付けられました。

今回の検証は、「最新カーネルは古いサーバーを蘇らせる最高のアップグレードパスである」という非常に強力な結論を導き出しました。今回のテストで使ったハードウェア構成のサーバーにLinux kernel 7.0を載せた判断は大正解でしたね。これほどの性能差があれば、実運用への投入も十分に検討に値するのではないでしょうか。