Η Τέταρτη Φάση των Δοκιμών: Γιατί το Chaos Engineering Ανήκει στο CI Pipeline σας
Μηχανική category.testing June 26, 2026

Η Τέταρτη Φάση των Δοκιμών: Γιατί το Chaos Engineering Ανήκει στο CI Pipeline σας

Unit tests, integration tests, container tests — και τώρα chaos tests. Φτιάξαμε ένα τριεπίπεδο chaos engineering stack σε C99, Java, και JUnit 5 που μετατρέπει περιστατικά παραγωγής σε αναπαραγώγιμες CI πύλες.

E
Engineering Team
Senior Solutions Architects
12 λεπτά ανάγνωσης

Το Test που Πέρασε. Το Περιστατικό Παραγωγής που Δεν Πέρασε.

Είναι 3 τα ξημερώματα. Η υπηρεσία σας πετάει 503. Ο circuit breaker που είστε σίγουροι ότι λειτουργεί σωστά — γιατί έχει tests — δεν ανοίγει. Η λογική retry σας, επίσης δοκιμασμένη, ενισχύει το πρόβλημα 9 φορές. Ο DNS resolver σας καταναλώνει EAI_AGAIN ενάντια σε ένα CoreDNS cluster που είναι υπερφορτωμένο, και κάθε retry το κάνει χειρότερο.

Κάνετε rollback. Το περιστατικό κλείνει. Γράφετε το post-mortem.

Έξι εβδομάδες αργότερα, κάνετε deploy ξανά. Ο ίδιος τρόπος αποτυχίας, ελαφρώς διαφορετική μορφή. Τα tests εξακολουθούν να περνούν.

Έχουμε δει αυτό το μοτίβο δεκάδες φορές. Τα tests δεν είναι λανθασμένα. Η δοκιμασμένη συμπεριφορά είναι αληθινή. Αλλά υπάρχει μια κατηγορία αποτυχίας που κανένα unit test, integration test ή container test δεν θα ανιχνεύσει ποτέ: τι συμβαίνει όταν η υποδομή κάτω από την εφαρμογή σας αρχίζει να συμπεριφέρεται με τρόπους που είναι τεχνικά έγκυροι αλλά επιχειρησιακά εχθρικοί. Τι συμβαίνει όταν το DNS αρχίζει να αστατεί; Όταν οι εγγραφές στο δίκτυο επιστρέφουν μικρότερα δεδομένα; Όταν το connection pool σας αδειάζει στο μηδέν, όχι λόγω σφάλματος, αλλά λόγω συντονισμένου φόρτου;

Αυτό είναι το κενό. Και γι' αυτό φτιάξαμε ένα chaos testing stack.

Οι Τέσσερις Φάσεις των Σύγχρονων Δοκιμών

Οι περισσότερες ομάδες μηχανικών σκέφτονται τις δοκιμές σε τρεις φάσεις. Υποστηρίζουμε ότι υπάρχουν τέσσερις, και ότι η παράλειψη της τέταρτης είναι αυτό που κάνει δυνατά τα νυχτερινά περιστατικά.

text
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

Τι Δεν Μπορούν να Ελέγξουν οι Πρώτες Τρεις Φάσεις

Ας είμαστε συγκεκριμένοι για το κενό. Να τι δεν μπορούν να επαληθεύσουν οι Φάσεις 1–3:

**Ο circuit breaker σας ανοίγει πραγματικά.** Μπορείτε να κάνετε unit test τη διαμόρφωση Resilience4j, αλλά ο circuit breaker δεν θα ανοίξει εκτός αν η downstream κλήση αποτύχει πραγματικά με τον σωστό ρυθμό, τη σωστή χρονική στιγμή, με τον σωστό τύπο εξαίρεσης. Τα mocks κάνουν την αποτυχία πολύ καθαρή.

**Η λογική retry σας δεν ενισχύει την αποτυχία.** Ένα 5x retry σε τρία replicas ενάντια σε ένα αποτυχημένο backend δεν είναι ενίσχυση 5x — είναι 15x. Αυτό είναι αόρατο στα unit tests. Είναι αόρατο στα integration tests εκτός αν μηχανευτείτε προσεκτικά μερικές αποτυχίες.

