В современных серверах устанавливается очень большой объем памяти. Иногда модули памяти ломаются и при ошибке сервер перезагружается. Если повезет, то умный системный контроллер подсветит неисправный модуль памяти, но может и не подсветить, тогда нужно искать, переустанавливая модули. Ситуация с перезагрузками сервера повторяется редко, но каждый раз это очень больно для бизнес-критичных приложений.

Для диагностики модулей есть хорошая программа memtest86+, но если памяти у нас 1ТБ, то полное тестирование растягивается на несколько дней, а бизнес не может так долго ждать.

Как же быть? В этой публикации я поделюсь опытом тестирования памяти сервера Gigabyte R292-4S0 с СУБД на Enteprice Linux 8 (EL8) и 1 ТБ памяти двумя методами:

  • С EFI загрузкой memtest86+ v7;

  • С автоматизированным созданием сотни libvirt-KVM виртуальных машин с memtest86+ внутри.

Запуск memtest внутри виртуальной машины... "Фу...", - скажут некоторые. И будут неправы:

  • На Хабре описан успешный пример такого запуска в VirtualBox;

  • У меня есть положительный опыт с автоматизированной PXE загрузкой сотни виртуальных машин с memtest в ESXi. Гипервизор с ошибкой в памяти падал за минуты, пока за несколько тестов мы не вычислили неисправный модуль. А с рабочими ("боевыми") виртуальными машинами сбой проявлялся раз в неделю;

  • Такое тестирование по моему подсчету охватывает ~97% памяти, и велик шанс того, что именно тут есть сбойные ячейки;

  • Такое тестирование существенно быстрей.

Приступим.

Нам понадобится сам memtest86+, его можно найти на сайте https://memtest.org. Можно было бы попробовать установить его через yum. Но, увы, в тестируемом мной EL8 так установился только memtest86+ v5.01, который EFI загрузку вообще не поддерживает. Нужна версия memtest 6 или новее.

EFI загрузка memtest+

Скачиваем Binary Files (.bin/.efi), распаковываем memtest64.efi в созданную папку /boot/efi/EFI/memtest (/boot/efi - примонтированная ФС VFAT). Мы готовы к EFI-загрузке .

Как загрузить memtest через EFI-загрузчик? На тестируемом сервере Gigabyte во время запуска нажимаем F10, попадаем в меню выбора загрузочного устройства, в нем выбираем EFI-shell.

В EFI-shell нужны следующие команды:

  1. map покажет, какие FAT-совместимые файловые системы у нас есть;

  2. fs0: (с двоеточием) переключится на первую файловую систему (ранее она у нас была смонтирована в /boot/efi);

  3. cd EFI/memtest поменяет текущую папку на ту, куда вы положили memtest (тут даже работает автодополнение TAB, удобно);

  4. ls позволит посмотреть, какие файлы есть в текущей папке;

  5. memtest64.efi - имя скачанного бинарного файла, который нужно запустить.

После этого загрузится сам memtest, в моем случае он еще несколько минут просто так показывал зависший экран и ничего не делал. Видимо, определял объем работы. Затем работа пошла и продолжалась... 95 часов (4 дня) до первого прохода!

libvirt-KVM загрузка memtest86+

Для загрузки memtest внутри виртуальных машин нам потребуется добавить в наш EL8 дополнительные утилиты для управления kernel-virtual-machine. Запустим сразу libvirtd.service и уберем поднятый внутренний сетевой мост default - сеть нам тут не пригодится.

yum install -y qemu-kvm libvirt virt-install virt-manager virt-viewer qemu-img 
systemctl enable --now libvirtd.service
virsh net-autostart default --disable 
virsh net-destroy default 

Создадим пустую папку, например, /opt/vm-memtest, положим в нее уже знакомый memtest64.efi. Далее в ней же необходимо создать пустой файл empty.

touch empty

В этой же папке создадим скрипт запуска, который будет использовать подобную команду:

virt-install -n memtest1 \
            --memory 12288 \
            --vcpus 2 \
            --disk "none" \
            --network "none" \
            --os-variant "detect=off,name=generic" \
            --location "/opt/vm-memtest,kernel=memtest64.efi,initrd=empty" \
            --noautoconsole \
            --serial file,path=/opt/vm-memtest/console-out-vm-memtest1 \
            --extra-args "console=ttyS0,115200 console=tty0"

Разберем, что она делает:

  • задаст имя новой виртуальной машины - memtest1;

  • выделит 12GB RAM;

  • выделит 2vCPU;

  • не будет выделять диск;

  • не будет подключать сетевые адаптеры;

  • отключит контроль успешной загрузки ОС со стороны virt-install;

  • запустит ядро из /opt/vm-memtest/memtest64.efi с пустым initrd файлом (без него virt-install не получится, empty - это костыль);

  • не будет открывать консоль виртуальной машины в virt-viewer;

  • подключит COM-порт из файла /opt/vm-memtest/console-out-vm-memtest1 (файл будет создан автоматически);

  • ядру ОС виртуальной машины будут переданы параметры загрузки о том, что свой вывод нужно дублировать как в указанный COM-порт (файл), так и на консоль tty0 (к ней можно подключиться через virt-viewer, если захочется).

