# Семинар 9: Репликация В рамках данного семинара предлагается разобраться с концепцией репликации данных. Идея простая — иногда вместо одного сервера необходимо завести его "реплики" (или копии), где будет лежать одна и та же информация. Это позволяет добиться: - Отказоустойчивость и сохранность данных — при отказе одного из серверов, данные можно получить с другого сервера. - Увеличение производительности — если один сервер не справляется с обработкой всех запросов на чтение. - Уменьшение latency — расположить сервера-реплики ближе к клиентам (пример — CDN). ## Репликация в СУБД Как правило в существующих хранилищах (СУБД) репликация уже реализована и используется повсеместно. Как правило выделяется узел, который принимает запросы от пользователя на запись. Далее этот узел должен отправить запрос на запись в остальные узлы (реплики). ### Sync and async replication Подход, когда есть один узел, принимающий запросы на запись и реплицирующий данные на другие реплики, называется leader-follower (ранее известный как master-slave).  *[Источник](https://arpitbhayani.me/blogs/replication-strategies/)* Выделяют два вида репликации по принципу "когда клиент получает ответ на запрос записи": - **Синхронная репликация** — данные сразу дублируются на все реплики, и только после подтверждения записи на других репликах, клиент получает ответ об успешной записи. Это позволяет избежать потери данных при отказе узла, который производит запись, но сложнее в реализации и увеличивает latency ответа клиенту. - **Асинхронная репликация** — клиент получает ответ сразу после успешной записи на узел, который изначально принял запрос пользователя. Данные на реплики дублируются в фоновой операции. Возможна комбинация данных режимов: например, одна sync-реплика и несколько async-реплик:  *Источник: Martin Kleppmann, Designing Data-Intensive Applications* Асинхронный режим репликации проще в реализации, уменьшает latency ответа клиенту, но может привести к потере данных (лидер не успел отправить всю информацию на реплики). Также в async-репликации возможны так называемые stale чтения (чтение возвращает не самую актуальную информацию):  *Источник: Martin Kleppmann, Designing Data-Intensive Applications* ### Что реплицировать Возникает вопрос, а что именно необходимо реплицировать. **Репликация запросов**. Лидер, приняв запрос от клиента, выполняет аналогичный запрос на всех репликах, а затем возвращает ответ клиенту. Вариант прост для реализации и довольно экономный, однако возможны трудности с недетерминированными запросами. Если запрос полагается на вызов rand(), это значение на каждом из узлов может быть разным, что приведет к расхождению данных. **Row-based репликация**. Если представить, что наша модель данных может быть представлена в виде таблицы (например, таблицы из пар ключ-значение), то можно реплицировать все изменения строк в данной таблице. Таким образом мы не будем страдать от недетерминированных запросов, так как предварительно лидер применяет все изменения к своему состоянию и только потом отправляет измененные значения строк. Однако в случае массовых операций обновления (PUT *=NULL) потребуется передавать довольно много информации. **Репликация файловой системы**. Можно реплицировать файловую систему. При вызове системного вызова write(offset, "data") этот запрос проксируется на все реплики, и каждая из них выполняет запись в свою локальную файловую систему. Реализация возможна на уровне операционной системы, однако в таком подходе необходимо следить за совместимостью версий операционной системы и файловой системы, что не всегда возможно. Идеального подхода к репликации не существует; необходимо выбирать его из решаемой задачи и имеющихся ограничений. В современных СУБД для восстановления и репликации часто используют WAL (write-ahead log) — журнал предзаписи. При изменении данных СУБД формирует запись WAL и обновляет страницу в оперативной памяти; страница может измениться в памяти раньше, чем журнал попадёт на диск. Правило write-ahead требует сохранить WAL на диске до того, как туда будет записана изменённая страница. Если после сбоя страница на диске оказалась устаревшей, СУБД может воспроизвести журнал и восстановить недостающие изменения. Для репликации лидер передаёт записи WAL реплике, а та применяет их у себя в том же порядке. При физической репликации узлам нужны совместимые форматы страниц и WAL.  *[Источник](https://martinfowler.com/articles/patterns-of-distributed-systems/write-ahead-log.html)* ### Кворумная репликация Дожидаться подтверждения операции от всех реплик может быть накладно, однако хочется недопустить потери значения. В таких условиях возникла идмея кворумной репликации. [Кворум](https://en.wikipedia.org/wiki/Quorum_(distributed_computing)) — это минимальное количество подтверждений, которое необходимо получить для того, чтобы считать операцию выполненной. Допустим, мы хотим реплицировать данные на три реплики. При получении запроса будем отправлять запросы на запись на все три реплики, но дожидаться ответа будем только от двух реплик (от тех, что выполнили операцию быстрее). Если ответ получен от двух реплик, то считаем операцию выполненной. Внимательный слушатель заметит, что при такой реализации запрос на чтение может вернуть неактуальные данные, если одна из реплик еще не успела применить обновления. Чтобы решить эту проблему, предлагается отправлять запросы на чтение на все реплики, дожидаться ответа от двух самых быстрых реплик и выбирать наиболее актуальную версию значения из полученных (тему версионирования затронем далее в курсе). В таком случае нам неважно, какие реплики подтвердили запись и ответили на операцию чтения. Так как на операцию записи и чтения мы обращались на две реплики, как бы мы не выбрали эти множества реплик, они обязательно пересекутся как минимум по одной реплике. Эта реплика будет содержать наиболее актуальные данные. Если обобщить задачу на произвольное число реплик N, то для кворума необходимо выбрать числа R и W. R — число реплик, от которых мы ждем ответа на операцию чтения, W — число реплик, которые должны подтвердить операцию записи. Чтобы система возвращала актуальные значения, должно выполняться условие `R + W > N`. В зависимости от отношения и сложности операций чтения и записи можно выбирать наиболее подходящие R и W для вашей системы. Реализацию этой идеи можно увидеть в СУБД Cassandra (и почитать в paper'e о Dynamo, секция 4, sloppy quorum & hinted handoff).  *[Источник](https://www.designgurus.io/course-play/grokking-the-advanced-system-design-interview/doc/replication)* В [учебном примере KV](../../08-scaling/seminar/kv/) есть режим `replication-quorum`. Прокси последовательно обращается ко всем узлам. Запись успешна, если её подтвердило большинство узлов. При чтении прокси возвращает значение, которое совпало в ответах большинства. Например, для трёх узлов ответы `v₂, v₂, v₁` дают `v₂`, а три разных значения приводят к ошибке. Этот пример показывает голосование по значениям. Для выбора самой свежей версии, описанного выше, понадобятся метки версий и другое правило обработки ответов. ## RAID [RAID](https://en.wikipedia.org/wiki/Standard_RAID_levels) (redundant array of inexpensive disks) — переносит идею репликации данных из распределенной системы на уровень одного компьютера. Оказывается, к одному серверу можно подключить сразу несколько дисков с целью увеличения скорости доступа и надежности сохранности (пережить отказ диска). Первая простая идея — поставить два диска рядом, писать половину данных на первый диск, вторую половину — на второй. Надежность не увеличивается, но зато увеличивается вместительность и скорость доступа. Такой подход называется RAID 0: