Service Mesh для C++ микросервисов: как укротить зоопарк

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

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

Начать курс

Почему C++ микросервисы не дружат с сетью (и как service mesh это исправляет)

Представьте: вы построили идеальный пайплайн на C++. Каждый микросервис летает, память управляется жёстко, а скорость — за гранью добра и зла. Но как только вы запускаете всё это в Kubernetes, начинается ад. Таймауты, ретраи, разрывы соединений, потеря пакетов. Вы лезете в код, вставляете curl в каждый угол, пишете свои велосипеды для ретраев — и проект превращается в болото.

Знакомо? Добро пожаловать в мир распределённых систем. Хорошая новость: проблему решает service mesh. Плохая: большинство туториалов написаны для Go или Java. А что делать нам, C++ разработчикам, которые не хотят тащить тяжёлый Istio с Envoy, если можно сделать легковесное и понятное решение?

В этой статье мы разберём, как внедрить service mesh для C++ микросервисов без боли и лишних зависимостей. Вы узнаете, что такое sidecar-прокси, как настроить балансировку и observability, и получите реальный пример кода на C++ с gRPC.

Что такое service mesh и зачем он C++ проектам?

Service mesh — это выделенный инфраструктурный слой для управления сетевым трафиком между микросервисами. Вместо того чтобы встраивать логику таймаутов, ретраев, балансировки и шифрования в каждый сервис, вы выносите её в прокси-контейнер (sidecar), который запускается рядом с каждым экземпляром вашего сервиса.

Для C++ это особенно актуально, потому что:

  • C++ код обычно пишут для high-load систем, где каждая микросекунда на счету;
  • Встроенная сетевая логика усложняет поддержку и тестирование;
  • Стандартная библиотека C++ не предоставляет готовых механизмов для service mesh (в отличие от Go с его стандартными HTTP-клиентами).

Основные компоненты service mesh:

  • Data plane — прокси-серверы (обычно Envoy, но можно и свои на C++);
  • Control plane — управляющий слой (например, Istio, Consul, или самописный).

Мы сосредоточимся на data plane, потому что именно здесь C++ показывает свою силу.

Пишем свой sidecar-прокси на C++ (минимальный пример)

Допустим, у нас есть два микросервиса: auth_service и user_service. Они общаются по gRPC. Без service mesh каждый сервис должен сам обрабатывать реконнекты, таймауты и метрики. С прокси — нет.

Вот минимальный пример sidecar-прокси на C++ с использованием библиотеки libcurl и nlohmann/json. Он принимает HTTP-запросы, проксирует их на бэкенд, добавляет ретраи и логирует задержки.

// proxy.cpp
#include <httplib.h>
#include <nlohmann/json.hpp>
#include <curl/curl.h>
#include <chrono>
#include <thread>

using json = nlohmann::json;
using namespace std::chrono_literals;

class SidecarProxy {
public:
    SidecarProxy(const std::string& backend_host, int backend_port, int max_retries = 3)
        : backend_host_(backend_host), backend_port_(backend_port), max_retries_(max_retries) {}

    void handle_request(const httplib::Request& req, httplib::Response& res) {
        auto start = std::chrono::high_resolution_clock::now();
        CURL* curl = curl_easy_init();
        std::string url = "http://" + backend_host_ + ":" + std::to_string(backend_port_) + req.path;
        curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
        curl_easy_setopt(curl, CURLOPT_TIMEOUT, 5L);

        std::string response_string;
        curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, +[](void* ptr, size_t size, size_t nmemb, void* stream) {
            ((std::string*)stream)->append((char*)ptr, size * nmemb);
            return size * nmemb;
        });
        curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response_string);

        int retries = 0;
        CURLcode code;
        do {
            code = curl_easy_perform(curl);
            if (code != CURLE_OK && retries < max_retries_) {
                std::this_thread::sleep_for(100ms * (retries + 1));
                retries++;
            }
        } while (code != CURLE_OK && retries < max_retries_);

        auto end = std::chrono::high_resolution_clock::now();
        auto latency = std::chrono::duration_cast<std::chrono::microseconds>(end - start).count();

        // Логируем latency в JSON-формате
        json log_entry = {
            {"path", req.path},
            {"latency_us", latency},
            {"retries", retries},
            {"status", (code == CURLE_OK) ? "OK" : "FAIL"}
        };
        std::cout << log_entry.dump() << std::endl;

        if (code == CURLE_OK) {
            res.set_content(response_string, "application/json");
        } else {
            res.status = 503;
            res.set_content("{\"error\":\"service unavailable\"}", "application/json");
        }
        curl_easy_cleanup(curl);
    }

private:
    std::string backend_host_;
    int backend_port_;
    int max_retries_;
};

int main() {
    SidecarProxy proxy("localhost", 8081);
    httplib::Server svr;
    svr.Get(".*", [&](const httplib::Request& req, httplib::Response& res) {
        proxy.handle_request(req, res);
    });
    svr.listen("0.0.0.0", 8080);
}

Этот прокси делает три вещи:

  • Проксирует запросы на бэкенд;
  • Добавляет ретраи с экспоненциальной задержкой;
  • Собирает метрики latency и логирует их.

Запускать его нужно рядом с каждым микросервисом. В Kubernetes это делается через init-контейнер или sidecar-паттерн.

Балансировка нагрузки без боли: round-robin на C++

В production у вас будет несколько инстансов одного сервиса. Прокси должен уметь балансировать. Вот простой round-robin балансировщик, который можно встроить в тот же прокси:

class RoundRobinBalancer {
public:
    void add_backend(const std::string& host, int port) {
        backends_.push_back({host, port});
    }

    std::pair<std::string, int> get_next() {
        auto backend = backends_[current_];
        current_ = (current_ + 1) % backends_.size();
        return backend;
    }

private:
    std::vector<std::pair<std::string, int>> backends_;
    size_t current_ = 0;
};

Теперь в handle_request мы вызываем get_next() вместо фиксированного адреса. Добавьте health check (например, периодический curl

Блоги

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