Как работает ZFS — часть 2: metaslab
В первой части я
описал как организованы данные на vdev в ZFS. Вторая часть описывает как
работает алгоритм выбора собственно места, куда запись будет идти в
данный момент.
Здесь я немного усложню задачу — в первой части был описан только один vdev; здесь их у нас будет несколько, поскольку алгоритм должен выбрать и vdev, куда мы будем писать блок данных, и metaslab внутри vdev'a. В продакшн системе может быть несколько десятков vdev, и правильно распределить данные по ним критично — перебалансировать их мы уже не сможем без копирования всех данных. Цель правильного алгоритма — распараллелить данные так, чтобы на каждом девайсе их было примерно одинаковое количество, выровнять неравномерное заполнение, но и не перегрузить один из девайсов (это будет тормозить запись на весь пул).
Для начала, важное замечание: ZFS рассчитан на то, что все девайсы в пуле имеют одинаковый размер. Иначе, например если добавить 2Тб диск в пул из 1Тб дисков, на 2Тб диске в результате окажется в два раза больше данных, и он начнёт влиять на суммарный IOPs системы — алгоритм аллокатора учитывает процент заполнения, а не количество данных в байтах.
На данный момент в ZFS есть четыре алгоритма аллокатора. Переменная
Пять из этих функций используются во всех алгоритмах; разница собственно только в
FF из них самый простой — он пишет куски данных в первое попавшееся свободное место при трассировке АВЛ-дерева по порядку, и разделяет блоки записи на сегменты, если они не влезают. За счёт этого, FF — очень медленный алгоритм, так как для записи одного блока он должен трассировать всё дерево, пока не наберётся достаточное количество сегментов. Для записи 1ГБ данных, например, worst case — 20 млн сегментов по 512 байт, и очень сильная фрагментация данных как результат. FF используется другими алгоритмами как последний вариант, если они не могут найти место по-другому — например, DF изпользует FF если в данном метаслабе меньше 4% свободного места (
*DF алгоритмы работают по другому — они строят карту свободного места
Например, для всех них работает функция
Итак, в алгоритмы аллокатора данные попадают из ZIO pipeline — оттуда вызываются функции
Туда передаётся:
Далее,
Далее, собственно, идёт самая важная часть кода, в которой логика самого выбора метаслаба для записи:
Сначала, мы берём все группы метаслабов для всех vdev'ов, и проходимся по ним циклом аллокатора (
Ротор работает по кругу пока не кончатся данные, отправленные на запись. Внутри этого цикла, по очереди выбирается оптимальный vdev, потом в нём, используя
Например, если нам надо записать 1МБ данных на массив из двух дисков, один из которых заполнен на 20%, а второй — на 80%, мы запишем 819КБ на первый, и 205КБ на второй. Здесь, кстати, можно сделать одну очень интересную вещь — несколько месяцев назад я добавил статистику latency для каждого vdeva в ZFS (она находится в
Наконец, итерация за итерацией, цикл отправляет все данные на запись в metaslab group, и заканчивается для этой transaction group (txg), a в группе метаслабов работает система
Здесь я немного усложню задачу — в первой части был описан только один vdev; здесь их у нас будет несколько, поскольку алгоритм должен выбрать и vdev, куда мы будем писать блок данных, и metaslab внутри vdev'a. В продакшн системе может быть несколько десятков vdev, и правильно распределить данные по ним критично — перебалансировать их мы уже не сможем без копирования всех данных. Цель правильного алгоритма — распараллелить данные так, чтобы на каждом девайсе их было примерно одинаковое количество, выровнять неравномерное заполнение, но и не перегрузить один из девайсов (это будет тормозить запись на весь пул).
NAME STATE READ WRITE CKSUM
tank ONLINE 0 0 0
c1t6d0 ONLINE 0 0 0
c1t5d0 ONLINE 0 0 0
Для начала, важное замечание: ZFS рассчитан на то, что все девайсы в пуле имеют одинаковый размер. Иначе, например если добавить 2Тб диск в пул из 1Тб дисков, на 2Тб диске в результате окажется в два раза больше данных, и он начнёт влиять на суммарный IOPs системы — алгоритм аллокатора учитывает процент заполнения, а не количество данных в байтах.
На данный момент в ZFS есть четыре алгоритма аллокатора. Переменная
zfs_metaslab_ops
содержит поинтер на структуру space_map_ops_t
,
в которой есть поинтеры на семь функций, которые использует каждый
конкретный алгоритм. Например, в Illumos используется алгоритм metaslab_df
, и соответствующий стракт с поинтерами на функции выглядит вот так: static space_map_ops_t metaslab_df_ops = {
metaslab_pp_load,
metaslab_pp_unload,
metaslab_df_alloc,
metaslab_pp_claim,
metaslab_pp_free,
metaslab_pp_maxsize,
metaslab_df_fragmented
};
Пять из этих функций используются во всех алгоритмах; разница собственно только в
metaslab_*_alloc()
и metaslab_*_fragmented()
— сам аллокатор, и функция, которая решает насколько фрагментировано
свободное место в конкретном metaslab. Аллокаторы, которые можно
использовать: DF (Dynamic-Fit), FF (First-Fit), и два экспериментальных,
CDF и NDF — что они означают, не знает никто. FF из них самый простой — он пишет куски данных в первое попавшееся свободное место при трассировке АВЛ-дерева по порядку, и разделяет блоки записи на сегменты, если они не влезают. За счёт этого, FF — очень медленный алгоритм, так как для записи одного блока он должен трассировать всё дерево, пока не наберётся достаточное количество сегментов. Для записи 1ГБ данных, например, worst case — 20 млн сегментов по 512 байт, и очень сильная фрагментация данных как результат. FF используется другими алгоритмами как последний вариант, если они не могут найти место по-другому — например, DF изпользует FF если в данном метаслабе меньше 4% свободного места (
int metaslab_df_free_pct = 4;
). Единственный плюс FF в том, что он единственный может заполнить фрагментированный метаслаб на 100%.*DF алгоритмы работают по другому — они строят карту свободного места
freemap
в
том метаслабе, в который на данный момент идёт запись, сортируют ее по
размеру и/или близости кусков непрерывного свободного места, и пытаются
выбрать наиболее оптимальный вариант размещения данных с точки зрения
скорости записи, количества движений головки диска, и минимальной
фрагментации записываемых данных. Например, для всех них работает функция
metaslab_weight()
,
которая даёт небольшой приоритет метаслабам, которые находятся на
внешних регионах пластины диска (для short-stroke эффекта). Если
использовать только SSD, то имеет смысл тюнить ZFS, отключая эту часть
алгоритма, потому-что к SSD short-stroking не применим.Итак, в алгоритмы аллокатора данные попадают из ZIO pipeline — оттуда вызываются функции
metaslab_alloc()
(сам аллокатор для записи) и metaslab_free()
(освобождаем место, собираем мусор).metaslab_alloc(spa_t *spa, metaslab_class_t *mc, uint64_t psize, blkptr_t *bp,
int ndvas, uint64_t txg, blkptr_t *hintbp, int flags)
Туда передаётся:
*spa
— поинтер на структуру самого массива данных (zpool); *mc — класс метаслабов, в котором в том числе есть поинтер на zfs_metaslab_ops
; psize
— размер данных; *bp
— поинтер на сам блок; ndvas
— количество независимых копий данных, которое требуется для данного
блока (1 для данных; 2 для большинства метаданных; 3 в некоторых случаях
для метаданных, которые находятся высоко в АВЛ-дереве. Смысл в
дупликации метаданных в том, что если единственный блок с метаданными
для сегмента дерева утерян, мы теряем всё, что находится под ним. Такие
блоки называются ditto blocks, и алгоритм старается писать их на разные
vdevы).Далее,
txg
— порядковый номер группы транзакций, которую мы пишем; *hintbp
— подсказка, используемая для того, чтобы блоки которые рядом логически были также рядом на диске, и шли на тот же vdev; flags
— 5 бит, которые позволяют аллокатору узнать нужно ли использовать
какие-либо специфические варианты аллокации — использовать или
игнорировать подсказку *hintbp
, и использовать ли ganging
(просьба писать группу child блоков на тот же vdev, что и их header, для
более эффективной работы ZFS prefetch и vdev cache).define METASLAB_HINTBP_FAVOR 0x0
define METASLAB_HINTBP_AVOID 0x1
define METASLAB_GANG_HEADER 0x2
define METASLAB_GANG_CHILD 0x4
define METASLAB_GANG_AVOID 0x8
/*
* Allow allocations to switch to gang blocks quickly. We do this to
* avoid having to load lots of space_maps in a given txg. There are,
* however, some cases where we want to avoid "fast" ganging and instead
* we want to do an exhaustive search of all metaslabs on this device.
* Currently we don't allow any gang, zil, or dump device related allocations
* to "fast" gang.
*/
#define CAN_FASTGANG(flags) \
(!((flags) & (METASLAB_GANG_CHILD | METASLAB_GANG_HEADER | \
METASLAB_GANG_AVOID)))
/*
* If we are doing gang blocks (hintdva is non-NULL), try to keep
* ourselves on the same vdev as our gang block header. That
* way, we can hope for locality in vdev_cache, plus it makes our
* fault domains something tractable.
*/
Далее, собственно, идёт самая важная часть кода, в которой логика самого выбора метаслаба для записи:
metaslab_alloc_dva()
. В функции почти 200 строк хитрого кода, который я попытаюсь объяснить.Сначала, мы берём все группы метаслабов для всех vdev'ов, и проходимся по ним циклом аллокатора (
mg_rotor
),
используя подсказки, если они есть. Мы пропускаем vdev'ы, на которые
запись в данный момент нежелательна, например те, в которых умер один из
дисков, или идёт восстановление raidz-группы. (Don't allocate from
faulted devices.) Мы также пропускаем диски, на которых были какие-то
ошибки записи, для данных у которых на дисках будет только одна копия.
(Avoid writing single-copy data to a failing vdev.) Ротор работает по кругу пока не кончатся данные, отправленные на запись. Внутри этого цикла, по очереди выбирается оптимальный vdev, потом в нём, используя
metaslab_group_alloc()
, выбирается лучший
метаслаб, потом решаем сколько данных записать в этот метаслаб,
сравнивая процент использования vdev'a с другими. Эта часть кода очень
критична, поэтому привожу ее полностью: offset = metaslab_group_alloc(mg, psize, asize, txg, distance,
dva, d, flags);
if (offset != -1ULL) {
/*
* If we've just selected this metaslab group,
* figure out whether the corresponding vdev is
* over- or under-used relative to the pool,
* and set an allocation bias to even it out.
*/
if (mc->mc_aliquot == 0) {
vdev_stat_t *vs = &vd->vdev_stat;
int64_t vu, cu;
vu = (vs->vs_alloc * 100) / (vs->vs_space + 1);
cu = (mc->mc_alloc * 100) / (mc->mc_space + 1);
/*
* Calculate how much more or less we should
* try to allocate from this device during
* this iteration around the rotor.
* For example, if a device is 80% full
* and the pool is 20% full then we should
* reduce allocations by 60% on this device.
*
* mg_bias = (20 - 80) * 512K / 100 = -307K
*
* This reduces allocations by 307K for this
* iteration.
*/
mg->mg_bias = ((cu - vu) *
(int64_t)mg->mg_aliquot) / 100;
}
Например, если нам надо записать 1МБ данных на массив из двух дисков, один из которых заполнен на 20%, а второй — на 80%, мы запишем 819КБ на первый, и 205КБ на второй. Здесь, кстати, можно сделать одну очень интересную вещь — несколько месяцев назад я добавил статистику latency для каждого vdeva в ZFS (она находится в
vdev_stat_t->vs_latency[]
в NexentaStor; в Illumos пока не добавили), и её можно использовать в
качестве одного из факторов при записи новых данных, либо учитывая и её и
свободное место в какой-либо пропорции, либо используя только её. Такой
изменённый алгоритм я тоже написал, но он пока не используется в
продакшн-системах. Он имеет смысл когда либо в массиве есть диски
разного типа и скорости, либо когда один из дисков начинает умирать
(тормозить), но пока не настолько плох, и на нём нет ошибок.Наконец, итерация за итерацией, цикл отправляет все данные на запись в metaslab group, и заканчивается для этой transaction group (txg), a в группе метаслабов работает система
metaslab_weight()
(см. начало статьи), и через систему space map
,
учитывая maxfree (максимальный кусок непрерывного свободного места), с
помощью трассировки АВЛ-деревьев и соответствующего алгоритма (DF, FF,
CDF, NDF) распихивает данные оптимальным для алгоритма образом, после
чего мы наконец получаем физический адрес блока, в который мы будем
писать на диск, и данные идут в очередь на запись в sd
(Scsi Device) драйвер.