Chaos Engineering для Go: ломаем приложения, чтобы спасти

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Предисловие: Почему ваш Go-сервис — это не тихая гавань

Вы написали идеальный код на Go. Он быстрый, конкурентный, использует горутины и каналы. Вы провели unit-тесты, интеграционные тесты, проверили покрытие. Но когда вы выкатили релиз в пятницу вечером, всё рухнуло. Почему? Потому что в продакшене живёт Хаос. Сеть лагает, диски заполняются, базы данных падают, а зависимые микросервисы уходят в бесконечный тайм-аут.

Добро пожаловать в реальный мир. Здесь на помощь приходит Chaos Engineering (инженерия хаоса) — дисциплина, которая учит нас намеренно ломать систему, чтобы понять её слабые места. В этой статье мы разберем, как применять Chaos Engineering для Go-приложений. Вы узнаете, как писать простые, но эффективные эксперименты, не превращая свой продакшен в руины. Готовы? Поехали!

Что такое Chaos Engineering и зачем он Go-разработчику?

Chaos Engineering — это не просто "поломать и убежать". Это научный подход к повышению отказоустойчивости. Основная идея: вместо того чтобы ждать сбоя, вы провоцируете его в контролируемой среде. Вы формулируете гипотезу ("если упадет база данных, сервис вернет ошибку, но не зависнет"), проводите эксперимент и смотрите на результат.

Для Go этот подход особенно актуален. Go изначально проектировался для создания надежных, масштабируемых систем. Но даже самый крутой код не спасет от проблем с сетью или внезапной смерти горутины. Chaos Engineering помогает проверить, как ваши middleware, retry-логика и circuit breakers (автоматические выключатели) работают в боевых условиях.

Инструментарий для Chaos Engineering в Go

В мире Go есть несколько мощных библиотек и подходов для внедрения хаоса. Мы рассмотрим самые популярные:

  • gofault — простая библиотека для инъекции задержек и ошибок.
  • chaostoolkit — фреймворк для оркестрации экспериментов (есть Go-драйверы).
  • Собственные middleware — самый гибкий и контролируемый способ.

В этой статье мы пойдем по пути создания собственного middleware. Это даст вам полное понимание процесса и возможность адаптировать его под свои нужды.

Практика: Создаем Chaos Middleware для HTTP-сервера на Go

Представьте, что у вас есть простой HTTP-сервер на Go. Мы добавим middleware, которое будет симулировать различные сбои: задержки, ошибки, панику.

Шаг 1: Базовая структура

Начнем с определения структуры конфигурации хаоса:

package chaos

import (
    "math/rand"
    "net/http"
    "time"
)

// Config содержит настройки для инъекции хаоса.
type Config struct {
    // Probability - вероятность (0.0 - 1.0) возникновения сбоя.
    Probability float64
    // DelayRange - диапазон задержки в миллисекундах [min, max].
    DelayRange [2]int
    // ErrorCode - HTTP-статус код, который будет возвращен при ошибке.
    ErrorCode int
    // Panic - если true, middleware вызовет панику.
    Panic bool
}

// DefaultConfig возвращает конфигурацию по умолчанию.
func DefaultConfig() Config {
    return Config{
        Probability: 0.1, // 10% запросов будут затронуты
        DelayRange:  [2]int{100, 500},
        ErrorCode:   http.StatusInternalServerError,
        Panic:       false,
    }
}

Шаг 2: Пишем сам middleware

Теперь создадим функцию, которая возвращает middleware. Она будет случайным образом решать, что делать с запросом.

// Middleware возвращает HTTP middleware для инъекции хаоса.
func Middleware(cfg Config) func(http.Handler) http.Handler {
    rng := rand.New(rand.NewSource(time.Now().UnixNano()))

    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            // Если случайное число меньше вероятности, применяем хаос
            if rng.Float64() < cfg.Probability {
                // 1. Инъекция задержки
                if cfg.DelayRange[1] > 0 {
                    delay := time.Duration(rng.Intn(cfg.DelayRange[1]-cfg.DelayRange[0])+cfg.DelayRange[0]) * time.Millisecond
                    time.Sleep(delay)
                    // Логируем задержку (в реальном приложении используйте структурированный логгер)
                    // log.Printf("Chaos: injected delay of %v for %s", delay, r.URL.Path)
                }

                // 2. Инъекция паники
                if cfg.Panic {
                    panic("Chaos Engineering: intentional panic!")
                }

                // 3. Инъекция ошибки
                if cfg.ErrorCode > 0 {
                    http.Error(w, http.StatusText(cfg.ErrorCode), cfg.ErrorCode)
                    return // Не передаем запрос дальше
                }
            }

            // Если хаос не применен, передаем запрос следующему обработчику
            next.ServeHTTP(w, r)
        })
    }
}

Шаг 3: Интегрируем в приложение

Теперь используем наш middleware в основном файле приложения:

package main

import (
    "fmt"
    "net/http"
    "your-project/chaos" // путь к вашему пакету
)

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Hello, world!")
    })

    // Создаем конфигурацию хаоса для 20% запросов
    cfg := chaos.Config{
        Probability: 0.2,
        DelayRange:  [2]int{200, 1000},
        ErrorCode:   http.StatusServiceUnavailable,
    }

    // Оборачиваем наш mux в chaos middleware
    handler := chaos.Middleware(cfg)(mux)

    fmt.Println("Server is running on :8080")
    http.ListenAndServe(":8080", handler)
}

Теперь каждый пятый запрос к вашему серверу будет либо задерживаться на 200-1000 мс, либо возвращать ошибку 503. Именно так выглядит тестирование хаосом на практике.

Усложняем задачу: Chaos для gRPC и микросервисов

В реальном мире вы используете не просто HTTP, а gRPC, очереди сообщений, базы данных. Для Go-приложений на основе микросервисов можно писать более сложные сценарии. Например, использовать gofault для инъекции ошибок в вызовы к базе данных или Redis.

Рассмотрим пример с использованием библиотеки gofault для симуляции сбоя при вызове внешнего сервиса:

package main

import (
    "context"
    "fmt"
    "github.com/brafales/gofault"
    "time"
)

// SimulateExternalCall имитирует вызов к внешнему API.
func SimulateExternalCall(ctx context.Context) (string, error) {
    // Создаем fault-инжектор для задержки
    fault := gofault.NewFault().
        WithDelay(time.Second * 2). // Задержка 2 секунды
        WithError(fmt.Errorf("service is down")) // Или ошиб                

Блоги

Рекомендаций по книгам