Предыстория

Недавно я начал готовить очередной Kubernetes кластер на Bare Metal серверах для одного из наших проектов дабы съехать с Google Cloud и снизить расходы на инфраструктуру примерно в 4 раза, получив при этом в 4 раза больше ресурсов vCPU/RAM/SSD (да и производительность сетевых дисков в облаках оставляет желать лучшего).

В качестве ОС я решил взять горячо мной любимую Talos Linux, которая позволяет очень просто развернуть Kubernetes-кластер на любом окружении, легко обновлять его компоненты и поддерживать конфигурацию в одинаковом состоянии на всех узлах кластера благодаря декларативности и иммутабельности.

Развернуть всё это я решил в Hetzner на серверах Dell PowerEdge R6615 (линейка DX182). Конфигурация каждого сервера выглядит так:

  • 1x AMD EPYC™ GENOA 9454P (96 vCPU, Zen 4)

  • 384GB DDR5 ECC RAM (12x32GB)

  • 2x960G SATA SSD в RAID1 используя Dell PERC H755 контроллер. На него непосредственно и устанавливался Talos Linux (эта ОС не поддерживает mdadm).

  • 2x7.68TB U.2 PCIe NVMe SSD Samsung PM9A3 — для Linstor хранилища в Kubernetes

  • 2x10G NIC (для 10G интернет-аплинка)

  • 2x25G NIC (для приватной сети, объединены с помощью LACP bonding)

Красота
Красота

У Hetzner также есть линейка AX162 с тем же AMD 9454P процессором, но существенно дешевле и на более дешевых компонентах.

Например, там используется материнская плата ASRock Rack, интерфейс BMC которой сильно хуже чем iDRAC в серверах Dell. К слову, Hetzner по какой-то причине вообще не даёт доступ к BMC ASRock Rack, а вот к iDRAC — пожалуйста. На Reddit есть топик, в котором обсуждаются проблемы со стабильностью серверов этой линейки, так что подумайте дважды, если захотите брать такие сервера в аренду.

Также, в отличие от Dell, в серверах линейки AX162 не зарезервировано питание, поскольку БП всего один.

Сервер линейки AX162
Сервер линейки AX162

А вот так выглядит выбранный стэк технологий для Kuberrnetes-кластера:

  • Cilium в качестве CNI с включенным eBPF, kube-proxy replacement, native routing, BBR, XDP и прочими плюшками

  • Linstor+DRBD в async-режиме для хранения поверх LVM Stripe.

  • VictoriaMetrics K8S Stack для мониторинга

  • VictoriaLogs для сбора, хранения и анализа логов

  • FluxCD чтобы декларативно менеджить это всё используя GitOps-подход.

Метод тестирования NVMe SSD дисков

Перед тем как запускать кластер в production, стоит как следует его протестировать, это включает в себя не только failover-тесты чтобы понять как ведёт себя кластер в тех или иных ситуациях, но и performance-бенчмарки процессора, памяти, сети и дисков. Это поможет нам в будущем понять как обновления ОС, ядра и других компонентов влияют на производительность со временем. Да и в целом не помешало бы понять, на что наше железо способно, особенно в сравнении с облаками. Вот на дисках мы и остановимся в этой статье.

Кстати, перед этими тестами я обновил прошивку NVMe SSD-дисков, а также всех других компонентов, включая BMC, BIOS, сетевых карт и прочего. Также поменял размер сектора на дисках с 512 байт до 4 килобайт.

В качестве метода тестирования я выбрал, на мой взгляд, достаточно объективный набор тестов, который репрезентативен для большинства типичных нагрузок в современных кластерах (СУБД, такие как Postgres и ClickHouse, объектные хранилища S3, и т.п.):

