Перейти к содержимому

Property-based, mutation, load и chaos testing в Go

Зачем знать. Юнит-тесты ловят только то, что разработчик подумал проверить. Middle 2 Go-инженер должен уметь дополнять их property-based (генеративные инварианты), оценивать качество тестов через mutation testing, нагружать сервис через load testing и проверять устойчивость к сбоям через chaos engineering. Это уровни тестов, без которых production-системы ломаются на ровном месте.

  1. Концепции четырёх дисциплин
  2. Production-практики и инструменты
  3. Gotchas
  4. Real cases
  5. 25 вопросов
  6. Practice
  7. Источники

Идея: вместо «для входа X ожидаем Y» (example-based) — описать свойство, которое должно держаться для любого input, а фреймворк генерирует входы автоматически.

Классические свойства:

Reverse: reverse(reverse(xs)) == xs для любого xs
Sort: is_sorted(sort(xs)) для любого xs
JSON: decode(encode(x)) == x для любого x (round-trip)
Append: len(append(xs, y)) == len(xs) + 1 для любого xs, y
Math: |a + b| <= |a| + |b| triangle inequality
Idempotent: hash(x) == hash(x) стабильно

Generator строит случайные input заданного типа; shrinker при провале сжимает до минимального contre-example (легче дебажить).

Идея: исказить (mutate) кусок кода — изменить > на >=, + на -, удалить вызов, инвертировать boolean. Запустить тесты. Если тесты всё ещё «pass» — значит они не покрывают эту мутацию, есть пробел.

Метрика: mutation score = killed / total_mutants. Чем выше, тем сильнее ваш suite.

Идея: нагрузить систему до её предельных режимов, измерить RPS, latency (p50/p95/p99), error rate. Различают:

  • Closed-loop: фиксированное число клиентов (как пользователи в браузере).
  • Open-loop: фиксированный rate новых запросов (как событийный поток).
  • Stress vs Spike vs Soak (длительный) — разные сценарии.

Идея (Netflix, 2010): «нельзя верить тому, что не сломано». Преднамеренно вносим failure (kill процесса, latency сети, OOM, partition) и проверяем, что система продолжает работать (стационарное состояние не нарушено).

Принципы (Chaos Engineering Principles):

  1. Сформулировать steady state hypothesis.
  2. Варьировать события реального мира.
  3. Запускать эксперименты в production (когда команда зрелая).
  4. Автоматизировать.
  5. Минимизировать blast radius.

БиблиотекаСтатусКогда
testing/quick (stdlib)очень минимальный, без shrinkingдля простых case
github.com/leanovate/gopterклассика, активная, matureосновной выбор до 2022
pgregory.net/rapidсовременный, отличный shrinker, recommendрекомендация 2025+
import "pgregory.net/rapid"
func TestSortIdempotent(t *testing.T) {
rapid.Check(t, func(t *rapid.T) {
xs := rapid.SliceOf(rapid.Int()).Draw(t, "xs")
ys := append([]int(nil), xs...)
sort.Ints(ys)
sort.Ints(ys) // повторно
zs := append([]int(nil), xs...)
sort.Ints(zs)
require.Equal(t, zs, ys, "sort должен быть идемпотентен")
})
}
func TestRoundtripJSON(t *testing.T) {
rapid.Check(t, func(t *rapid.T) {
order := genOrder(t) // custom generator
b, err := json.Marshal(order)
require.NoError(t, err)
var back Order
require.NoError(t, json.Unmarshal(b, &back))
require.Equal(t, order, back)
})
}
func genOrder(t *rapid.T) Order {
return Order{
ID: rapid.StringMatching(`[0-9a-f]{8}`).Draw(t, "id"),
Total: rapid.IntRange(0, 1_000_000).Draw(t, "total"),
Items: rapid.SliceOfN(rapid.String(), 1, 10).Draw(t, "items"),
}
}
import (
"github.com/leanovate/gopter"
"github.com/leanovate/gopter/gen"
"github.com/leanovate/gopter/prop"
)
func TestReverse(t *testing.T) {
p := gopter.NewProperties(nil)
p.Property("reverse(reverse(xs)) == xs", prop.ForAll(
func(xs []int) bool {
return reflect.DeepEqual(reverse(reverse(xs)), xs)
},
gen.SliceOf(gen.Int()),
))
p.TestingRun(t)
}
  • Round-trip: encode→decode (JSON, Avro, Protobuf), gzip→gunzip.
  • Инвариант: после операции свойство держится (BST остался отсортированным).
  • Эквивалентность реализаций: новая (быстрая) ≡ старой (медленной).
  • Идемпотентность: f(f(x)) == f(x) (нормализация).
  • Метаморфические соотношения: f(2x) ≈ 2·f(x) (для аналитических функций).

