Przejdź do głównej zawartości

Testy wstrzykiwania awarii (fault injection, nazywane też wstrzykiwaniem błędów) celowo psują zależność: dodają opóźnienie, zrywają połączenia, zwracają błędy 500, zabijają pody. Dowodzą w ten sposób, że timeouty, ponowienia i circuit breakery zadziałają, zanim ta sama awaria zdarzy się na produkcji. Agenci piszą takie eksperymenty szybko. Dowodem jest zatwierdzona przez człowieka macierz awarii i testy, które czerwienieją po usunięciu zabezpieczenia.

Twój serwis zamówień przechodzi wszystkie testy, a potem wisi 40 sekund, gdy PostgreSQL pierwszy raz zwalnia pod obciążeniem. Pula połączeń się zapełnia, żądania czekają w kolejce, a timeout, na który wszyscy liczyli, okazuje się ustawieniem po stronie serwera, którego opóźnienie w sieci nigdy nie wyzwala. Opakowanie z ponowieniami (retry wrapper) napisał agent w poprzednim sprincie i nikt nie przeczytał go dokładnie, bo nikt już nie czyta każdej linii.

Ta strona jest dla programistów, którzy odpowiadają za kod odporności serwisu, i dla tech leadów, którzy zatwierdzają eksperymenty chaosu na stagingu. Zakłada, że masz już testy integracyjne na prawdziwej bazie danych; wstrzykiwanie awarii obciąża te same styki.

Co dają testy wstrzykiwania awarii budowane przez agenta

Dział zatytułowany „Co dają testy wstrzykiwania awarii budowane przez agenta”
  • Format macierzy awarii, który zamienia „serwis ma być odporny” w wiersze, które agent umie zaimplementować, a recenzent zatwierdzić w dwie minuty
  • Działający zestaw testów z Toxiproxy dla serwisu w Node.js z PostgreSQL, razem z testem kontrolnym, który dowodzi, że proxy naprawdę stoi na ścieżce
  • Test circuit breakera dla opossum, który liczy wywołania usługi nadrzędnej (upstream) zamiast ufać nock.isDone()
  • Ograniczony w czasie workflow Chaos Mesh dla stagingu, z walidacją po stronie serwera i poleceniem przerwania
  • Cztery prompty do skopiowania, workflow CI z minimalnymi uprawnieniami i kroki naprawcze na wypadek, gdy eksperyment chaosu wymknie się spod kontroli

Wstrzykiwanie awarii działa na dwóch warstwach. Zacznij od warstwy aplikacji, bo jest deterministyczna i na tyle tania, że zmieści się w każdym pull requeście. Na warstwę systemu przejdź dopiero wtedy, gdy każdy serwis z osobna szybko zgłasza błąd.

WarstwaNarzędziaGdzie działaCzego dowodziCzęstotliwość
AplikacjaToxiproxy (proxy TCP), nock lub MSW (mocki HTTP)Laptop i CITimeoutów, ponowień, fallbacków i higieny puli jednego serwisuKażdy pull request
SystemChaos Mesh, LitmusChaos (CRD Kubernetesa)Klaster stagingowyOdtwarzania, zasięgu awarii (blast radius) i SLO stanu ustalonego między serwisamiNa żądanie lub według harmonogramu, po zatwierdzeniu

Zachowanie pod obciążeniem to osobne pytanie. Jeśli chcesz wiedzieć, ile żądań na sekundę serwis obsłuży, zanim zacznie się degradować, zacznij od testów obciążeniowych, przeciążeniowych i benchmarków; chaos na bezczynnym klastrze stagingowym niewiele dowodzi.

Agenci dobrze piszą testy odporności i źle dobierają liczby. Poproś o „testy timeoutów”, a agent wymyśli pięciosekundowy limit, który nie pasuje do niczego w konfiguracji, i dopisze kod, żeby go spełnić. Dlatego decyzja człowieka idzie pierwsza, jako tabela w docs/resilience/order-service.md:

ZależnośćAwariaOczekiwane zachowanieLimit
PostgreSQL4 s opóźnienia na każdej odpowiedzicreateOrder odrzuca z timeoutem zapytania po stronie klienta; żaden klient puli nie wyciekaBłąd w ciągu 2,5 s
PostgreSQLZerwanie połączeniaJedno ponowienie z backoffem, potem 503Łącznie dwie próby
API magazynuOdsetek błędów powyżej 50% (opossum porównuje ostro) przy co najmniej volumeThreshold wywołaniach (tu 5)Breaker się otwiera; następne wywołanie dostaje fallback z cache bez wywołania sieciZero wywołań usługi nadrzędnej, gdy breaker jest otwarty
API magazynuOdpowiedź po 3 sLiczy się jako błąd (timeout breakera 1 s)Błąd w ciągu 1,2 s

Każdy wiersz to kryterium akceptacji w kształcie Given/When/Then, ten sam kontrakt, który opisuje strona o wykonywalnych kryteriach akceptacji. Recenzujesz i zatwierdzasz tę tabelę zamiast kodu testów. Zadanie agenta: zamienić każdy wiersz w test, który dziś zawiedzie, jeśli zabezpieczenia brakuje.

Toxiproxy to proxy TCP z administracyjnym API HTTP na porcie 8474. Aplikacja łączy się z proxy zamiast z bazą, a każdy test dodaje na czas swojego trwania „toxic” (latency, timeout, bandwidth, reset_peer, slicer, limit_data, slow_close).

  1. Uruchom Toxiproxy obok lokalnego PostgreSQL i utwórz proxy, które nasłuchuje na 5433 i przekazuje ruch na 5432. W terminalu:

    Okno terminala
    docker run -d --name toxiproxy --network host ghcr.io/shopify/toxiproxy:2.12.0
    curl -sf -X POST http://localhost:8474/proxies \
    -d '{"name":"postgres","listen":"127.0.0.1:5433","upstream":"127.0.0.1:5432"}'

    --network host to najprostsza opcja na Linuksie. W Docker Desktop (macOS lub Windows) opublikuj porty 8474 i 5433, a jako upstream podaj host.docker.internal:5432, chyba że masz włączoną sieć hosta w Docker Desktop.

  2. Zainstaluj klienta: npm install --save-dev toxiproxy-node-client (4.1.0 na dzień 2026-09-26).

  3. Skieruj testową pulę na port 5433 i ustaw timeout zapytania po stronie klienta. Na tym potknął się scenariusz ze wstępu: statement_timeout w PostgreSQL mierzy czas wykonania na serwerze, więc opóźnienie wstrzyknięte w sieci nigdy go nie wyzwala. Wolną sieć łapie tylko query_timeout w node-postgres, egzekwowany przez klienta.

  4. Napisz zestaw testów. Resetuj Toxiproxy po każdym teście, żeby nieudana asercja nigdy nie zostawiła zatrutego proxy dla następnego:

    test/resilience/db-faults.test.js
    import { Toxiproxy } from 'toxiproxy-node-client';
    import pg from 'pg';
    import { createOrder } from '../../src/services/order.service.js';
    const toxiproxy = new Toxiproxy('http://localhost:8474');
    const pool = new pg.Pool({
    host: '127.0.0.1',
    port: 5433, // through the proxy, never 5432
    database: 'app',
    user: 'postgres', // the CI service has only this role
    query_timeout: 2000, // client-side: trips on network latency
    connectionTimeoutMillis: 1000,
    });
    afterEach(() => toxiproxy.reset()); // removes every toxic, re-enables proxies
    afterAll(() => pool.end());
    test('control: a reset connection breaks the query, so the proxy is in the path', async () => {
    const proxy = await toxiproxy.get('postgres');
    await proxy.addToxic({ name: 'reset', type: 'reset_peer', stream: 'downstream', toxicity: 1, attributes: { timeout: 0 } });
    await expect(pool.query('SELECT 1')).rejects.toThrow();
    });
    test('createOrder fails fast when PostgreSQL responses are 4 s late', async () => {
    const warm = await pool.connect(); // the pool is empty after the control test
    warm.release(); // so open one now, or the connect timeout fires instead
    const proxy = await toxiproxy.get('postgres');
    await proxy.addToxic({ name: 'slow-db', type: 'latency', stream: 'downstream', toxicity: 1, attributes: { latency: 4000, jitter: 0 } });
    const start = performance.now();
    await expect(createOrder(pool, { sku: 'ABC-1', qty: 1 })).rejects.toThrow('Query read timeout');
    expect(performance.now() - start).toBeLessThan(2500);
    expect(pool.totalCount - pool.idleCount).toBe(0); // no client left checked out
    });

