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

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

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

Описание интерфейса BlobStorage

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

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

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