← Блог

· 15 мин чтения #rust#mqtt#iot#unikernel#open source

urMQTT на настоящем VPS: ISO, рукопожатие за 0,24 мс, собственный диск и усиленное ядро

Что стало с тремя обещаниями из первого текста об urMQTT (один ISO, сырой диск без файловой системы, доказуемая безопасность) за первые четыре дня разработки: с измерениями, таблицами и списком того, что еще не готово.

В первом тексте об urMQTT мы описали unikernel, который является MQTT-брокером, до вехи M17 и закончили тремя обещаниями в разделе «Что дальше»: один ISO, который загружается на нескольких платформах, сырой диск без файловой системы и безопасность, которую можно доказать, а не просто заявить. Тогда ничего из этого еще не было построено. Этот текст прослеживает, что произошло с каждым обещанием, в том же порядке, что и раньше: что мы планировали, что построили, что нас удивило и что остается открытым. Заодно покажем, сколько времени это заняло, потому что это тоже часть истории.

Одно замечание заранее: проект по-прежнему находится на стадии пре-альфа. Цифры в тексте измеряют код и тесты, а не зрелость в продакшене, а первый релиз намеренно ограничен VPS с KVM. Bare metal остается на потом.

Как это выглядело во времени

Первый коммит появился 28 октября 2026 года в 23:48 (M0). К 23:00 31 октября в репозитории уже 119 коммитов, распределенных по календарным дням как 3, 39, 38 и 39. Сразу скажем, как это возможно: проект строится с Claude Code, а коммиты несут трейлеры Co-Authored-By: Claude. Четыре календарных дня истории не означают, что один человек за четыре дня написал 90 000 строк; они означают, что план, реализация, тесты и ADR пишутся короткими замкнутыми циклами, и каждый шаг заканчивается тем же ритуалом проверки, что и раньше. Цифры ниже говорят об объеме кода и тестов, а не о том, сколько всего этого было проделано в реальной работе.

Рост проекта за четыре дня с вехами от M0 до сегодняшнего дня: тысячи строк Rust (от 1,6 до 89,8), число функций #[test] (от 27 до 739) и число ADR (от 13 до 104), а также столбцы коммитов по дням (39, 38 и 39 за 29, 30 и 31 октября).
Рост проекта за четыре дня с вехами от M0 до сегодняшнего дня: тысячи строк Rust (от 1,6 до 89,8), число функций #[test] (от 27 до 739) и число ADR (от 13 до 104), а также столбцы коммитов по дням (39, 38 и 39 за 29, 30 и 31 октября).

На графике три кривые, которые растут почти вместе, и это намеренно: тесты и записанные решения идут вровень с кодом, а не отстают от него. Наклон круче всего 29 октября, когда создается весь путь от ядра до брокера. Дальше строки добавляются медленнее, но тесты растут быстрее кода (27 тестов на 1,6 тысячи строк в начале, 739 на 89,8 тысячи сегодня).

Веха Коммит Время Строк Rust Тестов ADR
M0 9412a9c 28.10. 23:48 1 592 27 13
M5 a61083b 29.10. 01:32 9 314 88 24
M9 4be8cc9 29.10. 02:21 15 729 149 28
M11 4800904 29.10. 03:43 22 737 180 30
M13 a558d5e 29.10. 05:22 26 556 193 32
M17 7df4f98 29.10. 08:48 33 504 232 36
S1 ecf72ec 30.10. 00:46 41 910 299 45
X8 fb1504a 30.10. 19:08 56 341 456 59
PERF 76f7c36 31.10. 14:19 72 511 618 76
сегодня HEAD 31.10. 23:00 89 790 739 104

Строки это все файлы .rs вне vendor/ и fuzz/, тесты это функции #[test]. Что добавлялось и когда, по датам из истории git:

Дата Вехи Что добавлено
29.10. M0 до M17 ядро, сеть, TLS, брокер, SQLite, несколько ядер, собственный TCP, усиление, воспроизводимый релиз
вечер 29.10. и 30.10. X0 до X10, S1, H2, H3 (X0 начинается 29.10. в 20:54) порт на x86_64, универсальный ISO, один диск и самоустанавливающийся ISO, проверки TLS, атакующие клиенты
31.10. O1 до O4, T1, PERF, H4, H5, H9, S2 до S6 наблюдаемость и консоль через MQTT, живой режим и диагностика, SNTP, ускорения, усиление, верификация, цепочка поставок, хранение

Обещание 1: один ISO для нескольких платформ

Что мы планировали. Один файл, который загружается на aarch64 UEFI, x86_64 UEFI и x86_64 BIOS. Что мы построили. Порт на x86_64 (X0 до X10) и универсальный ISO с загрузчиком Limine, один гибридный ISO, с которого загружаются все эти комбинации; смоук-тесты поднимают его на четырех виртуальных машинах. В меню три пункта: urMQTT (живой режим, работает из RAM, на диск ничего не пишет), установка на диск машины, на которой он запущен (установка записывает GPT и данные, а затем выключает машину) и Диагностика (определение оборудования и измерения).

Меню загрузки ISO, снятое на VPS: пункты «urMQTT», «Install urMQTT on this machine's disk» и «Diagnostic: hardware detection and benchmarks» с обратным отсчетом до автоматической загрузки. Снимок сделан с тестового ISO с более длинным отсчетом; по умолчанию 10 секунд.
Меню загрузки ISO, снятое на VPS: пункты «urMQTT», «Install urMQTT on this machine's disk» и «Diagnostic: hardware detection and benchmarks» с обратным отсчетом до автоматической загрузки. Снимок сделан с тестового ISO с более длинным отсчетом; по умолчанию 10 секунд.

Первой настоящей целью стал VPS, а не физический компьютер: решено, что первый релиз поддерживает только VPS (KVM), а bare metal идет после него. Мы тестировали на VPS у Contabo с процессором AMD EPYC (семейство 23, Zen 1), 4 vCPU и 8 GiB RAM, с сетью virtio-net и диском virtio-scsi. Первая попытка нас удивила: ISO, подключенный как виртуальный CD, загрузил пункт по умолчанию, продакшен-образ отказался работать с носителя только для чтения и перезагрузился, и мы получили бесконечный цикл меню, ядро, сброс без единой читаемой строки. Сегодня фатальная ошибка в продакшен-образе оставляет сообщение на консоли на 60 секунд перед перезагрузкой, а с оптического носителя выводит, что загрузка произошла с носителя только для чтения, и останавливается. Живой режим в RAM (O3) позже обошел эту проблему для обычного случая.

Пункт диагностики родился из этого опыта: машину, которую мы не видим, нужно уметь спросить, что у нее есть. Он выводит CPU, RAM, сеть, диски и PCI-устройства, измеряет пропускную способность памяти, диск и криптографию, а те же измерения выводит в виде JSON на последовательный порт. Запись на диск разрешена только в нераспределенном пространстве и только после ввода WIPE; на снимке ниже это пропущено.

Экран диагностики на VPS у Contabo (AMD EPYC, KVM, 4 vCPU): система, память, диск, PCI-устройства, пропускная способность и задержка RAM, а также криптоизмерения, включая 244 us на mTLS-рукопожатие, X25519 48 us, подпись ECDSA 35,7 us и проверку 107 us.
Экран диагностики на VPS у Contabo (AMD EPYC, KVM, 4 vCPU): система, память, диск, PCI-устройства, пропускная способность и задержка RAM, а также криптоизмерения, включая 244 us на mTLS-рукопожатие, X25519 48 us, подпись ECDSA 35,7 us и проверку 107 us.

Что остается открытым. Настоящее железо. После первого релиза идет bare metal (загрузка с USB, NVMe и AHCI, сетевая карта реального компьютера) вместе с IOMMU и Secure Boot, потому что без IOMMU вредоносное или неисправное устройство может писать по всей памяти. В этой версии мы ничего об этом не утверждаем.

Рукопожатие: от 1,2 мс до 0,24 мс

