De Laag die Toxiproxy Niet Kan Bereiken
Toxiproxy is uitstekend. tc netem is uitstekend. De LD_PRELOAD chaos-bibliotheken zijn uitstekend. Ze werken allemaal op de netwerk- of OS-grens — ze onderscheppen TCP-verbindingen, pakketaflevering en systeemaanroepen.
Maar dit is wat geen van hen kan zien: wat er in uw HikariPool gebeurt als het probeert een verbinding te lenen en een time-out tegenkomt. Wat er in uw ScheduledExecutorService gebeurt als die verzadigd is. Wat er in de SSL-implementatie van de JDK gebeurt als die een herverzendfout tegenkomt. Wat er in uw threadpool gebeurt als taken sneller binnenstromen dan workers ze kunnen verwerken.
Dit zijn JVM-interne storingen. Ze manifesteren zich niet als netwerkfouten op de TCP-laag. Ze manifesteren zich als uitzonderingen die worden gegooid vanuit JDK-klassen — `SQLException`, `RejectedExecutionException`, `SSLHandshakeException`, `TimeoutException` — en als patronen van resource-uitputting die pas zichtbaar worden wanneer u de juiste aanroeplocaties binnen de JVM zelf instrumenteert.
Dit is de kloof die de JVM chaos-agent opvult. Het vervangt de LD_PRELOAD-laag niet — het componeert ermee. Samen bieden ze foutinjectie op zowel de OS-grens als de JVM-interne grens: het volledige oppervlak van de storingen van een Java-applicatie.
62 JDK-aanroeplocaties: Het Volledige Oppervlak
De agent instrumenteert 62 specifieke JDK-aanroeplocaties — niet willekeurige, maar de locaties die ertoe doen voor enterprise backend-workloads. De selectie was gebaseerd op incidentanalyse van storingen in gedistribueerde systemen:
**Threading & Executors** Thread.start(), ThreadPoolExecutor.execute(), ScheduledExecutorService.schedule(), CompletableFuture.runAsync/supplyAsync, BlockingQueue-operaties (put, offer, poll, take), ForkJoinPool-inzendingen, levenscyclus van virtuele threads
**Netwerk & I/O** Socket.connect/read/write, SocketChannel.connect/read/write, Selector.select/selectNow, DatagramChannel-operaties, ServerSocket.accept
**DNS & Naamresolutie** InetAddress.getByName/getAllByName — het interne resolutiepad van de JDK voordat de OS-resolver wordt aangeroepen
**JDBC & Gegevenstoegang** DataSource.getConnection (verbindingspool lenen), Statement.execute/executeQuery, PreparedStatement-operaties — op het niveau waar Hikari, DBCP en c3p0 allemaal doorheen gaan
**HTTP-clients** HttpURLConnection, HttpClient (JDK 11+) — send, sendAsync
**SSL/TLS** SSLEngine.wrap/unwrap, SSLSocket.startHandshake
**Tijd** System.currentTimeMillis(), System.nanoTime() — met VASTE offset-, DRIFT- en BEVRIEZINGSEFFECTEN
**JVM-internals** System.gc(), laden van klassen, reflectie, object-serialisatie/deserialisatie, ThreadLocal.get/set, JNDI-opzoekingen, JMX-operaties, laden van native bibliotheken, ZIP-inflatie
// 62 interception targets across the JDK surface
Sealed interface ChaosSelector permits
ThreadSelector, // Thread.start, pool operations
ExecutorSelector, // ThreadPoolExecutor, ForkJoinPool
QueueSelector, // BlockingQueue operations
FutureSelector, // CompletableFuture
SocketSelector, // Socket, ServerSocket
NioSelector, // SocketChannel, Selector
JdbcSelector, // DataSource.getConnection, Statement
HttpSelector, // HttpURLConnection, HttpClient
DnsSelector, // InetAddress resolution
SslSelector, // SSLEngine, SSLSocket
ClockSelector, // System.currentTimeMillis, nanoTime
GcSelector, // System.gc
ClassloadingSelector, // ClassLoader.loadClass
SerializationSelector,// ObjectInputStream, ObjectOutputStream
ThreadLocalSelector, // ThreadLocal.get, set
VirtualThreadSelector;// VirtualThread lifecycle ByteBuddy + Bootstrap Bridge: Hoe Het Werkt
Het instrumenteren van JDK-klassen is niet eenvoudig. JDK-klassen worden geladen door de bootstrap classloader — de root classloader zonder parent. Agent-code leeft in de agent classloader, die de bootstrap classloader niet bij naam kan zien. Het correct overbruggen van dit gat vereist zorgvuldig gebruik van JVMTI en het Java Memory Model.
**Stap 1 — Premain of agentmain.** De agent koppelt ofwel bij het opstarten (via `-javaagent:`) of dynamisch tijdens een test (via de JDK Attach API). Bij dynamisch koppelen roept de Spring Boot-teststarter `VirtualMachine.attach(processId)` aan en injecteert de agent-jar.
**Stap 2 — Bootstrap-injectie.** Een `BootstrapDispatcher`-klasse wordt uit de agent-jar geëxtraheerd en naar een tijdelijke JAR geschreven. Deze JAR wordt toegevoegd aan het bootstrap-classpath via `Instrumentation.appendToBootstrapClassLoaderSearch()`. De bootstrap classloader kan `BootstrapDispatcher` nu zien.
**Stap 3 — MethodHandle-bedrading.** Er wordt een array van 62 `MethodHandle[]` gebouwd, één handle per interceptiedoel, die verwijzen naar de implementatie in de agent classloader. De array en een `delegate`-object worden als `volatile`-velden in `BootstrapDispatcher` gepubliceerd. Het JMM garandeert dat elke thread die `delegate != null` observeert ook de volledig geïnitialiseerde `handles`-array ziet.
**Stap 4 — Bytecode-advice inlining.** Het `@Advice`-mechanisme van ByteBuddy kopieert de advice-bytecode direct in de body van de geïnstrumenteerde JDK-methode. Er is geen virtuele dispatch, geen interface-aanroep — de geweven bytecode is inline. Na JIT-compilatie bij warm-up (~10.000 aanroepen) compileert de volledige dispatch-keten naar native code.
// Hot path after JIT compilation — effectively just:
public static void connect(Socket socket, SocketAddress endpoint, int timeout) {
Object delegate = BootstrapDispatcher.delegate; // volatile read
if (delegate != null) { // null check — fast path
BootstrapDispatcher.handles[SOCKET_CONNECT] // MethodHandle
.invoke(delegate, socket, endpoint); // → ChaosRuntime
}
originalConnect(socket, endpoint, timeout); // real JDK call
}
// Zero-scenario overhead: one volatile read + one null check + one untaken branch
// Measured: ~60 ns per call in the uncontested case after warm-up Sessie-isolatie: Elke Test Krijgt Zijn Eigen Chaos
Het moeilijkste technische probleem in een JVM-niveau chaos-agent is niet de bytecode-instrumentatie — het is isolatie. Meerdere JUnit-tests draaien gelijktijdig in dezelfde JVM. Als test A JDBC-verbindingsfouten injecteert, mag test B die fouten niet zien.
De oplossing is `ChaosSession`, ondersteund door `ThreadLocal<UUID>`.
Elke testmethode (of testklasse, afhankelijk van de levenscyclus) krijgt een uniek sessie-ID. Chaos-scenario's kunnen worden geregistreerd als ofwel JVM-scoped (heeft invloed op alle threads) of sessie-scoped (heeft alleen invloed op threads die het sessie-ID van deze test meedragen). De `ThreadLocal` zorgt voor de binding.
Voor door executors ingediende taken moet het sessie-ID worden doorgegeven van de indienende thread naar de werkthread. De agent instrumenteert `ThreadPoolExecutor.execute()` om de ingediende `Runnable` te omhullen met een decorator die het huidige sessie-ID vastlegt en het op de werkthread herstelt voordat de taak wordt uitgevoerd. Deze doorgave vindt transparant plaats — de applicatiecode weet niet dat het gebeurt.
@ChaosTest // @SpringBootTest composed annotation
class OrderServiceChaosTest {
@Test
void retryLogic_handles_transient_jdbc_failures(ChaosSession session) {
var scenario = ChaosScenario.builder("transient-db")
.selector(ChaosSelector.jdbc(OperationType.JDBC_CONNECTION_ACQUIRE))
.effect(ChaosEffect.reject("chaos: simulated pool timeout"))
.activationPolicy(ActivationPolicy.builder()
.probability(1.0)
.maxApplications(2) // First 2 borrows fail, 3rd succeeds
.build())
.build();
try (var handle = session.activate(scenario)) {
try (var scope = session.bind()) {
// This thread and any executor tasks it submits
// will see the JDBC failures — nothing else will
var result = orderService.placeOrder(testOrder);
assertThat(result.status()).isEqualTo(OrderStatus.CONFIRMED);
}
}
// Chaos cleaned up — next test is unaffected
}
} Spring Boot 3 en 4: Zero-Config Bedrading
De Spring Boot-teststarters bestaan omdat de bedrading onzichtbaar moet zijn. U hoeft ByteBuddy, de bootstrap classloader of JVMTI niet te begrijpen om uw eerste chaostest uit te voeren. De starter verzorgt:
- Programmatisch instellen van `jdk.attach.allowAttachSelf=true` voordat de JVM deze vlag verwerkt (met behulp van een statische initializer in de auto-configuratieklasse van de starter) - Detecteren of de agent al gekoppeld is (idempotent — veilig bij parallelle uitvoering van testklassen) - Het koppelen van de agent via `VirtualMachine.attach("0")` (zelf-koppeling) als deze nog niet aanwezig is - Registreren van `ChaosControlPlane` en `ChaosSession` als Spring-beans in de test-ApplicationContext - Bieden van JUnit 5-parameterresolutie voor `ChaosSession`- en `ChaosControlPlane`-constructorargumenten
De runtime-starter (voor niet-testimplementatie) stelt daarnaast `/actuator/chaos` beschikbaar als een beveiligd Actuator-eindpunt voor live scenariobeheer.
<!-- Spring Boot 3 — add to pom.xml testImplementation -->
<dependency>
<groupId>com.macstab.chaos</groupId>
<artifactId>chaos-agent-spring-boot3-test-starter</artifactId>
<version>${chaos-agent.version}</version>
<scope>test</scope>
</dependency>
<!-- That's it. No -javaagent flag. No JVM args. No beans to define. -->
<!-- Spring Boot 4 -->
<dependency>
<groupId>com.macstab.chaos</groupId>
<artifactId>chaos-agent-spring-boot4-test-starter</artifactId>
<version>${chaos-agent.version}</version>
<scope>test</scope>
</dependency> De Acht-Poort Activeringspijplijn
Elke evaluatie van een chaos-scenario gaat achtereenvolgens door acht poorten. Alle poorten moeten slagen voordat het effect wordt uitgevoerd. De poorten zijn snel — geëvalueerd op de aanroepende thread zonder I/O en minimale vergrendeling.
**Poort 1 — Gestart-controle.** Is het scenario geactiveerd? (Goedkope vlaguitl.)
**Poort 2 — Sessie-ID-overeenkomst.** Als het sessie-scoped is, draagt de `ThreadLocal` van de huidige thread het juiste UUID? (Thread-local uitl., geen vergrendeling.)
**Poort 3 — Selector-overeenkomst.** Komt deze aanroeplocatie overeen met de selector van het scenario? (Enum-vergelijking, O(1).)
**Poort 4 — Activeringsvenster.** Valt de huidige tijd binnen het geconfigureerde start-/eindvenster van het scenario? (Twee long-vergelijkingen.)
**Poort 5 — Opwarmtelling.** Heeft dit scenario minimaal N keer gematcht voordat het begon toe te passen? (AtomicLong uitl.)
**Poort 6 — Snelheidslimiet.** Heeft het schuivende-venster-tokenemmer capaciteit? (Gesynchroniseerd blok; onbetwist pad ~5 ns.)
**Poort 7 — Kans.** Willekeurige trekking tegen de geconfigureerde kans. (SplittableRandom, gezaaid met scenario-ID + tellingaantal — deterministisch voor het reproduceren van fouten.)
**Poort 8 — Maximale toepassingen.** CAS-lus op AtomicLong om een harde grens af te dwingen. (compareAndSet-lus; correct onder contention — in tegenstelling tot een simpele incrementAndGet die kan overschrijden.)
Alle acht poorten slagen → het effect toepassen. Enige poort faalt → direct doorgaan naar de echte JDK-methode zonder wijzigingen.
Achtergrondsbelasters: Voorbij Aanvraagpad-fouten
Naast inline aanvraagpad-effecten (vertraging, afwijzing, uitzondering-injectie) ondersteunt de agent achtergrondsbelasters — aan levenscyclus gebonden threads die continu resource-druk uitoefenen, onafhankelijk van het verkeer.
Belasters simuleren de langzaam-brandende storingen die niet worden getriggerd door specifieke operaties maar zich in de loop van de tijd ophopen:
**Geheugensbelasters:** Heap-druk (byte[]-chunks vasthouden), GC-druk (allocatiestorm), Metaspace-druk (synthetische klassedefinities), directe bufferdruk (off-heap ByteBuffer), String-intern-overstroming
**JVM-belasters:** Code-cache-druk (door ByteBuddy gegenereerde klassenoverstroming voor JIT-thrashing), finalizer-achterstand (phantom-reference queue-overstroming), referentiewachtrij-overstroming, safepoint-stormen (periodieke hertransformatie om stop-the-world-pauzes te triggeren)
**Thread-belasters:** Thread-lek (permanent geparkeerde threads die thread-stack-geheugen verbruiken), ThreadLocal-lek (vermeldingen op gepoolde threads die zich ophopen), deadlock-injectie (echte JVM-monitor-deadlock tussen N threads — geverifieerd met ThreadMXBean), monitor-contention (achtergrondsthreads die concurreren om een gedeeld slot), keep-alive-threads (voorkomen dat de JVM wordt afgesloten)
Belasters starten wanneer een scenario wordt geactiveerd en stoppen wanneer het wordt gesloten. Ze componeren met inline effecten: u kunt tegelijkertijd aanvraagpad-JDBC-afwijzingen en een code-cache-belaster actief hebben, en testen of uw applicatie herstelt van verbindingsfouten terwijl deze al onder JIT-compilatiedruk staat.
- Heap-druk: configureerbare MB/s vasthouden in byte[]-allocaties om GC-druk te forceren
- Metaspace-druk: synthetische klassen definiëren in een geïsoleerde ClassLoader om permgen/metaspace te verbruiken
- Code-cache-druk: ByteBuddy-klassen genereren om de JIT-compiler te verzadigen
- Safepoint-storm: periodieke hertransformatie forceren om stop-the-world-pauzes te triggeren
- Echte deadlock: twee threads verwerven monitors in omgekeerde volgorde — gedetecteerd door ThreadMXBean
- Thread-lek: threads permanent parkeren om -Xss stack-geheugen in de loop van de tijd uit te putten
Voorbeeld: SLA-gedrag Valideren onder JDBC-uitputting
Een concreet testscenario dat de kernwaarde van de agent demonstreert: bewijzen dat uw service correct reageert op een betaling met een SLA-doel wanneer de databaseverbindingspool onder druk staat.
@SpringBootTest
@ChaosTest
class PaymentSLAChaosTest {
@Autowired PaymentService paymentService;
@Test
void payment_meets_sla_under_intermittent_jdbc_pressure(ChaosSession session) {
// Activate 20% JDBC connection failure — simulates pool under load
var jdbcChaos = ChaosScenario.builder("jdbc-pressure")
.selector(ChaosSelector.jdbc(OperationType.JDBC_CONNECTION_ACQUIRE))
.effect(ChaosEffect.delay(Duration.ofMillis(150))) // Not rejection — delay
.activationPolicy(ActivationPolicy.probability(0.20))
.build();
// Also activate a background heap stressor
var heapStressor = ChaosScenario.builder("heap-pressure")
.effect(ChaosEffect.stressor(StressorType.HEAP_PRESSURE)
.retainMbPerSecond(50)
.build())
.build();
try (var h1 = session.activate(jdbcChaos);
var h2 = session.activate(heapStressor)) {
try (var scope = session.bind()) {
long start = System.nanoTime();
var result = paymentService.process(testPayment);
long elapsedMs = (System.nanoTime() - start) / 1_000_000;
assertThat(result.status()).isEqualTo(PaymentStatus.COMPLETED);
assertThat(elapsedMs).isLessThan(500); // SLA: p99 < 500ms
}
}
}
} JMX en Observeerbaarheid
De agent stelt een JMX MBean beschikbaar op `com.macstab.chaos.jvm:type=ChaosDiagnostics` die een live momentopname biedt van alle actieve scenario's — hun huidige toestand, tellingaantallen en toegepaste tellingaantallen — zonder code-wijzigingen of aanvullende configuratie.
Dit is handig bij het debuggen van tests: als een chaostest onverwacht faalt, kunt u JConsole of VisualVM verbinden met de test-JVM en precies inspecteren welke scenario's actief zijn en hoe vaak ze zijn uitgevoerd.
De observeerbare bus maakt het mogelijk chaos-gebeurtenissen te publiceren naar externe metrische systemen. Als u chaostests uitvoert in een gedeelde integratieomgeving en chaos-gebeurtenissen wilt correleren met Grafana-dashboards of gedistribueerde traces, biedt de bus het integratiepunt.
- JMX MBean: actieve scenario-lijst, tellingaantallen, toegepaste tellingaantallen, huidige toestand per scenario
- In-process momentopname-API: bevraagbaar vanuit testcode voor beweringen over chaos-gedrag
- Debug-dump: tekstweergave van alle scenario's en toestand — nuttig in testfoutuitvoer
- Observeerbare bus: uitbreidbare gebeurtenisuitgever voor metrische/trace-integratie
Key Takeaways
De JVM chaos-agent vult de kloof tussen foutinjectie op infrastructuurniveau en echte JVM-interne storingen. 62 JDK-aanroeplocaties, per-testsessie-isolatie, Spring Boot 3/4 auto-wiring, achtergrondsbelasters voor langzaam-brandende storingen, en een JIT-geoptimaliseerd dispatch-pad dat ~60 ns per aanroep kost in het nul-scenario-geval.
De agent componeert met de C99 LD_PRELOAD-bibliotheek voor volledige stack-foutdekking: syscall-niveau fouten onder de JVM, bytecode-niveau fouten erin. Beide worden georkestreerd door het annotatiesysteem van het Java-testframework — één test, beide lagen actief, nul configuratieoverlap.
Chaos engineering als CI-gate is klaar. De enige vraag is welke storingsmodus uw circuit breaker nog niet afhandelt.
Engineering Team
Senior Solutions Architects
We bouwen al gedistribueerde systemen voordat 'microservices' een begrip was. Onze littekens vertellen verhalen.