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

Вопрос этой статьи практический: сколько оперативной памяти держать в запасе на VPS, чтобы рабочие и служебные операции не мешали друг другу. Универсального процента здесь нет, и дальше станет понятно почему. Зато есть способ собрать расчёт из частей, проверить каждую часть измерением и оставить коллеге записку, по которой он поймёт, зачем эти гигабайты простаивают.

Две разные нагрузки под одним графиком

Первое, что стоит сделать, — перестать думать о памяти сервера как об одном числе. Там живут как минимум две разные категории потребления.

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

Вторая — редкие тяжёлые действия. Сборка нового релиза, миграция схемы базы, создание дампа, сжатие архива, проверка копии, восстановление. Они запускаются раз в сутки, раз в неделю или раз в квартал. На длинном графике они выглядят как короткие иголки, а то и вовсе теряются при усреднении по пятиминутным или часовым интервалам. Если мониторинг сохраняет усреднённые точки, иголка в две минуты превращается в едва заметную неровность.

Разница принципиальна для выбора запаса. Привычная нагрузка определяет, сколько памяти нужно, чтобы просто работать. Редкие операции определяют, сколько нужно, чтобы работать и при этом что-то менять или чинить. Запас — это не абстрактная подушка «на всякий случай», а оплата конкретных будущих событий. Если не назвать события, невозможно сказать, достаточна ли подушка.

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

Когда пики встречаются

Каждый тяжёлый процесс по отдельности обычно помещается в сервер. Беда начинается, когда их пики совпадают по времени. Поэтому самый полезный вопрос звучит не «сколько ест релиз?», а «что ещё работает в эти минуты?».

Возьмём условный пример, не измерение с реального проекта, а модель для тренировки расчёта. Сервер на 4 ГБ оперативной памяти: тариф уровня Uab из публичного предложения MAATRIX, 2 vCPU, 4 ГБ. На нём живут приложение, PostgreSQL и веб-сервер. В обычный день вместе они занимают около 2,2 ГБ. Остаётся 1,8 ГБ, и это выглядит как щедрый запас.

Теперь распишем сценарий четверга. Ночью по расписанию идёт резервное копирование: утилита дампа читает базу, а сжатие архива грузит процессор и держит буферы. Допустим, на это уходит 400 МБ сверх обычного. В тот же вечер команда выкатывает релиз: новые зависимости собираются прямо на сервере, миграция перестраивает индекс, а на время переключения рядом живут старая и новая версии приложения. Допустим, это ещё 900 МБ. Если эти два события разведены на несколько часов, всё в порядке. Но достаточно сдвинуть окно копирования, выкатить срочный фикс в неурочное время или запустить проверочное восстановление, и суммируются не 900, а 1300 МБ плюс всплеск от базы.

Условный расчёт на сервере 4 ГБ (числа учебные)
СоставляющаяПамять, МБКомментарий
Обычная работа: приложение, база, веб-сервер2200Видно на графике
Релиз: сборка, миграция, две версии рядом900Короткий пик
Копия: дамп и сжатие400Короткий пик
Сумма при пересечении3500Остаётся около 500 МБ на всё остальное

Остаток в 500 МБ, из которых надо ещё оплатить файловый кеш, системные службы и всплеск пользовательской нагрузки, уже не выглядит запасом. А ведь в графике «обычного дня» нигде не было красных зон. Вот в чём главная ловушка: запас считают от пустоты на графике, а не от суммы худшего совпадения.

Обратите внимание, что мы не складываем все возможные пики вообще, иначе придётся покупать сервер размером с офис. Мы складываем те, которые могут совпасть по расписанию или по человеческим привычкам. Релиз и копия совпасть могут: оба любят ночь и тихие часы. Релиз и восстановление из копии тоже: восстановление часто происходит ровно после неудачного релиза. А вот квартальная проверка аварийного сценария и плановая миграция схемы — если вы сами разнесли их по календарю и записали это правило, — можно в одну сумму не класть.

Как разнести пики вместо покупки гигабайтов

Иногда дешевле и честнее не увеличивать запас, а разделить события во времени. Окно копирования переносят так, чтобы оно заканчивалось до обычного времени релизов. В процедуре выпуска добавляют шаг «убедиться, что копия не идёт» или, наоборот, «убедиться, что копия последней ночи завершилась». Такое правило стоит ноль рублей, но работает только пока его помнят, поэтому его надо записывать рядом с расчётом, а не держать в голове у одного человека.

Вместе с тем у разнесения есть предел. Восстановление копии невозможно запланировать: оно приходит с аварией. А значит, под него запас придётся оставлять в любом случае, и именно оно часто становится самым тяжёлым пунктом таблицы.

Свободно, занято, доступно и что делает подкачка

