Redis: fork – Cannot allocate memory, Linux, виртуальная память и vm.overcommit_memory

Автор: | 27/08/2019
 

Cейчас настраиваю Redis как кеширующий сервис приложения, и среди прочего встал вопрос – надо ли включать vm.overcommit_memory в 1, или нет?

Вопрос достаточно старый, см. История, но вот докопаться до сути, упорядочить и привести в читабельный вид удалось только сейчас.

Проблема заключается в том, что официальная документация и практически все гайды/HowTo-шки по Redis достаточно легкомысленно предлагают “волшебную пилюлю” в виде безусловного разрешения overcommit_memory, особенно в случае ошибки вида “fork — Cannot allocate memory“.

Попробуем разобраться – что overcommit_memory даёт, когда она используется, и нужна ли она в нашем случае.

Почему оверкоммит вреден

Смотрите полный пост тут>>>.

Когда операционная система, такая как Linux, выделяет память процессу из юзерспейса (запрошенного с помощью brk() или mmap()) – между виртуальными и реальными областями памяти нет никакой связи до тех пор, пока процесс не попробует обратиться к этой памяти – после этого ядро и MMU выполнят маппинг виртуального адреса к физическому.

Overcommit означает выделение виртуальной памяти больше, чем доступно физической, т.е. нет никакой гарантии, что при необходимости, когда процесс начнёт активно использовать выделенную ему виртуальную память – ядро сможет сформировать связь между всей выделенной виртуальной памятью и физической.

Если использовать аналогии, то это как использовать кредитную карту и не следить за расходами: система просто выделяет нам бесконечное количество виртуальной памяти, пока не приходят коллекторы – т.е, пока программа не начинает использовать ранее незатронутую память, для которой потребуется физическая память – а ядро не может её выделить, потому что она закончилась.

Redis persistance

Redis использует два механизма обеспечения целостности данных – RDB (point-in-time snapshot), который выполняет копирование существующих данных из памяти на диск, и AOF, при котором записываются все операции, выполненные сервером во время работы. См. Redis Persistence.

overcommit_memory играет роль при выполнении операций созданий снапшотов данных из памяти на диск, а конкретно – при вызовах BGSAVE и BGREWRITEAOF.

Ниже мы будем рассматривать процесс именно BGSAVE, при котором Redis порождает дочерний процесс, который и выполняет копирование базы данных Redis из памяти на диск, т.е. создаёт snapshot данных.

Redis save, SAVE и BGSAVE

Немного путаницы может внести сам Redis: в файле настроек параметр save отвечает как раз за создание резервной копии из дочернего процесса, т.е. BGSAVE.

При этом, у Redis есть команда SAVE, которая так же создаёт дамп данных – но работает она иначе:

  • SAVE является синхронной командой, и выполняет блокирование операций записи в память на время, пока создаётся копия данных
  • BGSAVE, в свою очередь, выполняет асинхронное копирование, т.е. запускается параллельно основному процесу Redis, и не влияет на его работу и подключенных к нему клиентов, а потому является предпочтительным способом создания копии данных

Однако в случае, если BGSAVE не может быть выполнена, например из-за ошибки “Can’t save in background: fork: Cannot allocate memory” – можно вызвать SAVE.

Что бы проверить это – используем strace.

Создаём тестовый конфиг redis-testing.conf:

save 1 1
port 7777

Запускаем strace и redis-server с этим конфигом:

[simterm]

root@bttrm-dev-console:/home/admin# strace -o redis-trace.log redis-server redis-testing.conf

[/simterm]

strace пишет данные в файл redis-trace.log, который и будем проверять на предмет используемых системных вызовов, которые выполняет redis-server при операциях SAVE и BGSAVE:

[simterm]

root@bttrm-dev-console:/home/admin# tail -f redis-trace.log | grep -v 'gettimeofday\|close\|open\|epoll_wait\|getpid\|read\|write'

[/simterm]

Тут через grep -v убираем “мусорные” вызовы, которые нас сейчас не интересуют. Можно было бы через -e trace= выбрать только нужные – но пока не ясно, что именно будет интересно.

В файле Redis настроек мы указали port 777 и save 1 1, т.е. – создавать копию базы из памяти на диск каждую секунду если изменился как минимум 1 ключ.

Добавляем ключ в базу:

[simterm]

