urMQTT na prawdziwym VPS-ie: ISO, uzgadnianie w 0,24 ms, własny dysk i utwardzone jądro
Co stało się z trzema obietnicami z pierwszego tekstu o urMQTT (jedno ISO, surowy dysk bez systemu plików, bezpieczeństwo, które da się udowodnić) po pierwszych czterech dniach pracy, z pomiarami, tabelami i listą tego, co jeszcze nie jest gotowe.

W pierwszym tekście o urMQTT opisaliśmy unikernel, który jest brokerem MQTT, do kamienia milowego M17, i zakończyliśmy trzema obietnicami pod nagłówkiem „Co dalej": jedno ISO uruchamiane na wielu platformach, surowy dysk bez systemu plików oraz bezpieczeństwo, które można udowodnić, a nie tylko zadeklarować. Nic z tego nie było wtedy zbudowane. Ten tekst śledzi, co stało się z każdą obietnicą, w tej samej kolejności co poprzednio: co planowaliśmy, co zbudowaliśmy, co nas zaskoczyło i co jest jeszcze otwarte. Przy okazji pokażemy, ile to trwało, bo to część tej historii.
Jedna uwaga na wstępie: projekt nadal jest w fazie pre-alfa. Liczby w tekście mierzą kod i testy, a nie dojrzałość produkcyjną, a pierwsze wydanie jest celowo ograniczone do VPS-ów z KVM. Bare metal zostaje na później.
Jak to wyglądało w czasie
Pierwszy commit powstał 28 października 2026 o 23:48 (M0). Do 31 października, godz. 23:00, w repozytorium jest 119 commitów, rozłożonych jako 3, 39, 38 i 39 na kolejne dni kalendarzowe. Od razu powiedzmy, jak to możliwe: projekt powstaje z Claude Code, a commity mają trailery Co-Authored-By: Claude. Cztery dni kalendarzowe historii nie znaczą, że jedna osoba napisała w cztery dni 90 000 linii; znaczą, że plan, implementacja, testy i ADR-y powstają w krótkich, zamkniętych pętlach, a każdy krok kończy się tym samym rytuałem weryfikacji co wcześniej. Liczby poniżej mówią o ilości kodu i testów, a nie o tym, ile z tego zostało sprawdzone w prawdziwej pracy.
Wykres pokazuje trzy krzywe, które rosną niemal razem, i to jest zamierzone: testy i zapisane decyzje nadążają za kodem, a nie zostają w tyle. Nachylenie jest największe 29 października, kiedy powstaje cała droga od jądra do brokera. Później linii przybywa wolniej, ale testy rosną szybciej niż kod (27 testów na 1,6 tys. linii na początku, 739 na 89,8 tys. dziś).
| Kamień milowy | Commit | Czas | Linii Rusta | Testów | ADR-ów |
|---|---|---|---|---|---|
| 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 |
| dziś | HEAD | 31.10. 23:00 | 89 790 | 739 | 104 |
Linie to wszystkie pliki .rs poza vendor/ i fuzz/, a testy to funkcje #[test]. Co dodał każdy etap, według dat z historii gita:
| Data | Kamienie milowe | Co dodano |
|---|---|---|
| 29.10. | M0 do M17 | jądro, sieć, TLS, broker, SQLite, wiele rdzeni, własny TCP, utwardzanie, powtarzalne wydanie |
| 29.10. wieczorem i 30.10. | X0 do X10, S1, H2, H3 (X0 zaczyna się 29.10. o 20:54) | port na x86_64, uniwersalne ISO, jeden dysk i samoinstalujące się ISO, kontrole TLS, klienci-atakujący |
| 31.10. | O1 do O4, T1, PERF, H4, H5, H9, S2 do S6 | wgląd i konsola przez MQTT, tryb live i diagnostyka, SNTP, przyspieszenia, utwardzanie, weryfikacja, łańcuch dostaw, przechowywanie |
Obietnica 1: jedno ISO dla wielu platform
Co planowaliśmy. Jeden plik, który startuje na aarch64 UEFI, x86_64 UEFI i x86_64 BIOS. Co zbudowaliśmy. Port na x86_64 (X0 do X10) i uniwersalne ISO z bootloaderem Limine, jeden hybrydowy obraz, z którego startują wszystkie te kombinacje; smoke testy uruchamiają go na czterech maszynach wirtualnych. Menu ma trzy pozycje: urMQTT (tryb live, działa z RAM-u, nic nie zapisuje na dysku), instalacja na dysku maszyny, na której jest uruchomione (instalacja zapisuje GPT i dane, po czym wyłącza maszynę) oraz Diagnostyka (wykrywanie sprzętu i pomiary).

