Все уроки
Linux и серверыСредний12 мин чтенияВ моём стеке

Базы данных на собственных серверах

Установка, закрытие доступа, тюнинг, бэкапы и реальное восстановление MySQL, PostgreSQL и Redis.

  • Install & tune
  • Users & access
  • Backups
  • Replicas
Шаг 1/5
Каждую ночь в 02:00, пока все спят, база делает backup и отправляет копию на NAS.

За 30 секунд

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

Почему я это использую

  • Я веду MySQL/MariaDB, PostgreSQL и Redis на продакшен-серверах, так что правильный доступ, тюнинг и бэкапы — часть моей повседневной ответственности.
  • Бэкап для меня реален только после проверенного восстановления; это правило сберегло больше нервов, чем любой инструмент.
  • База данных, слушающая публичный порт, — один из самых частых способов утечки данных в небольших компаниях, поэтому закрыть её на localhost или частную сеть — первый пункт моего чек-листа.

Как это работает

01Где должна жить база данных

Есть три распространённых варианта размещения. На том же сервере, что и приложение: проще и быстрее всего (нет сетевого перехода), подходит для небольших и средних проектов, но приложение и база конкурируют за RAM, а потеря сервера означает потерю обоих. На отдельном сервере в частной сети: серверы приложений масштабируются независимо, а базе достаётся вся память. Как управляемый сервис (Amazon RDS, DigitalOcean Managed Databases, Google Cloud SQL и другие): провайдер берёт на себя установку, патчи, бэкапы, failover и восстановление на момент времени. Что бы вы ни выбрали, правила одни и те же: база данных никогда не доступна из интернета, у каждого приложения свой пользователь с минимальными правами, и есть резервная копия за пределами машины. Начинайте с самого простого размещения, которое удовлетворяет вашим требованиям к восстановлению, и выносите базу, когда этого потребуют нехватка памяти или требования к доступности.

02Установка и привязка к localhost или частной сети

В Ubuntu apt install postgresql, mysql-server (или mariadb-server) и redis-server даёт вам работающие сервисы. Ubuntu 26.04 поставляется с PostgreSQL 18; официальные репозитории вендоров предлагают другие версии, если нужно совпасть с продакшеном где-то ещё. Самая важная настройка — где сервис слушает. MySQL и Redis в Ubuntu по умолчанию уже слушают 127.0.0.1, а PostgreSQL — localhost, но всегда проверяйте через ss -tlnp. Если серверы приложений подключаются по частной сети, привязывайтесь только к адресу приватного интерфейса, разрешите эту подсеть в файрволе, а для PostgreSQL добавьте соответствующую строку в pg_hba.conf с аутентификацией scram-sha-256. В Redis protected-mode остаётся включённым, и даже на localhost он должен требовать пароль или ACL-пользователя. Никогда не открывайте в ufw порты 3306, 5432 или 6379 для всего мира.

ini
# /etc/mysql/mysql.conf.d/mysqld.cnf
[mysqld]
bind-address = 127.0.0.1
 
# /etc/postgresql/18/main/postgresql.conf
listen_addresses = 'localhost'        # or 'localhost,10.0.0.5' for a private NIC
 
# /etc/postgresql/18/main/pg_hba.conf (private subnet only)
host  app_db  app_user  10.0.0.0/24  scram-sha-256
 
# /etc/redis/redis.conf
bind 127.0.0.1 -::1
protected-mode yes
requirepass change_me_long_random_value
 
# Verify what is listening and where
sudo ss -tlnp | grep -E '3306|5432|6379'

03Пользователи и минимальные привилегии

Никогда не позволяйте приложению подключаться как root (MySQL) или postgres (PostgreSQL). Создайте одну базу и одного пользователя на приложение и выдайте только то, что этому приложению нужно в этой базе. Многие команды также создают отдельного пользователя для миграций, который может менять схему, рабочего пользователя, который может только читать и писать строки, и пользователя только для чтения для отчётов или поддержки. Используйте длинные случайные пароли, хранящиеся в env-файле приложения или в менеджере секретов, а не в Git. В PostgreSQL 15 и новее обычные пользователи по умолчанию больше не могут создавать объекты в схеме public, поэтому владельцем базы должна быть роль приложения или миграций. Время от времени пересматривайте пользователей и удаляйте учётки людей и приложений, которых больше нет.