fio -name=randwrite_fsync -filename=/dev/nvme0n1 -output-format=json -ioengine=libaio -direct=1 -runtime=60 -randrepeat=0 -rw=randwrite -bs=4k -numjobs=1 -iodepth=1 -fsync=1
fio -name=randwrite_jobs4 -filename=/dev/nvme0n1 -output-format=json -ioengine=libaio -direct=1 -runtime=60 -randrepeat=0 -rw=randwrite -bs=4k -numjobs=4 -iodepth=128 -group_reporting
fio -name=randwrite -filename=/dev/nvme0n1 -output-format=json -ioengine=libaio -direct=1 -runtime=60 -randrepeat=0 -rw=randwrite -bs=4k -numjobs=1 -iodepth=128
fio -name=write -filename=/dev/nvme0n1 -output-format=json -ioengine=libaio -direct=1 -runtime=60 -randrepeat=0 -rw=write -bs=4M -numjobs=1 -iodepth=16
fio -name=randread_fsync -filename=/dev/nvme0n1 -output-format=json -ioengine=libaio -direct=1 -runtime=60 -randrepeat=0 -rw=randread -bs=4k -numjobs=1 -iodepth=1 -fsync=1
fio -name=randread_jobs4 -filename=/dev/nvme0n1 -output-format=json -ioengine=libaio -direct=1 -runtime=60 -randrepeat=0 -rw=randread -bs=4k -numjobs=4 -iodepth=128 -group_reporting
fio -name=randread -filename=/dev/nvme0n1 -output-format=json -ioengine=libaio -direct=1 -runtime=60 -randrepeat=0 -rw=randread -bs=4k -numjobs=1 -iodepth=128
fio -name=read -filename=/dev/nvme0n1 -output-format=json -ioengine=libaio -direct=1 -runtime=60 -randrepeat=0 -rw=read -bs=4M -numjobs=1 -iodepth=16

Взял я этот набор тестов здесь. Кстати, это довольно интересная статья от @vitalif советую почитать.

Обратите внимание, что fio тесты выполняются напрямую на блочном устройстве /dev/nvme0n1, чтобы исключить влияние файловой системы на результаты. Также перед каждым отдельным вызовом fio выполнялась команда blkdiscard -f /dev/nvme0n1 для того чтобы исключить влияние предыдущего теста на следующий.

Я подготовил удобный docker-образ maxpain/fio:3.38, который включает в себя последнюю версию fio, а также вышеупомянутый набор тестов в скрипте /run.sh, который я любезно позаимствовал у @kvaps и немного модицифировал. Он работает 8 минут и выдаёт CSV строчку, которую можно легко вставить в Google-табличку, наподобие этой, сразу же получив наглядные графики и полную картину по Bandwidth/IOPS/Latency.

Пример запуска в Docker:

docker run -it --rm --entrypoint /run.sh --privileged maxpain/fio:latest /dev/nvme0n1

Пример запуска в Kubernetes (после запуска подов заходим внутрь с помощью kubectl exec):

kind: DaemonSet
apiVersion: apps/v1
metadata:
  name: fio-test
  namespace: debug
spec:
  selector:
    matchLabels:
      app: fio-test
  template:
    metadata:
      labels:
        app: fio-test
    spec:
      terminationGracePeriodSeconds: 0
      containers:
        - name: fio
          image: maxpain/fio
          command:
            - sleep
            - infinity
          securityContext:
            privileged: true

Тестирование в разных ОС

Изначально я хотел измерить влияние DRBD поверх LVM на производительность I/O по сравнению с RAW-диском, но обманул сам себя, сразу же запустив тесты на Talos Linux и приняв их как должное. В какой-то момент, сравнивая получившиеся цифры с результатами тестирования @kvapsна куда более медленных и старых серверах я удивился тому, что мои результаты получились куда хуже.

Я решил протестировать производительность RAW диска на Debian 12, скомпилировав ту же самую LTS-версию ядра Linux 6.6.54, которая используется в Talos Linux v1.8.1, вот что получилось:

Я решил на всякий случай сравнить всякие системные параметры вроде scheduler и cpu governor (на обеих системах стоял performance), но все они были одинаковыми.

Что за мистика? Очевидно, дело в конфигурации ядра. Я решил скомпилировать Linux ядро под Debian используя kernel config от Talos Linux и получил снижение производительности подобно тому, что я видел непосредственно в Talos Linux!

IOMMU

Искать параметр в конфиге ядра, который так сильно убивает производительность, это как искать иголку в стоге сена, поскольку в kernel config'e более 6000 строк, а если файла два? Как понять какой именно параметр влияет? А если влияющих параметров несколько?

В решении такой непростой задачи мне помог скрипт scripts/diffconfig из состава ядра Linux, он получает на вход 2 конфига и выдаёт diff вроде такого:

-AMD_XGBE_HAVE_ECC y
 NUMA_BALANCING y -> n
 NUMA_EMU y -> n
 NVIDIA_WMI_EC_BACKLIGHT m -> n
 NVME_AUTH n -> y
 NVME_CORE m -> y
+IMA_LOAD_X509 n

Diff-файл на выходе получился достаточо объёмным и всё ещё не представлялось возможным вручную найти злополучный параметр.

На помощь пришёл ChatGPT, а именно новая модель o1-preview, которая с удовольствием проглотила вышеупомянутый огромный diff-файл и, на моё удивление, с первой же попытки выдала проблемный параметр:

IOMMU настройки по умолчанию:

В Talos Linux включен параметр CONFIG_IOMMU_DEFAULT_DMA_STRICT=y, в то время как в Debian используется CONFIG_IOMMU_DEFAULT_DMA_LAZY=y. Режим strict в IOMMU заставляет ядро немедленно выполнять сброс кэшей IOMMU при каждом связывании и отвязывании DMA (то есть при каждом вводе-выводе), что приводит к дополнительной нагрузке на систему и может значительно снизить производительность ввода-вывода при интенсивных операциях, таких как тестирование IOPS.

   Рекомендуемое действие: Изменить настройку CONFIG_IOMMU_DEFAULT_DMA_STRICT=y на CONFIG_IOMMU_DEFAULT_DMA_LAZY=y в конфигурации ядра Talos Linux, чтобы соответствовать настройке в Debian и уменьшить накладные расходы на операции DMA.

Однако, можно не пересобирать ядро, а вместо этого передать kernel аргумент iommu.strict=0 :

config IOMMU_DEFAULT_DMA_LAZY
bool "Translated - Lazy"
help
Trusted devices use translation to restrict their access to only
DMA-mapped pages, but with "lazy" batched TLB invalidation. This
mode allows higher performance with some IOMMUs due to reduced TLB
flushing, but at the cost of reduced isolation since devices may be
able to access memory for some time after it has been unmapped.
Equivalent to passing "iommu.passthrough=0 iommu.strict=0" on the
command line.

И да, этот параметр действительно вернул производительность I/O в Talos Linux на значения уровня Debian!

Кстати, Talos — не единственная ОС, которая столкнулась с подобной проблемой. Например, судя по этому баг-репорту, в Ubuntu 24.04 из-за этой же настройки у некоторых людей падала производительность в 3.5 раза! Но уже не дисков, а сети:

iommu.strict=1 (strict): 233464.914 TPS
iommu.strict=0 (lazy): 835123.193 TPS

Что-то мне подсказывает что и у GPU в AI/ML-кластерах будут проблемы из-за этой настройки.

Разработчики Talos Linux оперативно поменяли дефолтное значение этого параметра, так что в новых версиях этой ОС не потребуется передавать iommu.strict=0 для исправления проблем с производительностью.

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

Security-патчи и spec_rstack_overflow

Мне было любопытно, как на производительность NVMe SSD дисков влияет версия Linux ядра, поэтому я скомпилировал все ядра начиная с Linux 6.1, заканчивая 6.11, и вот что получилось:

Как такое возможно? Почему в 6.2 и 6.3 в два раза больше IOPS? А почему в 6.1 такие же значения как и у 6.4 и более новых ядрах? Мне не пришло ничего в голову лучше чем сделать git bisect, и скомпилировав ещё 15 разных вариантов ядра я нашёл злополучный коммит.

Оказывается, в процессорах AMD нашли уязвимость Speculative Return Stack Overflow (SRSO), заплатка для которой очень влияет на производительность I/O.

Так как я всегда обновляю микрокод процессоров благодаря официальным Talos extensions amd-ucode и intel-ucode, собирая кастомные образы на factory.talos.dev, можно частично исправить эту уязвимость без программного патча используя kernel аргумент spec_rstack_overflow=microcode.

Микрокод защищает лишь от атак в направлении пользователь->ядро и гость->хост. Но не защищает от пользователь->пользователь и виртуалка->виртуалка, для этого нужна программная заплатка.

Текущий статус защиты можно посмотреть в /sys/devices/system/cpu/vulnerabilities/spec_rstack_overflow

При отсутствии программной заплатки будет статус "Vulnerable: Microcode, no safe RET".

Можно было бы воспользоваться mitigations=off, но я не рекомендую это делать, по крайней мере по моим тестам никакого прироста производительности по сравнению с spec_rstack_overflow=microcode я не получил.

Всё же остаётся вопрос, почему в ядрах 6.2 и 6.3 этого security-патча нет, а в 6.1 есть? Всё просто, 6.1 — Long Term Support (LTS) релиз, а 6.2 и 6.3 — End Of Life (EOL).

Влияние каждого параметра по отдельности

Я решил проверить, насколько сильно каждый kernel arg параметр влияет производительность I/O, вот что получилось:

Помимо всего прочего, я очень рекомендую на любых Bare Metal инсталляциях использовать Performance CPU Governor (kernel аргумент cpufreq.default_governor=performance). Также интересно, что новый scaling драйвер amd_pstate не повлиял на I/O. От mitigations=off также нет никакого прироста по сравнению с отключением SRSO-патча.

Итог

