В современных серверах устанавливается очень большой объем памяти. Иногда модули памяти ломаются и при ошибке сервер перезагружается. Если повезет, то умный системный контроллер подсветит неисправный модуль памяти, но может и не подсветить, тогда нужно искать, переустанавливая модули. Ситуация с перезагрузками сервера повторяется редко, но каждый раз это очень больно для бизнес-критичных приложений.
Для диагностики модулей есть хорошая программа 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 нужны следующие команды:
-
map покажет, какие FAT-совместимые файловые системы у нас есть;
-
fs0: (с двоеточием) переключится на первую файловую систему (ранее она у нас была смонтирована в /boot/efi);
-
cd EFI/memtest поменяет текущую папку на ту, куда вы положили memtest (тут даже работает автодополнение TAB, удобно);
-
ls позволит посмотреть, какие файлы есть в текущей папке;
-
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="memtest.x64.efi.6.20"
memtest_kernel="memtest.x64.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 /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 часов, что существенно быстрей, чем на barre-metall.
Выводы
Сравним два описанных метода.
Бесспорно, метод с нативной загрузкой memtest выполняет свою работу качественней, но он это делает неприемлемо медленно для больших объемов памяти, возможности по параллельному тестированию используются не полностью, а процессор не загружается целиком.
Об этом можно косвенно судить даже по скорости оборотов вентиляторов сервера. Во время нативного исполнения memtest их скорость отображалась как 10К RPM, а с libvirt они же разогнались до 20K RPM. "Здесь мерилом работы считают усталость", - посмеются некоторые, но в случае с нативным исполнением memtest сервер никак не выдает метрики загрузки своих процессоров; приходится выкручиваться.
Метод тестирования через виртуальные машины охватывает ~97% памяти, нагружает процессоры целиком (зафиксировано с node_exporter), и в большинстве случаев этот результат будет достаточным, чтобы с стресс-тесте отбраковать неисправный модуль. Основной упор на то, что метод должен быстро воспроизводить неисправность. Ведь серия тестов может быть длинной, возможно придется несколько раз менять конфигурацию памяти в сервере перед тем, как мы найдем "виновный" модуль.
Проверка с применением виртуализации потенциально может выявить какие-то проблемы, но она абсолютно не гарантирует, что вся память исправна. Зачем тогда проверять? Затем, что этот метод раскрывает себя хорошо на системах, которые иногда сами сбоят из-за ошибок в памяти. Данный метод позволяет относительно быстро локализовать неисправность, повышая её воспроизводимость.