diff --git a/materials/09-data-replication/09-data-replication.pdf b/materials/09-data-replication/09-data-replication.pdf new file mode 100644 index 0000000..2e4b55e Binary files /dev/null and b/materials/09-data-replication/09-data-replication.pdf differ diff --git a/materials/09-data-replication/readme.md b/materials/09-data-replication/readme.md new file mode 100644 index 0000000..cf25275 --- /dev/null +++ b/materials/09-data-replication/readme.md @@ -0,0 +1,10 @@ +## 9. Репликация данных. + +### Лекция + +- [Презентация](09-data-replication.pdf) +- См. также материалы в конце + +### Семинар + +- [Материалы семинара](seminar/readme.md) \ No newline at end of file diff --git a/materials/09-data-replication/seminar/media/async-lost.png b/materials/09-data-replication/seminar/media/async-lost.png new file mode 100644 index 0000000..aacf6c4 Binary files /dev/null and b/materials/09-data-replication/seminar/media/async-lost.png differ diff --git a/materials/09-data-replication/seminar/media/dynamo-replication.svg b/materials/09-data-replication/seminar/media/dynamo-replication.svg new file mode 100644 index 0000000..cdbecc4 --- /dev/null +++ b/materials/09-data-replication/seminar/media/dynamo-replication.svg @@ -0,0 +1,3 @@ + + +
Server 4
Server 4
Server 1
Server 1
Server 2
Server 2
Server 3
Server 3
Server 5
Server 5
Server 6
Server 6
Key "K"
Key "K"
Server 1, 2, & 3 store data for keys in the range between server 6 & 1
Server 1, 2, & 3 store data for keys i...
Replica
Replica
Replica
Replica
Replication factor = 3
Replication factor = 3
Server 2, 3, & 4 store data for keys in the range between server 1 & 2
Server 2, 3, & 4 store data for keys in the range...
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/materials/09-data-replication/seminar/media/lrc.png b/materials/09-data-replication/seminar/media/lrc.png new file mode 100644 index 0000000..1eac2d1 Binary files /dev/null and b/materials/09-data-replication/seminar/media/lrc.png differ diff --git a/materials/09-data-replication/seminar/media/raid-0.png b/materials/09-data-replication/seminar/media/raid-0.png new file mode 100644 index 0000000..21ca62e Binary files /dev/null and b/materials/09-data-replication/seminar/media/raid-0.png differ diff --git a/materials/09-data-replication/seminar/media/raid-1.png b/materials/09-data-replication/seminar/media/raid-1.png new file mode 100644 index 0000000..41bb9ac Binary files /dev/null and b/materials/09-data-replication/seminar/media/raid-1.png differ diff --git a/materials/09-data-replication/seminar/media/raid-6.png b/materials/09-data-replication/seminar/media/raid-6.png new file mode 100644 index 0000000..3a32e5a Binary files /dev/null and b/materials/09-data-replication/seminar/media/raid-6.png differ diff --git a/materials/09-data-replication/seminar/media/sync-async.png b/materials/09-data-replication/seminar/media/sync-async.png new file mode 100644 index 0000000..230a968 Binary files /dev/null and b/materials/09-data-replication/seminar/media/sync-async.png differ diff --git a/materials/09-data-replication/seminar/media/sync-replication.png b/materials/09-data-replication/seminar/media/sync-replication.png new file mode 100644 index 0000000..ba73a5b Binary files /dev/null and b/materials/09-data-replication/seminar/media/sync-replication.png differ diff --git a/materials/09-data-replication/seminar/media/wal.svg b/materials/09-data-replication/seminar/media/wal.svg new file mode 100644 index 0000000..30cbd4b --- /dev/null +++ b/materials/09-data-replication/seminar/media/wal.svg @@ -0,0 +1,131 @@ +ClientKVStoreLog1Set(key1, value1)2Set(key2, value4)3Set(key3, value3)4Set(key4, value4)Write Ahead Log/var/log/wal000.logStoragekey1value1key2value2key3value3key4value4Set(key4, value4)1. Append2. Set(Key4, Value4) \ No newline at end of file diff --git a/materials/09-data-replication/seminar/readme.md b/materials/09-data-replication/seminar/readme.md new file mode 100644 index 0000000..196156d --- /dev/null +++ b/materials/09-data-replication/seminar/readme.md @@ -0,0 +1,239 @@ +# Семинар 9: Репликация + +В рамках данного семинара предлагается разобраться с концепцией репликации данных. +Идея простая — иногда вместо одного сервера необходимо завести его "реплики" (или копии), +где будет лежать одна и та же информация. + +Это позволяет добиться: +- Отказоустойчивость и сохранность данных — при отказе одного из серверов, данные можно получить с другого сервера. +- Увеличение производительности — если один сервер не справляется с обработкой всех запросов на чтение. +- Уменьшение latency — расположить сервера-реплики ближе к клиентам (пример — CDN). + +## Репликация в СУБД + +Как правило в существующих хранилищах (СУБД) репликация уже реализована и используется повсеместно. + +Как правило выделяется узел, который принимает запросы от пользователя на запись. Далее этот узел должен +отправить запрос на запись в остальные узлы (реплики). + +### Sync and async replication + +Подход, когда есть один узел, принимающий запросы на запись и реплицирующий данные на другие реплики, +называется leader-follower (ранее известный как master-slave). + +![sync replication](media/sync-replication.png) + +*[Источник](https://arpitbhayani.me/blogs/replication-strategies/)* + +Выделяют два вида репликации по принципу "когда клиент получает ответ на запрос записи": +- **Синхронная репликация** — данные сразу дублируются на все реплики, и только после + подтверждения записи на других репликах, клиент получает ответ об успешной записи. + Это позволяет избежать потери данных при отказе узла, который производит запись, но сложнее + в реализации и увеличивает latency ответа клиенту. +- **Асинхронная репликация** — клиент получает ответ сразу после успешной записи на узел, который + изначально принял запрос пользователя. Данные на реплики дублируются в фоновой операции. + +Возможна комбинация данных режимов: например, одна sync-реплика и несколько async-реплик: +![sync and async replica](media/sync-async.png) + +*Источник: Martin Kleppmann, Designing Data-Intensive Applications* + +Асинхронный режим репликации проще в реализации, уменьшает latency ответа клиенту, +но может привести к потере данных (лидер не успел отправить всю информацию на реплики). + +Также в async-репликации возможны так называемые stale чтения (чтение возвращает не самую актуальную информацию): +![stale read from async replica](media/async-lost.png) + +*Источник: Martin Kleppmann, Designing Data-Intensive Applications* + +### Что реплицировать + +Возникает вопрос, а что именно необходимо реплицировать. + +**Репликация запросов**. Лидер, приняв запрос от клиента, выполняет аналогичный запрос на всех репликах, +а затем возвращает ответ клиенту. Вариант прост для реализации и довольно экономный, однако возможны трудности +с недетерминированными запросами. Если запрос полагается на вызов rand(), это значение на каждом из +узлов может быть разным, что приведет к расхождению данных. + +**Row-based репликация**. Если представить, что наша модель данных может быть представлена в виде таблицы +(например, таблицы из пар ключ-значение), то можно реплицировать все изменения строк в данной таблице. +Таким образом мы не будем страдать от недетерминированных запросов, так как предварительно лидер применяет +все изменения к своему состоянию и только потом отправляет измененные значения строк. Однако в случае +массовых операций обновления (PUT *=NULL) потребуется передавать довольно много информации. + +**Репликация файловой системы**. Можно реплицировать файловую систему. При вызове системного вызова +write(offset, "data") этот запрос проксируется на все реплики, и каждая из них выполняет запись в +свою локальную файловую систему. Реализация возможна на уровне операционной системы, однако в таком +подходе необходимо следить за совместимостью версий операционной системы и файловой системы, что не всегда +возможно. + +Идеального подхода к репликации не существует; необходимо выбирать его из решаемой задачи и имеющихся +ограничений. + +В современных СУБД для восстановления и репликации часто используют WAL (write-ahead log) — журнал +предзаписи. При изменении данных СУБД формирует запись WAL и обновляет страницу в оперативной памяти; +страница может измениться в памяти раньше, чем журнал попадёт на диск. Правило write-ahead требует сохранить WAL +на диске до того, как туда будет записана изменённая страница. Если после сбоя страница на диске оказалась +устаревшей, СУБД может воспроизвести журнал и восстановить недостающие изменения. + +Для репликации лидер передаёт записи WAL реплике, а та применяет их у себя в том же порядке. При физической +репликации узлам нужны совместимые форматы страниц и WAL. + +![write ahead log](media/wal.svg) + +*[Источник](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). + +![Dynamo replication](media/dynamo-replication.svg) + +*[Источник](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: + +

