Почему 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
Блоги
Рекомендаций по книгам
Читайте также