Bootcamp backendontwikkeling met Node.js · Les

Het circuit-breakerpatroon en bulkhead-isolatie

Voorkom kettingstoringen door circuits te onderbreken en resourcepools per afhankelijkheid te isoleren.

Les 2 van 413 stappen

Het circuit-breakerpatroon en bulkhead-isolatie is een gratis Bootcamp backendontwikkeling met Node.js-les op CoddyKit. Dit is les 2 van 4. Je kunt de volledige les hieronder gratis lezen en daarna in de browser praktisch oefenen met een ingebouwde code-editor en een AI-begeleider die 24/7 beschikbaar is. Deze les maakt deel uit van het leertraject Bootcamp backendontwikkeling met Node.js. Je voortgang wordt gesynchroniseerd op het web en in de CoddyKit-app. De cursus Bootcamp backendontwikkeling met Node.js bevat in totaal 4 lessen.

Waarom trapsgewijze fouten ontstaan

In een microservicebackend is je Node.js-API vaak afhankelijk van downstreamservices: een betalingsprovider, een voorraadservice of een database. Wanneer een dependency traag wordt (niet eens uitvalt — alleen traag is), stapelt elk verzoek dat ervan afhankelijk is zich op.

  • Elk wachtend verzoek houdt een plek in de eventlus, een socket en geheugen bezet.
  • Aanroepers proberen het opnieuw, waardoor de belasting op de al worstelende dependency toeneemt.
  • Je gezonde eindpunten krijgen geen ruimte meer omdat het proces verzadigd is door het wachten op de zieke dependency.

Dit is een trapsgewijze fout: één trage dependency trekt de hele service omlaag. Twee patronen gaan dit tegen — de circuitonderbreker (stop met het aanroepen van een falende dependency) en het compartiment (begrens hoeveel resources één dependency mag gebruiken).

De naïeve nieuwe poging maakt het erger

Een veelvoorkomende eerste ingeving is om een onbetrouwbare aanroep in een lus voor herhaalde pogingen te verpakken. Blinde herhaalde pogingen versterken een gedeeltelijke storing echter tot een volledige storing — je verdrievoudigt het verkeer precies op het moment waarop de dependency dit het minst aankan.

Hieronder wordt een kwetsbare downstreamservice overspoeld. Let erop hoe herhaalde pogingen één logische aanvraag omzetten in veel fysieke aanroepen.

async function callDependency(attempt) {
  // Simulate a dependency that fails 70% of the time
  if (Math.random() < 0.7) {
    throw new Error('dependency timeout');
  }
  return 'ok';
}

async function withBlindRetry(maxRetries) {
  let physicalCalls = 0;
  for (let i = 0; i <= maxRetries; i++) {
    physicalCalls++;
    try {
      const res = await callDependency(i);
      console.log(`Success after ${physicalCalls} physical call(s)`);
      return res;
    } catch (e) {
      console.log(`Attempt ${i + 1} failed: ${e.message}`);
    }
  }
  console.log(`Gave up after ${physicalCalls} physical calls`);
}

withBlindRetry(3);

De toestanden van de circuitonderbreker

Een circuitonderbreker verpakt een aanroep en houdt de gezondheid ervan bij. Het is een kleine toestandsmachine met drie toestanden:

  • CLOSED — aanroepen gaan normaal door. Fouten worden geteld.
  • OPEN — er zijn te veel recente fouten. Aanroepen worden onmiddellijk geweigerd zonder de dependency aan te raken (direct afbreken).
  • HALF_OPEN — na een afkoelperiode worden enkele proefoverroepen toegestaan. Bij succes ga je terug naar CLOSED; bij een fout spring je terug naar OPEN.

Het belangrijkste inzicht: in de toestand OPEN verspil je geen resources meer aan een dependency die al faalt. Zo krijgt deze ruimte om te herstellen en blijft je eventlus vrij.

Een minimale circuitonderbreker

Hier is een zelfstandige onderbreker. Deze opent na een foutdrempel, weigert snel zolang de toestand OPEN is en voert vervolgens een proefaanroep uit zodra de time-out voor het resetten is verstreken. Dit is de kernlogica die elke bibliotheek voor productie implementeert.

class CircuitBreaker {
  constructor(fn, { threshold = 3, resetMs = 5000 } = {}) {
    this.fn = fn;
    this.threshold = threshold;
    this.resetMs = resetMs;
    this.failures = 0;
    this.state = 'CLOSED';
    this.nextTry = 0;
  }

