Распределённое хранилище YDB
YDB distributed storage — это подсистема YDB, которая отвечает за надёжное хранение данных.
Блобовый интерфейс позволяет хранить блобы (бинарные фрагменты размером от 1 байта до 10 мегабайт) с уникальным идентификатором. Подсистема также предоставляет примитивы DDisk и PersistentBuffer для прямого блочного доступа. Эти пути используют общую инфраструктуру управления устройствами и размещения, но имеют разные контракты данных и репликации.
Компоненты и границы ответственности
| Компонент | Ответственность | С чего начать |
|---|---|---|
| DS proxy | Выполняет запросы к группам блобов, включая erasure-кодирование и обеспечение избыточности на уровне группы. | Исходный код DS proxy |
| VDisk | Хранит части блобов группы и поддерживает их индексы, синхронизацию, сборку мусора и состояние восстановления. | Исходный код VDisk |
| PDisk | Владеет физическим устройством, выделяет чанки владельцам и предоставляет журналирование и ввод-вывод устройства. | Исходный код PDisk |
| DDisk и PersistentBuffer | Предоставляют адресуемое блочное хранилище и сохраняемые промежуточные записи для клиентов прямого блочного доступа. | DDisk, PersistentBuffer |
| NodeWarden | Применяет конфигурацию хранения на узле и управляет локальными акторами PDisk, VDisk/DDisk и прокси. | NodeWarden |
| Контроллер BlobStorage (BSC) | Поддерживает пулы хранения, размещение, конфигурацию групп и закрепление ресурсов за группами прямого блочного доступа. | Исходный код контроллера |
Обычный путь блобовых запросов — tablet → DS proxy → VDisk → PDisk. При прямом блочном доступе клиент, например партиция NBS, отправляет запросы непосредственно сервисам DDisk и PersistentBuffer. Этот клиент выбирает политику репликации, кворума, повторных попыток и переноса данных из PB. DDisk не реализует блобовый протокол VDisk или его алгоритм восстановления группы.
Группы прямого блочного доступа описывает размещение и выделение ресурсов для прямого блочного доступа. Остальная часть этой страницы посвящена обычному блобовому интерфейсу и группам VDisk.
Общие контракты
- Сервисные идентификаторы акторов обозначают расположение или сервис. Актор, обслуживающий такой идентификатор, может быть заменён при восстановлении; сам по себе идентификатор не доказывает, что запрос относится к текущему экземпляру.
- Поколение таблетки, поколение группы блобов, раунд владельца PDisk и токен соединения DDisk имеют разных владельцев и назначение. На каждом интерфейсе сохраняйте соответствующее значение защиты от устаревших запросов, не подменяя его другим.
- Закрепление ёмкости пула, локальное выделение чанков и завершённая запись данных — разные состояния. Успешное выделение ресурсов контроллером не подтверждает запись данных.
- Успех запроса ограничен областью ответственности получателя. Запись на локальное устройство или одна успешная реплика не означают автоматического подтверждения надёжного сохранения на уровне группы.
- Правила жизненного цикла акторов и обмена сообщениями применяются к таблеткам, сервисам хранения, акторам запросов и завершениям их асинхронных операций ввода-вывода.
Расположение исходного кода
Начните с ydb/core/blobstorage: там находятся dsproxy/, vdisk/, pdisk/, ddisk/ и nodewarden/. Общие для нескольких компонентов определения событий и форматов также находятся в ydb/core/base и ydb/core/protos; транзакции размещения — в ydb/core/mind/bscontroller. Низкоуровневая реализация TUringRouter находится в ydb/library/pdisk_io, а политика партиции NBS — в ydb/core/nbs/cloud/blockstore/libs/storage/partition_direct.
Описание интерфейса distributed storage
Формат идентификатора блоба
Каждый блоб имеет 192-битный идентификатор, состоящий из следующих полей (в порядке, используемом для сортировки):
- TabletId (64 бита) — идентификатор таблетки-владельца блоба.
- Channel (8 бит) — порядковый номер канала.
- Generation (32 бита) — номер поколения, в котором была запущена таблетка, записавшая данный блоб.
- Step (32 бита) — внутренний номер группы блобов в рамках Generation.
- Cookie (24 бита) — идентификатор, который можно использовать, если Step не хватает.
- CrcMode (2 бита) — выбирает режим для избыточного контроля целостности блоба на уровне distributed storage.
- BlobSize (26 бит) — размер данных блоба.
- PartId (4 бита) — номер фрагмента при erasure-кодировании блоба; на уровне взаимодействия «BlobStorage ↔ таблетка» этот параметр всегда равен 0, что означает целый блоб.
Два блоба считаются различными, если у их идентификаторов отличается хотя бы один из первых пяти параметров (TabletId, Channel, Generation, Step, Cookie). Таким образом, нельзя записать два блоба, которые различаются только BlobSize и/или CrcMode.
Для целей отладки существует строковое представление идентификатора блоба, которое имеет формат [TabletId:Generation:Step:Channel:Cookie:BlobSize:PartId], например, [12345:1:1:0:0:1000:0].
При выполнении записи блоба таблетка выбирает параметры Channel, Step и Cookie. TabletId фиксирован и должен указывать на ту таблетку, которая выполняет запись, а Generation — на поколение, в котором запущена таблетка, выполняющая операцию.
При чтении указывается идентификатор блоба, который может быть произвольным, но желательно ранее записанным.
Группы
Запись блобов производится в логическую сущность, называемую группой. На каждом узле для каждой группы, в которую осуществляется запись, создаётся специальный актор, который называется DS proxy. Этот актор отвечает за выполнение всех операций, связанных с группой. Создание этого актора производится автоматически через сервис NodeWarden.
Физически группа представляет собой набор из нескольких физических устройств (блочные устройства в ОС), которые расположены на разных узлах таким образом, чтобы выход из строя одного устройства как можно меньше коррелировал с выходом из строя другого устройства. Как правило, эти устройства располагаются в разных стойках или в разных дата-центрах. На каждом из этих устройств для группы выделено место, которое управляется специальным сервисом, называемым VDisk. Каждый VDisk работает поверх блочного устройства, от которого он отделён другим сервисом — PDisk. Блобы разбиваются на фрагменты в соответствии со стирающим кодированием, и на VDisk'и записываются строго фрагменты. Перед разбиванием на фрагменты может выполняться опциональное шифрование данных в группе.
Схематично это показано на рисунке ниже.
Разноцветными квадратиками выделены VDisk'и разных групп; один цвет означает одну группу.
Группу можно рассматривать как совокупность VDisk'ов:
Каждый VDisk внутри группы имеет порядковый номер. Диски нумеруются от 0 до N-1, где N — число дисков в группе.
Кроме того, диски в группе объединены в fail domain'ы, а fail domain'ы объединяются в fail realm'ы. Как правило, каждый fail domain имеет ровно один диск внутри (хотя теоретически возможно и больше, но на практике это не используется), а несколько fail realm'ов используется только для групп, которые размещают свои данные сразу в трёх дата-центрах. Так, каждый VDisk получает, помимо порядкового номера в группе, идентификатор, который состоит из индекса fail realm'а, индекса fail domain'а внутри fail realm'а и индекса VDisk'а внутри fail domain'а. Этот идентификатор в строковом виде записывается как VDISK[GroupId:GroupGeneration:FailRealm:FailDomain:VDisk].
Все fail realm'ы имеют одинаковое число fail domain'ов, а все fail domain'ы — одинаковое число дисков внутри. Количество fail realm'ов, количество fail domain'ов внутри fail realm'а и количество дисков внутри fail domain'а образуют геометрию группы. Геометрия зависит от способа кодирования данных в группе. Например, для block-4-2 numFailRealms = 1, число numFailDomainsInFailRealm >= 8 (на практике используется только 8), numVDisksInFailDomain >= 1 (на практике строго 1); для mirror-3-dc numFailRealms >= 3, numFailDomainsInFailRealm >= 3, numVDisksInFailDomain >= 1 (используется 3x3x1).
Каждый PDisk имеет идентификатор, который складывается из номера узла, на котором он запущен, а также внутреннего номера PDisk'а внутри этого узла. Как правило, этот идентификатор записывается в виде NodeId:PDiskId, например, 1:1000. Зная идентификатор PDisk'а, можно вычислить сервисный ActorId этого диска и отправить ему сообщение.
Каждый VDisk запущен поверх определённого PDisk'а и имеет идентификатор слота, который состоит из трёх полей — NodeId:PDiskId:VSlotId, а также идентификатор VDisk'а, о котором было сказано выше. Строго говоря, существуют различные понятия: «слот» — это место, зарезервированное на PDisk'е, которое занимает VDisk, и сам VDisk — это элемент группы, который занимает определённый слот и выполняет над ним операции. По аналогии с PDisk'ами, зная идентификатор слота, можно вычислить сервисный ActorId запущенного VDisk'а и отправить ему сообщение. Для отправки сообщений из DS proxy в VDisk используется промежуточный актор, который называется BS_QUEUE.
Для диагностики распределения чанков, выделенных VDisk, используйте внутренний API, описанный в Отчёт об использовании дискового пространства VDisk.
Состав каждой группы не является фиксированным — он может меняться в процессе работы системы. Для этого вводится понятие «поколения группы». Каждой паре "GroupId:GroupGeneration" соответствует фиксированный набор слотов (вектор, состоящий из N идентификаторов слотов, где N — размер группы), в которых расположены данные всей группы. Не следует путать поколение группы и поколение таблетки — они никак не связаны.
Как правило, группы двух соседних поколений различаются не более чем на один слот.
Подгруппы
Для каждого блоба вводится специальное понятие подгруппы — это упорядоченное подмножество дисков группы, которое имеет строго фиксированное число элементов, зависящее от типа кодирования (число элементов в группе должно быть не меньше), на которых будут храниться данные этого блоба. Для однодатацентровых групп с обычным кодированием подмножество выбирается как первые N элементов циклической перестановки дисков в группе; перестановка зависит от хэша BlobId.
Каждый диск в подгруппе соответствует диску в группе, но ограничен по допустимым хранимым блобам. Например, для кодирования block-4-2 с четырьмя фрагментами данных и двумя фрагментами чётности (data part, parity part) функциональное назначение дисков в подгруппе следующее:
| Номер в подгруппе | Допустимые PartId |
|---|---|
| 0 | 1 |
| 1 | 2 |
| 2 | 3 |
| 3 | 4 |
| 4 | 5 |
| 5 | 6 |
| 6 | 1,2,3,4,5,6 |
| 7 | 1,2,3,4,5,6 |
В данном случае PartId=1..4 соответствует фрагментам данных (которые получаются разрезанием исходного блоба на 4 равные части), а PartId=5..6 — фрагментам чётности. Диски с номерами 6 и 7 в подгруппе называются handoff-дисками. На них могут быть записаны любые фрагменты, включая несколько. На диски 0..5 можно записывать только соответствующие им фрагменты блоба.
На практике при выполнении записи система пытается разместить 6 фрагментов на первых 6 дисках подгруппы, и в подавляющем большинстве случаев это проходит успешно. Однако если один из этих дисков недоступен, то операция записи не может завершиться успешно, и тогда в работу вступают handoff-диски — на них отправляются фрагменты тех дисков, которые не ответили вовремя. Может случиться так, что в результате сложных тормозов и гонок на один handoff-диск уйдёт несколько фрагментов одного блоба. Это допустимо, хотя с точки зрения хранения неэффективно — каждый фрагмент должен иметь свой уникальный диск.
Восстановление избыточности
В случае отказа физического диска YDB может автоматически реконфигурировать связанные с ним группы хранения. При этом не важно, связана ли недоступность диска с выходом из строя сервера целиком или нет. Автоматическая реконфигурация групп хранения снижает вероятность потери данных при возникновении последовательности множественных отказов, при условии, что между отказами проходит достаточно времени для завершения восстановления избыточности. По умолчанию реконфигурация начинается через 1 час после того, как YDB выявляет отказ.
Реконфигурация дисковой группы заменяет VDisk, размещённый на отказавшем оборудовании, новым VDisk, который система старается разместить на работоспособном оборудовании. Действуют те же правила, что и при создании новой группы хранения:
- новый VDisk создаётся в домене отказа, отличающемся от доменов отказа всех остальных VDisk в группе;
- в режиме
mirror-3-dcновый VDisk размещается в той же области отказа, что и отказавший VDisk.
Для того чтобы реконфигурация была возможной, в кластере должны быть свободные слоты для создания VDisk в разных доменах отказа. При расчёте количества оставляемых свободными слотов следует учитывать вероятность отказа оборудования, длительность репликации, а также время, необходимое на замену отказавшего оборудования.
Процесс реконфигурации дисковой группы создаёт повышенную нагрузку на остальные VDisk группы и на сеть. Чтобы уменьшить влияние восстановления избыточности на производительность системы, суммарная скорость репликации данных ограничивается как на стороне VDisk-источника, так и на стороне VDisk-получателя.
Время восстановления избыточности зависит от объёма данных и производительности оборудования. Например, репликация на быстрых NVMe SSD может завершиться за час, а на больших HDD репликация может длиться более суток.
Реконфигурация дисковых групп может быть ограничена или оказаться полностью невозможной, если оборудование кластера состоит из минимально необходимого количества доменов отказа:
- при отказе всего домена реконфигурация теряет смысл, так как новый VDisk может быть размещён только в уже отказавшем домене отказа;
- при отказе части домена реконфигурация возможна, но нагрузка, ранее обрабатываемая отказавшим оборудованием, будет перераспределена только по оставшемуся оборудованию в том же домене отказа.
Если в кластере доступен хотя бы на один домен отказа больше, чем минимально необходимо для создания групп хранения (9 доменов для block-4-2 и по 4 домена в каждой области отказа для mirror-3-dc), то при отказе части оборудования нагрузка может быть перераспределена по всему оставшемуся в строю оборудованию.
Хотите присоединиться к команде разработки YDB?
Ознакомьтесь с разделами о команде YDB и открытых вакансиях, а также о возможностях для студентов.