Uma Anotação. Seu Último Incidente em Produção. Reproduzido.
Em algum lugar no histórico do Slack da sua equipe há um post-mortem. Ele tem uma seção chamada "Causa Raiz" que descreve como uma atualização contínua do Kubernetes criou uma janela de 30 segundos em que as regras do iptables ainda não haviam propagado a remoção do novo pod, e ECONNRESET estava sendo retornado em conexões ativas com o pod antigo. Há uma seção "Mitigação" que diz que você configurou retry-with-backoff. Há uma seção "Itens de Ação" com uma caixa de seleção ao lado de "Adicionar teste de caos para este cenário."
Essa caixa de seleção nunca foi marcada.
Não porque a equipe não se importa. Mas porque escrever um teste de caos para esse cenário específico requer entender como o atraso do iptables se mapeia para as taxas de ECONNRESET, qual chamada libc é interceptada, qual é a probabilidade certa e como injetar a falha em um container Docker rodando em um ambiente de CI.
Isso é muito conhecimento pré-requisito para uma caixa de seleção.
`@IncidentChaosK8sRollingUpdateRst(toxicity = 0.3)` é um import e uma anotação. É isso — a caixa de seleção marcada.
// 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);
} A Hierarquia de Anotações em Três Camadas
O framework organiza os cenários de caos em três camadas, cada uma abordando um nível diferente de expertise e caso de uso:
**L1 — Primitivos Brutos (448 anotações):** Controle direto no nível de syscall. Uma anotação mapeia para uma regra de falha: um errno, um tipo de operação, uma probabilidade. Esta é a camada para engenheiros que sabem exatamente qual chamada de kernel querem falhar e em qual taxa. Poder expressivo total, sem abstração.
**L2 — Compostos Nomeados (92 anotações):** Famílias de falhas documentadas, cada uma compondo de 2 a 6 regras L1 em um padrão nomeado. Esta é a camada para cenários de falha comuns que têm uma forma bem compreendida, mas não precisam rastrear até um incidente específico. Os padrões são calibrados a partir de análises de falhas do mundo real.
**L3 — Cenários de Incidentes (64 anotações):** Incidentes reais em produção codificados como composições de múltiplos domínios. Cada anotação L3 carrega uma classificação de Severidade, uma referência ao incidente original ou fonte do setor, e uma classe Composer que sabe como decompor o cenário na mistura certa de regras L1 em múltiplos domínios. Esta é a camada para "reproduzir o que aconteceu no post-mortem".
As camadas não são exclusivas — você pode misturar anotações L1, L2 e L3 na mesma classe ou método de teste, e elas se acumulam corretamente. Anotações com escopo de método substituem anotações com escopo de classe durante a execução do método de teste e restauram as regras com escopo de classe quando o método termina.
L1: Primitivos Brutos de Syscall — 448 Anotações
A camada L1 oferece controle direto sobre regras de falha de syscall individuais. Toda anotação segue o mesmo padrão: operação alvo + errno ou efeito + probabilidade + padrão opcional de host ou caminho.
// 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: Compostos de Falha Nomeados — 92 Padrões Documentados
A camada L2 codifica padrões de falha documentados com padrões calibrados. Cada composto representa uma forma de falha que tem um nome em post-mortems de incidentes e um impacto conhecido e previsível no comportamento da aplicação.
- @CompositeChaosConnectionRefused — ECONNREFUSED em todo connect(), simulando um serviço downstream inativo
- @CompositeChaosTransientDnsFailure — EAI_AGAIN em 15% das chamadas getaddrinfo(), simulando sobrecarga do CoreDNS
- @CompositeChaosLowMemoryPressure — ENOMEM em 0,5% das chamadas mmap(), simulando contenção de memória
- @CompositeChaosNetworkFlap — alternância entre ECONNRESET e sucesso, simulando uma conexão de rede instável
- @CompositeChaosSlowDisk — latência de 50 a 200ms no write(), simulando armazenamento com I/O saturado
- @CompositeChaosClockDrift — desvio de clock de ±5 segundos com sinal, simulando deriva de NTP
- @CompositeChaosConnectionTimeout — ETIMEDOUT em 5% dos connect() com latência de 3s, simulando timeout de firewall
- @CompositeChaosShortWrite — TORN em 10% das escritas, simulando filesystem retornando contagens curtas
L3: Incidentes Reais em Produção — 64 Cenários Anotados
A camada L3 é onde o chaos engineering se torna memória institucional. Cada anotação codifica um modo de falha real — documentado em relatórios de incidentes em produção, rastreadores de issues do Kubernetes ou análises conhecidas de falhas em sistemas distribuídos.
O padrão Composer lida com a decomposição: cada anotação L3 referencia uma classe Composer que sabe como traduzir uma descrição de incidente de alto nível na mistura certa de regras L1 em múltiplos domínios de falha.
// 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: Caos em Qualquer Imagem de Produção
Para testes que executam código de aplicação dentro de containers Docker (padrão Testcontainers), o framework injeta automaticamente as bibliotecas C99 LD_PRELOAD no container antes de iniciá-lo.
O mecanismo de injeção usa o fluxo tar da API Docker — não shell exec, não volumes, não init containers:
1. A extensão JUnit detecta o sistema operacional base da imagem do container (Alpine → musl, Debian/RHEL/Ubuntu → glibc). 2. Ela seleciona o binário pré-compilado correto (glibc/musl × amd64/arm64 — quatro variantes, empacotadas no JAR). 3. Copia o arquivo `.so` para dentro do container via `DockerClient.copyArchiveToContainerCmd()` antes de `container.start()`. 4. Define `LD_PRELOAD=/chaos/libchaos-net.so` (e outros conforme declarado) no ambiente do container.
Isso significa que o chaos testing funciona em imagens `distroless`, imagens baseadas em `scratch`, UBI minimal, Alpine — qualquer imagem que use um dynamic linker ELF padrão. Sem modificação de Dockerfile, sem sidecar, sem infraestrutura adicional.
// @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);
}
} Integrações com Frameworks: Spring Boot, Quarkus, Micronaut
Cada grande framework Java recebe um módulo de integração dedicado que cuida tanto da orquestração no lado do teste quanto do plano de controle de caos opcional em tempo de execução.
**Spring Boot 3 e 4:** O starter de teste fornece `@ChaosTest` como uma anotação composta, registra `ChaosControlPlane` e `ChaosSession` como beans Spring, e fornece resolução de parâmetros JUnit 5 para ambos. Compõe com `@SpringBootTest`, `@DataJpaTest`, `@WebMvcTest` e qualquer outra anotação de fatia de teste Spring.
O starter de runtime expõe `/actuator/chaos` como um endpoint Actuator protegido para gerenciamento de cenários em tempo real em ambientes não produtivos.
**Quarkus:** A extensão Quarkus fornece `@QuarkusChaosTest` como uma anotação de teste Quarkus composta com injeção CDI de `ChaosControlPlane`. Integra-se com `@QuarkusTest`, `@QuarkusIntegrationTest` e testes de imagem nativa.
**Micronaut:** A integração com Micronaut fornece `@MicronautChaosTest` com beans `ChaosControlPlane` e `ChaosSession` compatíveis com `@Inject`. Funciona com `@MicronautTest` nos modos de teste JVM e nativo.
- Spring Boot 3/4: anotação composta @ChaosTest, ChaosControlPlane como bean Spring, endpoint /actuator/chaos
- Quarkus: @QuarkusChaosTest, injeção CDI, funciona com @QuarkusIntegrationTest e nativo
- Micronaut: @MicronautChaosTest, beans compatíveis com @Inject, suporte a testes JVM e nativo
- JUnit 5 puro: @ExtendWith(ChaosTestingExtension.class) com ChaosControlPlane manual — nenhum framework necessário
- Compatível com Gradle e Maven: todos os módulos publicados no Maven Central com coordenadas padrão
Recursos e Restrições: Anotação @Resources
Além da injeção de falhas, o framework suporta testes de restrição de recursos via `@Resources`. Isso declara limites de CPU e memória que a extensão JUnit aplica ao container Docker antes de iniciá-lo — sem modificar a definição do container no código de teste.
Isso viabiliza uma classe de testes frequentemente ignorada: "meu serviço se comporta corretamente quando está com recursos limitados?" Este é um cenário realista no Kubernetes, onde pods executam com limites de CPU e memória, e onde atingir esses limites aciona throttling e encerramento por 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
}
} A Arquitetura de Plugins: Uma Extensão, Todos os Containers
As versões iniciais do framework tinham extensões JUnit separadas para cada tipo de container: uma para Redis, uma para PostgreSQL, uma para Kafka, e assim por diante. Cada extensão tinha mais de 200 linhas de código de ciclo de vida quase idêntico. Estender para um novo tipo de container significava copiar e modificar.
A arquitetura atual usa um SPI `ChaosPlugin` descoberto via `ServiceLoader.load(ChaosPlugin.class)`. Cada tipo de container registra uma implementação. A única `ChaosTestingExtension` orquestra todas elas.
O contrato SPI é mínimo: descobrir containers na classe de teste, fornecer informações de conexão, lidar com a aplicação de recursos e lidar com a configuração de caos pré-inicialização. A extensão universal lida com todo o resto: processamento de anotações, gerenciamento de ciclo de vida, isolamento de sessão, resolução de parâmetros.
Isso elimina aproximadamente 8.000 linhas de duplicação e torna o framework extensível a novos tipos de container sem modificar o núcleo.
// 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 Módulos, 11 Domínios, Um Modelo Mental
O framework está organizado em 43 módulos Gradle cobrindo 11 domínios de falha. Aqui está o panorama completo:
**Infraestrutura central:** chaos-core (extensão JUnit 5, SPI, ciclo de vida), chaos-spi (contrato de plugin), chaos-api (tipos de seletor e efeito)
**Pacotes de teste por domínio de falha:** testpacks-connection (TCP/UDP), testpacks-dns (resolução DNS), testpacks-memory (malloc/mmap), testpacks-process (fork/exec/sinais), testpacks-time (clock, desvio de tempo), testpacks-filesystem (I/O de arquivo)
**Integração JVM:** chaos-java (transporte de bytecode JVM, fiação no lado do container)
**Pacotes de incidentes L3:** testpacks-l3-kubernetes (atualizações contínuas, tempestades DNS), testpacks-l3-feign (amplificação de retry), testpacks-l3-spring (deadlock transacional, starvation OSIV), testpacks-l3-redis (tempestades de failover), testpacks-l3-kafka (failover de broker), testpacks-l3-grpc (tempestades GOAWAY)
**Integrações com frameworks:** java-spring-boot3, java-spring-boot3-test, java-spring-boot4, java-spring-boot4-test, java-quarkus, java-micronaut
- 448 anotações L1: primitivos syscall brutos em 6 domínios de falha no nível do sistema operacional
- 92 compostos L2: padrões de falha nomeados com padrões calibrados derivados de produção
- 64 incidentes L3: post-mortems reais como anotações únicas com classificações de Severidade
- 43 módulos Gradle: versionados independentemente, sem dependências transitivas obrigatórias
- 4 variantes de binário: glibc-amd64, glibc-arm64, musl-amd64, musl-arm64 — empacotadas no JAR
- Spring Boot 3/4, Quarkus, Micronaut: integração completa com frameworks pronta para uso
Key Takeaways
O framework existe porque post-mortems de incidentes têm itens de ação que nunca são implementados. Não porque os engenheiros são preguiçosos, mas porque a lacuna de ferramental entre "deveríamos testar este modo de falha" e "aqui está um teste que testa este modo de falha" era grande demais.
448 anotações L1 para controle cirúrgico. 92 compostos L2 para famílias de falhas documentadas. 64 cenários de incidentes L3 que permitem transformar um post-mortem do Slack em um teste de CI com falha em cinco minutos.
A camada de caos não substitui testes unitários ou testes de integração. Ela completa o quadro. A Fase 4 é a fase em que seu pipeline de CI aprende a reproduzir incidentes de produção antes que eles aconteçam novamente.
Engineering Team
Senior Solutions Architects
Construímos sistemas distribuídos desde antes de 'microsserviços' ser um conceito. Nossas cicatrizes contam histórias.