+https://en.wikipedia.org/wiki/File:RAID_0.svg +

+ +Вторая простая идея — это использовать несколько дисков для хранения одного и того же файла +(или блока данных). Это называют RAID 1: + +

+https://en.wikipedia.org/wiki/File:RAID_1.svg +

+ +Можно также обратиться к теории алгебры и вспомнить коды Хэмминга и коды Рида-Соломона, которые +позволяют детектировать ошибки в записи и хранении данных. На N полезных байт можно завести дополнительные +K байт для исправления ошибок. Такой подход называется RAID 6 (используются коды Рида-Соломона): + +

+https://en.wikipedia.org/wiki/File:RAID_6.svg +

+ +Не погружаясь в детали алгебру, идею восстанавливающих кодов можно понять на примере операции XOR. +Допустим есть две половины данных: A и B. Давайте посчитаем `C = A ^ B`. Распределим эти три блока по +трём дискам. Тогда при потере одного из дисков мы всегда можем обратиться к оставшимся двум и восстановить +данные с помощью великолепного свойства операции XOR: `A = C ^ B`. + +## Erasure coding + +Подход с RAID-массивами можно обратно перенести на уровень приложений в распределенных системах. + +Стандартная репликация данных как правило влечет большой расход дискового пространства: приходится +хранить на 200% больше данных, чем нужно. + +Подход Erasure coding предлагает использовать коды коррекции ошибок на уровне репликации хранилищ. +При записи очередного блока данных необходимо распределить этот блок и его parity-блоки (чексуммы) +на несколько других реплик. Тем самым мы сможем добиться меньшего дублирования данных и по-прежнему +уметь переживать отказы небольшого числа узлов. + +Этим свойством пользуются при реализации хранилищ, которые должны работать в рамках большого кластера +(сотни и тысячи узлов). + +При реализации данного подхода часто обращаются к алгоритму +[Erasure Coding in Windows Azure Storage](https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/LRC12-cheng20webpage.pdf). + +