sql
-- PostgreSQL
CREATE ROLE app_user LOGIN PASSWORD 'long_random_password';
CREATE DATABASE app_db OWNER app_user;
CREATE ROLE report_ro LOGIN PASSWORD 'another_random_password';
GRANT CONNECT ON DATABASE app_db TO report_ro;
\c app_db
GRANT USAGE ON SCHEMA public TO report_ro;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO report_ro;
ALTER DEFAULT PRIVILEGES FOR ROLE app_user IN SCHEMA public GRANT SELECT ON TABLES TO report_ro;
 
-- MySQL / MariaDB
CREATE DATABASE app_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'app_user'@'localhost' IDENTIFIED BY 'long_random_password';
GRANT SELECT, INSERT, UPDATE, DELETE ON app_db.* TO 'app_user'@'localhost';

04Базовый тюнинг: отдайте базе её память

Базы данных быстры, когда горячие данные помещаются в RAM. Значения по умолчанию намеренно крошечные, поэтому несколько настроек дают большую разницу. Для MySQL/InnoDB innodb_buffer_pool_size кэширует данные и индексы; на выделенном сервере БД типично 50–70 процентов RAM, и гораздо меньше, если машину делит приложение. Для PostgreSQL shared_buffers обычно около 25 процентов RAM, effective_cache_size (подсказка, а не выделение памяти) — 50–75 процентов, а work_mem остаётся небольшим, потому что выделяется на каждую операцию сортировки или хэширования в каждом соединении. Держите max_connections умеренным и используйте пулер вроде PgBouncer, когда подключается много процессов приложения. Для Redis задайте maxmemory и политику вытеснения, если он используется как кэш; используйте noeviction, если он хранит очереди, которые нельзя терять. А затем измеряйте: включите лог медленных запросов и используйте EXPLAIN, прежде чем покупать железо.

ini
# MySQL, 8 GB server shared with the app
[mysqld]
innodb_buffer_pool_size = 3G
max_connections = 200
slow_query_log = 1
long_query_time = 1
 
# PostgreSQL, 8 GB dedicated server
shared_buffers = 2GB
effective_cache_size = 6GB
work_mem = 16MB
maintenance_work_mem = 512MB
log_min_duration_statement = 1000
 
# Redis used as a cache
maxmemory 512mb
maxmemory-policy allkeys-lru

05Бэкапы: логические и физические, локальные и вне площадки

Логический бэкап — это дамп SQL или структурированный архив (mysqldump, pg_dump). Он переносим между версиями, из него легко восстановить одну таблицу, и он идеален для небольших и средних баз, но медленный для очень больших. Физический бэкап копирует сами файлы данных (pg_basebackup, pgBackRest, WAL-G, Percona XtraBackup, mariabackup). В сочетании с непрерывным архивированием журнала предзаписи (WAL) или бинарного лога он позволяет восстановиться на момент времени: откатиться на минуту до того, как кто-то удалил таблицу. Снапшоты сервера от провайдера полезны, но сами по себе недостаточны: они живут в том же аккаунте и могут захватить базу посреди записи. Следуйте правилу 3-2-1: три копии, два типа хранилища, одна вне площадки. Шифруйте бэкапы, храните их в другом аккаунте или у другого провайдера и следите, чтобы задача действительно отработала.

bash
#!/usr/bin/env bash
# /usr/local/bin/db-backup.sh  (cron: 30 2 * * * root /usr/local/bin/db-backup.sh)
set -euo pipefail
STAMP=$(date +%F_%H%M)
DIR=/var/backups/db
mkdir -p "$DIR"
 
# PostgreSQL: compressed custom format, restorable per table
sudo -u postgres pg_dump -Fc app_db > "$DIR/pg_app_db_$STAMP.dump"
 