Pierwszym prawdziwym celem był VPS, a nie fizyczny komputer: decyzja brzmi, że pierwsze wydanie obsługuje tylko VPS (KVM), a bare metal przychodzi po nim. Testowaliśmy na VPS-ie Contabo z procesorem AMD EPYC (rodzina 23, Zen 1), 4 vCPU i 8 GiB RAM, z siecią virtio-net i dyskiem virtio-scsi. Pierwsza próba nas zaskoczyła: ISO podłączone jako wirtualny CD uruchamiało domyślną pozycję, obraz produkcyjny odmówił pracy z nośnika tylko do odczytu i się zresetował, więc dostaliśmy nieskończoną pętlę menu, jądro, reset, bez niczego czytelnego. Dziś błąd krytyczny w obrazie produkcyjnym zostawia komunikat na konsoli na 60 sekund przed resetem, a z nośnika optycznego wypisuje, że uruchomiono z nośnika tylko do odczytu, i się zatrzymuje. Tryb live w RAM-ie (O3) obszedł później ten problem w typowym przypadku.
Pozycja diagnostyczna wzięła się z tego doświadczenia: maszynę, której nie widzimy, musimy móc zapytać, co ma. Wypisuje CPU, RAM, sieć, dyski i urządzenia PCI, mierzy przepustowość pamięci, dysk i kryptografię, a te same pomiary wypisuje jako JSON na port szeregowy. Zapis na dysk jest dozwolony tylko w nieprzydzielonym obszarze i dopiero po wpisaniu WIPE; na zrzucie poniżej to pominięto.