При провале фреймворк автоматически уменьшает input. Пример:

  • найдено: xs=[42, -17, 88, 1, 1000, -5] ломает тест.
  • shrinker: попробует [], [42], [1000], [42, 88]
  • финал: xs=[1000] — минимальный contre-example.

⚠️ Сложные generator’ы требуют custom shrinker для понятных contre-example.

Go добавил встроенный fuzzing go test -fuzz:

func FuzzParse(f *testing.F) {
f.Add([]byte(`{"a": 1}`))
f.Fuzz(func(t *testing.T, data []byte) {
v, err := Parse(data)
if err != nil { return }
// round-trip
b, _ := Marshal(v)
v2, err := Parse(b)
if err != nil { t.Fatal("re-parse failed") }
if !reflect.DeepEqual(v, v2) {
t.Fatal("round-trip mismatch")
}
})
}
Окно терминала
go test -fuzz=FuzzParse -fuzztime=30s

Различия:

  • Fuzzing — coverage-guided, ищет crashes; работает с []byte/примитивами.
  • Property-based (rapid) — описание свойств для типизированных значений; не coverage-guided, но богаче.
  • Часто комбинируют: fuzz → ловит панику; property → проверяет инвариант.

github.com/go-gremlins/gremlins — основной инструмент для Go (2025+).

Установка и запуск:

Окно терминала
go install github.com/go-gremlins/gremlins/cmd/gremlins@latest
gremlins run ./...

Pipeline:

  1. gremlins парсит AST, идентифицирует операторы.
  2. Для каждого создает «мутанта» (например >>=).
  3. Компилирует, запускает тесты.
  4. Если тест fall — мутант killed, иначе survived (тестов не хватает).

Типы мутаций:

  • Arithmetic: +, -, *, / swap.
  • Conditional: ><, ==!=.
  • Increment/Decrement: ++--.
  • Constants: truefalse, 01.
  • Return values: return xreturn zero/nil.

Пример отчёта:

Mutator Killed Survived Score
ARITHMETIC_BASE 24 3 88%
CONDITIONALS_BOUND 12 1 92%
INCREMENT_DECREMENT 6 0 100%
─────────────────────────────────────────────
Total 42 4 91%

Survived mutants — место для усиления тестов: написать пример, где разница между исходным кодом и мутантом видна.

⚠️ Mutation testing медленный: для каждого мутанта прогон тестов. Гремлины умеют параллелить + использовать только тесты для конкретного пакета.

ИнструментЯзыкСильные стороныКогда
vegetaGoconstant rate (open-loop), CLI, простойquick http бенчмарк
k6JS-сценарий + Go-enginescripted, mature reporting, Grafanaсложные сценарии
wrk2Cconstant throughput (правильно), latencyстрогий p99
ghzGogRPC специфичнодля gRPC
ApacheBench (ab)Cочень простойодин endpoint, dirty test
LocustPythonscripted, distributedесли уже Python в экосистеме
GatlingScalaenterprise, отчётыJVM-команды
Окно терминала
echo "GET http://localhost:8080/api/orders" | \
vegeta attack -rate=100/s -duration=30s | \
tee results.bin | vegeta report
# гистограмма
vegeta report -type=hist[0,10ms,50ms,100ms,500ms,1s] < results.bin
# HTML отчёт
vegeta report -type=plot < results.bin > plot.html

Vegeta — open-loop (constant rate), что правильно моделирует трафик пользователей; не зависит от latency сервиса (в отличие от ab, который выдаёт rate ~= 1/latency).

