NestJS: backend-API:er för företag · Lektion

Circuit breakers för fel i nedströms tjänster

Skydda tjänster mot kaskadfel med circuit breaker-integration av opossum-typ.

Lektion 2 av 413 steg

Circuit breakers för fel i nedströms tjänster är en gratis lektion i NestJS: backend-API:er för företag på CoddyKit. Detta är lektion 2 av 4. Ni kan läsa hela lektionen gratis nedan och sedan öva praktiskt i webbläsaren med en inbyggd kodredigerare och en AI-handledare som är tillgänglig dygnet runt. Den ingår i lärvägen för NestJS: backend-API:er för företag, och Era framsteg synkroniseras mellan webben och CoddyKit-appen. Kursen i NestJS: backend-API:er för företag innehåller totalt 4 lektioner.

Varför kaskadfel uppstår

I en NestJS-backend för företag kör er tjänst sällan isolerat. Den anropar en betalningsgateway, en autentiseringstjänst, ett sökkluster och andra mikrotjänster. När ett downstream-beroende blir långsamt håller varje väntande anrop en anslutning, en tråd och en del av händelseslingan upptagna.

  • Ett långsamt beroende tömmer er HTTP-anslutningspool.
  • Väntande anrop hopar sig och latensen ökar överallt.
  • Er tjänst blir ohälsosam och dess egna anropare börjar misslyckas.

Denna dominoeffekt är ett kaskadfel. En circuit breaker är mönstret som stoppar dominobrickorna från att falla.

Tillståndsmaskinen för en circuit breaker

En circuit breaker omsluter ett riskfyllt anrop och följer dess hälsa genom tre tillstånd:

  • CLOSED — anrop passerar normalt. Fel räknas.
  • OPEN — för många fel har inträffat; anrop avvisas omedelbart utan att beroendet kontaktas. Det ger downstream-tjänsten tid att återhämta sig.
  • HALF_OPEN — efter en nedkylningsperiod tillåts några testanrop. Om de lyckas stängs brytaren; om de misslyckas öppnas den igen.

Den viktiga insikten är att ni när brytaren är OPEN misslyckas snabbt i stället för att vänta på en timeout för varje anrop.

En minimal circuit breaker från grunden

Innan ni använder ett bibliotek är det bra att förstå mekaniken. Här är en liten, fristående circuit breaker i TypeScript som växlar mellan CLOSED och OPEN baserat på konsekutiva fel och en timeout för återställning.

Den körs fristående så att ni kan se hur tillstånden växlar.

type State = 'CLOSED' | 'OPEN' | 'HALF_OPEN';

class MiniBreaker {
  private state: State = 'CLOSED';
  private failures = 0;
  private openedAt = 0;
  constructor(private threshold = 3, private resetMs = 1000) {}

  async call<T>(fn: () => Promise<T>): Promise<T> {
    if (this.state === 'OPEN') {
      if (Date.now() - this.openedAt >= this.resetMs) this.state = 'HALF_OPEN';
      else throw new Error('Circuit OPEN: failing fast');
    }
    try {
      const result = await fn();
      this.failures = 0;
      this.state = 'CLOSED';
      return result;
    } catch (e) {
      this.failures++;
      if (this.failures >= this.threshold) {
        this.state = 'OPEN';
        this.openedAt = Date.now();
      }
      throw e;
    }
  }
  get current() { return this.state; }
}

async function main() {
  const breaker = new MiniBreaker(2, 500);
  const flaky = () => Promise.reject(new Error('downstream down'));
  for (let i = 0; i < 4; i++) {
    try { await breaker.call(flaky); }
    catch (e) { console.log(`call ${i}: ${(e as Error).message} [state=${breaker.current}]`); }
  }
}
main();

Möt opossum

Handskrivna circuit breakers missar de svåra delarna: statistik över rullande fönster, procentbaserade tröskelvärden, begränsningar för testanrop i half-open-läge och mätvärden. opossum är den de facto-standardiserade circuit breakern för Node.js och integreras smidigt i NestJS-providers.

  • timeout — hur länge det dröjer innan ett anrop betraktas som misslyckat.
  • errorThresholdPercentage — andelen fel i fönstret som utlöser brytaren.
  • resetTimeout — hur länge OPEN varar innan ett HALF_OPEN-test.
  • rollingCountTimeout — storleken på statistikfönstret.

