SSD Nodes Learn Hosting plans →
ガイド Matt Connor著者 Matt Connor ・更新日 2026-08-26

systemdでプロセスのメモリとCPUを制限する方法

systemdのunitにMemoryHigh、MemoryMax、CPUQuota、TasksMaxを設定し、上限後もVPS全体が停止する理由とOOM killの確認方法を解説します。

systemd の drop-in でプロセスのメモリと CPU を制限する

プロセスを実行する unit に数行追加すると、Linux VPS 上でプロセスのメモリと CPU を制限できます。MemoryMax= はメモリのハード上限です。CPUQuota= はプロセッサ時間の上限です。どちらも cgroup v2(control groups、version 2)によって適用されます。これは、systemd がサーバー上の各サービスを管理するためにすでに使用しているカーネル機能です。

sudo systemctl edit myapp.service

コメント内の手順が記載された drop-in ファイルが開きます。コメントより上に次の内容を追加します。

[Service]
MemoryHigh=512M
MemoryMax=768M
MemorySwapMax=0
CPUQuota=80%
TasksMax=128
sudo systemctl daemon-reload
sudo systemctl restart myapp.service
systemctl show myapp.service -p MemoryHigh -p MemoryMax -p CPUQuotaPerSecUSec -p TasksMax

systemctl show には、指定した数値がカーネル独自の単位で表示される必要があります。単位は MemoryMax=805306368 と CPUQuotaPerSecUSec=800ms です。MemoryMax=infinity と表示された場合、drop-in は読み込まれていません。ファイルが /etc/systemd/system/myapp.service.d/override.conf に配置されていることを確認してください。また、[Service] ヘッダーで始まっていることも確認します。設定行の前にセクションがないと、systemd は Assignment outside of section. Ignoring. をログに記録し、制限なしでサービスを起動します。

このガイドの残りでは、これらの数値の選び方と、設定後も発生する問題について説明します。

メモリを使い切っていないのに暴走プロセスがVPSを停止させる理由

厳格なメモリ上限に達したプロセスが約1秒で終了し、サービスが再起動する場合は、良いケースです。悪いケースでは、何も終了しません。サーバーはpingに応答し、SSH接続も受け付けますが、シェルプロンプトが表示されません。マシンは稼働して処理を続けていますが、その処理はどれも役に立ちません。

仕組みは次のとおりです。空きメモリが少なくなると、カーネルは新しいメモリを割り当てる代わりにページを回収します。回収しやすいのはファイルにバックアップされたページです。ページキャッシュには、実行中のすべてのプログラムの実行コードが保持されています。そのためカーネルは sshd のコードページを追い出します。次に sshd が実行する命令はページフォルトになり、そのバイト列をストレージから読み戻す必要があります。すべてのプロセスが実行する代わりにディスクの処理を待つ状態になります。同じページが追い出されては戻る処理を繰り返す状態を、スラッシングと呼びます。

VPSでは、ラップトップよりもこの状態が悪化しやすい理由が2つあります。ストレージがネットワーク接続型または共有型であることが多く、ページフォルト1回あたりの待ち時間がローカルのNVMeデバイスより長くなります。また、カーネルは時間ではなく失敗を基準に判定します。ページの回収がどれほど遅くても、回収したページを返し続けている限り、カーネルは処理が進んでいると判断し、out of memory (OOM) killerを起動しません。何かがkillされるまで、サーバーがこの状態で何分も止まることがあります。

この状態は監視できます。Linux 4.20以降のカーネルは、pressure stall information (PSI)を公開します。

cat /proc/pressure/memory
cat /proc/pressure/io
some avg10=63.72 avg60=41.02 avg300=12.33 total=13729481
full avg10=48.15 avg60=30.44 avg300=8.90 total=9114233

重要なのは full の行です。full avg10=48.15 は、直近10秒間のうち48%の時間、サーバー上の実行可能なタスクが すべて メモリ処理の待機で停止し、何も実行されなかったことを意味します。正常なサーバーの full はほぼ0です。10を超えると人間にも遅さを感じられ、40以上になると一般にフリーズした状態とみなされます。

このため、上限を設定するだけでは保証になりません。MemoryHigh= の制御下にあるunitはkillされずにスロットリングされるため、動作し続けながら遅い状態になります。systemdから見ると失敗していないため、再起動も行われません。swapの使用を許可した上限付きunitでは、読み書きがそのunitに課金されますが、処理は1つの共有デバイスによって提供されます。そのため、サーバー上の他のすべてのサービスについて /proc/pressure/io を押し上げる可能性があります。上限は不足の負担者を決めるだけであり、容量を生み出すことはできません。

