Создание надёжного iSCSI-хранилища на Linux, часть 1

в 19:31, , рубрики: drbd, fault, iscsi, linux, scst, target, tolerance, VMware, vSphere, виртуализация, системное администрирование, метки: , , , , , , , ,

Прелюдия

Сегодня я расскажу вам как я создавал бюджетное отказоустойчивое iSCSI хранилище из двух серверов на базе Linux для обслуживания нужд кластера VMWare vSphere. Были похожие статьи (например), но мой подход несколько отличается, да и решения (тот же heartbeat и iscsitarget), используемые там, уже устарели.

Статья предназначена для достаточно опытных администраторов, не боящихся фразы «патчить и компилировать ядро», хотя какие-то части можно было упростить и обойтись вовсе без компиляции, но я напишу как делал сам. Некоторые простые вещи я буду пропускать, чтобы не раздувать материал. Цель этой статьи скорее показать общие принципы, а не расписать всё по шагам.

Вводные

Требования у меня были простые: создать кластер для работы виртуальных машин, не имеющий единой точки отказа. А в качестве бонуса — хранилище должно было уметь шифровать данные, чтобы враги, утащив сервер, до них не добрались.

В качестве гипервизора был выбран vSphere, как наиболее устоявшийся и законченый продукт, а в качестве протокола — iSCSI, как не требующий дополнительных финансовых вливаний в виде коммутаторов FC или FCoE. С опенсурсными SAS таргетами довольно туго, если не сказать хуже, так что этот вариант тоже был отвергнут.

Осталось хранилище. Разные брендовые решения от ведущих вендоров были отброшены по причине большой стоимости как их самих по себе, так и лицензий на синхронную репликацию. Значит будем делать сами, заодно и поучимся.

В качестве софта было выбрано:

  • Debian Wheezy + LTS ядро 3.10
  • iSCSI-таргет SCST
  • DRBD для репликации
  • Pacemaker для управления ресурсами кластера и мониторинга
  • Подсистема ядра DM-Crypt для шифрования (инструкции AES-NI в процессоре нам очень помогут)

В итоге, в недолгих муках была рождена такая несложная схема:
image
На ней видно, что каждый из серверов имеет по 10 гигабитных интерфейсов (2 встроенных и по 4 на дополнительных сетевых картах). 6 из них подключены к стеку коммутаторов (по 3 к каждому), а остальные 4 — к серверу-соседу.
По ним то и пойдёт репликация через DRBD. Карты репликации при желании можно заменить на 10-Гбит, но у меня были под рукой эти, так что «я его слепила из того, что было».

Таким образом, скоропостижная гибель любой из карт не приведёт к полной неработоспособности какой-либо из подсистем.

Так как основная задача этих хранилищ — надежное хранение больших данных (файл-сервера, архивы почты и т.п.), то были выбраны сервера с 3.5" дисками:

За дело

Диски

Я создал на каждом из серверов по два RAID10 массива из 8 дисков.
От RAID6 решил отказаться т.к. места хватало и так, а производительность у RAID10 на задачах случайного доступа выше. Плюс, ниже время ребилда и нагрузка при этом идёт только на один диск, а не на весь массив сразу.

В общем, тут каждый решает для себя.

Сетевая часть

С протоколом iSCSI бессмысленно использовать Bonding/Etherchannel для ускорения его работы.
Причина проста — при этом используются хэш-функции для распределения пакетов по каналам, поэтому очень сложно подобрать такие IP/MAC адреса, чтобы пакет от адреса IP1 до IP2 шел по однму каналу, а от IP1 до IP3 — по другому.
На cisco даже есть команда, которая позволяет посмотреть в какой из интерфейсов Etherchannel-а улетит пакет:

# test etherchannel load-balance interface port-channel 1 ip 10.1.1.1 10.1.1.2
Would select Gi2/1/4 of Po1

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

На коммутаторе я создал 6 VLAN-ов (по одному на каждый внешний интерфейс сервера):

stack-3750x# sh vlan | i iSCSI
24   iSCSI_VLAN_1                     active
25   iSCSI_VLAN_2                     active
26   iSCSI_VLAN_3                     active
27   iSCSI_VLAN_4                     active
28   iSCSI_VLAN_5                     active
29   iSCSI_VLAN_6                     active

Интерфейсы были сделаны транковыми для универсальности и еще кое-чего, будет видно дальше:

interface GigabitEthernet1/0/11
 description VMSTOR1-1
 switchport trunk encapsulation dot1q
 switchport mode trunk
 switchport nonegotiate
 flowcontrol receive desired
 spanning-tree portfast trunk
