🍍

cgroup を使用してプロセスのリソース使用量を制限する

2023/05/16に公開

cgroup はなに

cgroups は、Linux カーネルが提供する、プロセスグループのリソース使用を制限および隔離するためのメカニズムです。これにより、システム管理者はプロセスグループに対して CPU、メモリ、ディスクI/O などのリソースを制御することができます。cgroups はプロセスのグループ化とリソース管理を容易にし、システムのパフォーマンスとセキュリティを向上させる役割を果たしています。

以下では、systemd または libcgroup ツールを使用してプロセスのリソースを制限する方法について説明します。

  • Linux 6.3.2-arch1-1
  • systemd 253 (253.4-1-arch)
  • libcgroup-git 2.0.r828.g16fdb8c-1

systemd service を使用してリソースを制限する

例えば、以下の server.py の場合、CPU の使用量を制限しない場合、ほぼすべてのリソースを1つの CPU コアが占有します。

server.py
# !/usr/bin/python3

from http.server import BaseHTTPRequestHandler, HTTPServer

ADDR = "127.0.0.1"
PORT = 8080


class MyServer(BaseHTTPRequestHandler):
    def do_GET(self):
        while True:
            pass


if __name__ == "__main__":
    webServer = HTTPServer((ADDR, PORT), MyServer)
    print(f"Server started http://{ADDR}:{PORT}")

    try:
        webServer.serve_forever()
    except KeyboardInterrupt:
        pass

    webServer.server_close()
    print("Server stopped.")

以下の手順で、Unit ファイルを使用して自動起動サービスを作成します。~/.config/systemd/user/test-server.service ファイルを編集し、以下の内容を追加してください。<PATH OF server.py> の部分は、該当のファイルパスに変更してください。。

[Unit]
Description=test-server

[Service]
ExecStart=/usr/bin/python3 <PATH OF server.py>
Restart=on-failure
RestartSec=3

[Install]
WantedBy=default.target

その後 systemctl start --user test-server

systemd はこのプロセスのために自動的に cgroup を作成します。

CGroup: /user.slice/user-1000.slice/user@1000.service/app.slice/test-server.service

cgroup の情報は、/sys/fs/cgroup の対応するサブパスにあります。例えば

>> cat /sys/fs/cgroup/user.slice/user-1000.slice/user@1000.service/app.slice/test-server.service/cpu.stat
usage_usec 58021
user_usec 45898
system_usec 12123
core_sched.force_idle_usec 0

この cgroup では CPU リソースに制限が設定されていないため、ターミナルでローカルの 8080 ポートにリクエストを送信してみると、%CPU が約 100% になることがわかります。

以下、ユニットファイルでリソースの制限を宣言します。ここでは、CPU リソースに関連するいくつかのパラメータを示します。他のリソースのパラメータについては、以下のリンクを参照してください:https://www.freedesktop.org/software/systemd/man/systemd.resource-control.html

  • CPUAccounting: このユニットの CPU 使用量のアカウンティングを有効にするオプションです。
  • CPUWeight=weight:実行されるプロセスに指定した CPU 時間のウェイトを割り当てます。値の範囲は 1 から 10000 で、より大きなウェイトはより多くの CPU 時間を割り当てることを意味します。
  • StartupCPUWeight=weight:システムの起動およびシャットダウンのフェーズに適用される CPU のウェイトです。通常のランタイム時には CPUWeight= が適用されますが、StartupCPUWeight= が設定されていない場合、起動およびシャットダウンのフェーズにも適用されます。
  • CPUQuota= は、実行されるプロセスに割り当てる CPU 時間のクォータを指定します。

server プロセスのリソース制限を 50% に設定するために、ユニットファイルに以下の設定を追加します。

[Service]
# ...
CPUQuota=50%

その後

>> systemctl --user daemon-reload
>> systemctl restart --user test-server	

以前と同じ手順を繰り返し、プロセスの CPU 使用率を確認します。

CPU のリソース使用率は約 50% の範囲で変動しています。

子プロセスが存在する場合

server.py を変更し、子プロセスを作成しても busy loop を実行します。上記の手順を繰り返し、使用率を top コマンドで観察します。