VPS が cgroup v2 で動作していることを確認する

stat -fc %T /sys/fs/cgroup

cgroup2fs は統合階層です。以下のすべての設定で、この階層が必要です。tmpfs の場合、サーバーは古い v1 レイアウトで起動しています。このレイアウトには MemoryHigh= と MemorySwapMax= が存在せず、ユニットごとの OOM 動作も異なります。Ubuntu 22.04 以降と Debian 11 以降では、デフォルトで v2 が使用されます。古いイメージや、systemd.unified_cgroup_hierarchy=0 を指定して起動したカーネルでは使用されません。

cgroup v2 では、systemd がデフォルトで各ユニットのメモリ accounting を有効にします。そのため、数値はすでに取得できます。

systemd-cgtop -m

このコマンドは、メモリ使用量の順に cgroup を一覧表示します。サーバーがまだ応答できるうちに、「このサーバーで何がメモリを消費しているのか」を最も早く確認できます。新しいサーバーの場合は、この作業より先に、新しい VPS で最初の 10 分間に行う作業にあるアカウント設定とファイアウォール設定を実施します。

MemoryHigh によるスロットリングと MemoryMax による強制終了

2 つのメモリ設定の違いによって、障害時の動作が決まります。

  • MemoryHigh= はソフト上限です。この値を超えると、kernel はその cgroup から積極的にメモリを回収し、割り当てを意図的に遅くします。使用量が値を超えることはありますが、プロセスは強制終了されません。
  • MemoryMax= はハード上限です。この上限内でメモリ割り当てを満たせない場合、OOM killer が その cgroup 内で 実行され、その unit 自身のプロセスを 1 つ強制終了します。

MemoryMax= を、完全には信頼できない対象に設定する本当の理由は後半にあります。上限がない場合、メモリ不足はホスト全体の問題になり、global OOM killer は oom_score に基づいて対象を選びます。多くの場合、これは最大のプロセスを意味します。最大のプロセスは通常、メモリリークを起こした script ではなく database です。上限を設定すると、原因となった unit の内部で強制終了が発生します。

両方を設定し、MemoryHigh= は MemoryMax= より 20 to 30 パーセント程度低くします。この差が警告ゾーンになります。緩やかなリークは High を超え、サービスの動作が遅くなって現れます。一方、急激なスパイクは Max を直接超え、プロセスが強制終了されます。

パーセント値は搭載された物理メモリを基準に解釈されます。そのため、4 GB plan では MemoryMax=25% は 1 GB となり、plan のサイズを変更した後もホスト全体の 4 分の 1 のままです。MemorySwapMax=0 を設定すると、その unit は swap を完全に使用しなくなります。これにより、長時間の低速化ではなく、速やかで明確な強制終了になります。

いくつかのサービスでは、使用量を測定する代わりに、必要なメモリ量を事前に決められます。Ollama unit は指定した context window に基づいて KV cache のサイズを決めます。そのため、unit の上限を決める前に num_ctx を増やすと RAM がどれだけ必要になるか を確認してください。

上限を設定する場合は、restart policy も併せて設定してください。そうしないと、強制終了後にサービスが停止したままになります。

[Unit]
StartLimitIntervalSec=300
StartLimitBurst=5

[Service]
Restart=on-failure
RestartSec=5s

StartLimit* は [Unit] に、Restart= は [Service] に記述します。どちらかを誤った section に記述すると、systemd は無視します。5 分間に 5 回の restart は一時的な問題ではなく、リークと判断できます。その後 systemd は再起動を諦め、unit を failed 状態のままにします。これは、問題を隠す crash loop ではなく、後から確認したい状態です。

CPUQuota で CPU 使用率を制限するか、CPUWeight で CPU 時間を分配する

CPUQuota= は、1 個の CPU で利用可能な時間の割合を指定します。CPUQuota=50% は 1 コアの半分です。CPUQuota=200% は 2 コア相当で、unit は必要に応じて複数のスレッドに処理を分散できます。2 vCPU プランでは、CPUQuota=200% がマシン全体に相当します。

ほとんどのサービスでは、CPUWeight= を既定値として使用する方が適しています。これは 1 から 10000 までの相対的な配分で、kernel の既定値は 100 です。CPU を他の処理と競合するときだけ効果があります。負荷がかかっている場合、CPUWeight=20 のバックアップジョブは 100 の Web サーバーに CPU 時間を譲ります。一方、マシンがアイドル状態なら、CPU 全体を使用できます。ハードな quota を設定すると、このアイドル時の余力が失われます。