Installera det med npm i opossum och npm i -D @types/opossum.

import CircuitBreaker from 'opossum';

const options: CircuitBreaker.Options = {
  timeout: 3000,                 // a call slower than 3s counts as a failure
  errorThresholdPercentage: 50,  // trip when >=50% of calls fail
  resetTimeout: 10000,           // stay OPEN for 10s, then try HALF_OPEN
  rollingCountTimeout: 10000,    // 10s statistical window
  rollingCountBuckets: 10,       // split the window into 10 buckets
};

// The action is the function we want to protect
async function fetchUser(id: string): Promise<{ id: string }> {
  // ... real HTTP call to a downstream user service ...
  return { id };
}

export const userBreaker = new CircuitBreaker(fetchUser, options);

Omsluta ett downstream-anrop i en NestJS-provider

I NestJS hör brytaren hemma i en provider som äger ett logiskt beroende. Skapa CircuitBreaker en gång i konstruktorn (eller i en factory) så att den rullande statistiken bevaras mellan anrop — skapa aldrig en ny brytare per anrop, eftersom den då aldrig kan lära sig beroendets hälsa.

Exponera en metod som delegerar till breaker.fire(...).

import { Injectable } from '@nestjs/common';
import { HttpService } from '@nestjs/axios';
import { firstValueFrom } from 'rxjs';
import CircuitBreaker from 'opossum';

interface PricingDto { sku: string; cents: number; }

@Injectable()
export class PricingClient {
  private readonly breaker: CircuitBreaker<[string], PricingDto>;

  constructor(private readonly http: HttpService) {
    this.breaker = new CircuitBreaker(
      (sku: string) => this.requestPrice(sku),
      { timeout: 2000, errorThresholdPercentage: 50, resetTimeout: 15000 },
    );
  }

  private async requestPrice(sku: string): Promise<PricingDto> {
    const res = await firstValueFrom(
      this.http.get<PricingDto>(`https://pricing.internal/skus/${sku}`),
    );
    return res.data;
  }

  getPrice(sku: string): Promise<PricingDto> {
    return this.breaker.fire(sku);
  }
}

Fallbacks: degradera på ett kontrollerat sätt

Att misslyckas snabbt är bra, men att returnera ett hårt fel till användaren är ofta sämre än att returnera något rimligt. opossums fallback() körs när åtgärden avvisas eller när brytaren är OPEN.

  • Servera ett cachelagrat eller senast kända fungerande värde.
  • Returnera ett säkert standardvärde (till exempel en tom lista med rekommendationer).
  • Lägg arbetet i kö för senare i stället för att kasta bort det.

Fallback-funktionen får samma argument samt det fel som utlöste den, så att ni kan välja hantering utifrån felet.

import CircuitBreaker from 'opossum';

type Recommendation = { id: string };

function buildRecommendationBreaker(
  action: (userId: string) => Promise<Recommendation[]>,
) {
  const breaker = new CircuitBreaker(action, {
    timeout: 1500,
    errorThresholdPercentage: 40,
    resetTimeout: 20000,
  });

  // When OPEN or the action fails, return a safe empty list
  breaker.fallback((_userId: string, err?: Error) => {
    if (err) console.warn('recommendations degraded:', err.message);
    return [] as Recommendation[];
  });

  return breaker;
}

Lyssna på händelser från brytaren

En brytare som växlar tillstånd utan att signalera det är en operativ blind fläck. opossum genererar händelser för varje betydelsefull övergång. Koppla dessa till er loggning och era mätvärden så att jourtekniker ser helheten i realtid.

  • open / halfOpen / close — tillståndsövergångar.
  • reject — ett anrop avvisades eftersom brytaren var OPEN.
  • timeout — ett anrop överskred den konfigurerade timeouten.
  • fallback — fallback-funktionen anropades.
  • success / failure — resultatet av varje anrop som kördes.
import { Logger } from '@nestjs/common';
import CircuitBreaker from 'opossum';

export function attachBreakerTelemetry(
  breaker: CircuitBreaker,
  name: string,
  logger = new Logger('CircuitBreaker'),
) {
  breaker.on('open', () => logger.error(`[${name}] OPEN - failing fast`));
  breaker.on('halfOpen', () => logger.warn(`[${name}] HALF_OPEN - probing`));
  breaker.on('close', () => logger.log(`[${name}] CLOSED - recovered`));
  breaker.on('reject', () => logger.warn(`[${name}] call rejected (OPEN)`));
  breaker.on('timeout', () => logger.warn(`[${name}] call timed out`));
  breaker.on('fallback', () => logger.warn(`[${name}] fallback served`));
}