Когда расчёт готов, возникает следующая трудность: с чем сравнивать результат? Строка «свободно» в выводе системных утилит редко отвечает на вопрос, сколько реально осталось. Ядро использует «лишнюю» память под файловый кеш: недавно прочитанные с диска данные держатся в оперативной памяти, чтобы повторные чтения были быстрее. Если кешу понадобится уступить место, ядро обычно может его вытеснить. Поэтому у сервера, который давно работает, «свободная» память стремится к малой величине, и это не признак беды. Полезнее смотреть на показатель доступной памяти, то есть на оценку того, сколько можно выделить, не уходя в подкачку.

Но осторожность нужна и здесь. Кеш — это не бесплатная память на все случаи. Если вы рассчитываете, что при релизе «освободится» кеш на гигабайт, вы одновременно рассчитываете, что база начнёт чаще читать с диска и станет медленнее ровно в тот момент, когда на неё идёт миграция. Запас, сложенный из кеша, превращается не в отказ, а в замедление, которое труднее заметить и объяснить. Поэтому при расчёте пика разумно считать кеш мягкой подушкой, а не твёрдым резервом.

Подкачка (swap) устроена иначе и вызывает больше всего иллюзий. Она позволяет системе вытеснить редко используемые страницы на диск и освободить оперативную память под активные. Это полезный механизм сглаживания, но не замена нехватающей RAM. Если рабочий набор процесса действительно нужен в памяти прямо сейчас, а места нет, страницы начинают гонять туда-сюда, и сервер замедляется куда сильнее, чем при аккуратном ограничении. Даже на быстром NVMe обращение к диску остаётся несравнимо медленнее обращения к оперативной памяти. Поэтому подкачка в расчёте запаса — это страховка от одиночного неприятного всплеска, но не часть бюджета. Если в таблице пиков вы закрываете дефицит словами «подкачка вытянет», дефицит остаётся дефицитом.

Признаком того, что вы живёте на подкачке, а не на памяти, служит не её наличие (небольшое использование swap бывает нормальным), а постоянная активность обмена в моменты нагрузки: система не просто выгрузила спящее, а непрерывно перекладывает нужное.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации Россия, США, Франция и Великобритания. Оплата картой РФ и по СБП.

АРЕНДОВАТЬ СЕРВЕР

Контейнеры: у памяти появляются границы

Если приложение работает в контейнерах, картина осложняется ещё одним слоем. По документации Docker, по умолчанию контейнер не имеет ресурсных ограничений и может использовать доступные ресурсы хоста; ограничения процессора и памяти при этом настраиваются отдельно (Docker resource constraints). Практический вывод для нашего расчёта такой: пока вы ничего не задали, контейнер сборки или копирования способен взять память у соседа по серверу. То есть разовый тяжёлый шаг может ударить по рабочему приложению, даже если они логически не связаны.

Ядро Linux в рамках cgroup v2 даёт два разных рычага. Файл memory.current показывает текущее потребление группы вместе с потомками, и это удобный инструмент для измерения, о котором ниже. Параметр memory.high запускает ограничение и возврат памяти, но сам по себе не вызывает завершение процессов по нехватке памяти, а memory.max задаёт жёсткий предел и может приводить к OOM (Linux kernel cgroup v2).

Отсюда вытекает мысль, которая касается именно запаса. Лимит — это не способ получить память, а способ решить, кто пострадает первым. Если задать служебному контейнеру мягкую границу, при пересечении пиков замедлится копирование, а не клиентское приложение. Если задать жёсткую, то при ошибочной оценке умрёт процесс копирования — причём ночью, и вы узнаете об этом утром по отсутствующему архиву. Значит, выбор между мягким и жёстким пределом — это выбор между «дольше» и «упадёт», и для разных операций он может быть разным. Лимит также не добавляет производительности приложению: он лишь перераспределяет то, что есть.

База данных как скрытый множитель

Отдельного внимания заслуживает PostgreSQL, потому что именно база часто даёт самый непредсказуемый вклад в пересечение пиков. В документации по потреблению ресурсов сказано, что work_mem применяется к отдельным операциям сортировки и хеширования, а в одном запросе и в параллельных сессиях таких операций может быть несколько; при нехватке памяти операции могут использовать временные файлы, а параллельные работники дополнительно расходуют ресурсы (PostgreSQL resource consumption).

Что это значит для запаса? Значение work_mem нельзя просто умножить на число соединений и получить потолок: один сложный запрос может открыть несколько сортировок и хеш-соединений, каждая со своим бюджетом, а параллельные работники добавят свои. Во время миграции или построения индекса, а также во время массовых отчётных запросов, которые часто запускаются как раз ночью, база способна расти заметно сильнее, чем в обычной дневной работе. При этом избыточные операции не обязательно упрутся в стену: их часть уйдёт во временные файлы на диск, и вы получите не отказ, а замедление и рост нагрузки на ввод-вывод. А замедленная база удлиняет релиз и копию, то есть увеличивает вероятность их пересечения. Одно усиливает другое.

Поэтому в таблице пиков строку «база» полезно разделить на две: обычная и в момент миграции или отчётов. Первую вы видите на графике, вторую нужно специально измерить.