import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
scenarios: {
ramp_up: {
executor: 'ramping-vus',
startVUs: 0,
stages: [
{ duration: '1m', target: 100 },
{ duration: '5m', target: 100 },
{ duration: '1m', target: 0 },
],
},
},
thresholds: {
http_req_duration: ['p(95)<200', 'p(99)<500'],
http_req_failed: ['rate<0.01'],
},
};
export default function () {
const r = http.get('http://localhost:8080/api/orders');
check(r, { 'status 200': (r) => r.status === 200 });
sleep(0.1);
}
Окно терминала
k6 run script.js
k6 run --out influxdb=http://localhost:8086/k6 script.js
Окно терминала
ghz --insecure \
--proto=order.proto --call=order.Service.Create \
-d '{"customer_id": 1}' \
-c 50 -n 10000 \
localhost:50051

Open-loop (vegeta, wrk2):

  • Каждую секунду создаёт N новых запросов независимо от ответа предыдущих.
  • Корректно моделирует входящий event-flow.
  • Если сервис тормозит — queue запросов растёт, latency × 100.

Closed-loop (ab, jmeter по умолчанию):

  • N клиентов в цикле: «отправил — дождался ответа — отправил снова».
  • Скорость = N / latency. Если сервис тормозит — клиенты тоже тормозят.
  • Маскирует деградацию: «вижу 1000 RPS» при p99=10s.

⚠️ Closed-loop искажает результаты. Для realistic load — open-loop. Это знаменитая «coordinated omission» проблема (Gil Tene).

  • Warm-up: 30-60 секунд лёгкой нагрузки, чтобы JIT/cache/connection pool прогрелся. Пер-период измерения отдельно от warm-up.
  • Ramp: постепенный набор RPS (0→target за минуту).
  • Soak: 1-24 часа на target нагрузке (ловим memory leak, GC degradation).
  • Spike: внезапный 5x burst (моделирует продуктовые акции).
ИнструментПлатформаЧто делает
Chaos MeshKubernetes (CNCF)pod kill, network, IO, time chaos
LitmusChaosKubernetes (CNCF)workflows, gameday-сценарии
Chaos Toolkituniversalopen-source движок, плагины
Gremlin (commercial)universalmanaged, GUI
toxiproxyTCP-уровеньlatency / disconnect между app и зависимостями
PumbaDockernetwork/process chaos для контейнеров
chaos-monkeyAWS (Spinnaker)random instance kill
КатегорияПримеры
Processkill -9, OOM, panic
Networklatency, packet loss, partition, DNS fail
ResourceCPU stress, memory exhaust, disk full, fork-bomb
Timeclock skew, NTP-jump
DependenciesDB unavailable, slow query, broker disconnect
Statecorrupted data, partial write
apiVersion: chaos-mesh.org/v1alpha1
kind: NetworkChaos
metadata:
name: orders-latency
spec:
action: delay
mode: one
selector:
labelSelectors:
"app": "orders"
delay:
latency: "200ms"
correlation: "100"
jitter: "50ms"
duration: "5m"

Применит 200ms задержку к 1 поду orders на 5 минут.

import "github.com/Shopify/toxiproxy/v2/client"
toxiClient := toxiproxy.NewClient("localhost:8474")
proxy, _ := toxiClient.CreateProxy("pg", "0.0.0.0:25432", "real-pg:5432")
proxy.AddToxic("slow", "latency", "downstream", 1.0, toxiproxy.Attributes{
"latency": 500, "jitter": 100,
})
// тесты теперь ходят на localhost:25432 → toxiproxy → real-pg c 500ms latency

Очень полезно в integration-тестах для проверки timeouts/retries.

Game day — учения: команда в офис-час собирается, запускает chaos-эксперимент в проде (или почти проде), мониторит, фиксит. Цели:

  • проверить runbooks (а можем ли мы реально восстановить);
  • найти неочевидные dependencies;
  • обучить новичков.

Минимальный сценарий: kill-9 одного pod в проде → SLO держится? оповещения сработали? сколько 5xx видел юзер?

Формулировка до эксперимента:

«Latency p99 < 500ms, error rate < 0.1%, success rate >= 99.9% для /api/orders в течение эксперимента.»

Если во время chaos гипотеза держится — система устойчива. Если нарушена — есть слабое место.

Начинай с минимума:

  1. One pod в staging.
  2. → One pod в prod (canary).
  3. → Multiple pods staging.
  4. → Multiple pods prod.
  5. → AZ failure (если осилит).

