Дисковая подсистема кластера aka YDB distributed storage

YDB distributed storage — это подсистема YDB, которая отвечает за надёжное хранение данных.

Она позволяет хранить блобы (бинарные фрагменты размером от 1 байта до 10 мегабайт) с уникальным идентификатором.

Описание интерфейса distributed storage

Формат идентификатора блоба

Каждый блоб имеет 192-битный идентификатор, состоящий из следующих полей (в порядке, используемом для сортировки):

  1. TabletId (64 бита) — идентификатор таблетки-владельца блоба.
  2. Channel (8 бит) — порядковый номер канала.
  3. Generation (32 бита) — номер поколения, в котором была запущена таблетка, записавшая данный блоб.
  4. Step (32 бита) — внутренний номер группы блобов в рамках Generation.
  5. Cookie (24 бита) — идентификатор, который можно использовать, если Step не хватает.
  6. CrcMode (2 бита) — выбирает режим для избыточного контроля целостности блоба на уровне distributed storage.
  7. BlobSize (26 бит) — размер данных блоба.
  8. 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'и записываются строго фрагменты. Перед разбиванием на фрагменты может выполняться опциональное шифрование данных в группе.

Схематично это показано на рисунке ниже.

PDisk 1:1000
PDisk 1:1000
PDisk 1:1001
PDisk 1:1001
PDisk 2:1000
PDisk 2:1000
PDisk 2:1001
PDisk 2:1001
PDisk 3:1000
PDisk 3:1000
PDisk 3:1001
PDisk 3:1001
PDisk 4:1000
PDisk 4:1000
PDisk 4:1001
PDisk 4:1001
Node 1
Node 1
Node 2
Node 2
Node 3
Node 3
Node 4
Node 4
DS proxy
DS proxy
Viewer does not support full SVG 1.1

Разноцветными квадратиками выделены VDisk'и разных групп; один цвет означает одну группу.

Группу можно рассматривать как совокупность VDisk'ов:

0
0
1
1
2
2
3
3
4
4
5
5
6
6
7
7
fail realm 0
fail realm 0
fail
domain
0
fail...
fail
domain
1
fail...
fail
domain
2
fail...
fail
domain
3
fail...
fail
domain
4
fail...
fail
domain
5
fail...
fail
domain
6
fail...
fail
domain
7
fail...
VDISK[GroupId:Generation:0:6:0]
VDISK[GroupId:Generation:0:6:0]
0
0
1
1
2
2
3
3
4
4
5
5
6
6
7
7
8
8
fail realm 0
fail realm 0
fail realm 1
fail realm 1
fail realm 2
fail realm 2
fail
domain
0
fail...
fail
domain
1
fail...
fail
domain
2
fail...
VDISK[GroupId:Generation:1:2:0]
VDISK[GroupId:Generation:1...
Viewer does not support full SVG 1.1

Каждый 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-диск уйдёт несколько фрагментов одного блоба. Это допустимо, хотя с точки зрения хранения неэффективно — каждый фрагмент должен иметь свой уникальный диск.