Знакомьтесь: шумный сосед
Знакомьтесь: шумный сосед

Приходилось ли вам запускать на одной виртуальной или физической машине несколько экземпляров PostgreSQL или любого другого ПО? Зачастую это вызывает эффект шумного соседа: приложения «отбирают» друг у друга ресурсы и мешают корректной работе. Мы не понаслышке знакомы с этой проблемой и готовы рассказать о способах борьбы с эффектом шумного соседа. Если коротко — запустите приложение в контейнере.

Контейнеры

Преимущества контейнеров, например Docker: удобный интерфейс, возможность скачать готовый образ на Docker Hub или собрать его самостоятельно, а затем использовать для развёртывания.

Есть и недостатки: Docker-контейнеры не всегда легко интегрировать в сертифицированные среды — нужно уметь управлять ими и поддерживать работоспособность. Проблемы могут возникать и при высокой нагрузке на сетевой стек (оверхед). 

Контрольные группы (cgroups)

В Linux удобно управлять ресурсами с помощью встроенных контрольных групп (cgroups), которые поставляются «из коробки». Этот механизм хорошо отлажен, вероятность оверхеда исключена.

Control Groups (cgroups) позволяют ограничивать ресурсы, доступные процессам ОС, и управлять ими. Среди таких ресурсов — CPU, память, устройства ввода-вывода. C помощью cgroups можно группировать процессы по типу задачи, запускать PostgreSQL во временной контрольной группе и накладывать временные ограничения на ресурсы, которые снимаются по завершении процесса.

Особенности архитектуры cgroups:

  • cgroups v2, в отличие от v1, подразумевает строгую иерархию: каждый процесс может принадлежать только одной контрольной группе;

  • корневая группа находится в /sys/fs/cgroup;

  • контроллер в родительской группе влияет и на дочерние: если отключить его в родительской группе, он отключится и в дочерних. То же касается ограничений: заданные на родительском уровне, они распространяются и на дочерние группы;

  • управлять cgroups можно через systemd — систему с удобным интерфейсом и гибкими ограничениями.

Как настроить cgroups

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

Дано: 

  • сервер СУБД на Debian 12 (2 сокета по 4 ядра, 16 ГБ RAM);

  • 2 экземпляра PostgreSQL 16;

  • 1 DBA.

Для управления и мониторинга cgroups служат утилиты:

  • systemd-cgtop —  утилита top только для контрольных групп.

  • systemd-cgls — утилита для отображения иерархической структуры контрольных групп. Можно отследить родительские и дочерние контрольные группы.

  • systemctl status postgresql@16-main.service — стандартный вывод статуса юнита с ограничениями, которые используются для сервисов или процессов.

Пример 1

Рассмотрим вывод утилиты systemd-cgtop с двумя инстансами: main и second, которые занимают 2,6 и 2,7 ГБ соответственно и указывают количество задействованных ядер процессора.

Шаг 1. Командой systemctl show -p Memory Max <имя_юнита> ограничим максимально допустимую память для инстансов PostgreSQL, запущенных с помощью systemd:

# systemctl show -p MemoryMax postgresql@16-main.service
MemoryMax=16661413888 
# systemctl show -p MemoryMax postgresql@16-second.service
MemoryMax=16661413888

где MemoryMax=16661413888 — максимально допустимая память для инстансов (в байтах).

Шаг 2. Командой systemctl set-property <имя_юнита> MemoryMax=40% установим ограничение памяти в 40% от максимума для обоих экземпляров:

# systemctl set-property postgresql@16-main.service MemoryMax=40%
# systemctl show -p MemoryMax postgresql@16-main.service
MemoryMax=6664564736
# systemctl set-property postgresql@16-second.service MemoryMax=40%
# systemctl show -p MemoryMax postgresql@16-second.service
MemoryMax=6664564736

Видим, что ограничение установлено примерно на 6,2 ГБ, текущий расход памяти — 22,9 МБ (то есть нагрузки нет), а максимальное значение — 15,5 ГБ.

Шаг 3. Создадим разную нагрузку на отдельные инстансы с помощью скрипта:

set work_mem = '1GB';
select * from generate_series(1,1000000) as a inner join
generate_series(1,1000000) as b on a=b inner join
generate_series(1,10000000) as c on b=c;

Шаг 4. Запустим скрипт:

pgbench -c 10 -C -T 1000 -f load_script.sql -n -p 5432 # инстанс main
pgbench -c 5 -C -T 1000 -f load_script.sql -n -p 5433 # инстанс second

Спустя время замечаем, что второй инстанс продолжает работать в пределах установленных ограничений, то есть расходует 2,7 ГБ и не имеет проблем.

Где же основной инстанс main? Он израсходовал выделенный ему лимит, и его остановил out of memory (OOM) killer.

Вывод dmesg:

[2928950.287414] memory: usage 6508364kB, limit 6508364kB
[2928950.287490] oom-kill:constraint=CONSTRAINT_MEMCG,nodemask=(null),cpuset=postgresql@16-
main.service,mems_allowed=0-1,oom_memcg=/system.slice/system-
postgresql.slice/postgresql@16-main.service,task_memcg=/system.slice/system-
postgresql.slice/postgresql@16-main.service,task=postgres,pid=301415,uid=114
[2928950.287507] Memory cgroup out of memory: Killed process 301415
(postgres) total-vm:1451176kB, anon-rss:874168kB, file-rss:62684kB, shmem-
rss:3268kB, UID:114 pgtables:2116kB oom_score_adj:0

Memory cgroup out of memory означает, что процесс был остановлен непосредственно в контрольной группе. Рекомендуем использовать директиву oom_group_kill, чтобы OOM Killer гарантированно отключал контрольную группу после достижения лимита памяти. Если же установить Limit High меньше, чем Memory Max, память освобождается или путём выгрузки в swap, или посредством возвращения файловых страниц на сами файлы, поэтому Limit High предпочтителен.

Пример 2

Рассмотрим ограничение CPU для тех же экземпляров PostgreSQL — main и second:

pgbench -c 30 -j 30 -T 1000 --progress 5 -p 5432 -n --protocol extended
pgbench -c 30 -j 30 -T 1000 --progress 5 -p 5433 -n --protocol extended

Шаг 1. Запустим оба бенчмарка: количество TPS за первую минуту работы на обоих инстансах примерно одинаково. 

Шаг 2. Урежем лимит времени работы на CPU для инстанса main в два раза:

systemctl set-property postgresql@16-main.service CPUQuota=50%

С 65-й секунды количество TPS на первом инстансе уменьшилось вдвое, а второй инстанс продолжает работать без изменений.

Шаг 3. Изменим лимит работы для инстанса second: передадим ему те 50%, которые забрали у первого инстанса:

systemctl set-property postgresql@16-second.service CPUQuota=150%

С сотой секунды наблюдаем прирост TPS, а чем больше процессорного времени, тем больше задач можно выполнить.

У CPU существуют weight-контроллеры — приоритеты для одноранговых контрольных групп — их можно использовать для равномерного распределения процессорного времени между несколькими инстансами.

Пример 3

Рассмотрим привязку групп процессов к определённому узлу CPU. В нашей конфигурации ядра с нулевого до третьего находятся на нулевом узле, с четвёртого по седьмое — на первом узле:

#numactl -H

available: 2 nodes (0-1)
node 0 cpus: 0 1 2 3
node 0 size: 7933 MB
node 0 free: 204 MB
node 1 cpus: 4 5 6 7
node 1 size: 7955 MB
node 1 free: 4926 MB
node distances:
node 0 1
0: 10 20
1: 20 10

Шаг 1. Сгенерируем нагрузку с помощью pgbench и понаблюдаем с помощью pidstat, какие CPU использует postgres:

#pidstat -C "postgres" -u -G "postgres"

Видим, что PostgreSQL использует все ядра, доступные системе. На больших машинах это может привести к потере производительности из-за неравномерного времени доступа к памяти.

Шаг 2. Закрепим использование CPU за нулевым узлом:

#systemctl set-property postgresql@16-main.service AllowedCPUs=0-3