end

MTU на коммутаторе следует выставить в максимум, чтобы снизить нагрузку на сервера (больше пакет -> меньше количество пакетов в секунду -> меньше генерируется прерываний). В моем случае это 9198:

(config)# system mtu jumbo 9198

ESXi не поддерживает MTU больше 9000, так что тут есть еще некоторый запас.

Каждому VLAN-у было выбрано адресное пространство, для простоты имеющее такой вид: 10.1.VLAN_ID.0/24 (например — 10.1.24.0/24). При нехватке адресов можно уложиться и в меньших подсетях, но так удобнее.

Каждый LUN будет представлен отдельным iSCSI-таргетом, поэтому каждому таргету были выбраны «общие» кластерные адреса, которые будут подниматься на ноде, обслуживающей этот таргет в данный момент: 10.1.VLAN_ID.10 и 10.1.VLAN_ID.20

Также у серверов будут постоянные адреса для управления, в моем случае это 10.1.0.10/24 и .20 (в отдельном VLAN-е)

Софт

Итак, тут мы устанавливаем на оба сервера Debian в минимальном виде, на этом останавливаться подробно не буду.

Сборка пакетов

Сборку я проводил на отдельной виртуалке, чтобы не захламлять сервера компиляторами и исходниками.
Для сборки ядра под Debian достаточно поставить мета-пакет build-essential и, возможно, еще что-то по мелочи, точно уже не помню.

Качаем последнее ядро 3.10 с kernel.org: и распаковываем:

# cd /usr/src/
# wget https://www.kernel.org/pub/linux/kernel/v3.x/linux-3.10.27.tar.xz
# tar xJf linux-3.10.27.tar.xz

Далее скачиваем через SVN последнюю ревизию стабильной ветки SCST, генерируем патч для нашей версии ядра и применяем его:

# svn checkout svn://svn.code.sf.net/p/scst/svn/trunk scst-svn
# cd scst-svn
# scripts/generate-kernel-patch 3.10.27 > ../scst.patch
# cd linux-3.10.27
# patch -Np1 -i ../scst.patch

Теперь соберем демон iscsi-scstd:

# cd scst-svn/iscsi-scst/usr
# make

Получившийся iscsi-scstd нужно будет положить на сервера, к примеру в /opt/scst

Далее конфигурируем ядро под свой сервер.
Включаем шифрование (если нужно).

Не забываем включить вот эти опции для SCST и DRBD:

CONFIG_CONNECTOR=y
CONFIG_SCST=y
CONFIG_SCST_DISK=y
CONFIG_SCST_VDISK=y
CONFIG_SCST_ISCSI=y
CONFIG_SCST_LOCAL=y

Собираем его в виде .deb пакета (для этого нужно установить пакеты fakeroot, kernel-package и заодно debhelper):

# fakeroot make-kpkg clean prepare
# fakeroot make-kpkg --us --uc --stem=kernel-scst --revision=1 kernel_image

На выходе получаем пакет kernel-scst-image-3.10.27_1_amd64.deb

Далее собираем пакет для DRBD:

# wget http://oss.linbit.com/drbd/8.4/drbd-8.4.4.tar.gz
# tar xzf drbd-8.4.4.tar.gz
# dh_make --native --single
После вопроса жмакаем Enter

Изменяем файл debian/rules до следующего состояния (там есть стандартный файл, но он не собирает модули ядра):

#!/usr/bin/make -f

# Путь до исходников ядра
export KDIR="/usr/src/linux-3.10.27"

override_dh_installdocs:
<тут два таба, без них не будет работать>

override_dh_installchangelogs:
<и тут тоже>

override_dh_auto_configure:
        ./configure 
        --prefix=/usr 
        --localstatedir=/var 
        --sysconfdir=/etc 
        --with-pacemaker 
        --with-utils 
        --with-km 
        --with-udev 
        --with-distro=debian 
        --without-xen 
        --without-heartbeat 
        --without-legacy_utils 
        --without-rgmanager 
        --without-bashcompletion

%:
        dh $@

В файле Makefile.in подправим переменную SUBDIRS, уберем из нее documentation, иначе пакет не соберётся с руганью на документацию.

Собираем:

# dpkg-buildpackage -us -uc -b

Получаем пакет drbd_8.4.4_amd64.deb

Всё, собирать больше вроде ничего не надо, копируем оба пакета на сервера и устанавливаем:

# dpkg -i kernel-scst-image-3.10.27_1_amd64.deb
# dpkg -i drbd_8.4.4_amd64.deb
Настройка серверов