admin@bttrm-dev-console:~$ redis-cli -p 7777 set test test
OK

[/simterm]

И проверяем лог strace:

[simterm]

root@bttrm-dev-console:/home/admin# tail -f redis-trace.log | grep -v 'gettimeofday\|close\|open\|epoll_wait\|getpid\|read\|write'
accept(5, {sa_family=AF_INET, sin_port=htons(60816), sin_addr=inet_addr("127.0.0.1")}, [128->16]) = 6
...
stat("/etc/localtime", {st_mode=S_IFREG|0644, st_size=2097, ...}) = 0
clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7ff26beda190) = 1790
stat("/etc/localtime", {st_mode=S_IFREG|0644, st_size=2097, ...}) = 0
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=1790, si_uid=0, si_status=0, si_utime=0, si_stime=0} ---
wait4(-1, [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], WNOHANG, NULL) = 1790
stat("/etc/localtime", {st_mode=S_IFREG|0644, st_size=2097, ...}) = 0

[/simterm]

Вот и вызов clone() (почему clone(), а не fork() – будет чуть ниже, в fork() vs fork() vs clone()), который и порождает дочерний процесс, который выполняет создание дампа базы.

Теперь – выполним команду SAVE:

[simterm]

admin@bttrm-dev-console:~$ redis-cli -p 7777 save
OK

[/simterm]

И проверяем лог:

[simterm]

accept(5, {sa_family=AF_INET, sin_port=htons(32870), sin_addr=inet_addr("127.0.0.1")}, [128->16]) = 6
...
rename("temp-1652.rdb", "dump.rdb")     = 0
stat("/etc/localtime", {st_mode=S_IFREG|0644, st_size=2097, ...}) = 0
epoll_ctl(3, EPOLL_CTL_DEL, 6, 0x7ffe6712430c) = 0

[/simterm]

clone() нет – дамп базы был создан напрямую основным процессом Redis-сервера, и сохранён в файл dump.rdb – строка rename(“temp-1652.rdb”, “dump.rdb”). в выводе strace (мы сейчас увидим – откуда взялось имя temp-1652.rdb).

Вызываем BGSAVE:

[simterm]

admin@bttrm-dev-console:~$ redis-cli -p 7777 bgsave
Background saving started

[/simterm]

Проверяем лог:

[simterm]

accept(5, {sa_family=AF_INET, sin_port=htons(33030), sin_addr=inet_addr("127.0.0.1")}, [128->16]) = 6
...
clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7ff26beda190) = 1879
stat("/etc/localtime", {st_mode=S_IFREG|0644, st_size=2097, ...}) = 0
epoll_ctl(3, EPOLL_CTL_DEL, 6, 0x7ffe6712430c) = 0
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=1879, si_uid=0, si_status=0, si_utime=0, si_stime=0} ---
wait4(-1, [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], WNOHANG, NULL) = 1879
stat("/etc/localtime", {st_mode=S_IFREG|0644, st_size=2097, ...}) = 0

[/simterm]

И снова наш clone(), который породил дочерний процесс с PID 1879:

[simterm]

...
clone([...]) = 1879
...

[/simterm]

Redis функции rdbSave() и rdbSaveBackground()

Собственно RDB-дамп выполняется единственной функцией Redis – rdbSave():

