«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, поправим.