«Redis умирает на 200k RPM, Prometheus не успевает скрейпить 50 серверов, а бизнес требует real-time дашборды. Знакомо?»
Пятница, 18:00. Дашборд в Grafana показывает timeout’ы при сборе метрик. Redis, который хранит данные для prometheus_client_php, жрёт 8GB памяти и 100% CPU. Prometheus не успевает опросить все 50+ серверов за отведённые 15 секунд. А в понедельник запускается Black Friday…
Эта статья — о том, как на одном из проектов перешли с pull на push модель для мониторинга PHP-приложения в highload, почему выбор пал на UDP + Telegraf вместо классического подхода, и как теперь собираем метрики PHP с 50+ серверов без единого timeout’а.
Архитектура: Pull vs Push для метрик в PHP

Проблема: почему Prometheus PHP Client не всегда подходит для highload
Начнём с типичного сценария. У вас есть PHP-приложение на Symfony, нужны метрики для мониторинга. Первое, что приходит в голову — prometheus_client_php. Отличная библиотека, но есть нюансы:
// Классический подход с prometheus_client_php $registry = new CollectorRegistry(new Redis()); $counter = $registry->getOrRegisterCounter('app', 'requests_total', 'Total requests'); $counter->inc(['method' => 'GET', 'endpoint' => '/api/users']);
Что здесь происходит под капотом:
-
Каждая метрика сохраняется в Redis/APC/in-memory storage
-
Prometheus периодически скрейпит endpoint
/metrics -
При скрейпинге происходит чтение всех метрик из хранилища
Где начинаются проблемы
Масштабирование: При 50+ серверах Prometheus должен опрашивать каждый. С ростом числа серверов это становится узким местом.
Хранилище метрик: Redis добавляет латенси, APC работает только в рамках одного сервера, in-memory не переживёт рестарт FPM.
Сложность конфигурации: Нужно настроить service discovery в Prometheus для всех серверов, следить за их доступностью.
Производительность: На 200k RPM каждый вызов Redis для инкремента счётчика — это overhead.
Решение: Push-модель через UDP для мониторинга PHP в highload
Мы пошли другим путём: отправляем метрики через UDP протокол в Telegraf, который уже сам разбирается, куда их дальше передать.

