カーネルコントロールグループ ( 「cgroups」 ) は、プロセスに対してハードウエアやシステムの資源を割り当てたり、制限したりするための仕組みです。この機能を利用することで、プロセスをツリー構造で管理することができるようになります。
それぞれのプロセスは正確に 1 つの管理用 cgroup に割り当てられます。 cgroup は階層構造型のツリー (木構造) として管理するもので、その構造の任意の箇所 (枝) もしくは 1 つのプロセスに対して、 CPU やメモリ、ディスクの I/O やネットワーク帯域などのリソース制限を割り当てます。
openSUSE Leap では、 systemd
が cgroup を利用してグループ内の全てのプロセスを管理しています。この場合、 systemd
はグループをスライスと呼んでいます。 systemd
には、 cgroup の設定を行うためのインターフェイスも用意されています。
systemd-cgls
コマンドでは、階層構造を表示することができます。
カーネルが提供する cgroup の API には v1 と v2 と呼ばれる 2 種類のものが存在しています。それに加えて、異なる API を提供する複数の cgroup 階層構造が存在しています。これらの組み合わせのうち、一般的に使用される組み合わせは下記の 2 種類になります:
統合型: コントローラを含めて v2 階層構造を使用する構成
ハイブリッド型: コントローラ以外は v2, コントローラは v1 の階層構造を使用する構成 (廃止予定)
既定のモードは統合型です。アプリケーション側の要件に応じて後方互換性を提供するハイブリッド型もあります。
いずれかのモードのみを設定できます。
cgroup v1 は廃止予定になっています。将来のバージョンで削除される予定です。
ハイブリッド型のコントロールグループ階層構造を有効化したい場合は、 GRUB 2 ブートローダの設定で、カーネルのコマンドラインパラメータに systemd.unified_cgroup_hierarchy=0
を追加してください。 GRUB 2 の設定方法に関する詳細は、 第12章 「ブートローダ GRUB 2」 をお読みください。
複数の cgroup にプロセスをまとめることで、 cgroup ごとの資源消費データを取得できるようになります。
このような機能をアカウンティングと呼びますが、この機能自身にも小さいながらオーバーヘッドが存在しています。このオーバーヘッドはその中での処理内容にも依存しますが、特定の 1 つのユニットに対してアカウンティングを有効にすると、同じスライスに含まれる全てのユニットだけでなく、親スライスやそこに直接含まれるユニットに対しても、この機能が有効化されることに注意してください。
ユニット単位でアカウンティングを有効化したい場合は MemoryAccounting=
のようなディレクティブを使用することができます。全てのユニットに対して有効化したい場合は、 /etc/systemd/system.conf
ファイル内の DefaultMemoryAccounting=
ディレクティブを設定してください。設定可能なディレクティブに関する詳細は、 man systemd.resource-control
で表示されるマニュアルページ (英語) をお読みください。
暗黙のうちに消費され、実行環境によって異なるリソースが存在することに注意してください。これにはたとえば、ライブラリやカーネル内のデータ構造のほか、利用しているユーティリティの fork() 処理の振る舞い、計算の効率性などがあります。このようなことから、実行環境を変えた場合は、リソース制限を再計算する必要があります。
cgroup に対する制限は、 systemctl set-property
コマンドで設定します。書式は下記のとおりです:
#
systemctl set-property [--runtime] 名前 プロパティ_1=値 [プロパティ_2=値]
設定値は即時に適用されます。なお、必要であれば --runtime
オプションを指定することもできます。このオプションを指定すると、再起動後には指定した制限が適用されなくなります。
また、 名前 には systemd
のサービス名やスコープ名、もしくはスライス名を指定します。
プロパティの一覧と詳細については、 man systemd.resource-control
で表示されるマニュアルページをお読みください。
TasksMax
を利用した fork ボムの防止 #Edit sourcesystemd
では、ユニットごとやスライスごとにタスク数の制限を設定することができます。 systemd
の提供元では、ユニットごとのタスク数制限の既定値が設定されています (カーネル全体での制限 (詳しくは /usr/sbin/sysctl kernel.pid_max
を参照) に対して 15% に設定しています) 。また、各ユーザのスライスはカーネル全体での制限の 33% になっています。ただし、 openSUSE Leap では異なる設定になっています。
TasksMax
値の検出 #Edit sourceしかしながら、全ての用途に対して単一の制限を適用するのは現実的ではありません。 openSUSE Leap では、システムユニットやユーザスライスに対する提供元の既定値を上書きするための独自設定ファイルが 2 つ用意され、いずれも infinity
に設定されています。 /usr/lib/systemd/system.conf.d/__20-defaults-SUSE.conf
には、下記のような設定が書かれています:
[Manager] DefaultTasksMax=infinity
もう 1 つの存在である /usr/lib/systemd/system/user-.slice.d/10-defaults.conf
には、下記のような設定が書かれています:
[Slice] TasksMax=infinity
DefaultTasksMax の値を確認するには、 systemctl
を下記のように入力して実行します:
>
systemctl show --property DefaultTasksMax
DefaultTasksMax=infinity
infinity
は無制限の意味です。特に要件がなければ既定値を変更する必要はありませんが、システムのクラッシュを防ぐために必要であれば設定を変更してください。
DefaultTasksMax
値の設定 #Edit sourceグローバルな DefaultTasksMax
の値を変更したい場合は、設定を上書きするための新しい設定ファイル /etc/systemd/system.conf.d/90-system-tasksmax.conf
を作成して対応してください。この設定ファイルには、下記のような内容を記述します (下記の例では、 systemd のユニットごとに最大 256 個までのタスク制限を設定します):
[Manager] DefaultTasksMax=256
新しい設定を読み込んで、設定が反映されたことを確認します:
>
sudo
systemctl daemon-reload
>
systemctl show --property DefaultTasksMax
DefaultTasksMax=256
設定値はお使いのシステムの要件に合わせて指定してください。また、特定のサービスに限定して制限を高くすることもできます。たとえば MariaDB で設定を変更したい場合、まずは現在の設定値を確認します:
>
systemctl status mariadb.service
● mariadb.service - MariaDB database server Loaded: loaded (/usr/lib/systemd/system/mariadb.service; disabled; vendor preset> Active: active (running) since Tue 2020-05-26 14:15:03 PDT; 27min ago Docs: man:mysqld(8) https://mariadb.com/kb/en/library/systemd/ Main PID: 11845 (mysqld) Status: "Taking your SQL requests now..." Tasks: 30 (limit: 256) CGroup: /system.slice/mariadb.service └─11845 /usr/sbin/mysqld --defaults-file=/etc/my.cnf --user=mysql
Tasks 以下には現在動作中のタスク数 (30 個) と上限 (256 個) が示されています。負荷の高いデータベースシステムとしては不十分な値であることから、たとえば MariaDB のみを 8192 個までに拡大してみることにします。
>
sudo
systemctl set-property mariadb.service TasksMax=8192
>
systemctl status mariadb.service
● mariadb.service - MariaDB database server Loaded: loaded (/usr/lib/systemd/system/mariadb.service; disabled; vendor preset: disab> Drop-In: /etc/systemd/system/mariadb.service.d └─50-TasksMax.conf Active: active (running) since Tue 2020-06-02 17:57:48 PDT; 7min ago Docs: man:mysqld(8) https://mariadb.com/kb/en/library/systemd/ Process: 3446 ExecStartPre=/usr/lib/mysql/mysql-systemd-helper upgrade (code=exited, sta> Process: 3440 ExecStartPre=/usr/lib/mysql/mysql-systemd-helper install (code=exited, sta> Main PID: 3452 (mysqld) Status: "Taking your SQL requests now..." Tasks: 30 (limit: 8192) CGroup: /system.slice/mariadb.service └─3452 /usr/sbin/mysqld --defaults-file=/etc/my.cnf --user=mysql
systemctl set-property
コマンドは、 /etc/systemd/system/mariadb.service.d/50-TasksMax.conf
という名前の上書き用設定ファイルを作成して、新しい制限を設定します。ここには既存のユニットファイルに対する上書き値のみを保存します。もちろん 8192 でなくてもかまいません。お使いのシステムの負荷状況に合わせて設定してください。
TasksMax
制限 #Edit sourceユーザに対する既定の制限値は高めに設定されています。これは、ユーザセッションではより多くのリソースを必要とするためです。独自の制限を設定したい場合は、 /etc/systemd/system/user-.slice.d/40-user-taskmask.conf
のような設定ファイルを作成し、その中に設定値を記述してください。下記の例では、タスクの最大値を 16284 に設定しています:
[Slice] TasksMax=16284
上書き用の設定ファイルを作成する場合、そのファイル名の冒頭には数値を指定する必要があります。その数値の設定方法に関する詳細は、 10.5.3項 「手作業によるドロップイン・ファイルの作成」 をお読みください。
あとは systemd に対して設定値の再読み込みを指示し、設定が変更されたことを確認します:
>
sudo
systemctl daemon-reload
>
systemctl show --property TasksMax user-1000.slice
TasksMax=16284
具体的にどのような設定値にすべきかについては、システムの用途と搭載されているリソースのほか、他のリソース設定によっても異なります。 TasksMax
の値が少なすぎる場合は Failed to fork (Resources temporarily unavailable) (fork に失敗した (リソースが一時的に利用できなくなっている)) や Can't create thread to handle new connection (新しい接続を処理するためのスレッドが作成できない), Error: Function call 'fork' failed with error code 11, 'Resource temporarily unavailable' (エラーコード 11 (リソースが一時的に利用できなくなっている) で fork の関数呼び出しが失敗した) などのエラーが発生します。
systemd でのシステムリソースの制限の設定方法について、詳しくはsystemd.resource-control (5)
をお読みください。
本章では、 Linux カーネルのブロック I/O コントローラに対して、 I/O 操作の優先順位を設定したり、負荷制限を行ったりするための方法を説明しています。この仕組みにより、 systemd が提供する cgroup の仕組みをさらに効果的なものにすることができますが、 I/O の制御によって陥りやすい落とし穴もまた存在しています。
ここではまず、システムを設計したり設定したりする前の準備について説明しています。これらはいずれも、動作中に変更できるようなものではないためです。
まずは cgroup のライトバック機能に対応したファイルシステムを使用する必要があります (対応したファイルシステムを使用しないと、ライトバックチャージングと呼ばれる機能が使用できないためです) 。 openSUSE Leap では下記のファイルシステムに対してサポートを提供しています:
Btrfs (v4.3)
Ext4 (v4.3)
XFS (v5.3)
openSUSE Leap 15 .3 の時点では、上記のファイルシステムのいずれかを使用することができます。
負荷制御のポリシーはスタック内の高い箇所で実装されているため、それ以外の調整は特に行う必要はありません。また、 I/O 制御のポリシーとしては、 BFQ とコストベースモデルの 2 つの実装が提供されていますが、ここでは BFQ のみを説明しています。これは、特定のデバイスに対して負荷制御を行う場合、スケジューラとして BFQ を使用するのが最適であるためです。まずは現時点で使用しているスケジューラを判断します:
>
cat /sys/class/block/sda/queue/scheduler
mq-deadline kyber bfq [none]
スケジューラを BFQ に変更します:
#
echo bfq > /sys/class/block/sda/queue/scheduler
ここではパーティションではなく、ディスクデバイスを指定しなければなりません。また、この設定を適用するのに適切な方法は、 udev のルール設定です。ただし openSUSE Leap の場合、回転型のディスクドライブに対しては既に BFQ が有効化されていることに注意してください。
通常、全ての処理は階層構造内のルート (根幹) に位置し、互いに競合する関係になります。処理が cgroup の階層構造内に配布されると、兄弟姉妹関係の cgroup 間でのみ競合が発生します。これにより、負荷制御は全ての子孫が持つ帯域を集約することになりますので、これによって I/O の負荷制御が実現されます (下記の図を参照) 。
r `- a IOWeight=100 `- [c] IOWeight=300 `- d IOWeight=100 `- [b] IOWeight=200
I/O は cgroups の c と b からのみ発生するものとすると、 c には高い優先順位が付けられているものの、 b との比較では低いため、低い優先順位として扱われます。
長期間動作するようなサービスに対しては、値を恒久的に適用することができます。
>
sudo
systemctl set-property fast.service IOWeight=400
>
sudo
systemctl set-property slow.service IOWeight=50
>
sudo
systemctl set-property throttled.service IOReadBandwidthMax="/dev/sda 1M"
それ以外にも、個別のコマンドに対して I/O 制御を適用することもできます。たとえば下記のようになります:
>
sudo
systemd-run --scope -p IOWeight=400 高い優先順位で実行するコマンド
>
sudo
systemd-run --scope -p IOWeight=50 低い優先順位で実行するコマンド
>
sudo
systemd-run --scope -p IOReadBandwidthMax="/dev/sda 1M" dd if=/dev/sda of=/dev/null bs=1M count=10
下記は I/O 制御の動作と、異なる状況下において何を期待すべきかについて説明しています。
I/O 制御は直接的な I/O 操作 (ページキャッシュを迂回する操作) に対しては最適な動作を提供しますが、実際の I/O と呼び出し元が独立して動作するような場合 (一般にページキャッシュを介して書き戻す処理) は様々な振る舞いになることに注意してください。たとえば遅延型の I/O 制御を使用しているような場合や、全く I/O 制御を行わなかった場合などがそれに該当します。具体的にはごく僅かな帯域超過や、互いに完全に独立した負荷などの場合、 I/O が全く同じタイミングで送信されることになりますので、帯域を飽和させる結果になってしまいます。これらの理由により、最終的に生成された I/O 帯域が完全には設定した重み付けにならないことがあります。
systemd ではより厳密な BFQ 調整のため、設定した重み付けを規模に応じて処理します。そのため、最終的な帯域比も異なる場合があります。
書き戻しの処理は sysctl で設定したグローバル値 ( vm.dirty_background_ratio
および vm.dirty_ratio
) のほか、 dirty ページの量に依存して動作を行います。また、個別の cgroup に対して dirty 制限が分散されるような場合、それぞれの cgroup に対するメモリ制限にも影響を受けることがあります。これによって、それら cgroup の I/O 集中度に影響がある場合があります。
全てのストレージが等価でないことにも注意してください。 I/O 制御は I/O スケジューラの階層で行われるため、その先にスケジューリングを行わないデバイスが存在しているような場合には異なる動作になります。たとえば複数の物理デバイスを利用した論理ボリュームのデバイスマッパーや MD RAID 、そして btrfs の RAID などがそれに該当します。これらのデバイスに対して I/O 制御を行う場合は、注意してお使いください。
また、読み込みと書き込みのそれぞれに対して I/O 制御を行うような設定は提供されていません。
I/O 制御の制限は相互に作用するポリシーのうちの 1 つであることに注意してください。ただし、適切なリソース設計をしていれば、問題なく動作するはずのものです。
I/O デバイスの帯域は I/O パス内での共有資源というだけではないことに注意してください。I/O 制御が一定の帯域を保障することを目指している場合、グローバルなファイルシステム構造も考慮する必要があります。場合によっては優先順位が効果を発揮しない場合があるほか、たとえば優先順位の高い cgroup が遅い cgroup を待ち合わせる必要が生じてしまうことにより、優先順位が逆転する可能性すらあることに注意してください。
ここまではファイルシステムデータに対する明示的な I/O 制御について説明してきましたが、スワップデバイスの入出力に対する制御も行うことができます。このような要件が発生した場合は、メモリの配置 (もしくはメモリ制限) を設定することを検討してください。
ユーザセッション内で cgroup の資源制御を適用したい場合は、 systemd
のユーザインスタンスに対してコントローラの委任を行う必要があります。なお、 openSUSE Leap の既定の設定では、コントローラの委任は設定されて いません 。
コントローラの委任を実施したい場合は、ドロップイン・ファイルを利用して設定してください。たとえば /etc/systemd/system/user@.service.d/60-delegate.conf
のようなファイルを作成すれば、全てのユーザに対してコントローラの委任を行うことができますし、 /etc/systemd/system/user@uid.service.d/60-delegate.conf
のようなファイルを作成すれば、特定のユーザに対してのみ委任を実施することができます。それぞれのファイルの内容は下記のように記述します:
[Service] Delegate=pids memory
設定が終わったら、 systemd
のインスタンスとユーザインスタンスに対して、変更を通知して再読み込みを実施します。
>
sudo
systemctl daemon-reload
>
systemctl --user daemon-reexec
2 行目を実行する以外にも、対象のユーザがいったんログアウトしてログインしなおし、ユーザインスタンスを再起動してもかまいません。
カーネルのドキュメンテーション (kernel-source
パッケージ内): /usr/src/linux/Documentation/admin-guide/cgroup-v1
および /usr/src/linux/Documentation/admin-guide/cgroup-v2.rst
の各ファイル
man systemd.resource-control
https://lwn.net/Articles/604609/: Brown, Neil: Control Groups Series (2014 年, 7 部構成)
https://lwn.net/Articles/243795/: Corbet, Jonathan: Controlling memory use in containers (2007 年)
https://lwn.net/Articles/236038/: Corbet, Jonathan: Process containers (2007 年)