Шифрование оперативной памяти на лету выглядит как операция, которая обязана замедлить любой сервер, ведь каждый байт должен пройти через AES до записи в DRAM. На процессорах AMD эту работу выполняет контроллер памяти, а вычислительные ядра продолжают видеть открытые данные в кэшах и регистрах, поэтому цена шифрования зависит не от программы, а от того, как часто данные покидают кристалл. Второй слой приносит SEV-SNP: таблица Reverse Map Table и метки целостности проверяются при первом обращении к каждой частной странице гостя. На практике накладные расходы измеряются единицами процентов для большинства нагрузок, и это легко проверить на любой машине штатными утилитами. Далее разобран механизм SME, SEV и SEV-SNP, порядок включения шифрования на хосте, методика замера командами sysbench, mbw и fio, типичные результаты и приемы, которые возвращают часть потерянной скорости.
Механизм шифрования SME и место, где рождается потеря скорости
Технология Secure Memory Encryption появилась на серверных процессорах EPYC первого поколения в 2017 году. Страница помечается в таблице страниц специальным битом шифрования, его позицию сообщает инструкция CPUID с номером 0x8000001f: бит 0 регистра EAX говорит о поддержке SME, бит 1 о SEV, бит 4 о SEV-SNP, а младшие разряды EBX хранят номер бита в элементе таблицы страниц. При чтении такой страницы из DRAM контроллер расшифровывает ее, при записи обратно шифрует, и это делает аппаратный блок на базе AES-128-XTS внутри кристалла. Данные в кэшах L1, L2 и L3 и в регистрах остаются открытыми, поэтому горячий рабочий набор, целиком помещающийся в кэш, шифрования почти не замечает. SEV добавляет к схеме персональный ключ для каждой виртуальной машины: ключ выбирается по номеру ASID, память гипервизора шифруется своим ключом, а гостевые страницы делятся на частные и разделяемые. Расширение SEV-ES выносит при выходе в гипервизор состояние процессора в защищенный блок GHCB, а SNP доводит защиту до самой памяти, привязывая каждую страницу к конкретной машине.
Потеря скорости возникает ровно там, где данные идут в DRAM и обратно. Числовой ориентир считается на пальцах: при полосе памяти 200 ГБ/с контроллеру приходится обрабатывать около 48,8 млн страниц по 4 КиБ в секунду, ведь деление 200 млрд байт на 4096 дает примерно столько. Если приложение прогоняет через память большие массивы, например сжимает резервные копии, сортирует таблицы базы данных или обслуживает сетевые очереди, каждый доступ упирается в шифратор, и разница становится измеримой. Компиляция, работа интерпретаторов и счет в регистрах почти не платят, потому что соответствующие данные вычитываются из DRAM один раз и остаются на кристалле.
Проверка поддержки процессора и включение шифрования на хосте
Поддержка видна в стандартных флагах процессора, а ядро печатает ее в журнале загрузки:
grep flags /proc/cpuinfo | head -n1 | grep -o -E 'sme|sev|sev_es|sev_snp' | sort -u
dmesg | grep -i 'memory encryption'
Тот же факт проверяется напрямую через регистры MSR: бит 23 регистра 0xc0010010 включает механизмы шифрования, бит 0 регистра 0xc0010131 показывает, что SEV активен. Утилита rdmsr из пакета msr-tools читает значения без какой-либо правки:
modprobe msr
rdmsr 0xc0010010
rdmsr 0xc0010131
Ключевая деталь в том, что ядро доверяет включение бита в регистре SYSCFG прошивке платформы. Если в BIOS опция не активирована, ядро не станет включать ее сам, и параметр mem_encrypt=on не сработает. Когда прошивка все сделала, шифрование включается параметром ядра:
sed -i 's/^GRUB_CMDLINE_LINUX_DEFAULT="/GRUB_CMDLINE_LINUX_DEFAULT="mem_encrypt=on /' /etc/default/grub
grub2-mkconfig -o /boot/grub2/grub.cfg
CPUID 0x8000001f в битах с 6 по 11 регистра EBX сообщает, на сколько бит сузится физическое адресное пространство при активном шифровании, на типичных системах это один бит. За гостевые системы отвечают опции модуля kvm_amd, их состояние видно в файлах параметров:
cat /sys/module/kvm_amd/parameters/sev
cat /sys/module/kvm_amd/parameters/sev_es
cat /sys/module/kvm_amd/parameters/snp
Зафиксировать настройки навсегда можно в файле /etc/modprobe.d/kvm.conf:
options kvm_amd sev=1 sev_es=1 snp=1
Методика замера, свободная от влияния фоновых процессов
Сравнивать нужно одну и ту же машину с выключенным и включенным шифрованием, иначе в разницу вплетутся версии прошивки и разные планки памяти. Перед прогонами стоит зафиксировать профиль энергопотребления и сбросить кэши страниц в одинаковое состояние:
cpupower frequency-set -g performance
sync && echo 3 > /proc/sys/vm/drop_caches
Полосу памяти показывает sysbench, копирование и чувствительность к латентности mbw, а fio на файловой системе в памяти прокачивает страницы через ядро без диска:
sysbench memory --memory-block-size=1M --memory-total-size=50G --memory-oper=write --threads=8 run
mbw 512 | grep AVG
fio --name=r1 --filename=/dev/shm/fio.bin --size=8G --rw=read --bs=1M --direct=0 --time_based --runtime=30
Итоговая строка fio выглядит примерно так:
read: IOPS=9200, BW=9201MiB/s (9652MB/s)(270GiB/30002msec)
Каждый тест полезно повторить три раза и взять среднее, поскольку одиночный прогон ловит всплески от служб и миграций страниц. Шум от соседних потоков убирают привязкой к зафиксированным ядрам:
taskset -c 4-11 sysbench memory --memory-block-size=1M --memory-total-size=50G run
mbw 256 | grep AVG
Строка из вывода mbw короткая, но говорит о многом:
Method: MEMCPY Measured: 8324.4MiB/s
Отдельный ориентир дает софтверный AES-XTS:
openssl speed -evp aes-128-xts
Современный процессор с векторными расширениями шифрует на одном потоке гигабайты в секунду, поэтому требование к аппаратному блоку поддерживать темп всей шины памяти выглядит выполнимым, и замер как раз показывает, так это или нет на конкретной плате.
Если базовый прогон sysbench дал 9,4 ГБ/с, а с включенным SME полоса упала до 8,9 ГБ/с, потери составили около 5 процентов, и такая картина для потоковой записи вполне типична. Доверять стоит только паре прогонов на одном железе и в одной сессии, а не цифрам из чужих обзоров на другой прошивке.
Таблица Reverse Map Table и цена целостности в SEV-SNP
SEV-SNP требует таблицы Reverse Map Table, по одной записи на каждую страницу памяти, которую гипервизор теоретически может отдать гостю. Запись занимает 16 байт, перед записями лежит служебная область на 16 КиБ, а границы таблицы задают регистры 0xc0010132 и 0xc0010133, прошивка выравнивает их с шагом в 1 МиБ. Арифметика простая: 16 байт на каждые 4 КиБ памяти это ровно 1/256 ее объема, поэтому хост с 512 ГиБ отдает под таблицу 2 ГиБ, система с 1 ТиБ уже 4 ГиБ, и все это вырезается из памяти, видимой операционной системе.
Метка целостности хранится вместе с записью и накрывает содержимое страницы, проверку выполняет процессор при первом обращении к странице после назначения ее гостю. Валидацию проводит сам гость инструкцией PVALIDATE, а при работе на уровне привилегий выше нулевого запросы уходят модулю SVSM внутри защищенного контура. Размер валидируемой страницы выбирает гость, и разница огромна: 64 ГиБ памяти это 16,7 млн страниц по 4 КиБ или 32 768 страниц по 2 МиБ, то есть в 512 раз меньше вызовов на старте виртуальной машины. Переключение владения страницей между гипервизором и гостем требует инструкции RMPUPDATE, а обращение гостя к разделяемой памяти в защищенном режиме проходит через шлюз исключения #VC и буфер GHCB. Частые мелкие обмены с устройствами множат такие переходы, и именно они объясняют разброс результатов между внешне похожими нагрузками.
На многоузловых платформах помогает сегментированный RMP: поддержка видна в CPUID 0x80000025, регистр 0xc0010136 задает размер покрытия сегмента, например значение 0x2401 означает сегмент на 64 ГиБ. Смысл в том, чтобы записи лежали на том же NUMA узле, что и покрытая ими память, иначе чтение записи с удаленного узла растягивается по шине между сокетами и добавляет наносекунды к каждому обращению к новой странице.
Числа, которые показывают замеры, и разумные ожидания
Вычислительные нагрузки, где данные живут в кэшах, реагируют слабее всего, разница укладывается в доли процента. Потоковые тесты с насыщением всех каналов памяти платят больше всех: на типичных серверных конфигурациях падение полосы измеряется единицами процентов, а на неудобных паттернах записи доходит до десятка. Латентность единичных обращений меняется едва заметно, потому что строка шифруется уже после того, как кэш принял данные. Виртуальные машины с SEV теряют еще единицы процентов к собственной нешифрованной копии, а SEV-SNP добавляет сверху проверки RMP и обработку исключений виртуализации через канал GHCB.
Самый чувствительный показатель это время старта гостя с большим объемом памяти: валидация страниц по 4 КиБ на десятках гигабайт превращается в десятки секунд чистой работы процессора, и переход на страницы по 2 МиБ заметно сокращает паузу. Сводка типичных прогонов выглядит скромно: sysbench теряет от 2 до 6 процентов, mbw от 1 до 4, fio на tmpfs от 3 до 7, а старт гостя на 64 ГиБ со страницами по 4 КиБ растягивается в разы сильнее, чем со страницами по 2 МиБ. Запас пропускной способности у шифратора растет с поколениями, на конфигурациях с восемью каналами памяти старых платформ его порой не хватало, новые семейства держат темп заметно увереннее. Хороший тон сравнивать не только среднее, но и хвосты: разница в 99 перцентиле латентности для сервисов с жесткими таймаутами порой важнее средней полосы.
Практические приемы, которые возвращают часть потерянной скорости
Первый прием это большие страницы: они экономят записи TLB и в сотни раз сокращают число валидаций PVALIDATE. Резерв под них выделяется на лету или через параметр ядра:
sysctl -w vm.nr_hugepages=16384
grep Huge /proc/meminfo
Резерв в 16384 страницы по 2 МиБ дает 32 ГиБ готовой памяти, значение подбирается под суммарный объем гостей. Второй прием это привязка нагрузок к NUMA узлам, она уменьшает трафик между сокетами и шанс наткнуться на удаленные записи RMP:
numactl --hardware
numactl --cpunodebind=0 --membind=0 sysbench memory --memory-block-size=1M --memory-total-size=50G run
Третий прием организационный: не включать шифрование там, где секрета нет. Хост с общедоступными сервисами может работать с выключенной опцией, а защищать стоит только гостевые системы, которые действительно держат чувствительные данные. Буферы для обмена с устройствами полезно делать крупными и редкими, каждое переключение между частной и разделяемой памятью стоит обращений к таблицам и копирования. Свежая прошивка платформы сокращает время генерации ключей и аттестации при старте гостей, на больших парках виртуальных машин это заметно ускоряет планирование запусков. Готовность гипервизора к защищенным гостям перед миграцией проверяется одной командой:
virsh domcapabilities | grep -A3 launchSecurity
Чек-лист перед включением защищенной памяти в продакшене
Перед включением на боевом хосте порядок действий укладывается в шесть шагов:
1. убедиться в флагах sme, sev, sev_es и sev_snp в /proc/cpuinfo и обновить прошивку платформы;
2. включить опции шифрования в BIOS и зафиксировать параметры модуля kvm_amd в /etc/modprobe.d;
3. снять базовую линию sysbench, mbw и fio до активации шифрования;
4. включить шифрование и повторить прогоны на тех же параметрах и том же железе;
5. посчитать объем RMP делением объема памяти на 256 и проверить, что резерв не заденет планы по памяти;
6. выбрать страницы валидации по 2 МиБ и привязать гостей к NUMA узлам.
Плата за шифрование памяти оказывается предсказуемой величиной, которую администратор измеряет за вечер на своей машине, а не принимает на веру. Понимание того, где именно рождаются потери, превращает абстрактный страх перед AES в конкретный план: горячие данные в кэше, страницы по 2 МиБ, аккуратная привязка к узлам и честная базовая линия до включения. С такой подготовкой защищенный контур SEV-SNP обходится в единицы процентов и оправдывает себя каждый раз, когда гостевая система хранит действительно секретные данные.