Конфиденциальные вычисления на ARM перестали быть обещанием со слайдов: вместе с архитектурой ARMv9-A пришло расширение Realm Management Extension, которое отводит гостевой системе собственный изолированный мир с шифрованием памяти на уровне железа. Хост в такой схеме физически не может прочитать страницы гостя, а расплачивается за это скоростью: каждая страница проходит через прошивочный монитор, каждый выход виртуального процессора делает лишний прыжок, память шифруется на лету. Насколько дорога подобная безопасность в цифрах? Брать ответ на веру нельзя, потому что расклад зависит от типа нагрузки: на тяжелом счете realm-машина почти не отличается от обычной, а на сетевом микротрафике отставание становится заметным. Дальше разобраны три источника накладных расходов RME, собран стенд для честного сравнения с обычной KVM и приведены команды замеров, которые повторяются на любом сервере с поддержкой конфиденциальных вычислений.
Realm Management Extension в ARMv9-A и три источника цены изоляции
Realm Management Extension входит в архитектуру Arm Confidential Compute Architecture. Архитектурное расширение задает новые миры безопасности, а спецификация DEN0137 описывает монитор Realm Management Monitor, через который идет вся работа с realm. К привычным состояниям secure и non-secure добавляется realm-мир, а для прошивки выделяется корневое адресное пространство, недоступное никому другому. Память realm шифрует аппаратный блок в тракте контроллера памяти: ключи порождаются и живут внутри чипа, наружу они не выходят, и даже операционная система хоста видит только шифрованные байты. Работой гостя управляет RMM на уровне EL2 realm-мира: хост разговаривает с ним через SMC-вызовы RMI, гость через интерфейс RSI. Механизм закрепления работает в обе стороны: хост отдает realm страницу вызовом RMI_DATA_CREATE и с этой минуты теряет к ней доступ, любая попытка чтения со стороны хоста проваливается в отказ, а вернуть страницу можно только разрушением realm вызовами RMI_DATA_DESTROY и RMI_REALM_DESTROY. Для ввода-вывода предусмотрена вторая категория страниц: вызов RMI_RTT_MAP_UNPROTECTED объявляет страницу разделяемой, и именно через такие страницы идут virtio-устройства и сеть. Отсюда складывается цена изоляции, и у нее три слагаемых: наполнение страниц при создании realm, шифрование при каждом промахе кэша и дополнительные переходы при выходах vCPU. Эти три статьи расходов и предстоит измерить.
Проверка платформы и сборка стенда с realm-машиной
Для замеров нужен сервер на ARMv9-A с прошивкой, включающей RMM, и ядро Linux с поддержкой RME в KVM. Новейшие серверные процессоры Arm уже несут нужные блоки шифрования в тракте памяти, а на более старых платформах realm поднять не выйдет, как ни настраивай ядро. Проверка начинается с процессора, гипервизора и лога загрузки:
LANG=C lscpu | grep -E 'Architecture|Model name'
ls -l /dev/kvm
dmesg | grep -iE 'rme|realm|rmm'
Если монитор инициализировался, в выводе найдутся строки с упоминанием realm, а пустой ответ говорит о том, что прошивка или ядро пока без нужной поддержки. Поднять realm-машину позволяют свежие сборки QEMU и kvmtool, научившиеся работать с RMM; главное, чтобы версии прошивки, ядра и инструмента совпадали по интерфейсам. В гостевой системе достаточно обычного aarch64-дистрибутива и набора измерительных инструментов:
apt install -y hyperfine sysbench fio iperf3
После загрузки realm-машины работа с ней внешне не отличается от работы с обычным гостем: тот же ssh, те же пакеты, те же скрипты, и вся разница прячется внутри переходов между мирами.
Создание realm и арифметика наполнения страниц на старте
Обычная виртуальная машина стартует быстро, потому что ее память отдается ссылками на страницы хоста и наполняется по мере обращения. Realm устроен строже. Хост через вызовы RMI_REALM_CREATE, RMI_REC_CREATE и RMI_DATA_CREATE заводит структуры управления и каждую защищенную страницу отдельно, гранулами по 4 КБ, и закрепляет их за realm навсегда. Арифметика здесь беспощадная: для гостя с 8 ГБ памяти число гранул равно 8192 МБ делить на 4 КБ, то есть 2 097 152 страницы, и каждая требует отдельного вызова в прошивку. При типичной цене порядка одной-двух микросекунд на сам RMI-вызов это от двух до четырех секунд чистого времени монитора, а с учетом обхода страниц при первом касании старт realm занимает десятки секунд против единиц секунд у обычного гостя. Замер старта стоит держать в методике отдельно: в конвейерах, где машины пересоздаются часто, эти секунды складываются в часы:
hyperfine --runs 5 './start-realm.sh' './start-kvm.sh'
Отдельного взгляда заслуживают таблицы трансляции. Хост заводит их для realm вызовами RMI_RTT_CREATE и наполняет по уровням, а сами структуры живут в памяти хоста, но их содержимое читает только монитор: трансляция адресов гостя идет мимо привычных механизмов гипервизора. Время загрузки гостя при этом удобно смотреть изнутри стандартной утилитой:
systemd-analyze
Она разбивает старт на время ядра и время пользовательского пространства, и в realm разница с обычным гостем проявляется в обеих частях: страницы подтягиваются через RMI, а прошивка восстанавливает контекст каждого vCPU. Мелкая гранула в 4 КБ объясняет масштаб числа вызовов: в свежих ревизиях спецификации появляется поддержка более крупных гранул, и там, где прошивка с ядром их поддерживают, количество RMI-вызовов на старте падает в разы, но на распространенных сборках расчет честнее вести именно по 4 КБ.
Есть и второе следствие. Закрепленная память не выгружается на своп и не переиспользуется, поэтому 8 ГБ под realm означают 8 ГБ по-настоящему занятых, и переподпись памяти, к которой привыкли операторы обычных кластеров, здесь не работает.
Методика сравнения обычной KVM и realm на одинаковых ресурсах
Сравнение дает смысл только при равных условиях, и методика сводится к трем правилам:
- обе машины получают поровну vCPU и памяти, например 4 vCPU и 8 ГБ, и один и тот же образ гостя;
- каждый тест выполняется минимум трижды, крайние результаты отбрасываются, в отчет идет средний;
- на время прогонов хост простаивает, лишние службы на нем остановлены, иначе фон даст разброс больше, чем сама изоляция.
Первый блок замеров проверяет чистый счет и криптографию, потому что шифрование страниц бьет прежде всего по работе с памятью:
sysbench cpu --threads=4 --time=30 run
openssl speed -evp aes-256-xts -seconds 3
openssl speed -evp sha256 -seconds 3
Полезно прогнать те же команды и на хосте без виртуализации: три колонки, хост, обычный гость и realm, дают полную лестницу расходов, где видно, сколько забирает гипервизор и сколько добавляет изоляция. Для чистоты эксперимента ядра процессов привязывают к конкретным ядрам утилитой taskset, например taskset -c 0-3 sysbench cpu --threads=4 --time=30 run, иначе планировщик переместит нагрузку и цифры поплывут.
Шифрование памяти и его след в тестах пропускной способности
Аппаратный шифратор работает параллельно с контроллером памяти и добавляет к каждому промаху кэша задержку порядка десятков наносекунд. Для данных, живущих в кэшах процессорных ядер, разрыв почти нулевой, а для потоковых рабочих наборов с большим следом пропускная способность проседает типично на единицы процентов. Проверять это удобно на замере полосы памяти с крупным и мелким блоком:
sysbench memory --memory-block-size=1M --memory-total-size=16G run
sysbench memory --memory-block-size=4K --memory-total-size=4G run
Дисковый и сетевой пути устроены иначе. Virtio-устройства обмениваются с хостом через общие страницы, которые шифрование не затрагивает, поэтому fio на блочном устройстве показывает почти те же числа, что и обычный гость:
fio --name=seq --rw=read --bs=1M --direct=1 --size=4G --time_based --runtime=30 --group_reporting --filename=/tmp/fio.bin
Хорошая практика измерений: прогонять каждую конфигурацию дважды, сразу после холодного старта и после прогрева. Свежесозданный realm подтягивает первые страницы через заполнение и в первые секунды всегда медленнее устойчивого состояния, и без этой поправки легко записать в расходы изоляции то, что на самом деле лежит на старте. Расклад получается обманчиво спокойный: самый дорогой ресурс, процессорное время на счет, страдает меньше всего, тогда как потеря размазана по памяти и переходам, и собирать ее приходится из нескольких замеров сразу. Гость сам решает, какие страницы объявить общими, и эта пропорция между приватной и разделяемой памятью превращается в настройку производительности: больше общих страниц, быстрее ввод-вывод, меньше секретов за пределами realm.
Выходы vCPU и нагрузки, чувствительные к переходам через RMM
Выход виртуального процессора в обычной KVM стоит порядка микросекунды, и этот путь вылизан годами. В realm маршрут длиннее: гость делает SMC-вызов к RMM, монитор сохраняет контекст, передает управление хосту, а на возврате все повторяется в обратном порядке, что добавляет еще порядка микросекунды и больше. Отсюда простое правило: чем чаще нагрузка покидает гостя, тем заметнее разрыв. Микробенчмарк на конкуренцию блокировок и сетевой трафик мелкими пакетами вскрывают это наглядно:
sysbench threads --threads=8 --thread-yields=1000 --time=30 run
iperf3 -c 192.168.122.1 -u -l 64 -b 0 -t 30
На принимающей стороне к этому моменту запущен iperf3 в роли службы командой iperf3 -s. На UDP-потоке с пакетами по 64 байта главным становится число пакетов в секунду, и именно здесь добавленные микросекунды заметнее всего: полоса в мегабитах может почти не измениться, а количество обработанных пакетов упасть на заметную долю. Объем переходов считают на стороне хоста, в счетчиках KVM:
cat /sys/kernel/debug/kvm/*/stats/exits
Полный каталог статистики лежит рядом, и полезные имена видны сразу:
ls /sys/kernel/debug/kvm/*/stats/ | head -40
Среди них встречаются halt_exits, hypercalls и другие события, и сопоставление профилей обычного гостя и realm показывает, какие типы переходов добавились и чей вклад больше. Разница показаний exits до и после прогона дает число выходов, и умножение на добавленную цену перехода переводит разрыв в понятные проценты: при 50 тысячах выходов в секунду и добавке в 2 микросекунды realm теряет десятую часть ядра, поскольку 50 000 × 0,000002 с равно 0,1 с на каждую секунду работы. Ранние сборки монитора к тому же не дают гостю всех привычных измерительных возможностей, поэтому команды вроде perf stat внутри realm могут молчать или выдавать отказ, и время надежнее снимать системными часами и внешними счетчиками.
Ограничения монитора и практический расклад результатов
Измерительная картина дополняется функциональной. Первые версии RMM не поддерживают живую миграцию realm, отладка гостя ограничена архитектурой, а закрепление памяти исключает переподпись, так что планировщик емкости обязан считать за realm-машиной реально занятые гигабайты. Для планирования полезна простая арифметика. Допустим, ферма пересоздает по 100 realm-машин в сутки, и каждая стартует на 20 секунд дольше обычного гостя: 100 × 20 с дает 2000 секунд, то есть больше получаса машинного времени ежедневно, а в очередях сборки это оборачивается задержками конвейеров. Сводный расклад типичных прогонов выглядит так: тяжелый счет теряет единицы процентов, полоса памяти еще немного, дисковая и сетевая пропускная способность почти не меняются, старт дороже на секунды и десятки секунд, а нагрузки, живущие на частых выходах, могут потерять десятки процентов. Отдельная статья расходов затаилась в аттестации: гость собирает токен измерения через вызовы RSI_ATTESTATION_TOKEN_INIT и RSI_ATTESTATION_TOKEN_CONTINUE, процедура идет порциями и на крупных фермах выполняется заранее, чтобы не вставать посреди рабочего сценария. Формула выбора здесь та же, что и для любого механизма изоляции: если цена утечки данных выше цены лишней секунды и десятой доли ядра, RME окупается на месте; для одноразовых задач без секретов обычная KVM остается дешевле по всем статьям. И главный вывод анализа в том, что универсальной цифры у RME нет: изоляция размазана по старту, памяти и переходам, и только замер своей реальной нагрузки на конкретной платформе превращает архитектурную красоту в обоснованное решение.