web-dev-qa-db-ja.com

単一のLinuxプロセスのメモリ使用量を制限する

ユーザー提供のPDFを300DPI画像に変換するためにpdftoppmを実行しています。これは、ユーザーがPDF =非常に大きなページサイズ。pdftoppmは、そのサイズの300DPIイメージをメモリに保持するのに十分なメモリを割り当てます。100インチの正方形のページの場合、100 * 300 * 100 * 300 *ピクセルあたり4バイトです= 3.5GB。悪意のあるユーザーが私に愚かな大きなPDFを与えるだけで、あらゆる種類の問題を引き起こす可能性があります。

だから私がしたいのは、実行しようとしている子プロセスのメモリ使用量に何らかのハード制限を課すことです。たとえば、500MBを超えるメモリを割り当てようとする場合は、プロセスを停止させてください。それは可能ですか?

これにはulimitは使用できないと思いますが、1プロセスの同等機能はありますか?

172
Ben Dilts

Ulimitにはいくつかの問題があります。以下は、このトピックに関する有用な資料です。 Linuxでのプログラムの時間とメモリの消費を制限する 。これにより、プロセス(およびそのフォーク)をケージできる timeout ツールが表示されます。 )時間またはメモリ消費量。

タイムアウトツールには、Perl 5+と/procファイルシステムがマウントされている必要があります。その後、ツールをたとえばにコピーします。 /usr/local/binのように:

curl https://raw.githubusercontent.com/pshved/timeout/master/timeout | \
  Sudo tee /usr/local/bin/timeout && Sudo chmod 755 /usr/local/bin/timeout

その後、あなたの質問のように、メモリ消費によってプロセスを「ケージ」することができます:

timeout -m 500 pdftoppm Sample.pdf

または、-t <seconds>および-x <hertz>を使用して、時間またはCPUの制約によってプロセスをそれぞれ制限することもできます。

このツールが機能する方法は、生成されたプロセスがその設定された境界をオーバーサブスクライブしていないかどうかを1秒あたり複数回チェックすることです。つまり、実際には小さなウィンドウがあり、タイムアウトが通知されてプロセスが強制終了する前に、プロセスが潜在的にオーバーサブスクライブする可能性があります

したがって、より適切なアプローチにはcgroupが含まれる可能性がありますが、DockerまたはrunCを使用する場合でも、cgroupに関するよりユーザーフレンドリーな抽象化を提供する場合でも、セットアップははるかに複雑になります。

68
kvz

これを制限する別の方法は、Linuxのコントロールグループを使用することです。これは、プロセス(またはプロセスのグループ)の物理メモリの割り当てを仮想メモリとは別に制限する場合に特に便利です。例えば:

cgcreate -g memory:myGroup
echo 500M > /sys/fs/cgroup/memory/myGroup/memory.limit_in_bytes
echo 5G > /sys/fs/cgroup/memory/myGroup/memory.memsw.limit_in_bytes

myGroupという名前のコントロールグループを作成し、myGroupで実行される一連のプロセスに最大500 MBの物理メモリと最大5000 MBのスワップを制限します。制御グループの下でプロセスを実行するには:

cgexec -g memory:myGroup pdftoppm

最新のUbuntuディストリビューションでは、この例ではcgroup-binパッケージをインストールし、/etc/default/grubを編集してGRUB_CMDLINE_LINUX_DEFAULTを次のように変更する必要があることに注意してください。

GRUB_CMDLINE_LINUX_DEFAULT="cgroup_enable=memory swapaccount=1"

次にSudo update-grubを実行し、再起動して新しいカーネルブートパラメータでブートします。

129
user65369

プロセスがメモリを最も多く消費する子を生成しない場合は、 setrlimit 関数を使用できます。そのためのより一般的なユーザーインターフェイスは、シェルのulimitコマンドを使用することです。

$ ulimit -Sv 500000     # Set ~500 mb limit
$ pdftoppm ...

