О памяти

2020-08-22

Память, она мне нужна. Ноутбук я выбирал, чтобы впихнуть туда как можно больше памяти. Больше 16 гигабайт не влезло. 8 гигов распаяно на материнке. 8 гигов я доставил плашкой DDR4.

Хотя вот сейчас рою спеки. Вообще-то процессор (а контроллер памяти нынче пихают в процессор) умеет до 32 гигов. А в мой UX310UA обычно распаивают 4 гига, а не 8. То бишь, вроде как можно мне заполучить ещё 8 гигов, если воткнуть плашку на 16 гигабайт. Есть у кого попробовать? 😉

Зачем нужна память? Ну, я в работе постоянно использую Intellij IDEA. А она спокойно сжирает пару гигабайт и больше. И в Докере запускаю кучу разных сервисов. И в Хроме постоянно что-то открыто, Gmail и всякие гуглодокументы.

Проблема c Java, Chrome и многими другими приложениями в том, что они видят всю системную память. Все мои 16 гигабайт. И какого-то чорта считают, что все эти 16 гигабайт — их собственные. И пытаются их отожрать.

Ладно, Java сервисы и IDEA я постоянно перезапускаю. Но Chrome через несколько дней аптайма сжирает всё. Сначала кончается память. Потом кончается свап. Потом всё встаёт колом и приходится перезагружаться. Конечно же это происходит в тот момент, когда мне позарез нужно прогнать интеграционные тесты с Elasticsearch в Докере, которым ну вот ещё пару гигабайт памяти нужно прямо сейчас.

Chrome eats memory

На примере Java видно, насколько прожорливость приложений действительно зависит от доступной им памяти. Раньше в JRE 8 был баг. Если запустить Джаву в Докер контейнере с ограниченным объёмом памяти, она не видела этого ограничения, а видела общий объём памяти хоста. Она сразу заводила большущий heap, и ещё несколько раз его расширяла. Редко запускала сборку мусора. И довольно быстро упиралась в границы контейнера и дохла с OutOfMemoryError.

Тогда это чинилось флагом -XX:MaxRAM=256M. Мы просто говорим, что памяти меньше. И JRE спокойно ужимается до значительно меньшего объёма. И работает. Конечно, обычные ограничения на размер heap, тот же флаг -Xmx, тоже работает. Но heap — это не единственная память, нужная Java, поэтому -XX:MaxRAM для контейнера надёжнее.

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

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

А всю память, как оказалось, часто съедает Chrome. Берёшь и перезапускаешь все хромовые вкладки, отдельные окна (c Gmail) и процессы. Полностью. И, хоба, снова почти такая свежая система с пустой памятью, как после перезагрузки.

Получается, что если приложение видит много памяти, оно старается её съесть. А можно ли Chrome сказать, что ему доступно немного памяти? С помощью тех же cgroups?

Cgroups — это control groups. Контрольные группы процессов. Чтобы за ними следить и управлять. Этот механизм ядра повсеместно используется всяческими разными менеджерами контейнеров под Linux, включая и Докер. Кому, если не ему, ограничивать разные пользовательские процессы?

Оказывается, МОЖНО. Нужно поставить пакет cgroup-tools, скопировать несколько файлов конфигурации из /usr/share/doc/cgroup-tools/examples/ в /etc/ и настроить запуск двух команд как системных сервисов.

Файл номер раз. /etc/cgred.conf.

# /etc/sysconfig/cgred.conf - CGroup Rules Engine Daemon configuration file

# The pathname to the configuration file for CGroup Rules Engine
CONFIG_FILE="/etc/cgrules.conf"

# Uncomment the following line to log to specified file instead of syslog
#LOG_FILE="/var/log/cgrulesengd.log"

# Uncomment the second line to run CGroup Rules Engine in non-daemon mode
NODAEMON=""
#NODAEMON="--nodaemon"

# Set owner of cgred socket. 'cgexec' tool should have write access there
# (either using suid and/or sgid permissions or Linux capabilities).
SOCKET_USER=""
SOCKET_GROUP="cgred"

# Uncomment the second line to disable logging for CGroup Rules Engine
# Uncomment the third line to enable more verbose logging.
LOG=""
#LOG="--nolog"
#LOG="-v"

