Как настроить репликацию чтения БД на Symfony 6 с помощью Doctrine

CoderStudio, 20.08.2023 11:51
Как настроить репликацию чтения БД на Symfony 6 с помощью Doctrine

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

Понимание концепции

В мире современных веб-приложений масштабируемость и производительность имеют

Прежде чем погружаться в технические детали, давайте разберемся в концепции реплик чтения/записи. В типичном веб-приложении существует два типа операций с базой данных: операции чтения (операторы SELECT) и операции записи (операторы INSERT, UPDATE, DELETE). Вместо того чтобы нагружать один сервер базы данных обоими типами операций, реплики чтения позволяют разгрузить запросы на чтение на отдельные серверы базы данных, тем самым распределяя нагрузку и повышая общую производительность. Кроме того, может быть полезно иметь экземпляры, доступные только для чтения, которые служат для вторичных целей (например.g. исследования), а не первичные.

Реализация

Ниже приведены шаги для проекта Symfony 6.2 , использующего doctrine/doctrine-bundle: ^2.7 . Для других младших версий этих пакетов (например, Symfony 6.4 и doctrine-bundle 2.10) процесс должен быть практически идентичным.

Начнем с конфигурации doctrine ( config/packages/doctrine.yaml ):

when@prod:
    doctrine:
        dbal:
            default_connection: default
            connections:
                default:
                    url: '%env(resolve:DATABASE_URL)%'
                    driver: pdo_pgsql
                    server_version: 15
                    replicas:
                        replica1:
                            url: '%env(resolve:DATABASE_RO_URL)%'

Как видно, в данном примере добавлена одна реплика (replica1), но вы можете добавить и другие (Doctrine произвольно выбирает одну) в зависимости от условий использования и загрузки базы данных. Важно отметить, что хотя для операций чтения предпочтение будет отдаваться экземпляру реплики, ваш основной экземпляр также будет выполнять их, если он был выбран ранее в течение всего времени существования соединения (обслуживания запроса). Если вы хотите выделить основной экземпляр только для операций записи, соединение по умолчанию в доктрине.yaml должен иметь дополнительный параметр keep_replica: true (для сохранения реальной реплики, а не использования основного экземпляра в качестве реплики), и после выполнения любых операций записи необходимо вручную обеспечить подключение к реплике:.

$connection = $this->getEntityManager()->getConnection();
if ($connection instanceof PrimaryReadReplicaConnection) {
  $connection->ensureConnectedToReplica();
}

Обратите внимание на условие instanceof - оно необходимо для того, чтобы обеспечить принудительное использование первичного соединения только в случае производственной среды, где реплика действительно настроена. В среде разработки ваше $connection, скорее всего, будет экземпляром Doctrine\DBAL\Connection .

Кроме того, в конфигурации doctrine.yaml DATABASE_URL ссылается на основной экземпляр вашей базы данных, а DATABASE_RO_URL - на реплику, доступную только для чтения - убедитесь, что эти переменные определены в вашем env-файле:

DATABASE_URL="postgresql://user:password@read-write-database-instance:5432/table?charset=utf8"
DATABASE_RO_URL="postgresql://user:password@read-database-instance:5432/table?charset=utf8"

В достаточно простых приложениях такой конфигурации должно быть достаточно, и при использовании методов Doctrine Repository или QueryBuilder она будет работать "из коробки". Тем не менее, иногда может потребоваться выбор SQL-запросов, когда вам важна производительность или вы просто предпочитаете не тратить время на реализацию собственных сложных DQL функций, поскольку не собираетесь переходить на другой движок базы данных и готовы пожертвовать DB-agnostic логикой запросов. При выполнении SQL-запросов необходимо помнить, что executeQuery - это метод, предназначенный только для операций READ. Приведенный ниже код не будет работать, так как Doctrine выберет реплику только для чтения и в итоге проксирует ошибки движка DB о том, что он не может выполнить операции записи.

$sql = <<<SQL
  UPDATE smart_contract SET uaw = 0
  FROM dapp_chain dc
  WHERE dc.id = smart_contract.dapp_chain_id AND dc.chain_id = :chainId;
SQL;

$this->getEntityManager()->getConnection()->executeQuery($sql, [
  'chainId' => $chainId,
]);

Для преодоления этой проблемы существует, по крайней мере, два возможных решения:

  1. (Рекомендуется) Предпочитать executeStatement вместо executeQuery для любого SQL-оператора, изменяющего/обновляющего любое состояние записи в базе данных. Использование executeStatement для операций вставки, удаления и других операций записи или транзакций (beginTransaction, commit, rollback) заставляет Doctrine выбирать первичное соединение.
  2. В некоторых случаях может потребоваться вручную заставить Doctrine Connection использовать основной экземпляр. Обновленный пример сценария может выглядеть следующим образом:
$sql = <<<SQL
  UPDATE smart_contract SET uaw = 0
  FROM dapp_chain dc
  WHERE dc.id = smart_contract.dapp_chain_id AND dc.chain_id = :chainId;
SQL;

$connection = $this->getEntityManager()->getConnection();
if ($connection instanceof PrimaryReadReplicaConnection) {
  $connection->ensureConnectedToPrimary();
}
$connection->executeQuery($sql, ['chainId' => $chainId]);

Заключение

В постоянно развивающемся ландшафте веб-приложений оптимизация производительности - это непрерывный путь. Интеграция реплик чтения/записи в проект Symfony с помощью пакета Doctrine может стать решающим фактором в этом стремлении. Стратегическое распределение нагрузки на базу данных позволяет повысить скорость отклика и масштабируемость приложения, обеспечивая бесперебойную работу пользователей даже при высоком трафике.

Есть что добавить к этой статье? Не стесняйтесь комментировать!