Первая часть маленького «срывания покрова» о работе подсистемы виртуальной памяти, связи механизмов mmap, разделяемых библиотек и кэшей вызвало такое бурное обсуждение, что я не смог удержаться от того, чтобы не продолжить исследование на практике
Поэтому, сегодня мы сделаем… Крошечную лабораторную работу. В виде крошечной же программы на C, которую мы напишем, скомпилируем и проверим в деле — со свапом и без свапа.
Программа делает очень простую вещь — она запрашивает большой кусок памяти, обращается к нему и активно с ним работает. Чтобы не мучаться с загрузкой каких-либо библиотек, мы просто создадим большой файл, который отобразим в память так, как это делает система при загрузке разделяемых библиотек.
А вызов кода из этой «библиотеки» мы просто эмулируем чтением из такого mmap-нутого файла.
Программа сделает несколько итераций, на каждой итерации она будет параллельно обращаться к «коду» и к одному из участков большого сегмента данных.
И, чтобы не писать лишнего кода, мы определим две константы, которые определят размер «сегмента кода» и общий размер оперативной памяти:
- MEM_GBYTES — размер оперативной памяти для теста
- LIB_GBYTES — размер «кода»
Объем «данных» у нас меньше объема физической памяти:
- DATA_GBYTES = MEM_GBYTES — 2
Суммарный объем «кода» и «данных» чуть больше объема физической памяти:
- DATA_GBYTES + LIB_GBYTES = MEM_GBYTES + 1
Для теста на ноутбуке я взял MEM_GBYTES = 16, и получил следующие характеристики:
- MEM_GBYTES = 16
- DATA_GBYTES = 14 — значит «данных» будет 14GB, то есть «памяти достаточно»
- Swap size = 16GB
Текст программы
#include <sys/mman.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#define GB 1073741824l
#define MEM_SIZE 16
#define LIB_GBYTES 3
#define DATA_GBYTES (MEM_SIZE - 2)
long random_read(char * code_ptr, char * data_ptr, size_t size) {
long rbt = 0;
for (unsigned long i=0 ; i<size ; i+=4096) {
rbt += code_ptr[(8l * random() % size)] + data_ptr[i];
}
return rbt;
}
int main() {
size_t libsize = LIB_GBYTES * GB;
size_t datasize = DATA_GBYTES * GB;
int fd;
char * dataptr;
char * libptr;
srandom(256);
if ((fd = open("library.bin", O_RDONLY)) < 0) {
printf("Required library.bin of size %ldn", libsize);
return 1;
}
if ((libptr = mmap(NULL, libsize,
PROT_READ, MAP_SHARED, fd, 0)) == MAP_FAILED) {
printf("Failed build libptr due %dn", errno);
return 1;
}
if ((dataptr = mmap(NULL, datasize,
PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS,
-1, 0)) == MAP_FAILED) {
printf("Failed build dataptr due %dn", errno);
return 1;
}
printf("Preparing test ...n");
memset(dataptr, 0, datasize);
printf("Doing test ...n");
unsigned long chunk_size = GB;
unsigned long chunk_count = (DATA_GBYTES - 3) * GB / chunk_size;
for (unsigned long chunk=0 ; chunk < chunk_count; chunk++) {
printf("Iteration %d of %dn", 1 + chunk, chunk_count);
random_read(libptr, dataptr + (chunk * chunk_size), libsize);
}
return 0;
}
Тест без использования swap
Запрещаем swap указав vm.swappines=0 и запускаем тест
$ time ./swapdemo
Preparing test ...
Killed
real 0m6,279s
user 0m0,459s
sys 0m5,791s
Что произошло? Значение swappiness=0 отключило свап — анонимные страницы в него больше не вытесняются, то есть данные всегда в памяти. Проблема в том, что оставшихся 2GB не хватило для работающих в фоне Chrome и VSCode, и OOM-killer убил тестовую программу. А заодно нехватка памяти похоронила вкладку Chrome, в которой я писал эту статью. И мне это не понравилось — пусть даже автоматическое сохранение сработало. Я не люблю когда мои данные «хоронят».
Включенный swap
Выставляем vm_swappines = 60 (по умолчанию)
Запускаем тест:
$ time ./swapdemo
Preparing test ...
Doing test ...
Iteration 1 of 11
Iteration 2 of 11
Iteration 3 of 11
Iteration 4 of 11
Iteration 5 of 11
Iteration 6 of 11
Iteration 7 of 11
Iteration 8 of 11
Iteration 9 of 11
Iteration 10 of 11
Iteration 11 of 11
real 1m55,291s
user 0m2,692s
sys 0m20,626s
Фрагмент top:
Tasks: 298 total, 2 running, 296 sleeping, 0 stopped, 0 zombie
%Cpu(s): 0,6 us, 3,1 sy, 0,0 ni, 85,7 id, 10,1 wa, 0,5 hi, 0,0 si, 0,0 st
MiB Mem : 15670,0 total, 156,0 free, 577,5 used, 14936,5 buff/cache
MiB Swap: 16384,0 total, 12292,5 free, 4091,5 used. 3079,1 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
10393 viking 20 0 17,0g 14,2g 14,2g D 17,3 93,0 0:18.78 swapdemo
136 root 20 0 0 0 0 S 9,6 0,0 4:35.68 kswapd0
Плохой-плохой линукс!!! Он использует swap почти на 4 гигабайт хотя у него 14 гигабайт кэша и 3 гигабайта доступно! У линукса неправильные настройки! Плохой outlingo, плохие старые админы, они ничего не понимают, они сказали включить swap и теперь у меня из-за них система свапится и плохо работает. Надо отключить swap как советуют намного более молодые и перспективные интернет-эксперты, ведь они точно знают что делать!
Ну … Пусть будет так. Давайте максимально отключим свап по советам экспертов?
Тест почти без swap
Выставляем vm_swappines = 1
Это значение приведет к тому, что свапинг анонимных страниц будет производиться только если нет другого выхода.
Я верю Крису Дауну, поскольку считаю что он отличный инженер и знает что говорит, когда объясняет что swap-файл позволяет системе лучше работать. Поэтому, ожидая, что, «что-то» пойдет «не так» и возможно система будет ужасно неэффективно работать, я заранее подстраховался и запустил тестовую программу, лимитировав её таймером, чтобы увидеть хотя бы ее аварийное завершение.
Сначала рассмотрим вывод top:
Tasks: 302 total, 1 running, 301 sleeping, 0 stopped, 0 zombie
%Cpu(s): 0,2 us, 4,7 sy, 0,0 ni, 84,6 id, 10,0 wa, 0,4 hi, 0,0 si, 0,0 st
MiB Mem : 15670,0 total, 162,8 free, 1077,0 used, 14430,2 buff/cache
MiB Swap: 20480,0 total, 18164,6 free, 2315,4 used. 690,5 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
6127 viking 20 0 17,0g 13,5g 13,5g D 20,2 87,9 0:10.24 swapdemo
136 root 20 0 0 0 0 S 17,2 0,0 2:15.50 kswapd0
Ура?! Свап используется всего лишь на 2.5 гигабайт, что почти 2 в два раза меньше чем в тесте со включенным swap (и swappiness=60). Свапа используется меньше. Свободной памяти тоже меньше. И наверное, мы можем смело отдать победу молодым экспертам. Но вот что странно — наша программа так и не смогла завершить даже 1 (ОДНОЙ!) итерации за 2 (ДВЕ!) минуты:
$ { sleep 120 ; killall swapdemo ; } &
[1] 6121
$ time ./swapdemo
Preparing test …
Doing test …
Iteration 1 of 11
[1]+ Done { sleep 120; killall swapdemo; }
Terminated
real 1m58,791s
user 0m0,871s
sys 0m23,998s
Повторим — программа не смогла завершить 1 итерацию за 2 минуты хотя в предыдущем тесте она сделала 11 итераций за 2 минуты — то есть с почти отключенным свапом программа работает более чем в 10(!) раз медленнее.
Но есть один плюс — ни одной вкладки Chrome не пострадало. И это хорошо.
Тест с полным отключением swap
Но может быть, просто «задавить» свап через swappiness недостаточно, и его надо полностью отключать? Естественно, что надо проверить и эту теорию. Мы сюда тесты пришли провести, или что?
Это идеальный случай:
- унас нет свопа и все наши данные будут гарантированно в памяти
- свап не будет использоваться даже случайно, потому что его нет
И теперь наш тест завершится со скоростью молнии, старики пойдут на заслуженное ими место и будут менять картриджи — дорогу молодым.
К сожалению, результат запуска тестовой программы аналогичный — не завершилось даже одной итерации.
Вывод top:
Tasks: 217 total, 1 running, 216 sleeping, 0 stopped, 0 zombie
%Cpu(s): 0,0 us, 2,2 sy, 0,0 ni, 85,2 id, 12,6 wa, 0,0 hi, 0,0 si, 0,0 st
MiB Mem : 15670,0 total, 175,2 free, 331,6 used, 15163,2 buff/cache
MiB Swap: 0,0 total, 0,0 free, 0,0 used. 711,2 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
136 root 20 0 0 0 0 S 12,5 0,0 3:22.56 kswapd0
7430 viking 20 0 17,0g 14,5g 14,5g D 6,2 94,8 0:14.94 swapdemo
Почему это происходит
Объяснение очень простое — “сегмент кода” который мы подключаем через mmap (libptr) лежит в кэше. Поэтому когда мы запрещаем (или почти запрещаем) swap тем или иным способом, не важно каким — физическим ли отключением swap, или через vm.swappines=0|1 — это всегда заканчивается одним и тем же сценарием — вымыванием mmap’нутого файла из кэша и последующей его загрузкой с диска. А библиотеки загружаются именно через mmap, и чтобы убедиться в этом, достаточно просто сделать ls -l /proc//map_files:
$ ls -l /proc/8253/map_files/ | head -n 10
total 0
lr-------- 1 viking viking 64 фев 7 12:58 556799983000-55679998e000 -> /usr/libexec/gnome-session-binary
lr-------- 1 viking viking 64 фев 7 12:58 55679998e000-5567999af000 -> /usr/libexec/gnome-session-binary
lr-------- 1 viking viking 64 фев 7 12:58 5567999af000-5567999bf000 -> /usr/libexec/gnome-session-binary
lr-------- 1 viking viking 64 фев 7 12:58 5567999c0000-5567999c4000 -> /usr/libexec/gnome-session-binary
lr-------- 1 viking viking 64 фев 7 12:58 5567999c4000-5567999c5000 -> /usr/libexec/gnome-session-binary
lr-------- 1 viking viking 64 фев 7 12:58 7fb22a033000-7fb22a062000 -> /usr/share/glib-2.0/schemas/gschemas.compiled
lr-------- 1 viking viking 64 фев 7 12:58 7fb22b064000-7fb238594000 -> /usr/lib/locale/locale-archive
lr-------- 1 viking viking 64 фев 7 12:58 7fb238594000-7fb2385a7000 -> /usr/lib64/gvfs/libgvfscommon.so
lr-------- 1 viking viking 64 фев 7 12:58 7fb2385a7000-7fb2385c3000 -> /usr/lib64/gvfs/libgvfscommon.so
И, как мы рассматривали в первой части статьи, система в условиях фактической нехватки памяти при отключенном свапинге анонимных страниц выберет единственный вариант который её оставил владелец, отключивший свап. И этот вариант — реклейминг (освобождение) чистых страниц, занимаемых под данные mmap-нутых библиотек.
Заключение
Активное использование методики распространения программ «всё свое везу с собой» (flatpak, snap, docker image) приводит к тому, что количество кода, который подключается через mmap, существенно увеличивается.
Это может привести к тому, что использование «экстремальных оптимизаций», связанных с настройкой/отключением swap, может привести к совершенно неожиданным эффектам, потому, что swap-файл — это механизм оптимизации подсистемы виртуальной памяти в условиях memory pressure, а available memory это совсем не «неиспользуемая память», а сумма размеров кэша и свободной памяти.
Отключая swap-файл, вы не «убираете неправильный вариант», а «не оставляете вариантов»
Следует очень осторожно интерпретировать данные о потреблении памтяи процессом — VSS и RSS. Они отображают «текущее состояние» а не «оптимальное состояние».
Если вы не хотите, чтобы система использовала свап — добавьте ей памяти но не отключайте свап. Отключение свапа на пороговых уровнях сделает ситуацию значительно хуже, чем она была бы, если бы система немного отсвапилась.
P.S.: В обсуждениях регулярно задаются вопросы «а вот если включить сжатие памяти через zram...». Мне стало интересно, и я провел соответствующие тесты: если включить zram и swap, как это сделано по умолчанию в Fedora, то время работы ускоряется примерно до 1 минуты.
Но причина этого то, что страницы с нулями очень хорошо сжимаются, поэтому на самом деле данные уезжают не в swap, а хранятся в сжатом виде в оперативной памяти. Если заполнить сегмент данных случайными плохосжимаемыми данными, картина станет не такой эффектной и время работы теста опять же увеличится до 2 минут, что сравнимо (и даже чуть хуже), чем у «честного» swap-файла.
Автор: outlingo