Первое измерение диагностики на VPS дало неприятную цифру: mTLS-рукопожатие, только криптография на стороне сервера, занимало 1,2 мс. На aarch64 мы уже использовали аппаратные инструкции AES и SHA, но для открытого ключа на x86_64 ничего подобного не было: RustCrypto переносим и работает за постоянное время, а значит, и медленно. Три изменения это исправили.

  • Для P-256 мы взяли ассемблер nistz256 из OpenSSL и ring (ADR-130), с таблицами с фиксированным основанием для подписи и проверки подписи CA (ADR-110, ADR-111).
  • X25519 переписан на четыре 64-битные ветви с инструкциями MULX/ADX (ADR-131).
  • После этого мы профилировали и остальное (ADR-132, ADR-200).
Рукопожатие на VPS: 1200 us до, 237 us после ассемблера и таблиц; A/B-сравнение memcpy в одном запуске, 278,8 us со старым, 229,4 us с новым; а также до и после для X25519 (146 до 48 us), подписи ECDSA (673 до 34 us) и проверки (309 до 90 us).
Рукопожатие на VPS: 1200 us до, 237 us после ассемблера и таблиц; A/B-сравнение memcpy в одном запуске, 278,8 us со старым, 229,4 us с новым; а также до и после для X25519 (146 до 48 us), подписи ECDSA (673 до 34 us) и проверки (309 до 90 us).
Измерение на VPS (сервер, микросекунды) До После
mTLS-рукопожатие, вся криптография 1200 около 240 (237 в измерении для графика, 244 на снимке)
X25519 146 48
Подпись ECDSA 673 34
Проверка ECDSA 309 90 (107 на снимке)
рукопожатие, старый memcpy, та же загрузка 278,8 229,4 (-17,7%)

Цифры отличаются от запуска к запуску примерно на 10%, поэтому мы записали диапазон вместо одного значения. В сумме это примерно в пять раз меньше процессорного времени на рукопожатие.

Что мы узнали о memcpy. Когда криптография стала быстрой, профиль показал неожиданное: функции mem* (memcpy, memmove, memset) занимали 7% рукопожатия, от 13 до 39% пути TLS-записи (39% для 64 B, 13% для 4 KiB) и от 17 до 34% разбора PUBLISH. Мы заменили их ассемблером SSE2 для x86_64 (ADR-200) с дифференциальными тестами против эталонной реализации для всех размеров, выравниваний и перекрытий. На один вызов это 55-75 циклов против 7-12 для 1-64 байт.

Слева: доля времени сервера в функциях mem* до переписывания (рукопожатие 7%, TLS-запись 64 B 39%, 256 B 37%, 1 KiB 24%, разбор 17-34%). Справа: насколько быстрее каждый путь после (рукопожатие 17,7%, 64 B 26,4%, 256 B 19,6%, 1 KiB 16,1%, 4 KiB 6,6%, разбор 7-34%).
Слева: доля времени сервера в функциях mem* до переписывания (рукопожатие 7%, TLS-запись 64 B 39%, 256 B 37%, 1 KiB 24%, разбор 17-34%). Справа: насколько быстрее каждый путь после (рукопожатие 17,7%, 64 B 26,4%, 256 B 19,6%, 1 KiB 16,1%, 4 KiB 6,6%, разбор 7-34%).
Путь Доля mem* до Быстрее после
рукопожатие 7% 17,7%
TLS-запись 64 B 39% 26,4%
TLS-запись 256 B 37% 19,6%
TLS-запись 1 KiB 24% 16,1%
TLS-запись 4 KiB 13% 6,6%
разбор сообщений 17 до 34% 7 до 34%

Как читать график: чем меньше запись, тем большая часть времени уходит на копирование, а значит, тем больше выигрыш. Для маленьких сообщений, которых большинство в MQTT-трафике, это и есть то, что имеет значение.

Что это не означает. На нативном x86_64 мы измеряли OpenSSL 3.5.5 (ADR-130) только как эталон для ассемблера, а не как сравнение продуктов. Сравнение Linux плюс Mosquitto на том же VPS (задача X11) мы еще не сделали. Поэтому мы не утверждаем, что urMQTT быстрее Linux: это не измерено. Цифры выше сравнивают urMQTT с самим собой.