**Ο DNS client σας χειρίζεται μεταβατικές αποτυχίες επίλυσης.** Το getaddrinfo() που επιστρέφει EAI_AGAIN είναι μια έγκυρη, αναμενόμενη απόκριση POSIX. Οι περισσότερες υπηρεσίες δεν δοκιμάζουν ποτέ αυτό το μονοπάτι. Το αποτέλεσμα: μια αναταραχή CoreDNS καταρρέει σε μια διακοπή 30 δευτερολέπτων.

**Το connection pool σας ανακάμπτει υπό πίεση.** Η εξάντληση του pool ακολουθούμενη από μερική ανάκαμψη είναι μια λεπτή state machine. Η λογική ανάκαμψης συνήθως γράφεται μια φορά, δοκιμάζεται ποτέ, και ανακαλύπτεται στην παραγωγή.

**Η στοίβα timeout σας συνθέτεται σωστά.** Ένα read timeout 30s πίσω από ένα HTTP client timeout 10s πίσω από ένα load balancer timeout 5s — η σύνθετη συμπεριφορά δεν είναι αυτό που ασκεί οποιοδήποτε μεμονωμένο test.

Παρουσιάζοντας το Chaos Testing Stack

Φτιάξαμε τρεις open-source βιβλιοθήκες που μαζί καλύπτουν ολόκληρη την επιφάνεια των Phase 4 chaos testing.

**Βιβλιοθήκη 1 — C99 LD_PRELOAD Chaos (`chaos-testing-libraries`):** Ένα σύνολο shared objects που παρεμβαίνουν σε κλήσεις libc στο επίπεδο του dynamic linker. Αγνωστικιστικό ως προς τη γλώσσα — λειτουργεί σε οποιοδήποτε ELF binary: Java, Go, Python, Node.js, Rust με CGO. Έξι fault domains: I/O αρχείων, δικτύωση, DNS, ρολόι, μνήμη, και κύκλος ζωής διεργασιών. Δεν απαιτούνται αλλαγές στον κώδικα της εφαρμογής. Ολόκληρη η διεπαφή είναι ένα αρχείο διαμόρφωσης απλού κειμένου.

**Βιβλιοθήκη 2 — JVM Chaos Agent (`chaos-testing-java-agent`):** Ένας Java agent που οργανώνει 62 σημεία κλήσεων JDK χρησιμοποιώντας ByteBuddy. Εισάγει αποτυχίες, καθυστερήσεις, και πίεση πόρων στο επίπεδο JVM — μέσα στο HikariCP, μέσα στο Netty, μέσα στις δικές του SSL και DNS στοίβες του JDK. Απομονωμένο ανά session ανά test, ώστε πολλαπλά chaos tests να μπορούν να τρέχουν ταυτόχρονα στο ίδιο JVM χωρίς να παρεμβαίνουν μεταξύ τους.

**Βιβλιοθήκη 3 — Java Testing Framework (`chaos-testing`):** Ένα σύστημα επέκτασης JUnit 5 με 448 L1 annotations (ακατέργαστα syscall primitives), 92 L2 composites (τεκμηριωμένα μοτίβα αποτυχίας), και 64 L3 scenarios (πραγματικά περιστατικά παραγωγής αναπαραγόμενα ως μεμονωμένα annotations). Ενοποιείται με Spring Boot 3/4, Quarkus, και Micronaut. Χειρίζεται αυτόματα την έγχυση LD_PRELOAD σε Docker containers μέσω του Docker API.

Και οι τρεις μοιράζονται την ίδια γραμματική DSL selector × effect × probability. Μαθαίνετε το νοητικό μοντέλο μια φορά. Το εφαρμόζετε σε κάθε επίπεδο.

Επίπεδο 1: Πραγματικά Σφάλματα Πυρήνα για Κάθε Διεργασία

Το C99 library set λειτουργεί στο χαμηλότερο επίπεδο διαθέσιμο στον κώδικα χώρου χρήστη: το όριο libc. Όταν η Java διεργασία σας καλεί read() σε ένα socket, περνάει μέσα από glibc. Όταν το Python script σας καλεί socket.connect(), περνάει μέσα από glibc. Εκεί παρεμβαίνουμε — πριν τον πυρήνα, μετά τον κώδικά σας.

