Ένα Annotation. Το Τελευταίο Περιστατικό Παραγωγής σας. Αναπαραγόμενο.
Κάπου στο ιστορικό Slack της ομάδας σας υπάρχει ένα post-mortem. Έχει μια ενότητα «Βασική Αιτία» που περιγράφει πώς μια κυλιόμενη ενημέρωση Kubernetes δημιούργησε ένα παράθυρο 30 δευτερολέπτων όπου οι κανόνες iptables δεν είχαν ακόμα αντικατοπτρίσει την αφαίρεση του νέου pod, και ECONNRESET επιστρεφόταν σε ενεργές συνδέσεις προς το παλιό pod. Έχει μια ενότητα «Μετριασμός» που λέει ότι ρυθμίσατε επαναλήψεις με backoff. Έχει μια ενότητα «Ενέργειες» με ένα πλαίσιο ελέγχου δίπλα στο «Προσθήκη chaos test για αυτό το σενάριο».
Αυτό το πλαίσιο ελέγχου δεν έχει ποτέ επιλεγεί.
Όχι επειδή η ομάδα δεν νοιάζεται. Επειδή η συγγραφή ενός chaos test για αυτό το συγκεκριμένο σενάριο απαιτεί κατανόηση του πώς η καθυστέρηση iptables αντιστοιχίζεται σε ποσοστά ECONNRESET, ποια κλήση libc παρεμβάλλεται, ποια είναι η σωστή πιθανότητα, και πώς να εισαχθεί το σφάλμα σε ένα Docker container που εκτελείται σε περιβάλλον CI.
Αυτή είναι πολλή προαπαιτούμενη γνώση για ένα πλαίσιο ελέγχου.
`@IncidentChaosK8sRollingUpdateRst(toxicity = 0.3)` είναι ένα import και ένα annotation. Αυτό είναι το πλαίσιο ελέγχου που επιλέγεται.
// The post-mortem's action item, as a test:
@Test
@IncidentChaosK8sRollingUpdateRst(toxicity = 0.3)
void service_survives_rolling_update_rst_storm() {
// 30% of RECV calls return ECONNRESET
// This is exactly what a Kubernetes rolling update looks like
assertThat(callService("/api/health")).isEqualTo(200);
} Η Τριεπίπεδη Ιεραρχία Annotations
Το πλαίσιο οργανώνει τα σενάρια chaos σε τρία επίπεδα, καθένα από τα οποία αντιμετωπίζει διαφορετικό επίπεδο εξειδίκευσης και περίπτωση χρήσης:
**L1 — Ακατέργαστα Πρωτόγονα (448 annotations):** Άμεσος έλεγχος σε επίπεδο syscall. Ένα annotation αντιστοιχίζεται σε έναν κανόνα σφάλματος: ένα errno, έναν τύπο λειτουργίας, μια πιθανότητα. Αυτό είναι το επίπεδο για μηχανικούς που γνωρίζουν ακριβώς ποια κλήση πυρήνα θέλουν να αποτύχει και με ποιο ρυθμό. Πλήρης εκφραστική δύναμη, χωρίς αφαίρεση.
**L2 — Κατονομαζόμενα Σύνθετα (92 annotations):** Τεκμηριωμένες οικογένειες σφαλμάτων, καθεμία συνθέτοντας 2–6 κανόνες L1 σε ένα κατονομαζόμενο πρότυπο. Αυτό είναι το επίπεδο για κοινά σενάρια αποτυχίας που έχουν μια καλά κατανοητή μορφή αλλά δεν χρειάζεται να ανιχνευθούν σε συγκεκριμένο περιστατικό. Οι προεπιλογές είναι βαθμονομημένες από ανάλυση πραγματικών αποτυχιών.
**L3 — Σενάρια Περιστατικών (64 annotations):** Πραγματικά περιστατικά παραγωγής κωδικοποιημένα ως συνθέσεις πολλαπλών τομέων. Κάθε annotation L3 φέρει βαθμολογία Σοβαρότητας, αναφορά στο αρχικό περιστατικό ή πηγή του κλάδου, και μια κλάση Composer που γνωρίζει πώς να αποσυνθέσει το σενάριο στο σωστό μείγμα κανόνων L1 σε πολλαπλούς τομείς. Αυτό είναι το επίπεδο για «αναπαραγωγή αυτού που συνέβη στο post-mortem».
Τα επίπεδα δεν είναι αποκλειστικά — μπορείτε να αναμίξετε annotations L1, L2 και L3 στην ίδια κλάση ή μέθοδο δοκιμής, και συσσωρεύονται σωστά. Τα annotations σε εμβέλεια μεθόδου παρακάμπτουν τα annotations σε εμβέλεια κλάσης για τη διάρκεια της μεθόδου δοκιμής, και μετά επαναφέρουν τους κανόνες εμβέλειας κλάσης όταν η μέθοδος τελειώσει.
L1: Ακατέργαστα Πρωτόγονα Syscall — 448 Annotations
Το επίπεδο L1 σάς δίνει άμεσο έλεγχο επί μεμονωμένων κανόνων σφαλμάτων syscall. Κάθε annotation ακολουθεί το ίδιο πρότυπο: λειτουργία στόχου + errno ή αποτέλεσμα + πιθανότητα + προαιρετικό μοτίβο κεντρικού υπολογιστή ή διαδρομής.
// Network — connect/recv/send
@ChaosConnectEconnrefused(probability = 0.10)
@ChaosConnectEtimedout(probability = 0.03)
@ChaosRecvEconnreset(probability = 0.05)
@ChaosSendEpipe(probability = 0.02)
// DNS — getaddrinfo
@ChaosDnsGetaddrinfoEaiAgain(hostPattern = "*.internal", probability = 0.20)
@ChaosDnsGetaddrinfoEaiFail(hostPattern = "legacy.svc", probability = 1.0)
// File I/O — writes and reads
@ChaosWriteTorn(pathPrefix = "/data/wal", probability = 0.05)
@ChaosReadEio(pathPrefix = "/etc/app", probability = 0.01)
@ChaosWriteEnospc(pathPrefix = "/data", probability = 0.001)
// Memory
@ChaosMmapEnomem(probability = 0.005)
// Timing — breaks JWT expiry checks, caching TTLs
@ChaosClock(effect = ClockEffect.OFFSET, offsetMs = 30_000) // +30 second skew
// All annotations support:
// probability — 0.0 (never) to 1.0 (always)
// id — target a specific container by ID
// onMissingEnv — ERROR (fail test) or ABORT (skip test) if chaos env unavailable L2: Κατονομαζόμενα Σύνθετα Σφαλμάτων — 92 Τεκμηριωμένα Πρότυπα
Το επίπεδο L2 κωδικοποιεί τεκμηριωμένα πρότυπα αποτυχίας με βαθμονομημένες προεπιλογές. Κάθε σύνθετο αντιπροσωπεύει μια μορφή αποτυχίας που έχει όνομα στα post-mortems περιστατικών και γνωστή, προβλέψιμη επίπτωση στη συμπεριφορά της εφαρμογής.
- @CompositeChaosConnectionRefused — ECONNREFUSED σε κάθε connect(), προσομοιώνοντας νεκρό downstream
- @CompositeChaosTransientDnsFailure — EAI_AGAIN στο 15% των κλήσεων getaddrinfo(), προσομοιώνοντας υπερφόρτωση CoreDNS
- @CompositeChaosLowMemoryPressure — ENOMEM στο 0,5% των κλήσεων mmap(), προσομοιώνοντας διαμάχη μνήμης
- @CompositeChaosNetworkFlap — εναλλαγή ECONNRESET και επιτυχίας, προσομοιώνοντας ασταθή σύνδεση δικτύου
- @CompositeChaosSlowDisk — καθυστέρηση 50–200ms στο write(), προσομοιώνοντας κορεσμένο αποθηκευτικό χώρο I/O
- @CompositeChaosClockDrift — υπογεγραμμένη απόκλιση ρολογιού ±5 δευτερόλεπτα, προσομοιώνοντας απόκλιση NTP
- @CompositeChaosConnectionTimeout — ETIMEDOUT στο 5% των connect() με καθυστέρηση 3s, προσομοιώνοντας λήξη χρονικού ορίου τείχους προστασίας
- @CompositeChaosShortWrite — TORN στο 10% των εγγραφών, προσομοιώνοντας επιστροφή μικρότερων μετρήσεων από το σύστημα αρχείων
L3: Πραγματικά Περιστατικά Παραγωγής — 64 Σχολιασμένα Σενάρια
Το επίπεδο L3 είναι όπου το chaos engineering γίνεται θεσμική μνήμη. Κάθε annotation κωδικοποιεί έναν πραγματικό τρόπο αποτυχίας — τεκμηριωμένο σε αναφορές περιστατικών παραγωγής, ιχνηλάτες ζητημάτων Kubernetes, ή γνωστές αναλύσεις αποτυχιών κατανεμημένων συστημάτων.
Το πρότυπο Composer χειρίζεται την αποσύνθεση: κάθε annotation L3 αναφέρεται σε μια κλάση Composer που γνωρίζει πώς να μεταφράσει μια περιγραφή περιστατικού υψηλού επιπέδου στο σωστό μείγμα κανόνων L1 σε πολλαπλούς τομείς σφαλμάτων.
// L3 Incident: Kubernetes Rolling Update RST Storm
// Source: Kubernetes GitHub Issue #56903, multiple production incidents
// Severity: CRITICAL
// What happens: During rolling updates, iptables rules update asynchronously.
// Old pods receive connection attempts for ~15-30s after replacement.
// Active connections to old pods get ECONNRESET when the pod terminates.
@IncidentChaosK8sRollingUpdateRst(toxicity = 0.3)
// L3 Incident: Feign Retry Amplification Storm
// Source: Documented in multiple high-traffic Java service incidents
// Severity: CRITICAL
// What happens: Feign default retry (5x) × replicas (3) × backends (N)
// creates multiplicative amplification. A 50% failure rate becomes 9x load.
@IncidentChaosFeignRetryAmplification(toxicity = 0.5)
// L3 Incident: Spring @Transactional(REQUIRES_NEW) Deadlock
// Source: Spring Framework known issue pattern
// Severity: SEVERE
// What happens: REQUIRES_NEW suspends outer transaction and borrows new
// connection. Under load, pool exhausts — outer tx holds one connection,
// REQUIRES_NEW waits for another, deadlock.
@IncidentChaosSpringTransactionalPoolDeadlock
// L3 Incident: Kubernetes DNS ndots=5 Storm
// Source: CoreDNS performance analysis, ndots search path behavior
// Severity: SEVERE
// What happens: With ndots:5 (K8s default), each hostname generates 5+
// DNS queries (search path expansion). Under CoreDNS load, EAI_AGAIN
// spikes. Services that don't handle DNS transient failures cascade.
@IncidentChaosK8sDnsNdots5Storm
// L3 Incident: JVM Code Cache Full
// Source: JVM JIT compilation documentation, production profiling
// Severity: SEVERE
// What happens: When code cache fills, JIT stops compiling new methods.
// Throughput drops 10–50x as hot paths deoptimize to interpreted mode.
@IncidentChaosJvmCodeCacheFull
// L3 Incident: Redis Sentinel Failover Storm
// Source: Redis Sentinel documentation, observed failover cascades
// Severity: MEDIUM
// What happens: Sentinel failover triggers connection flap on all clients.
// Applications that don't handle LOADING errors loop reconnecting,
// overwhelming the new master.
@IncidentChaosRedisNetworkFlap Docker + LD_PRELOAD: Chaos σε Οποιαδήποτε Εικόνα Παραγωγής
Για δοκιμές που εκτελούν κώδικα εφαρμογής μέσα σε Docker containers (πρότυπο Testcontainers), το πλαίσιο εισάγει αυτόματα τις βιβλιοθήκες C99 LD_PRELOAD στο container πριν ξεκινήσει.
Ο μηχανισμός έγχυσης χρησιμοποιεί τη ροή tar του Docker API — όχι shell exec, όχι τόμους, όχι init containers:
1. Η επέκταση JUnit ανιχνεύει το βασικό λειτουργικό σύστημα της εικόνας container (Alpine → musl, Debian/RHEL/Ubuntu → glibc). 2. Επιλέγει το σωστό προ-μεταγλωττισμένο δυαδικό αρχείο (glibc/musl × amd64/arm64 — τέσσερις παραλλαγές, ενσωματωμένες στο JAR). 3. Αντιγράφει το αρχείο `.so` στο container μέσω `DockerClient.copyArchiveToContainerCmd()` πριν από το `container.start()`. 4. Ορίζει `LD_PRELOAD=/chaos/libchaos-net.so` (και άλλα όπως δηλώνονται) στο περιβάλλον του container.
Αυτό σημαίνει ότι το chaos testing λειτουργεί σε εικόνες `distroless`, εικόνες βασισμένες σε `scratch`, UBI minimal, Alpine — οποιαδήποτε εικόνα που χρησιμοποιεί τυπικό δυναμικό συνδέτη ELF. Χωρίς τροποποίηση Dockerfile, χωρίς sidecar, χωρίς πρόσθετη υποδομή.
// @SyscallLevelChaos declares which libraries to inject
// The framework handles the rest: image detection, binary selection, injection
@Testcontainers
@ExtendWith(ChaosTestingExtension.class)
@SyscallLevelChaos({LibchaosLib.NET, LibchaosLib.DNS}) // Inject both
class InventoryServiceChaosTest {
@Container @AppContainer
static GenericContainer<?> inventory =
new GenericContainer<>("inventory-service:latest") // Any image works
.withExposedPorts(8080);
@Container
static PostgreSQLContainer<?> db = new PostgreSQLContainer<>("postgres:16");
@Test
@CompositeChaosTransientDnsFailure // L2: 15% EAI_AGAIN on DNS
void inventory_lookup_resilient_to_dns_hiccups(String inventoryUrl) {
// libchaos-dns.so is active inside the inventory-service container
// DNS calls from the service will see 15% EAI_AGAIN failures
var response = given().get(inventoryUrl + "/api/product/123");
assertThat(response.statusCode()).isEqualTo(200);
}
} Ενσωματώσεις Πλαισίου: Spring Boot, Quarkus, Micronaut
Κάθε σημαντικό πλαίσιο Java αποκτά ένα αφιερωμένο module ενσωμάτωσης που χειρίζεται τόσο τη διαχείριση από την πλευρά των δοκιμών όσο και το προαιρετικό επίπεδο ελέγχου chaos κατά την εκτέλεση.
**Spring Boot 3 και 4:** Το starter δοκιμών παρέχει `@ChaosTest` ως σύνθετο annotation, καταχωρεί `ChaosControlPlane` και `ChaosSession` ως Spring beans, και παρέχει επίλυση παραμέτρων JUnit 5 και για τα δύο. Συνθέτεται με `@SpringBootTest`, `@DataJpaTest`, `@WebMvcTest`, και οποιοδήποτε άλλο annotation φέτας δοκιμής Spring.
Το runtime starter εκθέτει το `/actuator/chaos` ως προστατευμένο endpoint Actuator για διαχείριση σεναρίων σε πραγματικό χρόνο σε περιβάλλοντα μη παραγωγής.
**Quarkus:** Η επέκταση Quarkus παρέχει `@QuarkusChaosTest` ως σύνθετο annotation δοκιμής Quarkus με CDI injection του `ChaosControlPlane`. Ενσωματώνεται με `@QuarkusTest`, `@QuarkusIntegrationTest`, και δοκιμές native image.
**Micronaut:** Η ενσωμάτωση Micronaut παρέχει `@MicronautChaosTest` με `ChaosControlPlane` και `ChaosSession` beans συμβατά με `@Inject`. Λειτουργεί με `@MicronautTest` τόσο σε λειτουργία δοκιμών JVM όσο και native.
- Spring Boot 3/4: σύνθετο annotation @ChaosTest, ChaosControlPlane ως Spring bean, endpoint /actuator/chaos
- Quarkus: @QuarkusChaosTest, CDI injection, λειτουργεί με @QuarkusIntegrationTest και native
- Micronaut: @MicronautChaosTest, beans συμβατά με @Inject, υποστήριξη δοκιμών JVM και native
- Απλό JUnit 5: @ExtendWith(ChaosTestingExtension.class) με χειροκίνητο ChaosControlPlane — δεν απαιτείται πλαίσιο
- Συμβατό με Gradle και Maven: όλα τα modules δημοσιευμένα στο Maven Central με τυπικές συντεταγμένες
Πόροι και Περιορισμοί: Annotation @Resources
Πέρα από την έγχυση σφαλμάτων, το πλαίσιο υποστηρίζει δοκιμές περιορισμού πόρων μέσω του `@Resources`. Αυτό δηλώνει όρια CPU και μνήμης που η επέκταση JUnit εφαρμόζει στο Docker container πριν ξεκινήσει — χωρίς τροποποίηση του ορισμού container στον κώδικα δοκιμής.
Αυτό επιτρέπει μια κατηγορία δοκιμών που συχνά παραλείπεται: «συμπεριφέρεται σωστά η υπηρεσία μου όταν έχει περιορισμούς πόρων;» Αυτό είναι ένα ρεαλιστικό σενάριο στο Kubernetes, όπου τα pods εκτελούνται με όρια CPU και μνήμης, και όπου η υπέρβαση αυτών των ορίων ενεργοποιεί throttling και αναγκαστικές τερματίσεις λόγω OOM.
// Test your service under the same resource constraints as production
@Testcontainers
@ExtendWith(ChaosTestingExtension.class)
@SyscallLevelChaos(LibchaosLib.NET)
@Resources(cpuQuota = 0.5, memoryMb = 256) // Same limits as production K8s pod
class ResourceConstrainedChaosTest {
@Container @AppContainer
static GenericContainer<?> app =
new GenericContainer<>("my-service:latest")
.withExposedPorts(8080);
@Test
@IncidentChaosJvmCodeCacheFull // Code cache fills → JIT stops
void service_degrades_gracefully_under_resource_pressure(String appUrl) {
// CPU-throttled + code cache pressure = realistic production stress test
var response = given().get(appUrl + "/api/health");
// Service should return 200 or 503 — not hang forever
assertThat(response.statusCode()).isIn(200, 503);
assertThat(response.time()).isLessThan(5000); // Under 5s even degraded
}
} Η Αρχιτεκτονική Plugin: Μια Επέκταση, Όλα τα Containers
Οι πρώτες εκδόσεις του πλαισίου είχαν ξεχωριστές επεκτάσεις JUnit για κάθε τύπο container: μία για Redis, μία για PostgreSQL, μία για Kafka, και ούτω καθεξής. Κάθε επέκταση είχε 200+ γραμμές σχεδόν πανομοιότυπου κώδικα κύκλου ζωής. Η επέκταση σε νέο τύπο container σήμαινε αντιγραφή και τροποποίηση.
Η τρέχουσα αρχιτεκτονική χρησιμοποιεί ένα SPI `ChaosPlugin` που ανακαλύπτεται μέσω `ServiceLoader.load(ChaosPlugin.class)`. Κάθε τύπος container καταχωρεί μία υλοποίηση. Η ενιαία `ChaosTestingExtension` τα ενορχηστρώνει όλα.
Το συμβόλαιο SPI είναι ελάχιστο: ανακάλυψη containers στην κλάση δοκιμής, παροχή πληροφοριών σύνδεσης, διαχείριση εφαρμογής πόρων, και διαχείριση ρύθμισης chaos πριν την εκκίνηση. Η καθολική επέκταση χειρίζεται τα υπόλοιπα: επεξεργασία annotations, διαχείριση κύκλου ζωής, απομόνωση συνεδρίας, επίλυση παραμέτρων.
Αυτό εξαλείφει ~8.000 γραμμές επανάληψης και καθιστά το πλαίσιο επεκτάσιμο σε νέους τύπους container χωρίς τροποποίηση του πυρήνα.
// Registering a custom container type
public class MyServiceChaosPlugin implements ChaosPlugin {
@Override
public boolean supportsContainer(GenericContainer<?> container) {
return container.getDockerImageName().startsWith("my-service:");
}
@Override
public ConnectionInfo extractConnectionInfo(GenericContainer<?> container,
Annotation annotation) {
return ConnectionInfo.of(
"http://" + container.getHost() + ":" + container.getMappedPort(8080)
);
}
@Override
public Class<? extends Annotation> containerAnnotation() {
return AppContainer.class; // @AppContainer marks this container
}
}
// META-INF/services/com.macstab.chaos.core.spi.ChaosPlugin:
// com.example.chaos.MyServiceChaosPlugin 43 Modules, 11 Τομείς, Ένα Νοητικό Μοντέλο
Το πλαίσιο οργανώνεται σε 43 modules Gradle που καλύπτουν 11 τομείς σφαλμάτων. Εδώ είναι η πλήρης εικόνα:
**Βασική υποδομή:** chaos-core (επέκταση JUnit 5, SPI, κύκλος ζωής), chaos-spi (συμβόλαιο plugin), chaos-api (τύποι επιλογέα και αποτελέσματος)
**Πακέτα δοκιμών τομέα σφαλμάτων:** testpacks-connection (TCP/UDP), testpacks-dns (επίλυση DNS), testpacks-memory (malloc/mmap), testpacks-process (fork/exec/σήματα), testpacks-time (ρολόι, χρονική απόκλιση), testpacks-filesystem (I/O αρχείων)
**Ενσωμάτωση JVM:** chaos-java (μεταφορά bytecode JVM, καλωδίωση πλευράς container)
**Πακέτα περιστατικών L3:** testpacks-l3-kubernetes (κυλιόμενες ενημερώσεις, καταιγίδες DNS), testpacks-l3-feign (ενίσχυση επαναλήψεων), testpacks-l3-spring (αδιέξοδο συναλλαγών, στέρηση OSIV), testpacks-l3-redis (καταιγίδες failover), testpacks-l3-kafka (failover broker), testpacks-l3-grpc (καταιγίδες GOAWAY)
**Ενσωματώσεις πλαισίου:** java-spring-boot3, java-spring-boot3-test, java-spring-boot4, java-spring-boot4-test, java-quarkus, java-micronaut
- 448 annotations L1: ακατέργαστα πρωτόγονα syscall σε 6 τομείς σφαλμάτων σε επίπεδο λειτουργικού συστήματος
- 92 σύνθετα L2: κατονομαζόμενα πρότυπα σφαλμάτων με βαθμονομημένες, παραγωγής-βασισμένες προεπιλογές
- 64 περιστατικά L3: πραγματικά post-mortems ως μεμονωμένα annotations με βαθμολογίες Σοβαρότητας
- 43 modules Gradle: ανεξάρτητα εκδόσεων, χωρίς υποχρεωτικές μεταβατικές εξαρτήσεις
- 4 δυαδικές παραλλαγές: glibc-amd64, glibc-arm64, musl-amd64, musl-arm64 — ενσωματωμένες στο JAR
- Spring Boot 3/4, Quarkus, Micronaut: πλήρης ενσωμάτωση πλαισίου εκτός κουτιού
Key Takeaways
Το πλαίσιο υπάρχει επειδή τα post-mortems περιστατικών έχουν ενέργειες που ποτέ δεν υλοποιούνται. Όχι επειδή οι μηχανικοί είναι τεμπέληδες, αλλά επειδή το χάσμα εργαλείων μεταξύ «πρέπει να δοκιμάσουμε αυτό τον τρόπο αποτυχίας» και «εδώ είναι μια δοκιμή που ελέγχει αυτό τον τρόπο αποτυχίας» ήταν πολύ μεγάλο.
448 annotations L1 για χειρουργικό έλεγχο. 92 σύνθετα L2 για τεκμηριωμένες οικογένειες αποτυχιών. 64 σενάρια περιστατικών L3 που σας επιτρέπουν να μετατρέψετε ένα post-mortem Slack σε αποτυχημένη δοκιμή CI σε πέντε λεπτά.
Το στρώμα chaos δεν αντικαθιστά τις unit tests ή τις integration tests. Συμπληρώνει την εικόνα. Η Φάση 4 είναι η φάση όπου το CI pipeline σας μαθαίνει να αναπαράγει περιστατικά παραγωγής πριν συμβούν ξανά.
Engineering Team
Senior Solutions Architects
Χτίζουμε κατανεμημένα συστήματα από πριν τα «microservices» γίνουν μόδα. Οι πληγές μας έχουν ιστορίες να διηγηθούν.