![Знакомьтесь: шумный сосед Знакомьтесь: шумный сосед](https://habrastorage.org/getpro/habr/upload_files/b21/25a/f9d/b2125af9d1661c906359d6b429618277.png)
Приходилось ли вам запускать на одной виртуальной или физической машине несколько экземпляров 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 ГБ соответственно и указывают количество задействованных ядер процессора.
![](https://habrastorage.org/getpro/habr/upload_files/2be/530/757/2be53075795e91c1538322faea367ff1.png)
Шаг 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 ГБ.
![](https://habrastorage.org/getpro/habr/upload_files/3bf/7c3/402/3bf7c3402ed540561182fa50d0d613e7.png)
Шаг 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 ГБ и не имеет проблем.
![](https://habrastorage.org/getpro/habr/upload_files/b2d/53c/49b/b2d53c49bc80e1a73cb59d743f286164.png)
Где же основной инстанс 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 за первую минуту работы на обоих инстансах примерно одинаково.
![](https://habrastorage.org/getpro/habr/upload_files/75b/182/ac6/75b182ac6a696071c3b5ae15f16d7b24.png)
Шаг 2. Урежем лимит времени работы на CPU для инстанса main в два раза:
systemctl set-property postgresql@16-main.service CPUQuota=50%
С 65-й секунды количество TPS на первом инстансе уменьшилось вдвое, а второй инстанс продолжает работать без изменений.
![](https://habrastorage.org/getpro/habr/upload_files/3b1/ed5/3e6/3b1ed53e643541d1f243eab99f0add96.png)
Шаг 3. Изменим лимит работы для инстанса second: передадим ему те 50%, которые забрали у первого инстанса:
systemctl set-property postgresql@16-second.service CPUQuota=150%
С сотой секунды наблюдаем прирост TPS, а чем больше процессорного времени, тем больше задач можно выполнить.
![](https://habrastorage.org/getpro/habr/upload_files/4e7/0b5/79b/4e70b579b069e705abc05116ed471aa2.png)
У 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"
![](https://habrastorage.org/getpro/habr/upload_files/0fd/7d8/8bc/0fd7d88bca4ed8166ae4a2b1292aa4fb.png)
Видим, что PostgreSQL использует все ядра, доступные системе. На больших машинах это может привести к потере производительности из-за неравномерного времени доступа к памяти.
Шаг 2. Закрепим использование CPU за нулевым узлом:
#systemctl set-property postgresql@16-main.service AllowedCPUs=0-3
Шаг 3. Запустим нагрузку.
![](https://habrastorage.org/getpro/habr/upload_files/e72/769/c7d/e72769c7d037730f4f808429ee5f9fc3.png)
Видим, что 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 в комментариях!
rampler
Никогда . Особенно в продуктивном контуре.
А зачем ?