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()
:
- есть
fork()
, который является системным вызовом Linux-ядра - есть
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)
– функция из библиотеки glibc
– https://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), и которая выполняет:
- если overcommit_memory == 1 (
if (sysctl_overcommit_memory == OVERCOMMIT_ALWAYS)
):- возвращем 0, и разрешаем выделение
- если overcommit_memory == 0 (
if (sysctl_overcommit_memory == OVERCOMMIT_GUESS)
, аsysctl_overcommit_memory
по умолчанию задаётся в OVERCOMMIT_GUESS, а OVERCOMMIT_GUESS задаётся в 0 в файлеlinux/mman.h
):- считается количество свободных страниц памяти, которое заносится в переменную free:
free = global_zone_page_state(NR_FREE_PAGES)
- считается количество file-backed (см. File-backed and Swap, Memory-mapped file) страниц памяти, т.е. те траницы памяти, которые могут быть особождены
free += global_node_page_state(NR_FILE_PAGES)
- вычитается кол-во разделяемой памяти (см. Shared Memory, Shared memory)
free -= global_node_page_state(NR_SHMEM)
- добавляются swap-страницы
free += get_nr_swap_pages()
- добавляет SReclaimable (см.
man 5 proc
SReclaimable)
free += global_node_page_state(NR_SLAB_RECLAIMABLE)
- и добавляет KReclaimable (см.
man 5 proc
KReclaimable)
free += global_node_page_state(NR_KERNEL_MISC_RECLAIMABLE)
- вычитает минимально зарезервированное количество страниц (см.
calculate_totalreserve_pages()
и An enhancement of OVERCOMMIT_GUESS)
free -= totalreserve_pages
- вычитает память, зарезервиванную для root (см.
init_admin_reserve()
)
free -= sysctl_admin_reserve_kbytes
- и последним шагом проверяется значение доступной памяти, и запрошенной – если free (в которой содержится результат всех предыдущих вычислений) больше pages (кол-во требуемых страниц) – то возвращается 0, и память выделяется:
if (free > pages) return 0;
- считается количество свободных страниц памяти, которое заносится в переменную free:
См. 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 архитектуру. А во-вторых – я в очередной раз убедился, что слепо верить написанному может быть не слишком полезно для здоровья братьев наших младших – серверов.
Ссылки по теме
- Background saving fails with a fork() error under Linux even if I have a lot of free RAM!
- Redis Persistence
- Launching Linux threads and processes with clone
- Where did fork go?
- What is Overcommit? And why is it bad?
- Linux – fork system call and its pitfalls
- Searchable Linux Syscall Table for x86 and x86_64
- The Linux COW
- Physical and virtual memory
- mtrace (3) – Linux Man Pages
- Out Of Memory Management
- Kernel overcommit-accounting
- Virtual memory settings in Linux – The Problem with Overcommit
- Memory – Part 1: Memory Types
- Memory mapping