Как работает ZFS — часть 2: metaslab

    В первой части я описал как организованы данные на vdev в ZFS. Вторая часть описывает как работает алгоритм выбора собственно места, куда запись будет идти в данный момент.

    Здесь я немного усложню задачу — в первой части был описан только один 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КБ на второй. Поправка: сравнивается свободное место в пуле, и свободное место в vdev'e, так что результат будет немного другим.Здесь, кстати, можно сделать одну очень интересную вещь — несколько месяцев назад я добавил статистику 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) драйвер.
    Поделиться публикацией
    Похожие публикации
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 4
    • +1
      Очень интересно, а продолжение будет?
      • +1
        Будет обязательно, в планах как минимум 8-10 статей про разные компоненты ZFS. Не обещаю каждые 2 дня, но к выходным будет ещё одна скорее всего.
        • 0
          Можете разъяснить ситуацию с перманентно DEGRADED-пулом при замене в нём отказавшего устройства сбойным устройством? Проблема называется «реплейсинг никогда не закончится».

          Типичная ситуация, с которой сталкиваются люди: bochafreebsd.livejournal.com/246029.html

          Ещё похожая ситуация: forum.ixbt.com/topic.cgi?id=11:44215-168#4848
          • 0
            Ага, есть такое дело, сталкивался пару раз. Для (Open)Solaris придумал достаточно некрасивый, но работающий хак:

            echo zfs_no_scrub_io/W0t1 | mdb -kw

            После этого надо дать resilver закончиться (он пройдёт достаточно быстро), и «zpool offline» сбойное устройство.

            Потом поменять параметр назад:

            echo zfs_no_scrub_io/W0t0 | mdb -kw

            и «zpool replace» его работающим диском.

            Принцип работы хака: в треде, где идёт resilver, мы пропускаем собственно часть, которая пишет данные; resilver заканчивается (естественно, на битый диск мы ничего не пишем, так что ошибок на нём не будет, но и данных тоже).

      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.