Дисковая подсистема кластера aka YDB distributed storage
YDB distributed storage — это подсистема YDB, которая отвечает за надёжное хранение данных.
Она позволяет хранить блобы (бинарные фрагменты размером от 1 байта до 10 мегабайт) с уникальным идентификатором.
Описание интерфейса 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'ов:
domain
0
domain
1
domain
2
domain
3
domain
4
domain
5
domain
6
domain
7
domain
0
domain
1
domain
2
Каждый 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.
Состав каждой группы не является фиксированным — он может меняться в процессе работы системы. Для этого вводится понятие «поколения группы». Каждой паре "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-диск уйдёт несколько фрагментов одного блоба. Это допустимо, хотя с точки зрения хранения неэффективно — каждый фрагмент должен иметь свой уникальный диск.