+Microsoft LRC +

+ +## Линеаризуемость. Примеры для самостоятельного разбора + +В каждой истории исходно `x = 0`, а все операции указаны в таблице. Числа обозначают моменты на общей +условной временной оси. `write(x, 1)` записывает единицу, `read(x)` возвращает значение `x`. + +История линеаризуема, если каждой операции можно назначить точку между её вызовом и ответом так, чтобы +в полученном порядке каждое чтение возвращало значение последней записи или исходное значение, если записей +ещё не было. Если одна операция завершилась до вызова другой, их порядок нужно сохранить. + +Для каждой истории попробуйте расставить точки линеаризации или объяснить, почему это невозможно. + +### История 1 + +| Клиент | Операция | Вызов | Ответ | Результат | +| --- | --- | ---: | ---: | --- | +| A | `write(x, 1)` | 1 | 6 | `OK` | +| B | Первое `read(x)` | 2 | 3 | `0` | +| B | Второе `read(x)` | 4 | 5 | `1` | + +Эту историю можно линеаризовать. Выберем точку первого чтения в момент 2,5, записи — в момент 3,5, +второго чтения — в момент 4,5. Получим порядок «чтение 0, запись 1, чтение 1». +Все точки лежат внутри интервалов своих операций. Второе чтение может увидеть единицу ещё до ответа +на запись, поскольку эти операции перекрываются во времени. + +### История 2 + +| Клиент | Операция | Вызов | Ответ | Результат | +| --- | --- | ---: | ---: | --- | +| A | `write(x, 1)` | 1 | 6 | `OK` | +| B | Первое `read(x)` | 2 | 3 | `1` | +| B | Второе `read(x)` | 4 | 5 | `0` | + +Эту историю линеаризовать нельзя. Чтобы первое чтение вернуло единицу, запись должна стоять перед ним. +Первое чтение завершилось до вызова второго, поэтому второе обязано стоять ещё позже. +Оно тоже должно вернуть единицу, поскольку других записей нет. Перекрытие записи с обоими чтениями +не позволяет объяснить возврат к нулю. + +Такой сценарий возможен при чтении с разных реплик, если первое чтение попало на реплику с новым +значением, а второе — на отстающую. Первая история показывает, что чтение во время записи может +возвращать разные значения, а вторая — почему порядок этих ответов имеет значение. + +## Полезные материалы + +- [Видео](https://youtu.be/gkg-FAEXIkY) и [вики-страница](https://en.wikipedia.org/wiki/CAP_theorem) о CAP-теореме +- [Критика CAP-теоремы](https://jvns.ca/blog/2016/11/19/a-critique-of-the-cap-theorem/) +- [Описание режимов репликации в Postgres](https://www.postgresql.org/docs/current/different-replication-solutions.html) +- [Кворумная репликация в Dynamo](https://www.allthingsdistributed.com/files/amazon-dynamo-sosp2007.pdf) (секция 4) +- Коды коррекции: [код Хэмминга](https://en.wikipedia.org/wiki/Hamming_code) и [коды Рида-Соломона](https://en.wikipedia.org/wiki/Reed%E2%80%93Solomon_error_correction) +- [Понятное описание различных RAID-стандартов](https://en.wikipedia.org/wiki/Standard_RAID_levels) +- [Особенности устройства и обращения с HDD и как выбирать длину блока](https://ioflood.com/blog/optimal-raid-stripe-size-and-filesystem-readahead-for-raid-10/#:~:text=In%20modern%20operating%20systems%2C%20the,raid%20cards%20default%20stripe%20size) +- [How RAID-6 dual parity calculation works](https://igoro.com/archive/how-raid-6-dual-parity-calculation-works/) +- [Описание Erasure coding в версии семинара 2018](https://github.com/osukhoroslov/distsys-course-hse/tree/2020/seminars/08-scalability#erasure-coding) +- [Алгоритм Erasure Coding в Windows Azure Storage](https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/LRC12-cheng20webpage.pdf)