Как заменить диск в BTRFS-рейде

Допустим встала задача заменить диск в рейде BTRFS. Замена может быть плановой, когда диск начал активно ругаться через SMART, или аварийной, когда диск уже умер. Рассмотрим случай, когда у нас нет свободных портов для подключения еще одного диска к существующим, и даже для плановой замены нам потребуется сначала извлечь заменяемый диск. Тогда оба сценария у нас будут максимально похожи, и будут сопряжены с выключением сервера.

Итак.

  1. Если замена плановая (если сервер уже упал переходим к п.2), то лучше сразу в /etc/fstab добавить опцию монтирования degraded ко всем точкам монтирования, которые будут затронуты при изъятии диска.
     
     
    1. UUID="b8022e27-2df2-404d-a404-1ca03f9c90e9" / btrfs defaults,degraded,subvol=@rootfs,noatime,compress-force=zstd 0 0
    2. UUID="b8022e27-2df2-404d-a404-1ca03f9c90e9" /var/log btrfs defaults,degraded,subvol=@var-log,compress-force=zstd 0 0
    3. UUID="ea4980c5-dee0-49e0-960c-3975a70f77eb" /share btrfs defaults,degraded,noatime,nofail 0 0

    В данном примере физические диски разбиты на разделы, и из разделов составлены RAID1 для «/» и «/var/log» (два сабволюма на общей ФС) и RAID5 для /share (вторая независимая ФС). Таким образом при изъятии диска оба рейда деградируют, и для обоих нужно добавить данную опцию монтирования.

  2. Выключаем сервер и меняем диск на новый
  3. При загрузке нажимаем «e» чтобы отредактировать конфигурацию загрузчика. Находим строчку «linux….» как на скриншоте ниже.
    * Если рутовый раздел монтируется из сабволюма, т.е. как в п.1 в fstab в параметрах монтирования указана опция «subvol=» с именем сабволюма, то и в строке загрузчика  уже будет опция «rootflags=…» в которой эта опция будет повторена — как на скриншоте. В этом случае просто дописываем «,degraded».
    * В том случае, если рутовый раздел монтируется из корня самой файловой системы (в п.1 так монтируется /share — параметр «subvol=» при этом  отсутствует), то скорее всего в загрузчике в принципе будет отсутствовать опция «rootflags» и ее нужно будет добавить в виде «rootflags=degraded»
  4. Нажимаем F10 и загружаемся с указанными опциями загрузчика.
  5. Если мы на первом шаге не исправили /etc/fstab, то наш сервер не загрузится и предложит перейти в maintenance mode. Делаем это, после чего правим  /etc/fstab как в п.1. После этого монтируем разделы командой «mount -a».
    Если все прошло удачно и разделы смонтировались — я рекомендовал бы перезагрузить сервер, снова повторить п.п. 3-4, и тем самым добиться полноценной загрузки сервера на деградированных рейдах. Таким образом дальнейшие шаги по пересборке рейда, которые могут быть довольно протяженными по времени, будут проходить при работающем сервере. С другой стороны если вам важны данные, вы хотите снизить нагрузку на диски, и вам не так важно чтобы сервер продолжал быть в работе, можете перейти к следующему пункту сразу после успешного монтирования ваших разделов.
  6. Выполните команду lsblk и убедитесь что все ваши диски видны (на скриншоте диски sda, sdb и sdc — рабочие, sdd — новый, не размеченный диск того же размера)
  7. Копируем таблицу разделов с одного из рабочих дисков на новый
     
     
    1. sfdisk -d /dev/sda | sfdisk /dev/sdd
  8. Дальше нужно посмотреть конфигурацию btrfs и выяснить какой диск, с ее точки зрения, у нас отсутствует. Выполняем команду
     
     
    1. btrfs fi sh

    В данном случае отсутствует диск 3, а сам рейд составлен из разделов sda2, sdb2 и sdc2

  9. Производим замену: вместо пропавшего диска 3 подставляем раздел нового диска — sdd2
     
     
    1. btrfs replace start 3 /dev/sdd2 /
  10. Для отслеживания прогресса выполняем команду
     
     
    1. btrfs replace status /

    Также можно отметить, что до окончания операции замены в рейде будут видны оба диска — missing и новый

     
     
    1. btrfs fi sh /

  11. После завершения замены обязательно производим мягкий ребаланс, так как часть данных на текущий момент скорее всего не будет соответствовать уровню рейда (иными словами не будет задублирована и защищена избыточностью). Для этого сначала смотрим какие профили используются для данных и для метаданных:
     
     
    1. btrfs fi us /


    После чего выполняем команду:

     
     
    1. btrfs balance start -dconvert=RAID1C4,soft -mconvert=RAID1C4,soft /

    dconvert — профиль для данных (Data)
    mconvert — профиль для медатанных (Metadata)

  12. Повторяем п.п. 8-11 для всех оставшихся отдельных файловых систем (в данном примере: для ФС, монтируемой в /share, и расположенной на разделах sda3, sdb3 и sdc3).
  13. Редактируем /etc/fstab, удаляя добавленные ранее опции dagraded.