Объём памяти в 12ГБ появился удвоением (2vcpu в вм) деления количества памяти в системе на общее количество vcpu в 4-х сокетном сервере.

До создания виртуальных машин нужно остановить "боевые" приложения и не забыть, что СУБД (если она у вас есть) должна вернуть свои huge pages в ОС, отключить swap, а еще сбросить все КЭШи из памяти на диск:

swapoff -a
sysctl -w vm.nr_hugepages=0
sync
echo 3 > /proc/sys/vm/drop_caches

Конечный скрипт запуска получился такой:

vm-manage.sh
#!/bin/bash

# Written by Alex Golikov 2023.11.22

## Prepare host to provide KVM service

# yum install -y qemu-kvm libvirt virt-install virt-manager virt-viewer qemu-img 
# systemctl enable --now libvirtd.service
# virsh net-autostart default --disable 
# virsh net-destroy default 

## To get Memory per One vCPU in this system run:
# awk '/MemTotal/{ printf("%.0f\n",$2/1024/'$(grep -c processor /proc/cpuinfo)') }' /proc/meminfo

vcpu_per_vm=2
memory_per_vm=12288
vmprefix="memtest"

## uncomment to limit VM number
#max_vm_number=3

#create lastvm to spend all the rest available memory
lastvm=true

#memtest_kernel="memtestx64.efi.6.20"
memtest_kernel="memtestx64.efi"

startvm() {
 
   #avail_mem=$(awk '/MemAvailable/{ printf("%.0f",$2/1024) }' /proc/meminfo)
   avail_mem=$(free -m | awk '/Mem/{print $7}')

   #calculate vm_num (number of VMs to create)
   vm_num=$(($avail_mem / $memory_per_vm))
   [[ -z "${vm_num}" ]] && echo "Unable to calculate vm_num (number of VMs to create)" && exit 1

   #Check vm limit variable
   [[ -n ${max_vm_number} ]] && [[ ${vm_num} -gt ${max_vm_number} ]] && vm_num=${max_vm_number}

   #Check current memtest VMs that have already created before
   current_vm_number=$(virsh list --all| awk '{if($2~/'${vmprefix}'/) {print $2}}' | sed 's|'${vmprefix}'||' | sort -n | tail -1)
   [[ -z "${current_vm_number}" ]] && current_vm_number=0

   echo "Creating ${vm_num} VMs with ${memory_per_vm}MB onboard to test ${avail_mem}MB of RAM."

   for (( i=$((${current_vm_number}+1)) ; i<=$((${vm_num} + ${current_vm_number})); i++ )); do
      echo -e "##############\n\nCreating ${vmprefix}${i}..."
      virt-install -n ${vmprefix}${i} \
                --memory ${memory_per_vm} \
                --vcpus ${vcpu_per_vm} \
                --disk "none" \
                --network "none" \
                --os-variant "detect=off,name=generic" \
                --location "${MyDir},kernel=${memtest_kernel},initrd=empty" \
                --noautoconsole \
                --serial file,path=${MyDir}/console-out-vm-${vmprefix}${i} \
                --extra-args "console=ttyS0,115200 console=tty0"
   done
}


stopvm() {

        for vm in $(virsh list --all | awk '{if($2~/'${vmprefix}'/) print $2}'); do
                echo -e "##############\n\nRemoving $vm"
                virsh destroy $vm 
                virsh undefine $vm 
        done
        echo -e "##############\n\nWork completed"

}


checklogs() {
        for console_out in $(ls -1 console-out-vm-*); do
                echo -en "\n${console_out}        "
                sed -n 's/.*\(Time: [ 0-9:]*\).*\(Pass: [ 0-9]*\).*\(Errors: [0-9]*\).*/\1 \2 \3/p' $console_out
        done
        echo -e "\n\nRun command 'rm -f ${MyDir}/console-out-vm-*' to remove VM console logs"
}

[[ "$1" =~ start|stop|check ]] || { echo "Usage: $0 {start|stop|check}"; exit 1; }

MyDir=$(dirname "$0") && cd "${MyDir}" && MyDir="$(pwd)"