Έξι ανεξάρτητα shared objects, το καθένα να κατέχει ένα αποσυνδεδεμένο σύνολο συμβόλων ώστε να μπορούν να συντεθούν χωρίς σύγκρουση:

- `libchaos-io.so` — ανάγνωση/εγγραφή αρχείων, fsync, σχισμένες εγγραφές (short returns), διαφθορά ανάγνωσης ενός bit - `libchaos-net.so` — connect, send, recv, accept, με έγχυση ERRNO, καθυστέρηση, και διαφθορά φορτίου - `libchaos-dns.so` — getaddrinfo/getnameinfo, με ανατάξεις ονομάτων, EAI_AGAIN, EAI_FAIL, ανακάτεμα και περιορισμό απαντήσεων - `libchaos-time.so` — clock_gettime, nanosleep με υπογεγραμμένη απόκλιση χρόνου και έγχυση καθυστέρησης - `libchaos-memory.so` — mmap/munmap/mprotect με έγχυση ENOMEM σε ρυθμιζόμενη πιθανότητα - `libchaos-process.so` — fork, pthread_create, posix_spawn, exec — σημασιολογία fail-after-N και καθυστέρηση

Η διεπαφή είναι ένα αρχείο διαμόρφωσης απλού κειμένου σε μια γνωστή διαδρομή. Χωρίς daemons, χωρίς APIs, χωρίς agents για εκτέλεση. Γράψτε μια διαμόρφωση, φορτώστε τη βιβλιοθήκη, εκτελέστε τη διεργασία σας.

bash
# /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

Επίπεδο 2: 62 Σημεία Παρεμβολής JDK

Το επίπεδο C99 είναι ισχυρό αλλά χονδροειδές: λειτουργεί σε κλήσεις επιπέδου λειτουργικού συστήματος. Για φόρτους εργασίας JVM, χρειάζεστε μεγαλύτερη ακρίβεια. Η διαφορά μεταξύ ενός timeout δανεισμού σύνδεσης από το HikariCP έναντι ενός TCP connection timeout στον πυρήνα είναι ακριβώς η διάκριση που έχει σημασία για τη ρύθμιση circuit breaker.

Ο JVM agent οργανώνει 62 σημεία κλήσεων JDK χρησιμοποιώντας ByteBuddy με @Advice inlining. Μετά τη θέρμανση JIT, η chaos dispatch μεταγλωττίζεται σε ένα null check και ένα μη ληφθέν branch — σχεδόν μηδενικό overhead στη συνηθισμένη περίπτωση.

Κρίσιμα: κάθε test λαμβάνει ένα ChaosSession που υποστηρίζεται από ThreadLocal<UUID>. Τα scenarios εγγράφονται ως session-scoped, ενεργοποιούμενα μόνο σε threads που φέρουν το session ID του test. Πολλαπλά tests τρέχουν ταυτόχρονα στο ίδιο JVM χωρίς παρεμβολή — το chaos από το test A είναι αόρατο στο test B.

Τα Spring Boot 3 και 4 starters κάνουν την καλωδίωση διαφανή — μια εξάρτηση test, ένα annotation, και ο agent επισυνάπτεται μόνος του:

java
@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);
            }
        }
    }
}

Επίπεδο 3: 604 Annotations, 64 Περιστατικά Παραγωγής

Το Java framework είναι αυτό που κάνει το chaos testing προσβάσιμο σε επίπεδο ομάδας. Αντί να μαθαίνουν DSL σφαλμάτων και να ρυθμίζουν πιθανότητες, ένας προγραμματιστής σχολιάζει ένα test με το όνομα ενός πραγματικού μοτίβου περιστατικού και το framework διαχειρίζεται τη σύνθεση.

**448 L1 annotations** δίνουν χειρουργικό έλεγχο στο επίπεδο syscall: `@ChaosRecvEconnreset(probability = 0.05)`, `@ChaosDnsGetaddrinfoEaiAgain(hostPattern = "*.internal", probability = 0.20)`, `@ChaosMmapEnomem(probability = 0.001)`

