SQL и Qt

Репликация: master-slave, master-master, для чего нужна

Введение

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

Концепция

Master-slave (также называемая primary-replica) репликация — одна «главная» БД (master/primary) принимает все операции записи, и одна или несколько «подчинённых» копий (slave/replica) асинхронно или синхронно получают и применяют те же изменения, обычно используемых для распределения нагрузки чтения (приложение направляет запросы на чтение на реплики, разгружая основной сервер) и/или для повышения отказоустойчивости (реплика может быть продвинута до роли нового master при отказе исходного). Master-master репликация позволяет принимать операции записи на нескольких узлах одновременно, синхронизируемых друг с другом, что даёт большую гибкость, но значительно усложняет разрешение конфликтов при одновременном изменении одних и тех же данных на разных узлах.

Пример кода: Репликация

-- Настройка master-slave репликации в PostgreSQL — концептуальный обзор основных шагов
-- (полная настройка требует конфигурации на уровне файлов postgresql.conf/pg_hba.conf,
-- не показанной полностью здесь ради краткости иллюстрации концепции)

-- На master-сервере: включение архивирования WAL (Write-Ahead Log) для передачи реплике
-- wal_level = replica
-- max_wal_senders = 3

-- На slave-сервере: настройка как реплики конкретного master-сервера
-- standby.signal (файл-маркер режима реплики)
-- primary_conninfo = 'host=master_server port=5432 user=replicator'
// На уровне приложения — направление запросов на чтение и запись на разные соединения
class DatabaseRouter
{
public:
    QSqlDatabase &writeConnection() { return m_masterConnection; } // только для INSERT/UPDATE/DELETE
    QSqlDatabase &readConnection() { return m_replicaConnection; }  // для SELECT, разгружает master

private:
    QSqlDatabase m_masterConnection;
    QSqlDatabase m_replicaConnection;
};

void exampleUsage(DatabaseRouter &router)
{
    QSqlQuery writeQuery(router.writeConnection());
    writeQuery.exec("INSERT INTO orders (...) VALUES (...)");

    QSqlQuery readQuery(router.readConnection());
    readQuery.exec("SELECT * FROM orders WHERE customer_id = 5");
    // ВНИМАНИЕ: из-за асинхронной природы репликации этот SELECT может НЕ увидеть
    // только что вставленную через writeConnection строку — см. подводные камни (replication lag)
}

Пояснения к коду

Концептуальный обзор настройки master-slave репликации PostgreSQL (через wal_level/max_wal_senders на master, и standby.signal/primary_conninfo на slave) показывает, что репликация в PostgreSQL построена на передаче и применении WAL (журнала транзакций) от master к репликам — тот же механизм, что обеспечивает долговечность (Durability из ACID, статья 408) отдельных транзакций на одном сервере, расширяется для синхронизации между несколькими серверами. DatabaseRouter показывает архитектурный паттерн на уровне приложения — явное разделение соединений для записи (всегда на master) и чтения (на реплику, для разгрузки основного сервера), с явным предупреждением о возможной задержке репликации (replication lag), из-за которой реплика может на короткое время отставать от реального, актуального состояния master.

Подводные камни

  • Чтение с реплики сразу после записи на master без учёта возможной задержки репликации (replication lag), что может привести к ситуации «пользователь только что создал заказ, но не видит его в списке своих заказов», если страница со списком заказов читается с реплики, ещё не успевшей применить только что сделанное изменение с master — для операций, где немедленная видимость собственного только что сделанного изменения критична, нужно либо явно читать с master сразу после соответствующей записи (хотя бы для этого конкретного, чувствительного к задержке случая), либо использовать синхронную репликацию (с соответствующей платой в задержке самой операции записи, ожидающей подтверждения от реплики).
  • Использование master-master репликации без продуманной стратегии разрешения конфликтов при одновременном изменении одних и тех же данных на разных узлах — в отличие от master-slave (где единственный master исключает возможность конфликта одновременной записи одних и тех же данных), master-master принципиально создаёт возможность такого конфликта, и без явной, продуманной стратегии его разрешения (последняя запись побеждает, явное объединение изменений, отказ от записи при обнаружении конфликта) поведение системы при реальном возникновении такого конфликта может быть неожиданным и труднопредсказуемым.
  • Восприятие репликации как полноценной замены резервного копирования (статья 431) — хотя реплика действительно даёт дополнительную копию данных, она синхронизирована с master почти в реальном времени, что означает: ошибочное, разрушительное изменение данных (например, случайный DELETE без WHERE, статья 395) на master почти немедленно реплицируется и на все реплики тоже, не давая защиты от такой человеческой ошибки — для защиты именно от таких сценариев нужны отдельные, не синхронизируемые в реальном времени резервные копии с возможностью восстановления на конкретный момент времени в прошлом.
  • Недооценка операционной сложности поддержания репликации (мониторинг задержки репликации, обработка сценариев отказа master с продвижением реплики, переключение приложения на новый master) — настройка самой репликации технически относительно прямолинейна, но полноценная, надёжная эксплуатация репликации в production требует значительно большего операционного внимания и автоматизации (мониторинг, автоматическое или полу-автоматическое переключение при отказе), чем может показаться при первоначальной, простой настройке.