Никогда не лезь сразу «убить весь регион».


⚠️ Property tests без shrinker дают огромные contre-example. Падает с xs=[1942, -17, 88, 1, 0, ...500 элементов] — непонятно где баг. Используй rapid или пиши custom shrinker.

⚠️ Custom generator может не покрывать edge cases. Если genInt = IntRange(0, 100) — никогда не тестируется отрицательное число. Думай про границы (MaxInt, 0, nil, "").

⚠️ Property test, который иногда падает — flake. Сохраняй seed для воспроизведения (rapid печатает seed).

⚠️ Mutation testing exponentially slow. 1000 файлов × 50 мутантов × 2 секунды на test suite = 27 часов. Используй --threshold и фильтры по changed-файлам.

⚠️ Survived mutants — не всегда баг тестов. Могут быть equivalent mutants — изменения кода, которые семантически эквивалентны (например i < 100i <= 99). Их нельзя «убить».

⚠️ Load test против localhost ничего не показывает. TCP-стек, network latency, OS-tuning — всё локально нерелевантно. Делай против stage с реальным сетевым путём.

⚠️ Closed-loop benchmark маскирует degradation. «Apache Benchmark показывает 1000 RPS» при p99=10s — это плохо, а не хорошо.

⚠️ Warm-up забывают, и p99 «грязный». Первые 30 секунд — connection pool + JIT + cache — отдельный buckets.

⚠️ Coordinated omission: измеритель ждёт ответа и не отправляет новые запросы → пропускает latency. wrk2 и vegeta его учитывают; ab — нет.

⚠️ Chaos в проде без мониторинга = безответственность. Сначала observability (метрики, алерты, runbook), потом chaos.

⚠️ «Chaos Monkey» в стартапе — обычно вред, а не польза. Сначала надёжность базовой системы. Chaos — после нескольких 9.

⚠️ toxiproxy в production не нужен. Это test-tool. Для prod-chaos используй Chaos Mesh / LitmusChaos.

⚠️ Mocked dependency в property test — теряется ценность. Property-тесты лучше всего работают над «чистой логикой». Для интегрированных property — реальная зависимость (testcontainers).

⚠️ Fuzz-инпуты не сохраняются автоматически в git — Go хранит их в testdata/fuzz/.... Коммить — иначе разработчик не воспроизведёт.

⚠️ k6 в Go-проектах — JavaScript-сценарии. Это не «meta» в Go-стеке, но дешевле, чем писать своё.

⚠️ Игнорирование observability во время load test. Графики throughput без CPU/GC/p99 не дают ответа «почему деградирует».


