Высоконагруженные системы. Глава 5. Репликация.
Tags: высоконагруженныеприложения, клеппман, книги
Published: 2024-02-02 19:42:09

## Основное

Репликация - это хранение копий одних и тех же данных по нескольким серверам, соединенным по сети. Репликация служит для нескольких целей:

* Доставка контента из географически близкого к пользователю сервера

* Чтобы система продолжала работать при отказе некоторых серверов БД

* Для горизонтально масштабирования по чтению

Говоря о репликация, пользуются термином узел (синоним сервера). Узел при репликации может быть ведущим (куда может осуществляться и запись, и чтение) и ведомым (только для чтения).

Репликация бывает следующих видов с точки зрения узлов:

* Репликация с одним ведущим узлов. Клиенты пишут в один узел. После этого записи отправляются в ведомые узлы. Есть риск чтения устаревшых данных с узлов, куда данные по какой-то причине не дошли.

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

* Репликация без ведущего узла. Название немного не отражает сути - ведь все узлы ведущие. Клиенты могуть писать в любые узлы. Данные в этот момент не отправляются в другие узлы. Только при чтении происходит проверка всех узлов в поиске самой последней версии. Отдаются свежие данные и во время/после этого актуализируются остальные ноды.



Репликация бывает следующих видов в точки зрения момента отправки записей с одного узла на другой:

* Синхронная. Гарантирует согласованность данных на всех узлах. Клиент всегда будет читать свежие данные. Однако обладает низкой доступностью, так как один недоступный узел влечет неуспех записи на остальных узлах, так как повлечет откат. И скорость записи снижается, так как клиент должен дождаться, пока данные скопируются на все узлы.

* Асинхронная. Не гарантирует согласованности, поэтому есть вероятность чтения устаревшых данных. Обладает высокой доступностью, так как копирование с одного узла на другой происходит уже после того, как клиент получил "успех" при записи. Скорость записи высокая.

* Смешанная. Не гарантирует абсолютной согласованности и обладает средней доступностью. На какие-то узлы репликация синхронна. После этого запись для клиента считается "успешной". Затем в фоне происходит копирование на остальные ведомые узлы.

Как раз смешанный вариант на практике показывает селя лучше всего.



## Реализация репликации

* Операторная. Заключается в том, что с ведущего узла на ведомые передаются sql операторы. Однако в таком виде недерменированные функции (now(), rand()) возвращали бы на разных узлах разные результаты. Аналогично с счетчиками, которые должны для каждой записи выполняться строго в том же порядке, что и на ведущем узле. Поэтому на практике команды передаются немного в измененном виде - вызов недерменированных функция заменен на их результат на ведущем узле.
Раньше такая репликация использовалась в MySQL.

* Перенос WAL журнала
Все записываемые данные заносятся в журнал упреждаюей записи. Этот журнал хранится в бинарном виде. Можно этот журнал отправлять на ведомые узлы. Недостаток в том, что есть привязка к внутренней структуре бинарного файла (к уровню физического представления), которая от версии к версии субд может меняться. Поэтому во первых, на всех узлах должны быть СУБД одинаковой версии, а обновление СУБД становится головной болью.
Этот метод репликации используется в PostgreSQL.

* Логическая репликация. Подразумевает использование логического журнала, в котором в утвержденном формате хранятся строки, однозначно описыващие операции с данными. Например, для обновления это идентификатор, список измененных полей и их новых значения. Для удаление - только идентификатор. С ведущего узла на ведомые передается записи из логического журнала. Формат этих записей не привязан к версии СУБД и может легко парсится внешними приложениями


## Проблемы задержки репликации

Как уже написал, использование асинхронной репликации может приводить к несогласованности данных между узлами. Рассмотрим возможные проблемы и пути их решения.


### Чтение своих же устаревшых записей

Это очень частый сценарий - пользователь точно знает, какие данные вставлял или модифицировал. Поэтому, если отдадутся устаревшие данные, пользователь это сразу заметит. Вариантов решения несколько:

* "свои" данные читать только с ведущего узла (например, информация о своем профиле) - в большинстве случаев невозможен, так как таких данных может быть очень много. Да и вообще на корню рушит идею высокой доступности, ради которой затевалась идея с репликацией.

* отслеживать время последней модификации данных - на первое время читать с ведущего, а все остальное время с ведомого.

* развитие предыдущего пункта - передавать при чтении метку времени последней записи, и если напротив записи в БД метка меньше (т.е. изменения еще не дошли), то читать с ведущего

В идее с меткой времени также стоит помнить, что клиент может заходить с разных устройств, и информацию о времени последней модификации нужно хранить в централизованном месте (этим снижается доступность).


### Монотонное чтение

Есть вероятность, что при каждом чтении пользовател будет видеть данные в разном порядке.
Возможное решение - дать пользователю возможность читать только с какой-то одной реплики (которая присваиватся пользователю по хэшу ид пользователя, например). Тогда есть гарантия, что чтения будут в неименающимся порядке порядке (но гарантии чтения самых актуальных данных нет, так как данные по присвоенного ведомого узла могли не дойти). Также нет гарантии, что пользователь будет видеть записи в том порядке, в котором они были при записи (для этого при чтении можно делать сортировку).


### Согласованное префиксное чтение

Есть вероятность потери причинно-следственных связей, если на реплику более новая запись попала раньше более старой (например, сначала ответ, затем - чтение). Гарантия согласованного префиксного чтения гарантирует, что данные будут прочитаны в том же порядке, в котором были записаны. Для этого можно использовать метки времени, читая в отсортированном виде.



## Репликация с несколькими ведущими узлами

### обработка конфликтов

Предположим, что есть 2 ведущих узла - 1 и 2. И есть пользователь 1 и 2, и записывают они данные в таком порядке 1 -> 1, 2 -> 2. Первый пользователь меняет запись A на B на ведущем узле 1, второй - A на C на ведущем узле 2. Возникает конфликт при попытке реплицировать изменения с одного ведущего на другой. Возможное решение - изменение одной и той же строки делать через один и тот же узел. Если узел выходит из строя, менять на другой. Если такой возможности нет, то можно напротив строчки ставить метку времени и при конфликте выигрывает значение с самым большим значением этой метки.


### Топология репликации с несколькими ведущими узлами


Сущестуют следущие топологии

* Колько

* Звезда

* Каждый с каждым



![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/r4pzt7pd9lqdzfpgb1fe.png)

Топология "каждый с каждым" самая общая и надежная. Топология "звезда" подразумевает использование центрального узла, через которой идет коммуникация, что снижает надежность. Топология "кольцо" самая ненадежная (и несмотря на это, используется в MySQL), так как узел может получить запись строго от конкретного узла. И если один узел вышел из строя, то репликация до узлов, следующих за ним, встанет.


## Репликация без ведущих узлов

При записи какие-то узлы могут быть недоступны. Пусть есть n узлов, на w узлов усуществилась запись и с r узлов читают. Тогда гарантией, что всегда при такой конфигурации будут читаться актульные данные, является условие w + r > n (это условие показывает, что узлы для записи и чтения пересекаются) - тогда операции записи и чтения называются операциями по кворуму. Тогда можно считать итоговый результат операции записи или чтения неуспешным, если не выполняется условие выше.