# MySQL/MariaDB: consistent InnoDB snapshot without locking (credentials in /root/.my.cnf)
mysqldump --single-transaction --routines --triggers app_db | gzip > "$DIR/my_app_db_$STAMP.sql.gz"
 
# Copy off the server to an S3-compatible bucket configured in rclone
rclone copy "$DIR" offsite:db-backups/
 
# Keep 14 days locally
find "$DIR" -type f -mtime +14 -delete

06Тесты восстановления: единственное доказательство, что бэкап работает

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

bash
#!/usr/bin/env bash
set -euo pipefail
LATEST=$(ls -t /var/backups/db/pg_app_db_*.dump | head -1)
 
sudo -u postgres dropdb --if-exists restore_test
sudo -u postgres createdb restore_test
time sudo -u postgres pg_restore --no-owner -d restore_test "$LATEST"
 
# Sanity checks: data exists and is recent
sudo -u postgres psql -d restore_test -c "SELECT count(*) FROM orders;"
sudo -u postgres psql -d restore_test -c "SELECT max(created_at) FROM orders;"
 
sudo -u postgres dropdb restore_test

07Управляемая или своя?

Управляемые базы данных стоят дороже за гигабайт RAM, но вы покупаете время и безопасность: автоматические бэкапы с восстановлением на момент времени, патчи минорных версий, реплики в один клик, failover на резервный узел и метрики из коробки. Проектирование схемы, индексы, пользователи и производительность запросов по-прежнему на вас. Своя база дешевле и даёт полный контроль над версиями, расширениями и конфигурацией, и для небольших команд это вполне разумно, если бэкапы, мониторинг и обновления автоматизированы скриптами и проверены. Хорошее правило: если несколько часов простоя базы или день потерянных данных серьёзно ударят по бизнесу, а в команде никто не горит желанием быть DBA, используйте управляемую базу. Если проект небольшой, чувствителен к бюджету, а команда уверенно чувствует себя в Linux, ведите базу сами, аккуратно, и пересматривайте решение по мере роста проекта.

Когда применять

  • Своя база на сервере приложения — для небольших и средних проектов, где важна стоимость и допустим короткий простой.
  • Перенос базы на отдельный приватный сервер — когда она конкурирует с приложением за RAM или CPU.
  • Управляемая база — когда нужны высокая доступность, восстановление на момент времени и минимум эксплуатационных усилий.
  • Redis рядом с SQL — для кэша, сессий, rate limits и очередей, но не как основное хранилище данных.

Когда не стоит

  • Не ведите базу сами, если никто не будет отвечать за патчи, мониторинг и учения по восстановлению.
  • Не открывайте порт базы публично, чтобы заработал GUI-клиент; используйте SSH-туннель.
  • Не полагайтесь на снапшоты провайдера как на единственный бэкап.

Альтернативы

  • Управляемый SQL (RDS, Cloud SQL, DigitalOcean)

    Бэкапы, failover и патчи берёт на себя провайдер; выше стоимость, меньше низкоуровневого контроля.

  • Serverless Postgres (Neon, Supabase и подобные)

    Быстрый старт, ветвление и масштабирование до нуля; следите за лимитами соединений и ценами при росте.

  • База данных в Docker Compose

    Удобно и воспроизводимо; храните данные в именованных томах и никогда не публикуйте порт на 0.0.0.0.

  • SQLite

    Никакого администрирования для небольших приложений на одном сервере; ограниченная конкурентная запись.

Лучшие практики

  1. Привязывайте каждую базу к localhost или приватному интерфейсу и проверяйте через ss -tlnp.
  2. Одна база и один пользователь с минимальными правами на приложение; никогда не подключайте приложения как root или postgres.
  3. Осознанно задавайте buffer pool или shared_buffers и включайте лог медленных запросов с первого дня.
  4. Автоматизируйте ночные дампы или физические бэкапы с архивированием WAL/binlog, зашифрованные и скопированные вне площадки.
  5. Проверяйте восстановление по расписанию и записывайте, сколько оно занимает.
  6. Мониторьте место на диске, соединения, отставание репликации и успешность задач бэкапа.
  7. Обновляйте мажорные версии сначала на копии, с проверенным планом отката.