Это конфигурация CGroup Rules Engine Daemon. Это действительно демон cgrulesengd, который следит за процессами в системе и засовывает их в группы согласно правилам. Собственно, это и есть способ навешать ограничения на процессы, не меняя способ их запуска. То есть как Хром запускали из главного меню, так и будем запускать. А демон уже его найдёт.

Второй файл. /etc/cgconfig.conf.

group chrome {
    memory {
        memory.limit_in_bytes = 4g;
    }
}

Это объявление групп и ограничений в них. За ограничениями следят контроллеры (controllers) в ядре. Как минимум нам доступны cpu и memory. Можно и ЦПУ прирезать. Но пока сосредоточимся на памяти.

У memory контроллера много параметров. Здесь мы просто ставим ограничение на доступную память.

Третий файл. /etc/cgrules.conf.

# /etc/cgrules.conf
#
# Example:
#<user>         <controllers>   <destination>
#@student       cpu,memory      usergroup/student/
#peter          cpu             test1/
#%              memory          test2/

gelin:/opt/google/chrome/chrome         memory          chrome

# End of file

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

Проверка.

Загрузить конфигурацию групп из файла командой cgconfigparser.

# /usr/sbin/cgconfigparser -l /etc/cgconfig.conf

Запустить нашего демона.

# /usr/sbin/cgrulesengd -vvv

В /sys/fs/cgroup/memory должна появиться наша группа.

$ ls -1 --group-directories-first /sys/fs/cgroup/memory/ | head -5 
chrome
docker
machine.slice
system.slice
user.slice

Внутри будут файлы со всеми ограничениями группы. Можно проверить, что ограничение на память стоит. Наши четыре гигабайта.

$ cat /sys/fs/cgroup/memory/chrome/memory.limit_in_bytes 
4294967296

И самое главное. Нужно убедиться, что наш демон нашёл процессы Chrome.

$ cat /sys/fs/cgroup/memory/chrome/tasks | head -5 
  3682
  3701
  3702
  3705
  3709

Это PIDы всех процессов Chrome. Даже не процессов, а задач (tasks). Можете проверить.

Чтобы это всё осталось жить, нужно добавить пару Systemd Unit.

Инициализация групп при старте системы. Файл /etc/systemd/system/cgconfigparser.service.

[Unit]
Description=cgroup config parser
After=network.target

[Service]
User=root
Group=root
ExecStart=/usr/sbin/cgconfigparser -l /etc/cgconfig.conf
Type=oneshot

[Install]
WantedBy=multi-user.target

Запуск демона. Файл /etc/systemd/system/cgrulesgend.service.

[Unit]
Description=cgroup rules generator
After=network.target cgconfigparser.service

[Service]
User=root
Group=root
Type=forking
EnvironmentFile=-/etc/cgred.conf
ExecStart=/usr/sbin/cgrulesengd
Restart=on-failure

[Install]
WantedBy=multi-user.target

Не забудьте:

# systemctl daemon-reload
# systemctl enable cgconfigparser
# systemctl enable cgrulesgend

А эти cgroups действительно работают?

Chrome у нас порождает довольно много процессов. Как увидеть общий объём занимаемой ими памяти?

Большинство менеджеров процессов не умеют отображать данные сразу по группе одноимённых процессов. А вот atop умеет. Если его запустить, и нажать m для показа информации о памяти, а затем p для суммирования по "program (i.e. same process name)" (или просто запустить как atop -mp), то, в нижней половине экрана, можно увидеть примерно это:

NPROCS  SYSCPU  USRCPU   VSIZE   RSIZE  PSIZE  SWAPSZ  RDDSK  WRDSK  RNET  SNET  MEM  CMD       1/10
    36   0.10s   1.25s  162.3G    4.4G     0K  431.6M     0K     0K     0     0  28%  chrome
     4   0.80s  74.40s   17.2G    2.6G     0K      0K     0K     0K     0     0  17%  java
     5   0.02s   0.02s    4.2G    1.4G     0K      0K     0K     0K     0     0   9%  upwork
     1   0.04s   0.33s    3.3G  896.5M     0K    104K     0K     0K     0     0   6%  latte-dock
     4   0.00s   0.10s   25.1G  877.5M     0K  70704K     0K     0K     0     0   6%  slack
     8   0.00s   0.01s   15.8G  542.1M     0K      0K     0K     0K     0     0   3%  jetbrains-tool
     1   0.18s   0.41s    2.1G  484.9M     0K  40364K     0K     0K     0     0   3%  Xorg
     4   0.02s   0.53s    4.0G  478.6M     0K      0K     0K     0K     0     0   3%  insomnia
     1   0.05s   0.11s    2.5G  361.0M     0K   4248K     0K     0K     0     0   2%  Telegram