Przykłady używają modułów ES, więc zakładają Jest skonfigurowany pod ESM lub TypeScript (ts-jest albo babel-jest). Vitest uruchomi je po ustawieniu test.globals: true (albo imporcie test, expect, afterEach, afterAll z vitest), jeśli zamiast npx jest wpiszesz npx vitest run.

Test kontrolny znaczy więcej, niż się wydaje. Jeśli ktoś przestawi pulę z powrotem na 5432, każdy test opóźnień nadal „przejdzie”, bo zapytanie wróci szybko. Test kontrolny wtedy czerwienieje.

Test opóźnień celowo rozgrzewa pulę. Po teście kontrolnym pula nie ma żywego klienta, więc bez rozgrzewki 4 s opóźnienia spowalniają handshake z PostgreSQL. Wtedy pierwszy odpala connectionTimeoutMillis z komunikatem timeout exceeded when trying to connect, a luźna asercja /timeout/i zostaje zielona nawet po usunięciu query_timeout. Dokładny komunikat Query read timeout (node-postgres) wyklucza tę pomyłkę. Jeśli chcesz udowodnić także timeout połączenia, daj mu osobny test: dodaj toxic latency, zanim powstanie jakikolwiek klient, i sprawdź komunikat timeout exceeded when trying to connect.

Resztę macierzy przekaż agentowi. Prompt jest ten sam w każdym narzędziu:

Narzędzia różnią się tym, kto uruchamia kontenery i jak wynik do ciebie wraca.

Uruchom claude w repozytorium i wklej prompt. Claude Code iteruje sam: generuje, uruchamia, czyta błąd, dodaje brakujący timeout i uruchamia ponownie. Toxiproxy uruchom wcześniej sam (krok 1), żeby agent nie potrzebował uprawnień do Dockera. Jeśli będziesz powtarzać to dla każdego serwisu, zapisz prompt jako skill projektu w .claude/skills/resilience-suite/SKILL.md.

Udowodnij, że circuit breaker się otwiera bez wywołania sieci

Dział zatytułowany „Udowodnij, że circuit breaker się otwiera bez wywołania sieci”

Typowy test circuit breakera mockuje API tak, żeby dziesięć razy zwróciło błąd, wyzwala breaker i sprawdza nock.isDone() === false. Ta asercja niczego nie dowodzi: przy dziesięciu interceptorach i sześciu wywołaniach isDone() zwraca false niezależnie od tego, czy breaker zablokował szóste żądanie. Zamiast tego policz wywołania usługi nadrzędnej.

Test zakłada, że klient eksportuje swój breaker z opossum. opossum otwiera obwód według procentu błędów (errorThresholdPercentage, domyślnie 50), gdy zobaczy co najmniej volumeThreshold żądań, a nie po stałej liczbie kolejnych błędów. Dlatego test czyta próg z breakera:

test/resilience/inventory-breaker.test.js
import nock from 'nock';
import { getInventory, inventoryBreaker } from '../../src/clients/inventory.client.js';
afterEach(() => nock.cleanAll());
test('breaker opens, then serves the fallback with zero upstream calls', async () => {
let upstreamCalls = 0;
nock('https://inventory.internal')
.get(/.*/)
.times(50)
.reply(() => {
upstreamCalls += 1;
return [500, { error: 'boom' }];
});
const opened = new Promise((resolve) => inventoryBreaker.once('open', resolve));
const volume = Math.max(inventoryBreaker.options.volumeThreshold ?? 1, 1);
for (let i = 0; i < volume; i += 1) {
await getInventory('SKU-1').catch(() => {}); // the fallback may resolve these
}
await opened;
const callsWhenOpened = upstreamCalls;
const result = await getInventory('SKU-1');
expect(result.source).toBe('fallback-cache');
expect(upstreamCalls).toBe(callsWhenOpened); // the breaker, not luck, stopped the call
});

Uruchom chaos na poziomie systemu na stagingu z Chaos Mesh

Dział zatytułowany „Uruchom chaos na poziomie systemu na stagingu z Chaos Mesh”

Gdy każdy serwis szybko zgłasza błąd, sprawdź, czy system się podnosi. Chaos Mesh (v2.8.4 na dzień 2026-09-26) opisuje każdy eksperyment jako zasób niestandardowy Kubernetesa, więc wynik pracy agenta to manifest, który możesz porównać, zwalidować i ograniczyć w czasie. Jedno opóźnienie sieci do bazy, zawężone do przestrzeni nazw staging i do 10 minut:

chaos/postgres-latency.yaml
apiVersion: chaos-mesh.org/v1alpha1
kind: NetworkChaos
metadata:
name: postgres-latency
namespace: staging
spec:
action: delay
mode: all
selector:
namespaces: [staging]
labelSelectors:
app: postgresql
delay:
latency: '200ms'
jitter: '50ms'
duration: '10m'

Przebieg etapowy łączy kilka awarii w zasobie Workflow z templateType: Serial, wstawia szablony Suspend jako przerwy na odtworzenie i nadaje każdemu krokowi deadline. Każdy manifest zwaliduj względem schematów CRD zainstalowanych w klastrze, niczego nie tworząc:

Okno terminala
kubectl apply --dry-run=server -n staging -f chaos/

Dry run po stronie klienta (--dry-run=client) nigdy nie dociera do serwera API. Webhooki walidujące Chaos Mesh i walidacja pól po stronie serwera w ogóle się nie uruchamiają, więc manifest, który klaster by odrzucił, może przejść. Poproś agenta o workflow etapowy i runbook naraz, żeby polecenie przerwania powstało, zanim ktokolwiek cokolwiek zastosuje:

Wklej prompt w sesji, która ma dostęp kubectl do stagingu; potrzebuje go dry run.

Żeby Claude Code obserwował pody i zdarzenia w trakcie eksperymentu, dodaj serwer MCP dla Kubernetesa z konfiguracją tylko do odczytu (read_only = true w pliku TOML):

Okno terminala
claude mcp add kubernetes -- npx -y kubernetes-mcp-server@latest --config ~/.config/k8s-mcp.toml

Skąd wiesz, że test awarii w ogóle może zawieść?

Dział zatytułowany „Skąd wiesz, że test awarii w ogóle może zawieść?”

Zielony zestaw testów odporności napisany przez agenta jest dowodem tylko wtedy, gdy każdy test zawiedzie (zmieni kolor na czerwony) bez zabezpieczenia, które rzekomo sprawdza. Weryfikuj to mechanicznie, nie czytając testów:

  • Kontrola negatywna dla każdego zabezpieczenia. Usuwaj po kolei każdy timeout, ponowienie i breaker i uruchamiaj zestaw; odpowiadający test musi zawieść. To celowana odmiana testów mutacyjnych, a strona o sile wyroczni pokazuje, jak zautomatyzować ją narzędziem mutacyjnym na zmienionych plikach.
  • Obserwowalny efekt w każdym teście. Każdy test awarii sprawdza czas, konkretny błąd albo liczbę wywołań. Gołe rejects.toThrow() przechodzi nawet przy literówce.
  • Test kontrolny zostaje w zestawie. Dowodzi, że ruch idzie przez proxy.
  • Testy są poza zasięgiem edycji agenta. Obejmij test/resilience/ i docs/resilience/ plikiem CODEOWNERS, żeby kolejny agent nie poluzował limitu dla zielonego wyniku; zobacz ochronę wyroczni.

