De Test Die Slaagde. Het Productie-incident Dat Niet Slaagde.
Het is 3 uur 's nachts. Je service gooit 503-fouten. De circuit breaker waarvan je zeker weet dat die correct werkt — want die heeft tests — opent niet. Je retry-logica, ook getest, versterkt het probleem met factor 9. Je DNS-resolver draait EAI_AGAIN tegen een overbelast CoreDNS-cluster, en elke nieuwe poging maakt het erger.
Je voert een rollback uit. Het incident wordt gesloten. Je schrijft de post-mortem.
Zes weken later deploy je opnieuw. Hetzelfde faalpatroon, iets andere vorm. De tests slagen nog steeds.
We hebben dit patroon tientallen keren gezien. De tests zijn niet verkeerd. Het geteste gedrag is reëel. Maar er bestaat een categorie van fouten die geen enkele hoeveelheid unittests, integratietests of containertests ooit zal opvangen: wat er gebeurt wanneer de infrastructuur onder je applicatie zich gaat gedragen op manieren die technisch geldig zijn, maar operationeel vijandig. Wat er gebeurt als DNS begint te flappen? Als netwerk-writes afgekapt worden teruggegeven? Als je connection pool naar nul daalt, niet door een bug, maar door gecoördineerde belasting?
Dat is de kloof. En daarom bouwden we een chaos-testing-stack.
De Vier Fasen van Modern Testen
De meeste engineeringteams denken aan testen in drie fasen. Wij stellen dat er vier zijn, en dat het overslaan van de vierde fase nachtelijke incidenten mogelijk maakt.
Phase 1: Unit Tests
└── Tests a single unit of logic in isolation
└── Fast, precise, tells you the code is correct
└── Cannot tell you the system handles failure
Phase 2: Integration Tests
└── Tests interactions between components
└── Catches interface mismatches and contract violations
└── Still uses predictable, cooperative dependencies
Phase 3: Container Tests (Testcontainers, Docker Compose)
└── Runs real databases, brokers, caches
└── Validates real-system behavior with real dependencies
└── Dependencies are healthy, well-behaved, and available
Phase 4: Chaos Tests ← the missing phase
└── Injects controlled failures into real system components
└── Validates recovery logic, circuit breakers, retry strategies
└── Proves your system handles the unexpected, not just the expected Wat de Eerste Drie Fasen Niet Kunnen Testen
Laten we specifiek zijn over de kloof. Dit is wat Fasen 1–3 niet kunnen valideren:
**Je circuit breaker opent daadwerkelijk.** Je kunt de Resilience4j-configuratie unit-testen, maar de circuit breaker zal niet openen tenzij de downstream-aanroep ook echt faalt op het juiste tempo, met de juiste timing en het juiste uitzonderingstype. Mocks maken de fout te netjes.
**Je retry-logica versterkt de fout niet.** 5x retry over drie replica's tegen een falende backend is geen 5x versterking — het is 15x. Dit is onzichtbaar in unittests. Het is onzichtbaar in integratietests, tenzij je zorgvuldig partiële fouten inzet.
**Je DNS-client omgaat met tijdelijke resolutiefouten.** getaddrinfo() dat EAI_AGAIN retourneert is een geldige, verwachte POSIX-respons. De meeste services testen dit pad nooit. Het gevolg: een CoreDNS-hapering trapsgewijs escaleert naar een brownout van 30 seconden.
**Je connection pool herstelt onder druk.** Pool-uitputting gevolgd door gedeeltelijk herstel is een genuanceerde toestandsmachine. De herstellogica wordt doorgaans één keer geschreven, nooit getest, en ontdekt in productie.
**Je timeout-stack composeert correct.** Een read-timeout van 30s achter een HTTP-client-timeout van 10s achter een load balancer-timeout van 5s — het samengestelde gedrag is niet wat een individuele test uitoefent.
Introductie van de Chaos-testing-stack
We bouwden drie open-source-bibliotheken die samen het volledige oppervlak van Phase 4 chaos-testing afdekken.
**Bibliotheek 1 — C99 LD_PRELOAD Chaos (`chaos-testing-libraries`):** Een set gedeelde objecten die libc-aanroepen onderscheppen op het niveau van de dynamische linker. Taalonafhankelijk — werkt op elke ELF-binary: Java, Go, Python, Node.js, Rust met CGO. Zes foutdomeinen: bestands-I/O, netwerken, DNS, klok, geheugen en proceslevenscyclus. Geen wijzigingen in applicatiecode vereist. De volledige interface is een plaintext-configuratiebestand.
**Bibliotheek 2 — JVM Chaos Agent (`chaos-testing-java-agent`):** Een Java-agent die 62 JDK-aanroeplocaties instrumenteert met behulp van ByteBuddy. Injecteert fouten, vertragingen en resourcedruk op JVM-niveau — binnen HikariCP, binnen Netty, binnen de eigen SSL- en DNS-stacks van de JDK. Sessie-geïsoleerd per test, zodat meerdere chaos-tests gelijktijdig in dezelfde JVM kunnen draaien zonder elkaar te storen.
**Bibliotheek 3 — Java Testing Framework (`chaos-testing`):** Een JUnit 5-extensiesysteem met 448 L1-annotaties (ruwe syscall-primitieven), 92 L2-composites (gedocumenteerde faalpatronen) en 64 L3-scenario's (echte productie-incidenten gereproduceerd als enkele annotaties). Integreert met Spring Boot 3/4, Quarkus en Micronaut. Verzorgt automatisch LD_PRELOAD-injectie in Docker-containers via de Docker API.
Alle drie delen dezelfde selector × effect × probability DSL-grammatica. Je leert het mentale model één keer. Je past het op elke laag toe.
Laag 1: Kernel-Echte Fouten voor Elk Proces
De C99-bibliotheekset werkt op het laagste niveau dat beschikbaar is voor user-space-code: de libc-grens. Wanneer je Java-proces read() aanroept op een socket, gaat dat via glibc. Wanneer je Python-script socket.connect() aanroept, gaat dat via glibc. Daar onderscheppen we — vóór de kernel, na je code.
Zes onafhankelijke gedeelde objecten, elk met een disjuncte set symbolen zodat ze zonder conflicten gecombineerd kunnen worden:
- `libchaos-io.so` — bestandslezen/-schrijven, fsync, afgeknotte writes (korte returns), enkelbits leescorruptie - `libchaos-net.so` — connect, send, recv, accept, met ERRNO-injectie, latentie en payload-corruptie - `libchaos-dns.so` — getaddrinfo/getnameinfo, met naamherschrijvingen, EAI_AGAIN, EAI_FAIL, antwoordschudden en -beperking - `libchaos-time.so` — clock_gettime, nanosleep met getekende tijdverschuiving en latentie-injectie - `libchaos-memory.so` — mmap/munmap/mprotect met ENOMEM-injectie bij configureerbare kans - `libchaos-process.so` — fork, pthread_create, posix_spawn, exec — falen-na-N-semantiek en latentie
De interface is een plaintext-configuratiebestand op een bekende locatie. Geen daemons, geen API's, geen agents om te draaien. Schrijf een configuratie, preload de bibliotheek, start je proces.
# /tmp/.chaos-net.conf — 10% connection failures to your downstream
tcp4://payments-service:8080:connect:ECONNREFUSED:0.10
# /tmp/.chaos-dns.conf — transient DNS failures for internal services
dns://*.internal:EAI_AGAIN:0.15
# Inject faults into any process — no code changes
LD_PRELOAD=libchaos-net.so:libchaos-dns.so ./your-service Laag 2: 62 JDK-onderscheppingspunten
De C99-laag is krachtig maar grof: die werkt op OS-niveau-aanroepen. Voor JVM-workloads heb je fijnere precisie nodig. Het verschil tussen HikariCP dat een verbindingslening laat verlopen versus een TCP-verbinding die op kernelniveau uitvalt is precies het onderscheid dat van belang is voor het afstemmen van circuit breakers.
De JVM-agent instrumenteert 62 JDK-aanroeplocaties met ByteBuddy met @Advice-inlining. Na JIT-opwarming compileert de chaos-dispatch terug naar een null-check en een niet-genomen branch — vrijwel geen overhead in het gewone geval.
Kruciaal: elke test krijgt een ChaosSession die wordt ondersteund door ThreadLocal<UUID>. Scenario's registreren zich sessie-scoped, en worden alleen geactiveerd op threads die de sessie-ID van de test dragen. Meerdere tests draaien gelijktijdig in dezelfde JVM zonder onderlinge verstoring — de chaos van test A is onzichtbaar voor test B.
Spring Boot 3- en 4-starters maken de bedrading transparant — één testafhankelijkheid, één annotatie, en de agent koppelt zichzelf automatisch:
@SpringBootTest
@ChaosTest
class PaymentServiceChaosTest {
@Test
void circuitBreaker_opens_on_jdbcPoolExhaustion(ChaosSession session) {
try (var h = session.activate(
ChaosScenario.jdbc()
.reject("chaos: pool exhausted")
.probability(1.0)
.maxApplications(3)
.build())) {
try (var scope = session.bind()) {
// First 3 JDBC borrows fail → circuit must open
assertThat(service.charge(order))
.isEqualTo(PaymentResult.REJECTED_CIRCUIT_OPEN);
}
}
}
} Laag 3: 604 Annotaties, 64 Productie-incidenten
Het Java-framework is wat chaos-testing toegankelijk maakt op teamniveau. In plaats van fault-DSL's te leren en kansen af te stemmen, annoteert een ontwikkelaar een test met de naam van een echt incidentpatroon en zorgt het framework voor de compositie.
**448 L1-annotaties** bieden chirurgische controle op syscall-niveau: `@ChaosRecvEconnreset(probability = 0.05)`, `@ChaosDnsGetaddrinfoEaiAgain(hostPattern = "*.internal", probability = 0.20)`, `@ChaosMmapEnomem(probability = 0.001)`
**92 L2-composites** groeperen gerelateerde fouten in gedocumenteerde patronen: `@CompositeChaosTransientDnsFailure`, `@CompositeChaosConnectionRefused`, `@CompositeChaosLowMemoryPressure`
**64 L3-scenario's** coderen echte productie-incidenten als enkele annotaties — elk met een ernstbeoordeling, bronverwijzing en een multi-domein Composer-klasse die weet hoe het incident te ontleden in de juiste mix van syscall-fouten:
`@IncidentChaosK8sRollingUpdateRst` — ECONNRESET op 30% van de RECV-aanroepen, waarmee het iptables-vertragingsvenster tijdens Kubernetes rolling updates wordt gereproduceerd
`@IncidentChaosFeignRetryAmplification` — ECONNREFUSED op 50% van connect() plus IOException-injectie op Feign-niveau, waarmee het retry storm-patroon wordt gereproduceerd dat 9x lastversterking veroorzaakt
`@IncidentChaosSpringTransactionalPoolDeadlock` — het @Transactional(REQUIRES_NEW)-deadlock-patroon dat de connection pool van binnenuit leegtrekt
Het framework injecteert automatisch de juiste LD_PRELOAD-bibliotheken in je Docker-container voordat de test start — via de Docker API tar-stream, zonder shell exec, zonder sidecar, zonder imagewijziging.
@Testcontainers
@ExtendWith(ChaosTestingExtension.class)
@SyscallLevelChaos(LibchaosLib.NET)
class OrderServiceChaosTest {
@Container @AppContainer
static GenericContainer<?> app =
new GenericContainer<>("order-service:latest")
.withExposedPorts(8080);
@Test
@IncidentChaosK8sRollingUpdateRst(toxicity = 0.3)
void service_handles_rst_during_rolling_deploy() {
// 30% of RECV calls return ECONNRESET — exactly what happens
// during a Kubernetes rolling update when iptables lags behind
assertThat(callService("/api/order/42")).isEqualTo(200);
}
} Één DSL, Drie Lagen, Geen Lacunes
De sleutelontwerpbeslissing is dat alle drie de lagen dezelfde conceptuele grammatica delen: selector × effect × policy.
Een **selector** bepaalt wat er gefaald moet worden: een bestandspadprefix, een TCP-eindpunt, een JDK-operatietype, een DNS-naampatroon. Een **effect** definieert hoe er gefaald moet worden: ERRNO-injectie, latentie, datacorruptie, weigering, waardegrenscorruptie. Een **policy** bepaalt wanneer er gefaald moet worden: kans, snelheidslimiet, opwarmtelling, tijdvenster, maximale toepassingen.
Of je nu een C99-configuratiebestand schrijft, een ChaosScenario bouwt voor de JVM-agent, of een JUnit 5-annotatie kiest — je werkt met hetzelfde mentale model. De ontwikkelaar die `@IncidentChaosFeignRetryAmplification` begrijpt, kan inzoomen op de onderliggende L1-annotaties en precies lezen welke syscalls worden gefaald, met welke snelheden en waarom.
En alle drie de lagen kunnen tegelijkertijd actief zijn. Een enkele testrun kan het volgende hebben: - De container draait met `libchaos-net.so` geïnjecteerd (30% ECONNRESET) - De JVM-agent produceert code-cache-druk (achtergrondstressor) - Een ChaosSession met actieve JDBC-weigering (eerste 3 pool-borrows falen)
Alle drie onafhankelijk beheerd, alle drie correct afgebakend, geen van alle interfereren met elkaar.
Chaos als Volwaardig CI-onderdeel
De verschuiving die wij voorstaan gaat niet over Game Days of chaos-experimenten in productie. Het is eenvoudiger en waardevoller: **elke PR die foutafhandeling, retry-logica, circuit breakers of verbindingsbeheer aanraakt, zou een chaos-test moeten hebben die de build blokkeert**.
Dit herpositioneert chaos engineering van een ops-discipline naar een ontwikkeldiscipline. De vraag is niet langer "kan ons platform een netwerkpartitie overleven?" — beantwoord in productie, te laat. De vraag wordt: "opent de circuit breaker van deze PR correct wanneer de JDBC-pool uitgeput is, voordat deze code wordt verzonden?"
De tooling bestaat. De annotaties zijn één import verwijderd. De Docker-injectie is automatisch. De overhead van het toevoegen van `@IncidentChaosK8sRollingUpdateRst` aan een integratietest is gelijk aan het toevoegen van elke andere JUnit-annotatie. De overhead van het weglaten ervan is wat je betaalt om 3 uur 's nachts.
Waar te Beginnen
Elke bibliotheek heeft zijn eigen diepgaande artikel, maar als je vandaag chaos-tests wilt draaien, begin dan met het Java-framework — dat heeft het meest complete startverhal en de meest directe impact.
**Stap 1:** Voeg de afhankelijkheid toe voor je framework (Spring Boot 3/4, Quarkus of Micronaut). **Stap 2:** Annoteer je bestaande containertests met @ExtendWith(ChaosTestingExtension.class) en @SyscallLevelChaos. **Stap 3:** Kies één L3-annotatie die overeenkomt met een faalmode die je in productie hebt gezien of waar je bezorgd over bent. **Stap 4:** Voer de test uit. Kijk hoe die faalt als je veerkrachtcode er nog niet is. Repareer het. Verzend het met vertrouwen.
De chaos-laag vervangt je andere testfasen niet. Die maakt ze compleet.
- chaos-testing-libraries — C99 LD_PRELOAD-fouten voor elk ELF-proces, elke taal
- chaos-testing-java-agent — JVM-bytecode-agent, 62 JDK-aanroeplocaties, per-test sessie-isolatie
- chaos-testing — JUnit 5-framework, Spring Boot / Quarkus / Micronaut, 64 incident-annotaties
Key Takeaways
De testpiramide had het nooit mis — die was onvolledig. Unittests verifiëren correctheid. Integratietests verifiëren contracten. Containertests verifiëren gedrag tegen echte afhankelijkheden. Chaos-tests verifiëren overleving in een vijandige omgeving.
We bouwden drie bibliotheken die deze laatste mijl afdekken: een C99 LD_PRELOAD-toolkit voor kernel-echte libc-onderschepping, een JVM-bytecode-agent met 62 onderscheppingspunten en per-test sessie-isolatie, en een JUnit 5-annotatie-framework met 64 echte productie-incidenten gecodeerd als herbruikbare testscenario's.
Fase 4 is klaar. Je CI-pipeline wacht.
Engineering Team
Senior Solutions Architects
We bouwen gedistribueerde systemen al voordat 'microservices' een begrip was. Onze littekens vertellen verhalen.