[[ "$1" == "start" ]] && {
        startvm
        [[ "${lastvm}" == "true" ]] && {
              memory_per_vm=$(free -m | awk '/Mem/{print $7 - 100 }')
              #memory_per_vm=$(awk '/MemAvailable/{ printf("%.0f",$2/1024 - 100) }' /proc/meminfo)

              #Create one more VM if we have at least 500MB of RAM left
              [[ "${memory_per_vm}" -gt 500 ]] && {
                   echo "Spending the last ${memory_per_vm}MB"
                   startvm 
              }
        }
        echo -e "##############\n\nWork completed:"
        virsh list --all | awk '{if($2~/'${vmprefix}'/) print $0}'
        free -m
        }
[[ "$1" == "stop"  ]] && stopvm
[[ "$1" == "check" ]] && checklogs

Команда запуска

./vm-manage.sh start 

последовательно запустит почти сотню виртуальных машин:

Команда анализа выводов в консолях виртуальных машин:

./vm-manage.sh check

Команда остановки всех созданных виртуальных машин:

./vm-manage.sh stop

Память во всех запущенных виртуальных машинах была протестирована за 5 часов, что существенно быстрей, чем на bare-metal.

Выводы

Сравним два описанных метода.

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

Об этом можно косвенно судить даже по скорости оборотов вентиляторов сервера. Во время нативного исполнения memtest их скорость отображалась как 10К RPM, а с libvirt они же разогнались до 20K RPM. "Здесь мерилом работы считают усталость", - посмеются некоторые, но в случае с нативным исполнением memtest сервер никак не выдает метрики загрузки своих процессоров; приходится выкручиваться.

Метод тестирования через виртуальные машины охватывает ~97% памяти, нагружает процессоры целиком (зафиксировано с node_exporter), и в большинстве случаев этот результат будет достаточным, чтобы с стресс-тесте отбраковать неисправный модуль. Основной упор на то, что метод должен быстро воспроизводить неисправность. Ведь серия тестов может быть длинной, возможно придется несколько раз менять конфигурацию памяти в сервере перед тем, как мы найдем "виновный" модуль.

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

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


  1. brammator
    01.12.2023 14:07
    +3

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


  1. INSTE
    01.12.2023 14:07
    +2

    А почему бы просто кучу qemu-system-x86_64 из cli не запустить? libvirt кажется избыточным.


    1. n27051538 Автор
      01.12.2023 14:07
      +2

      Libvirt - по-привычке.
      Возможно действительно можно будет убрать эту прослойку. Хорошая идея.


      1. kt97679
        01.12.2023 14:07

        Если у вас будет время написать скрипт без libvirt лично мне было бы очень интересно посмотреть на результат.


  1. ruata
    01.12.2023 14:07
    +1

    ИМХО есть варианты попроще Prime95 в режиме Torture Test или обычный stress с нужным набором --vm --vm-bytes --vm-stride --vm-hang --vm-keep или еще варианты https://wiki.archlinux.org/title/Stress_testing#Discovering_Errors
    Если памяти много с виртуалками непонянто в каком именно модуле сбой
    memtest на физике может указать вполне конкретный модуль и без management платы (да и не каждая укажет) можно конечно разобрать и через MCE в современных CPU + mcelog


  1. iminfinitylol
    01.12.2023 14:07

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

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


    1. n27051538 Автор
      01.12.2023 14:07
      +1

      Да, память ломается значительно реже, чем диски. Но ломается все. И enterprice сервера (mb) и их бп, и вентиляторы и память в них. Ломается память даже в контроллере СХД. ВСЕ это видел многократно. В продвинутых серверах и СХД сбой памяти не приводит к простою в обслуживании (и такое видел).

      А вот поломок десктопов наоборот не видел (просто я с ними не работаю). Но я не утверждаю, что они не ломаются.

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


    1. Comraddm
      01.12.2023 14:07

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


  1. Comraddm
    01.12.2023 14:07

    Файл EFI называется memtest64.efi, а в скрипте он идет как memtest.x64.efi.

    Надо бы подправить.

    А еще попробовал повторить на Almalinux 9.3, ругается при запуске:

    ERROR Unable to open file: /opt/vm-memtest/console-out-vm-memtest1: Permission denied

    Если закомментировать строчку

    --serial file,path=${MyDir}/console-out-vm-${vmprefix}${i} \

    то запускается.


    1. n27051538 Автор
      01.12.2023 14:07
      +1

      Возможно там есть нюанс с SELinux. На моей системе он был отключен. Попробуйте его отключить тоже. Вероятно ему контекст консольного файла не нравится.


      1. Comraddm
        01.12.2023 14:07

        Вы правы. После отключения SELinux все работает!

        Вижу, вы исправили имя EFI в скрипте, но не до конца, надо еще убрать лишний x, чтобы стало memtest64.efi :)

        И вопрос про методику тестирования. Допустим, гипервизор упал в процессе тестирования. Что делать дальше?

        Запускать половину виртуальных машин с memtest и тестировать половину памяти или вынимать половину планок памяти физически и тестировать?