Команда написала custom JSON-парсер. Покрытие unit — 95%. Property-test (encode → decode → encode' равенство) + fuzz нашёл 12 багов: NaN не сериализуется обратно, escape Unicode surrogate, отрицательный zero, etc. Mutation score вырос с 78% до 94%.

Прогон gremlins на сервисе показал что 30% мутантов выживают в модуле pricing. Расследование: «тест» был t.Skip("flaky") несколько месяцев. Исправили, mutation score вырос до 88%.

vegeta 200 RPS, soak 1 час → к 30-й минуте p99 ушёл с 50ms на 30s. Pprof показал deadlock в connection pool. Ни один unit/integration не воспроизводил, потому что не было длительной нагрузки.

Эксперимент: кросс-AZ partition между app и Postgres replica. Гипотеза: failover на другой replica в 30s. Реально: 4 минуты (DNS-кэш). Зафиксили — добавили health-check + DNS TTL=5.

Pioneer chaos engineering: Chaos Monkey (kill instances), Latency Monkey (delay), Chaos Gorilla (AZ), Chaos Kong (region). Сейчас интегрировано в Spinnaker.


1. Что такое property-based testing? Тестирование, где описывают свойство (инвариант), которое должно держаться для любого входа; framework генерирует случайные входы автоматически.

2. Чем property test отличается от example-based? Example: «для X получи Y». Property: «для любого x: P(x) истина». Больше покрытия, ловит edge cases.

3. Какие свойства типично проверяют? Round-trip (encode→decode), идемпотентность, эквивалентность реализаций, инварианты структур (BST остался отсортированным).

4. Что такое shrinker? При провале теста — алгоритм уменьшает input до минимального contre-example, удобного для дебага.

5. Какие библиотеки property-testing в Go? testing/quick (stdlib, minimal), gopter (классика), pgregory.net/rapid (recommended 2025+).

6. Чем rapid лучше gopter? Современный API, лучший shrinker, активная разработка, лучшие custom generators.

7. Чем property test отличается от fuzzing? Fuzz — coverage-guided ищет crash на []byte. Property — описывает инвариант на типизированных данных. Можно комбинировать.

8. Что такое mutation testing? Метод оценки качества тестов: исказить код (mutate), проверить что тесты падают; survived mutant = тест не покрывает.

9. Какой инструмент в Go? go-gremlins/gremlins — основной.

10. Что такое mutation score? Доля убитых мутантов: killed / total. Высокий score = тесты сильные.

11. Что такое equivalent mutant? Изменение кода, семантически эквивалентное оригиналу; не может быть «убит» (например i<100i<=99 при integer).

12. Закладные ограничения mutation testing? Медленный (N × прогон тестов); equivalent mutants создают «ложные» survived; шумит на сложной логике.

13. Что такое load testing? Нагрузка на сервис до предельных режимов; измерение throughput, latency, error rate.

14. Closed-loop vs Open-loop? Closed: N клиентов в цикле, RPS зависит от latency. Open: фиксированный rate, не зависит от ответа. Open — корректнее.

15. Что такое coordinated omission? Артефакт benchmark-инструментов, где замедление сервиса маскируется (измеритель ждёт и не отправляет новые запросы). wrk2 и vegeta это учитывают.

16. Какие инструменты load testing для Go? vegeta (CLI, Go), k6 (JS-сценарий, Go-engine), ghz (gRPC), wrk2 (C, constant throughput).

17. Что такое warm-up? Период до измерения (30-60s): прогрев JIT, connection pool, cache. Не входит в результаты.

18. Что такое soak test? Длительная (1-24h) нагрузка target-уровня; ловит memory leak, GC degradation, file descriptor leak.

19. Что такое chaos engineering? Дисциплина: целенаправленно ломать систему в production-like условиях, чтобы найти слабые места до реальных инцидентов.

20. Принципы chaos engineering?

  1. Steady state hypothesis. 2) Vary real-world events. 3) Run in production (когда зрело). 4) Automate. 5) Minimize blast radius.

21. Какие классы failure? Process (kill, OOM), Network (latency, partition), Resource (CPU/mem/disk), Time (clock skew), Dependencies (DB down).

22. Инструменты chaos engineering? Chaos Mesh (K8s, CNCF), LitmusChaos (K8s, CNCF), Chaos Toolkit, Gremlin (commercial), toxiproxy (TCP).

23. Что такое game day? Учения команды: запускают chaos-эксперимент, мониторят, фиксят runbooks. Цель — обучение и валидация процедур.

24. Что такое blast radius? Объём ущерба от эксперимента. Минимизируется (1 pod → AZ → region) поэтапно по мере уверенности.

25. Зачем toxiproxy в integration-тестах? Симулирует latency/disconnect между app и зависимостью на TCP-уровне; полезно проверять timeouts, retries, circuit breakers.


  1. Round-trip property. Напиши custom JSON-encoder/decoder для нескольких типов; rapid-тест: decode(encode(x)) == x для случайных values.

  2. Sort idempotence. rapid-тест на собственную sort-функцию: sort(sort(xs)) == sort(xs) + is_sorted(sort(xs)).

  3. Property + fuzz combo. Напиши FuzzParse для парсера + TestPropertyParse для инварианта. Сравни найденные баги.

  4. Custom generator + shrinker. Сгенерируй валидный JWT-token (header+payload+signature) для тестов — научи shrinker сжимать payload.

  5. gremlins на pet-проект. Прогоняй mutation testing на свой Go-проект; идентифицируй survived mutants; усиль тесты.

  6. vegeta benchmark. Снагрузи свой HTTP-сервис: 100/s, 500/s, 1000/s на 30s, построй гистограмму latency.

  7. k6 ramp test. Скрипт с ramp 0→500 VU за 2 минуты, thresholds p99<300ms; запусти, посмотри отчёт.

  8. wrk2 vs ab. Сравни latency p99 для одного endpoint в wrk2 (open-loop) и ab (closed-loop) — должны различаться сильно при загруженном сервисе.

  9. toxiproxy в тестах. В integration-suite подними toxiproxy перед Postgres, добавь 500ms latency, проверь что app срабатывает по timeout.

  10. Chaos Mesh game day. Подними K8s локально (kind/minikube), задеплой app, примени PodChaos (kill-pod), проверь что service остаётся доступным.

  11. Steady state hypothesis. Сформулируй гипотезу для своего сервиса; реализуй проверку через метрики Prometheus.

  12. Soak test. Запусти vegeta 100/s в течение 6 часов; собери pprof в начале и в конце; сравни heap (memory leak?).


