Add week 9 materials
This commit is contained in:
1 parent
8963ac82d5
commit
a4dc591ca0
12 files changed
+383
No files matched your search
@@ -0,0 +1,239 @@
|
||||
# Семинар 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:
|
||||
|
||||
<p align="center">
|
||||
<img alt="https://en.wikipedia.org/wiki/File:RAID_0.svg" src="media/raid-0.png" width=200px/>
|
||||
</p>
|
||||
|
||||
Вторая простая идея — это использовать несколько дисков для хранения одного и того же файла
|
||||
(или блока данных). Это называют RAID 1:
|
||||
|
||||
<p align="center">
|
||||
<img alt="https://en.wikipedia.org/wiki/File:RAID_1.svg" src="media/raid-1.png" width=200px/>
|
||||
</p>
|
||||
|
||||
Можно также обратиться к теории алгебры и вспомнить коды Хэмминга и коды Рида-Соломона, которые
|
||||
позволяют детектировать ошибки в записи и хранении данных. На N полезных байт можно завести дополнительные
|
||||
K байт для исправления ошибок. Такой подход называется RAID 6 (используются коды Рида-Соломона):
|
||||
|
||||
<p align="center">
|
||||
<img alt="https://en.wikipedia.org/wiki/File:RAID_6.svg" src="media/raid-6.png" width=400px/>
|
||||
</p>
|
||||
|
||||
Не погружаясь в детали алгебру, идею восстанавливающих кодов можно понять на примере операции 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).
|
||||
|
||||
<p align="center">
|
||||
<img alt="Microsoft LRC" src="media/lrc.png" width=600px/>
|
||||
</p>
|
||||
|
||||
## Линеаризуемость. Примеры для самостоятельного разбора
|
||||
|
||||
В каждой истории исходно `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)
|
||||
Reference in new issue
Block a user