urMQTT na pravem VPS-ju: ISO, rokovanje v 0,24 ms, lasten disk in utrjeno jedro
Kaj se je zgodilo s tremi obljubami iz prvega besedila o urMQTT (en ISO, surovi disk brez datotečnega sistema, dokazljivo varno) po prvih štirih dneh razvoja, z meritvami, tabelami in seznamom tega, kar še ni končano.

V prvem besedilu o urMQTT smo opisali unikernel, ki je MQTT posrednik, do mejnika M17, in končali s tremi obljubami pod naslovom »Kaj sledi«: en ISO, ki se zažene na več platformah, surovi disk brez datotečnega sistema in varnost, ki jo je mogoče dokazati, ne le zatrjevati. Nič od tega takrat še ni bilo zgrajeno. To besedilo spremlja, kaj se je zgodilo s posamezno obljubo, v enakem vrstnem redu kot prej: kaj smo načrtovali, kaj smo zgradili, kaj nas je presenetilo in kaj je še odprto. Ob tem bomo pokazali tudi, koliko časa je to trajalo, saj je tudi to del zgodbe.
Ena opomba vnaprej: projekt je še vedno v predalfa fazi. Številke v besedilu merijo kodo in teste, ne zrelosti v produkciji, prva objava pa je namenoma omejena na VPS-je s KVM-jem. Bare metal ostaja za pozneje.
Kako je to izgledalo skozi čas
Prvi commit je nastal 28. oktobra 2026 ob 23:48 (M0). Do 31. oktobra ob 23:00 je v repozitoriju 119 commitov, razporejenih kot 3, 39, 38 in 39 po koledarskih dneh. Takoj povejmo tudi, kako je to mogoče: projekt gradimo s Claude Code, commiti pa nosijo trailerje Co-Authored-By: Claude. Štirje koledarski dnevi zgodovine ne pomenijo, da je ena oseba v štirih dneh napisala 90 000 vrstic; pomenijo, da se načrt, implementacija, testi in ADR-ji pišejo v kratkih, zaprtih zankah, vsak korak pa se konča z enakim obredom preverjanja kot prej. Spodnje številke govorijo o količini kode in testov, ne o tem, koliko dejanskega dela je bilo vloženega.
Graf kaže tri krivulje, ki rastejo skoraj skupaj, kar je tudi namen: testi in zapisane odločitve sledijo kodi, ne zaostajajo za njo. Naklon je najstrmejši 29. oktobra, ko nastane celotna pot od jedra do posrednika. Pozneje se vrstice dodajajo počasneje, testi pa rastejo hitreje od kode (27 testov na 1,6 tisoč vrstic na začetku, 739 na 89,8 tisoč danes).
| Mejnik | Commit | Čas | Vrstic Rusta | Testov | ADR-jev |
|---|---|---|---|---|---|
| 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 |
| danes | HEAD | 31.10. 23:00 | 89 790 | 739 | 104 |
Vrstice so vse datoteke .rs zunaj vendor/ in fuzz/, testi so funkcije #[test]. Kaj je dodal kateri del, po datumu iz zgodovine gita:
| Datum | Mejniki | Kaj je bilo dodano |
|---|---|---|
| 29.10. | M0 do M17 | jedro, omrežje, TLS, posrednik, SQLite, več jeder, lasten TCP, utrjevanje, ponovljiva izdaja |
| 29.10. zvečer in 30.10. | X0 do X10, S1, H2, H3 (X0 se začne 29.10. ob 20:54) | port na x86_64, univerzalni ISO, en disk in samonamestitveni ISO, preverjanja TLS, napadalski odjemalci |
| 31.10. | O1 do O4, T1, PERF, H4, H5, H9, S2 do S6 | vpogled in konzola prek MQTT, živi način in diagnostika, SNTP, pospešitve, utrjevanje, verifikacija, dobavna veriga, shranjevanje |
Obljuba 1: en ISO za več platform
Kaj smo načrtovali. Ena datoteka, ki se zažene na aarch64 UEFI, x86_64 UEFI in x86_64 BIOS. Kaj smo zgradili. Port na x86_64 (X0 do X10) in univerzalni ISO z zagonskim nalagalnikom Limine, en hibridni ISO, s katerega se zaženejo vse te kombinacije; dimni testi ga zaženejo na štirih virtualnih strojih. Meni ima tri vnose: urMQTT (živi način, teče iz RAM-a, nič ne piše na disk), namestitev na disk računalnika, na katerem je zagnan (namestitev zapiše GPT in podatke, nato ugasne stroj), in Diagnostika (zaznavanje strojne opreme in meritve).