package csv
// Parse + Marshal — round trip property
func TestParser_RoundTrip(t *testing.T) {
rapid.Check(t, func(t *rapid.T) {
rows := rapid.SliceOf(
rapid.SliceOfN(
rapid.StringMatching(`[^\n"]{0,40}`),
1, 6,
),
).Draw(t, "rows")
var buf bytes.Buffer
require.NoError(t, Marshal(&buf, rows))
parsed, err := Parse(&buf)
require.NoError(t, err)
require.Equal(t, rows, parsed)
})
}
// Fuzz: ловим панику и не-recovery ошибки
func FuzzParser_NoCrash(f *testing.F) {
f.Add([]byte("a,b,c\n1,2,3\n"))
f.Add([]byte(`"quoted, value","escaped""quote"`))
f.Fuzz(func(t *testing.T, data []byte) {
defer func() {
if r := recover(); r != nil {
t.Fatalf("parser panicked: %v", r)
}
}()
_, _ = Parse(bytes.NewReader(data))
})
}

В норме fuzz запускают в CI на 1-10 минут; найденные corpus сохраняются в testdata/fuzz/FuzzParser_NoCrash/<hash>.

Когда нет «эталонной» реализации, ищи отношения между разными inputs:

// log(a*b) == log(a) + log(b)
rapid.Check(t, func(t *rapid.T) {
a := rapid.Float64Range(1, 1e6).Draw(t, "a")
b := rapid.Float64Range(1, 1e6).Draw(t, "b")
lhs := math.Log(a * b)
rhs := math.Log(a) + math.Log(b)
require.InDelta(t, lhs, rhs, 1e-9)
})
// сортировка инвариант: после удаления одного элемента — всё ещё отсортировано
rapid.Check(t, func(t *rapid.T) {
xs := rapid.SliceOfN(rapid.Int(), 1, 100).Draw(t, "xs")
sort.Ints(xs)
i := rapid.IntRange(0, len(xs)-1).Draw(t, "i")
ys := append([]int(nil), xs[:i]...)
ys = append(ys, xs[i+1:]...)
require.True(t, sort.IntsAreSorted(ys))
})

.gremlins.yaml:

silent: false
threshold:
efficacy: 80
mutants-coverage: 70
mutants:
arithmetic-base: { enabled: true }
conditionals-boundary: { enabled: true }
conditionals-negation: { enabled: true }
increment-decrement: { enabled: true }
invert-negatives: { enabled: true }
test-cpu: 4
include:
- ./internal/...
exclude:
- "_test.go"
- "**/gen/**"
Окно терминала
gremlins run --tags=integration
  • Survived в trivial-getter: return x мутируется на return zero → выживает, потому что getter обычно не тестируют отдельно. Это не «плохой тест», это естественно.
  • Equivalent mutants — например i < len(arr)i <= len(arr)-1 в loop — эквивалентно. Помечай вручную как «excluded».
Окно терминала
echo "GET http://app/orders" | vegeta attack -rate=100/s -duration=2m | tee out.bin
vegeta report < out.bin
vegeta report -type=json < out.bin > metrics.json

Импортировать в Prometheus можно через vegeta-prometheus exporter или вручную распарсить JSON.

k6 cloud (managed) или self-hosted k6-operator (Kubernetes):

apiVersion: k6.io/v1alpha1
kind: TestRun
metadata:
name: orders-load
spec:
parallelism: 4
script:
configMap:
name: k6-test-script
arguments: --out json=/results/result.json

4 пода × N VU = распределённая нагрузка.