Благодаря такому вот примитивному тюнингу мне удалось вернуть производительность I/O в Talos Linux на уровень ванильного Debian, как бы иронично это не звучало. Ну или почти удалось:

По всем остальным тестам паритет, а вот в Rand 4K T1Q128 Talos Linux отстаёт на 70K IOPS (14%).

Ядро Talos Linux сконфигурировано согласно гайдлайнам KSPP (Kernel self-protection project), поэтому там включен Page Table Isolation (PTI). Отключить его Talos Linux не позволяет (и правильно делает), а вот в Debian я решил его включить и проверить влияние PTI на I/O — производительность снизилась с 520K IOPS до 480K.

Вот итоговый набор kernel args, который может "разблокировать" иопсы:

machine:
  install:
    extraKernelArgs:
      - cpufreq.default_governor=performance
      - amd_pstate=active
      - iommu=off
      - spec_rstack_overflow=microcode

У меня нет виртуализации, поэтому я решил отключить iommu вовсе, что дало ещё небольшой прирост.

Я не сомневаюсь что есть ещё уйма способов затюнить Linux, но это уже за гранью моих текущих знаний, да и в целом я доволен результатом.

Бонус

В качестве хранилища в Kubernetes я решил использовать одно из самых производительных решений на текущий момент — Linstor. Для Kubernetes есть удобный оператор — Piraeus Operator.

Я создал два StorageClassnvme-lvm-local и nvme-lvm-replicated-async. В первом случае мы монтируем thick LVM volume непосредственно в под, не используя при этом DRBD репликацию, поскольку множество современных СУБД умеют сами реплицироваться и лучше этим пользоваться, поскольку это более эффективный подход. Второй же использует асинхронную DRBD репликацию на другой сервер. Чаще всего такой подход используется для приложений, которые сами реплицироваться не умеют.

В случае с DRBD-репликацией поды всегда работают с данными локально благодаря настройкам volumeBindingMode: WaitForFirstConsumer и linstor.csi.linbit.com/allowRemoteVolumeAccess: "false", что позволило выжать максимальную производительность, не смотря на репликацию.

Результаты получились следующими:

Google-таблица с подробностями находится тут.

Спасибо за внимание!