Частые ошибки

  • Открывают 3306 или 5432 в интернет, часто «временно», и забывают закрыть.
  • Хранят бэкапы только на том же сервере или в том же облачном аккаунте, что и база.
  • Ни разу не восстанавливают бэкап до того дня, когда он понадобится, — и обнаруживают, что он пустой или повреждённый.
  • Запускают mysqldump без --single-transaction на нагруженной базе InnoDB, получая блокировки или неконсистентные дампы.
  • Используют Redis с политикой вытеснения для данных очередей и молча теряют задачи при нехватке памяти.

Глубже

08Соединения дороги: пулинг через PgBouncer

Каждое соединение с PostgreSQL — отдельный процесс операционной системы со своей памятью, поэтому сотни простаивающих соединений от воркеров PHP-FPM, воркеров очередей и cron-задач тратят RAM и замедляют сервер, и вы упираетесь в max_connections задолго до того, как загрузится CPU. MySQL использует потоки и справляется лучше, но на масштабе та же проблема появляется и там. Пулер стоит между приложениями и базой и мультиплексирует множество клиентских соединений на несколько серверных. PgBouncer в режиме transaction выдаёт клиенту серверное соединение только на время одной транзакции — это самая эффективная настройка. Цена в том, что состояние сессии не сохраняется: ломаются команды SET, advisory locks, удерживаемые между транзакциями, LISTEN/NOTIFY и временные таблицы. Начиная с PgBouncer 1.21, max_prepared_statements позволяет prepared statements на уровне протокола работать в режиме transaction. Размер пула подбирайте под то, сколько база реально может выполнять параллельно, — примерно небольшое кратное числа ядер CPU.

ini
; /etc/pgbouncer/pgbouncer.ini
[databases]
app_db = host=127.0.0.1 port=5432 dbname=app_db
 
[pgbouncer]
listen_addr = 127.0.0.1
listen_port = 6432
auth_type = scram-sha-256
auth_file = /etc/pgbouncer/userlist.txt
pool_mode = transaction
max_client_conn = 1000
default_pool_size = 20
max_prepared_statements = 100

09Write-ahead log, архивирование и point-in-time recovery

Оба движка записывают каждое изменение в последовательный журнал до того, как трогать файлы данных: WAL в PostgreSQL, redo log плюс binary log в MySQL. После сбоя база проигрывает журнал, чтобы прийти к согласованному состоянию. Этот же журнал — основа репликации и восстановления на момент времени: восстанавливаете физический базовый бэкап, затем проигрываете заархивированные сегменты журнала до выбранной метки времени. В PostgreSQL инструмент вроде pgBackRest берёт на себя базовые бэкапы, архивирование WAL в объектное хранилище, retention, сжатие и шифрование. archive_command отправляет каждый завершённый сегмент WAL; если он начинает падать, PostgreSQL хранит сегменты локально и pg_wal растёт, пока не заполнится диск, поэтому ставьте алерт на сбои архивирования (их показывает pg_stat_archiver). В MySQL храните binary log с включёнными GTID и копируйте их за пределы сервера; после восстановления полного бэкапа mysqlbinlog может проиграть их до нужной позиции или времени.

ini
# postgresql.conf
wal_level = replica
archive_mode = on
archive_command = 'pgbackrest --stanza=main archive-push %p'
 
# /etc/pgbackrest/pgbackrest.conf (S3 credentials kept in a root-only include)
[global]
repo1-type=s3
repo1-s3-bucket=example-db-backups
repo1-s3-endpoint=s3.example.com
repo1-s3-region=eu-central-1
repo1-retention-full=2
repo1-cipher-type=aes-256-cbc
 
[main]
pg1-path=/var/lib/postgresql/18/main

10Обновление мажорной версии без драмы