Перегрузка стала вторым результатом. На VPS, с одного Mac (RTT 35 мс), 128 клиентов дали 618 рукопожатий в секунду, 256 клиентов 393, а 512 клиентов 67, с тысячами отклоненных соединений. Это не было плато, это был обвал. Причины мы нашли в коде (две подтверждены в QEMU/HVF, на VPS воспроизвести их не удалось), и их было несколько сразу: отклоненное соединение возвращало клиента через следующий сегмент, несущий действительную SYN-куки, отказ через RST говорил клиенту немедленно пробовать снова, а прием был O(очередь). Исправление (ADR-080 до ADR-082) отбрасывает SYN, который не был бы принят, запоминает отклоненные потоки, держит прием в O(1) и считает рукопожатия по ядрам. Возможно (гипотеза, не проверена), часть причины была и на стороне клиента: инструмент нагрузки закрывал соединение первым, и один Mac с 16 384 эфемерными портами и TIME_WAIT в 30 секунд был ограничен примерно 550 соединениями в секунду. Поэтому настоящий потолок сервера мы еще не измерили.

Обещание 2: сырой диск, но аккуратный

Что мы планировали. Сохранить собственную раскладку без файловой системы и добавить все то, что файловая система обычно дает: выравнивание, рост, discard, резервные копии и проверки. Почему не файловая система. Мы это обсуждали, и решение подтверждено: urMQTT пишет одну базу, один журнал телеметрии и собственные метаданные, ему не нужны каталоги, права доступа и имена файлов, а каждый дополнительный слой был бы еще одним кодом в доверенной части и еще одним способом, которым запись тихо исчезнет. Диск это GPT с разделом ESP и одним разделом данных urMQTT, внутри которого собственная раскладка.

Что мы построили (S2 до S6).

  • Раскладка v2 (S2). Топология диска, выравнивание на 1 MiB или оптимальный размер ввода-вывода, целые физические блоки, A/B-пара суперблоков с поколением (ADR-140). Раскладку v1 ядро отклоняет с понятным сообщением, а мигрирует ее офлайн-инструмент urmqtt-disk migrate (ADR-141): по умолчанию пробный прогон, сначала копирование, потом переключение, с откатом и внедрением ошибок на каждой записи.
  • Рост (S3). При загрузке диск может вырасти (никогда не уменьшается), а на virtio-blk и во время работы, через прерывание об изменении конфигурации (ADR-142, ADR-143).
  • Discard и write-zeroes (S4). Тонко выделенные диски возвращают место хосту. В тесте qcow2-файл на хосте уменьшился с 62 до 16 MiB после истечения сегментов телеметрии, а восстановление никогда не доверяет содержимому, которое было отброшено.
  • Резервная копия базы на диске (S5). A/B-копия с помощью онлайн-API резервного копирования SQLite шагами по 16 страниц: самый длинный шаг 4,6 мс против 445 мс при копировании одним вызовом. Коммиты во время копирования имели максимум 2,4 мс против 2,2 мс без нее. Если quick_check при загрузке не проходит, база восстанавливается из копии.
  • Очистка и ограниченная загрузка (S6). Scrubber читает диск с ограниченным бюджетом ввода-вывода и сообщает о поврежденных записях, а загрузка читает только самый новый сегмент каждого раздела, так что время загрузки не растет вместе с размером региона телеметрии. Так мы закрыли проблему, которую рассчитали заранее: на большом диске полное восстановление заняло бы от минут до десятков минут.

Вся группа смоук-тестов хранения проходит на обеих архитектурах: 79 успешных, 0 неуспешных, на aarch64 под HVF и на x86_64 под TCG. Мы также измерили, что S4 не замедлил базу: 53,3 тысячи вставок в секунду пакетами до и после, без измеримой разницы.

Что удивило. Сколько в «простой» раскладке уходит на крайние случаи: virtio-scsi и virtio-blk сообщают топологию по-разному, а неполная запись посреди миграции всегда должна оставлять хотя бы одну действительную версию. Поэтому каждая запись на пути миграции и роста прошла внедрение ошибок.