Тут у нас присутствует аж несколько разных *SIZE и *SZ. Что всё это значит?

VSIZE — это объём виртуальной памяти процессов. Виртуальная память — это виртуальное адресное пространство, выделенное процессу. Аж 162 гигабайта у Хрома, как видите. Далеко не всё это пространство действительно представлено в физической памяти. Что-то может уйти в свап. А что-то само по себе является отображением файлов в память.

RSIZE — это объём резидентной памяти процессов. Также известен как resident set size (RSS). Это физическая память, занятая процессом. Сюда также включаются разделяемые библиотеки, возможно, поэтому тут всё же больше 4 гигабайт.

PSIZE — это proportional memory size. Тут пытаются учесть, что разделяемые библиотеки используются несколькими процессами, и поделить используемую ими память между ними. По умолчанию atop этот параметр не считает, потому что это сложно. Но если добавите ключик -R или нажмёте R, то окажется, что PSIZE для Chrome раза в два меньше, чем RSIZE.

Ох, непростое это дело, попытаться честно подсчитать использование памяти.

SWAPSZ — это размер свапа процессов. Хоть мы, вроде, и ограничили Хром в пожирании памяти, в свап он всё равно лезет. Ну и пусть. Это, косвенно, подтверждает, что ему тесновато, и ограничение по памяти работает. Если что, в cgroup можно и свап прирезать.

Virtual memory

Можно поступить попроще и пошаманить с ps и grep. Вот только у ps тоже много вариантов подсчитать размер памяти.

$ ps -eo rssize,size,vsize,command | grep chrome | head -5 
404232 657240 1252856 /opt/google/chrome/chrome
17496 12376 271832 /opt/google/chrome/chrome --type=zygote --no-zygote-sandbox
20600 12376 271832 /opt/google/chrome/chrome --type=zygote
 2012  2656  10792 /opt/google/chrome/nacl_helper
 7820 12376 271832 /opt/google/chrome/chrome --type=zygote

rsssize — это RSS, резидентная память. vsize — это виртуальная память.

size — это "approximate amount of swap space that would be required if the process were to dirty all writable pages and then be swapped out. This number is very rough!" Получается, это некий объём свапа, который понадобится процессу, если его весь придётся срочно скинуть в свап и полностью освободить память. Ну, как-то ps это считает. Я говорил, что это непросто?

Чтобы просуммировать по всем процессам Хрома, пошаманим с awk.

$ ps -eo rssize,size,vsize,command | grep chrome \
| awk '{ r=$1/1024/1024; s=$2/1024/1024; v=$3/1024/1024; sumr+=r; sums+=s; sumv+=v } END {print sumr, sums, sumv}'
4.5276 8.60119 163.155

Получились показания, совпадающие с выводом atop (Хром немного шевелился между замерами). А size оказался раза в два больше RSS. 🤷

После недели экспериментов могу сказать, что, вроде, работает. Хотя без atop, в обычном системном мониторе ничего особо не видно. Вся память chrome размазана по куче мелких процессов.

KDE System Monitor

На фоне присмиревшего Chrome видны другие пожиратели памяти. Например, клиент Upwork и мой любимый Latte-Dock. Чего этот док жрёт столько heapа, не знаю.

Latte-Dock memory usage

Надо попробовать отказаться от этого дока, раз он так память жрёт. Попробовал его так же через cgroup впихнуть в 256 мегабайт, он тормозил и сдох через часик. На 512 мегабайтах вроде шевелится.

А Chrome на четырёх гигабайтах работает вроде нормально. Тормозов или неудобств не ощущаю. Но CPU по-прежнему готов весь съесть. Если Google Meet работает, не пытайтесь открыть Google Docs, будет грузить документ пару минут.

Есть ещё, что оптимизировать.