Сеть

Интерфейсы у меня были переименованы в /etc/udev/rules.d/70-persistent-net.rules следующим образом:
int1-6 идут к свичу, а drbd1-4 — к соседнему серверу.

/etc/network/interfaces имеет крайне устрашающий вид, который и в кошмарном сне не приснится:

auto lo
iface lo inet loopback

# Interfaces
auto int1
iface int1 inet manual
    up ip link set int1 mtu 9000 up
    down ip link set int1 down

auto int2
iface int2 inet manual
    up ip link set int2 mtu 9000 up
    down ip link set int2 down

auto int3
iface int3 inet manual
    up ip link set int3 mtu 9000 up
    down ip link set int3 down

auto int4
iface int4 inet manual
    up ip link set int4 mtu 9000 up
    down ip link set int4 down

auto int5
iface int5 inet manual
    up ip link set int5 mtu 9000 up
    down ip link set int5 down

auto int6
iface int6 inet manual
    up ip link set int6 mtu 9000 up
    down ip link set int6 down

# Management interface
auto int1.2
iface int1.2 inet manual
    up ip link set int1.2 mtu 1500 up
    down ip link set int1.2 down
    vlan_raw_device int1

auto int2.2
iface int2.2 inet manual
    up ip link set int2.2 mtu 1500 up
    down ip link set int2.2 down
    vlan_raw_device int2

auto int3.2
iface int3.2 inet manual
    up ip link set int3.2 mtu 1500 up
    down ip link set int3.2 down
    vlan_raw_device int3

auto int4.2
iface int4.2 inet manual
    up ip link set int4.2 mtu 1500 up
    down ip link set int4.2 down
    vlan_raw_device int4

auto int5.2
iface int5.2 inet manual
    up ip link set int5.2 mtu 1500 up
    down ip link set int5.2 down
    vlan_raw_device int5

auto int6.2
iface int6.2 inet manual
    up ip link set int6.2 mtu 1500 up
    down ip link set int6.2 down
    vlan_raw_device int6

auto bond_vlan2
iface bond_vlan2 inet static
    address 10.1.0.100
    netmask 255.255.255.0
    gateway 10.1.0.1
    slaves int1.2 int2.2 int3.2 int4.2 int5.2 int6.2
    bond-mode active-backup
    bond-primary int1.2
    bond-miimon 100
    bond-downdelay 200
    bond-updelay 200
    mtu 1500

# iSCSI
auto int1.24
iface int1.24 inet manual
    up ip link set int1.24 mtu 9000 up
    down ip link set int1.24 down
    vlan_raw_device int1

auto int2.25
iface int2.25 inet manual
    up ip link set int2.25 mtu 9000 up
    down ip link set int2.25 down
    vlan_raw_device int2

auto int3.26
iface int3.26 inet manual
    up ip link set int3.26 mtu 9000 up
    down ip link set int3.26 down
    vlan_raw_device int3

auto int4.27
iface int4.27 inet manual
    up ip link set int4.27 mtu 9000 up
    down ip link set int4.27 down
    vlan_raw_device int4

auto int5.28
iface int5.28 inet manual
    up ip link set int5.28 mtu 9000 up
    down ip link set int5.28 down
    vlan_raw_device int5

auto int6.29
iface int6.29 inet manual
    up ip link set int6.29 mtu 9000 up
    down ip link set int6.29 down
    vlan_raw_device int6

# DRBD bonding
auto bond_drbd
iface bond_drbd inet static
    address 192.168.123.100
    netmask 255.255.255.0
    slaves drbd1 drbd2 drbd3 drbd4
    bond-mode balance-rr
    mtu 9216

Так как мы хотим иметь отказоустойчивость и по управлению сервером, то применяем военную хитрость: в bonding в режиме active-backup собираем не сами интерфейсы, а VLAN-субинтерфейсы. Тем самым сервер будет доступен до тех пор, пока работает хотя бы один интерфейс. Это избыточно, но пуркуа бы не па. И при этом те же интерфейсы могут свободно использоваться для iSCSI траффика.

Для репликации создан интерфейс bond_drbd в режиме balance-rr, в котором пакеты отправляются тупо последовательно по всем интерфейсам. Ему назначен адрес из серой сети /24, но можно было бы обойтись и /30 или /31 т.к. хоста-то будет всего два.

Так как это иногда приводит к приходу пакетов вне очереди, увеличиваем буффер внеочередных пакетов в /etc/sysctl.conf. Ниже приведу весь файл, что какие опции делают пояснять не буду, очень долго. Можно самостоятельно почитать при желании.