server.py
def endless_loop():
    while True:
        pass


class MyServer(BaseHTTPRequestHandler):
    def do_GET(self):
        p = multiprocessing.Process(target=endless_loop).start()
        endless_loop()

プロセスグループ全体に制限がかけられていることがわかります。

Double Fork

ダブルフォークしてプロセスの親子関係から脱する場合はどうでしょうか? server.py の内容を変更して、前述の手順を繰り返してください。

server.py
class MyServer(BaseHTTPRequestHandler):
    def do_GET(self):
        pid = os.fork()

        if pid > 0:
            # parent
            endless_loop()
        else:
            # child
            os.chdir("/")
            os.setsid()
            os.umask(0)

            # double fork
            pid = os.fork()
            if pid > 0:
                # parent
                sys.exit(0)
            else:
                # child
                endless_loop()
	

cgroup はまだ同じです。

Delegate パラメータ

Docker の設定を参考にして

/usr/lib/systemd/system/docker.service
[Unit]
Description=Docker Application Container Engine

[Service]
# ...
Delegate=yes

[Install]
WantedBy=multi-user.target

systemd において、delegate は ServiceとScope ユニットに対して使用される特別なブール型のプロパティです。このプロパティが設定されている場合、スコープまたはサービス内のプロセスは自身の制御グループのサブツリーを制御することができます(つまり、直接 /sys/fs/cgroup を介してサブグループを作成できます)

systemd-run コマンド

systemd-runコマンドを使用すると、簡単に一時的なユニットを作成してプロセスのリソースを制限することができま

>> systemd-run --slice=user-1000.slice --property="CPUQuota=50%" --scope --user python server.py

libcgroup-tools を使用する

libcgroup は、C言語で記述されたcgroup関連のAPIを提供するライブラリであり、また、cgroupを作成、割り当て、削除するための多くのコマンドラインツールも提供しています。

下記は、libcgroupを使用してcgrouptestという名前のcgroupを作成する例です。

>> sudo cgcreate -g cpu:cgrouptest
>> sudo cgset -r cpu.max=50000 cgrouptest
>> sudo cgget -g cpu:cgrouptest
cgrouptest:
cpu.weight: 100
cpu.stat: usage_usec 0
        user_usec 0
        system_usec 0
        core_sched.force_idle_usec 0
        nr_periods 2
        nr_throttled 0
        throttled_usec 0
        nr_bursts 0
        burst_usec 0
cpu.weight.nice: 0
cpu.pressure: some avg10=0.00 avg60=0.00 avg300=0.00 total=0
        full avg10=0.00 avg60=0.00 avg300=0.00 total=0
cpu.idle: 0
cpu.max.burst: 0
cpu.max: 50000 100000
cpu.uclamp.min: 0.00
cpu.uclamp.max: max

その後、cgexec -g cpu:cgrouptest python3 server.py コマンドを実行すると、以前のsystemdユニットを使用した場合と同じ効果が得られます。

もし、systemd unit と一緒に使用する場合はどうでしょうか。

実験してみましょう。まず、ExecStart を変更しましょう。

[Unit]
Description=test-server

[Service]
ExecStart=/usr/bin/cgexec -g cpg:cgrouptest /user/bin/python3 <FILE PATH>
Restart=on-failure
RestartSec=3
CPUQuota=20%

[Install]
WantedBy=default.target

ここで使用されているのは、私たちがcgexecで指定したcgroupです。

cpulimit を使用する

このツールは、cgroup を使用して制限をかけるわけではありませんが、ここで紹介しておきます。

cpulimit コマンドの仕組みは、プロセスに CPU 利用率のしきい値を設定し、リアルタイムでプロセスがそのしきい値を超えているかどうかを監視し、超えている場合にプロセスを一時停止させることです。cpulimit は、SIGSTOP とSIGCONT の2つのシグナルを使用してプロセスを制御します。

cpulimit -l 50 python server.py

top コマンドの出力を継続的に観察すると、プロセスが頻繁に T 状態になっていることがわかります。

Discussion