Что остается открытым. У страниц SQLite нет контрольной суммы, поэтому изменившийся бит внутри значения в строке база не обнаружит; записи телеметрии и содержимое копий имеют CRC и обнаруживаются. Шифрование диска (S7) не готово.

Обещание 3: доказуемо безопасно, насколько получится

Усиление (H4). Аллокатор стирает освобожденную память и проверяет ее при повторном использовании, обнаруживает двойное освобождение, связывает указатели свободного списка через XOR и обнуляет ключи при освобождении (ADR-170). Защита стека (-Zstack-protector=strong, 1315 функций, а SQLite с -fstack-protector-strong, ADR-171). Отчеты об ошибках в продакшене не содержат регистров и адресов (ADR-172). KASLR на обеих архитектурах: ядро является PIE, и Limine сдвигает его примерно на 19 бит, а куча, стеки и окно MMIO размещаются случайно (ADR-173). На aarch64 MTE помечает кучу (ADR-174), но только под TCG: в HVF и на x86 его нет. kCFI мы оценили и не приняли, потому что компилятор на закрепленном nightly падает с внутренней ошибкой; IOMMU отложен вместе с bare metal (ADR-175, ADR-176).

Цена измерена, а не предположена. На пути рукопожатия (dp-bench, aarch64 HVF, полные рукопожатия TLS 1.3 + mTLS + CONNECT):

Шаг 1 ядро 4 ядра Изменение
без усиления 1941 7620
+ аллокатор 1898 7429 -2,2% до -2,5%
+ защита стека 1845 7243 вместе -4,9% до -5%
+ PIE и метки около 1800 около 6750 на загруженном компьютере, с шумом

В целом цена на пути рукопожатия составляет примерно 5% для частей, измеренных чисто, а с PIE и метками выходит около 7% на одном и около 11% на четырех ядрах, но это измерение шумное. Мы считаем ее приемлемой, но не повторяли ее на x86_64 VPS.

Стоит сравнить с цифрой из первого текста (862 рукопожатия в секунду на одном ядре). Тот же инструмент сегодня, с драйвером аппаратного шифрования на aarch64 (X0b) и рядом ускорений, пришедших с тех пор, дает 1941 до усиления. Вклад каждого изменения мы не выделяли, поэтому о причине ничего не утверждаем.

Как проверено, и что нет. H5 добавил Miri на подмножество хост-крейтов, доказательства Kani, санитайзеры, модельный фаззер для ACL и отчеты о покрытии. Kani доказал record_len в TLS, parse в TCP и две арифметики колеса таймеров. Не доказано (истекло время): оба MQTT-декодера, оба разбора конфигурации и поведение колеса таймеров; CI-задача для Kani поэтому пока не блокирует. Miri не умеет выполнять ассемблер. Цепочка поставок (H9): CycloneDX SBOM с 215 компонентами, cargo-vet проходит с 39 полностью проверенными и 140 исключениями, а cargo-deny настроен так, чтобы рассматривать советы об уязвимостях как падение CI (эти CI-задачи пока проверены только локально, не на GitHub, а негативный тест не запускался), плюс VULNERABILITY-HANDLING.md. Честное положение дел: из 67 версий сторонних крейтов, линкуемых в ядро, 53 это исключения, а не аудиты. Vet доказывает, что набор зависимостей нельзя изменить без видимой разницы, а не то, что TCB проверен. Собственный аудит ни одного крейта мы не писали.

SNTP (T1) в этом смысле маленькая история о безопасности: он опционален, только к частным IPv4-серверам, с максимальным скачком и при условии, что источники согласны (медиана, большинство в пределах одной секунды). Часы, которые кто угодно на пути может сдвинуть, это не часы, которым мы можем доверять при проверке срока действия сертификата.

На Mac: aarch64 нативно и x86_64 эмуляцией

Мы разрабатываем на Mac, а у него два способа запускать один и тот же код, и они очень разные.

