Бюджетная виртуализация. Оптимизация NFS и iSCSI

Статья опубликована в журнале «Системный Администратор»

Выжимаем максимум из имеющегося оборудования и каналов связи

Введение

Основная часть материалов, посвященная виртуализации, ориентирована в основном на крупные и средние инфраструктуры. О внедрениях виртуализации в масштабах 10-20 ВМ на нескольких серверах сегодня почти никто не говорит. А на самом деле, сред, в которых работает не большее число ВМ, на проложенном в далеком прошлом Ethernet и самосборных сетевые хранилищах, достаточно. И ситуация эта особенно характерна для СНГ, где на развитие ИТ-отдела и на внедрение новых технологий порой выделяют столько средств, что трудно не заплакать.
В этой статье, хочется уделить внимание возможным путям оптимизации сетевой подсистемы, как на аппаратном уровне, так и на программном, для повышения производительности имеющегося оборудования и каналов связи.
В данной статье, под каналами связи везде, где не указанно обратное, подразумевается доступный даже в самых бюджетных организациях Gigabit Ethernet.

Аппаратная оптимизация

Аппаратная оптимизация подразумевает под собой приобретение дополнительного оборудования, в частности сетевого, что может показаться не вписывающимся в контекст данной статьи. Все же я решил об этом упомянуть, так как возможно, это будет следующим этапом развития вашей инфраструктуры.
Сетевые контроллеры с TCP Offload Engine (сокращенно TOE) — это технология, встраиваемая в сетевые адаптеры для разгрузки центрального процессора и возложения функций по обработке сетевых пакетов стека протоколов TCP/IP таких как формирование и подсчет контрольной суммы пакетов на контроллер сетевого адаптера. Как правило, применяется в высокоскоростных сетевых адаптерах: Gigabit Ethernet и 10 Gigabit Ethernet, когда накладные расходы на обработку сетевых пакетов становятся существенными[1].
Устройство TOE взаимодействует с хост-системой посредством интерфейса сеансового уровня. К примеру, при загрузке файла длиной 64 Кбайт обычная сетевая плата (без TOE) инициирует  около 60-ти транзакций (прием около 40 пакетов данных и передача 20 подтверждений) для ЦП хост-системы. Если же на сетевом адаптере имеется устройство TOE, обработка пакетов и передача подтверждений осуществляются самим адаптером. В этом случае данные загружаются в буферы приложений и выгружаются из них с помощью аппаратно реализованного механизма прямого доступа к памяти (DMA), а сэкономленная вычислительная мощность ЦП используется для выполнения критически важных приложений.
Сетевые контроллеры с TOE в одинаковой степени повышают производительность сетевой подсистемы и снижают нагрузку на ЦПУ не зависимо от используемого протокола на более высоком уровне(NFS,iSCSI и так далее).
В случае с iSCSI требуется больше вычислительных ресурсов, чем для простого обмена TCP/IP-данными, поскольку в нем предусмотрены дополнительные операции по упаковыванию и распаковыванию SCSI-блоков. Поэтому рекомендуется использовать сетевые адаптеры (iSCSI Host Bus Adapter), в которых кроме TOE  аппаратно реализован и уровень iSCSI. Эти адаптеры имеют специальные чипы iSCSI и TCP/IP, что позволяет достигать высоких скоростей обработки пакетов iSCSI  и максимальной разгрузки процессора хоста.
В свою очередь, NFS является сетевой файловой системой, большая часть стека которой должна поддерживаться ОС, поэтому каких либо адаптеров, способных аппаратно выполнять уровни NFS, не существует. В этом плане у iSCSI есть преимущество.
Все вышесказанное, является теорией и в большинстве своем общеизвестно. От себя хочется добавить, что при сегодняшних реалиях с многоядерными серверами. Которые сейчас используются даже в организациях с небольшим ИТ-бюджетом. На практике, положительный эффект от контроллеров с TOE будет на уровне нескольких процентов. В случае же если сервера совсем старые, с высокой загрузкой процессора, использование контроллеров с аппаратной разгрузкой будет наиболее полезно.
Такая же ситуация и с iSCSI HBA. Если производительности процессоров с избытком, то эффект от таких контроллеров будет минимальный, почти не заметный.
Рис. №1. Уровни разгрузки сетевого стека различными контроллерами

Различные методики агрегации каналов

Одними из достоинств технологии Ethernet, являются ее гибкость и простота.
Если дорогостоящие контроллеры с аппаратными функциями не вписываются в ваш бюджет, можно воспользоваться другим подходом.
Например, обзавестись кучей дешевых контроллеров Gigabit Ethernet и воспользоваться одной из хорошо зарекомендовавших себя техник агрегации каналов.
Например, технология «Multipath IO» и ее реализация в linux (DM-Multipath) позволяет прозрачно для ОС и приложений агрегировать несколько каналов ведущих к одному дисковому массиву системы хранения в один логический. Тем самым пропорционально количеству каналов повышается производительность и отказоустойчивость в случае выхода одного из каналов.