これは、プロセスの「仮想」メモリのみを制限し、呼び出されるプロセスが他のプロセスと共有するメモリ、およびマップされているが予約されていないメモリ(Javaの大きなヒープなど)を考慮し、制限します。それでも、仮想メモリは、非常に大きくなるプロセスに最も近いため、前述のエラーは重要ではありません。

プログラムが子を生成し、メモリを割り当てるのが子である場合、それはより複雑になり、制御下でプロセスを実行するための補助スクリプトを記述する必要があります。私はブログに whyhow と書いた。

81
P Shved

Systemdベースのディストリビューションでは、systemd-runを介してcgroupを間接的に使用することもできます。例えば。 pdftoppmを500MのRAMに制限する場合は、次を使用します。

systemd-run --scope -p MemoryLimit=500M pdftoppm

注:これはパスワードを要求しますが、アプリはユーザーとして起動されます。これは、コマンドがSudoを必要とすると誤解させないでください。これは、コマンドをrootの下で実行することになるため、ほとんど意図していませんでした。

パスワードを入力したくない場合(結局のところ、あなたが自分のメモリを所有しているユーザーとして、それを制限するためにパスワードが必要なのはなぜですか)、あなた--userオプションを使用できますが、これを機能させるには、cgroupsv2サポートを有効にする必要があります。これは、現在 systemd.unified_cgroup_hierarchyカーネルパラメータでブートする必要があります です。

15
Hi-Angel

以下のスクリプトを使用しています。 cgmanagerを通じてcgroupを使用します。 更新:cgroup-toolsからのコマンドを使用するようになります。このスクリプトにlimitmemという名前を付けて$ PATHに配置すると、limitmem 100M bashのように使用できます。これにより、メモリとスワップの両方の使用が制限されます。メモリのみを制限するには、memory.memsw.limit_in_bytesの行を削除します。

編集:デフォルトのLinuxインストールでは、これはメモリの使用のみを制限し、スワップの使用は制限しません。スワップ使用制限を有効にするには、Linuxシステムでスワップアカウンティングを有効にする必要があります。 swapaccount=1/etc/default/grubを設定/追加して、次のようにします。

GRUB_CMDLINE_LINUX="swapaccount=1"

次にSudo update-grubを実行して再起動します。

免責事項:cgroup-toolsも将来壊れても驚かないでしょう。正しい解決策はcgroupの管理にsystemd apiを使用することですが、そのa.t.m用のコマンドラインツールはありません。

#!/bin/sh

# This script uses commands from the cgroup-tools package. The cgroup-tools commands access the cgroup filesystem directly which is against the (new-ish) kernel's requirement that cgroups are managed by a single entity (which usually will be systemd). Additionally there is a v2 cgroup api in development which will probably replace the existing api at some point. So expect this script to break in the future. The correct way forward would be to use systemd's apis to create the cgroups, but afaik systemd currently (feb 2018) only exposes dbus apis for which there are no command line tools yet, and I didn't feel like writing those.

# strict mode: error if commands fail or if unset variables are used
set -eu

if [ "$#" -lt 2 ]
then
    echo Usage: `basename $0` "<limit> <command>..."
    echo or: `basename $0` "<memlimit> -s <swaplimit> <command>..."
    exit 1
fi

cgname="limitmem_$$"

# parse command line args and find limits

limit="$1"
swaplimit="$limit"
shift

if [ "$1" = "-s" ]
then
    shift
    swaplimit="$1"
    shift
fi

if [ "$1" = -- ]
then
    shift
fi

if [ "$limit" = "$swaplimit" ]
then
    memsw=0
    echo "limiting memory to $limit (cgroup $cgname) for command $@" >&2
else
    memsw=1
    echo "limiting memory to $limit and total virtual memory to $swaplimit (cgroup $cgname) for command $@" >&2
fi

