Может ли в Linux «закончиться оперативная память»?

Может ли в Linux «закончиться оперативная память»?

Я видел несколько сообщений в Интернете, в которых люди, по-видимому, жаловались на то, что размещенный VPS неожиданно завершает процессы из-за того, что они используют слишком много оперативной памяти.

Как это возможно? Я думал, что все современные ОС предоставляют "бесконечную оперативную память", просто используя подкачку диска для всего, что выходит за пределы физической оперативной памяти. Это правильно?

Что может произойти, если процесс «завершен из-за нехватки оперативной памяти»?

решение1

Что может произойти, если процесс «завершен из-за нехватки оперативной памяти»?

Иногда говорят, что Linux по умолчанию никогда не отклоняет запросы на дополнительную память из кода приложения, например malloc(), 1. На самом деле это не так; по умолчанию используется эвристика, при которой

Очевидные перерасходы адресного пространства отклоняются. Используется для типичной системы. Это гарантирует, что серьезное дикое распределение не сработает, в то же время позволяя перерасходу сократить использование подкачки.

Из [linux_src]/Documentation/vm/overcommit-accounting(все цитаты из дерева 3.11). Что именно считается "серьезно диким распределением", не указано явно, поэтому нам придется пройтись по источнику, чтобы определить детали. Мы также могли бы использовать экспериментальный метод в сноске 2 (ниже), чтобы попытаться получить некоторое отражение эвристики -- основываясь на этом, мое первоначальное эмпирическое наблюдение заключается в том, что в идеальных обстоятельствах (== система простаивает), если у вас нет подкачки, вам будет разрешено выделить около половины вашей оперативной памяти, а если у вас есть подкачка, вы получите около половины вашей оперативной памяти плюс всю вашу подкачку. Это более или менееза процесс(но обратите внимание на этот пределявляетсядинамичен и может меняться в зависимости от состояния, см. некоторые замечания в сноске 5).

Половина вашей оперативной памяти плюс подкачка явно является значением по умолчанию для поля "CommitLimit" в /proc/meminfo. Вот что это значит -- и обратите внимание, что на самом деле это не имеет ничего общего с ограничением, которое только что обсуждалось (из [src]/Documentation/filesystems/proc.txt):

CommitLimit:На основе коэффициента перераспределения ('vm.overcommit_ratio') это общий объем памяти, доступный в данный момент для выделения.в системе.Этот лимит соблюдается только в том случае, если включен строгий учет перерасхода (режим 2 в 'vm.overcommit_memory'). CommitLimit рассчитывается по следующей формуле: CommitLimit = ('vm.overcommit_ratio' * Физическая оперативная память) + Подкачка. Например, в системе с 1 ГБ физической оперативной памяти и 7 ГБ подкачки при 'vm.overcommit_ratio', равном 30, CommitLimit составит 7,3 ГБ.

В ранее цитируемом документе по учету избыточных выделений указано, что значение по умолчанию vm.overcommit_ratioравно 50. Поэтому, если вы sysctl vm.overcommit_memory=2, вы можете настроить vm.covercommit_ratio (с помощью sysctl) и увидеть последствия. 3 Режим по умолчанию, когда CommitLimitне применяется и отклоняются только «очевидные избыточные выделения адресного пространства», — это когда vm.overcommit_memory=0.

Хотя стратегия по умолчанию имеет эвристический предел для каждого процесса, предотвращающий «совершенно дикое распределение», она оставляет системе в целом свободу для совершенно дикого распределения. 4 Это означает, что в какой-то момент она может исчерпать память и объявить о банкротстве некоторым процессам черезУбийца ООМ.

Что убивает OOM killer? Не обязательно тот процесс, который запросил память, когда ее не было, поскольку это не обязательно тот процесс, который действительно виноват, и, что еще важнее, не обязательно тот, который быстрее всего вытащит систему из проблемы, в которой она находится.

Это цитируется изздеськоторый, вероятно, ссылается на источник 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) и заметил, что этот комментарий за это время изменился:

/**
 * 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»,и по смыслу #4 («мы хотим убить минимальное количество процессов (один)).

Если вы хотите увидеть убийцу 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. Это совершенно безвредно, так как процесс не использует никакого пространства, которое он запрашивает, то есть он никогда не использует оперативную память.

На системе 3.11 x86_64 с 4 ГБ системы и 6 ГБ подкачки я потерпел неудачу на ~7400000 КБ; число колеблется, так что, возможно, state является фактором. Это совпадение близко к in CommitLimit, /proc/meminfoно изменение этого via vm.overcommit_ratioне имеет никакого значения. Однако на 3.6.11 32-битной системе ARM 448 МБ с 64 МБ подкачки я потерпел неудачу на ~230 МБ. Это интересно, поскольку в первом случае объем почти вдвое больше объема ОЗУ, тогда как во втором он составляет около 1/4 от этого — явно подразумевая, что объем подкачки является фактором. Это было подтверждено отключением подкачки на первой системе, когда порог сбоя снизился до ~1,95 ГБ, очень похожее соотношение с маленькой коробочкой ARM.

Но действительно ли это относится к каждому процессу? Похоже, что да. Короткая программа ниже запрашивает определенный пользователем фрагмент памяти и, если она успешно выполняется, ждет, пока вы не нажмете Enter — таким образом вы можете попробовать несколько одновременных экземпляров:

#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;
}

Однако следует помнить, что речь идет не только об объеме оперативной памяти и раздела подкачки независимо от их использования — см. сноску 5 для наблюдений о влиянии состояния системы.


3 CommitLimit относится к объему адресного пространства, разрешенного длясистемакогда vm.overcommit_memory = 2. Тогда, по-видимому, сумма, которую вы можете выделить, должна быть равна этой сумме за вычетом того, что уже выделено, что, по-видимому, и есть поле Committed_AS.

Потенциально интересный эксперимент, демонстрирующий это, заключается в добавлении #include <unistd.h>в начало virtlimitcheck.c (см. сноску 2) и fork()прямо перед while()циклом. Это не гарантирует, что будет работать так, как описано здесь, без некоторой утомительной синхронизации, но есть приличный шанс, что это будет работать, YMMV:

> 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 в вывод), пока один из них, очевидно, не затребует достаточно, чтобы другой потерпел неудачу. Победитель затем может забрать все до CommitLimitminus Committed_AS.


4 На этом этапе стоит отметить, если вы еще не понимаете виртуальную адресацию и подкачку по требованию, что в первую очередь избыточное выделение памяти возможно из-за того, что ядро ​​выделяет пользовательским процессам вовсе не физическую память, авиртуальное адресное пространство. Например, если процесс резервирует 10 МБ для чего-то, это выкладывается как последовательность (виртуальных) адресов, но эти адреса еще не соответствуют физической памяти. Когда к такому адресу осуществляется доступ, это приводит кошибка страницыи затем ядро ​​пытается отобразить его в реальную память, чтобы оно могло хранить реальное значение. Процессы обычно резервируют гораздо больше виртуального пространства, чем они фактически используют, что позволяет ядру максимально эффективно использовать оперативную память. Однако физическая память по-прежнему является конечным ресурсом, и когда вся она отображена в виртуальное адресное пространство, часть виртуального адресного пространства должна быть устранена, чтобы освободить часть оперативной памяти.


5 Первыйпредупреждение: Если вы попробуете это с помощью vm.overcommit_memory=0, обязательно сначала сохраните свою работу и закройте все критические приложения, поскольку система будет заморожена примерно на 90 секунд, а некоторые процессы завершатся!

Идея состоит в том, чтобы запуститьвилочная бомбавремя ожидания истекает через 90 секунд, при этом форки выделяют место, а некоторые из них записывают большие объемы данных в оперативную память, при этом все время отправляя отчеты в 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.

В этой среде этот вид fork bomb не зайдет далеко. Обратите внимание, что число в "says N forks" не является общим числом процессов, это число процессов в цепочке/ветви, ведущих к этому.

Теперь попробуйте с 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!

Только 15 процессов не смогли выделить 1 ГБ — это демонстрирует, что эвристика для overcommit_memory = 0являетсязатронуто состоянием. Сколько процессов было? Если посмотреть на конец tmp.txt, то, вероятно, > 100 000. Теперь, как на самом деле можно использовать 1 ГБ?

> 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

Восемь — что опять же имеет смысл, поскольку на тот момент у меня было свободно около 3 ГБ оперативной памяти и 6 ГБ подкачки.

После этого посмотрите системные журналы. Вы должны увидеть отчеты OOM killer (помимо прочего); предположительно, это относится к oom_badness.

решение2

Этого не случится с вами, если вы загрузите в память только 1G данных. А что, если вы загрузите гораздо больше? Например, я часто работаю с огромными файлами, содержащими миллионы вероятностей, которые нужно загрузить в R. Это занимает около 16 ГБ оперативной памяти.

Запуск вышеуказанного процесса на моем ноутбуке заставит его начать сумасшедшую подкачку, как только мои 8 ГБ ОЗУ будут заполнены. Это, в свою очередь, замедлит все, потому что чтение с диска намного медленнее, чем чтение из ОЗУ. Что, если у меня ноутбук с 2 ГБ ОЗУ и только 10 ГБ свободного места? Как только процесс займет всю ОЗУ, он также заполнит диск, потому что он пишет в подкачку, и у меня не останется больше ОЗУ и больше места для подкачки (люди склонны ограничивать подкачку выделенным разделом, а не файлом подкачки именно по этой причине). Вот тут-то и появляется убийца OOM и начинает убивать процессы.

Итак, система действительно может исчерпать память. Более того, интенсивно подкачиваемые системы могут стать непригодными для использования задолго до того, как это произойдет, просто из-за медленных операций ввода-вывода из-за подкачки. Обычно хочется избегать подкачки как можно чаще. Даже на высокопроизводительных серверах с быстрыми SSD наблюдается явное снижение производительности. На моем ноутбуке, на котором установлен классический диск на 7200 об/мин, любая значительная подкачка по сути делает систему непригодной для использования. Чем больше она подкачки, тем медленнее она становится. Если я быстро не убью нарушающий процесс, все зависнет, пока не вмешается OOM killer.

решение3

Процессы не завершаются, когда больше нет оперативной памяти, они завершаются, когда их обманывают следующим образом:

  • Ядро Linux обычно позволяет процессам выделять (т.е. резервировать) объем виртуальной памяти, превышающий фактически доступный объем (часть оперативной памяти + вся область подкачки).
  • Пока процессы обращаются только к подмножеству зарезервированных ими страниц, все работает нормально.
  • если через некоторое время процесс пытается получить доступ к принадлежащей ему странице, но свободных страниц больше нет, возникает ситуация нехватки памяти
  • Убийца OOM выбирает один из процессов, не обязательно тот, который запросил новую страницу, и просто завершает его, чтобы восстановить виртуальную память.

Это может произойти даже тогда, когда система не выполняет активный подкачок, например, если область подкачки заполнена страницами памяти спящих демонов.

Это никогда не происходит в ОС, которые не перераспределяют память. В них ни один случайный процесс не уничтожается, но первый процесс, запрашивающий виртуальную память, когда она исчерпана, возвращает ошибку malloc (или что-то подобное). Таким образом, ему дается шанс правильно обработать ситуацию. Однако в этих ОС может также случиться, что система исчерпает виртуальную память, в то время как оперативная память все еще свободна, что довольно запутанно и обычно неправильно понимается.

решение4

Чтобы добавить еще одну точку зрения из других ответов, многие VPS размещают несколько виртуальных машин на любом сервере. Любая отдельная VM будет иметь определенный объем ОЗУ для собственного использования. Многие провайдеры предлагают «burst RAM», в котором они могут использовать ОЗУ сверх выделенного им объема. Это предназначено только для краткосрочного использования, и те, кто выходит за рамки этого объема в течение длительного периода времени, могут быть оштрафованы хостом, убивающим процессы, чтобы снизить объем используемой ОЗУ, чтобы другие не страдали от перегрузки хост-машины.

Связанный контент