Минорные релизы (с 18.0 на 18.1) требуют только обновления пакета и перезапуска. Мажорные версии меняют формат хранения на диске и требуют плана. В Ubuntu pg_upgradecluster оборачивает pg_upgrade и структуру кластеров; pg_upgrade с --link не копирует файлы данных и укладывается в минуты даже для больших баз, но старый кластер после этого запустить уже нельзя, так что сначала сделайте бэкап. PostgreSQL 18 к тому же переносит статистику планировщика через обновление, поэтому производительность хорошая сразу, но запуск ANALYZE — всё равно дешёвая страховка. Для почти нулевого простоя используйте логическую репликацию: поднимите новую версию рядом, реплицируйте таблицы со старого primary, дайте ей догнать и переключите приложение за секунды. Последовательности и изменения схемы не реплицируются, поэтому на время переключения заморозьте миграции и синхронизируйте sequences. Каким бы ни был метод, отрепетируйте на восстановленной копии, прогоните на ней тесты приложения и заранее проверьте расширения — отсутствующая версия расширения классически блокирует обновление.

11Поиск медленных запросов и здоровье таблиц

pg_stat_statements агрегирует каждый нормализованный запрос с числом вызовов и суммарным временем. Сортировка по суммарному, а не среднему времени находит то, что реально нагружает сервер: запрос на 2 ms, вызываемый миллион раз, часто важнее отчёта на 3 секунды. В MySQL ту же картину дают performance_schema и схема sys. MVCC в PostgreSQL хранит старые версии строк, пока VACUUM их не освободит. Обычно с этим справляется autovacuum, но очень большим или часто обновляемым таблицам нужны более жёсткие пороги на уровне таблицы, а долгая транзакция или забытый слот репликации блокирует очистку для всего кластера, вызывая bloat и рост занятого места. Следите за dead tuples, возрастом самой старой транзакции и размером крупнейших таблиц. Здоровая база — это в основном скучный мониторинг плюс правильные индексы, проверенные через EXPLAIN (ANALYZE, BUFFERS).

sql
-- postgresql.conf: shared_preload_libraries = 'pg_stat_statements' (restart)
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
 
SELECT calls,
       round(total_exec_time::numeric) AS total_ms,
       round(mean_exec_time::numeric, 1) AS mean_ms,
       left(query, 80) AS query
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 10;
 
-- Tables that need vacuum attention
SELECT relname, n_dead_tup, last_autovacuum
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC
LIMIT 10;

Реальные ситуации

01

Диск заполняется за ночь на спокойной базе

Проблема

Алерты о заполнении диска срабатывают в 3 часа ночи, хотя трафик низкий. Сам каталог данных почти не вырос, но pg_wal занимает много гигабайт и продолжает расти.

Как решить

WAL растёт, когда PostgreSQL не разрешено его удалять. Проверьте две причины. Первая — SELECT * FROM pg_stat_archiver: падающий archive_command (истёкшие учётные данные хранилища, переполненный бакет) оставляет каждый сегмент локально. Исправьте учётные данные, и архивирование догонит. Вторая — SELECT slot_name, active, pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) FROM pg_replication_slots: неактивный слот от удалённой реплики или CDC-инструмента держит WAL навсегда. Если он действительно не нужен, удалите его через pg_drop_replication_slot. Предотвратите повтор с помощью max_slot_wal_keep_size, алерта на сбои архивирования и алерта по диску на 80 процентах вместо 95.

02

DELETE без WHERE в продакшене

Проблема

В 11:58 кто-то вручную выполнил DELETE по таблице orders без условия WHERE. Приложение продолжает работать, новые заказы поступают.

Как решить

Не восстанавливайте поверх продакшена — так потеряются все заказы, оформленные с того момента. Восстановите последний базовый бэкап на отдельный инстанс и проиграйте WAL до момента прямо перед ошибкой, например pgbackrest restore --type=time с целью 11:57:30. Проверьте там количество строк, затем верните только удалённые строки — дампом этой таблицы или INSERT ... SELECT через postgres_fdw, пропуская уже существующие. Запишите точные шаги восстановления и затраченное время для отчёта об инциденте. После этого уберите у людей прямой доступ на запись в продакшен, требуйте транзакций для ручных исправлений и держите учения по восстановлению в календаре.