Multipathing никак не отменяет все сказанное в этой статье, а является следующим шагом в оптимизации производительности сетевой подсистемы. На эту тему будет подготовлен отдельный материал.

Оптимизация сетевого стека

Современные операционные системы, в частности серверные версии Linux, которые чаще всего используются в качестве ОС для серверов хранения и виртуализации, по умолчанию не оптимизированы для конкретных ролей. Большинство компонентов и сервисов ОС настроены универсально для широкого круга задач и готовы к работе в любых условиях и на различном оборудовании. Например, сетевой стек, без каких либо настроек способен одновременно поддерживать сотни подключений к веб-серверу и при этом обслуживать тяжелые операции чтения/записи в роле программного iSCSI SAN. Такой подход, понятен и он удобен в большинстве задач. Но, в случае, когда требования к производительности высоки, для конкретных ролей, сервер и ОС должны быть дополнительно оптимизированы и настроены более тонко.
Используя 1G Ethernet мы, по большему счету (в сравнении с FC), не получаем широких каналов и уж тем более приемлемого, критичного для ВМ времени отклика. В таком случае, упираясь в производительность сетевого оборудования остается только попробовать оптимизировать работу сетевых протоколов, для получения максимального эффекта от того что имеем.
К программной оптимизации можно отнести всевозможные методы ускорения сетевого стека ядра за счет переключения различных режимов или возможных выделений дополнительных ресурсов.
В дальнейшем, по ходу статьи, полагается, что среда передачи стабильная. Обрывы и многочисленные потери пакетов исключаются.

Увеличение MTU