Шаг 3. Запустим нагрузку.

Видим, что CPU работает на нулевом узле, то есть мы привязали конкретно CPU к конкретной ноде. То же самое можно сделать для узлов памяти, выставив параметр AllowMemoryNodes. Если указать нулевое значение, то память будет располагаться только на нулевом узле.

Мониторинг

Псевдофайловая система cgroups позволяет вести мониторинг с помощью следующих файлов:

  • *.pressure (PSI, Pressure Stall Information);

  • *.stat;

  • *.current;

  • *.events.

PSI

PSI (Pressure Stall Information) — средство реактивного мониторинга, которое помогает понять, где мы теряем производительность. PSI предоставляет данные о процессах, находящихся в ожидании ресурса (CPU, memory, I/O) 10, 60 или 300 с.

Пример вывода:

some avg10=54.82 avg60=13.98 avg300=3.10 total=10452199
full avg10=0.78 avg60=0.21 avg300=0.04 total=217371

some avg10=54.82 показывает, какую часть наблюдаемого периода процесс простаивал. В нашем случае наблюдение продолжалось 10 с, а половину времени (примерно 5 с) процесс простаивал. 

full avg10=0.78 показывает, какую часть наблюдаемого периода все процессы в системе находились в ожидании ресурса. 

Файлы stat 

Файлы с расширением .stat содержат статистическую информацию о ресурсах, которые используют группы процессов (CPU, memory, I/O). 

Пример вывода cpu.stat:

# cat cpu.stat

usage_usec 860802
user_usec 255721
system_usec 605080
nr_periods 287
nr_throttled 0
throttled_usec 0
nr_bursts 0
burst_usec 0

где:

  • usage_usec — общее время использования;

  • user_usec — время, в течение которого процесс находился в пространстве;

  • system_usec — время, в течение которого процесс находился в системном пространстве; 

  • nr_periods — количество периодов; 

  • nr_throttled — количество пропущенных циклов процесса (тротлинга);

  • throttled_usec — времея, затраченное на тротлинг;

  • nr_bursts — количество случаев превышения лимита.

Файлы events 

Файлы с расширением .events содержат информацию о событиях, произошедших в этой контрольной группе. Существуют events-файлы для memory, memory.swap, misc и pids.

Пример вывода memory.events:

# cat memory.events

low 0
high 11899
max 0
oom 0
oom_kill 0
oom_group_kill 0

где:

  • low — сколько раз мы достигали нижней границы; 

  • high — сколько раз упирались в верхний предел;

  • max — сколько раз достигали максимального предела;

  • oom — сколько раз мы были на грани того, что придёт OOM killer;

  • oom_kill — сколько раз OOM killer приходил за этой контрольной группой;

  • oom_group_kill — сколько раз OOM killer приходил за всей контрольной группой. Если задать параметру oom_group_kill значение true, то при срабатывании OOM killer будут завершаться все процессы в контрольной группе. В PostgreSQL при срабатывании OOM killer в происходит перезагрузка СУБД.

Файлы current

Файлы с расширением .current содержат информацию о текущем объёме используемого ресурса, включая кэши, страницы, структуры данных ядра и т. д. Существуют current-файлы для memory, memory.swap, memory.zswap (сжатый swap), misc, pids и rdma.

Почему cgroups

cgroups позволяют:

  • избежать эффекта шумного соседа: выделить приоритетным инстансам больше ресурсов и правильно распределить нагрузку;

  • защититься от злоупотребления системными ресурсами;

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

Кроме того, cgroups доступны «из коробки» в Linux.


Делитесь своим опытом использования cgroups в комментариях!

Комментарии (3)


  1. rampler
    13.02.2025 10:12

    когда-нибудь запускали несколько экземпляров PostgreSQL или другого ПО на одной машине 

    Никогда . Особенно в продуктивном контуре.

    А зачем ?


  1. badangel
    13.02.2025 10:12

    Что-то мешает одной СУБД держать множество баз? И какие-то выдуманные недостатки у Docker.


    1. GrMax
      13.02.2025 10:12

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