aarch64 под HVF x86_64 под TCG
Что это нативное выполнение, скорость близка к реальному процессору QEMU эмулирует инструкции
Рукопожатие в госте около 0,58 мс (впервые измерено около M13); dp-bench сегодня 1941/с на ядро около 3,3 мс
Сценарии TLS-смоука быстрые около 15 минут на сценарий
MTE нет (в HVF его нет) на x86 его нет; разметка кучи работает только для гостя aarch64 под TCG
Для чего служит реальные измерения корректность: x86-путь проверяется, цифры не являются производительностью

Правило простое: цифры под TCG это эмуляция, и мы не приводим их как производительность. Производительность x86 мы измеряем только на настоящем VPS. Поэтому криптографические цифры выше взяты с VPS у Contabo, а не с Mac.

Уроки

  • Измеряйте на настоящей машине, прежде чем оптимизировать. Первый подход состоял в оптимизации того, что мы считали медленным; только диагностика на VPS показала 1,2 мс и где именно уходит время.
  • Перегрузка это отдельная характеристика. Система, которая быстра под нагрузкой, может обвалиться выше нее; отказ должен быть дешевле принятия.
  • Профиль удивляет. Самый большой отдельный выигрыш для маленьких сообщений оказался не в криптографии, а в копировании памяти.
  • Диск без файловой системы требует больше работы, а не меньше. То, что файловая система обычно дает (выравнивание, рост, discard, копии, проверки), мы должны написать и проверить сами, каждый шаг с внедрением ошибок.
  • Честность в цифрах. Измерения памяти на общем VPS колеблются до 10 раз между запусками и ничего не говорят о коде, поэтому мы их не приводим. Криптографические цифры стабильны примерно до 10%.
  • Скорость разработки это не зрелость. Тесты и ADR растут вместе с кодом, но четыре дня это не настоящая эксплуатация.

Что дальше

  • H6: SQLite, изолированный на уровне EL0, чтобы ошибка в базе не открывала ядро.
  • H7: остальная безопасность времени (NTS и обработка скачков); SNTP это лишь первый шаг.
  • X11: сравнение с Linux и Mosquitto на том же VPS, только тогда имеет смысл говорить об относительной скорости.
  • Релиз: первая публичная версия для VPS (KVM), с подписанными артефактами и инструкциями.
  • После релиза: bare metal (X12 до X16), IOMMU и Secure Boot, а также шифрование диска.

Код находится на github.com/zeljan-alduk/urMQTT, а docs/PROGRESS.md по-прежнему единственный источник истины о том, что действительно работает. Пре-альфа, как и раньше; сообщения об ошибках и о безопасности по инструкции в SECURITY.md приветствуются.

Комментарии

Правила комментирования

Правила комментирования

Комментарии нужны для обмена опытом и вопросами по темам блога. Эти правила помогают сохранить обсуждение полезным для всех.

  1. Вежливость Без оскорблений, угроз, травли, языка вражды и дискриминации по любому признаку. Критикуйте аргумент, а не человека.
  2. По теме Комментарий должен относиться к записи или к обсуждению под ней.
  3. Без рекламы и спама Без рекламы, писем счастья и ссылок без контекста. Ссылки и изображения приветствуются, если они дополняют обсуждение.
  4. Чужая приватность Не публикуйте чужие персональные данные: полные имена частных лиц, адреса, телефоны, почту, фотографии без согласия.
  5. Авторские права Публикуйте только то, чем вправе делиться. При цитировании указывайте источник.
  6. Одна личность Используйте один ник и не выдавайте себя за другого.
  7. Подтверждение по почте Комментарий публикуется только после перехода по ссылке из письма. Адрес не показывается и ни для чего другого не используется; неподтверждённые комментарии удаляются через 48 часов.
  8. Модерация Комментарии, нарушающие правила, скрываются или удаляются без предупреждения, обсуждение может быть закрыто. Решения модераторов окончательны.
  9. Ответственность За содержание комментария отвечает его автор. Комментарии не отражают позицию ALDO TECH.
  10. Жалобы О неуместном содержании сообщайте на info@aldo.tech.