Почему именно UDP?
-
Fire & forget: Отправили пакет и забыли. Никаких ожиданий ответа, никаких таймаутов.
-
Минимальный overhead: UDP-пакет улетает за микросекунды.
-
Fault tolerance: Если Telegraf упал, приложение продолжает работать.
-
Простота: Не нужны connection pools, retry-логика, circuit breakers для метрик.
Важно: Да, UDP может терять пакеты. Но для метрик это не критично — потеря 0.01% данных не исказит общую картину на дашборде.
TelegrafMetricsBundle: реализация
Все это добро я собрал в простенький TelegrafMetricsBundle — Symfony-бандл для отправки метрик через UDP.
Установка и настройка
composer require yakovlef/telegraf-metrics-bundle
Конфигурация в config/packages/telegraf_metrics.yaml:
telegraf_metrics: namespace: 'my_app' # Префикс для всех метрик client: url: 'http://localhost:8086' # InfluxDB URL (для конфигурации клиента) udpPort: 8089 # UDP порт Telegraf
Архитектура бандла
Бандл построен на трёх ключевых компонентах:
// MetricsCollectorInterface - контракт для DI interface MetricsCollectorInterface { public function collect(string $name, array $fields, array $tags = []): void; } // MetricsCollector - реализация через InfluxDB UDP Writer class MetricsCollector implements MetricsCollectorInterface { private UdpWriter $writer; public function __construct(Client $client, string $namespace) { $this->writer = $client->createUdpWriter(); } public function collect(string $name, array $fields, array $tags = []): void { // Отправляем метрику в формате InfluxDB Line Protocol $this->writer->write( new Point("{$this->namespace}_$name", $tags, $fields) ); } }
Интеграция с Symfony DI происходит автоматически:
services: # Автоматическая регистрация через autowiring Yakovlef\TelegrafMetricsBundle\Collector\MetricsCollectorInterface: '@telegraf_metrics.collector'
Практические кейсы использования
1. Мониторинг API endpoints
class ApiController { public function __construct( private MetricsCollectorInterface $metrics ) {} public function getUsers(): JsonResponse { $startTime = microtime(true); try { $users = $this->userRepository->findAll(); $responseTime = (microtime(true) - $startTime) * 1000; $this->metrics->collect('api_request', [ 'response_time' => $responseTime, 'count' => 1 ], [ 'endpoint' => '/api/users', 'method' => 'GET', 'status' => '200' ]); return new JsonResponse($users); } catch (\Exception $e) { $this->metrics->collect('api_error', ['count' => 1], [ 'endpoint' => '/api/users', 'error_type' => get_class($e), 'status' => '500' ]); throw $e; } } }
2. Бизнес-метрики в e-commerce
class OrderService { public function createOrder(OrderDto $dto): Order { $order = new Order($dto); $this->em->persist($order); $this->em->flush(); // Отправляем бизнес-метрики $this->metrics->collect('order_created', [ 'amount' => $order->getTotalAmount(), 'items_count' => $order->getItemsCount(), 'count' => 1 ], [ 'payment_method' => $order->getPaymentMethod(), 'currency' => $order->getCurrency(), 'user_type' => $order->getUser()->getType() ]); return $order; } public function processPayment(Order $order): void { $startTime = microtime(true); try { $result = $this->paymentGateway->charge($order); $this->metrics->collect('payment_processed', [ 'amount' => $order->getTotalAmount(), 'processing_time' => (microtime(true) - $startTime) * 1000, 'count' => 1 ], [ 'gateway' => $this->paymentGateway->getName(), 'status' => 'success' ]); } catch (PaymentException $e) { $this->metrics->collect('payment_failed', [ 'amount' => $order->getTotalAmount(), 'count' => 1 ], [ 'gateway' => $this->paymentGateway->getName(), 'error_code' => $e->getCode() ]); throw $e; } } }
3. Мониторинг фоновых задач
class EmailConsumer implements MessageHandlerInterface { public function __invoke(SendEmailMessage $message): void { $startTime = microtime(true); try { $this->mailer->send($message->getEmail()); $this->metrics->collect('consumer_processed', [ 'processing_time' => (microtime(true) - $startTime) * 1000, 'count' => 1 ], [ 'consumer' => 'email', 'status' => 'success', 'priority' => $message->getPriority() ]); } catch (\Exception $e) { $this->metrics->collect('consumer_failed', ['count' => 1], [ 'consumer' => 'email', 'error' => get_class($e) ]); throw $e; } } }
4. Circuit Breaker паттерн с метриками
class ExternalApiClient { private int $failures = 0; private bool $isOpen = false; public function call(string $endpoint): array { if ($this->isOpen) { $this->metrics->collect('circuit_breaker', ['count' => 1], [ 'service' => 'external_api', 'state' => 'open', 'action' => 'rejected' ]); throw new CircuitBreakerOpenException(); } try { $response = $this->httpClient->request('GET', $endpoint); $this->failures = 0; $this->metrics->collect('circuit_breaker', ['count' => 1], [ 'service' => 'external_api', 'state' => 'closed', 'action' => 'success' ]); return $response->toArray(); } catch (\Exception $e) { $this->failures++; if ($this->failures >= 5) { $this->isOpen = true; $this->metrics->collect('circuit_breaker', ['count' => 1], [ 'service' => 'external_api', 'state' => 'open', 'action' => 'opened' ]); } throw $e; } } }
Агрегация метрик в Telegraf
Одна из киллер-фич Telegraf — встроенная агрегация через плагин basicstats. Вместо отправки сырых данных в Prometheus, можно агрегировать их прямо в Telegraf:
|
Метрика |
Описание |
Когда использовать |
|---|---|---|
|
count |
Количество значений за период |
Подсчёт событий (запросы, ошибки, регистрации) |
|
sum |
Сумма всех значений |
Суммарная выручка, общее время обработки |
|
mean |
Среднее арифметическое |
Среднее время ответа, средний чек |
|
min |
Минимальное значение |
Минимальное время ответа, минимальная сумма заказа |
|
max |
Максимальное значение |
Пиковая нагрузка, максимальное время обработки |
|
stdev |
Стандартное отклонение |
Анализ стабильности (разброс времени ответа) |
|
s2 |
Дисперсия (stdev²) |
Более чувствительная метрика разброса |
Пример конфигурации Telegraf с агрегацией
# /etc/telegraf/telegraf.conf # Input: принимаем метрики по UDP [[inputs.socket_listener]] service_address = "udp://:8089" data_format = "influx" # Aggregation: агрегируем метрики каждые 10 секунд [[aggregators.basicstats]] period = "10s" drop_original = false stats = ["count", "mean", "sum", "min", "max", "stdev"] # Агрегируем только метрики API namepass = ["my_app_api_*"] # Output для Prometheus [[outputs.prometheus_client]] listen = ":9273" metric_version = 2 path = "/metrics" # Батчинг для оптимизации metric_batch_size = 1000 metric_buffer_limit = 10000 # Output для InfluxDB (опционально) [[outputs.influxdb_v2]] urls = ["http://localhost:8086"] token = "your-token" organization = "your-org" bucket = "metrics" # Батчинг для снижения нагрузки flush_interval = "10s" metric_batch_size = 5000
Подводные камни и как их обойти
UDP теряет пакеты — и это нормально
Проблема: При высокой нагрузке возможна потеря пакетов.
Решение: Мониторьте метрики самого Telegraf. Если потери критичны — увеличьте UDP буферы или добавьте батчинг на стороне приложения.
Помним главное: Потеря 0.01% метрик лучше, чем падение приложения из-за недоступного Redis.
Размер UDP пакета: почему ваши метрики могут не долетать
Проблема: UDP пакет ограничен ~65KB, при большом количестве тегов можно превысить лимит.
Решение: Ограничьте количество уникальных тегов, используйте короткие имена:
// Плохо: длинные теги с высокой кардинальностью $this->metrics->collect('api_request', ['time' => 100], [ 'user_email' => $user->getEmail(), // Высокая кардинальность! 'request_id' => uniqid(), // Уникальное значение каждый раз 'full_endpoint_path_with_parameters' => $request->getUri() ]); // Хорошо: короткие теги с низкой кардинальностью $this->metrics->collect('api_request', ['time' => 100], [ 'endpoint' => '/api/users', // Группировка 'method' => 'GET', // Всего 5-7 значений 'status' => '200' // Всего 5-10 значений ]);
Меньше уникальных тегов = меньше размер пакета = надёжнее доставка.
Альтернативные сценарии использования
VictoriaMetrics вместо Prometheus
Для highload-систем Prometheus может становиться узким местом: высокое потребление памяти, долгие запросы при большом объёме данных и отсутствие кластерного режима «из коробки». VictoriaMetrics полностью совместима с Prometheus-протоколом, но эффективнее в хранении, быстрее обрабатывает длинные запросы и поддерживает горизонтальное масштабирование, что делает её более надёжным выбором для систем с сотнями тысяч метрик в секунду.
Отправка в несколько систем одновременно
# Дублируем метрики в разные системы [[outputs.prometheus_client]] listen = ":9273" [[outputs.influxdb_v2]] urls = ["http://influxdb:8086"] [[outputs.graphite]] servers = ["graphite:2003"]
Roadmap и текущие ограничения
Что уже работает
Production-ready
Интеграция с Symfony 6.4+ и 7.0+
Поддержка Prometheus / VictoriaMetrics
Zero-overhead доставка метрик
Важно: Несмотря на отсутствие тестов в текущей версии, бандл уже больше года работает на проде на нескольких highload проектах.
Выводы: что мы получили и чему научились
Переход на push-модель через UDP + Telegraf для мониторинга PHP дает нам три ключевых тейка:
Производительность как конкурентное преимущество
Снижение latency в 60 раз (с 3ms до 0.05ms) — это не просто цифры. На 200k RPM это экономит 10 минут CPU-времени в час, что позволяет обрабатывать на 15% больше запросов на том же железе.
Масштабирование без головной боли
Линейное масштабирование — добавление новых серверов теперь занимает 30 секунд. Просто деплоим приложение с тем же UDP endpoint. Никаких изменений в Prometheus, никакого service discovery.
Антихрупкость системы
Изоляция сбоев — система метрик может полностью упасть, но приложение продолжит работать. За годы эксплуатации это спасло нас несколько раз во время инцидентов с инфраструктурой мониторинга.
Метрики в PHP — это не роскошь, а необходимость для понимания, что происходит в production. Подход с Telegraf UDP позволил забыть о проблемах масштабирования и сосредоточиться на том, что действительно важно — на бизнес-логике и пользовательском опыте.
Да, мы пожертвовали гарантированной доставкой каждого пакета. Но взамен получили систему, которая выдерживает любые нагрузки и не становится точкой отказа в самый критический момент — когда начинается пик на проекте
Если у вас есть опыт мониторинга PHP в highload или вопросы по настройке метрик через Telegraf — делитесь в комментариях. Особенно интересны альтернативные подходы: может, кто-то решил эту задачу через другие инструменты?
Bundle доступен на GitHub и в Packagist.
P.S. Если статья сэкономила вам время на изобретении велосипеда — поставьте звезду репозиторию. А если найдёте баги — создавайте issues, поправим.
ссылка на оригинал статьи https://habr.com/ru/articles/935808/
Добавить комментарий