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 пишутся короткими замкнутыми циклами, и каждый шаг заканчивается тем же ритуалом проверки, что и раньше. Цифры ниже говорят об объеме кода и тестов, а не о том, сколько всего этого было проделано в реальной работе.
На графике три кривые, которые растут почти вместе, и это намеренно: тесты и записанные решения идут вровень с кодом, а не отстают от него. Наклон круче всего 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 и данные, а затем выключает машину) и Диагностика (определение оборудования и измерения).

Первой настоящей целью стал 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; на снимке ниже это пропущено.

Что остается открытым. Настоящее железо. После первого релиза идет 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 (сервер, микросекунды) | До | После |
|---|---|---|
| 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% | 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 приветствуются.
Комментарии
Правила комментирования