  async exec(...args) {
    if (this.state === 'OPEN') {
      if (Date.now() < this.nextTry) {
        throw new Error('Circuit OPEN - failing fast');
      }
      this.state = 'HALF_OPEN';
    }
    try {
      const result = await this.fn(...args);
      this.failures = 0;
      this.state = 'CLOSED';
      return result;
    } catch (err) {
      this.failures++;
      if (this.failures >= this.threshold) {
        this.state = 'OPEN';
        this.nextTry = Date.now() + this.resetMs;
      }
      throw err;
    }
  }
}

let n = 0;
const flaky = async () => { n++; if (n <= 5) throw new Error('boom'); return 'ok'; };
const breaker = new CircuitBreaker(flaky, { threshold: 3, resetMs: 1000 });

(async () => {
  for (let i = 0; i < 4; i++) {
    try { console.log(await breaker.exec()); }
    catch (e) { console.log(`call ${i}: ${e.message} [state=${breaker.state}]`); }
  }
})();

Snel falen is beter dan langzaam falen

De echte winst van de toestand OPEN is latentie. Een time-out kan 10 seconden duren; een geactiveerde onderbreker weigert binnen microseconden. Onder belasting is dat het verschil tussen overleven en instorten.

Vergelijk de kosten van 100 verzoeken naar een dependency met een time-out van 2 seconden met die van een onderbreker die onmiddellijk weigert zodra deze is geactiveerd.

function estimate(reqs, timeoutMs, openAfter) {
  let totalMs = 0;
  for (let i = 0; i < reqs; i++) {
    if (i < openAfter) {
      totalMs += timeoutMs; // these waited for the full timeout
    } else {
      totalMs += 0.01; // breaker rejected instantly
    }
  }
  return totalMs;
}

const reqs = 100, timeout = 2000, openAfter = 5;
const withBreaker = estimate(reqs, timeout, openAfter);
const noBreaker = reqs * timeout;
console.log(`No breaker: ${noBreaker} ms of blocked time`);
console.log(`With breaker: ${withBreaker.toFixed(2)} ms of blocked time`);
console.log(`Saved: ${(noBreaker - withBreaker).toFixed(0)} ms`);

Opossum gebruiken in productie

Lever geen zelfgeschreven onderbreker. De de-factostandaardbibliotheek voor Node.js is opossum. Deze voegt foutpercentages over rollende vensters, een fallback, time-outs per aanroep en uitgebreide gebeurtenissen en meetgegevens toe.

  • timeout — breek een aanroep af die te lang blijft hangen.
  • errorThresholdPercentage — activeer de onderbreker wanneer dit percentage van de aanroepen in het venster mislukt.
  • resetTimeout — hoe lang de toestand OPEN duurt voordat een proefaanroep wordt uitgevoerd.

Voor dit fragment zijn het pakket opossum en een netwerkaanroep nodig, dus het dient ter illustratie en kan hier niet worden uitgevoerd.

const CircuitBreaker = require('opossum');

async function getInventory(sku) {
  const res = await fetch(`http://inventory.internal/items/${sku}`);
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  return res.json();
}

const breaker = new CircuitBreaker(getInventory, {
  timeout: 3000,                 // fail a call after 3s
  errorThresholdPercentage: 50,  // open at 50% failures in the window
  resetTimeout: 10000,           // try again after 10s
  rollingCountTimeout: 10000,    // 10s stats window
});

// Serve stale/cached data instead of erroring out
breaker.fallback((sku) => ({ sku, stock: 'unknown', cached: true }));

breaker.on('open', () => console.warn('inventory breaker OPEN'));
breaker.on('halfOpen', () => console.info('inventory breaker probing'));

module.exports = (sku) => breaker.fire(sku);

Fallbacks: degradeer, ga niet ten onder

Een geactiveerde onderbreker moet meestal iets bruikbaars retourneren in plaats van een 500. Dit is gecontroleerde degradatie:

  • Lever een gecachte of verouderde waarde.
  • Retourneer een veilige standaardwaarde (lege aanbevelingen, laatst bekende prijs).
  • Zet het werk in een wachtrij voor later (schrijf het naar een buffer en verwerk het wanneer de dependency herstelt).

Kies de fallback per dependency op basis van bedrijfsregels. Een prijsservice moet gesloten falen (de verkoop blokkeren); een aanbevelingsservice moet open falen (niets tonen), zodat de pagina toch wordt geladen.

Het compartimentpatroon

Een onderbreker stopt aanroepen nadat er iets misgaat. Een compartiment voorkomt juist dat één dependency ooit alle resources monopoliseert. De naam komt van schepen: waterdichte compartimenten zorgen ervoor dat één ondergelopen deel niet het hele schip laat zinken.

In een backend geef je elke dependency een eigen begrensde pool:

  • Een begrensd aantal gelijktijdige actieve aanroepen.
  • Een afzonderlijke verbindingenpool per database of HTTP-doel.
  • Optioneel een begrensde wachtrij; overtollige aanvragen worden direct geweigerd.

Als de betalings-API blijft hangen, zitten daar dus hoogstens N plekken vast — je voorraad- en authenticatieaanroepen houden hun eigen plekken en blijven gezond.

Een compartiment voor gelijktijdigheid implementeren

Het eenvoudigste compartiment is een begrenzer voor gelijktijdigheid (een semafoor). Deze begrenst hoeveel aanroepen naar een bepaalde dependency tegelijk worden uitgevoerd en weigert de rest (of zet deze in een wachtrij), zodat een trage dependency nooit meer dan maxConcurrent plekken kan bezet houden.

class Bulkhead {
  constructor(maxConcurrent, maxQueue = 0) {
    this.max = maxConcurrent;
    this.maxQueue = maxQueue;
    this.active = 0;
    this.queue = [];
  }

  async run(task) {
    if (this.active >= this.max) {
      if (this.queue.length >= this.maxQueue) {
        throw new Error('Bulkhead full - rejected');
      }
      await new Promise((resolve) => this.queue.push(resolve));
    }
    this.active++;
    try {
      return await task();
    } finally {
      this.active--;
      const next = this.queue.shift();
      if (next) next();
    }
  }
}

const bh = new Bulkhead(2, 2);
const slow = (id) => () => new Promise((r) => setTimeout(() => { console.log('done', id); r(id); }, 50));

(async () => {
  const results = await Promise.allSettled(
    [1, 2, 3, 4, 5, 6].map((id) => bh.run(slow(id)))
  );
  results.forEach((r, i) =>
    console.log(`task ${i + 1}: ${r.status}${r.reason ? ' - ' + r.reason.message : ''}`)
  );
})();

Verbindingenpools per dependency

Het meest voorkomende compartiment in de praktijk is de HTTP-verbindingenpool. De standaard globale agent van Node deelt sockets over alle doelen. Geef elke downstreamservice in plaats daarvan een eigen Agent met een begrensde maxSockets. Een vastgelopen dependency kan dan alleen de eigen pool uitputten.

Dit gebruikt de Agent van de module http en een actieve socket, dus beschouw het als een configuratiepatroon en niet als een programma dat door de beoordelaar kan worden uitgevoerd.

const http = require('http');

// One isolated pool per downstream service
const paymentsAgent = new http.Agent({
  keepAlive: true,
  maxSockets: 10,        // at most 10 concurrent connections to payments
  maxFreeSockets: 5,
});

const inventoryAgent = new http.Agent({
  keepAlive: true,
  maxSockets: 20,        // inventory gets its own, independent budget
});

function callPayments(path) {
  return new Promise((resolve, reject) => {
    const req = http.request(
      { host: 'payments.internal', path, agent: paymentsAgent, timeout: 3000 },
      (res) => { res.resume(); res.on('end', resolve); }
    );
    req.on('timeout', () => req.destroy(new Error('payments timeout')));
    req.on('error', reject);
    req.end();
  });
}

module.exports = { callPayments, paymentsAgent, inventoryAgent };

Onderbreker en compartiment combineren

Veerkracht in productie combineert beide patronen per dependency:

  • Compartiment begrenst de gelijktijdigheid, zodat een trage dependency het proces niet kan verzadigen.
  • Time-out zorgt ervoor dat geen enkele aanroep voor altijd blijft hangen.
  • Circuitonderbreker stopt met aanroepen zodra het foutpercentage hoog is.
  • Fallback retourneert een verslechterd maar bruikbaar antwoord.

De volgorde is belangrijk: verpak de onbewerkte aanroep met een time-out, stuur deze door het compartiment en plaats de onderbreker aan de buitenkant, zodat deze de aanroep kortsluit voordat je zelfs maar een plek in het compartiment reserveert. Met opossum kunnen de timeout van de onderbreker en de opties capacity/volume het meeste hiervan afhandelen, maar expliciete pools per dependency bieden de sterkste isolatie.

// Compose: breaker(bulkhead(timeout(call)))
function withTimeout(fn, ms) {
  return (...args) => Promise.race([
    fn(...args),
    new Promise((_, rej) => setTimeout(() => rej(new Error('timeout')), ms)),
  ]);
}

function resilient(rawCall, { bulkhead, breaker, timeoutMs }) {
  const timed = withTimeout(rawCall, timeoutMs);
  // breaker on the OUTSIDE: it can fail fast before we ever take a bulkhead slot
  return (...args) => breaker.exec(() => bulkhead.run(() => timed(...args)));
}

async function demo() {
  await withTimeout(() => Promise.resolve('fast'), 50)().then(console.log);
  try { await withTimeout(() => new Promise(() => {}), 30)(); }
  catch (e) { console.log('slow call ->', e.message); }
  console.log('Compose order: breaker -> bulkhead -> timeout -> rawCall');
}
demo();

Korte controle

Je betalingsdependency begint traag te reageren (3-8s), maar geeft geen fouten. Elk verzoek eraan houdt een plek in de eventlus en een socket bezet. Welke combinatie voorkomt het best dat deze ene trage dependency je volledige Node.js-service neerhaalt?

Samenvatting

Om trapsgewijze fouten in een Node.js-backend te stoppen, isoleer en bescherm je elke dependency:

  • Trapsgewijze fout begint met een trage dependency, niet alleen met een defecte — wachtende aanroepen putten sockets en de eventlus uit.
  • Circuitonderbreker (CLOSED / OPEN / HALF_OPEN) faalt snel zodra het recente foutpercentage hoog is, zodat de dependency ruimte krijgt om te herstellen. Gebruik opossum in productie.
  • Fallbacks zetten een storing om in gecontroleerde degradatie — gecachte waarden, veilige standaardwaarden of werk in een wachtrij.
  • Compartiment begrenst de gelijktijdigheid en geeft elke dependency een eigen verbindingenpool, zodat één zieke service de rest niet kan uithongeren.
  • Combineer ze per dependency: een onderbreker rond een compartiment rond een aanroep met time-out, met een fallback voor het pad bij OPEN of weigering.

Blinde herhaalde pogingen zijn de valkuil: ze versterken de belasting precies wanneer het systeem het zwakst is.

Gratis beginnen

Leer JavaScript met een AI-tutor — gratis

Schrijf echte code en voer die uit in je browser, krijg direct hulp van een AI-tutor die 24/7 beschikbaar is en ga verder waar je gebleven bent op het web of in de app.

Cursussen
22
Lessen
92

Veelgestelde vragen

Is de les “Het circuit-breakerpatroon en bulkhead-isolatie” gratis?

Ja — de volledige tekst van “Het circuit-breakerpatroon en bulkhead-isolatie” kun je hier gratis op het web lezen. Als je interactief wilt oefenen met een ingebouwde code-editor en een AI-begeleider die 24/7 beschikbaar is, en de rest van de cursus Bootcamp backendontwikkeling met Node.js wilt ontgrendelen, kun je upgraden naar CoddyKit PRO. De cursus Bootcamp backendontwikkeling met Node.js bevat in totaal 4 lessen.

Wat leer ik in “Het circuit-breakerpatroon en bulkhead-isolatie”?

Voorkom kettingstoringen door circuits te onderbreken en resourcepools per afhankelijkheid te isoleren. Je oefent met Bootcamp backendontwikkeling met Node.js door code rechtstreeks in de browser uit te voeren. Een AI-begeleider die 24/7 beschikbaar is beantwoordt je vragen terwijl je de les doorwerkt.

Heb ik ervaring nodig om met Bootcamp backendontwikkeling met Node.js te beginnen?

Ervaring vooraf is niet nodig. Bootcamp backendontwikkeling met Node.js op CoddyKit is opgebouwd voor beginners tot gevorderden, zodat je hier of bij het begin kunt starten en in je eigen tempo kunt leren. Dit is les 2 van 4.

Hoe lang duurt de les “Het circuit-breakerpatroon en bulkhead-isolatie”?

De meeste lessen van CoddyKit duren ongeveer 5–10 minuten. Elke les is kort en interactief, zodat je gestaag vooruitgaat en op het web en in de app precies verdergaat waar je was gebleven.

Kan ik code schrijven en uitvoeren in deze les over Bootcamp backendontwikkeling met Node.js?

Ja. Elke les over Bootcamp backendontwikkeling met Node.js bevat een ingebouwde code-editor, zodat je rechtstreeks in je browser echte code kunt schrijven en uitvoeren en direct feedback van AI krijgt — lokale installatie is niet nodig.

Alle lessen in deze cursus

  1. Timeouts, retries en exponentiële back-off met jitter
  2. Het circuit-breakerpatroon en bulkhead-isolatie
  3. Graceful shutdown en het leegmaken van actieve requests
  4. Healthchecks, readiness probes en load shedding
← Terug naar Bootcamp backendontwikkeling met Node.js