CPU 制限で得られる効果を正しく理解してください。CPU 負荷が高いプロセスだけで Linux が停止することは、ほとんどありません。scheduler がすべてのプロセスに CPU 時間を割り当て続けるためです。マシンを停止させる原因になるのは、メモリであることが多いです。予測可能な上限が必要な場合は CPUQuota= を使用します。たとえば、何も制限しなければ 1 時間にわたって CPU を使い切る build や agent が該当します。この種のワークロードに必要なサイズの決め方は別の問題であり、coding agent に必要な VPS の RAM と CPU 容量で説明しています。

どのプロセスもあまり CPU を使用していないのに CPU 使用率が高い場合、原因は hypervisor の反対側にある可能性があります。これは 負荷の高い近隣ユーザーによる CPU steal time であり、設定した quota では変えられません。

TasksMax で fork ループを停止する

TasksMax= は、unit が保持できるプロセスとスレッドの数です。スレッドも数に含まれるため、Java や Go のサービスには、プロセス一覧から想定される数より大きな余裕が必要です。fork をループするスクリプトに対する最も低コストな保護策です。unit 内で fork が失敗するため、システム全体がプロセス ID を使い果たす事態を防げます。

TasksMax=128

unit が上限に達すると、kernel は cgroup 名を示す行をログに記録します。

cgroup: fork rejected by pids controller in /system.slice/myapp.service

通常、プログラム自体は fork: retry: Resource temporarily unavailable を報告します。manager がデフォルトで適用する値は systemctl show -p DefaultTasksMax で確認します。

systemd-run で単発ジョブを制限する

これらを使用するために unit file は必要ありません。systemd-run は、単一のコマンド用に一時的な unit を作成します。

sudo systemd-run --scope -p MemoryMax=1G -p MemorySwapMax=0 -p CPUQuota=50% -p TasksMax=64 ./import-data.sh

--scope は Running scope as unit: run-r7c1a....scope を表示した後、端末内でコマンドを実行します。出力は画面に表示され、コマンドの終了時に制限は解除されます。systemd.resource-control のプロパティは、-p の後に指定できます。

長時間実行するジョブでは、--scope を省略して名前を付けます。ジョブは一時的なサービスとしてバックグラウンドで実行され、journal にログが記録されます。

sudo systemd-run --unit=nightly-import -p MemoryMax=1G -p CPUWeight=20 ./import-data.sh
journalctl -u nightly-import -f

root でない場合も、--user で同じオプションを使用できます。ただし、ユーザー manager が利用できるのは委譲された controller だけであるため、プロパティが拒否されることがあります。その場合は sudo を付けて実行します。ジョブを永続的に管理する段階になったら、設定をそのまま実際の unit に移します。systemd の service と timer としてスクリプトを実行する を参照してください。

Swap に関する質問への率直な回答

Swap は障害を防ぐのではなく、障害の形を変えます。

Swap がない場合、メモリリークが上限に達すると、数秒以内に何かが停止します。障害は明確で短時間に収まり、その後に journal を確認すれば状況を読み取りやすくなります。Swap がある場合、kernel は使用頻度の低い anonymous page をディスクへ書き出し、時間を稼ぎます。プロセスの使用量がやがて落ち着くなら、Swap が役立ちます。暴走するプロセスの場合、Swap は 5 秒の障害を 20 分間の停止状態に変えます。この停止状態のほうが深刻です。停止したプロセスなら shell は引き続き使えますが、thrashing が発生したサーバーでは使えないためです。

swapon --show
free -h

小規模な VPS では、次の構成が現実的です。1 回だけ割り当てられ、その後は使われないページ用に、控えめなサイズの swap file を用意します。そのうえで、失ってもよい unit に MemorySwapMax=0 を設定します。重要なサービスでは Swap を使用できるようにします。予測できないサービスは早く上限に達して再起動します。

vm.swappiness を下げる方法は効果が限定的です。その理由を理解しておく必要があります。この値は、page cache を追い出すか anonymous page を swap するかのバランスを変えるだけで、どちらの場合も後でディスクからの読み取りが発生します。どのページが thrashing するかは変わりますが、サーバー自体が thrashing するかどうかは変わりません。

早期 OOM デーモンで停止前にプロセスを終了する

カーネルは reclaim が完全に失敗するまで待機します。小規模な VPS では、その待機時間がマシンを失う正確な時間帯になります。userspace デーモンを使用すると、メモリを監視して早めにプロセスを終了できます。

earlyoom は利用可能メモリと空き swap を監視します。いずれかがしきい値を下回ると、スコアが最も高いプロセスを終了します。

sudo apt install earlyoom
systemctl status earlyoom