Увеличение MTU (Maximum Transmission Unit) со стандартных 1500 до 9000 байт позволит сетевым контроллерам пропускать сверхдлинные Ethernet-кадры (Jumbo-кадры)[2].
В RedHat-подобных дистрибутивах (CentOS, OpenFiler, XenServer) это делается в файле настроек конкретного сетевого контроллера(/etc/sysconfig/network-scripts/ifcfg-<ИмяУстройства> где имя устройства, обычно eth<№>, где вместо № будет порядковый номер контроллера начиная с нуля(eth0, eth1 и т.д.). Меняем стандартное значение MTU=1500 на MTU=9000.
Это увеличит размер передаваемых данных за раз в одном пакете (максимальный размер блока), что значительно повысит производительность вне зависимости от типа трафика, так как размер блока устанавливается на канальном уровне модели OSI прозрачно для высокоуровневых протоколов(NFS, iSCSI, CIFS и прочие)
Подобное увеличение кадра должно быть проделано на каждом из участвующих в обмене интерфейсах. В случае если Jumbo-кадры будут задействованы не на всех узлах, значительно возрастет фрагментация пакетов, что окажет только негативное воздействие на производительность.
Кроме всего прочего, включение сверхдлинных кадров снижает нагрузку на процессор за счет генерации меньшего числа пакетов.
Не рекомендуется устанавливать значение MTU больше 9000 байт (максимально 16000 байт) поскольку в сетях Ethernet используется 32-битная CRC (алгоритм контроля целостности), который теряет свою эффективность при объеме данных больше 12000 байт; к тому же 9000 байт достаточно для передачи 8-килобайтной датаграммы (например такие как используются в NFS).

Изменение сетевых параметров ядра

Список наиболее важных параметров ядра, имеющих отношение к эффективности и производительности сети в контексте сетей хранения данных [3];
net.core.somaxconn = 256 (по умолчанию 128) — Максимальное число открытых сокетов, ждущих соединения. Имеет смысл увеличить значение при большем количестве соединений. Для высоко нагруженных серверов советуют значения в районе 15000-20000.
В нашем случае, количество соединений не большое (это не веб-сервер), поэтому можно и не увеличивать.
net.core.rmem_max = 1073741824 (по умолчанию 124928) — Устанавливает максимальный размер буфера на прием для всех протоколов.
net.core.wmem_max = 1073741824 (по умолчанию 124928) — Устанавливает максимальный размер буфера на передачу для всех протоколов.
net.ipv4.tcp_reordering = 20 (по умолчанию 3) Иногда увеличение времени прохождения пакета в локальной сети может считаться потерей пакета. Для таких случаев увеличение значения этого параметра повышает производительность.
net.ipv4.tcp_rmem = 1048576 16777216 1073741824 (по умолчанию 4096 87380 937024)
Соответственно минимум, по умолчанию и максимум. Переменная в файле tcp_rmem содержит 3 целых числа, определяющих размер приемного буфера сокетов TCP.
Каждый сокет TCP имеет право использовать эту память по факту своего создания. Размер минимального буфера по умолчанию составляет 4 Кбайт (4096)
net.ipv4.tcp_wmem = 1048576 16770216 1073741824 (по умолчанию 4096 16384 937024 )
Переменная в файле tcp_wmem содержит 3 целочисленных значения, определяющих минимальное, принятое по умолчанию, и максимальное количество памяти, резервируемой для буферов передачи сокета TCP.
Каждый сокет TCP имеет право использовать эту память по факту своего создания. Размер минимального буфера по умолчанию составляет 4 Кбайт (4096)
ВНИМАНИЕ: Максимальные значения параметров net.ipv4.tcp_rmem, net.ipv4.tcp_wmem не должны превышать значения параметров net.core.rmem_max и net.core.wmem_max!
net.ipv4.tcp_mem = 1048576 16770216 1073741824 (по умолчанию 21960 29282 43920)
Переменная в файле tcp_mem содержит 3 целых числа (порога), определяющих отношение протокола TCP к выделению памяти.
Нижний порог: при значениях ниже этого уровня TCP не заботится о расходе памяти.
Порог ограничения: при достижении этого порога TCP контролирует размер выделяемой памяти и переходит в режим memory pressure (нехватка памяти), из которого выходит при снижении расхода памяти до нижнего порога.
Верхний порог: число страниц памяти, доступных для создания очередей всеми сокетами TCP.
Значения всех порогов рассчитываются во время загрузки ОС с учетом доступной на компьютере памяти.
net.ipv4.tcp_window_scaling = 1 (по умолчанию обычно 1) — Активирует масштабирование окна, как определено в RFC 1323; должен быть включен(1) для поддержки окон размером больше, чем 64KB.

net.ipv4.tcp_sack = 1 (по умолчанию обычно 1) — Активирует выборочное подтверждение (selective acknowledgment), которое улучшает производительность, выборочно подтверждая пакеты, полученные вне очереди (в результате отправитель повторно передает только пропущенные сегменты); должен быть включен (для большой области сетевых коммуникаций), но может усилить использование CPU.

net.ipv4.tcp_fack = 1 (по умолчанию обычно 1) — Включает Forward Acknowledgment (прогнозное подтверждение), которое оперирует с выборочным подтверждением (SACK — Selective Acknowledgment) для уменьшения перегрузки; должен быть включен(1).

net.ipv4.tcp_timestamps = 1 (по умолчанию обычно 1) — Активирует вычисление RTT более достоверным способом (смотрите RFC 1323), чем интервал для повторной передачи; должен быть включен для быстродействия.

net.ipv4.tcp_low_latency = 0 (по умолчанию обычно 0) — Разрешает стеку TCP/IP отдавать предпочтение низкому времени ожидания перед более высокой пропускной способностью. Вот здесь спорный момент. Если каналы высоконравственны, то лучше не включать. Если трафик не большей и более важно время отклика, то лучше включить.
net.core.netdev_max_backlog = 30000 (по умолчанию обычно 1000) — Параметр определяет максимальное количество пакетов в очереди на обработку, если интерфейс получает пакеты быстрее, чем ядро может их обработать

Установить значения приведенных выше параметров можно воспользовавшись командой:

#sysctl -w <имяПараметра>=<Значение>

Например устанавливаем новое значение параметра net.ipv4.tcp_low_latency:

#sysctl -w net.ipv4.tcp_low_latency=1

Изменение значений параметров с помощью утилиты sysctl, действуют только до перезагрузки системы. Для установки постоянных значений необходимо прописать соответствующие параметры и их значения в файле /etc/sysctl.conf
Посмотреть текущие значения параметров можно в соответствующих файлах виртуальной файловой системы /proc:

#cat /proc/sys/net/ipv4/tcp_low_latency

Изменение значения перечисленных выше параметров в разной степени, но все же оказываю влияние на потребление памяти и ресурсов CPU.

Конкретно для NFS

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

Число потоков nfsd

Получая RPC-запрос от клиента, серверу может потребоваться время для его обработки. Даже доступ к локальной файловой системе может занять некоторое время. В течение этого времени сервер не хочет блокировать запросы от других клиентов, которые также должны быть обслужены. Чтобы справиться с подобной ситуацией, большинство NFS-серверов запускают несколько потоков в рамках процесса nfsd. Конкретные методы решения и число доступных потоков зависят от операционной системы.
В моем случае в качестве NFS-сервера использовался CentOS 6.3. По умолчанию, в этой ОС установлено 8 потоков.
Текущее количество потоков демона nfsd на сервере, посмотреть можно с помощью следующей команды:

#cat /proc/fs/nfsd/threads

В моем случае их было 8.

Задать собственное количество до перезагрузки:

#echo 128 > /proc/fs/nfsd/threads

Задать число потоков на постоянной основе можно в конфигурационном файле /etc/sysconfig/nfs. Параметр RPCNFSDCOUNT=8.
Как определить достаточно ли потоков?

На сервере.

#cat /proc/net/rpc/nfsd
rc 0 58707094 373082036
fh 0 0 0 0 0
io 4192757445 576131420
th 28 561052 15634.270 7895.289 27271.733 3240.960 229.407 127.945 68.418
…
(вывод сокращен)

В этом листинге, нас интересует строчка начинающаяся с «th».
28 = число потоков NFS-сервера
561052 = Количество раз, когда все 28 потоков были заняты. Данный листинг получен на высоко нагруженном сервере с месячным временем не прерывной работы.

Проверка утилизации потоков сервера

Утилита nfsstat выполненная на клиенте с ключами -rc выводит нам примерно следующую информацию:

#nfsstat -rc
Client rpc stats:
calls            retrans    authrefrsh
3564050     104        4050

Здесь видно, что значение retrans (повторная отправка RPC-запроса) достаточно высокое. Это явный признак того, что количества доступных потоков ядра NFS на сервере недостаточно, чтобы обрабатывать запросы от этого клиента.
С помощью утилиты nfsstat с ключами -s (на сервере) или -c (на клиенте) можно получить исчерпывающую информацию о работе обеих сторон. Например, получить процентное соотношение операций чтения и записи.
Напоследок, стоит отметить, что если процессор или дисковая подсистема компьютера уже перегружены, увеличение числа потоков не приведет к повышению производительности. Число потоков должно быть оптимально подобранно  с учетом возможностей оборудования! Тестируйте.

Опции монтирования (относится к NFS-клиентам)

Размеры блоков чтения и записи (rsize и wsize)

Эти опции отвечают за то, какими порциями nfs-клиент будет отправлять и получать данные за раз, в одном RPC-запросе.
В идеале, значения этих опций не должны превышать размер MTU, так как в этом случае, возрастает фрагментация пакетов. Это приводит к дополнительным затратам на сборку пакетов как на сервере так и на клиенте и негативно сказывается на производительности.
Обычно, если опции rsize и wsize не заданы, то в зависимости от версии NFS или сборки будет читать и писать блоками по 4096 или 8192 байтов. Некоторые ядра Linux(в частности, сетевой стек) и сетевые кары не могут обрабатывать такие большие блоки, что приводит к деградации производительности.
Посмотреть установленные по умолчанию в вашей ОС rsize и wsize можно вот так cat /proc/mounts. При этом должен быть смонтирован хотя бы один NFS-каталог.

#cat /proc/mounts
192.168.0.41:/NFS_Store/ /mnt nfs4
 rw,relatime,vers=4,rsize=65536,wsize=65536,namlen=255,hard,proto=tcp,port=0,timeo=600,retrans=2,sec=sys,clientaddr=192.168.0.1,minorversion=0,local_lock=none,addr=192.168.0.41 0 0

Выше приводится информация об одном из смонтированных каталогов. Здесь можно увидеть все опции монтирования, в том числе и желанные rsize и wsize.
Данный вывод получен на ОС Ubuntu 12.04 с настройками по умолчанию.
В дистрибутиве XenServer 6.0.2 по умолчанию, используются такие же значения.
Установить произвольные значения параметров rsize и wsize можно при монтировании NFS-каталога.

#mount -o rsize=8192,wsize=8192 SRV-01-CO-01:/NFS_Store/ /mnt

В данном примере, указанны значения в 8kb, что оптимально при включенных Jombo-кадрах(MTU=9000)

При подключении NFS-хранилища, например, в графической консоли Citrix XenCenter или VMware vSphere Client, опции монтирования так же указываются в соответствующих полях и разделах интерфейса.

Выводы

Сетевой стек так же, как и большинство серверных приложений и служб современных ОС, имеют достаточно универсальные настройки по умолчанию. Такие настройки позволяют одинаково не плохо (но и не очень хорошо) работать в различных условиях, не требуя вмешательства. Если ваше сетевое хранилище построено на базе одной из универсальных ОС, то придется засучить рукава и выполнить дополнительные настройки для получения максимальной производительности.

Ссылки

1. Исчерпывающая информация о TCP offload engine. К сожалению, только на английском — http://www.books.ru/books/tcp-offload-engine-1185738/
2. Наиболее полный материал о Jumbo-кадрах — http://en.wikipedia.org/wiki/Jumbo_frame
3. Описание некоторых sysctl переменных ядра Linux — http://www.opennet.ru/base/sys/sysctl_linux.txt.html