Co jest otwarte. Prawdziwy sprzęt. Po pierwszym wydaniu przyjdzie bare metal (start z USB, NVMe i AHCI, karta sieciowa prawdziwego komputera), razem z IOMMU i Secure Boot, bo bez IOMMU złośliwe lub wadliwe urządzenie może zapisywać w całej pamięci. W tej wersji niczego na ten temat nie twierdzimy.
Uzgadnianie: od 1,2 ms do 0,24 ms
Pierwszy pomiar diagnostyki na VPS-ie dał niewygodną liczbę: uzgadnianie mTLS, wyłącznie kryptografia po stronie serwera, trwało 1,2 ms. Na aarch64 używaliśmy już sprzętowych instrukcji AES i SHA, ale x86_64 nie miał nic podobnego dla kluczy publicznych: RustCrypto jest przenośne i constant-time, a przez to także wolne. Zmieniły to trzy kroki.
- Dla P-256 przejęliśmy asembler
nistz256z OpenSSL i ring (ADR-130), z tablicami o stałej bazie do podpisywania i weryfikacji podpisu CA (ADR-110, ADR-111). - X25519 został przepisany na cztery 64-bitowe kończyny z instrukcjami MULX/ADX (ADR-131).
- Następnie sprofilowaliśmy resztę (ADR-132, ADR-200).
| Pomiar na VPS-ie (serwer, mikrosekundy) | Przed | Po |
|---|---|---|
| uzgadnianie mTLS, cała kryptografia | 1200 | około 240 (237 w pomiarze z wykresu, 244 na zrzucie) |
| X25519 | 146 | 48 |
| podpis ECDSA | 673 | 34 |
| weryfikacja ECDSA | 309 | 90 (107 na zrzucie) |
| uzgadnianie, stary memcpy, ten sam boot | 278,8 | 229,4 (-17,7%) |
Liczby różnią się między uruchomieniami o około 10%, więc zapisaliśmy zakres zamiast jednej wartości. W sumie to mniej więcej pięć razy mniej czasu CPU na uzgadnianie.
Czego nauczył nas memcpy. Gdy kryptografia spadła, profil pokazał coś, czego się nie spodziewaliśmy: funkcje mem* (memcpy, memmove, memset) zajmowały 7% uzgadniania, 13 do 39% ścieżki rekordu TLS (39% dla 64 B, 13% dla 4 KiB) i 17 do 34% rozdzielania PUBLISH. Zastąpiliśmy je asemblerem SSE2 dla x86_64 (ADR-200), z testami różnicowymi względem implementacji referencyjnej dla wszystkich rozmiarów, wyrównań i nakładania się. Na wywołanie to 55 do 75 cykli wobec 7 do 12 dla 1 do 64 bajtów.
| Ścieżka | Udział mem* przed | Szybciej po |
|---|---|---|
| uzgadnianie | 7% | 17,7% |
| rekord TLS 64 B | 39% | 26,4% |
| rekord TLS 256 B | 37% | 19,6% |
| rekord TLS 1 KiB | 24% | 16,1% |
| rekord TLS 4 KiB | 13% | 6,6% |
| rozdzielanie wiadomości | 17 do 34% | 7 do 34% |
Jak czytać wykres: im mniejszy rekord, tym większa część czasu idzie na kopiowanie, więc tym większy zysk. Dla małych wiadomości, które stanowią większość ruchu MQTT, to właśnie ma znaczenie.
Czego to nie oznacza. Na natywnym x86_64 mierzyliśmy OpenSSL 3.5.5 (ADR-130) wyłącznie jako punkt odniesienia dla asemblera, a nie jako porównanie produktów. Porównania Linux plus Mosquitto na tym samym VPS-ie (zadanie X11) jeszcze nie zrobiliśmy. Dlatego nie twierdzimy, że urMQTT jest szybszy od Linuksa: nie zostało to zmierzone. Powyższe liczby porównują urMQTT z nim samym.
Przeciążenie było drugim wynikiem. Na VPS-ie, z jednego Maca (RTT 35 ms), 128 klientów dało 618 uzgodnień na sekundę, 256 klientów 393, a 512 klientów 67, przy tysiącach odrzuconych połączeń. To nie było plateau, tylko załamanie. Przyczyny znaleźliśmy w kodzie (dwie potwierdzono w QEMU/HVF, na VPS-ie nie dało się ich odtworzyć), kilka naraz: odrzucone połączenie wracało do klienta przez kolejny segment niosący ważne ciasteczko SYN, odrzucenie przez RST mówiło klientowi, żeby od razu spróbował ponownie, a odbiór był O(kolejka). Poprawka (ADR-080 do ADR-082) porzuca SYN, który i tak nie zostałby przyjęty, zapamiętuje odrzucone przepływy, utrzymuje odbiór w O(1) i liczy uzgodnienia na rdzeń. Możliwe (hipoteza, niesprawdzona), że część przyczyny leżała też po stronie klienta: narzędzie do obciążenia zamykało połączenia pierwsze, więc jeden Mac z 16 384 portami efemerycznymi i TIME_WAIT wynoszącym 30 sekund jest ograniczony do około 550 połączeń na sekundę. Dlatego prawdziwego limitu serwera jeszcze nie zmierzyliśmy.
Obietnica 2: surowy dysk, ale ostrożnie
Co planowaliśmy. Zachować własny układ bez systemu plików i dodać wszystko, co system plików zwykle daje: wyrównanie, rozrost, discard, kopie zapasowe i kontrole. Dlaczego nie system plików. Rozmawialiśmy o tym i decyzja została potwierdzona: urMQTT zapisuje jedną bazę, jeden dziennik telemetrii i własne metadane, nie potrzebuje katalogów, uprawnień ani nazw plików, a każda dodatkowa warstwa to kolejny kod w zaufanej części i kolejny sposób, by zapis po cichu zniknął. Dysk to GPT z partycją ESP i jedną partycją danych urMQTT, na której jest własny układ.
Co zbudowaliśmy (S2 do S6).
- Układ v2 (S2). Topologia dysku, wyrównanie do 1 MiB lub optymalnego rozmiaru I/O, całe bloki fizyczne, para superbloków A/B z generacją (ADR-140). Układ v1 jądro odrzuca z czytelnym komunikatem, a migruje go narzędzie offline
urmqtt-disk migrate(ADR-141): domyślnie jest to przebieg próbny, kopiuje się, a dopiero potem przełącza, z wycofaniem i wstrzykiwaniem błędów przy każdym zapisie. - Rozrost (S3). Przy starcie dysk może urosnąć (nigdy się nie zmniejsza), a na virtio-blk także w trakcie pracy, przez przerwanie zmiany konfiguracji (ADR-142, ADR-143).
- Discard i write-zeroes (S4). Dyski z cienkim przydziałem oddają miejsce hostowi. W teście plik qcow2 na hoście spadł z 62 do 16 MiB po wygaśnięciu segmentów telemetrii, a odzyskiwanie nigdy nie ufa zawartości, która została odrzucona.
- Kopia zapasowa bazy na dysku (S5). Kopia A/B za pomocą API online backup SQLite w krokach po 16 stron: najdłuższy krok 4,6 ms wobec 445 ms przy kopiowaniu jednym wywołaniem. Commity w trakcie kopii miały maksimum 2,4 ms wobec 2,2 ms bez niej. Jeśli
quick_checkprzy starcie się nie powiedzie, baza jest przywracana z kopii. - Scrubbing i ograniczony start (S6). Scrubber czyta dysk w ramach ograniczonego budżetu I/O i zgłasza uszkodzone rekordy, a start czyta tylko najnowszy segment każdej partycji, więc czas startu nie rośnie wraz z rozmiarem regionu telemetrii. Zamknęliśmy w ten sposób problem, który policzyliśmy z góry: na dużym dysku pełne odzyskiwanie trwałoby od minut do dziesiątek minut.
Cała grupa smoke testów przechowywania przechodzi na obu architekturach: 79 udanych, 0 nieudanych, na aarch64 pod HVF i na x86_64 pod TCG. Zmierzyliśmy też, że S4 nie spowolnił bazy: 53,3 tys. wstawień na sekundę w paczkach przed i po, bez mierzalnej różnicy.
Co zaskoczyło. Ile z „prostego" układu to przypadki brzegowe: virtio-scsi i virtio-blk zgłaszają topologię na różne sposoby, a niepełny zapis w środku migracji musi zawsze zostawić co najmniej jedną ważną wersję. Dlatego każdy zapis na ścieżce migracji i rozrostu przeszedł wstrzykiwanie błędów.
Co jest otwarte. Strony SQLite nie mają sumy kontrolnej, więc zmiana bitu wewnątrz wartości w wierszu nie zostanie przez bazę wykryta; rekordy telemetrii i zawartość kopii mają CRC i są wykrywane. Szyfrowanie dysku (S7) nie jest gotowe.
Obietnica 3: bezpieczeństwo, które da się udowodnić, na ile się uda
Utwardzanie (H4). Alokator zeruje zwolnioną pamięć i sprawdza ją przy ponownym użyciu, wykrywa podwójne zwolnienie, wiąże wskaźniki listy wolnych przez XOR i zeruje klucze przy zwolnieniu (ADR-170). Ochrona stosu (-Zstack-protector=strong, 1315 funkcji, a SQLite z -fstack-protector-strong, ADR-171). Raporty błędów w produkcji nie zawierają rejestrów ani adresów (ADR-172). KASLR na obu architekturach: jądro jest PIE, a Limine przesuwa je o około 19 bitów, losowo ustawiane są sterta, stosy i okno MMIO (ADR-173). Na aarch64 MTE oznacza stertę (ADR-174), ale tylko pod TCG: HVF i x86 go nie mają. kCFI oceniliśmy i nie przyjęliśmy, bo kompilator na przypiętym nightly wywala się z błędem wewnętrznym; IOMMU odłożono razem z bare metalem (ADR-175, ADR-176).
Koszt jest zmierzony, a nie założony. Na ścieżce uzgadniania (dp-bench, aarch64 HVF, pełne uzgadnianie TLS 1.3 + mTLS + CONNECT):
| Krok | 1 rdzeń | 4 rdzenie | Zmiana |
|---|---|---|---|
| bez utwardzania | 1941 | 7620 | |
| + alokator | 1898 | 7429 | -2,2% do -2,5% |
| + ochrona stosu | 1845 | 7243 | razem -4,9% do -5% |
| + PIE i oznaczanie | około 1800 | około 6750 | na obciążonym komputerze, z szumem |
W sumie koszt na ścieżce uzgadniania to więc około 5% dla części mierzonych czysto, a z PIE i oznaczaniem wychodzi około 7% na jednym i około 11% na czterech rdzeniach, ale ten pomiar jest zaszumiony. Uważamy go za akceptowalny, lecz nie powtórzyliśmy go na VPS-ie x86_64.
Warto porównać z liczbą z pierwszego tekstu (862 uzgodnienia na sekundę na jednym rdzeniu). To samo narzędzie dziś, ze sterownikiem sprzętowego szyfrowania na aarch64 (X0b) i serią przyspieszeń, które przyszły od tamtej pory, daje 1941 przed utwardzaniem. Nie wyodrębniliśmy wkładu każdej zmiany, więc o przyczynie nic nie twierdzimy.
Jak to sprawdzono, a czego nie. H5 dodał Miri na podzbiorze crate'ów hosta, dowody Kani, sanitizery, model-fuzzer dla ACL i raporty pokrycia. Kani udowodnił record_len w TLS, parse w TCP i dwie arytmetyki koła timerów. Nie udowodniono (przekroczony czas): obu dekoderów MQTT, obu parsowań konfiguracji i zachowania koła timerów; zadanie CI dla Kani dlatego jeszcze nie blokuje. Miri nie potrafi wykonywać asemblera. Łańcuch dostaw (H9): SBOM CycloneDX z 215 komponentami, cargo-vet przechodzi z 39 w pełni zrecenzowanymi i 140 wyjątkami, a cargo-deny jest skonfigurowany tak, by traktować zalecenia o podatnościach jako błąd CI (te zadania CI były dotąd sprawdzone tylko lokalnie, nie na GitHubie, a test negatywny nie został uruchomiony), wraz z VULNERABILITY-HANDLING.md. Szczery stan: ze 67 wersji crate'ów zewnętrznych linkowanych do jądra 53 to wyjątki, a nie recenzje. Vet dowodzi, że zbioru zależności nie da się zmienić bez widocznej różnicy, a nie że TCB został przejrzany. Własnej recenzji żadnego crate'a nie napisaliśmy.
SNTP (T1) jest pod tym względem małą opowieścią o bezpieczeństwie: jest opcjonalny, tylko wobec prywatnych serwerów IPv4, z maksymalnym skokiem i warunkiem zgodności źródeł (mediana, większość w ciągu jednej sekundy). Zegar, który ktokolwiek po drodze może przesunąć, nie jest zegarem, któremu wolno ufać przy ważności certyfikatów.
Na Macu: aarch64 natywnie i x86_64 przez emulację
Rozwijamy na Macu, a ten ma dwa sposoby uruchamiania tego samego kodu i bardzo się one różnią.
| aarch64 pod HVF | x86_64 pod TCG | |
|---|---|---|
| Co to jest | wykonanie natywne, szybkość bliska prawdziwego procesora | QEMU emuluje instrukcje |
| Uzgadnianie w gościu | około 0,58 ms (zmierzone po raz pierwszy około M13); dp-bench dziś 1941/s na rdzeń |
około 3,3 ms |
| Scenariusze smoke TLS | szybkie | około 15 minut na scenariusz |
| MTE | nie (nie ma go na HVF) | x86 go nie ma; oznaczanie sterty działa tylko dla gościa aarch64 pod TCG |
| Do czego służy | prawdziwe pomiary | poprawność: ścieżka x86 jest sprawdzana, liczby nie są wydajnością |
Zasada jest prosta: liczby pod TCG to emulacja i nie cytujemy ich jako wydajności. Wydajność x86 mierzymy tylko na prawdziwym VPS-ie. Dlatego liczby kryptograficzne powyżej pochodzą z VPS-a Contabo, a nie z Maca.
Lekcje
- Mierz na prawdziwej maszynie, zanim zaczniesz optymalizować. Pierwsze podejście polegało na optymalizowaniu tego, co uważaliśmy za wolne; dopiero diagnostyka na VPS-ie pokazała 1,2 ms i gdzie dokładnie ucieka czas.
- Przeciążenie to osobna cecha. System szybki pod obciążeniem może się załamać powyżej niego; odrzucenie musi być tańsze niż przyjęcie.
- Profil zaskakuje. Największy pojedynczy zysk dla małych wiadomości nie był w kryptografii, tylko w kopiowaniu pamięci.
- Dysk bez systemu plików wymaga więcej pracy, nie mniej. To, co system plików zwykle daje (wyrównanie, rozrost, discard, kopie, kontrole), musimy napisać i sprawdzić sami, każdy krok ze wstrzykiwaniem błędów.
- Uczciwość w liczbach. Pomiary pamięci na współdzielonym VPS-ie wahają się nawet do 10 razy między uruchomieniami i nic nie mówią o kodzie, więc ich nie podajemy. Liczby kryptograficzne są stabilne do około 10%.
- Tempo rozwoju to nie dojrzałość. Testy i ADR-y rosną razem z kodem, ale cztery dni to nie jest prawdziwa eksploatacja.
Co dalej
- H6: SQLite wyizolowany na poziomie EL0, tak aby błąd w bazie nie otwierał jądra.
- H7: reszta bezpieczeństwa czasu (NTS i obsługa skoków); SNTP to dopiero pierwszy krok.
- X11: porównanie z Linuksem i Mosquitto na tym samym VPS-ie, dopiero wtedy ma sens mówić o względnej szybkości.
- Wydanie: pierwsza publiczna wersja dla VPS (KVM), z podpisanymi artefaktami i instrukcjami.
- Po wydaniu: bare metal (X12 do X16), IOMMU i Secure Boot oraz szyfrowanie dysku.
Kod jest na github.com/zeljan-alduk/urMQTT, a docs/PROGRESS.md nadal jest jedynym źródłem prawdy o tym, co naprawdę działa. Pre-alfa, jak poprzednio; zgłoszenia błędów i zgłoszenia dotyczące bezpieczeństwa zgodnie z instrukcją w SECURITY.md są mile widziane.
Komentarze
Zasady komentowania