Prva prava tarča je bil VPS, ne fizični računalnik: odločitev je, da prva izdaja podpira samo VPS (KVM), bare metal pa pride za njo. Testirali smo na VPS-ju Contabo s procesorjem AMD EPYC (družina 23, Zen 1), 4 vCPU-ji in 8 GiB RAM-a, z omrežjem virtio-net in diskom virtio-scsi. Prvi poskus nas je presenetil: ISO, priključen kot virtualni CD, je zagnal privzeti vnos, produkcijska slika pa je zavrnila streženje z medija samo za branje in se ponastavila, tako da smo dobili neskončno zanko meni, jedro, ponastavitev brez česar koli berljivega. Danes usodna napaka v produkcijski sliki pusti sporočilo na konzoli 60 sekund pred ponastavitvijo, z optičnega medija pa izpiše, da je bil zagon z medija samo za branje, in se ustavi. Živi način v RAM-u (O3) je pozneje to težavo za običajni primer zaobšel.
Diagnostični vnos je nastal iz te izkušnje: stroj, ki ga ne vidimo, moramo znati vprašati, kaj ima. Izpiše CPU, RAM, omrežje, diske in naprave PCI, izmeri prepustnost pomnilnika, disk in kriptografijo, enake meritve pa izpiše tudi kot JSON na serijska vrata. Pisanje na disk je dovoljeno samo v nerazporejenem prostoru in šele, ko vtipkate WIPE; na spodnjem posnetku je bilo to preskočeno.

Kaj je odprto. Prava strojna oprema. Po prvi izdaji sledi bare metal (zagon z USB-ja, NVMe in AHCI, omrežna kartica pravega računalnika), skupaj z IOMMU-jem in Secure Bootom, ker brez IOMMU-ja lahko zlonamerna ali okvarjena naprava piše po celotnem pomnilniku. V tej različici o tem ne trdimo ničesar.
Rokovanje: od 1,2 ms do 0,24 ms
Prva meritev diagnostike na VPS-ju je dala neprijetno številko: rokovanje mTLS, samo kriptografija na strani strežnika, je trajalo 1,2 ms. Na aarch64 smo že uporabljali strojne ukaze AES in SHA, na x86_64 pa nismo imeli nič podobnega za javni ključ: RustCrypto je prenosljiv in constant-time, zato pa tudi počasen. Spustile so ga tri spremembe.
- Za P-256 smo prevzeli zbirnik
nistz256iz OpenSSL-a in ringa (ADR-130), s tabelami s fiksno bazo za podpisovanje in preverjanje podpisov CA (ADR-110, ADR-111). - X25519 je prepisan na štiri 64-bitne ude z ukazi MULX/ADX (ADR-131).
- Nato smo profilirali še preostanek (ADR-132, ADR-200).
| Meritev na VPS-ju (strežnik, mikrosekunde) | Prej | Potem |
|---|---|---|
| rokovanje mTLS, vsa kriptografija | 1200 | okoli 240 (237 v meritvi z grafa, 244 na posnetku) |
| X25519 | 146 | 48 |
| podpis ECDSA | 673 | 34 |
| preverjanje ECDSA | 309 | 90 (107 na posnetku) |
| rokovanje, stari memcpy, isti zagon | 278,8 | 229,4 (-17,7%) |
Številke se med zagoni razlikujejo za okoli 10%, zato smo zapisali razpon namesto ene vrednosti. Skupaj je to približno petkrat manj procesorskega časa za rokovanje.
Kaj smo se naučili o memcpy-ju. Ko je kriptografija padla, je profil pokazal nekaj, česar nismo pričakovali: funkcije mem* (memcpy, memmove, memset) so porabile 7% rokovanja, 13 do 39% poti zapisa TLS (39% za 64 B, 13% za 4 KiB) in 17 do 34% razdeljevanja PUBLISH-a. Zamenjali smo jih z zbirnikom SSE2 za x86_64 (ADR-200), z diferencialnimi testi proti referenčni implementaciji za vse velikosti, poravnave in prekrivanja. Na klic je to 55 do 75 ciklov proti 7 do 12 za 1 do 64 bajtov.
| Pot | Delež mem* prej | Hitreje potem |
|---|---|---|
| rokovanje | 7% | 17,7% |
| zapis TLS 64 B | 39% | 26,4% |
| zapis TLS 256 B | 37% | 19,6% |
| zapis TLS 1 KiB | 24% | 16,1% |
| zapis TLS 4 KiB | 13% | 6,6% |
| razdeljevanje sporočil | 17 do 34% | 7 do 34% |
Branje grafa: manjši ko je zapis, večji del časa odide za kopiranje, zato je tudi dobitek večji. Za majhna sporočila, ki so večina prometa MQTT, je to tisto, kar šteje.
Kaj to ne pomeni. Na izvornem x86_64 smo OpenSSL 3.5.5 merili samo kot referenco za zbirnik (ADR-130), ne kot primerjavo izdelkov. Primerjave Linux plus Mosquitto na istem VPS-ju (naloga X11) še nismo naredili. Zato ne trdimo, da je urMQTT hitrejši od Linuxa: ni izmerjeno. Zgornje številke primerjajo urMQTT sam s seboj.
Preobremenitev je bil drugi rezultat. Na VPS-ju, z enega Maca (RTT 35 ms), je 128 odjemalcev dalo 618 rokovanj na sekundo, 256 odjemalcev 393, 512 odjemalcev pa 67, z na tisoče zavrnjenih povezav. To ni bil plato, ampak sesutje. Vzroke smo našli v kodi (dva sta potrjena v QEMU-ju/HVF-ju, na VPS-ju se ju ni dalo ponoviti), več hkrati: zavrnjena povezava je odjemalca vrnila prek naslednjega segmenta, ki nosi veljaven piškotek SYN, zavrnitev z RST je odjemalcu sporočala, naj takoj poskusi znova, sprejem pa je bil O(vrsta). Popravek (ADR-080 do ADR-082) izpusti SYN, ki ne bi bil sprejet, si zapomni zavrnjene tokove, drži sprejem v O(1) in šteje rokovanja po jedru. Možno je (hipoteza, ni preverjeno), da je bil del vzroka tudi na odjemalcu: orodje za obremenitev je zapiralo prvo, zato je en Mac s 16 384 efemernimi vrati in TIME_WAIT-om 30 sekund omejen na okoli 550 povezav na sekundo. Zato pravega stropa strežnika še nismo izmerili.
Obljuba 2: surovi disk, a previden
Kaj smo načrtovali. Obdržati lasten razpored brez datotečnega sistema in dodati vse tisto, kar datotečni sistem sicer daje: poravnavo, rast, discard, varnostne kopije in preverjanja. Zakaj ne datotečni sistem. O tem smo razpravljali in odločitev je potrjena: urMQTT piše eno bazo, en telemetrijski dnevnik in lastne metapodatke, ne potrebuje imenikov, dovoljenj ali imen datotek, vsaka dodatna plast pa bi bila še več kode v zaupanja vrednem delu in še en način, da zapis tiho izgine. Disk je GPT s particijo ESP in eno particijo podatkov urMQTT, na kateri je lasten razpored.
Kaj smo zgradili (S2 do S6).
- Razpored v2 (S2). Topologija diska, poravnava na 1 MiB ali optimalno velikost V/I, celi fizični bloki, par superblokov A/B z generacijo (ADR-140). Razpored v1 kernel zavrne z jasnim sporočilom, preseli pa ga izvenmrežno orodje
urmqtt-disk migrate(ADR-141): privzeto je poskusni prehod, kopira se in šele nato preklopi, z vrnitvijo in vbrizgavanjem napak pri vsakem zapisu. - Rast (S3). Ob zagonu lahko disk zraste (nikoli se ne zmanjša), na virtio-blk pa tudi med delovanjem, prek prekinitve spremembe konfiguracije (ADR-142, ADR-143).
- Discard in write-zeroes (S4). Tanko dodeljeni diski vračajo prostor gostitelju. V testu je datoteka qcow2 na gostitelju padla s 62 na 16 MiB, ko so potekli telemetrijski segmenti, obnovitev pa nikoli ne zaupa vsebini, ki je bila zavržena.
- Varnostna kopija baze na disku (S5). Kopija A/B s SQLite online backup API-jem v korakih po 16 strani: najdaljši korak 4,6 ms proti 445 ms, kadar se kopira z enim klicem. Commiti med kopiranjem so imeli maksimum 2,4 ms proti 2,2 ms brez njega. Če
quick_checkob zagonu ne uspe, se baza obnovi iz kopije. - Čiščenje in omejen zagon (S6). Scrubber bere disk z omejenim V/I proračunom in poroča o poškodovanih zapisih, zagon pa prebere samo najnovejši segment vsake particije, tako da čas zagona ne raste z velikostjo telemetrijske regije. Tako smo zaprli težavo, ki smo jo izračunali vnaprej: na velikem disku bi popolna obnovitev trajala od minut do več deset minut.
Celotna skupina dimnih testov za shranjevanje prehaja na obeh arhitekturah: 79 uspešnih, 0 neuspešnih, na aarch64 pod HVF-jem in na x86_64 pod TCG-jem. Izmerili smo tudi, da S4 ni upočasnil baze: 53,3 tisoč vstavljanj na sekundo v paketih prej in potem, brez merljive razlike.
Kaj je presenetilo. Koliko »preprostega« razporeda odpade na robne primere: virtio-scsi in virtio-blk poročata topologijo na različne načine, nepopoln zapis sredi migracije pa mora vedno pustiti vsaj eno veljavno različico. Zato je vsak zapis na poti migracije in rasti šel skozi vbrizgavanje napak.
Kaj je odprto. Strani SQLite nimajo kontrolne vsote, zato spremenjenega bita znotraj vrednosti v vrstici baza ne bo zaznala; telemetrijski zapisi in vsebina kopij imajo CRC in se zaznajo. Šifriranje diska (S7) ni končano.
Obljuba 3: dokazljivo varno, kolikor nam uspe
Utrjevanje (H4). Alokator briše sproščeni pomnilnik in ga preverja ob ponovni uporabi, zazna dvojno sprostitev, kazalce proste liste veže z XOR-om in ob sprostitvi ničli ključe (ADR-170). Zaščita sklada (-Zstack-protector=strong, 1315 funkcij, SQLite pa s -fstack-protector-strong, ADR-171). Poročila o napakah v produkciji ne vsebujejo registrov ali naslovov (ADR-172). KASLR na obeh arhitekturah: kernel je PIE in ga Limine premakne za okoli 19 bitov, naključno pa se postavijo tudi kopica, skladi in okno MMIO (ADR-173). Na aarch64 MTE označuje kopico (ADR-174), vendar samo pod TCG-jem: HVF in x86 ga nimata. kCFI smo ovrednotili in ga nismo sprejeli, ker prevajalnik na pripetem nightlyju pade z notranjo napako; IOMMU je odložen skupaj z bare metalom (ADR-175, ADR-176).
Ceno smo izmerili, ne predpostavili. Na poti rokovanja (dp-bench, aarch64 HVF, polna rokovanja TLS 1.3 + mTLS + CONNECT):
| Korak | 1 jedro | 4 jedra | Sprememba |
|---|---|---|---|
| brez utrjevanja | 1941 | 7620 | |
| + alokator | 1898 | 7429 | -2,2% do -2,5% |
| + zaščita sklada | 1845 | 7243 | skupaj -4,9% do -5% |
| + PIE in oznake | okoli 1800 | okoli 6750 | na obremenjenem računalniku, šumno |
Skupaj je torej cena na poti rokovanja približno 5% za dele, izmerjene čisto, s PIE-jem in oznakami pa pride okoli 7% na enem in okoli 11% na štirih jedrih, a je ta meritev šumna. Menimo, da je sprejemljiva, vendar je nismo ponovili na VPS-ju x86_64.
Vredno je primerjati s številko iz prvega besedila (862 rokovanj na sekundo na enem jedru). Isto orodje danes, s strojnim šifriranjem na aarch64 (X0b) in nizom pospešitev, ki so prišle od takrat, da 1941 pred utrjevanjem. Prispevka vsake spremembe nismo izolirali, zato o vzroku ne trdimo ničesar.
Kako je preverjeno in kaj ni. H5 je dodal Miri na podmnožico gostiteljskih cratov, dokaze Kani, sanitizerje, model-fuzzer za ACL in poročila o pokritosti. Kani je dokazal record_len v TLS-u, parse v TCP-ju in dve aritmetiki kolesa časovnikov. Ni dokazano (potekel čas): oba dekodirnika MQTT, oba razčlenjevanja konfiguracije in obnašanje kolesa časovnikov; delo CI za Kani zato še ne blokira. Miri ne more izvajati zbirnika. Dobavna veriga (H9): CycloneDX SBOM s 215 komponentami, cargo-vet prehaja s 39 v celoti pregledanimi in 140 izjemami, cargo-deny pa je nastavljen tako, da varnostna obvestila obravnava kot padec CI-ja (ta dela CI so bila doslej preverjena samo lokalno, ne na GitHubu, negativni test pa ni bil zagnan), skupaj z VULNERABILITY-HANDLING.md. Iskreno stanje: od 67 različic tretjih cratov, povezanih v kernel, jih je 53 izjem, ne pregledov. Vet dokazuje, da se množica odvisnosti ne more spremeniti brez vidne razlike, ne da je bil TCB pregledan. Lastnega pregleda nobenega cratea nismo napisali.
SNTP (T1) je v tem smislu majhna zgodba o varnosti: je neobvezen, samo proti zasebnim strežnikom IPv4, z največjim skokom in pogojem, da se viri ujemajo (mediana, večina znotraj ene sekunde). Ura, ki jo lahko premakne kdor koli na poti, ni ura, ki ji smemo zaupati pri veljavnosti certifikatov.
Na Macu: aarch64 izvorno in x86_64 z emulacijo
Razvijamo na Macu, ta pa ima dva načina za zagon iste kode in sta zelo različna.
| aarch64 pod HVF-jem | x86_64 pod TCG-jem | |
|---|---|---|
| Kaj je to | izvorno izvajanje, hitrost blizu pravega procesorja | QEMU emulira ukaze |
| Rokovanje v gostu | okoli 0,58 ms (prvič izmerjeno okoli M13); dp-bench danes 1941/s na jedro |
okoli 3,3 ms |
| Scenariji dimnega testa TLS | hitri | okoli 15 minut na scenarij |
| MTE | ne (na HVF-ju ga ni) | x86 ga nima; označevanje kopice deluje samo za gosta aarch64 pod TCG-jem |
| Čemu služi | prave meritve | pravilnost: pot x86 se preverja, številke niso zmogljivost |
Pravilo je preprosto: številke pod TCG-jem so emulacija in jih ne navajamo kot zmogljivost. Zmogljivost x86 merimo samo na pravem VPS-ju. Zato kripto številke zgoraj izhajajo z VPS-ja Contabo, ne z Maca.
Lekcije
- Merite na pravem stroju, preden optimirate. Prvi pristop je bil optimirati tisto, za kar smo mislili, da je počasno; šele diagnostika na VPS-ju je pokazala 1,2 ms in kje natančno odhaja čas.
- Preobremenitev je ločena lastnost. Sistem, ki je hiter pod obremenitvijo, se lahko sesuje nad njo; zavrnitev mora biti cenejša od sprejema.
- Profil preseneča. Največji posamični dobitek za majhna sporočila ni bil v kriptografiji, ampak v kopiranju pomnilnika.
- Disk brez datotečnega sistema zahteva več dela, ne manj. Tisto, kar datotečni sistem sicer daje (poravnavo, rast, discard, kopije, preverjanja), moramo napisati in preveriti sami, vsak korak z vbrizgavanjem napak.
- Poštenost v številkah. Meritve pomnilnika na deljenem VPS-ju se med zagoni razlikujejo do 10-krat in o kodi ne povedo ničesar, zato jih ne navajamo. Kripto številke so stabilne do okoli 10%.
- Hitrost razvoja ni zrelost. Testi in ADR-ji rastejo skupaj s kodo, a štirje dnevi niso pravo obratovanje.
Kaj sledi
- H6: SQLite, izoliran na ravni EL0, tako da napaka v bazi ne odpre jedra.
- H7: preostanek varnosti časa (NTS in obdelava skokov); SNTP je šele prvi korak.
- X11: primerjava z Linuxom in Mosquittom na istem VPS-ju, šele takrat je smiselno govoriti o relativni hitrosti.
- Izdaja: prva javna objava za VPS (KVM), s podpisanimi artefakti in navodili.
- Po izdaji: bare metal (X12 do X16), IOMMU in Secure Boot ter šifriranje diska.
Koda je na github.com/zeljan-alduk/urMQTT, docs/PROGRESS.md pa je še naprej edini vir resnice o tem, kaj res deluje. Predalfa, kot prej; prijave napak in varnostne prijave po navodilih v SECURITY.md so dobrodošle.
Komentarji
Pravila komentiranja