Хьюстон, у нас проблема, или Чего не договаривают производители HDD

в 10:10, , рубрики: fio, hdd, pt nad, raid, seagate, wd, wireshark, запись трафика, сетевой трафик, тестирование
Хьюстон, у нас проблема, или Чего не договаривают производители HDD - 1

Мы с командой вот уже 10 лет вполне успешно пилим систему поведенческого анализа сетевого трафика PT Network Attack Discovery (PT NAD). Продукт выявляет аномальные сетевые активности и сложные целенаправленные атаки на периметре и внутри сети организации. Он захватывает трафик со скоростями 100 Мбит/с — 10 Гбит/с, индексирует и хранит его в виде исходной копии в формате PCAP. Оператор SOC может всегда заглянуть в PT NAD, провести анализ сообщений сетевых протоколов (IPv4, IPv6, ICMP, TCP, UDP, HTTP, DNS, NTP, FTP, TFTP) и расследовать атаку. Но это лирическое начало.

Однажды к нам обратился клиент с проблемой: имеется 2 HDD с производительностью записи 250 MБ/с. Из них делается хранилище RAID 0. Начинаем записывать трафик, скорость — 350 MБ/с. Он успешно пишется, но через некоторое время утилизация дисков подходит к 100% и начинаются потери при записи. Вывод клиента: проблема в PT NAD, так как диски должны все успевать. Думаю, многие уже догадываются, в чем соль. У нас тоже имелись догадки, но тем не менее мы решили их проверить. Из этой проблемы и родилось небольшое исследование по записи трафика в хранилище. Под катом — наше расследование «заговора» разработчиков HDD.

Запись трафика

В PT NAD сетевой трафик разбирается на сессии — соединения TCP/UDP/ICMP и т. д.

О сессиях хранится метаинформация: начало и конец соединения, количество переданных данных, используемые протоколы и другие данные, которые помогают при расследовании атак. Кроме того, весь сетевой трафик сессии сохраняется в хранилище в неизменном виде. Это позволяет пользователям PT NAD скачивать дампы конкретных соединений в формате PCAP для более подробного анализа и изучения с помощью сторонних приложений, например Wireshark.

Вот пример карточки сессии.

Хьюстон, у нас проблема, или Чего не договаривают производители HDD - 2

Под хранилище сессий обычно используется массив HDD, дампы заполняют 90% предоставленного места, а затем начинается процесс ротации: старые сессии удаляются, чтобы в хранилище всегда было 10% свободного места. Таким образом, в хранилище всегда присутствуют сессии за последние N дней. N зависит от объема хранилища, а также от объема поступающего в PT NAD сетевого трафика.

Сессии в хранилище объединяются в файлы размером примерно по 1 ГиБ, запись в файлы происходит последовательно блоками по 2 МиБ, используется Direct I/O (флаг открытия файла O_DIRECT). Имеются небольшие индексные файлы для поиска конкретной сессии в файле с трафиком.

Хранилище должно успевать записывать поступающий трафик. Так, при скорости трафика 10 Гбит/с хранилище должно выдавать стабильные 1,25 ГБ/с и более на запись, иначе часть трафика потеряется и не будет доступна для скачивания пользователем.