Debian と Ubuntu のパッケージは、インストール時にサービスを起動します。オプションは /etc/default/earlyoom にあります。

EARLYOOM_ARGS="-m 5,2 -s 5,2 --avoid '^(sshd|systemd)$' --prefer '^(node|python3)$'"

-m PERCENT は利用可能メモリの最小値を設定し、-s PERCENT は空き swap の最小値を設定します。どちらもデフォルトは 10 percent です。各組の 2 番目の値は SIGKILL のしきい値です。最初の値を下回ると earlyoom は SIGTERM を送信し、2 番目の値を下回ると SIGKILL を送信します。2 番目の値はデフォルトで最初の値の半分です。sudo systemctl restart earlyoom で変更を適用し、journalctl -u earlyoom で終了したプロセスと、そのプロセスが保持していたメモリ量を確認します。

systemd-oomd はもう一つの選択肢です。マニュアルページでは、これを「cgroups-v2 と pressure stall information (PSI) を使用して、kernel space で OOM が発生する前に監視と是正措置を行う system service」と説明しています。単一のプロセスではなく cgroup 全体を対象にするため、個別の子プロセスではなく unit を終了します。unit は ManagedOOMMemoryPressure=kill または ManagedOOMSwap=kill で対象に追加し、しきい値は /etc/systemd/oomd.conf に設定します。

systemctl status systemd-oomd
oomctl

oomctl は現在監視している対象を表示します。unit ごとに対象を追加する設定のため、サーバーイメージでは何も表示されないことがよくあります。デーモンは 1 つだけ選んでください。両方を実行すると、2 つのデーモンが終了対象の選択を競合し、終了理由の特定が難しくなります。

担当した unit はどれか

まず kernel を確認します。kernel は実行した kill をすべて記録するためです。

journalctl -k --grep "Killed process" --since "2 hours ago"

global OOM killer による kill は、次のように記録されます。

Out of memory: Killed process 4127 (node) total-vm:2731084kB, anon-rss:1874232kB, file-rss:0kB, shmem-rss:0kB, UID:1000 pgtables:4212kB oom_score_adj:0

anon-rss は、プロセスが終了した時点で RAM に保持していたメモリ量です。この例では約 1.8 GB です。角括弧内の名前はそのまま信用しないでください。そこに表示されるのは kernel が選んだ被害プロセスです。kernel は最大のプロセスを選ぶため、メモリ不足を引き起こしたプロセスとは限りません。

cgroup の上限による kill には別のプレフィックスが付き、その直前に出力されるレポートには、独自の上限に達した cgroup が示されます。

Memory cgroup out of memory: Killed process 8811 (python3) total-vm:1044320kB, anon-rss:769112kB, file-rss:0kB, shmem-rss:0kB, UID:998 pgtables:1720kB oom_score_adj:0

このプレフィックスだけで、診断の大部分ができます。Memory cgroup out of memory は、1 つの unit が設定した MemoryMax= に達し、システムの他の部分には問題がなかったことを意味します。単純な Out of memory は、マシン全体のメモリが不足したことを意味します。この場合、設定した上限がないか、合計すると大きすぎます。

次に、systemd が確認した内容を調べます。

systemctl status myapp.service
journalctl -u myapp.service -n 50
myapp.service: A process of this unit has been killed by the OOM killer.
myapp.service: Main process exited, code=killed, status=9/KILL
myapp.service: Failed with result 'oom-kill'.

systemctl status は、Active: failed (Result: oom-kill) として同じ内容を 1 行で示します。

3 つ目の情報源は cgroup のカウンターです。スロットリングを記録する唯一の情報源でもあります。スロットリングはログ行を一切出力しません。

cat /sys/fs/cgroup/system.slice/myapp.service/memory.events
cat /sys/fs/cgroup/system.slice/myapp.service/memory.peak
low 0
high 4213
max 118
oom 12
oom_kill 12

high は、unit が MemoryHigh= を超えてスロットリングされた回数です。max はハード上限に達した回数、oom_kill は実際に kill されたプロセス数です。oom_kill 0 を伴う high が大きい場合、前述した無言のケースに該当します。サービスは動作を続けていますが、処理が極端に遅くなり、失敗を誰にも報告していません。memory.peak(Linux 5.19 以降)には、cgroup が到達した最大使用量が格納されます。MemoryMax= はこの値を基準にサイズを決めます。unit が再起動すると systemd が cgroup を再作成するため、どちらのファイルもリセットされます。

このすべての基盤となる前提条件があります。/var/log/journal が存在しない場合、journal は RAM 上に保存されます。システムを復旧するために必要だった再起動後には、すべての行が失われます。