net.ipv4.tcp_reordering = 127

net.core.rmem_max = 33554432
net.core.wmem_max = 33554432
net.core.rmem_default = 16777216
net.core.wmem_default = 16777216

net.ipv4.tcp_rmem = 131072 524288 33554432
net.ipv4.tcp_wmem = 131072 524288 33554432
net.ipv4.tcp_no_metrics_save = 1
net.ipv4.tcp_window_scaling = 1
net.ipv4.tcp_timestamps = 0
net.ipv4.tcp_sack = 0
net.ipv4.tcp_dsack = 0
net.ipv4.tcp_fin_timeout = 15

net.core.netdev_max_backlog = 300000

vm.min_free_kbytes = 720896

По результатам тестов интерфейс репликации выдает где-то 3.7 Гбит/сек, что вполне приемлимо.

Так как сервер у нас многоядерный, а сетевые карты и RAID-контроллер умеют разделять обработку прерываний по нескольким очередям, то был написан скрипт, который привязывает прерывания к ядрам:

#!/usr/bin/perl -w

use strict;
use warnings;

my $irq = 77;
my $ifs = 11;
my $queues = 6;
my $skip = 1;
my @tmpl = ("0", "0", "0", "0", "0", "0");

print "Applying IRQ affinity...n";

for(my $if = 0; $if < $ifs; $if++) {
    for(my $q = 0; $q < $queues; $q++, $irq++) {
        my @tmp = @tmpl;
        $tmp[$q] = 1;
        my $mask = join("", @tmp);

        my $hexmask = bin2hex($mask);
        #print $irq . " -> " . $hexmask . "n";

        open(OUT, ">/proc/irq/".$irq."/smp_affinity");
        print OUT $hexmask."n";
        close(OUT);
    }

    $irq += $skip;
}

sub bin2hex {
    my ($bin) = @_;
    return sprintf('%x', oct("0b".scalar(reverse($bin))));
}

Диски

Перед экспортом дисков мы их зашифруем и забекапим мастер-ключи на всякий пожарный:

# cryptsetup luksFormat --cipher aes-cbc-essiv:sha256 --hash sha256 /dev/sdb
# cryptsetup luksFormat --cipher aes-cbc-essiv:sha256 --hash sha256 /dev/sdc
# cryptsetup luksHeaderBackup /dev/sdb  --header-backup-file /root/header_sdb.bin
# cryptsetup luksHeaderBackup /dev/sdc  --header-backup-file /root/header_sdc.bin

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

Далее был написан скрипт для упрощения дешифровки:

#!/usr/bin/perl -w

use strict;
use warnings;

use IO::Prompt;

my %crypto_map = (
    '1bd1f798-d105-4150-841b-f2751f419efc' => 'VM_STORAGE_1',
    'd7fcdb2b-88fd-4d19-89f3-5bdf3ddcc456' => 'VM_STORAGE_2'
);

my $i = 0;
my $passwd = prompt('Password: ', '-echo' => '*');

foreach my $dev (sort(keys(%crypto_map))) {
    $i++;

    if(-e '/dev/mapper/'.$crypto_map{$dev}) {
        print "Mapping '".$crypto_map{$dev}."' already exists, skippingn";
        next;
    }

    my $ret = system('echo "'.$passwd.'" | /usr/sbin/cryptsetup luksOpen /dev/disk/by-uuid/'.$dev.' '.$crypto_map{$dev});

    if($ret == 0) {
        print $i . ' Crypto mapping '.$dev.' => '.$crypto_map{$dev}.' added successfully' . "n";
    } else {
        print $i . ' Failed to add mapping '.$dev.' => '.$crypto_map{$dev} . "n";
        exit(1);
    }
}

Скрипт работает с UUID-ами дисков, чтобы всегда однозначно идентифицировать диск в системе без привязки к /dev/sd*.

Скорость шифрования зависит от частоты процессора и количества ядер, причем запись распараллеливается лучше чем чтение. Проверить с какой скоростью шифрует сервер можно следующим нехитрым способом:

Создаем виртуальный диск, запись на который будет уходить в никуда, а чтение выдавать нули
# echo "0 268435456 zero" | dmsetup create zerodisk

Создаем на его основе шифрованный виртуальный диск
# cryptsetup --cipher aes-cbc-essiv:sha256 --hash sha256 create zerocrypt /dev/mapper/zerodisk
Enter passphrase: <любой пароль>

