MAATRIX / Блог / База уехала на отдельный сервер: что ломается в первый же день после разделения

База уехала на отдельный сервер: что ломается в первый же день после разделения

MAATRIX

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

Приложение всё ещё думает, что база рядом: сокет вместо сети

Пока приложение и база жили на одном сервере, самым простым и быстрым способом подключения был Unix-сокет или localhost — это не просто адрес, это отдельный механизм передачи данных внутри ядра ОС, без сетевого стека вообще. В PostgreSQL это /var/run/postgresql/.s.PGSQL.5432, в MySQL — /var/run/mysqld/mysqld.sock. Многие фреймворки и ORM по умолчанию используют именно сокет, если хост не указан явно или указан как localhost — это важный нюанс: localhost для многих клиентских библиотек (в первую очередь для MySQL/PHP) означает «попробовать сокет», а не «TCP-соединение на 127.0.0.1». Когда базы на этом сервере больше нет, попытка подключения к несуществующему сокету падает с ошибкой уровня «No such file or directory» — и это не сетевая ошибка, поэтому часто не сразу понятно, в чём дело.

Что нужно поменять в конфиге приложения:

  • Явный хост — IP или внутреннее DNS-имя нового сервера БД, а не localhost и не пустое значение.
  • Явный порт, если он отличается от стандартного (5432 для PostgreSQL, 3306 для MySQL).
  • Для MySQL/PHP отдельно проверить, что драйвер не пытается достучаться до сокета даже при указанном хосте — в my.cnf или строке подключения иногда нужно явно указать protocol=TCP или использовать 127.0.0.1 вместо localhost, чтобы форсировать сетевое соединение.
  • Строку подключения (DATABASE_URL, DB_DSN и подобные) — если она собирается из переменных окружения, убедитесь, что переменная DB_HOST реально обновилась во всех окружениях, а не только в одном .env-файле на сервере разработки.

Отдельная грабля — конфиги, разбросанные по нескольким местам: основной .env, systemd unit с Environment=, конфиг воркеров очередей, cron-скрипты, которые дергают базу напрямую через psql/mysql в bash-скриптах. Разделение серверов — хороший повод пройтись grep -rn "localhost" . по всему репозиторию и инфраструктурным конфигам, а не полагаться на память о том, «где хост базы прописан».

# Быстрый способ найти все места, где база подразумевалась локальной
grep -rn "localhost\|127.0.0.1" --include="*.env*" --include="*.yml" --include="*.conf" /etc/myapp /opt/myapp

Задержка, которой раньше физически не было

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

Проблема не в абсолютной величине задержки, а в том, что она умножается на число запросов. Если код делает один запрос на страницу — разницы никто не заметит. Если код в цикле дергает базу N+1 раз (классический антипаттерн ORM — сначала запрос списка, потом отдельный запрос на каждую связанную запись), то раньше N+1 обращений к сокету стоили условно бесплатно, а теперь каждое из них — это отдельный round-trip по сети. Сто последовательных запросов по 0,3–0,5 мс каждый — это уже 30–50 мс дополнительной задержки на одну страницу, и это без учёта самого времени выполнения запроса на стороне базы.

На что смотреть в первый день после переключения:

  • Страницы и эндпоинты с большим количеством вложенных объектов (список с деталями, отчёты, дашборды) — именно там чаще всего скрывается N+1.
  • Логи медленных запросов на стороне приложения (не только на стороне БД) — если время ответа выросло, а сами запросы к базе по отдельности быстрые, дело в их количестве, а не в качестве.
  • Батч-джобы и импорт/экспорт скрипты, которые построчно читают или пишут в базу в цикле — там эффект накопленной задержки особенно заметен, минуты могут превратиться в часы.

Лечится это не откатом разделения, а обычными приёмами: батчинг запросов (WHERE id IN (...) вместо цикла), eager loading в ORM (JOIN или preload вместо ленивой подгрузки), пул соединений с разумным числом keep-alive подключений, чтобы не тратить время на установку нового TCP-соединения на каждый запрос.

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

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

Заказать VPS под базу

Файрвол на сервере базы по умолчанию закрыт — и правильно

