Med dette enkle trikset kan du spare millioner (av bytes)

tl;dr
Sliter Rust-appen din med fragmentering?
#[global_allocator]
static GLOBAL: Jemalloc = Jemalloc;
Tada!
Allokering
I noen programmeringsspråk har man et mer bevisst forhold til datamaskinens minne (RAM) enn andre. Det de fleste programmer har til felles, er at man ikke kan vite på forhånd hvor mye minne som trengs under kjøring. Derfor må de benytte seg av dynamisk minneallokering. Dette er når programmet spør operativsystemet om ekstra minne, for eksempel til å lagre en innkommende HTTP-spørring, en streng med tekst som genereres ut ifra inndata, eller en liste av tall.
I C, så kommer dynamisk minneallokering tydelig fram:
#include <stdlib.h>
char *ptr = malloc(512 * sizeof(char));
// her har man et buffer på en halv kilobyte å kose seg med
free(ptr); // husk å frigjøre minnet igjen!
I andre språk, som f.eks. Rust, skjules allokeringen delvis:
{
let my_name = String::from("Jeff");
// her skjedde det en malloc bak kulissene
} // og her skjedde en frigjøring
Mens i språk som Python, så skal man helst ikke tenke på at minneallokering skjer dynamisk. Her finnes ikke konseptet "heap", og Pythons minnebehandlingsmetode tar seg av frigjøring i bakkant, uten at man behøver eller kan gjøre noe med det.
def efficient_pi():
parts = [3, '.', 1, 4, 1, 5, 9, 2]
return float(''.join(map(str, parts)))
pi = efficient_pi()
# man kan ikke si for sikkert når minnet bak `parts` ble frigjort
Når man kjører et hvilket som helst vanlig program, og spesielt web servere, så skjer det mange minneallokeringer og frigjøringer hele tiden.
Langtlevende prosesser
Det at minneallokeringer og frigjøringer skjer i hytt og vær er vanligvis ikke noe problem. Spesielt dersom programmet ditt av og til avsluttes, f.eks. så kan det være en engangsjobb, hvor prosessen avsluttes når jobben er ferdig. Når man derimot lar en webserver surre og gå i dages-, ukes- og månedsvis, så kan det dukke opp en interessant problemstilling.

Minneforbruket kan øke gradvis med tid, uten at det egentlig tas vare på mer data i RAM. Man kan fort tenke at dette handler om minnelekkasje, dvs. allokeringer som ikke frigjøres. Dette kan skje på flere måter, men i språk hvor man er godt beskyttet mot manuell allokering, så handler det som regel om sirkulære referanser. Men en annen årsak kan være fragmentering.
Fragmentering er når små objekter ligger spredt i minnet, mens det ikke er plass til nye objekter imellom.

Figuren over viser tre steg i en minneoperasjon hvor objekter allokeres, frigjøres, og et nytt objekt legges til:
- Fem objekter (rosa) ligger tettpakket, med ledig plass på høyre side.
- To av dem frigjøres (grønn) når de ikke lenger er i bruk.
- Et nytt objekt (gult) skal allokeres, og må ta plass helt til høyre, siden de frigjorte blokkene ikke er store nok for det nye objektet.
Dette fører til at objektene tar opp mer plass enn summen av størrelsene deres, og er resultatet av fragmentering.
Fragmentering i persondatatjeneste
Vi har en persondatatjeneste i Rust som har problemet med økende minneforbruk, hvor vi mistenker fragmentering.
Grafen over minneforbruk over er fra denne tjenesten.
Rust bruker libcs malloc
som standard på Linux.
Denne allokatoren er ikke optimalisert for langtkjørende applikasjoner hvor allokering skjer samtidig fra flere tråder.
I slike applikasjoner (som vår persondatatjeneste), så er malloc
sårbar for fragmentering.
Derfor ønsket vi å teste et alternativ.
Bruk jemalloc
som Rusts globale allokator
jemalloc er en minneallokator som er bygget med tanke på samtidighet og resistens mot fragmentering.
Det er ikke lengre aktiv utvikling i jemalloc
-prosjektet, men allokatoren er meget populær og er ansett velfungerende og stabil.
På grunn av Rusts allokator-API, så er det meget enkelt å bytte ut et program sin globale allokator. Diffen i PR-en som gjør dette for persondatatjenesten vår ser slik ut (litt forenkla):
diff --git a/Cargo.toml b/Cargo.toml
index 6987df4..2396388 100644
--- a/Cargo.toml
+++ b/Cargo.toml
+tikv-jemallocator = "0.6"
diff --git a/src/main.rs b/src/main.rs
index 5f050e5..97bce25 100644
--- a/src/main.rs
+++ b/src/main.rs
+use tikv_jemallocator::Jemalloc;
+#[global_allocator]
+static GLOBAL: Jemalloc = Jemalloc;
Resultater
Umiddelbart etter deploy kunne vi se en halvering av minneforbruket. Dette overrasket oss stort, og vi er ikke helt sikre på hvorfor minnebruken falt bare ved å bytte ut allokator.

En arbeidshypotese er at man generelt sett trenger mer minne når fragmenteringa er høy, og at man ved å bytte allokator slipper unna med mindre.

Nå har tjenesten kjørt i nesten 24 timer med jemalloc
, og resultatene er foreløpig lovende.
Gjennomsnittsforbruket når 60 MiB, og er tilbake på samme nivå som ca samme tidspunkt dagen før.
Alternative allokatorer
Det finnes alternativer til malloc
og jemalloc
, bl.a.
Hver av disse har antagelig sine styrker og svakheter, som jeg ikke har utforska noe særlig.
Vi er fornøyd med jemalloc
så langt!