Хаос — это не баг, а фича: знакомство с 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