Сразу проговорим, почему мы не рассматривали SSD для хранения трафика. Теоретически SSD должны быть быстрее HDD, но серверные SSD сильно выше по цене, а для хранения трафика их требуется много: обычно это десятки терабайт. Кстати, на Хабре уже обращали внимание на недостатки SSD (см, например, https://habr.com/ru/articles/154235/ и https://habr.com/ru/companies/yadro/articles/716220/).

Подготовка к тестированию

Для исследования проблемы у нас имелся сервер с 8 дисками по 8 ТБ. Диски одинаковые, в характеристиках указана скорость записи 255 MБ/с. Из них через аппаратный RAID-контроллер (кэш RAID был включен) был собран массив RAID 6 полезной емкостью 48 ТБ.

Так как данные пишутся достаточно большими блоками и последовательно, у нас должны получаться full stripe writes. И теоретическая скорость записи в этот массив должна достигать 255 MБ/с × 6, то есть приблизительно 1,5 ГБ/с. Сравнение характеристик разных видов RAID можно посмотреть в сводной таблице: https://en.wikipedia.org/wiki/Standard_RAID_levels#Comparison.

Хьюстон, у нас проблема, или Чего не договаривают производители HDD - 3

Для тестов в качестве «сети» используется двухпортовая сетевая карта, в один порт проигрывается трафик с помощью tcpreplay, второй — слушает PT NAD.

На сервере был развернут PT NAD в ограниченном режиме, а там отключено все, кроме непосредственно записи дампов в хранилище. Для эмуляции трафика использовался tcpreplay + netmap.

sudo tcpreplay -K -i ens2f0 --topspeed -l 0 --netmap --nm-delay=5 --unique-ip /home/administrator/pcaps/syslog_1mb.pcap

Получившаяся скорость трафика по информации от tcpreplay: 9832,78 Мбит/с, 2490070,41 пак/с.

На хранилище создан раздел файловой системы (FS) ext4.

Тестирование

С такими параметрами отставили сервер примерно на сутки, и вот что получилось.

Хьюстон, у нас проблема, или Чего не договаривают производители HDD - 4
  • DPI traffic, DPI drops — статистика по захвату трафика до обработки и записи.

  • PCAP writer — статистика по записи дампов в хранилище.

  • PCAP writer buffers use — статистика по используемым буферам записи (здесь видно, что когда они заканчиваются, то хранилище не успевает записывать поступающие объемы и появляются дропы записи).

  • IO Bytes, IO counts, IO utilization — IO-статистика по хранилищу от Linux.

  • Free disk space — показатель того, что хранилище заполняется и держится на отметке 10% свободного пространства.

На графиках мы видим циклы по скорости записи в хранилище: примерно 4 часа оно успевает записывать все, а затем в течение нескольких часов нарастает отставание скорости записи от скорости трафика. Затем цикл повторяется снова.

Чтобы убедиться, что такое проседание скорости записи — это не особенность PT NAD, хранилище было протестировано утилитой fio. На сервере стояла версия fio 3.25, и, как оказалось, в ней присутствуют некоторые баги. В дальнейшем был собран fio-3.36 из исходников, и в нем все работает как ожидается.

Подобрали конфиг fio, чтобы запись была похожа на таковую в PT NAD:

[write-test]
ioengine=libaio
rw=write
bs=2048k
iodepth=8
fallocate=truncate
direct=1
filesize=1073741824
nrfiles=10000
openfiles=12
group_reporting=1
numjobs=2
unique_filename=1
directory=/pcaps/fio_tests

Запустили тест, где два процесса пишут по 10 ТБ каждый. И вот что получилось.

Хьюстон, у нас проблема, или Чего не договаривают производители HDD - 5

В отличие от PT NAD, fio не ограничен входящим трафиком и пишет с максимальной скоростью. Здесь хранилище уже не было пустым, и в начале теста скорость была в районе 1,3 ГБ/с. А затем начала проседать вплоть до 660 MБ/с.

PT NAD достаточно оптимально использует диск, и утилита fio не показывает каких-то значительно более высоких результатов по сравнению с PT NAD. Fio показывает скорость повыше в начале цикла, но тут у PT NAD упор в трафик: tcpreplay выдает ≈1,25 Гбит/с, не более.

При этом теоретические 1,5 ГБ/с достигаются утилитой fio на пустом хранилище, но через некоторое время скорость также падает.

Было решено попробовать другую файловую систему, на хранилище была развернута XFS. Запустили подобный тест PT NAD, и вот что получилось.

Хьюстон, у нас проблема, или Чего не договаривают производители HDD - 6

Тоже есть колебания, но не такие значительные.
Если смотреть среднюю скорость записи за сутки, то и на ext4, и на XFS она получается около 1 ГБ/с, что недостаточно для трафика 10 Гбит/с, если писать его полностью на диск (нужны стабильные 1,25 ГБ/с). При этом теоретическая скорость записи на диски в RAID 6 должна быть 1,5 ГБ/с.
XFS в целом показала себя стабильнее. Нет просадок в два раза, но и изначальная скорость меньше.

Решили протестировать в других конфигурациях. Разобрали рейд, настроили запись в каждый диск отдельно, 2 ext4, 3 XFS, 3 ZFS (без сжатия). На каждый диск попадает примерно 156 МБ/с.
Вот что получилось.

ext4:

Хьюстон, у нас проблема, или Чего не договаривают производители HDD - 7

XFS:

Хьюстон, у нас проблема, или Чего не договаривают производители HDD - 8

ZFS:

Хьюстон, у нас проблема, или Чего не договаривают производители HDD - 9

Как видим, на всех файловых системах есть циклы. Когда утилизация диска составляет 100%, присутствуют дропы по записи.

ext4 и XFS в индивидуальном зачете практически на равных, но у ext4 один длинный цикл понижения производительности, а у XFS — два более коротких. При этом в RAID 6 XFS был более стабильным. ZFS показывает более высокий IO utilization в целом.

Был также протестирован RAID 0, где теоретическая скорость записи должна быть выше, чем в RAID 6. А точнее, количество дисков умножить на производительность одного диска. В нашем случае это целых 2 ГБ/с.

Вот результат.

Хьюстон, у нас проблема, или Чего не договаривают производители HDD - 10

Но в какие-то моменты даже RAID 0 не успевает записывать весь трафик. Был также протестирован RAID 10, но там скорость записи еще ниже.

HDD и физика

Тут пора вспомнить про устройство HDD. Грубо говоря, он представляет собой вращающийся блин, на котором в плоскости по направлению вращения есть дорожки с данными.

Хьюстон, у нас проблема, или Чего не договаривают производители HDD - 11

Если взять отдельный HDD и начать его заполнять от начала и до конца, то скорость записи проседает по мере заполнения диска. Скорость зависит от того, в какое физическое место на диске мы пишем.

На внешней дорожке диска каждый сектор с данными вращается быстрее, и сама дорожка вмещает больше секторов, и скорость записи/чтения быстрее. А по мере приближения к центру диска вращение каждого сектора медленнее и секторов в кольце меньше. Тут имеется в виду линейная скорость вращения отдельных точек на дорожке.
[https://en.wikipedia.org/wiki/Hard_disk_drive_performance_characteristics#Data_transfer_rate]

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

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

Мы протестировали одиночный диск 8 ТБ на нашем сервере. Использовали утилиту dd с флагами: direct — для прямой записи, минуя кэш Linux, seek_bytes — для выбора смещения на диске. Пример запуска: 

seek_bytes

Write speed

Примечание

0

264 МБ/с

Запись в начало диска

4000000000000

227 МБ/с

Запись в середину диска по объему, но, так как на внешних кольцах помещается больше данных, чем на внутренних, по радиусу это ближе к внешней границе. Чем дальше к концу диска, тем медленнее доступ

6000000000000

189 МБ/с

7000000000000

156 МБ/с

7700000000000

133 МБ/с

Запись в конец диска

Как видим, скорость записи варьируется от 133 до 264 МБ/с, то есть разница почти в два раза!

Вообще, эти наблюдения касаются как скорости записи, так и чтения. В нашем случае нас интересовала запись, и мы тестировали только ее.

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

Как могла бы делать FS:
Заполняем диск на 90%. По мере заполнения скорость записи медленно падает. Когда доходим до планки в 90%, начинают удаляться самые первые файлы, файловая система начинает писать новые данные на их место. Получается скользящее окно по диску, где самые медленные внутренние 10% по объему не задействованы. Это если говорить о записи на один диск. В случае использования RAID ситуация становится более сложной.

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

 Клиенты смотрят в спецификации на диск и видят что-то вроде этого.

Хьюстон, у нас проблема, или Чего не договаривают производители HDD - 12

Или этого.

Хьюстон, у нас проблема, или Чего не договаривают производители HDD - 13

Это скриншоты из официальных спецификаций на диски Seagate и WD.

При этом в сноске у WD написано:

Up to stated speed. 1 MB/s = 1 million bytes per second. Based on internal testing; performance may vary depending upon host device, usage conditions, drive capacity, and other factors

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

Скрипт для тестирования скорости записи на диск

Для быстрой проверки эффекта замедления хранилища на языке Python был написан скрипт-обертка над dd, который делает тестовые записи данных в разные участки диска от начала до конца и выводит результаты.

Список доступных девайсов в системе можно посмотреть через lsblk. Обратите внимание, что прямая запись через dd сломает файловую систему и данные в хранилище. Поэтому тестируйте только тогда, когда вам не жалко потерять все хранящиеся там данные.

Пример запуска на одном из наших тестовых хранилищ: 10 дисков по 4 ТБ в RAID 6

$ ./test_storage.py -d /dev/sda --yes

seek=0 (0.0%): 20971520000 bytes (21 GB, 20 GiB) copied, 12.6719 s, 1.7 GB/s

seek=1600090065920 (5.0%): 20971520000 bytes (21 GB, 20 GiB) copied, 12.4469 s, 1.7 GB/s

seek=3200180131840 (10.0%): 20971520000 bytes (21 GB, 20 GiB) copied, 12.6146 s, 1.7 GB/s

seek=4800270197760 (15.0%): 20971520000 bytes (21 GB, 20 GiB) copied, 12.6342 s, 1.7 GB/s

seek=6400360263680 (20.0%): 20971520000 bytes (21 GB, 20 GiB) copied, 13.0093 s, 1.6 GB/s

seek=8000450329600 (25.0%): 20971520000 bytes (21 GB, 20 GiB) copied, 13.1088 s, 1.6 GB/s

seek=9600540395520 (30.0%): 20971520000 bytes (21 GB, 20 GiB) copied, 13.3502 s, 1.6 GB/s

seek=11200630461440 (35.0%): 20971520000 bytes (21 GB, 20 GiB) copied, 13.6182 s, 1.5 GB/s

seek=12800720527360 (40.0%): 20971520000 bytes (21 GB, 20 GiB) copied, 13.9915 s, 1.5 GB/s

seek=14400810593280 (45.0%): 20971520000 bytes (21 GB, 20 GiB) copied, 14.3044 s, 1.5 GB/s

seek=16000900659200 (50.0%): 20971520000 bytes (21 GB, 20 GiB) copied, 14.6606 s, 1.4 GB/s

seek=17600990725120 (55.0%): 20971520000 bytes (21 GB, 20 GiB) copied, 14.9608 s, 1.4 GB/s

seek=19201080791040 (60.0%): 20971520000 bytes (21 GB, 20 GiB) copied, 16.3047 s, 1.3 GB/s

seek=20801170856960 (65.0%): 20971520000 bytes (21 GB, 20 GiB) copied, 16.2788 s, 1.3 GB/s

seek=22401260922880 (70.0%): 20971520000 bytes (21 GB, 20 GiB) copied, 16.8956 s, 1.2 GB/s

seek=24001350988800 (75.0%): 20971520000 bytes (21 GB, 20 GiB) copied, 17.8074 s, 1.2 GB/s

seek=25601441054720 (80.0%): 20971520000 bytes (21 GB, 20 GiB) copied, 18.8904 s, 1.1 GB/s

seek=27201531120640 (85.0%): 20971520000 bytes (21 GB, 20 GiB) copied, 20.1645 s, 1.0 GB/s

seek=28801621186560 (90.0%): 20971520000 bytes (21 GB, 20 GiB) copied, 21.9175 s, 957 MB/s

seek=30401711252480 (95.0%): 20971520000 bytes (21 GB, 20 GiB) copied, 23.0649 s, 909 MB/s

seek=31980829802496 (99.9%): 20971520000 bytes (21 GB, 20 GiB) copied, 25.0934 s, 836 MB/s

Как видим, на RAID принцип записи такой же. Самая быстрая часть располагается в начале, а самая медленная — в конце. При этом в самом конце емкости RAID скорость записи в два раза меньше, чем в начале.

Вот сам скрипт.
#!/usr/bin/env python3
import subprocess
import re
import os
import json
from argparse import ArgumentParser

def shell_exec(cmd: str):
    proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='utf-8', shell=True)
    ret_code = proc.wait()
    if ret_code != 0:
        raise RuntimeError(f'"{cmd}" return {ret_code}')
    return proc.communicate()

def get_device_size(dev: str):
    stdout, stderr = shell_exec(f"blockdev --getsize64 '{dev}'")
    return int(stdout)

def maybe_wipe_fs(dev: str, warning: str):
    print(warning)
    print(f'Script WILL DESTROY the file system, partitions or any data on testing device.')
    agree = input("Are you sure you want to continue (yes/no)? ")
    if (agree != 'yes'):
        raise RuntimeError('aborted')
    shell_exec(f"wipefs --all '{dev}'")

def is_mounted(cmd: str, device: dict):
    NOT_FOUND = object()
    mnt = device.get("mountpoints", device.get("mountpoint", NOT_FOUND))
    if mnt == NOT_FOUND:
        raise RuntimeError(f'Failed to get device mountpoint, check: "{cmd}"')
    if mnt not in ([], [None], None):
        return True
    if device.get("children") not in ([], [None], None):
        for child in device.get("children"):
            if is_mounted(cmd, child):
                return True
    return False

def check_device(dev: str):
    cmd = f"lsblk -J -i '{dev}'"
    stdout, stderr = shell_exec(cmd)
    data = json.loads(stdout)

    if "blockdevices" not in data or len(data["blockdevices"]) != 1:
        raise RuntimeError(f'Failed to get blockdevice, check: "{cmd}"')

    device = data["blockdevices"][0]

    if device.get("type") != "disk":
        raise RuntimeError(f'Device is not disk, check: "{cmd}"')

    if is_mounted(cmd, device):
        raise RuntimeError(f'Device "{dev}" is mounted, check: "{cmd}"')

    if device.get("children") not in ([], [None], None):
        maybe_wipe_fs(dev, f'Disk device have children (partitions), check: "{cmd}"')

    cmd = f"file --special-files '{dev}'"
    stdout, stderr = shell_exec(cmd)
    if stdout.strip() != f'{dev}: data':
        maybe_wipe_fs(dev, f'Device "{dev}" possible contains filesystem, check: "{cmd}"')

def parse_dd_size(size: str):
    # parse subset of possible suffixes of dd tool
    units = {"K": 2 ** 10, "KB": 10 ** 3,
             "M": 2 ** 20, "MB": 10 ** 6,
             "G": 2 ** 30, "GB": 10 ** 9,
             "T": 2 ** 40, "TB": 10 ** 12}

    match = re.fullmatch(r'([0-9]+)([KMGT]B?)?', size)
    if not match:
        raise RuntimeError(f'Failed to parse bs "{size}"')
    number, unit = match.group(1), match.group(2)
    result = int(number)
    if unit is not None:
        result = result * units[unit]
    return result

def make_step(args, seek: int, dev_size : int):
    dd_cmd = f'dd if=/dev/zero of="{args.device}" bs={args.bs} count={args.bc} oflag=direct,seek_bytes seek={seek}'
    stdout, stderr = shell_exec(dd_cmd)
    percent = seek / dev_size * 100
    out = stderr.split(sep='n')[2]
    print(f"seek={seek} ({percent:.1f}%): {out}")

def check_is_positive(name: str, val: int):
    if val <= 0:
        raise RuntimeError(f'"{name}" must be greater then 0')

if __name__ == "__main__":
    try:
        parser = ArgumentParser(description='Storage test. Script WILL DESTROY the file system, partitions or any data on testing device!')
        parser.add_argument('device', help='device for test, i.e /dev/sdb')
        parser.add_argument('--bs', help='block size for dd. Default is 2M.', default='2M')
        parser.add_argument('--bc', help='block count for dd. Default is 10000.', type=int, default=10000)
        parser.add_argument('--steps', help='number of steps exclude last. Default is 20.', type=int, default=20)
        parser.add_argument('--alignment', help='alignment of write operations. Default is 1024.', type=int, default=1024)
        args = parser.parse_args()

        args.bs = parse_dd_size(args.bs)
        check_is_positive("bs", args.bs)
        check_is_positive("bc", args.bc)
        check_is_positive("steps", args.steps)
        check_is_positive("alignment", args.alignment)

        if os.geteuid() != 0:
            raise RuntimeError('Script must be run as root')

        check_device(args.device)

        dev_size = get_device_size(args.device)
        for step in range(0, args.steps):
            seek = step * (dev_size // args.steps)
            seek = seek - (seek % args.alignment)
            make_step(args, seek, dev_size)

        write_size = args.bs * args.bc
        last_seek = dev_size - write_size
        last_seek = last_seek - (last_seek % args.alignment)
        make_step(args, last_seek, dev_size)

    except Exception as ex:
        print(f'ERROR: {ex}')
        exit(1)
    except KeyboardInterrupt as ex:
        print(f'nERROR: interrupted')
        exit(1)

Выводы

  • Скорость записи в хранилище на HDD непостоянна. Нельзя полагаться на спецификации и теоретические расчеты, необходимо перепроверять производительность.

  • Имея хранилище на десятки терабайт, нельзя записать в него пару десятков гигабайт и надеяться, что полученная скорость записи будет всегда достижима. В идеале нужно провести тест на полное заполнение хранилища, и уже исходя из этого будет понятна средняя, минимальная и максимальная скорость записи.

  • Если вам нужно писать поток данных с ротацией и без потерь, то скорость потока данных не должна превышать минимальную скорость записи. Если необходима более высокая скорость, то советуем создавать более мощный RAID из большего количества дисков с большей производительностью.

  • При использовании утилит для тестирования производительности нужно понимать, что они делают, чтобы не получить бесполезные результаты. В утилитах бывают баги, и даже при правильном использовании нужно убедиться, что тестируется то, что надо, и как надо.


На этом наше расследование по HDD — всё. Нашу задачу по записи больших объемов трафика мы если не решили, то нашли источник проблемы, а это уже главное. А с какими проблемами при записи трафика сталкивались вы? Поделитесь в комментариях под статьей!

Хьюстон, у нас проблема, или Чего не договаривают производители HDD - 14

Алексей Сагин

Разработчик PT NAD в Positive Technologies

Автор: ptsecurity

Источник

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


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