Timeout är en del av brytaren

Ett vanligt misstag är att ställa in brytarens timeout på en längre tid än den underliggande HTTP-klientens timeout. Om er axios-timeout är 30 s men brytarens timeout är 3 s ger opossum upp efter 3 s — bra — men socketen kan fortfarande hållas öppen downstream.

Samordna dem: ställ in brytarens timeout något lägre än transportens timeout och se till att transporten faktiskt avbryter anropet. Hela poängen är att begränsa hur länge ett enskilt anrop kan hålla en resurs upptagen.

import { Module } from '@nestjs/common';
import { HttpModule } from '@nestjs/axios';

@Module({
  imports: [
    HttpModule.register({
      timeout: 2500,        // axios aborts the socket at 2.5s
      maxRedirects: 0,
    }),
  ],
})
export class DownstreamModule {}

// Breaker timeout (e.g. 2000ms) should sit just BELOW the axios timeout
// so opossum records the failure while the socket is still being released.

En brytare per beroende, inte per app

Bulkheading innebär att isolera fel så att ett sjukt beroende inte kan sänka ett friskt. Ge varje downstream-tjänst en egen brytarinstans med tröskelvärden som är anpassade till dess SLA.

  • En instabil analystjänst kan utlösa sin brytare utan att påverka betalningar.
  • Ett latenskänsligt autentiseringsanrop får en kort timeout; ett anrop för batchrapporter får en längre.
  • Mätvärden per beroende gör instrumentpanelerna lättare att läsa.

Dela inte en enda global brytare mellan orelaterade anrop — deras statistik skulle påverka varandra.

import { Injectable } from '@nestjs/common';
import CircuitBreaker from 'opossum';

@Injectable()
export class BreakerRegistry {
  private readonly breakers = new Map<string, CircuitBreaker>();

  get<TArgs extends unknown[], TRet>(
    name: string,
    action: (...args: TArgs) => Promise<TRet>,
    options: CircuitBreaker.Options,
  ): CircuitBreaker<TArgs, TRet> {
    const existing = this.breakers.get(name);
    if (existing) return existing as CircuitBreaker<TArgs, TRet>;
    const breaker = new CircuitBreaker(action, options);
    this.breakers.set(name, breaker);
    return breaker;
  }
}

Exponera brytarens hälsa för probes

Ur ett operativt perspektiv vill ni kunna se brytarens tillstånd i en health-endpoint och samla in det med Prometheus. opossum exponerar breaker.stats (antal lyckade, misslyckade och timeoutade anrop samt avvisningar) och booleska värden för breaker.opened / breaker.halfOpen.

En anpassad Terminus health indicator kan rapportera OPEN-brytare som ett degraderat — men inte nödvändigtvis nere — tillstånd, så att orkestrerare inte i onödan avslutar en pod som korrekt avlastar trafik.

import { Injectable } from '@nestjs/common';
import { HealthIndicator, HealthIndicatorResult } from '@nestjs/terminus';
import CircuitBreaker from 'opossum';

@Injectable()
export class BreakerHealthIndicator extends HealthIndicator {
  check(name: string, breaker: CircuitBreaker): HealthIndicatorResult {
    const isUp = !breaker.opened;
    return this.getStatus(name, isUp, {
      state: breaker.opened ? 'open' : breaker.halfOpen ? 'half_open' : 'closed',
      failures: breaker.stats.failures,
      timeouts: breaker.stats.timeouts,
      rejects: breaker.stats.rejects,
    });
  }
}

Justera tröskelvärden på ett förnuftigt sätt

Dåliga tröskelvärden är sämre än ingen brytare alls. Justera dem utifrån verklig trafik:

  • errorThresholdPercentage som är för lågt (t.ex. 10 %) utlöses av normal jitter; för högt (t.ex. 90 %) motverkar syftet. 40–60 % är ett vanligt startintervall.
  • resetTimeout som är för kort belastar en tjänst som återhämtar sig; för lång fördröjer återhämtningen. Börja runt 10–30 s.
  • Ta hänsyn till låg trafik: med endast 2 anrop i fönstret utgör ett fel 50 %. Opossums volumeThreshold kräver ett minsta antal anrop innan procentandelen kan utlösa brytaren.
