На схеме два веб-сервера и балансировщик. Выглядит солидно: стрелки расходятся веером, подписи аккуратные. Но если опустить взгляд чуть ниже, обе стрелки сходятся в одну точку: одна база данных и одна папка с пользовательскими файлами. Получается песочные часы: сверху широко, посередине узкое горлышко, и всё, что есть в приложении ценного, находится именно в нём.
Это не повод отказываться от двух серверов. Это повод понять, от чего именно они защищают, а от чего нет. Ниже мы пройдём путь одного запроса, найдём всё, что у двух веб-узлов общее, разберём отказы по одному и в конце перерисуем схему так, чтобы резервирование стояло там, где оно нужно.
Путь одного запроса: от имени сайта до строки в таблице
Пользователь вводит адрес. Первое, что происходит, это разрешение имени в DNS. Запись указывает на какой-то IP-адрес, и это уже первая зависимость: если запись указывает на один балансировщик, то весь «веер» двух серверов подвешен на одном адресе. Кроме того, у записи есть время жизни кэширования: изменить её можно за секунду, а клиенты и промежуточные резолверы будут помнить старое значение ещё какое-то время. Поэтому «переключим DNS» не равно «переключили трафик».
Дальше запрос попадает на балансировщик. Он завершает TLS (или пропускает его дальше), выбирает один из двух веб-узлов и передаёт запрос. Здесь нужно честно признать: сам балансировщик в нашей схеме один. Если это одиночный процесс на одной виртуальной машине, мы просто перенесли единую точку отказа на шаг вперёд. Два сервера за одним балансировщиком защищают от падения сервера, но не от падения балансировщика.
Веб-узел принимает запрос, запускает код приложения и начинает ходить за состоянием: смотрит сессию, читает и пишет в базу, обращается к файлам, берёт секреты (ключи API, пароль к базе, ключ подписи токенов). Только тут выясняется, что «два экземпляра» были экземплярами только кода. Всё, что код читает и пишет, существует в единственном числе.
Ответ идёт обратно по той же цепочке. И каждый шаг вперёд-назад можно проверить вопросом: «а если этого не станет, у меня есть второй?» Давайте задавать его последовательно.
Что у двух веб-узлов общее на самом деле
Составим список без украшений. Он оказывается длиннее, чем ожидаешь.
- База данных. Обычно одна, на отдельной машине или на одной из двух веб-машин. Во втором случае падение этого сервера отнимает и веб, и данные одновременно.
- Пользовательские файлы. Загрузки, аватары, вложения. Если они лежат на локальном диске одного узла, второй их просто не видит; если на сетевом хранилище или общем томе, то единой точкой становится оно.
- Секреты. Пароль к базе, ключи шифрования, токены внешних сервисов. Они одинаковы на обоих узлах, а значит, утечка или ошибочная ротация бьёт по обоим сразу.
- Сессии. Если они хранятся в памяти процесса, пользователь, переброшенный на второй узел, оказывается разлогинен. Если в общем хранилище, то общее хранилище сессий становится ещё одной зависимостью.
- Код и конфигурация. Один и тот же релиз раскатывается на оба узла. Ошибка в релизе, например неудачная миграция или битая конфигурация, воспроизводится дважды.
- Внешние сервисы. Почтовый шлюз, платёжный API, хранилище объектов. От них зависят оба узла одинаково.
- Место размещения. Два VPS у одного провайдера в одной локации могут делить сетевую инфраструктуру, электропитание и административные границы. Сам факт, что это две машины, ещё не говорит, что они не связаны общим оборудованием или общей причиной отказа.
Про последний пункт полезно знать, как это решают в крупных облаках. В документации Amazon EC2 описаны группы размещения: стратегия spread размещает небольшой набор экземпляров на разном оборудовании, чтобы снизить связанные отказы, а partition разделяет группы экземпляров на непересекающееся оборудование. Важная деталь: это отдельная необязательная настройка. То есть по умолчанию разнесение не подразумевается, о нём нужно специально просить и проверять, предоставляет ли такую возможность ваша платформа. Для VPS любого провайдера разумная привычка такая: не считать две машины независимыми, пока не выяснено, что для этого сделано, и по возможности выбирать для них разные локации.
Отказ, который убивает обоих сразу
Теперь главный пример. Возьмём типичный сайт: два веб-узла на VPS, балансировщик, одна база на третьем VPS. Допустим, база перестала отвечать: закончилось место на диске, процесс упал или просто сеть до неё недоступна.
Что видит каждый веб-узел? Процесс приложения жив, порт слушает, отдаёт статику. Балансировщик отправляет проверку на простой адрес, получает 200 OK и считает оба узла здоровыми. Пользователи при этом получают ошибки 500 на любой странице, где нужны данные, а это почти все страницы. Два «живых» узла в этой ситуации бесполезны: они обслуживают пустоту.
Это и есть тот самый отказ, ради которого стоило нарисовать схему. Его суть в том, что резервирование было применено к слою, который и так отказывает редко и чинится быстро (перезапустить stateless-процесс проще всего), а нижний слой, где живёт состояние, остался в одном экземпляре. Удвоение дало ощущение надёжности, но не уменьшило вероятность простоя от самой частой тяжёлой причины.
Тот же эффект даёт и ошибка в релизе. Если новая версия кода при старте выполняет миграцию, несовместимую со старой, и вы выкатили её на оба узла, второй узел не страхует от первого: ошибка лежит в артефакте, а не в железе. Здесь помогает не количество серверов, а порядок выката, когда новая версия сначала получает малую долю трафика. Подробнее про такую схему можно прочитать в материале про blue-green деплой на VPS.
Отказы по очереди: как разбирать, не ломая рабочий сервис
Хочется проверить всё «по-взрослому»: выдернуть сервер и посмотреть. На рабочем сервисе с живыми пользователями это плохая идея. Разумнее разделить проверки на три уровня по степени риска.
Уровень первый: бумага. Для каждого компонента выписать: что делает приложение, когда этого нет; что видит пользователь; кто и как узнаёт об этом; сколько времени уйдёт на восстановление. Этот разбор бесплатен и уже отсеивает самые неприятные сюрпризы.
Уровень второй: копия окружения. Поднять уменьшенную копию (два небольших VPS и тестовая база, без реальных данных) и ломать её. Остановить процесс на одном узле, заблокировать порт к базе файрволом, заполнить тестовый диск, подменить секрет неверным значением. Всё это воспроизводимо и безопасно, а результаты наблюдаемы.
Уровень третий: ограниченное испытание на рабочем сервисе. Только если первые два пройдены и есть окно с понятным откатом. Выключается один веб-узел, а не база, в низкую нагрузку, с заранее назначенным человеком, который наблюдает за ошибками. Цель испытания узкая: убедиться, что балансировщик убрал узел из ротации и второй узел справился с нагрузкой. Разрушительные эксперименты с данными на рабочей системе не нужны вообще: ответ на вопрос «что будет, если базы нет» можно получить на копии.
Ниже таблица того, что каждый отказ делает с нашей схемой. Это авторский разбор для типичной конфигурации «два веб-узла, одна база, общий каталог файлов», а не измерения конкретной системы.
| Что отказало | Что видит пользователь | Заметит ли простая проверка | Спасает ли второй веб-узел |
|---|---|---|---|
| Один веб-узел | Почти ничего, возможно, разлогин при хранении сессий в памяти | Да, если проверка идёт на узел | Да |
| Балансировщик | Сайт недоступен целиком | Нет, проверяет он сам | Нет |
| База данных | Ошибки на страницах с данными | Нет, если проверка не трогает базу | Нет |
| Общее файловое хранилище | Пропали картинки и вложения, сбои загрузок | Нет | Нет |
| Неверный или просроченный секрет | Отказ входа, сбои платежей и писем | Нет | Нет, оба используют одно значение |
| Ошибочный релиз | Ошибки или сломанные страницы | Иногда | Нет, если выкатили на оба |
Обратите внимание на колонку «Спасает ли второй веб-узел». Положительный ответ стоит только в первой строке. Это не значит, что второй узел бесполезен: сбой одного узла самый вероятный в обычной жизни, а обновление по очереди без остановки сервиса тоже его заслуга. Но «да» стоит ровно в одной клетке из шести.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации Россия, США, Франция и Великобритания. Оплата картой РФ и по СБП.
Что балансировщик замечает сам, а что нет
Здоровье в глазах балансировщика определяется проверкой, которую вы ему задали. Чаще всего это запрос по HTTP на какой-то адрес и ожидание успешного кода. Такая проверка отвечает на вопрос «процесс принимает соединения и возвращает 200». Она не отвечает на вопрос «приложение способно выполнить полезную работу».
Различают, по сути, два вида проверок, и путаница между ними порождает половину неприятных историй.
- Проверка живости (процесс жив). Простой ответ без обращения к зависимостям. Нужна, чтобы перезапустить зависший процесс.
- Проверка готовности (можно слать трафик). Включает короткое обращение к базе и, возможно, к хранилищу. Нужна, чтобы убрать узел из ротации, когда он не может работать.
Тут легко ошибиться в обратную сторону. Если проверка готовности глубокая и каждый узел при недоступности базы объявляет себя неготовым, балансировщик уберёт из ротации оба узла сразу. Пользователь вместо ошибок 500 получит «нет доступных серверов». Это не лучше, но и не хуже; важно, что вы сделали это осознанно. Иногда полезнее, чтобы приложение отвечало аккуратной страницей «сервис временно недоступен» и отдавало статику, а не исчезало совсем. Решение об этом принимается на уровне приложения, а не балансировщика.
Есть и другая ловушка: проверка с глубоким обращением к общей зависимости при кратком сбое может вызвать «мигание». Узлы то выходят из ротации, то возвращаются, а нагрузка скачками падает на ещё не прогретые процессы. Помогают пороги: считать узел нездоровым после нескольких подряд неудачных проверок и возвращать после нескольких подряд успешных. Конкретные числа зависят от вашего балансировщика и допустимой задержки реакции, подбирать их стоит на тестовой копии.
И отдельная мысль про сам балансировщик. Он тоже процесс на машине, и у него бывают собственные причины для падения: исчерпанные дескрипторы, неверная конфигурация после перезагрузки, закончившийся сертификат. Сертификат вообще любопытный пример: он общий для всех узлов, истекает по календарю и роняет сайт предсказуемо. Это отказ, который можно предотвратить напоминанием, а не резервом.
Файлы и сессии: тихие общие зависимости
Про базу обычно вспоминают быстро. Файлы и сессии вспоминают, когда пользователь жалуется.
Разберём файлы. Пользователь загрузил документ, запрос обслужил узел А, файл лёг в его локальную папку. Следующий запрос с просмотром документа ушёл на узел Б, где файла нет. Ошибка «то видно, то нет» проявляется плавающе и выглядит как глючная магия. Решений несколько, и каждое переносит вопрос отказа в новое место.
- Общий сетевой том. Прост, но сам становится единой точкой, а блокировки и задержки сетевой файловой системы приносят свои сюрпризы.
- Объектное хранилище. Приложение пишет файлы по API, оба узла читают оттуда. Узлы остаются stateless, а надёжность файлов определяется хранилищем и его резервными копиями. Зависимость от внешнего сервиса и его доступности при этом остаётся.
- Репликация между узлами. Файл копируется на оба узла после записи. Это даёт дублирование данных, но добавляет окно, в котором копия ещё не доехала, и вопрос о том, как разрешать конфликты.
Ни один вариант не «бесплатный». Выбор определяется тем, насколько болезненна потеря файла и насколько важна доступность прямо сейчас. Для пользовательских аватарок и для сканов договоров будут разные ответы.
Сессии устроены проще. Если пользователь вошёл на узле А, а балансировщик отправил следующий запрос на Б, возможны три развития. Липкие сессии привязывают клиента к узлу, но при падении узла пользователи этого узла теряют вход. Общее хранилище сессий (например, быстрая база в памяти) снимает привязку, но добавляет зависимость. Подписанные токены на стороне клиента не требуют хранилища, но заставляют думать об отзыве и о защите ключа подписи, то есть возвращают нас к секретам. Опять тот же закон: состояние не исчезает, оно переезжает.
Секреты: общая точка, о которой не любят думать
Второй узел приносит неожиданный бонус, который одновременно является риском: всё, что есть на первом, нужно скопировать на второй. Пароль к базе, ключи API, ключ подписи. Теперь секреты лежат в двух местах, и у атакующего вдвое больше шансов их найти. С другой стороны, если ключ нужно срочно сменить, менять его надо согласованно на обоих узлах и в базе, иначе часть запросов пойдёт со старым ключом.
Практические следствия простые. Секреты не должны лежать в репозитории кода и в образе. Их нужно подавать через окружение или отдельные файлы с ограниченными правами. Нужен понятный порядок ротации: сначала система принимает оба значения, затем клиенты переходят на новое, затем старое отзывается. А если приложение запущено в контейнерах, помните, что конфигурация для рабочего окружения обычно отличается от разработческой. Документация Docker Compose в разделе про использование в production отмечает, что одну и ту же конфигурацию можно применять в CI, staging и production с различающимися настройками, а в production может понадобиться убрать bind mount кода, изменить порты и окружение, добавить restart policy. Для нашего сюжета отсюда следует одно: различия между узлами и средами должны быть в явных настройках, а не в ручных правках на каждой машине. Тогда два узла действительно одинаковы, а не «почти».
Тот же источник напоминает, что обновление кода в образе требует пересборки образа и пересоздания контейнера, а отдельный сервис можно обновить, не трогая зависимости. Это удобно для скользящего обновления: узел А получает новый образ, узел Б пока работает на старом, а база и хранилище не затронуты. Но убедитесь, что эти два релиза совместимы с одной и той же схемой данных, иначе одновременная работа двух версий превратится в отдельную проблему.
Время восстановления: у каждой зависимости своя цена
Теперь можно соотнести отказы с бюджетом. Для каждого компонента полезно задать два вопроса: сколько простоя вы готовы терпеть и сколько данных готовы потерять. Ответы почти всегда различны для разных частей системы.
Возьмём условный пример: небольшой интернет-магазин. Это иллюстрация, не измерение. Веб-узел можно заменить за минуты, потому что он собирается из образа. Базу по резервной копии придётся поднимать дольше, и время зависит от размера данных и скорости диска. Потеря последних заказов за час для магазина куда болезненнее, чем десять минут простоя. Значит, самое дорогое в этой системе: целостность и свежесть данных заказов, а не количество веб-узлов.
Отсюда выбор вариантов, от дешёвого к дорогому:
- Резервные копии и проверенное восстановление. Самая дешёвая ступень. Простой есть, но его длительность известна, потому что восстановление отрепетировано. Копия, которую ни разу не разворачивали, это гипотеза, а не копия.
- Холодный или тёплый резерв базы. Второй сервер с регулярно обновляемой копией. Переключение ручное, потеря данных ограничена периодом между обновлениями.
- Репликация с готовым переключением. Копия данных обновляется почти постоянно, переключение быстрее. Цена: сложность, мониторинг отставания реплики и осторожность при автоматическом переключении, чтобы не получить две «главные» базы одновременно.
Выбор зависит от того, сколько стоит минута простоя именно вашего проекта, и от того, есть ли у вас человек, который умеет переключать. Автоматика без подготовленного человека часто опаснее ручного режима с хорошей инструкцией.
Теперь про ресурсы VPS. Два небольших узла для веб-части и отдельная машина под базу ложатся в такую логику естественно: ресурсы на разных серверах не конкурируют, и перегрузка базы не душит веб-процессы. В линейке MAATRIX есть тарифы от Ba (1 vCPU, 2 ГБ RAM, 40 ГБ NVMe) до Djed (16 vCPU, 32 ГБ, 320 ГБ), с NVMe, процессорами AMD EPYC и root-доступом. Это перечень конфигураций, а не обещание ёмкости: сколько запросов выдержит узел, показывает только нагрузочная проверка вашего приложения. Для веб-узлов, которые собираются из образа и не хранят состояние, обычно хватает меньшей конфигурации, а крупнее стоит смотреть для машины с базой, где важны память и диск. Публично указаны локации UK, US, RU и FR, и если вы разносите узлы по разным локациям, учтите задержку между ними и базой: запросы к базе через океан складываются в заметное время ответа. Точные характеристики и доступность в нужной локации проверяются в форме заказа.
Одно уточнение, чтобы не питать иллюзий: два VPS у одного провайдера сами по себе не гарантируют размещение на разном оборудовании, и отказоустойчивость приложения из этого факта не вытекает. Она создаётся архитектурой и проверкой, а не числом купленных машин.
Исправленная схема
Вернёмся к исходному рисунку. Сверху те же два веб-узла и балансировщик, но изменилась расстановка акцентов.
- Вход. Балансировщик признан отдельной зависимостью. Для небольшого проекта это может быть осознанно принятый риск с коротким временем ручного восстановления и записью о том, как поднять его заново из конфигурации. DNS-запись имеет разумный срок кэширования, заранее известно, куда её переключать.
- Веб-узлы. Одинаковые, собираются из одного образа, без локального состояния. Обновляются по очереди. Проверка готовности учитывает базу, проверка живости нет.
- Файлы. Вынесены из локальных дисков узлов в одно хранилище с собственными резервными копиями и проверенным восстановлением.
- Сессии. Не привязаны к процессу узла, поэтому падение одного узла не разлогинивает пользователей.
- База. Именно здесь основное резервирование: копия с регулярным обновлением или реплика, на отдельной машине и в идеале в другой локации. Подготовлена и отрепетирована процедура переключения и восстановления.
- Секреты. Хранятся вне кода и образов, ротация описана и проверена на тестовой копии.
Вес схемы сместился вниз. Если раньше основная часть бюджета уходила на «удвоение сверху», то теперь деньги и внимание направлены туда, где отказ нельзя компенсировать вторым процессом. Два веб-узла остались: они дают обновления без остановки и переживают падение одной машины. Но теперь на схеме видно, что самое важное лежит ниже, и что воображаемая стрелка «если упадёт, переключится» нарисована только там, где её действительно можно проверить.