Измерить одну операцию отдельно

Расчёт на бумаге хорош как гипотеза. Чтобы превратить его в число, выберите одну служебную операцию и измерьте её в изоляции, на копии окружения или в тихое время. Не пытайтесь измерить всё сразу: общий результат не покажет, кто сколько взял.

Для процессов в контейнерах или службах systemd удобная точка зрения — cgroup: значение memory.current в группе операции показывает её текущее потребление, включая потомков. Важная деталь: одно считывание в случайный момент мало что скажет. Нужны периодические отсчёты на протяжении всей операции, чтобы поймать максимум, а не среднее. Пик короткий, поэтому интервал опроса должен быть заметно короче, чем сама операция.

Методика проста по структуре:

  1. Зафиксируйте базовую линию: доступную память и потребление основных служб за несколько минут до запуска.
  2. Запустите операцию отдельно, например дамп базы со сжатием, и записывайте потребление через короткие интервалы.
  3. Отметьте максимум и момент, когда он случился: в начале, при сжатии или при записи.
  4. Вычтите базовую линию: разница и есть вклад операции в таблицу пиков.
  5. Повторите хотя бы дважды в разное время: на «горячей» и «холодной» базе результаты могут отличаться.

Затем повторите измерение в условиях пересечения: запустите копию, пока идёт тестовый релиз на стенде. Если суммарный пик заметно выше суммы отдельных, значит, процессы мешают друг другу, например, через кеш и диск, и в запас надо закладывать не сумму, а то, что вы реально увидели. Такие замеры можно делать на отдельном тестовом сервере с теми же параметрами, чтобы не рисковать рабочим.

Отдельный важный случай — восстановление. Его стоит измерить хотя бы раз, потому что оно ведёт себя иначе, чем создание копии: база принимает массовую запись, перестраивает индексы, и профиль памяти не похож на профиль дампа. К тому же восстановление происходит в тот момент, когда рабочее приложение либо остановлено, либо тоже пытается подняться. Если вы никогда не восстанавливались на сервере такого же размера, вы не знаете, помещается ли восстановление вообще, и узнаете об этом в худший из возможных дней.

От сценария к запасу

Теперь можно собрать итог. Запас — это не процент от общего объёма, а сумма худших сценариев, которые вы готовы покрыть, плюс небольшой остаток на неточность измерений. Проценты вроде «держите 20 % свободной памяти» удобны как мнемоника, но они не знают ни вашей базы, ни вашего расписания. На сервере с лёгким приложением и тяжёлой копией 20 % может оказаться избытком, а на сервере с мелкой базой и раздутой сборкой — недостатком. Любой универсальный процент — это ответ на вопрос, которого вы не задавали.

Вернёмся к нашему учебному серверу на 4 ГБ. Допустим, мы измерили и убедились, что пересечение релиза и копии даёт 1300 МБ сверх обычной работы, а ещё хотим удержать возможность восстановления при остановленном приложении. Тогда у нас два сценария, а не один, и запас определяется большим из них, а не их суммой, потому что приложение при восстановлении не работает. Если же получается, что 4 ГБ впритык, у вас три честных пути: разнести операции по времени и записать правило, ограничить служебные процессы так, чтобы страдали они, а не рабочая нагрузка, или выбрать тариф покрупнее. Из публичного набора MAATRIX это могут быть Heka с 8 ГБ RAM или Maat с 16 ГБ, но ресурсы тарифа — отправная точка для вашего теста, а не обещание, что приложение на нём будет работать быстрее или выдержит определённое число клиентов. Подтвердить это можно только собственным замером на выбранной конфигурации. А если вы пока решаете, во сколько вообще оценивать такой резерв в деньгах, поможет разбор запаса в бюджете на инфраструктуру.

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

Записка для коллеги

Расчёт ценен лишь тогда, когда его можно передать. Хорошая записка занимает полстраницы и отвечает на пять вопросов:

  • Какие операции учтены: релиз, копия, восстановление, миграции.
  • Какой пик у каждой по замеру и когда замер сделан.
  • Какие из них могут совпасть и почему остальные считаются разнесёнными.
  • Какой объём оставлен в запасе и под какой именно сценарий.
  • Что менять в расчёте, если вырастет база, сменится версия приложения или изменится расписание.

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

Резерв под восстановление

Если из всего расчёта оставить одну строку, пусть это будет восстановление. Релиз можно отложить, копию перенести, тяжёлый отчёт отключить на час. Восстановление отложить нельзя: оно приходит тогда, когда у вас уже что-то сломалось и каждая минута на счету. Именно для него стоит держать на сервере столько памяти, сколько показал отдельный замер, снятый заранее и на данных реального размера. Остальной запас можно обсуждать, торговаться с расписанием и ужимать лимитами. Этот — нет: он существует ровно затем, чтобы в плохой день база поднялась с первой попытки, а не со второй, когда выяснится, что на вторую уже не хватает ни времени, ни памяти.