import CircuitBreaker from 'opossum';

const options: CircuitBreaker.Options = {
  timeout: 2000,
  errorThresholdPercentage: 50,
  resetTimeout: 15000,
  rollingCountTimeout: 10000,
  rollingCountBuckets: 10,
  volumeThreshold: 10, // need >=10 calls in the window before % can trip
  // errorFilter lets you NOT count expected errors (e.g. 404) as failures:
  errorFilter: (err: { statusCode?: number }) => err?.statusCode === 404,
};

export { options };

Snabbtest: välj rätt beteende

Testa er förståelse av hur en circuit breaker beter sig under belastning.

Sammanfattning

Ni vet nu hur ni förhindrar att ett fel i en downstream-tjänst slår ut er NestJS-tjänst:

  • Mönstret: en brytare växlar mellan CLOSED, OPEN och HALF_OPEN; OPEN innebär att misslyckas snabbt i stället för att vänta på timeouts.
  • opossum: konfigurera timeout, errorThresholdPercentage, resetTimeout och volumeThreshold, omslut sedan anropet med new CircuitBreaker(action, options) och anropa breaker.fire().
  • Skapa en gång: skapa brytaren i en provider eller ett register så att rullande statistik bevaras — en brytare per beroende för bulkheading.
  • Degradera: registrera en fallback() för cachelagrade svar eller säkra standardvärden.
  • Observera: koppla händelserna open/close/reject/timeout till loggar och mätvärden och exponera tillståndet i en health probe.

I kombination med retries (med backoff) och timeouts är circuit breakers en grundpelare i robusta, SLO-vänliga distribuerade system.

Gratis att börja

Lär dig TypeScript med en AI-lärare – gratis

Skriv och kör riktig kod i webbläsaren, få omedelbar hjälp av en AI-lärare dygnet runt och fortsätt där du slutade – på webben eller i appen.

Kurser
20
Lektioner
76

Vanliga frågor

Är lektionen ”Circuit breakers för fel i nedströms tjänster” gratis?

Ja – hela texten till ”Circuit breakers för fel i nedströms tjänster” kan läsas gratis här på webben. Om Ni vill öva interaktivt med en inbyggd kodredigerare och en AI-handledare som är tillgänglig dygnet runt och låsa upp resten av kursen i NestJS: backend-API:er för företag, kan Ni uppgradera till CoddyKit PRO. Kursen i NestJS: backend-API:er för företag innehåller totalt 4 lektioner.

Vad lär jag mig i ”Circuit breakers för fel i nedströms tjänster”?

Skydda tjänster mot kaskadfel med circuit breaker-integration av opossum-typ. Ni övar på NestJS: backend-API:er för företag med praktisk kod som körs direkt i webbläsaren, medan en AI-handledare som är tillgänglig dygnet runt svarar på Era frågor under lektionen.

Behöver jag någon erfarenhet för att börja lära mig NestJS: backend-API:er för företag?

Du behöver inga förkunskaper. Utbildningen i NestJS: backend-API:er för företag på CoddyKit är upplagd för allt från nybörjare till avancerade elever, så att du kan börja här eller från början och gå fram i din egen takt. Detta är lektion 2 av 4.

Hur lång tid tar lektionen ”Circuit breakers för fel i nedströms tjänster”?

De flesta CoddyKit-lektioner tar cirka 5–10 minuter. Varje lektion är kort och interaktiv, så att du gör stadiga framsteg och kan fortsätta precis där du slutade – på webben eller i appen.

Kan jag skriva och köra kod i den här NestJS: backend-API:er för företag-lektionen?

Ja. Varje NestJS: backend-API:er för företag-lektion innehåller en inbyggd kodredigerare, så att du kan skriva och köra riktig kod direkt i webbläsaren och få omedelbar AI-feedback – utan lokal installation.

Alla lektioner i den här kursen

  1. Tidsgränser, omförsök och bulkheads med interceptors
  2. Circuit breakers för fel i nedströms tjänster
  3. Distribuerad tracing med OpenTelemetry
  4. Definiera SLO:er och error budgets
← Tillbaka till NestJS: backend-API:er för företag