03

«Too many connections» после увеличения числа воркеров

Проблема

Команда удвоила число воркеров очередей и добавила второй сервер приложения. Теперь на пике PostgreSQL отклоняет соединения с «sorry, too many clients already», хотя CPU загружен лишь на 40 процентов.

Как решить

Посчитайте бюджет соединений: max_children PHP-FPM на сервер, умноженное на число серверов, плюс воркеры очередей, плюс cron и админские инструменты. Теперь это больше max_connections. Поднимать max_connections до тысяч — неправильное решение, потому что каждое соединение стоит памяти и конкуренции. Поставьте PgBouncer в режиме transaction на каждый сервер приложения или рядом с базой, направьте приложения на порт 6432 и держите реальный пул около 20–40 серверных соединений. Проверьте код на сессионные функции вроде advisory locks или команд SET, которые transaction pooling ломает. Затем мониторьте ожидания в пуле: если клиенты встают в очередь, узкое место — сама база, и запросы нужно оптимизировать.

Шпаргалка

Список кластеров PostgreSQL в Ubuntu
pg_lsclusters
Выполняющиеся запросы (PostgreSQL)
SELECT pid, now() - query_start AS age, state, left(query, 60) FROM pg_stat_activity WHERE state <> 'idle';
Отменить или убить запрос
SELECT pg_cancel_backend(12345); -- or pg_terminate_backend(12345)
Размер базы данных
SELECT pg_size_pretty(pg_database_size('app_db'));
Параллельно восстановить дамп в custom-формате
pg_restore -j 4 --no-owner -d app_db app_db.dump
Отставание реплики PostgreSQL
SELECT now() - pg_last_xact_replay_timestamp();
Выполняющиеся запросы (MySQL / MariaDB)
SHOW FULL PROCESSLIST;
Статус реплики (MySQL 8.0.22+)
SHOW REPLICA STATUS\G
Ограничить хранение binary log (MySQL)
binlog_expire_logs_seconds = 604800
Память Redis и большие ключи
redis-cli INFO memory && redis-cli --bigkeys

В моих реальных проектах

  • Я веду MariaDB/MySQL на серверах с хостинг-панелью и PostgreSQL с Redis на выделенном SaaS-сервере, где скрипты бэкапа входят в конфигурацию.
  • Мой базовый набор безопасности держит каждую базу на localhost или в частной сети, никогда публично.
  • Я сочетаю снапшоты серверов с проверенным восстановлением, а мой текущий план переносит бэкапы за пределы сервера.

Вопросы и ответы

Q1Чем логический бэкап отличается от физического?

Логический бэкап экспортирует данные в виде SQL-выражений или архива с помощью инструментов вроде pg_dump или mysqldump. Он переносим между версиями и позволяет восстановить одну таблицу, но для больших баз медленно создаётся и восстанавливается. Физический бэкап копирует файлы базы, например через pg_basebackup, pgBackRest или Percona XtraBackup. Для больших объёмов данных он гораздо быстрее, а в сочетании с непрерывным архивированием WAL или бинарного лога позволяет восстановиться на момент времени. Большинство серьёзных конфигураций использует физические бэкапы с архивированием логов для восстановления и периодические логические дампы для переносимости и восстановления отдельных таблиц.

Q2Как убедиться, что база данных не открыта в интернет?

Несколько слоёв. Во-первых, сервис привязан к 127.0.0.1 или интерфейсу частной сети, что я проверяю через ss -tlnp. Во-вторых, файрвол на хосте по умолчанию запрещает входящий трафик и пропускает только 22, 80 и 443; если доступ нужен серверам приложений, я разрешаю только частную подсеть. В-третьих, файрвол облачного провайдера дублирует эти правила. В-четвёртых, аутентификация всё равно строгая: никакого удалённого входа под root, scram-sha-256 в PostgreSQL, сильные пароли для каждого приложения. Для админского доступа с ноутбука я использую SSH-туннель, а не открываю порт. С Docker я привязываю опубликованные порты к 127.0.0.1, потому что Docker может обходить правила ufw.