Меряем скорость:
# dd if=/dev/zero of=/dev/mapper/zerocrypt bs=1M count=16384
16384+0 records in
16384+0 records out
17179869184 bytes (17 GB) copied, 38.3414 s, 448 MB/s

# dd of=/dev/null if=/dev/mapper/zerocrypt bs=1M count=16384
16384+0 records in
16384+0 records out
17179869184 bytes (17 GB) copied, 74.5436 s, 230 MB/s

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

Для сравнения, результаты этого же теста на новеньком Xeon E3-1270 v3 на ядре Haswell:

# dd if=/dev/zero of=/dev/mapper/zerocrypt bs=1M count=16384
16384+0 records in
16384+0 records out
17179869184 bytes (17 GB) copied, 11.183 s, 1.5 GB/s
# dd of=/dev/null if=/dev/mapper/zerocrypt bs=1M count=16384
16384+0 records in
16384+0 records out
17179869184 bytes (17 GB) copied, 19.4902 s, 881 MB/s

Вот, тут гораздо веселее дело идёт. Частота — решающий фактор, судя по всему.
А если деактивировать AES-NI, то будет в несколько раз медленнее.

DRBD

Настраиваем репликацию, конфиги с обоих концов должны быть на 100% идентичные.

/etc/drbd.d/global_common.conf

global {
    usage-count no;
}

common {
    protocol B;

    handlers {
    }

    startup {
        wfc-timeout 10;
    }

    disk {
        c-plan-ahead 0;
        al-extents 6433;
        resync-rate 400M;
        disk-barrier no;
        disk-flushes no;
        disk-drain yes;
    }

    net {
        sndbuf-size 1024k;
        rcvbuf-size 1024k;

        max-buffers 8192; # x PAGE_SIZE
        max-epoch-size 8192; # x PAGE_SIZE
        unplug-watermark 8192;

        timeout 100;
        ping-int 15;
        ping-timeout 60; # x 0.1sec
        connect-int 15;
        timeout 50; # x 0.1sec

        verify-alg sha1;
        csums-alg sha1;
        data-integrity-alg crc32c;
        cram-hmac-alg sha1;
        shared-secret "ultrasuperdupermegatopsecretpassword";
        use-rle;
    }
}

Тут наиболее интересным параметром является протокол, сравним их.

Запись считается удачной, если блок был записан…

  • A — на локальный диск и попал в локальный буфер отправки
  • B — на локальный диск и попал в удаленный буфер приема
  • С — на локальный и на удаленный диск

Самым медленным (читай — высоколатентным) и, одновременно, надёжным является C, а я выбрал золотую середину.

Далее идёт определение ресурсов, которыми оперирует DRBD и нод, участвующих в их репликации.

/etc/drbd.d/VM_STORAGE_1.res

resource VM_STORAGE_1 {
    device /dev/drbd0;

    disk /dev/mapper/VM_STORAGE_1;
    meta-disk internal;

    on vmstor1 {
        address 192.168.123.100:7801;
    }

    on vmstor2 {
        address 192.168.123.200:7801;
    }
}

/etc/drbd.d/VM_STORAGE_2.res

resource VM_STORAGE_2 {
    device /dev/drbd1;

    disk /dev/mapper/VM_STORAGE_2;
    meta-disk internal;

    on vmstor1 {
        address 192.168.123.100:7802;
    }

    on vmstor2 {
        address 192.168.123.200:7802;
    }
}

У каждого ресурса — свой порт.

Теперь инициализируем метаданные ресурсов DRBD и активируем их, это нужно сделать на каждом сервере:

# drbdadm create-md VM_STORAGE_1
# drbdadm create-md VM_STORAGE_2
# drbdadm up VM_STORAGE_1
# drbdadm up VM_STORAGE_2

Далее нужно выбрать какой-то один сервер (можно для каждого ресурса свой) и определяем, что он — главный и с него пойдёт первичная синхронизация на другой:

# drbdadm primary --force VM_STORAGE_1
# drbdadm primary --force VM_STORAGE_2

Всё, пошло-поехало, началась синхронизация.
В зависимости от размеров массивов и скорости сети она будет идти долго или очень долго.

За ее прогрессом можно понаблюдать командой watch -n0.1 cat /proc/drbd, очень умиротворяет и настраивает на философский лад.
В принципе, устройствами уже можно пользоваться в процессе синхронизации, но я советую отдохнуть :)

Конец первой части

Для одной части, я думаю, хватит. И так уже много информации для впитывания.

Во второй части я расскажу про настройку менеджера кластера и хостов ESXi для работы с этим поделием.

Автор: blind_oracle

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js