Chaos Engineering для C++: как закалить свой софт

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

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

Начать курс

Хаос — это не баг, а фича: знакомство с Chaos Engineering в C++

Представьте: ваш C++ сервис работал месяцами без сбоев. А потом — бац! — отвал диска, потеря пакета или внезапный SIGKILL. И всё. Система падает, пользователи в ярости, а вы лихорадочно ищете баг в логах. Знакомая ситуация?

Мы привыкли тестировать софт в идеальных условиях: стабильная сеть, быстрый диск, безлимитная память. Но реальность жестока. Именно здесь на помощь приходит Chaos Engineering — дисциплина, которая учит нас намеренно ломать систему, чтобы сделать её сильнее. И сегодня мы поговорим о том, как применять этот подход к C++ приложениям.

Chaos Engineering для C++ — это не про «запустить chaos monkey и молиться». Это про контролируемые эксперименты, которые выявляют слабые места до того, как их найдут пользователи. Мы научимся внедрять сбои памяти, эмулировать медленный I/O и разрывать сетевые соединения — и всё это с помощью чистого C++ кода.

Почему C++ особенно нуждается в хаос-инжиниринге?

Язык C++ даёт разработчику огромную власть: ручное управление памятью, низкоуровневый доступ к ресурсам, отсутствие сборщика мусора. Но эта сила — палка о двух концах. Ошибка в delete[], утечка в RAII-обёртке или неверный reinterpret_cast могут превратить стабильную систему в хрупкий карточный домик.

Традиционное юнит-тестирование редко проверяет, как поведёт себя код при:

  • Нехватке оперативной памяти (std::bad_alloc)
  • Внезапном закрытии файлового дескриптора
  • Задержках в сети (latency spikes)
  • Коррупции данных в shared memory

Chaos Engineering для C++ закрывает эти пробелы. Мы не просто пишем тесты — мы провоцируем систему на ошибки и смотрим, выживет ли она.

Инструментарий: как вносить хаос в C++ код

Для проведения экспериментов нам понадобятся инструменты Fault Injection (внедрения сбоев). В C++ мы можем действовать на разных уровнях:

  • На уровне кода: перехват системных вызовов через LD_PRELOAD или dlsym
  • На уровне библиотек: подмена стандартных аллокаторов и сокетов
  • На уровне ОС: использование ptrace или eBPF для вмешательства в работу процесса

Мы пойдём по самому практичному пути — напишем собственную библиотеку для внедрения сбоев, которую можно легко интегрировать в любой C++ проект.

Пример 1: Симуляция сбоя выделения памяти

Одна из самых частых проблем в C++ — неожиданный std::bad_alloc. Как поведёт себя ваш код, если new вернёт исключение? Давайте проверим.

// chaos_memory.h
#pragma once
#include <new>
#include <cstdlib>
#include <atomic>
#include <cstddef>

class ChaosAllocator {
public:
    // Устанавливаем вероятность сбоя (0.0 - 1.0)
    static void set_failure_probability(double prob) {
        failure_probability_ = prob;
    }

    static void* allocate(std::size_t size) {
        if (should_fail()) {
            throw std::bad_alloc();
        }
        return std::malloc(size);
    }

    static void deallocate(void* ptr) {
        std::free(ptr);
    }

private:
    static bool should_fail() {
        // Простейший генератор случайных чисел для демонстрации
        static std::atomic<unsigned int> seed = 42;
        seed = seed * 1103515245 + 12345;
        return (seed / 65536) % 100 < failure_probability_ * 100;
    }

    inline static double failure_probability_ = 0.0;
};

Теперь мы можем подменить стандартный аллокатор и проверить, как наш код обрабатывает отказы памяти:

// main.cpp
#include <iostream>
#include <vector>
#include "chaos_memory.h"

// Пользовательский аллокатор, использующий ChaosAllocator
template<typename T>
struct ChaosStdAllocator {
    using value_type = T;

    T* allocate(std::size_t n) {
        auto ptr = ChaosAllocator::allocate(n * sizeof(T));
        if (!ptr) throw std::bad_alloc();
        return static_cast<T*>(ptr);
    }

    void deallocate(T* ptr, std::size_t) {
        ChaosAllocator::deallocate(ptr);
    }
};

int main() {
    ChaosAllocator::set_failure_probability(0.3); // 30% шанс сбоя

    try {
        std::vector<int, ChaosStdAllocator<int>> vec;
        // Пытаемся выделить много блоков
        for (int i = 0; i < 100; ++i) {
            vec.push_back(i);
        }
        std::cout << "Успех! Размер вектора: " << vec.size() << std::endl;
    } catch (const std::bad_alloc& e) {
        std::cerr << "Пойман bad_alloc: " << e.what() << std::endl;
    }

    return 0;
}

Запустите этот код несколько раз — вы увидите, как ваше приложение ведёт себя при частичных сбоях памяти. Если ваш код не обрабатывает bad_alloc везде, где это необходимо — вы получите неожиданный crash.

Пример 2: Эмуляция медленного дискового ввода-вывода

Медленный I/O — убийца производительности. Давайте эмулируем задержки при чтении/записи файлов с помощью перехвата системных вызовов.

// chaos_io.cpp
#define _GNU_SOURCE
#include <dlfcn.h>
#include <unistd.h>
#include <thread>
#include <chrono>
#include <random>

// Указатели на оригинальные функции
using read_fn = ssize_t(*)(int, void*, size_t);
using write_fn = ssize_t(*)(int, const void*, size_t);

static read_fn original_read = nullptr;
static write_fn original_write = nullptr;

// Конфигурация хаоса
namespace {
    std::atomic<int> latency_ms = 0;  // Задержка в миллисекундах
    std::atomic<double> error_prob = 0.0; // Вероятность ошибки EIO
}

extern "C" void chaos_io_set_latency(int ms) {
    latency_ms.store(ms);
}

extern "C" void chaos_io_set_error_probability(double prob) {
    error_prob.store(prob);
}

extern "C" ssize_t read(int fd, void* buf, size_t count) {
    if (!original_read) {
        original_read = reinterpret_cast<read_fn>(dlsym(RTLD_NEXT, "read"));
    }

    // Симуляция задержки
    if (latency_ms.load() > 0) {
        std::this_thread::sleep_for(std::chrono::milliseconds(l                

Блоги

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