Q3Как вы настроите PostgreSQL или MySQL на новом сервере?

Начинаю с памяти. Для MySQL innodb_buffer_pool_size на выделенном сервере занимает большую часть RAM, около 50–70 процентов, и меньше, если машину делит приложение. Для PostgreSQL shared_buffers около 25 процентов RAM, effective_cache_size около 50–75 процентов и скромный work_mem, потому что он умножается на каждую операцию и соединение. Держу max_connections разумным и добавляю PgBouncer при большом числе клиентов. Затем включаю лог медленных запросов, разбираю худшие запросы через EXPLAIN и добавляю индексы. Большинство реальных выигрышей в производительности приходит от запросов и индексов, а не от экзотических флагов конфигурации.

Q4Что такое RPO и RTO и как они влияют на стратегию бэкапов?

RPO (recovery point objective) — это сколько данных вы можете позволить себе потерять, измеренное во времени. RTO (recovery time objective) — сколько вы можете простаивать во время восстановления. Ночной дамп даёт RPO до 24 часов; если бизнес может потерять только минуты, нужно непрерывное архивирование WAL или binlog либо управляемая база с восстановлением на момент времени. RTO зависит от объёма данных и практики: логическое восстановление 200 GB может занять часы, а повышение реплики — минуты. Сначала я согласовываю эти цифры с бизнесом, затем выбираю инструменты и проверяю оба показателя учениями по восстановлению с замером времени.

Q5Использовать управляемую базу данных или вести свою?

Управляемая — более безопасный выбор по умолчанию, когда простой или потеря данных обходятся дорого, а специалиста по базам в команде нет: вы получаете автоматические бэкапы, восстановление на момент времени, патчи, реплики и failover. Компромиссы — стоимость, меньший контроль над версиями и расширениями и определённая привязка к вендору. Своя база дешевле и полностью гибкая и хорошо работает для небольших и средних проектов, когда бэкапы, мониторинг и обновления автоматизированы, а восстановление проверено. Я часто советую начинать MVP со своей базы на хорошо настроенном сервере и переходить на управляемую, когда выручка или требования к доступности это оправдают, ведь миграция — это запланированные дамп и восстановление.

Q6Как безопасно дать разработчику или аналитику доступ только на чтение?

Я создаю отдельную роль только для чтения с SELECT лишь на нужную схему или таблицы, плюс привилегии по умолчанию, чтобы покрывались и новые таблицы. Подключения идут через SSH-туннель или бастион, никогда через публичный порт. В идеале роль смотрит на реплику для чтения или недавно восстановленную копию, чтобы тяжёлые отчёты не тормозили продакшен. Если в данных есть персональная информация, я ограничиваю колонки через представления или даю анонимизированную копию. Доступ персональный, а не общий, поэтому его можно отозвать индивидуально, а логирование запросов или расширения аудита фиксируют, что выполнялось. Учётку удаляют, когда она человеку больше не нужна.

Вопросы уровня senior

S1Как вы проектируете failover базы данных и как избегаете split-brain?

Failover — это повышение standby до primary, когда primary умирает, и перенаправление приложения на него. Опасный случай — split-brain: старый primary лишь недоступен, а не мёртв, и оба принимают записи, создавая расходящиеся данные, которые мучительно сливать. Для защиты нужен механизм консенсуса или fencing. Patroni с etcd для PostgreSQL или managed-сервис держит leader lock и понижает или изолирует старый primary до повышения нового. Приложения подключаются через стабильный endpoint — виртуальный IP, прокси или DNS под управлением кластера. Синхронная репликация может гарантировать нулевую потерю данных ценой задержки записи и доступности при отказе standby. Небольшим командам managed-база обычно выгоднее, чем строить всё это правильно самим.

S2Redis хранит наши очереди и сессии. Как вы настраиваете persistence?