После этого останется восстановить загрузчик на новом диске, если он там был, и перезагрузить сервер, чтобы убедиться что все работает как надо без дополнительных опций.

SAMBA: Неправильно отображается информация о размерах папки лежащей на BTRFS

Типичной ситуацией является некорректное отображение общего размера сетевой шары, если она лежит на BTRFS-рейде — вместо действительного размера тома показывается сумма размеров всех дисков в рейде. Для исправления этой ситуации можно воспользоваться опцией «dfree command», которая позволяет указать кастомный исполняемый файл, которые будет подсчитывать и возвращать данные о размерах диска.

Данные опции нужно добавить в конфигурацию шары:

/etc/samba/smb.conf
 
[Public]
    ....
    dfree command = /etc/samba/btrfs-dfree.py
    dfree cache time = 60
    force user = root
    force group = root
    ....

Здесь стоит обратить внимание на опцию «force user = root», которая с одной стороны принудительно сделает рута владельцем новых файлов и папок, что может быть вам не нужно, но также позволит выполняться скрипту с рутовыми правами — это необходимо для получения таких данных из вывода «btrfs filesystem usage» как «ratio» — коэффициент данных на рейде, учитывающий сколько копий данных фактически на рейде расположено, и «free (estimated) min» —  более точное значение свободного места.

Если в ваши планы не входит менять юзера с дефолтного, то свободное место будет браться скриптом из «Free (statfs, df)», а для показа точного общего размера сетевой шары можно вручную в скрипте указать значение «ratio», присвоив его переменной «CUSTOMRATIO».

Итак, создаем скрипт «/etc/samba/btrfs-dfree.py» (спасибо проекту openmediavault, я использовал их скрипт «omv-btrfs-dfree» как основу), не забываем сделать его исполняемым. Скрипт на гитхабе

/etc/samba/btrfs-dfree.py
 
#!/usr/bin/env python3
import os
import re
import subprocess
import sys
from systemd import journal
CUSTOMRATIO = 0 # set ratio of your btrfs volume if samba user is not root (smb.conf: force user = root)
LOGGING = 0 # 0 - off, 1 - on. journalctl -t 'SMBUS'
def main():
    path = os.path.realpath(sys.argv[1] if len(sys.argv) > 0 else '.')
    output = subprocess.run(
        ['btrfs', 'filesystem', 'usage', '--raw', '-T', path],
        capture_output=True
    ).stdout.decode()
    re_matches_for_total = r'.+Device size:\s+(\d+)\n.+'
    matches_for_total = re.match(
        re_matches_for_total,
        output, re.DOTALL
    )
    if os.getuid() == 0: # if samba user is root we can get ratio to calculate total space
        re_matches_for_ratio = r'.+Data ratio:\s+(\d+\.\d+).+'
        matches_for_ratio = re.match(
            re_matches_for_ratio,
            output, re.DOTALL
        )
        ratio = float(matches_for_ratio.group(1))
    else: # else set ratio = 1 but can get wrong total size
        if CUSTOMRATIO == 0:
            ratio = 1
        else:
            ratio = CUSTOMRATIO
    if os.getuid() == 0: # if user is root we can get estimated (minimum) free space
        re_matches_for_available = r'.+Free \(estimated\):\s+\d+\s+\(min: (\d+)\)\n.+'
    else: # otherwise only 'statfs' free
        re_matches_for_available = r'.+Free \(statfs, df\):\s+(\d+)\n.+'
    matches_for_available = re.match(
        re_matches_for_available,
        output, re.DOTALL
    )
    if matches_for_total is not None:
        totalb = matches_for_total.group(1)
        availableb = matches_for_available.group(1)
        if LOGGING == 1:
            journal.stream('SMBUS').write(f'SMB VARS: userid={os.getuid()}, ratio={ratio}, availableb={availableb}, totalb={totalb}')
            for line in output.splitlines():
                journal.stream('SMBUS').write(f'{line}')
        total =     round(int(totalb) // (1024 * ratio))
        available = round(int(availableb) // 1024)
    else:
        output = subprocess.run(
            ['df', '--portability', '--print-type', '--block-size=1K', path],
            capture_output=True
        ).stdout.decode()
        _, _, total, _, available, *_ = output.split('\n')[1].split()
    sys.stdout.write(f'{total} {available} 1024\n')
if __name__ == "__main__":
    sys.exit(main())

Проверить работу скрипта можно выполнив его вручную, указав в качестве аргумента путь к папке на btrfs: