Linux は「RAM 不足」になることがありますか?

Linux は「RAM 不足」になることがありますか?

ホストされている VPS が RAM を大量に使用したために予期せずプロセスを強制終了したという苦情を述べている投稿を Web 上でいくつか見かけました。

どうしてこんなことが可能なのでしょうか? 最近の OS はすべて、物理 RAM を超えるデータに対してディスク スワップを使用するだけで「無限の RAM」を提供すると考えていました。これは正しいでしょうか?

プロセスが「RAM 不足のため強制終了」された場合、何が起こるのでしょうか?

答え1

プロセスが「RAM 不足のため強制終了」された場合、何が起こるのでしょうか?

Linuxはデフォルトではアプリケーションコードからのメモリ要求を拒否しないと言われることがありますmalloc()。1これは実際には正しくありません デフォルトではヒューリスティックを使用して、

アドレス空間の明らかなオーバーコミットは拒否されます。一般的なシステムに使用されます。オーバーコミットによってスワップ使用量を削減しながら、深刻な乱暴な割り当てが失敗することを保証します。

(引用はすべて 3.11 ツリーから[linux_src]/Documentation/vm/overcommit-accounting)。「かなり乱暴な割り当て」とみなされるものが正確に何であるかは明示されていないため、詳細を判断するにはソースを調べる必要があります。また、脚注 2(下記)の実験的方法を使用して、ヒューリスティックの反映を試みることができます。これに基づいて、私の最初の経験的観察は、理想的な状況(システムがアイドル状態)では、スワップがない場合は RAM の約半分を割り当てることができ、スワップがある場合は RAM の約半分とスワップのすべてを割り当てることができるというものです。これは、多かれ少なかれプロセスごと(ただし、この制限に注意してください動的であり、状態によって変化する可能性がある(脚注 5 のいくつかの考察を参照)。

RAM の半分とスワップは、 の「CommitLimit」フィールドのデフォルトとして明示的に設定されています/proc/meminfo。これは次の意味です。ただし、これは実際には先ほど説明した制限 ( から) とは何の関係もありません[src]/Documentation/filesystems/proc.txt

コミット制限:オーバーコミット率('vm.overcommit_ratio')に基づいて、現在割り当て可能なメモリの合計量です。システム上。この制限は、厳密なオーバーコミット アカウンティングが有効になっている場合 ('vm.overcommit_memory' のモード 2) のみ適用されます。CommitLimit は次の式で計算されます: CommitLimit = ('vm.overcommit_ratio' * 物理 RAM) + スワップ たとえば、'vm.overcommit_ratio' が 30 のシステムで、物理 RAM が 1G、スワップが 7G の場合、CommitLimit は 7.3G になります。

前に引用したオーバーコミット アカウンティングのドキュメントでは、デフォルトはvm.overcommit_ratio50 であると述べられています。したがってsysctl vm.overcommit_memory=2、 の場合は、vm.covercommit_ratio (を使用sysctl) を調整して結果を確認できます。3が強制されず、「アドレス空間の明らかなオーバーコミットのみが拒否される」 場合のデフォルト モードはCommitLimit、 の場合ですvm.overcommit_memory=0

デフォルトの戦略では、プロセスごとにヒューリスティックな制限を設けて「過度な割り当て」を防止していますが、システム全体としては、割り当てに関して過度な割り当てを自由に行えるようになっています。4これ は、ある時点でメモリが不足し、いくつかのプロセスに対して破産を宣言しなければならない可能性があることを意味します。OOMキラー

OOM キラーは何を殺すのでしょうか? メモリがないのにメモリを要求したプロセスを殺すとは限りません。それは必ずしも本当に問題のあるプロセスではないからです。さらに重要なことは、必ずしもシステムを現在の問題から最も迅速に解決するプロセスではないということです。

これは以下から引用したものですこここれはおそらく 2.6.x のソースを引用しています:

/*
 * oom_badness - calculate a numeric value for how bad this task has been
 *
 * The formula used is relatively simple and documented inline in the
 * function. The main rationale is that we want to select a good task
 * to kill when we run out of memory.
 *
 * Good in this context means that:
 * 1) we lose the minimum amount of work done
 * 2) we recover a large amount of memory
 * 3) we don't kill anything innocent of eating tons of memory
 * 4) we want to kill the minimum amount of processes (one)
 * 5) we try to kill the process the user expects us to kill, this
 *    algorithm has been meticulously tuned to meet the principle
 *    of least surprise ... (be careful when you change it)
 */

これは妥当な根拠のように思えます。しかし、詳細に検討しなくても、#5 (#1 と重複) は実装上、受け入れがたいように思われ、#3 は #2 と重複しています。したがって、これを #2/3 と #4 に削減することを検討しても意味があるかもしれません。

最近のソース (3.11) を grep で調べたところ、このコメントがその間に変更されていることに気付きました。

/**
 * oom_badness - heuristic function to determine which candidate task to kill
 *
 * The heuristic for determining which task to kill is made to be as simple and
 * predictable as possible.  The goal is to return the highest value for the
 * task consuming the most memory to avoid subsequent oom failures.
 */

これは、#2 についてもう少し具体的に説明します。「目標は、その後の oom 障害を回避するために、最も多くのメモリを消費しているタスクを [kill] することです。」そして暗黙的に#4(「私たちは最小限のプロセスを停止したいのです(1つ))

OOM キラーの動作を確認したい場合は、脚注 5 を参照してください。


1ありがたいことにジルが私から取り除いてくれた妄想。コメントを参照してください。


2以下は、メモリの要求がいつ失敗するかを判断するために、メモリのチャンクを徐々に大きくしていく簡単な C 言語です。

#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>

#define MB 1 << 20

int main (void) {
    uint64_t bytes = MB;
    void *p = malloc(bytes);
    while (p) {
        fprintf (stderr,
            "%lu kB allocated.\n",
            bytes / 1024
        );
        free(p);
        bytes += MB;
        p = malloc(bytes);
    }
    fprintf (stderr,
        "Failed at %lu kB.\n",
        bytes / 1024
    );
    return 0;
}            

C を知らない場合は、これをコンパイルしてgcc virtlimitcheck.c -o virtlimitcheck、 を実行できます./virtlimitcheck。プロセスは要求するスペースをまったく使用しないため、つまり、実際には RAM をまったく使用しないため、完全に無害です。

4 GB システムと 6 GB のスワップを備えた 3.11 x86_64 システムでは、約 7400000 kB で失敗しました。この数値は変動するため、状態が要因である可能性があります。これは偶然にも の に近いですCommitLimit/proc/meminfo、これを で変更してもvm.overcommit_ratio違いはありません。ただし、64 MB のスワップを備えた 3.6.11 32 ビット ARM 448 MB システムでは、約 230 MB で失敗します。最初のケースでは量が RAM の量のほぼ 2 倍であるのに対し、2 番目のケースでは約 1/4 であるため、これはスワップの量が要因であることを強く示唆しているため、興味深いことです。これは、最初のシステムでスワップをオフにすることで確認されました。その場合、失敗しきい値は約 1.95 GB に下がり、小さな ARM ボックスと非常によく似た比率になりました。

しかし、これは本当にプロセスごとに行われるのでしょうか? どうやらそうであるようです。以下の短いプログラムは、ユーザー定義のメモリ チャンクを要求し、成功した場合はリターン キーが押されるまで待機します。この方法では、複数のインスタンスを同時に試すことができます。

#include <stdio.h>
#include <stdlib.h>

#define MB 1 << 20

int main (int argc, const char *argv[]) {
    unsigned long int megabytes = strtoul(argv[1], NULL, 10);
    void *p = malloc(megabytes * MB);
    fprintf(stderr,"Allocating %lu MB...", megabytes);
    if (!p) fprintf(stderr,"fail.");
    else {
        fprintf(stderr,"success.");
        getchar();
        free(p);
    }
    return 0;
}

ただし、使用状況に関係なく、RAM とスワップの量に厳密に関係するわけではないことに注意してください。システム状態の影響に関する観察については、脚注 5 を参照してください。


3は CommitLimit、許可されているアドレス空間の量を指しますシステムvm.overcommit_memory = 2 の場合。おそらく、割り当てることができる量は、すでにコミットされている量 (明らかにフィールド) を差し引いた量になりますCommitted_AS

これを実証する興味深い実験としては、#include <unistd.h>virtlimitcheck.c の先頭 (脚注 2 を参照) とループfork()の直前に次のコードを追加することwhile()が考えられます。面倒な同期を行わずにここで説明したとおりに動作する保証はありませんが、動作する可能性は十分にあります (結果は人によって異なります)。

> sysctl vm.overcommit_memory=2
vm.overcommit_memory = 2
> cat /proc/meminfo | grep Commit
CommitLimit:     9231660 kB
Committed_AS:    3141440 kB
> ./virtlimitcheck 2&> tmp.txt
> cat tmp.txt | grep Failed
Failed at 3051520 kB.
Failed at 6099968 kB.

これは理にかなっています。tmp.txt を詳しく見ると、プロセスが交互に割り当てを大きくしているのがわかります (出力に pid を入れると簡単になります)。明らかに、一方のプロセスが十分な割り当てを獲得して、もう一方のプロセスが失敗するまで続きます。勝者は、CommitLimitマイナスまでのすべてを自由に取得できますCommitted_AS


4この時点で、仮想アドレスとデマンドページングをまだ理解していないのであれば、そもそもオーバーコミットメントを可能にするのは、カーネルがユーザーランドプロセスに割り当てるのは物理メモリではなく、仮想アドレス空間たとえば、プロセスが何かのために10MBを予約した場合、それは一連の(仮想)アドレスとして配置されますが、それらのアドレスはまだ物理メモリに対応していません。そのようなアドレスにアクセスすると、ページフォールトそしてカーネルはそれを実際のメモリにマッピングして、実際の値を格納できるようにします。プロセスは通常、実際にアクセスするよりもはるかに多くの仮想空間を予約するため、カーネルは RAM を最も効率的に使用できます。ただし、物理メモリは依然として有限のリソースであり、物理メモリのすべてが仮想アドレス空間にマッピングされると、一部の仮想アドレス空間を削除して RAM を解放する必要があります。


5最初警告: これを で試す場合はvm.overcommit_memory=0、まず作業内容を保存し、重要なアプリケーションをすべて閉じてください。システムが約 90 秒間フリーズし、一部のプロセスが停止します。

アイデアは、フォーク爆弾90 秒後にタイムアウトし、フォークによってスペースが割り当てられ、その一部が大量のデータを RAM に書き込み、その間ずっと stderr にレポートします。

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/time.h>
#include <errno.h>
#include <string.h>

/* 90 second "Verbose hungry fork bomb".
Verbose -> It jabbers.
Hungry -> It grabs address space, and it tries to eat memory.

BEWARE: ON A SYSTEM WITH 'vm.overcommit_memory=0', THIS WILL FREEZE EVERYTHING
FOR THE DURATION AND CAUSE THE OOM KILLER TO BE INVOKED.  CLOSE THINGS YOU CARE
ABOUT BEFORE RUNNING THIS. */

#define STEP 1 << 30 // 1 GB
#define DURATION 90

time_t now () {
    struct timeval t;
    if (gettimeofday(&t, NULL) == -1) {
        fprintf(stderr,"gettimeofday() fail: %s\n", strerror(errno));
        return 0;
    }
    return t.tv_sec;
}

int main (void) {
    int forks = 0;
    int i;
    unsigned char *p;
    pid_t pid, self;
    time_t check;
    const time_t start = now();
    if (!start) return 1;

    while (1) {
    // Get our pid and check the elapsed time.
        self = getpid();
        check = now();
        if (!check || check - start > DURATION) return 0;
        fprintf(stderr,"%d says %d forks\n", self, forks++);
    // Fork; the child should get its correct pid.
        pid = fork();
        if (!pid) self = getpid();
    // Allocate a big chunk of space.
        p = malloc(STEP);
        if (!p) {
            fprintf(stderr, "%d Allocation failed!\n", self);
            return 0;
        }
        fprintf(stderr,"%d Allocation succeeded.\n", self);
    // The child will attempt to use the allocated space.  Using only
    // the child allows the fork bomb to proceed properly.
        if (!pid) {
            for (i = 0; i < STEP; i++) p[i] = i % 256;
            fprintf(stderr,"%d WROTE 1 GB\n", self);
        }
    }
}                        

これをコンパイルしますgcc forkbomb.c -o forkbomb。まず、次のように試してくださいsysctl vm.overcommit_memory=2。おそらく次のような結果になるはずです。

6520 says 0 forks
6520 Allocation succeeded.
6520 says 1 forks
6520 Allocation succeeded.
6520 says 2 forks
6521 Allocation succeeded.
6520 Allocation succeeded.
6520 says 3 forks
6520 Allocation failed!
6522 Allocation succeeded.

この環境では、この種のフォーク爆弾はそれほど遠くまで到達しません。「N 個のフォーク」の数字はプロセスの総数ではなく、そのプロセスに至るまでのチェーン/ブランチ内のプロセスの数であることに注意してください。

では、 で試してみましょうvm.overcommit_memory=0。stderr をファイルにリダイレクトすると、後で大まかな分析を行うことができます。例:

> cat tmp.txt | grep failed
4641 Allocation failed!
4646 Allocation failed!
4642 Allocation failed!
4647 Allocation failed!
4649 Allocation failed!
4644 Allocation failed!
4643 Allocation failed!
4648 Allocation failed!
4669 Allocation failed!
4696 Allocation failed!
4695 Allocation failed!
4716 Allocation failed!
4721 Allocation failed!

1 GBの割り当てに失敗したプロセスは15個のみで、overcommit_memory = 0のヒューリスティックが実証されています。状態によって影響を受けます。プロセスはいくつありましたか? tmp.txt の最後を見ると、おそらく 100,000 を超えています。では、実際に 1 GB をどれだけ使用できたでしょうか?

> cat tmp.txt | grep WROTE
4646 WROTE 1 GB
4648 WROTE 1 GB
4671 WROTE 1 GB
4687 WROTE 1 GB
4694 WROTE 1 GB
4696 WROTE 1 GB
4716 WROTE 1 GB
4721 WROTE 1 GB

8 個です。これは、当時約 3 GB の RAM が空きで、6 GB のスワップがあったため、納得できます。

これを実行した後、システム ログを確認してください。OOM キラーのレポート スコア (およびその他の情報) が表示されるはずです。これはおそらく に関連していますoom_badness

答え2

メモリに 1G のデータしかロードしない場合は、このようなことは起こりません。もっと多くのデータをロードするとどうなるでしょうか? たとえば、私は R にロードする必要がある数百万の確率を含む巨大なファイルを扱うことがよくあります。これには約 16GB の RAM が必要です。

上記のプロセスを私のラップトップで実行すると、8GB の RAM がいっぱいになるとすぐに、猛烈なスワップが始まります。その結果、ディスクからの読み取りは RAM からの読み取りよりもはるかに遅いため、すべてが遅くなります。2GB の RAM があり、空き容量が 10GB しかないラップトップの場合はどうでしょうか。プロセスが RAM をすべて使い果たすと、スワップに書き込むためディスクもいっぱいになり、RAM もスワップするスペースもなくなります (まさにこの理由から、スワップをスワップファイルではなく専用パーティションに制限する傾向があります)。ここで OOM キラーが登場し、プロセスを強制終了します。

したがって、システムは実際にメモリ不足になる可能性があります。さらに、スワッピングが頻繁に発生するシステムは、スワッピングによる I/O 操作の低速化により、これが発生するずっと前に使用できなくなる可能性があります。一般的に、スワッピングは可能な限り避けたいものです。高速 SSD を搭載したハイエンド サーバーでも、パフォーマンスは明らかに低下します。クラシックな 7200RPM ドライブを搭載した私のラップトップでは、大幅なスワッピングが発生すると、基本的にシステムが使用できなくなります。スワップが増えるほど、速度は低下します。問題のあるプロセスをすぐに終了しないと、OOM キラーが介入するまですべてがハングします。

答え3

プロセスは RAM がなくなったときに終了されるのではなく、次のように不正に操作されたときに終了します。

  • Linuxカーネルでは、通常、プロセスが実際に利用可能な量(RAMの一部+スワップ領域全体)よりも大きい量の仮想メモリを割り当てる(つまり予約する)ことを許可しています。
  • プロセスが予約したページのサブセットにのみアクセスする限り、すべて正常に実行されます。
  • しばらくして、プロセスが所有するページにアクセスしようとしたが、空きページがない場合は、メモリ不足の状況が発生します。
  • OOM キラーは、必ずしも新しいページを要求したプロセスとは限らないプロセスの 1 つを選択し、それを強制終了して仮想メモリを回復します。

これは、システムがアクティブにスワップしていない場合でも発生する可能性があります。たとえば、スワップ領域がスリープ状態のデーモンのメモリ ページでいっぱいになっている場合などです。

メモリをオーバーコミットしない OS では、このようなことは決して起こりません。これらの OS では、ランダム プロセスは強制終了されませんが、仮想メモリが不足しているときに仮想メモリを要求する最初のプロセスは、malloc (または同様のもの) でエラーを返します。したがって、状況を適切に処理する機会が与えられます。ただし、これらの OS では、空き RAM があるのにシステムの仮想メモリが不足することもあります。これは非常に混乱を招き、一般的に誤解されています。

答え4

他の回答とは別の視点から補足すると、多くの VPS は、任意のサーバー上で複数の仮想マシンをホストしています。単一の VM には、独自に使用するための RAM の量が決められています。多くのプロバイダーは、指定された量を超えて RAM を使用できる「バースト RAM」を提供しています。これは短期間の使用のみを想定しており、この量を長時間超えると、ホスト マシンの過負荷によって他のユーザーが被害を受けないように、ホストがプロセスを強制終了して RAM の使用量を減らすというペナルティを受ける可能性があります。

関連情報