Комментарии (17)


  1. UnknownCat
    22.10.2024 13:01

    Иронично

    Ой, удалите плиз, я перепутал email


  1. Preveder
    22.10.2024 13:01

    В случае с SDS будет весьма полезно. Спасибо большое


  1. Yamuzakopal
    22.10.2024 13:01

    регулятор частоты в режиме performance будет всегда выкручивать частоту процессора на максимум. На постоянной основе использовать не стоит. Только для получения максимальных баллов в бенчмарках. На дальних дистанциях в таком режиме от перегрева начнется троттлинг и вся производительность упадет. А при долгом использовании питальник на плате быстро деградирует. Пол года-год и пробьет. Вдруг кто из незнающих прочтет, будет предупреждением!


    1. maxpain Автор
      22.10.2024 13:01

      Это не так, если речь идёт о современных процессорах.

      При использовании amd_pstate все простаивающие ядра опускаются до 400мгц, а TDP перераспределяется между загруженными ядрами, позволяя им буститься до более высоких частот. Без использования performance cpu governor ядра не так быстро выходят из спячих состояний, соответственно latency в системе становится хуже.

      Никаких деградаций процессора, троттлинга и т.д. не будет.


  1. powerman
    22.10.2024 13:01

    Статья очень интересная, но при попытке воспроизвести это на десктопе (AMD Ryzen 9 5900X, Samsung SSD 980 PRO) результаты получились несколько иные: с spec_rstack_overflow=microcode IOPS случайного доступа действительно сильно (в полтора раза!) увеличиваются, линейная запись почти не меняется, но вот IOPS (как и BW) линейного чтения падает ещё сильнее (в 3 раза!). И для десктопа такой размен уже выгодным не выглядит, тем более что ещё и уязвимость дополнительная добавляется.

    Команды я запускал вот такие:

    fio -size=10G -ioengine=libaio -direct=1 -invalidate=1 -name=test -bs=4M -iodepth=32 \
        -rw=read -runtime=60 -output="$1-read.txt"
    fio -size=10G -ioengine=libaio -direct=1 -invalidate=1 -name=test -bs=4M -iodepth=32 \
        -rw=write -runtime=60 -output="$1-write.txt"
    fio -size=10G -ioengine=libaio -direct=1 -invalidate=1 -name=test -bs=4k -iodepth=128 \
        -rw=randread -runtime=60 -output="$1-randread.txt"
    fio -size=10G -ioengine=libaio -direct=1 -invalidate=1 -name=test -bs=4k -iodepth=128 \
        -rw=randwrite -runtime=60 -output="$1-randwrite.txt"

    Без этой опции ядра cat /sys/devices/system/cpu/vulnerabilities/spec_rstack_overflow выдает "Mitigation: Safe RET", с ней "Vulnerable: Microcode, no safe RET".


    1. maxpain Автор
      22.10.2024 13:01

      Пробовали spec_rstack_overflow=off? А ещё интересно посмотреть на результаты с mitigations=off. Всё-таки процессор более старый у вас (zen 3).


      1. powerman
        22.10.2024 13:01

        Мистику с замедлением линейного чтения в 3 раза я прояснил. Такие высокие результаты теста на линейное чтение получаются только при условии, что тестируемый файл (`test.0.0` на 10 GB) ещё не существует и создаётся в момент запуска первого теста (а это как раз линейное чтение). Полагаю, с моими 32 GB RAM, тут где-то проявляются кеши линуха. И -direct=1 -invalidate=1 почему-то не спасает. Update: А ещё этот же эффект (ускорение линейного чтения в 3 раза) можно получить если просто поменять первые два теста (линейное чтение и запись) местами.

        spec_rstack_overflow=off относительно spec_rstack_overflow=microcode добавляет 5-12% везде кроме линейного чтения.

        mitigations=off относительно spec_rstack_overflow=off добавляет ещё 2-4% на случайном доступе а вот линейная запись почему-то заметно (30%) замедлилась.

        В целом, относительно системы по умолчанию (все защиты активны), mitigations=off даёт выигрыш в 68% на случайном чтении/записи, и потерю 10% на линейной записи.

        Ещё я провёл более реалистичный тест: сборку ядра линуха (make -j24 сразу после загрузки в single-user mode) . Относительно системы по умолчанию (все защиты активны), mitigations=off даёт выигрыш в 3.5%. Что как бы намекает, что этот выигрыш в 68% IOPS на случайном доступе - всё-таки синтетический тест, и в реальных приложениях разница скорее всего будет ближе к 3.5%.


  1. sunnybear
    22.10.2024 13:01

    В продолжение темы:
    https://devopsconf.io/moscow-rit/2019/abstracts/5219


  1. karavan_750
    22.10.2024 13:01

    Talos Linux, которая позволяет очень просто развернуть Kubernetes-кластер на любом окружении, легко обновлять его компоненты и поддерживать конфигурацию в одинаковом состоянии на всех узлах кластера благодаря декларативности и иммутабельности.

    А можете упомянуть, какие еще ОС с подобными свойствами рассматривались? Если по этим критериям осуществлялся поиск и выборка.


    1. maxpain Автор
      22.10.2024 13:01

      Из подобных никакие не использовал. Знаю что есть CoreOS и Flatcar, но по опыту моих коллег они ещё очень далеки от Talos Linux.


  1. dartraiden
    22.10.2024 13:01

    Так как я всегда обновляю микрокод процессоров благодаря официальным Talos extensions amd-ucode и intel-ucode, собирая кастомные образы на factory.talos.dev, я могу себе позволить отключить софтверный патч этой уязвимости

    Микрокод защищает лишь от атак в направлении пользователь->ядро и гость->хост. Но не защищает от пользователь->пользователь и виртуалка->виртуалка, для этого нужна программная заплатка.

    Текущий статус защиты можно посмотреть в /sys/devices/system/cpu/vulnerabilities/spec_rstack_overflow

    При отсутствии программной заплатки будет статус "Vulnerable: Microcode, no safe RET".


    1. maxpain Автор
      22.10.2024 13:01

      Хм, верно, у меня также.

      Спасибо! Поправлю статью.


  1. gotch
    22.10.2024 13:01

    ChatGPT проанализировала diff файл, и выдала точный ответ, какая именно настройка влияет на разницу в производительности?

    Человечество обречено.


    1. karavan_750
      22.10.2024 13:01

      Как у члена группы, которую вы назвали обреченной, у меня вопрос. А что не так в процедуре экономии личного времени при делегировании решения задачи машинам?


      1. gotch
        22.10.2024 13:01

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


        1. karavan_750
          22.10.2024 13:01

          задачи, где надо подумать

          По моему, вы спутали задачу перебора с задачей на размышления.


          1. gotch
            22.10.2024 13:01

            Искать параметр в конфиге ядра, который так сильно убивает производительность, это как искать иголку в стоге сена, поскольку в kernel config'e более 6000 строк, а если файла два? Как понять какой именно параметр влияет?