Redis предлагает RDB-снимки — периодические дампы на момент времени, компактные и быстро загружаемые, но теряющие записи с последнего снимка, — и AOF, журнал каждой записи в режиме append-only. С appendfsync everysec, обычной настройкой, сбой теряет максимум около секунды. Для очередей и сессий я включаю AOF плюс RDB, ставлю maxmemory-policy noeviction, чтобы Redis возвращал ошибки, а не молча выбрасывал задачи, и ставлю алерт на память задолго до лимита. Ещё я разделяю ответственность: инстанс для кэша с allkeys-lru и без persistence и инстанс для очередей с надёжностью. Если даже секунда потерянных задач неприемлема, источником истины для задач должна быть SQL-база, а Redis — только транспортом.

S3Как защитить бэкапы от ransomware или скомпрометированного сервера?

Исходите из того, что атакующий с root на сервере базы попытается удалить и бэкапы. Поэтому сервер должен уметь записывать бэкапы, но не удалять и не перезаписывать их: используйте бакет в отдельном аккаунте с object lock или версионированием плюс политикой хранения и учётные данные, которые разрешают только загрузку. Шифруйте бэкапы на стороне клиента ключами, хранящимися вне сервера, чтобы утёкший бакет ничего не раскрыл. Держите хотя бы одну копию у другого провайдера. Мониторьте отсутствующие или подозрительно маленькие бэкапы, потому что тихий сбой выглядит так же, как атака. И наконец, проводите учения по восстановлению из удалённой копии без доступа к исходному серверу — это ровно та ситуация, что бывает после инцидента.

S4PostgreSQL или MySQL для нового продукта?

Обе отличные, и опыт команды часто важнее возможностей. PostgreSQL предлагает более богатый SQL, мощный JSONB с индексами, частичные индексы и индексы по выражениям, расширения вроде PostGIS и pgvector, транзакционный DDL и строгую корректность, поэтому для сложных доменов и приложений с большим количеством аналитики это мой выбор по умолчанию. MySQL с InnoDB прост в эксплуатации, имеет очень зрелую репликацию, огромную экосистему хостинга и мгновенное добавление колонок для многих изменений схемы, а фреймворки вроде Laravel работают на нём одинаково хорошо. Важны и эксплуатационные различия: PostgreSQL раньше требует пулинга соединений и понимания vacuum, MySQL требует осторожности с online DDL на больших таблицах. Я выбираю PostgreSQL, если только существующие системы, хостинг или навыки команды не говорят в пользу MySQL.

Глоссарий

WAL
Write-ahead log: последовательная запись PostgreSQL о каждом изменении, делается до файлов данных; нужна для восстановления после сбоя и репликации.
Binary log
Журнал закоммиченных изменений в MySQL; используется для репликации и проигрывания событий после восстановления бэкапа.
PITR
Point-in-time recovery: восстановление базового бэкапа и проигрывание журналов до точного момента, например прямо перед ошибкой.
Слот репликации
Закладка PostgreSQL, удерживающая WAL, пока реплика или потребитель его не получит; забытые слоты заполняют диски.
Отставание репликации
Насколько реплика отстаёт от primary по времени или байтам; данные, прочитанные с неё, могут быть слегка устаревшими.
Пулер соединений
Прокси вроде PgBouncer, который делит несколько соединений с базой между множеством клиентов приложения.
MVCC
Multi-version concurrency control: читатели видят снимок, а писатели создают новые версии строк, поэтому они редко блокируют друг друга.
VACUUM
Обслуживание PostgreSQL, освобождающее место от мёртвых версий строк и обновляющее данные о видимости; обычно его запускает autovacuum.
Buffer pool
Основной кэш InnoDB в памяти для страниц данных и индексов; его размер во многом определяет производительность MySQL.
GTID
Глобальный идентификатор транзакции в MySQL: у каждой транзакции уникальный ID, чтобы реплики и failover надёжно отслеживали позицию.

Есть идея продукта?Давайте создадим его.

Расскажите о своей идее, продукте или команде. Обычно отвечаю в течение одного рабочего дня.