5x snellere Edge Functions: Hoe we v8 isolates vervingen door Firecracker MicroVM's
Dagelijks worden er ongeveer een miljard Edge Functions op Netlify uitgevoerd — van Sunweb die pagina's personaliseert tot Loto-Québec die verkeer routeert op basis van een cookie-check, en honderdduizenden andere sites die alles doen van personalisatie tot routering en authenticatie. Dit alles draait op een volledige JavaScript-runtime die meeschaalt met het verkeer van onze klanten.
Dit vormt een aanzienlijke technische uitdaging, aangezien we streven naar een zo laag mogelijke latentie. Om tienduizenden, en soms honderdduizenden, edge functions per seconde uit te voeren, moeten we elk verzoek verwerken, correct routeren, rekencapaciteit (compute capacity) toewijzen en zowel onze platformcode als de code van de klant opstarten. Dit alles moet binnen enkele milliseconden gebeuren.
In de afgelopen maanden heeft ons team de infrastructuur achter Edge Functions herbouwd, in nauwe samenwerking met het team van Unikraft. Voorheen werden verzoeken verzonden naar een externe hosted execution service. Tegenwoordig draaien ze op MicroVM's binnen ons eigen edge-netwerk — wat bij de mediaan ongeveer 5x sneller is. Deze verschuiving verbetert ook de beveiliging en betrouwbaarheid, en opent meer mogelijkheden voor het uitvoeren van complexe compute-taken aan de edge.
Dit verandert niets aan de manier waarop Edge Functions worden geschreven of gebruikt — URL-imports, npm-pakketten, Node built-ins, netlify.toml-declaraties en lokale ontwikkeling werken allemaal precies zoals voorheen. Het is nu simpelweg sneller en veerkrachtiger. In dit artikel delen we meer over de nieuwe architectuur en onze ervaringen bij het bouwen van een nieuw compute-platform dat in staat is om hoge volumes te verwerken met een lage performance-overhead.
Eerst de cijfers
Een edge function draait vóór een site, bij elk verzoek dat ermee overeenkomt. De tijd die dit kost, is tijd die een klant moet wachten, dus milliseconden tellen hier zwaarder dan bijna overal anders.
Een warme aanroep (routing naar een compute node, betreden van een MicroVM, uitvoeren van de functie, genereren van respons-headers) kost nu:
- Mediaan (p50): ~5–6ms (terwijl dit voorheen 25–40ms was op onze oude infrastructuur).
- p99 aanroepen: 47,4% sneller.
- Beschikbaarheid: 99,998%.
- Log-levering: 5x snellere levering van edge function logs.
Ook een koude aanroep (cold invocation) is vermeldenswaardig. Wanneer een verzoek arriveert in een regio waar nog geen compute node actief is, moeten de relevante images eerst worden opgehaald voordat er iets kan worden uitgevoerd. Dit gebeurt bij ongeveer 1,2% van de aanroepen en duurt gemiddeld ongeveer 9ms.
Wat er gebeurt tijdens een verzoek
Hieronder volgt het pad dat een enkel verzoek aflegt: het arriveert bij de edge node, wordt omgezet in een specificatie, wordt gerouteerd naar een compute node en wordt vervolgens overgedragen aan een MicroVM (die al kan bestaan of ter plekke wordt opgestart).
1. Verzoek arriveert bij de edge node
Elk verzoek landt op de Netlify edge node die het dichtst bij de client staat. De node beëindigt de TLS-verbinding en controleert of het request-pad overeenkomt met de routes van de Edge Functions voor die specifieke deploy.
Als er niets overeenkomt, gaat het verzoek zoals gebruikelijk door naar de cache en naar de origin. Als er wel een match is, is dit het punt waar het verzoek voorheen ons netwerk verliet. Met de oude infrastructuur ging het over het internet, werd de edge function uitgevoerd en kwam het terug naar ons om te worden doorgegeven. Met het nieuwe compute-platform wordt het verzoek doorgestuurd naar een compute node binnen ons eigen netwerk.
2. Het creëren van een Edge Function-service
Wanneer de compute node het verzoek ontvangt met de machine-specificatie en het service-ID, controleert deze eerst of er al een service met dat ID bestaat. Zo ja, dan wordt het verzoek doorgestuurd naar de service om in de MicroVM te worden geplaatst.
Een service stelt ons in staat om meerdere MicroVM's te koppelen aan de Edge Functions van dezelfde site en laat ons parameters configureren voor het opschalen en afschalen van MicroVM's. We configureren bijvoorbeeld elke service zo dat een MicroVM slechts een vast aantal verzoeken afhandelt voordat deze wordt afgesloten, om te voorkomen dat MicroVM's onbeperkt blijven draaien. We gebruiken dezelfde parameters om te bepalen wanneer we preventief een nieuwe MicroVM opstarten.
Als er nog geen service bestaat voor de edge functions van de site op de compute node, wordt deze aangemaakt. We controleren dan of alle images uit de machine-specificatie op de schijf staan. Ontbrekende images worden opgehaald van de edge node en naar de schijf geschreven. Dit betekent dat we alleen de images ophalen van edge functions die daadwerkelijk verkeer ontvangen in die regio.
3. De edge node schrijft een specificatie
Voordat het verzoek wordt verzonden, schrijft de edge node een specificatie voor de machine die de functie zal uitvoeren. De spec benoemt drie images: de runtime, ons platform-image en het edge function-image. Ook worden de limieten voor CPU, geheugen en verbindingen vastgesteld.
De specificatie reist mee met elk verzoek. De hash en site-specifieke informatie hiervan vormen samen het service-ID. Dit zorgt voor isolatie: twee deploys met verschillende code of verschillende omgevingsvariabelen zijn verschillende services en delen nooit een MicroVM.
Deze isolatie is cruciaal voor het voorkomen van fouten. Een potentieel gecompromitteerde deploy draait in een aparte MicroVM; zelfs als deze uit de runtime ontsnapt, kan deze andere klanten of de compute-laag zelf niet infecteren. V8 isolates bieden, ondanks hun naam, niet dit niveau van isolatie.
4. Het kiezen van een compute node
Elke regio heeft een groep compute nodes. De edge node kiest er één voor de service via rendezvous hashing: dezelfde service landt telkens op dezelfde node. Dit houdt de MicroVM "warm" en zorgt dat de code al op de schijf en in de cache staat nadat deze eenmaal is gelezen. Deze persistentie vormt onze caching-strategie; als we verzoeken gelijkmatig over het zwermnetwerk zouden verspreiden, zouden we veel meer cold starts hebben.
Om "hot spots" te voorkomen — waarbij één drukke functie concurreert om resources met andere functies op dezelfde box — ontspannen we deze persistentie boven een bepaalde drempelwaarde. We spreiden de service dan over een selectie van nodes, zodat we plotselinge verkeerspieken van één klant kunnen absorberen zonder andere services te beïnvloeden.
Zodra een node is gekozen, wordt de code van de functie geladen. Een node die de functie al eerder heeft uitgevoerd, heeft deze al aanwezig. Een node die de functie voor het eerst ziet, haalt deze één keer op en cached deze.
5. Het opstarten van de MicroVM
Elke functie draait in zijn eigen Firecracker MicroVM. Deze worden in minder dan een milliseconde aangemaakt en starten in ongeveer 2ms (p99), omdat de VM een gestripte Linux-omgeving start in plaats van een volledig besturingssysteem. De bestanden van de edge function worden gemount als een ongecomprimeerd EROFS-image en vervolgens memory-mapped, zodat de VM alleen de delen van de bundle leest die daadwerkelijk worden gebruikt.
Wanneer de MicroVM opstart en de JavaScript-server begint te luisteren op een poort, maken we een snapshot van de MicroVM. Wanneer de edge function niet wordt aangeroepen, schalen de MicroVM's terug naar nul in plaats van idle te blijven. Bij de volgende aanroep starten we een nieuwe MicroVM vanuit die snapshot. De snapshot is memory-mapped, waardoor de VM kan beginnen met uitvoeren zonder te wachten tot de volledige snapshot in het geheugen is geladen.
De levenscyclus van de VM — boot, snapshot, restore en scale-to-zero — wordt verzorgd door het product van Unikraft.
6. Uitvoering en afhandeling van de respons
Op basis van ervaringen uit het verleden hebben we deze iteratie geoptimaliseerd voor performance en debugbaarheid. We hebben bijvoorbeeld lokale DNS-resolvers op de compute nodes geplaatst om problemen te voorkomen. Ook hebben we de verzamelde metrieken uitgebreid (zoals opstarttijd en tijd tot de eerste open poort) en circuit breakers geïmplementeerd voor snelle rerouting en uitfasering van compute nodes.
Op een warme instantie voegt dit hele proces ongeveer 6ms toe. Niets hiervan verlaat ons netwerk, waardoor we volledige controle hebben over de request-cyclus.
Het ontwerpen van veerkrachtige compute-infrastructuur
Bij het bouwen van dit systeem optimaliseren we voor twee zaken: de ervaring van de eindgebruiker en de veerkracht van de uitrol. We moeten wijzigingen snel kunnen uitrollen, maar ook net zo snel kunnen terugdraaien.
De compute nodes zijn gebouwd op een base image van Unikraft. Deze nodes worden apart van onze edge nodes gebouwd om de edge nodes licht en snel te houden, om verschillende instance-types te kunnen gebruiken en om deze nodes onafhankelijk te kunnen schalen.
Een control plane houdt bij welke compute nodes bestaan en gezond zijn; de edge nodes pollen deze lijst. Dit stuurt ook de deploys aan: een nieuwe vloot nodes wordt naast de huidige vloot opgestart, schaalt mee en neemt het verkeer pas over zodra deze gezond is.
Het is live
De herbouw van onze edge compute-architectuur is meer dan alleen een snelheidswinst. Het is een fundament waar we op kunnen blijven bouwen en waar we meer controle over hebben. Het mooie is dat dit nu al je productieverkeer verwerkt, tegen dezelfde prijs, zonder dat er een migratiestap nodig is of er iets in projecten hoeft te worden aangepast.
Omdat we de compute nu zelf beheren, kunnen we de limieten van Edge Functions zelf verhogen. Dit maakt drie zaken mogelijk die voorheen niet haalbaar waren:
- Volledige ondersteuning voor npm-pakketten: npm-pakketten werkten tot nu toe in bèta, met beperkingen rond native binaries en het importeren van bestanden tijdens runtime. Een echte VM met een echt bestandssysteem verwijdert de meeste van deze beperkingen.
- Herziening van operationele limieten: De huidige limieten (50ms CPU per verzoek, 512MB geheugen en 20MB gecomprimeerde code) waren een gevolg van het isolate-gebaseerde model. Deze kunnen nu worden herzien.
- Compute binnen ons eigen netwerk: Alles wat afhankelijk is van het controleren van het netwerkpad, in plaats van het bereiken van een derde partij over het internet, kunnen we nu zelf bouwen.
We zijn nog niet klaar; we werken nu aan de limieten en onvolkomenheden die we voorheen niet konden aanpassen.
5x snellere Edge Functions: Hoe we v8 isolates vervingen door Firecracker MicroVM's
Dagelijks worden er ongeveer een miljard Edge Functions op Netlify uitgevoerd — van Sunweb die pagina's personaliseert tot Loto-Québec die verkeer routeert op basis van een cookie-check, en honderdduizenden andere sites die alles doen van personalisatie tot routering en authenticatie. Dit alles draait op een volledige JavaScript-runtime die meeschaalt met het verkeer van onze klanten.
Dit vormt een aanzienlijke technische uitdaging, aangezien we streven naar een zo laag mogelijke latentie. Om tienduizenden, en soms honderdduizenden, edge functions per seconde uit te voeren, moeten we elk verzoek verwerken, correct routeren, rekencapaciteit (compute capacity) toewijzen en zowel onze platformcode als de code van de klant opstarten. Dit alles moet binnen enkele milliseconden gebeuren.
In de afgelopen maanden heeft ons team de infrastructuur achter Edge Functions herbouwd, in nauwe samenwerking met het team van Unikraft. Voorheen werden verzoeken verzonden naar een externe hosted execution service. Tegenwoordig draaien ze op MicroVM's binnen ons eigen edge-netwerk — wat bij de mediaan ongeveer 5x sneller is. Deze verschuiving verbetert ook de beveiliging en betrouwbaarheid, en opent meer mogelijkheden voor het uitvoeren van complexe compute-taken aan de edge.
Dit verandert niets aan de manier waarop Edge Functions worden geschreven of gebruikt — URL-imports, npm-pakketten, Node built-ins, netlify.toml-declaraties en lokale ontwikkeling werken allemaal precies zoals voorheen. Het is nu simpelweg sneller en veerkrachtiger. In dit artikel delen we meer over de nieuwe architectuur en onze ervaringen bij het bouwen van een nieuw compute-platform dat in staat is om hoge volumes te verwerken met een lage performance-overhead.
Eerst de cijfers
Een edge function draait vóór een site, bij elk verzoek dat ermee overeenkomt. De tijd die dit kost, is tijd die een klant moet wachten, dus milliseconden tellen hier zwaarder dan bijna overal anders.
Een warme aanroep (routing naar een compute node, betreden van een MicroVM, uitvoeren van de functie, genereren van respons-headers) kost nu:
- Mediaan (p50): ~5–6ms (terwijl dit voorheen 25–40ms was op onze oude infrastructuur).
- p99 aanroepen: 47,4% sneller.
- Beschikbaarheid: 99,998%.
- Log-levering: 5x snellere levering van edge function logs.
Ook een koude aanroep (cold invocation) is vermeldenswaardig. Wanneer een verzoek arriveert in een regio waar nog geen compute node actief is, moeten de relevante images eerst worden opgehaald voordat er iets kan worden uitgevoerd. Dit gebeurt bij ongeveer 1,2% van de aanroepen en duurt gemiddeld ongeveer 9ms.
Wat er gebeurt tijdens een verzoek
Hieronder volgt het pad dat een enkel verzoek aflegt: het arriveert bij de edge node, wordt omgezet in een specificatie, wordt gerouteerd naar een compute node en wordt vervolgens overgedragen aan een MicroVM (die al kan bestaan of ter plekke wordt opgestart).
1. Verzoek arriveert bij de edge node
Elk verzoek landt op de Netlify edge node die het dichtst bij de client staat. De node beëindigt de TLS-verbinding en controleert of het request-pad overeenkomt met de routes van de Edge Functions voor die specifieke deploy.
Als er niets overeenkomt, gaat het verzoek zoals gebruikelijk door naar de cache en naar de origin. Als er wel een match is, is dit het punt waar het verzoek voorheen ons netwerk verliet. Met de oude infrastructuur ging het over het internet, werd de edge function uitgevoerd en kwam het terug naar ons om te worden doorgegeven. Met het nieuwe compute-platform wordt het verzoek doorgestuurd naar een compute node binnen ons eigen netwerk.
2. Het creëren van een Edge Function-service
Wanneer de compute node het verzoek ontvangt met de machine-specificatie en het service-ID, controleert deze eerst of er al een service met dat ID bestaat. Zo ja, dan wordt het verzoek doorgestuurd naar de service om in de MicroVM te worden geplaatst.
Een service stelt ons in staat om meerdere MicroVM's te koppelen aan de Edge Functions van dezelfde site en laat ons parameters configureren voor het opschalen en afschalen van MicroVM's. We configureren bijvoorbeeld elke service zo dat een MicroVM slechts een vast aantal verzoeken afhandelt voordat deze wordt afgesloten, om te voorkomen dat MicroVM's onbeperkt blijven draaien. We gebruiken dezelfde parameters om te bepalen wanneer we preventief een nieuwe MicroVM opstarten.
Als er nog geen service bestaat voor de edge functions van de site op de compute node, wordt deze aangemaakt. We controleren dan of alle images uit de machine-specificatie op de schijf staan. Ontbrekende images worden opgehaald van de edge node en naar de schijf geschreven. Dit betekent dat we alleen de images ophalen van edge functions die daadwerkelijk verkeer ontvangen in die regio.
3. De edge node schrijft een specificatie
Voordat het verzoek wordt verzonden, schrijft de edge node een specificatie voor de machine die de functie zal uitvoeren. De spec benoemt drie images: de runtime, ons platform-image en het edge function-image. Ook worden de limieten voor CPU, geheugen en verbindingen vastgesteld.
De specificatie reist mee met elk verzoek. De hash en site-specifieke informatie hiervan vormen samen het service-ID. Dit zorgt voor isolatie: twee deploys met verschillende code of verschillende omgevingsvariabelen zijn verschillende services en delen nooit een MicroVM.
Deze isolatie is cruciaal voor het voorkomen van fouten. Een potentieel gecompromitteerde deploy draait in een aparte MicroVM; zelfs als deze uit de runtime ontsnapt, kan deze andere klanten of de compute-laag zelf niet infecteren. V8 isolates bieden, ondanks hun naam, niet dit niveau van isolatie.
4. Het kiezen van een compute node
Elke regio heeft een groep compute nodes. De edge node kiest er één voor de service via rendezvous hashing: dezelfde service landt telkens op dezelfde node. Dit houdt de MicroVM "warm" en zorgt dat de code al op de schijf en in de cache staat nadat deze eenmaal is gelezen. Deze persistentie vormt onze caching-strategie; als we verzoeken gelijkmatig over het zwermnetwerk zouden verspreiden, zouden we veel meer cold starts hebben.
Om "hot spots" te voorkomen — waarbij één drukke functie concurreert om resources met andere functies op dezelfde box — ontspannen we deze persistentie boven een bepaalde drempelwaarde. We spreiden de service dan over een selectie van nodes, zodat we plotselinge verkeerspieken van één klant kunnen absorberen zonder andere services te beïnvloeden.
Zodra een node is gekozen, wordt de code van de functie geladen. Een node die de functie al eerder heeft uitgevoerd, heeft deze al aanwezig. Een node die de functie voor het eerst ziet, haalt deze één keer op en cached deze.
5. Het opstarten van de MicroVM
Elke functie draait in zijn eigen Firecracker MicroVM. Deze worden in minder dan een milliseconde aangemaakt en starten in ongeveer 2ms (p99), omdat de VM een gestripte Linux-omgeving start in plaats van een volledig besturingssysteem. De bestanden van de edge function worden gemount als een ongecomprimeerd EROFS-image en vervolgens memory-mapped, zodat de VM alleen de delen van de bundle leest die daadwerkelijk worden gebruikt.
Wanneer de MicroVM opstart en de JavaScript-server begint te luisteren op een poort, maken we een snapshot van de MicroVM. Wanneer de edge function niet wordt aangeroepen, schalen de MicroVM's terug naar nul in plaats van idle te blijven. Bij de volgende aanroep starten we een nieuwe MicroVM vanuit die snapshot. De snapshot is memory-mapped, waardoor de VM kan beginnen met uitvoeren zonder te wachten tot de volledige snapshot in het geheugen is geladen.
De levenscyclus van de VM — boot, snapshot, restore en scale-to-zero — wordt verzorgd door het product van Unikraft.
6. Uitvoering en afhandeling van de respons
Op basis van ervaringen uit het verleden hebben we deze iteratie geoptimaliseerd voor performance en debugbaarheid. We hebben bijvoorbeeld lokale DNS-resolvers op de compute nodes geplaatst om problemen te voorkomen. Ook hebben we de verzamelde metrieken uitgebreid (zoals opstarttijd en tijd tot de eerste open poort) en circuit breakers geïmplementeerd voor snelle rerouting en uitfasering van compute nodes.
Op een warme instantie voegt dit hele proces ongeveer 6ms toe. Niets hiervan verlaat ons netwerk, waardoor we volledige controle hebben over de request-cyclus.
Het ontwerpen van veerkrachtige compute-infrastructuur
Bij het bouwen van dit systeem optimaliseren we voor twee zaken: de ervaring van de eindgebruiker en de veerkracht van de uitrol. We moeten wijzigingen snel kunnen uitrollen, maar ook net zo snel kunnen terugdraaien.
De compute nodes zijn gebouwd op een base image van Unikraft. Deze nodes worden apart van onze edge nodes gebouwd om de edge nodes licht en snel te houden, om verschillende instance-types te kunnen gebruiken en om deze nodes onafhankelijk te kunnen schalen.
Een control plane houdt bij welke compute nodes bestaan en gezond zijn; de edge nodes pollen deze lijst. Dit stuurt ook de deploys aan: een nieuwe vloot nodes wordt naast de huidige vloot opgestart, schaalt mee en neemt het verkeer pas over zodra deze gezond is.
Het is live
De herbouw van onze edge compute-architectuur is meer dan alleen een snelheidswinst. Het is een fundament waar we op kunnen blijven bouwen en waar we meer controle over hebben. Het mooie is dat dit nu al je productieverkeer verwerkt, tegen dezelfde prijs, zonder dat er een migratiestap nodig is of er iets in projecten hoeft te worden aangepast.
Omdat we de compute nu zelf beheren, kunnen we de limieten van Edge Functions zelf verhogen. Dit maakt drie zaken mogelijk die voorheen niet haalbaar waren:
- Volledige ondersteuning voor npm-pakketten: npm-pakketten werkten tot nu toe in bèta, met beperkingen rond native binaries en het importeren van bestanden tijdens runtime. Een echte VM met een echt bestandssysteem verwijdert de meeste van deze beperkingen.
- Herziening van operationele limieten: De huidige limieten (50ms CPU per verzoek, 512MB geheugen en 20MB gecomprimeerde code) waren een gevolg van het isolate-gebaseerde model. Deze kunnen nu worden herzien.
- Compute binnen ons eigen netwerk: Alles wat afhankelijk is van het controleren van het netwerkpad, in plaats van het bereiken van een derde partij over het internet, kunnen we nu zelf bouwen.
We zijn nog niet klaar; we werken nu aan de limieten en onvolkomenheden die we voorheen niet konden aanpassen.