【ITニュース解説】Stop Letting a Backup Job Starve Your Database: Practical cgroup v2 I/O Control on Linux
2026年11月05日に「Dev.to」が公開したITニュース「Stop Letting a Backup Job Starve Your Database: Practical cgroup v2 I/O Control on Linux」について初心者にもわかりやすく解説しています。
ITニュース概要
バックアップでデータベースが遅くなる問題は、I/O競合が原因だ。古い`ionice`では解決しない。Linuxの`cgroup v2`のI/O制御機能を使えば、バックアップのI/O帯域やIOPSに上限を設定し、データベースの性能低下を防ぐ。`systemd`で設定後、`io.max`で適用を確認できる。データベースの応答速度を保護する`io.latency`も有効だ。
ITニュース解説
データベースのバックアップが開始されると、データベースのクエリ応答速度が急激に悪化し、システム監視アラートが頻繁に発生することがある。これは、バックアップ処理がデータベースと同じストレージデバイス、特に高速なNVMeデバイスに対して大量のI/O(Input/Output、読み書き処理)を行うことが原因で、データベースが必要なI/Oを待たされる「飢餓状態」に陥ってしまう現象である。バックアップ自体は破損しておらず、処理速度が遅いわけでも、CPUを大量に消費しているわけでもない。単純に、毎秒数百MBといった高速な書き込みが、データベースのI/Oを阻害しているのである。
この問題に対して、伝統的な解決策としてionice -c3コマンドを使う方法が知られている。これは特定のプロセスのI/O優先度を低く設定するもので、かつては効果があった。しかし、現代の多くのLinuxシステムでは、この方法はほとんど機能しない。その理由は、ioniceがI/O優先度を設定しても、その優先度を解釈してI/Oをスケジューリングする「ブロックスケジューラ」が進化し、優先度を考慮しなくなったためである。特にNVMeデバイスで広く使われているmq-deadlineやnoneといったブロックスケジューラは、ioniceが設定するI/O優先度を全く考慮しない。ioniceを有名にしたCFQスケジューラは、Linuxカーネル5.0で削除されている。一部のシステムで使われるBFQスケジューラはI/O優先度を考慮するが、それは「比例配分」であり、バックアップがアイドル優先度であっても、データベースが使っていないI/O帯域を全て消費してしまうため、データベースの応答速度の遅延を完全に防ぐことはできないのである。自身のシステムでどのスケジューラが使われているかは、lsblk -o NAME,MAJ:MIN,SIZE,ROTA,SCHEDや/sys/block/nvme0n1/queue/schedulerの内容で確認できる。
そこで登場するのが、Linuxカーネルの機能である「cgroup v2 I/Oコントローラ」である。これはプロセスグループに対して、I/Oリソースの使用を制御するメカニズムを提供する。cgroup v2のI/Oコントローラには主に三つの機能がある。
一つ目はio.maxである。これは、デバイスごとのI/Oスループット(bytes/s)やIOPS(I/O Operations Per Second、一秒あたりのI/O回数)に厳密な上限(ハードリミット)を設定する。設定された上限を超えたI/Oは遅延され、制限内に収められる。この機能は「blk-throttle」というメカニズムで実装されている。ただし、この制限は「ブロックレイヤー」でI/Oがデバイスに書き込まれる際に適用される点に注意が必要である。write(2)システムコールでデータを書き込んでも、それがすぐにディスクに書き込まれるわけではなく、一度カーネルのページキャッシュというメモリ領域に保存され、後でまとめてディスクに書き込まれる(ライトバックされる)ことがある。このライトバック時に制限が適用されるため、プロセスが見かけ上高速に書き込みを完了しても、実際にデバイスへの書き込みは制限されているという状況が発生する。また、一時的なI/Oのバースト(瞬間的な大量I/O)は許容される設計となっている。このため、io.maxは持続的なデバイスへのトラフィックを制限するのに適している。
二つ目はio.weightである。これは、兄弟関係にあるcgroup間でI/O時間を比例配分する機能で、「iocost」または「BFQ」スケジューラが有効な場合にのみ機能する。デフォルト値は100で、1から10,000の範囲で設定できる。多くのシステムではiocostが無効であったり、対応するスケジューラが使われていないため、io.weightを設定しても実際には何も変化しないことが多い。
三つ目はio.latencyである。これは、特定のcgroupのI/Oレイテンシ目標を保護するために、競合する他のcgroupをスロットリングする機能である。「blk-iolatency」というメカニズムで実装されている。この機能はwork-conserving(リソースを無駄なく使う)であり、全てのグループが目標を達成できている間は何も制限されない。しかし、いずれかのグループが設定されたレイテンシ目標を達成できない場合、その遅延を解消するために、目標達成を阻害している他のグループのI/Oが抑制される。この機能は、同じ階層の「兄弟グループ」間でのみ比較して動作する。また、blkcg_debug_statsが有効になっていないと、実際の動作状況(レイテンシ目標の達成状況など)を観察することができないという制約がある。
cgroup v2を実際に利用するには、いくつかの前提条件を確認する必要がある。まず、システムがcgroup v2の統一階層を使っているか、/sys/fs/cgroupがcgroup2fsであるかを確認する。次に、ioコントローラがカーネルに組み込まれており、利用可能であるか、/sys/fs/cgroup/cgroup.controllersにioが含まれているかを確認する。さらに、制限を設定する物理デバイスのメジャー/マイナー番号をlsblkコマンドで確認する必要がある。cgroupのI/O制御はパーティションではなく、物理デバイスに対して行われるため、正確なデバイス指定が重要となる。
バックアップジョブにI/O上限を適用する具体的な手順は、systemdの機能と連携して行うのが一般的である。
まず、バックアップジョブを独立した「スライス」に割り当てる。スライスとは、systemdが提供するcgroupの管理単位の一つで、関連する複数のサービスやプロセスをまとめることができる。例えば、/etc/systemd/system/backup.sliceというファイルを作成し、[Slice]セクションにIOAccounting=yesを設定してI/Oのアカウンティングを有効にする。そして、バックアップサービス(例: /etc/systemd/system/restic-backup.service)の設定ファイルでSlice=backup.sliceを指定すると、そのサービスおよびそれが起動する全てのプロセスがbackup.sliceのI/O予算を継承するようになる。
次に、このスライスに対してI/Oのハードリミットを設定する。例えば、/etc/systemd/system/backup.slice.d/io.confというファイルを作成し、[Slice]セクションにIOReadBandwidthMax=/dev/nvme0n1 200M、IOWriteBandwidthMax=/dev/nvme0n1 100M、IOWriteIOPSMax=/dev/nvme0n1 2Kのように設定する。ここで注意すべきは、MやKといった単位サフィックスは1000ベース(例: 100Mは100,000,000バイト)である点である。また、帯域幅(bytes/s)とIOPS(I/O回数/s)の両方を設定した場合、どちらか先に到達した方が制限として機能する。これは、シーケンシャル書き込みでは帯域幅、ランダムなメタデータ書き込みではIOPSで制限したい場合に有効である。
設定を適用するには、sudo systemctl daemon-reloadを実行し、バックアップサービスを再起動する。設定が正しく適用されたかは、/sys/fs/cgroup/backup.slice/io.maxの内容を確認することでわかる。このファイルに設定した制限値が、物理デバイスのメジャー/マイナー番号(例: 259:0)と共に表示されていれば、カーネルによって制限が適用されている状態である。
設定したI/O制限が実際に機能していることを確認するのも重要である。/sys/fs/cgroup/backup.slice/io.statファイルには、そのcgroupのデバイスごとのリアルタイムI/O統計が表示される。バックアップジョブの前後でこのファイルの内容を比較し、wbytes(書き込みバイト数)をジョブの実行時間で割った値が、設定した帯域幅上限に収まっているかを確認する。高速なNVMeデバイスでは制限なしだと数百MB/sになるはずが、制限適用後は設定値近くに落ち着いていれば成功である。より制御されたテストを行うには、fioのようなツールを--direct=1オプション付きで、設定したスライス内で実行し、スループットを計測するのが有効である。
cgroup v2のI/O制御を利用する際には、いくつかの「落とし穴」にも注意が必要である。
一つ目は「バッファリングされた書き込み」である。ddコマンドなどでoflag=directやconv=fsyncオプションを使わずに書き込み速度を計測すると、データがページキャッシュに書き込まれる速度(メモリ速度)が表示され、実際のディスクへの書き込み速度(I/Oコントローラによる制限後)とは異なる結果となることがある。正確なI/O速度を計測するには、常に「ダイレクトI/O」を使用するか、ライトバックを強制する必要がある。
二つ目は「cgroupへのライトバック課金」である。カーネルは、ext2、ext4、btrfs、f2fs、xfsといった特定のファイルシステムにおいてのみ、ライトバックI/Oを対応するcgroupに課金する。他のファイルシステムでは、ライトバックI/Oはルートcgroupに課金されてしまうため、バックアップスライスに設定したI/O制限が、ページキャッシュからの遅延書き込みには適用されない可能性がある。
三つ目は「io.weightが機能しない問題」である。前述の通り、iocostが有効であるか、BFQスケジューラが使用されていることを確認する必要がある。これらが満たされない場合、io.weightを設定しても何も変わらない。
四つ目は「io.latencyの観察の難しさ」である。io.latencyの動作状況を示す統計情報(missedやtotalフィールド)は、blkcg_debug_statsが有効になっていないと表示されず、デフォルトではオフになっているため、設定の効果を視覚的に確認できないことがある。
五つ目は「ネットワークファイルシステム」である。NFSのようなネットワークファイルシステムはブロックデバイスを持たないため、io.maxなどのcgroup I/Oコントローラは適用できない。
六つ目は「NVMeにおける%utilの誤解」である。従来のHDDなどでディスク使用率を示す%utilは、デバイスが飽和しているかどうかの指標として有効だったが、NVMeデバイスでは100%を示してもまだ処理能力に余裕がある場合がある。NVMeデバイスの負荷状況を正確に把握するには、iostat -x 1でawait(I/Oリクエストの平均待ち時間)やr_await(読み込みリクエストの平均待ち時間)の値を確認するか、/sys/fs/cgroup/backup.slice/io.pressureファイルに表示されるI/O待ち時間の統計情報を見るのが適切である。
I/Oの予算が不明確な場合や、より動的な制御を望む場合は、io.maxでバックアップに上限を設定する代わりに、io.latencyを利用してデータベースを保護するアプローチも可能である。例えば、データベーススライスに/dev/nvme0n1 25msといった低レイテンシ目標を設定し、競合するバックアップスライスには/dev/nvme0n1 100msのような緩い目標を設定する。これにより、データベースのレイテンシが25msを超えた場合にのみ、バックアップがスロットリングされ、データベースのレイテンシが目標内に戻るまでI/Oが抑制される。データベースがアイドル状態であれば、バックアップはフルスピードで実行されるため、リソースを無駄なく利用できる。ただし、この機能を利用するにはCONFIG_BLK_CGROUP_IOLATENCYカーネルオプションが有効である必要があり、また設定する目標値はデバイスが実際に達成可能な範囲内である必要がある。達成不可能な目標を設定すると、常に他のプロセスがスロットリングされ、かえってシステム全体のパフォーマンスが低下する可能性があるため、事前にデバイスのレイテンシを計測し、その値よりわずかに高い値を目標として設定することが推奨される。
まとめとして、どのI/Oコントローラを選択すべきかは、システムの要件によって異なる。予測可能な上限設定と確実なI/O分離が必要な場合は、io.maxが最も適している。これはどのI/Oスケジューラにも依存せず、設定も比較的容易で、問題発生時の原因特定もしやすい。バックアップジョブやコンテナイメージのダウンロード、ログ転送など、一時的に大量のI/Oを発生させるプロセスに対して有効である。ハードキャップなしで公平なI/O共有を行いたい場合はio.weightが選択肢となるが、その有効性を事前に確認する必要がある。そして、データベースのようなレイテンシに敏感なワークロードを、他のI/O要求の多いプロセスから保護したい場合は、io.latencyが有効である。これはio.maxと組み合わせて使うことで、より堅牢なリソース制御を実現できる。
設定を元に戻す場合は、作成した設定ファイルを削除し、systemctl daemon-reloadを実行する。その後、/sys/fs/cgroup/backup.slice/io.maxの内容がrbps=max wbps=max riops=max wiops=maxのように全て「max」に戻っていることを確認すれば、制限が解除されたことがわかる。
データベースの性能を低下させるバックアップジョブの問題は、I/Oの「優先度」ではなく「予算」を割り当てることで解決されるべきである。バックアップジョブを独立したスライスに割り当て、そのスライスにI/Oの上限を設定し、io.maxファイルの内容でその制限がカーネルによって確実に適用されていることを検証することが、現代のLinuxシステムにおけるI/O制御の基本となる。