Когда база и приложение сидели на одном сервере, порт БД чаще всего вообще не торчал наружу — трафик не покидал localhost, и вопрос файрвола не вставал в принципе. На отдельном сервере базы данных ситуация меняется: теперь между приложением и базой обязательно сетевое соединение, и по умолчанию свежий сервер либо блокирует входящие подключения на порт БД вовсе, либо (что хуже) провайдер по умолчанию открывает все порты — и тогда возникает соблазн просто разрешить 5432 или 3306 для всех, чтобы «наконец заработало». Это тот самый антипаттерн, когда база оказывается открыта наружу — порт БД, доступный из интернета, находят сканеры за часы, а не за месяцы.

Правильный порядок: открыть порт только для конкретного IP-адреса сервера приложения (а если серверов приложения несколько — для каждого из них отдельно, или для диапазона, если он у вас стабильный и небольшой).

# UFW: разрешить PostgreSQL только с сервера приложения
ufw allow from 10.20.30.40 to any port 5432 proto tcp

# То же самое через iptables
iptables -A INPUT -p tcp -s 10.20.30.40 --dport 5432 -j ACCEPT
iptables -A INPUT -p tcp --dport 5432 -j DROP

Плюс сам сервис базы должен слушать не только 127.0.0.1. В PostgreSQL это listen_addresses в postgresql.conf — по умолчанию там часто стоит localhost, и его нужно явно поменять на IP сервера или * (с последующим ограничением файрволом и pg_hba.conf, о котором ниже). В MySQL — параметр bind-address в my.cnf, который тоже по умолчанию смотрит только на loopback.

Если оба сервера — ваши собственные и находятся в одной сети провайдера, стоит подумать не о том, чтобы разрешать порт БД во внешний интернет вообще, а о приватной сети между своими серверами — тогда база будет доступна только по внутреннему интерфейсу, который в принципе не маршрутизируется наружу, и вопрос «кому открыт порт 5432» снимается на уровне архитектуры, а не только правил файрвола.

Аутентификация: доверие по сокету больше не работает

На одном сервере многие настраивают базу «под себя» и упрощают доступ — не со зла, а потому что локальный сокет и так защищён правами файловой системы. В PostgreSQL это метод peer или trust в pg_hba.conf для локальных подключений: система доверяет тому, что раз процесс достучался до сокета, значит он имеет право это делать, и пароль не спрашивается вовсе. В MySQL похожая история — auth_socket плагин для root@localhost, тоже без пароля.

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

  • Завести отдельного пользователя БД для приложения (не root/postgres) с паролем и правами, ограниченными нужной базой — не всей СУБД.
  • В PostgreSQL добавить строку в pg_hba.conf для сетевого доступа с методом scram-sha-256 (современный вариант, не md5):
# pg_hba.conf — разрешить сетевой доступ только с IP приложения
host    myapp_db    myapp_user    10.20.30.40/32    scram-sha-256
  • В MySQL — создать пользователя, привязанного к конкретному хосту, а не к % (везде):
CREATE USER 'myapp_user'@'10.20.30.40' IDENTIFIED BY 'реальный_сложный_пароль';
GRANT ALL PRIVILEGES ON myapp_db.* TO 'myapp_user'@'10.20.30.40';
FLUSH PRIVILEGES;
  • Пароль хранить не в коде и не в git — в переменных окружения, секретах CI/CD или менеджере секретов, и убедиться, что после смены пароля перезапущены все процессы, которые держат старое соединение в пуле (иначе часть запросов будет падать с ошибками аутентификации ещё какое-то время после смены).

Отдельно стоит перезагрузить (не просто перечитать) конфигурацию pg_hba.confpg_ctl reload или SELECT pg_reload_conf(); обычно достаточно, но если менялся и postgresql.conf (например, listen_addresses), нужен полный рестарт сервиса, а не reload.

Бэкап был одним скриптом на весь сервер — теперь их минимум два

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