**92 L2 composites** ομαδοποιούν σχετικά σφάλματα σε τεκμηριωμένα μοτίβα: `@CompositeChaosTransientDnsFailure`, `@CompositeChaosConnectionRefused`, `@CompositeChaosLowMemoryPressure`

**64 L3 scenarios** κωδικοποιούν πραγματικά περιστατικά παραγωγής ως μεμονωμένα annotations — το καθένα με βαθμολογία σοβαρότητας, αναφορά πηγής, και μια κλάση Composer πολλαπλών domains που γνωρίζει πώς να αποσυνθέσει το περιστατικό στο σωστό μίγμα σφαλμάτων syscall:

`@IncidentChaosK8sRollingUpdateRst` — ECONNRESET στο 30% των κλήσεων RECV, αναπαράγοντας το παράθυρο καθυστέρησης iptables κατά τις Kubernetes rolling updates

`@IncidentChaosFeignRetryAmplification` — ECONNREFUSED στο 50% των connect() συν έγχυση IOException στο επίπεδο Feign, αναπαράγοντας το μοτίβο καταιγισμού retry που δημιουργεί ενίσχυση φόρτου 9x

`@IncidentChaosSpringTransactionalPoolDeadlock` — το μοτίβο αδιεξόδου @Transactional(REQUIRES_NEW) που αδειάζει το connection pool από μέσα

Το framework εγχύει αυτόματα τις σωστές βιβλιοθήκες LD_PRELOAD στο Docker container σας πριν ξεκινήσει το test — μέσω του Docker API tar stream, χωρίς shell exec, χωρίς sidecar, χωρίς τροποποίηση image.

java
@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);
    }
}

Ένα DSL, Τρία Επίπεδα, Χωρίς Κενά

Η βασική απόφαση σχεδιασμού είναι ότι και τα τρία επίπεδα μοιράζονται την ίδια εννοιολογική γραμματική: selector × effect × policy.

Ένας **selector** στοχεύει τι να σφάλει: ένα πρόθεμα διαδρομής αρχείου, ένα TCP endpoint, έναν τύπο λειτουργίας JDK, ένα μοτίβο ονόματος DNS. Ένα **effect** ορίζει πώς να σφάλει: έγχυση ERRNO, καθυστέρηση, διαφθορά δεδομένων, απόρριψη, διαφθορά ορίων τιμής. Μια **policy** ελέγχει πότε να σφάλει: πιθανότητα, όριο ρυθμού, αριθμός θέρμανσης, χρονικό παράθυρο, μέγιστες εφαρμογές.

Είτε γράφετε ένα αρχείο διαμόρφωσης C99, είτε χτίζετε ένα ChaosScenario για τον JVM agent, είτε επιλέγετε ένα JUnit 5 annotation, εργάζεστε με το ίδιο νοητικό μοντέλο. Ο προγραμματιστής που κατανοεί το `@IncidentChaosFeignRetryAmplification` μπορεί να εμβαθύνει στα υποκείμενα L1 annotations και να διαβάσει ακριβώς ποια syscalls σφάλλουν, με ποιους ρυθμούς, και γιατί.

Και και τα τρία επίπεδα μπορούν να είναι ενεργά ταυτόχρονα. Μια μεμονωμένη εκτέλεση test μπορεί να έχει: - Το container που τρέχει με `libchaos-net.so` εγχυμένο (30% ECONNRESET) - Τον JVM agent να παράγει πίεση κρυφής μνήμης κώδικα (background stressor) - Ένα ChaosSession με ενεργή απόρριψη JDBC (πρώτα 3 borrows pool αποτυγχάνουν)

Και τα τρία ανεξάρτητα ελεγχόμενα, και τα τρία σωστά εντοπισμένα, κανένα να μην παρεμβαίνει στα άλλα.

Το Chaos ως Πρωτοβάθμιος Πολίτης CI

Η αλλαγή που υποστηρίζουμε δεν αφορά Game Days ή chaos experiments στην παραγωγή. Είναι απλούστερη και πιο πολύτιμη: **κάθε PR που αγγίζει τη λογική χειρισμού σφαλμάτων, retry, circuit breakers, ή διαχείριση συνδέσεων θα πρέπει να έχει ένα chaos test που να αποτελεί πύλη του build**.

Αυτό επαναπλαισιώνει το chaos engineering από ένα πειθαρχία ops σε μια πειθαρχία ανάπτυξης. Το ερώτημα δεν είναι πλέον «μπορεί η πλατφόρμα μας να επιβιώσει από ένα network partition;» — απαντημένο στην παραγωγή, πολύ αργά. Το ερώτημα γίνεται «ανοίγει ο circuit breaker αυτού του PR σωστά όταν το JDBC pool εξαντλείται, πριν αυτός ο κώδικας σταλεί;"

Τα εργαλεία υπάρχουν. Τα annotations απέχουν ένα import. Η Docker injection είναι αυτόματη. Το κόστος προσθήκης του `@IncidentChaosK8sRollingUpdateRst` σε ένα integration test είναι το ίδιο με την προσθήκη οποιουδήποτε άλλου JUnit annotation. Το κόστος του να μην το έχετε είναι αυτό που πληρώνετε στις 3 τα ξημερώματα.

Πού να Ξεκινήσετε

Κάθε βιβλιοθήκη έχει τη δική της ανάλυση σε βάθος, αλλά αν θέλετε chaos tests να τρέχουν σήμερα, ξεκινήστε με το Java framework — έχει την πιο ολοκληρωμένη ιστορία εκκίνησης και τον πιο άμεσο αντίκτυπο.

**Βήμα 1:** Προσθέστε την εξάρτηση για το framework σας (Spring Boot 3/4, Quarkus, ή Micronaut). **Βήμα 2:** Σχολιάστε τα υπάρχοντα container tests σας με @ExtendWith(ChaosTestingExtension.class) και @SyscallLevelChaos. **Βήμα 3:** Επιλέξτε ένα L3 annotation που ταιριάζει με έναν τρόπο αποτυχίας που έχετε δει στην παραγωγή ή για τον οποίο ανησυχείτε. **Βήμα 4:** Εκτελέστε το test. Παρακολουθήστε το να αποτυγχάνει αν ο κώδικας ανθεκτικότητάς σας δεν είναι ακόμα εκεί. Διορθώστε το. Στείλτε το με εμπιστοσύνη.

Το chaos layer δεν αντικαθιστά τις άλλες φάσεις test σας. Τις ολοκληρώνει.

  • chaos-testing-libraries — C99 LD_PRELOAD σφάλματα για κάθε ELF διεργασία, κάθε γλώσσα
  • chaos-testing-java-agent — JVM bytecode agent, 62 σημεία κλήσεων JDK, απομόνωση session ανά test
  • chaos-testing — JUnit 5 framework, Spring Boot / Quarkus / Micronaut, 64 annotations περιστατικών

Key Takeaways

Η πυραμίδα δοκιμών δεν ήταν ποτέ λανθασμένη — ήταν ατελής. Τα unit tests επαληθεύουν την ορθότητα. Τα integration tests επαληθεύουν τα συμβόλαια. Τα container tests επαληθεύουν τη συμπεριφορά ενάντια σε πραγματικές εξαρτήσεις. Τα chaos tests επαληθεύουν την επιβίωση ενάντια σε ένα εχθρικό περιβάλλον.

Φτιάξαμε τρεις βιβλιοθήκες που καλύπτουν αυτό το τελευταίο μίλι: ένα C99 LD_PRELOAD toolkit για πραγματική παρεμβολή libc σε επίπεδο πυρήνα, ένα JVM bytecode agent με 62 σημεία παρεμβολής και απομόνωση session ανά test, και ένα JUnit 5 annotation framework με 64 πραγματικά περιστατικά παραγωγής κωδικοποιημένα ως επαναχρησιμοποιήσιμα σενάρια test.

Η Φάση 4 είναι έτοιμη. Το CI pipeline σας περιμένει.

#chaos-engineering #testing #ci-cd #java #c99 #ldpreload #jvm #junit5 #spring-boot #resilience
E

Engineering Team

Senior Solutions Architects

Χτίζουμε κατανεμημένα συστήματα από πριν το «microservices» γίνει μόδα. Τα σημάδια μας έχουν ιστορίες να διηγηθούν.