Kto zatwierdza. Właściciel serwisu zatwierdza macierz awarii i tabelę kontroli negatywnych w pull requeście; obie należą do pakietu dowodów. Tech lead albo dyżurny SRE zatwierdza każde uruchomienie chaosu na stagingu przez bramkę środowiska w CI opisaną niżej i zapisuje bazowy stan ustalony zmierzony tuż przed nim.

Uruchamiaj testy awarii w CI i bramkuj chaos na stagingu

Dział zatytułowany „Uruchamiaj testy awarii w CI i bramkuj chaos na stagingu”

Testy warstwy aplikacji działają w każdym pull requeście. Chaos na stagingu rusza tylko po ręcznym uruchomieniu, po zielonych testach, za środowiskiem GitHuba, którego wymagani recenzenci zatwierdzają zadanie, zanim dostanie ono poświadczenia do stagingu.

.github/workflows/fault-injection.yml
name: Fault injection
on:
pull_request:
workflow_dispatch:
inputs:
experiment:
description: Chaos Mesh experiment to apply to staging
type: choice
options: [postgres-latency, staged]
permissions:
contents: read
jobs:
resilience-tests:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:17
env:
POSTGRES_DB: app
POSTGRES_HOST_AUTH_METHOD: trust # throwaway CI database only
options: >-
--health-cmd pg_isready --health-interval 5s --health-retries 10
toxiproxy:
image: ghcr.io/shopify/toxiproxy:2.12.0
ports: ['8474:8474', '5433:5433']
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v7
with:
node-version: 22
- run: npm ci
- name: Route PostgreSQL through Toxiproxy
run: >-
curl -sf -X POST http://localhost:8474/proxies
-d '{"name":"postgres","listen":"0.0.0.0:5433","upstream":"postgres:5432"}'
- run: npx jest test/resilience/
staging-chaos:
if: github.event_name == 'workflow_dispatch'
needs: resilience-tests
runs-on: ubuntu-latest
environment: staging # required reviewers approve before secrets are exposed
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- name: Write the staging-only kubeconfig
env:
KUBECONFIG_DATA: ${{ secrets.STAGING_CHAOS_KUBECONFIG }}
run: printf '%s' "$KUBECONFIG_DATA" > "$RUNNER_TEMP/kubeconfig"
- name: Abort if staging is already degraded
env:
KUBECONFIG: ${{ runner.temp }}/kubeconfig
run: ./scripts/chaos-preflight.sh
- name: Apply the experiment (spec.duration or deadlines bound it)
env:
KUBECONFIG: ${{ runner.temp }}/kubeconfig
EXPERIMENT: ${{ inputs.experiment }}
run: kubectl apply -n staging -f "chaos/${EXPERIMENT}.yaml"

Bezpieczeństwo opiera się na trzech szczegółach. Wejście jest typu choice i trafia do powłoki przez zmienną środowiskową, nigdy przez wklejenie do polecenia. Kubeconfig należy do konta serwisowego, którego RBAC pozwala tylko na zasoby chaos-mesh.org w przestrzeni nazw staging. A scripts/chaos-preflight.sh piszesz sam: ma zwrócić kod różny od zera, gdy odpala się alert albo trwa incydent, żeby chaos nigdy nie trafił w system, który już jest w złym stanie. Ręczne uruchomienie wykonuje workflow i ten skrypt z gałęzi, którą wybierze uruchamiający, więc ogranicz gałęzie wdrożeń środowiska staging do gałęzi domyślnej; inaczej gałąź funkcji może podmienić własny skrypt preflight i uruchomić go z kubeconfigiem stagingu, gdy recenzent zatwierdzi przebieg.