...
/* Save the DB on disk. Return C_ERR on error, C_OK on success. */
int rdbSave(char *filename, rdbSaveInfo *rsi) {
    ...
    snprintf(tmpfile,256,"temp-%d.rdb", (int) getpid());
    fp = fopen(tmpfile,"w");
...

Которая вызывается при прямом вызове redis-cli -p 7777 SAVE.

А вот и имя файла temp-1652.rdb из результатов strace чуть выше:

...
snprintf(tmpfile,256,"temp-%d.rdb", (int) getpid());
...

Где 1652 – PID процесса Redis-сервера.

В свою очередь при BGSAVE вызывается функция rdbSaveBackground():

...
int rdbSaveBackground(char *filename, rdbSaveInfo *rsi) {
    ...
    start = ustime();
    if ((childpid = fork()) == 0) {
        int retval;

        /* Child */
        closeListeningSockets(0);
        redisSetProcTitle("redis-rdb-bgsave");
        retval = rdbSave(filename,rsi);
...

Которая как раз и порождает новый процесс:

...
if ((childpid = fork()) == 0)
...

В котором и вызывается rdbSave():

...
retval = rdbSave(filename,rsi);
...

fork() vs fork() vs clone()

Вернёмся к вопросу – почему в выводе strace мы видим не fork(), а clone(), ведь функция rdbSaveBackground() вызывает именно fork()?

А потому, что fork() != fork():

  1. есть fork(), который является системным вызовом Linux-ядра
  2. есть fork(), который является функцией библиотеки glibc, которая является обёрткой над системным вызовом clone()

Тут может пригодится apropos:

[simterm]

[setevoy@setevoy-arch-work ~/Temp/redis] [unstable*] $ apropos fork
fork (2)             - create a child process
fork (3am)           - basic process management
fork (3p)            - create a new process

[/simterm]

Итак, fork(2) – системный вызов, тогда как fork(3p) – функция из библиотеки glibchttps://github.com/bminor/glibc/blob/master/sysdeps/nptl/fork.c#L48.

Далее, открываем man 2 fork:

[simterm]

[setevoy@setevoy-arch-work ~/Temp/redis] [unstable*] $ man 2 fork | grep -A 5 NOTES
NOTES
       Under Linux, fork() is implemented using copy-on-write pages, so the only penalty that it incurs is the time and memory required to duplicate the parent's page tables, and to create a unique task structure for the child.

   C library/kernel differences
       Since version 2.3.3, rather than invoking the kernel's fork() system call, the glibc fork() wrapper that is provided as part of the NPTL threading implementation invokes clone(2) with flags that provide the same effect as the
       traditional system call.  (A call to fork() is equivalent to a call to clone(2) specifying flags as just SIGCHLD.)  The glibc wrapper invokes any fork handlers that have been established using pthread_atfork(3).

[/simterm]

rather than invoking the kernel’s fork() system call, the glibc fork() wrapper […] invokes clone(2)

Следовательно, когда вызывается fork() из rdbSaveBackground() – он вызывает не системный вызов fork(2), а функцию fork(3p) из библиотеки glibc, которая переадресовывается в __libc_fork():

...
weak_alias (__libc_fork, __fork)
libc_hidden_def (__fork)
weak_alias (__libc_fork, fork)

А в самом __libc_fork() – “магия” происходит в макросе arch_fork():

...
pid = arch_fork (&THREAD_SELF->tid);
...

Находим его:

[simterm]

[setevoy@setevoy-arch-work ~/Temp/glibc] [master*] $ grep -r arch_fork . 
./ChangeLog:    (arch_fork): Issue INLINE_CLONE_SYSCALL if defined.
./ChangeLog:    * sysdeps/nptl/fork.c (ARCH_FORK): Replace by arch_fork.
./ChangeLog:    * sysdeps/unix/sysv/linux/arch-fork.h (arch_fork): New function.
./sysdeps/unix/sysv/linux/arch-fork.h:/* arch_fork definition for Linux fork implementation.
./sysdeps/unix/sysv/linux/arch-fork.h:arch_fork (void *ctid)
./sysdeps/nptl/fork.c:  pid = arch_fork (&THREAD_SELF->tid);

[/simterm]

arch_fork() описан в файле sysdeps/unix/sysv/linux/arch-fork.h, и он и как раз и вызывает clone():

...
ret = INLINE_SYSCALL_CALL (clone, flags, 0, NULL, 0, ctid);
...

Который мы и видим в выводе strace.

Что бы убедиться, что мы в самом деле используем glibc fork(), а не системный вызов Linux – используем такой пример из документации GNU:

#include <unistd.h>
#include <sys/wait.h>
#include <stdio.h>

int main () {
  pid_t pid;
  pid = fork ();
  if (pid == 0) {
      printf("Child created\n");
      sleep(100);
  }
}

В pid = fork() вызываем fork() аналогично тому, как это происходит в rdbSaveBackground(), и запускаем с ltrace для отслеживания вызова библиотечных функций (в отличии от strace для системных вызовов):

[simterm]

$ ltrace -C -f ./test_fork_lib
[pid 5530] fork( <unfinished ...>
[pid 5531] <... fork resumed> )                                                                                                                    = 0
[pid 5530] <... fork resumed> )                                                                                                                    = 5531
[pid 5531] puts("Child created" <no return ...>
[pid 5530] +++ exited (status 0) +++
Child created
[pid 5531] <... puts resumed> )                                                                                                                    = 14
[pid 5531] sleep(100)                                                                                                                              = 0

[/simterm]

С помощью lsof проверяем открытые нашим процессом файлы:

[simterm]

[setevoy@setevoy-arch-work ~/Temp/glibc] [master*] $ lsof -p 5531
COMMAND    PID    USER   FD   TYPE DEVICE SIZE/OFF    NODE NAME
test_fork 5531 setevoy  cwd    DIR  254,3     4096 4854992 /home/setevoy/Temp/glibc
test_fork 5531 setevoy  rtd    DIR  254,2     4096       2 /
test_fork 5531 setevoy  txt    REG  254,3    16648 4855715 /home/setevoy/Temp/glibc/test_fork_lib
test_fork 5531 setevoy  mem    REG  254,2  2133648  396251 /usr/lib/libc-2.29.so
...

[/simterm]

Либо с помощью ldd проверим используемые библиотеки:

[simterm]

[setevoy@setevoy-arch-work ~/Temp/glibc] [master*] $ ldd test_fork_lib
        ...
        libc.so.6 => /usr/lib/libc.so.6 (0x00007f26ba77f000)

[/simterm]

libc-2.29.so входит в пакет glibc:

[simterm]

[setevoy@setevoy-arch-work ~/Temp/glibc] [master*] $ pacman -Ql glibc | grep libc-2.29.so
glibc /usr/lib/libc-2.29.so

[/simterm]

Другой вариант – с помощью objdump проверить саму библиотеку:

[simterm]

[setevoy@setevoy-arch-work ~/Temp/linux] [master*] $ objdump -T /usr/lib/libc.so.6 | grep fork
00000000000c93c0 g    DF .text  00000000000001fe  GLIBC_PRIVATE __libc_fork

[/simterm]

__libc_fork – вот искомая функция в секции .text (см. Linux: C — адресное пространство процесса и C: создание и применение shared library в Linux).

Причины Redis – fork: Cannot allocate memory

См. Background saving fails with a fork() error under Linux even if I have a lot of free RAM.

При создании копии данных Redis полагается на механизм Copy-on-Write.

Redis порождает новый процесс через вызов fork(), который является полной копией родительского процесса, и в теории – должен занимать столько же места, как и родительский процесс, т.е. процесс самого Redis-сервера. И хотя новый процесс может и, как правило, занимает значительно меньше места благодаря механизму ядра copy-on-write, Linux всё-равно постарается выделить ему столько же виртуальной памяти, сколько выделено под родительский процесс, т.к. нельзя предстаказать – сколько страниц памяти изменятся в процессе выполнения копирования.

Таким образом, если у системы недостаточно свободной памяти, а overcommit_memory установлен в 0 – то ядро откажет в выделении памяти под новый процесс, и fork() завершится с ошибкой.

Значения overcommit_memory

vm.overcommit_memory может иметь одно из трёх значений:

  • 0: ядро может выделить больше виртуальной памяти, чем доступно реально, но при этом полагается на “эвристический алгоритм” (heuristic overcommit handling) при принятии решения о выделении или об отказе
  • 1: ядро всегда будет выполнять overcommit, что увеличивает вероятность ошибок Out of memory, но должно позитивно влиять на производительность процессов, активно использующих память
  • 2: ядро не будет выделять памяти больше, чем определено в overcommit_ratio или overcommit_kbytes

Знаменитый “Эвристический алгоритм” (Heuristic Overcommit handling)

В большинстве документации/гайдов/вопросов на StackOverflow упоминается только этот самый “эвристический алгоритм”, но что за алгоритм – удалось найти не сразу.

Собственно, проверка выделения памяти выполняется во вспомогательной функции __vm_enough_memory() из модуля memory management из файла mm/util.c, которой передаётся количество запрашиваемых для выделения страниц памяти (long pages), и которая выполняет:

  1. если overcommit_memory == 1 (if (sysctl_overcommit_memory == OVERCOMMIT_ALWAYS)):
    1. возвращем 0, и разрешаем выделение
  2. если overcommit_memory == 0 (if (sysctl_overcommit_memory == OVERCOMMIT_GUESS), а sysctl_overcommit_memory по умолчанию задаётся в OVERCOMMIT_GUESS, а OVERCOMMIT_GUESS задаётся в 0 в файле linux/mman.h):
    1. считается количество свободных страниц памяти, которое заносится в переменную free:
      free = global_zone_page_state(NR_FREE_PAGES)
    2. считается количество file-backed (см. File-backed and Swap, Memory-mapped file) страниц памяти, т.е. те траницы памяти, которые могут быть особождены
      free += global_node_page_state(NR_FILE_PAGES)
    3. вычитается кол-во разделяемой памяти (см. Shared Memory, Shared memory)
      free -= global_node_page_state(NR_SHMEM)
    4. добавляются swap-страницы
      free += get_nr_swap_pages()
    5. добавляет SReclaimable (см. man 5 proc SReclaimable)
      free += global_node_page_state(NR_SLAB_RECLAIMABLE)
    6. и добавляет KReclaimable (см. man 5 proc KReclaimable)
      free += global_node_page_state(NR_KERNEL_MISC_RECLAIMABLE)
    7. вычитает минимально зарезервированное количество страниц (см. calculate_totalreserve_pages() и An enhancement of OVERCOMMIT_GUESS)
      free -= totalreserve_pages
    8. вычитает память, зарезервиванную для root (см. init_admin_reserve())
      free -= sysctl_admin_reserve_kbytes
    9. и последним шагом проверяется значение доступной памяти, и запрошенной – если free (в которой содержится результат всех предыдущих вычислений) больше pages (кол-во требуемых страниц) – то возвращается 0, и память выделяется:
      if (free > pages) return 0;

См. How Linux handles virtual memory overcommit, overcommit-accounting, Checking Available Memory.

Проверка vm.overcommit_memory

Что бы самим увидеть что происходит при vm.overcommit_memory, и как вообще выделяется память – используем простой код:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>

int main() {
    
    printf("main() started\n");
    
    long int mem_size = 4096;
    
    void *mem_stack = malloc(mem_size);
    
    printf("Parent pid: %lu\n", getpid());

    sleep(1200);
}

В строке void *mem_stack = malloc(mem_size) выполняем выделение памяти 4096 байт, которые заданы в переменной mem_size.

Проверяем значение overcommit_memory:

[simterm]

root@bttrm-dev-console:/home/admin# cat /proc/sys/vm/overcommit_memory 0

[/simterm]

Запускаем проверку:

[simterm]

root@bttrm-dev-console:/home/admin# ./test_vm 
main() started
Parent pid: 14353

[/simterm]

Проверяем занятую процессом память:

[simterm]

root@bttrm-dev-console:/home/admin# ps aux | grep test_vm | grep -v grep
root     14353  0.0  0.0   4160   676 pts/4    S+   17:29   0:00 ./test_vm

[/simterm]

4160 VSZ – как мы и запрашивали в malloc(mem_size).

Меняем значение переменной mem_size с 4096 байт на 1099511627776 – 1 террабайт:

...
long int mem_size = 1099511627776;
...

Собираем, запускаем – и:

[simterm]

root@bttrm-dev-console:/home/admin# ./test_vm

main() started
Segmentation fault

[/simterm]

Отлично!

Смотрим с помощью strace:

[simterm]

root@bttrm-dev-console:/home/admin# strace -e trace=mmap ./test_vm 
mmap(NULL, 47657, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7fa268f24000
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fa268f22000
mmap(NULL, 3795296, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7fa26896e000
mmap(0x7fa268d03000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x195000) = 0x7fa268d03000
mmap(0x7fa268d09000, 14688, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7fa268d09000
main() started
mmap(NULL, 1099511631872, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = -1 ENOMEM (Cannot allocate memory)
mmap(NULL, 1099511762944, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = -1 ENOMEM (Cannot allocate memory)
mmap(NULL, 134217728, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_NORESERVE, -1, 0) = 0x7fa26096e000
mmap(NULL, 1099511631872, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = -1 ENOMEM (Cannot allocate memory)
--- SIGSEGV {si_signo=SIGSEGV, si_code=SEGV_MAPERR, si_addr=0xfffffffff8} ---
+++ killed by SIGSEGV +++
Segmentation fault

[/simterm]

Вот и наша -1 ENOMEM (Cannot allocate memory), которую иногда и получает Redis, и возвращает нам в виде сообщения “Can’t save in background: fork: Cannot allocate memory.

Тут mmap() вызывает функцию security_vm_enough_memory_mm():

...
    if (security_vm_enough_memory_mm(mm, grow))
        return -ENOMEM;
...

Которая описана в security.h, и запускает собственно проверку через вызов __vm_enough_memory():

...
static inline int security_vm_enough_memory_mm(struct mm_struct *mm, long pages)
{
    return __vm_enough_memory(mm, pages, cap_vm_enough_memory(mm, pages));
}
...

Теперь – отключаем лимит на размер выделяемой виртуальной памяти:

[simterm]

root@bttrm-dev-console:/home/admin# echo 1 > /proc/sys/vm/overcommit_memory

[/simterm]

И пробуем запустить ещё раз:

[simterm]

root@bttrm-dev-console:/home/admin# ./test_vm 

main() started
Parent pid: 11337
Child pid: 11338
Child is running with PID 11338

[/simterm]

Проверяем VSZ:

[simterm]

admin@bttrm-dev-console:~/redis-logs$ ps aux | grep -v grep | grep 11337
root     11337  0.0  0.0 1073745988 656 pts/4  S+   16:34   0:00 ./test_vm

[/simterm]

VSZ == 1073745988, замечательно – мы выделили 1 террабайт виртуальной памяти на AWS t2.medium EC2 с 4 гигабайтами “реальной” 🙂

А теперь угадайте, что произойдёт, если дочерний процесс начнёт активно писать в эту память?

Добавляем вызов memset(), которая скопирует в наш mem_stack цифру 0 на весь заданный mem_size, т.е. 1ТБ:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>

int main() {
    
    printf("main() started\n");
    
    long int mem_size = 1099511627776;
    
    void *mem_stack = malloc(mem_size);
    
    printf("Parent pid: %lu\n", getpid());
                
    memset(mem_stack, 0, mem_size);

    sleep(120);
}

Запускаем (не стоит делать этого на Production-сервере):

[simterm]

root@bttrm-dev-console:/home/admin# ./test_vm 
main() started
Parent pid: 15219
Killed

[/simterm]

И проверяем лог:

Aug 27 17:46:43 localhost kernel: [7974462.384723] Out of memory: Kill process 15219 (test_vm) score 818 or sacrifice child
Aug 27 17:46:43 localhost kernel: [7974462.393395] Killed process 15219 (test_vm) total-vm:1073745988kB, anon-rss:3411676kB, file-rss:16kB, shmem-rss:0kB
Aug 27 17:46:43 localhost kernel: [7974462.600138] oom_reaper: reaped process 15219 (test_vm), now anon-rss:0kB, file-rss:0kB, shmem-rss:0kB

Пришёл OOM Killer, и всех убил. А мог бы и не успеть.

Обратите внимание, что наш процесс успел отожрать 3.4 гигабайта реальной памяти сервера – anon-rss:3411676k из выделенного ему 1ТБ – total-vm:1073745988kB.

Выводы

В нашем случае, с отключенным RDB и AOF, когда Redis используется только для кеширования, и хранить данные на диске смысла нет – нет смысла и в изменении overcommit_memory, и следует оставить его в значении по-умолчанию – 0.

А уж если хочется задать лимит вручную – то лучше использовать overcommit_memory == 2, и ограничение по %, что бы всегда оставался запас памяти.

История

На самом деле для меня лично вся история с vm.overcommit_memory началась ещё год тому.

С Redis я ещё тогда толком знаком не был, а на новом проекте он был.

В один прекрасный день наш Production (а в то время, когда я только пришёл – на проекте весь бекенд крутился на одном сервере) подустал и прилёг отдохнуть.

Когда после волшебного пенделя в виде ребута через панель управления AWS сервер всё-таки поднялся – я начал искать причины.

В логах была запись о том, что OOM Killer пришёл то ли к самому Redis, то ли к RabbitMQ, уже не помню. Но в процессе разбора я как раз и нашёл vm.overcommit_memory, заданный в 1, т.е. отключение проверки вообще.

В общем, во-первых, благодаря этой истории, – у меня появился повод создать более fault-tolerant архитектуру. А во-вторых – я в очередной раз убедился, что слепо верить написанному может быть не слишком полезно для здоровья братьев наших младших – серверов.

Ссылки по теме