Что нужно сделать явно, а не понадеявшись, что «как-нибудь само»:

  • Проверить на сервере приложения, что старый бэкап-скрипт больше не пытается снимать дамп базы локально — если он вызывал pg_dump или mysqldump без хоста (то есть по умолчанию к localhost), после разделения эта команда либо упадёт с ошибкой подключения, либо тихо создаст пустой/битый архив, если ошибка не обрабатывается.
  • Настроить отдельное расписание бэкапа на сервере базы — это может быть тот же pg_dump/mysqldump по cron с выгрузкой во внешнее хранилище, либо специализированный инструмент с инкрементальными бэкапами.
  • Проверить, что мониторинг бэкапов (если он был) следит за обоими серверами отдельно, а не за одной точкой — типичная ошибка: алерт настроен на «бэкап не создан на сервере X», а после разделения важные данные оказались на сервере Y, который никто не добавил в проверку.
  • Сделать пробное восстановление хотя бы одного дампа на тестовом сервере в первую неделю после разделения — это единственный способ узнать, что бэкап действительно содержит рабочие данные, а не факт «файл создался».
# Простой cron-бэкап PostgreSQL на самом сервере базы, с указанием хоста явно
0 3 * * * pg_dump -h localhost -U backup_user myapp_db | gzip > /backups/myapp_db_$(date +\%F).sql.gz

Если раньше бэкап базы был частью общего снапшота диска (например, снимок тома у провайдера), учтите: снимок тома, снятый «на живую» без учёта состояния СУБД, не гарантирует консистентность данных внутри базы — это тема отдельная, но в контексте разделения серверов важно просто не терять факт бэкапа базы из виду при переносе.

Как тестировать разделение до прода, а не после

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

Практический план, который снижает риск:

  1. Разверните пару тестовых серверов в том же дата-центре и по возможности в той же сети, что и боевые — с той же топологией «приложение отдельно, база отдельно». Не тестируйте разделение на одной виртуалке с двумя контейнерами — там не будет реальной сетевой задержки и файрвола между ними.
  2. Прогоните полный цикл конфигурации — от смены хоста в .env до правил файрвола и pg_hba.conf — на тесте, а не «настроим по ходу дела на проде». Каждая настройка, описанная выше, должна быть проверена: подключение работает, порт закрыт для всех кроме нужного IP, бэкап снимается по расписанию.
  3. Прогоните реальную нагрузку или хотя бы сценарии с большим числом последовательных запросов — списки, отчёты, фоновые задачи — чтобы поймать N+1-проблемы до того, как их поймают пользователи.
  4. Держите план отката на случай, если на проде что-то пойдёт не так — вернуть приложение на подключение к старой (ещё не выключенной) базе должно быть вопросом смены одной переменной, а не часовым восстановлением. Подробнее о том, как размечать шаги миграции и точку невозврата, — в статье о составлении плана миграции на новый сервер.
  5. Не выключайте старый сервер сразу — держите его в режиме read-only или просто выключенным (но не удалённым) несколько дней после переключения. Если что-то не работает и требуется откат, это разница между «поменять хост в конфиге» и «восстанавливать базу из бэкапа под давлением времени».

Разделение серверов — это не событие одного дня, а процесс с проверяемыми шагами. Чем скучнее и подробнее план, тем меньше сюрпризов в проде.

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

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

Заказать VPS под базу

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

Можно ли просто скопировать .env со старого сервера и поменять только хост базы?

Можно, но именно в таких файлах чаще всего прячутся неявные зависимости от localhost — переменные окружения для очередей, кэша, воркеров, cron-скриптов. Лучше пройтись по всем местам, где упоминается адрес базы, целиком, а не точечно править один параметр.

Нужно ли сразу переходить на приватную сеть между серверами, или можно оставить публичный IP с ограничением файрвола?

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

Как быстро проявляются проблемы с N+1-запросами после разделения?

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

Что делать, если после разделения приложение вообще не может подключиться к базе?

Проверять по порядку: резолвится ли хост (DNS или IP указан верно), открыт ли порт для этого конкретного IP (telnet host port или nc -zv host port с сервера приложения), слушает ли СУБД нужный интерфейс (listen_addresses/bind-address), разрешает ли pg_hba.conf/таблица пользователей MySQL подключение с этого хоста. В большинстве случаев проблема находится на одном из первых двух шагов.

Стоит ли автоматизировать проверку всех этих пунктов, а не проверять руками каждый раз?

Да, если разделение серверов — не разовая операция, а практика, которая будет повторяться (новые окружения, staging-серверы). Простой чек-лист-скрипт, который проверяет резолвинг, доступность порта, версию SSL-сертификата подключения к БД и факт последнего успешного бэкапа, экономит часы на каждой последующей миграции.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →