Bootcamp i backendutvikling med Node.js · leksjon

Oppdagelse og løsning av vanlige lekkasjemønstre

Spor opp lekkende closures, ubegrensede cacher og gjenværende lyttere som gradvis øker heap-størrelsen.

Leksjon 4 av 413 trinn

Oppdagelse og løsning av vanlige lekkasjemønstre er en gratis leksjon i Bootcamp i backendutvikling med Node.js på CoddyKit. Dette er leksjon 4 av 4. Du kan lese hele leksjonen gratis nedenfor – og deretter øve praktisk i nettleseren med en innebygd kodeeditor og en AI-veileder som er tilgjengelig døgnet rundt. Den er en del av læringsløpet i Bootcamp i backendutvikling med Node.js, og fremdriften din synkroniseres mellom nettet og CoddyKit-appen. Kurset i Bootcamp i backendutvikling med Node.js inneholder totalt 4 leksjoner.

Slik ser en faktisk lekkasje ut

En minnelekkasje i Node.js er ikke at minnet øker – det er at minnet øker og aldri går ned igjen mellom GC-sykluser. V8-heapen vokser, old-space forblir holdt tilbake, og til slutt når prosessen grensen for --max-old-space-size og blir avsluttet på grunn av OOM.

  • Normalt: Heapen går opp og ned når GC frigjør kortlivede objekter.
  • Lekkasje: Baselinjen i sagtannmønsteret stiger over flere timer, selv ved jevn trafikk.

I denne leksjonen ser vi etter de tre klassiske synderne i backend-kode: lekkende closures, ubegrensede cacher og hengende hendelseslyttere.

Lese heapen med process.memoryUsage()

Det første rimelige signalet er process.memoryUsage(). Følg med på heapUsed over tid. Hvis verdien stiger monotont under konstant belastning, har De et problem med objekter som holdes tilbake.

Utdraget nedenfor simulerer en lekkasje ved å legge til elementer i en array på modulnivå og logger veksten i heapen med hvert intervall – en frittstående reproduksjon De kan kjøre.

const leaky = [];

function tick(i) {
  // Each call retains a 10k-element array forever.
  leaky.push(new Array(10_000).fill(i));
  const { heapUsed } = process.memoryUsage();
  console.log(`tick ${i}: heapUsed=${(heapUsed / 1024 / 1024).toFixed(1)} MB, retained=${leaky.length}`);
}

for (let i = 1; i <= 5; i++) tick(i);
console.log('Heap keeps growing because `leaky` is never cleared.');

Lekkasjemønster 1: Closuren som fanger variabler

En closure holder alt i omfangskjeden sin levende, selv variabler den ikke bruker, så lenge noe har en referanse til closuren. Hvis slike closures lagres i en struktur med lang levetid, holder De store objekter fast for alltid.

Nedenfor fanger hver registrerte håndterer en buffer, bigData, på flere megabyte. Håndtererne lever i en array på modulen, så bufferne kan aldri samles inn.

const handlers = [];

function register(id) {
  const bigData = Buffer.alloc(1024 * 1024); // 1 MB per registration
  // The closure captures bigData even though it only logs id.
  handlers.push(() => console.log(`handler ${id} fired`));
  return bigData.length;
}

for (let i = 0; i < 3; i++) register(i);
console.log(`${handlers.length} handlers retained; each closure may pin its scope.`);

Rette closure-lekkasjen

To løsninger:

  • Ikke fang variabler De ikke trenger. Hent ut den nødvendige primitive verdien før De oppretter closuren, slik at det store objektet faller ut av closurens omfang.
  • Ikke lagre closures i containere med lang levetid med mindre De også fjerner dem.

Her fanger closuren bare den lille primitive verdien id; bigData brukes og forkastes, slik at GC frigjør den etter at register returnerer.

const handlers = [];

function register(id) {
  const bigData = Buffer.alloc(1024 * 1024);
  const summary = bigData.length; // use it now
  // Closure captures only `id` and `summary` (primitives) — bigData is free to GC.
  handlers.push(() => console.log(`handler ${id}: ${summary} bytes processed`));
}

register(1);
handlers[0]();
console.log('bigData is no longer reachable and will be collected.');

Lekkasjemønster 2: Den ubegrensede cachen

Den vanligste Node-lekkasjen: En Map eller et vanlig objekt som brukes som en cache i minnet og som bare vokser. Hver unike nøkkel (bruker-ID, hash for forespørsel eller sesjonstoken) legger til en oppføring, og ingen oppføringer fjernes.

Dette ser uskyldig ut i en kodegjennomgang, men lekker lineært med trafikken.

const cache = new Map();

function getUser(id) {
  if (!cache.has(id)) {
    cache.set(id, { id, profile: Buffer.alloc(50_000) }); // 50 KB each, never evicted
  }
  return cache.get(id);
}

for (let id = 0; id < 4; id++) getUser(id);
console.log(`cache size=${cache.size} — grows forever with unique ids.`);

Rette cachen: Begrenset LRU + TTL

En cache i minnet MÅ ha en grense. Bruk en velprøvd LRU (lru-cache) med max-oppføringer og en ttl, eller implementer en enkel Map som kaster ut oppføringer når kapasiteten er nådd. Innsettingsrekkefølgen i JS Maps gjør en grunnleggende LRU enkel: sletting etterfulgt av innsetting øker nyligheten, og den eldste nøkkelen fjernes når kapasiteten overskrides.

class LRU {
  constructor(max) { this.max = max; this.map = new Map(); }
  get(key) {
    if (!this.map.has(key)) return undefined;
    const v = this.map.get(key);
    this.map.delete(key); this.map.set(key, v); // mark most-recent
    return v;
  }
  set(key, val) {
    if (this.map.has(key)) this.map.delete(key);
    this.map.set(key, val);
    if (this.map.size > this.max) this.map.delete(this.map.keys().next().value); // evict oldest
  }
}

const cache = new LRU(2);
cache.set('a', 1); cache.set('b', 2); cache.set('c', 3); // 'a' evicted
console.log('keys:', [...cache.map.keys()]); // [ 'b', 'c' ]

Lekkasjemønster 3: Hengende hendelseslyttere

Hver emitter.on(...) lagrer en referanse. Hvis De legger til en lytter i en varm kallsti (per forespørsel, per socket eller per tidsur) og aldri kaller removeListener/off, vokser lytterarrayen uten grense – og hver lytter kan fange data med omfang knyttet til forespørselen.

Node advarer Dem: MaxListenersExceededWarning vises når en emitter får mer enn 10 lyttere. Behandle advarselen som et lekkasjevarsel, ikke som støy.

const { EventEmitter } = require('events');
const bus = new EventEmitter();

function handleRequest(reqId) {
  // BUG: a new listener every request, never removed.
  bus.on('shutdown', () => console.log(`drain req ${reqId}`));
}

for (let i = 0; i < 12; i++) handleRequest(i);
console.log('listenerCount(shutdown):', bus.listenerCount('shutdown'));

Rette lyttere: once, off og AbortSignal

Match hver on med en off, eller unngå opphopningen helt:

  • Bruk emitter.once(...) når håndtereren bare skal kjøre én gang.
  • Behold funksjonsreferansen slik at De kan kalle emitter.off(event, fn) under oppryddingen.
  • Moderne API-er godtar en AbortSignal – avbryt kontrolleren, så fjernes alle tilknyttede lyttere på én gang.
const { EventEmitter } = require('events');
const bus = new EventEmitter();

function handleRequest(reqId) {
  const controller = new AbortController();
  bus.on('shutdown', () => console.log(`drain req ${reqId}`), { signal: controller.signal });
  // When the request finishes, abort once — listener auto-removed.
  return () => controller.abort();
}

const cleanups = [];
for (let i = 0; i < 12; i++) cleanups.push(handleRequest(i));
cleanups.forEach((done) => done());
console.log('after cleanup, listenerCount:', bus.listenerCount('shutdown')); // 0

Tidsur og strømmer: De skjulte holderne

To holdere folk ofte glemmer:

  • setInterval holder tilbake callbacken (og closuren) til clearInterval kalles. Et intervall per forbindelse som aldri fjernes, lekker hele omfanget til forbindelsen.
  • Ufortærte strømmer og sockets som ikke er ødelagt, bufrer data i minnet. Kall alltid stream.destroy() ved feil, og håndter mottrykk.

timer.unref() lar prosessen avsluttes, men frigjør IKKE callbacken – De må fortsatt fjerne den for å stoppe tilbakeholdingen.

function startWorker(job) {
  const buf = Buffer.alloc(500_000); // retained by the interval closure
  const t = setInterval(() => {
    if (job.done) {
      clearInterval(t); // releases the closure -> buf can be collected
      console.log('worker stopped, scope released');
    }
  }, 10);
  return t;
}

const job = { done: false };
startWorker(job);
setTimeout(() => { job.done = true; }, 30);

WeakMap og WeakRef: Cacher som slipper taket

Når en cache-nøkkel er et objekt hvis levetid De ikke kontrollerer, bør De bruke en WeakMap. Oppføringene hindrer ikke at nøkkelen blir samlet inn av søppelinnsamlingen, så tilknyttede data forsvinner automatisk når nøkkelen dør – ingen utkastingspolicy er nødvendig.

Bruk WeakRef + FinalizationRegistry for avanserte cacher som holder verdier svakt. Merk: De kan ikke iterere over en WeakMap, og den godtar bare objektnøkler, så den egner seg ikke for primitive nøkler som streng-ID-er.

const meta = new WeakMap();

function attachMeta(obj) {
  meta.set(obj, { seen: Date.now() });
}

let session = { id: 'abc' };
attachMeta(session);
console.log('has meta:', meta.has(session)); // true

// Once `session` is unreachable, its WeakMap entry is collected automatically.
session = null;
console.log('session dropped; WeakMap entry becomes eligible for GC.');

Bekrefte en lekkasje: Heap-øyeblikksbilder

Minneverdier forteller Dem AT De lekker; heap-øyeblikksbilder forteller Dem HVA som lekker. Arbeidsflyt:

  • Start prosessen med node --inspect og åpne chrome://inspect, ELLER kall require('v8').writeHeapSnapshot() fra koden.
  • Ta øyeblikksbilde A, kjør endepunktet N ganger, og ta øyeblikksbilde B.
  • Bruk visningen Comparison og sorter etter Delta. Objekter med et antall som øker med nøyaktig N, er lekkasjen.
  • Undersøk Retainers for å se hvilken rot (en Map, en closure eller en lytterarray) som holder dem.

For automatisk oppdaging i CI skriver node --heapsnapshot-near-heap-limit=2 ut et øyeblikksbilde rett før OOM.

const v8 = require('v8');
const fs = require('fs');

const file = v8.writeHeapSnapshot();
console.log('wrote heap snapshot:', file);
console.log('size bytes:', fs.statSync(file).size);
console.log('Load this .heapsnapshot in Chrome DevTools > Memory > Load.');

Kort kontroll: Velge riktig cache

De legger til en cache i minnet, indeksert med strengbasert bruker-ID, i et langvarig API. Trafikkmengden er ubegrenset, og De må garantere at minnebruken holder seg begrenset. Hvilken tilnærming er riktig?

Oppsummering: Sjekkliste for lekkasjesøk

De kan nå finne og rette de tre viktigste heap-lekkasjene i Node.js:

  • Closures: fang bare opp primitivene De trenger; legg aldri closures i arrayer på modulnivå uten å fjerne dem igjen.
  • Cacher: Alle cacher i minnet trenger en grense — LRU max + ttl, eller en WeakMap når nøklene er objekter De ikke eier.
  • Hendelseslyttere og timere: par hver on med off (eller bruk once / AbortSignal), og bruk clearInterval for hver timer; behandle MaxListenersExceededWarning som et alarmsignal.

Arbeidsflyt: Følg med på heapUsed for å oppdage en stigende grunnlinje. Ta deretter sammenlignbare heap-øyeblikksbilder og følg referansekjeden til roten. Mål, ikke gjett.

Gratis å komme i gang

Lær deg JavaScript med en AI-veileder – gratis

Skriv og kjør ekte kode i nettleseren, få umiddelbar hjelp fra en AI-veileder som er tilgjengelig døgnet rundt, og fortsett der du slapp – på nettet eller i appen.

Kurs
22
Leksjoner
92

Ofte stilte spørsmål

Er leksjonen «Oppdagelse og løsning av vanlige lekkasjemønstre» gratis?

Ja – hele teksten i «Oppdagelse og løsning av vanlige lekkasjemønstre» er gratis å lese her på nettet. For å øve interaktivt med en innebygd kodeeditor og en AI-veileder som er tilgjengelig døgnet rundt, og for å låse opp resten av Bootcamp i backendutvikling med Node.js-kurset, kan du oppgradere til CoddyKit PRO. Kurset i Bootcamp i backendutvikling med Node.js inneholder totalt 4 leksjoner.

Hva lærer jeg i «Oppdagelse og løsning av vanlige lekkasjemønstre»?

Spor opp lekkende closures, ubegrensede cacher og gjenværende lyttere som gradvis øker heap-størrelsen. Du øver på Bootcamp i backendutvikling med Node.js med praktisk kode som du kjører direkte i nettleseren, mens en AI-veileder som er tilgjengelig døgnet rundt, svarer på spørsmålene dine mens du jobber deg gjennom leksjonen.

Trenger jeg erfaring for å begynne med Bootcamp i backendutvikling med Node.js?

Ingen tidligere erfaring er nødvendig. Bootcamp i backendutvikling med Node.js på CoddyKit er lagt opp for både nybegynnere og viderekomne, så De kan begynne her eller helt fra start og lære i Deres eget tempo. Dette er leksjon 4 av 4.

Hvor lang tid tar leksjonen «Oppdagelse og løsning av vanlige lekkasjemønstre»?

De fleste CoddyKit-leksjoner tar omtrent 5–10 minutter. Hver leksjon er kort og interaktiv, slik at De gjør jevne fremskritt og kan fortsette akkurat der De slapp – både på nettet og i appen.

Kan jeg skrive og kjøre kode i denne Bootcamp i backendutvikling med Node.js-leksjonen?

Ja. Alle Bootcamp i backendutvikling med Node.js-leksjoner har en innebygd kodeeditor, slik at De kan skrive og kjøre ekte kode direkte i nettleseren og få umiddelbar tilbakemelding fra AI – uten lokal konfigurering.

Alle leksjonene i dette kurset

  1. V8-heapen, generasjonsbasert GC og objektenes levetid
  2. Innhenting og sammenligning av heap-snapshots
  3. CPU-profilering og flame graphs for varme kodebaner
  4. Oppdagelse og løsning av vanlige lekkasjemønstre
← Tilbake til Bootcamp i backendutvikling med Node.js