Среднее latency = 50ms — звучит нормально.
Гистограмма:
0-10ms: ████████████████████ 80%
10-100ms: ███ 15%
1s-5s: █ 5% ← хвост!
p50 = 5ms, p99 = 4s.

Среднее всегда обманчиво — используй гистограмму + p95/p99/p99.9.

Kill pod:

apiVersion: chaos-mesh.org/v1alpha1
kind: PodChaos
spec:
action: pod-kill
mode: one
selector: { labelSelectors: {"app": "orders"} }
duration: "30s"

Slow disk:

apiVersion: chaos-mesh.org/v1alpha1
kind: IOChaos
spec:
action: latency
mode: one
selector: { labelSelectors: {"app": "orders"} }
volumePath: /data
path: /data/**/*
delay: 100ms
percent: 50
duration: "5m"

Network partition:

apiVersion: chaos-mesh.org/v1alpha1
kind: NetworkChaos
spec:
action: partition
direction: both
selector: { labelSelectors: {"app": "db"} }
target:
selector: { labelSelectors: {"app": "orders"} }
duration: "1m"
УровеньСостояние
1Никогда не пробовали
2Эксперименты в staging вручную
3Автоматизированные эксперименты в staging
4Game days раз в квартал в production
5Continuous chaos в production (Netflix)

Не торопиться — сначала observability, потом resilience patterns, потом chaos.

Минимальный шаблон:

  1. Гипотеза: «При kill 1 pod из 3 — клиенты видят < 10 ошибок 5xx за 2 минуты».
  2. Метод: chaosctl pod-kill --label app=orders --count 1.
  3. Мониторинг: Grafana дашборды, alert-канал.
  4. Abort criteria: error rate > 5%, p99 > 5s.
  5. Recovery: автоматическое восстановление K8s.
  6. Postmortem: что увидели, что фиксили.
  • Парсеры (JSON, CSV, protobuf, datetime).
  • Сериализаторы (Marshal/Unmarshal round-trip).
  • Шифрование (encrypt→decrypt).
  • Compression (compress→decompress).
  • Бизнес-инварианты (баланс счёта не отрицательный, сумма строк = total).
  • Конкурентные структуры (lock-free queue корректен под N горутинами).
  • Маршрутизация (для любого URL — попадает в нужный handler).
  • Property-тесты для парсеров, сериализаторов, бизнес-инвариантов.
  • Fuzz target для каждого парсера / входной точки []byte.
  • Mutation testing раз в неделю в CI, baseline >= 80%.
  • Load test: vegeta/k6 для критичных endpoint, p99 SLO зафиксирован.
  • Soak test раз в спринт (memory leak hunt).
  • toxiproxy в integration suite для критичных deps.
  • Game day каждый квартал (один pod kill + один dep down).
  • Steady state hypothesis для каждого эксперимента.
  • Blast radius: phased rollout chaos.
  • Observability в любом эксперименте.
PR → CI
├─ unit tests (go test)
├─ integration tests (testcontainers, тэг)
├─ contract tests (Pact)
├─ property tests (rapid)
├─ fuzz 60s (go test -fuzz)
├─ mutation testing (раз в день, не на каждый PR)
├─ load smoke (k6, 30s ramp)
merge → staging
├─ deploy
├─ chaos light (kill 1 pod)
└─ verify SLO
release → production
├─ canary
├─ progressive rollout
└─ chaos game day quarterly

  1. pgregory.net/rapid — property-based testing.
  2. gopter — альтернатива.
  3. Go fuzzing tutorial — официально.
  4. go-gremlins/gremlins — mutation testing для Go.
  5. «PIT mutation testing — overview» — концептуально (Java-tool, но идеи универсальны).
  6. vegeta — CLI load tester.
  7. k6 documentation — modern load testing.
  8. wrk2 (constant throughput, no coordinated omission) — Gil Tene.
  9. «Coordinated Omission» talk by Gil Tene — must-watch для load.
  10. Chaos Engineering Principles — каноническое определение.
  11. Chaos Mesh docs — CNCF chaos для K8s.
  12. «Chaos Engineering» — O’Reilly book (Rosenthal, Jones) — глубокая книга.
  13. Netflix Tech Blog: Simian Army — историческая база.
  14. Shopify/toxiproxy — TCP-chaos для тестов.