# create cgroup
Sudo cgcreate -g "memory:$cgname"
Sudo cgset -r memory.limit_in_bytes="$limit" "$cgname"
bytes_limit=`cgget -g "memory:$cgname" | grep memory.limit_in_bytes | cut -d\  -f2`

# try also limiting swap usage, but this fails if the system has no swap
if Sudo cgset -r memory.memsw.limit_in_bytes="$swaplimit" "$cgname"
then
    bytes_swap_limit=`cgget -g "memory:$cgname" | grep memory.memsw.limit_in_bytes | cut -d\  -f2`
else
    echo "failed to limit swap"
    memsw=0
fi

# create a waiting Sudo'd process that will delete the cgroup once we're done. This prevents the user needing to enter their password to Sudo again after the main command exists, which may take longer than Sudo's timeout.
tmpdir=${XDG_RUNTIME_DIR:-$TMPDIR}
tmpdir=${tmpdir:-/tmp}
fifo="$tmpdir/limitmem_$$_cgroup_closer"
mkfifo --mode=u=rw,go= "$fifo"
Sudo -b sh -c "head -c1 '$fifo' >/dev/null ; cgdelete -g 'memory:$cgname'"

# spawn subshell to run in the cgroup. If the command fails we still want to remove the cgroup so unset '-e'.
set +e
(
set -e
# move subshell into cgroup
Sudo cgclassify -g "memory:$cgname" --sticky `sh -c 'echo $PPID'`  # $$ returns the main Shell's pid, not this subshell's.
exec "$@"
)

# grab exit code 
exitcode=$?

set -e

# show memory usage summary

peak_mem=`cgget -g "memory:$cgname" | grep memory.max_usage_in_bytes | cut -d\  -f2`
failcount=`cgget -g "memory:$cgname" | grep memory.failcnt | cut -d\  -f2`
percent=`expr "$peak_mem" / \( "$bytes_limit" / 100 \)`

echo "peak memory used: $peak_mem ($percent%); exceeded limit $failcount times" >&2

if [ "$memsw" = 1 ]
then
    peak_swap=`cgget -g "memory:$cgname" | grep memory.memsw.max_usage_in_bytes | cut -d\  -f2`
    swap_failcount=`cgget -g "memory:$cgname" |grep memory.memsw.failcnt | cut -d\  -f2`
    swap_percent=`expr "$peak_swap" / \( "$bytes_swap_limit" / 100 \)`

    echo "peak virtual memory used: $peak_swap ($swap_percent%); exceeded limit $swap_failcount times" >&2
fi

# remove cgroup by sending a byte through the pipe
echo 1 > "$fifo"
rm "$fifo"

exit $exitcode
10
JanKanis

Mark Johnsonが提案したdaemontoolsのツールに加えて、chpstにあるrunitも検討できます。 Runit自体はbusyboxにバンドルされているため、すでにインストールされている可能性があります。

chpstのmanページ はオプションを示しています。

-mバイトはメモリを制限します。データセグメント、スタックセグメント、ロックされた物理ページ、およびプロセスごとのすべてのセグメントの合計をそれぞれバイトバイトに制限します。

7
Oz123

私はUbuntu 18.04.2 LTSを実行しています。JanKanisスクリプトは、彼が示唆するように私にはまったく機能しません。 limitmem 100M scriptを実行すると、100 MBのRAM with nlimited swap)が制限されます。

limitmem 100M -s 100M scriptにはcgget -g "memory:$cgname"という名前のパラメーターがないため、memory.memsw.limit_in_bytesの実行はサイレントに失敗します。

だから私はスワップを無効にしました:

# create cgroup
Sudo cgcreate -g "memory:$cgname"
Sudo cgset -r memory.limit_in_bytes="$limit" "$cgname"
Sudo cgset -r memory.swappiness=0 "$cgname"
bytes_limit=`cgget -g "memory:$cgname" | grep memory.limit_in_bytes | cut -d\  -f2`
4
d9ngle