sudo mkdir -p /var/log/journal
sudo systemd-tmpfiles --create --prefix /var/log/journal
sudo systemctl restart systemd-journald
journalctl --list-boots

journalctl --list-boots が現在の boot より多くの boot を示していれば、履歴が保持されています。そのため journalctl -k -b -1 で、停止した boot の kernel メッセージを表示できます。

小規模な VPS の出発点

2 GB プランでは、カーネルとページキャッシュ用に 300〜400 MB を残します。各ユニットが同時にピークへ達する可能性があるため、上限の合計を 2 GB 全体に設定しないでください。重要なサービスに最大の割り当てを与え、その周辺の不確実な処理には上限を設定します。

[Service]
MemoryHigh=256M
MemoryMax=384M
MemorySwapMax=0
CPUWeight=20
TasksMax=64
Restart=on-failure
RestartSec=5s

接続手段を確保するために、設定をもう 1 つ追加する価値があります。OOMScoreAdjust=-500 を ssh.service の drop-in に設定すると、グローバル OOM killer が SSH daemon を終了対象に選ぶ可能性を大幅に下げられます。これは、サーバーを復旧できるか、control panel から再起動するしかないかを分ける違いになります。この設定が変更するのは、カーネルが終了対象を選ぶ方法だけです。停止時間が短くなるわけではありません。

コンテナは専用の cgroup で実行されます。これらは unit file ではなく、コンテナランタイムが作成します。そのため、docker.service に設定した上限は、個々のコンテナの上限にはなりません。MemoryMax= と CPUQuota= に対応するコンテナ単位の設定については、Docker Compose でメモリと CPU の上限を設定するで説明しています。

FAQ

VPS が暴走プロセスを強制終了せずフリーズしたのはなぜですか?

カーネルは、回収によってページを解放できたかどうかで処理の進行を判断するためです。メモリが不足すると、実行中のプログラムの実行可能ページを含むページキャッシュを追い出し、次の命令実行時に再び読み込みます。すべての処理がストレージ待ちになりますが、割り当てが技術的に失敗したわけではないため、OOM killer は呼び出されません。発生中に /proc/pressure/memory を確認してください。full avg10 が 40 を超えている場合、直近の 10 秒間にほとんどのタスクが実行されていません。earlyoom のような userspace デーモンを使うと、システムがその状態に達する前にプロセスを強制終了できます。

MemoryHigh と MemoryMax の違いは何ですか?

MemoryHigh= は処理を抑制するソフト上限です。カーネルは unit から積極的にメモリを回収し、割り当てを遅くしますが、使用量はその値を超えることがあり、プロセスも強制終了されません。MemoryMax= はハード上限です。この上限の範囲内で満たせないメモリ割り当てが発生すると、その unit 自身の cgroup 内で OOM killer が起動します。そのため、問題を起こしたプロセスが終了し、システム全体で最大のプロセスが終了することはありません。MemoryHigh= は MemoryMax= より小さく設定し、その差を警告領域として扱ってください。

OOM killer が終了させたサービスを確認するにはどうすればよいですか?

journalctl -k --grep "Killed process" --since "2 hours ago" を実行します。Memory cgroup out of memory で始まる行は、1 つの unit が独自の MemoryMax= に達したことを示します。一方、通常の Out of memory はマシン全体のメモリが枯渇したことを示します。次に journalctl -u <unit> -n 50 を実行し、Failed with result 'oom-kill' を探します。サーバーに /var/log/journal が存在しない場合、journal は RAM 上に保持されており、再起動時に記録も失われています。次の障害が発生する前に、そのディレクトリを作成してください。

小規模な VPS に swap を追加すべきですか?

小さな swap ファイルは、1 回だけ割り当てられ、その後ほとんど使用されないコールドページには有効です。暴走プロセスには効果がありません。強制終了までの時間が延び、ログインして修正できない短時間の停止が、長時間のハングに変わるだけです。swap は控えめな容量にし、失ってもよい unit に MemorySwapMax=0 を設定してください。対象の unit は上限に達するとすぐ再起動し、重要なサービスは swap を使い続けられます。

unit ファイルを書かずにコマンドを制限できますか?

はい。sudo systemd-run --scope -p MemoryMax=1G -p CPUQuota=50% ./script.sh は、指定した制限を適用した一時的な scope 内で、ターミナルからコマンドを実行します。制限はコマンドの終了時に消えます。systemd.resource-control のすべてのプロパティは -p の後に利用できるため、MemorySwapMax=、TasksMax=、CPUWeight= も使用できます。--scope を削除して --unit=name を追加すると、ジョブをバックグラウンドで実行し、その出力を journal に記録できます。