Введение: без метрик проблема выглядит как «у нас всё тормозит»

Знакомая сцена. В рабочем чате появляется сообщение: «прокси тормозят». Через пять минут второе: «и ошибок много». Дальше начинается то, что инженеры между собой называют археологией. Кто-то открывает консоль и вручную гоняет curl через три случайных адреса. Кто-то смотрит на график загрузки процессора, который к прокси не имеет отношения. Кто-то пишет в поддержку провайдера письмо в духе «у вас что-то не так», не имея ни одного числа в руках. Проходит час. Выясняется, что деградировал только один сегмент пула, только в одной стране, и началось это не «сейчас», а два дня назад, просто никто не заметил.

Разница между командой, которая тратит на такую историю час, и командой, которая тратит минуту, не в опыте и не в таланте. Разница в том, что вторая команда заранее решила, что она будет измерять, что писать в логи и на что ставить оповещения. Когда у вас есть график доли успешных запросов в разбивке по стране и оператору, фраза «всё тормозит» превращается в конкретное утверждение: «с 14:32 p95 времени до первого байта по мобильным прокси оператора A в регионе B вырос с 1,8 до 6,4 секунды, доля ошибок не изменилась, доля повторных попыток утроилась». С таким утверждением уже можно работать: перераспределить нагрузку, открыть тикет провайдеру с данными, а не с эмоциями, или понять, что проблема вообще не в прокси, а в целевом ресурсе.

В этом руководстве мы разберём наблюдаемость вокруг пула прокси от начала до конца. Вы узнаете, какие четыре сигнала дают почти всю полезную информацию, какие поля обязаны быть в каждой строке лога и какие туда попадать не должны никогда, почему среднее время ответа лжёт и как считать перцентили правильно, как размечать данные по измерениям, чтобы видеть деградацию отдельного среза, как строить алерты, которые срабатывают редко, но по делу, и как по дашборду за минуту различать три типовые картины проблем. В конце разберём минимальный технологический стек, реальные кейсы с цифрами и ответим на частые вопросы.

Сразу обозначим границы. Выбор конкретного IP из пула, проверки здоровья прокси и карантин неисправных адресов мы здесь подробно не обсуждаем: этому посвящён отдельный материал. Здесь речь идёт только о двух вещах: измерение и оповещение. Всё, что описано ниже, применимо к легальным инженерным сценариям: мониторинг цен на публичных площадках, проверка отображения рекламы в разных регионах, тестирование геозависимого контента собственных сервисов, сбор открытых данных с соблюдением правил ресурсов.

Немного основ: что такое наблюдаемость применительно к прокси

Классическое определение говорит, что наблюдаемость (observability) — это свойство системы, позволяющее понять её внутреннее состояние по внешним выходам. В переводе на инженерный язык: вы можете задать системе новый вопрос, который не предусмотрели заранее, и получить ответ без деплоя нового кода. Мониторинг отвечает на заранее известные вопросы («жив ли сервис?»), наблюдаемость — на неизвестные («почему запросы к этому домену через мобильные адреса стали медленнее именно по вторникам?»).

Три опоры наблюдаемости хорошо известны: метрики (числовые временные ряды, дёшево хранить, удобно агрегировать), логи (события с контекстом, дорого хранить, но содержат детали) и трассировки (путь одного запроса через компоненты). Для пула прокси картина немного специфична. Между вашим кодом и целевым сайтом стоит внешняя инфраструктура, которую вы не контролируете: выходной адрес, оператор связи, маршрут до цели. Вы не можете поставить агент на мобильную вышку. Поэтому единственная точка, где вы измеряете всё, — это ваш собственный клиент, который отправляет запрос через прокси. Каждый запрос через пул должен оставлять после себя два следа: приращение метрик и строку структурированного лога. Трассировки полезны, но для большинства команд избыточны, и мы вернёмся к ним только в FAQ.

Почему это важно именно для прокси? Потому что деградация прокси почти никогда не выглядит как падение. Сервер не отвечает — это просто. Прокси же «немного медленнее», «чуть чаще отдаёт ошибки соединения», «иногда обрывает загрузку на середине». Каждый такой симптом в отдельности ниже порога внимания человека. В сумме они удваивают стоимость сбора данных и ломают сроки. Заметить медленную деградацию можно только измерением.

Четыре сигнала для прокси: успешность, задержка, ретраи, трафик

В инженерной практике надёжности есть понятие «золотых сигналов»: задержка, трафик, ошибки и насыщение. Для пула прокси мы адаптируем эту идею и получаем четыре сигнала, которых достаточно для 90% ситуаций. Почему именно четыре, а не двадцать? Потому что каждый дополнительный график на дашборде уменьшает шанс, что дежурный посмотрит на нужный. Четыре сигнала — это то, что помещается в одну строку экрана и читается за десять секунд.

Сигнал 1: доля успешных запросов

Доля успешных запросов (success rate) — это отношение запросов, завершившихся полезным результатом, к общему числу запросов за окно времени. Ключевое слово — «полезным». Успех для прокси — не только HTTP 200. Успех — это когда целевой ресурс ответил тем, что вы ожидали, и ответ пришёл целиком. Обрыв соединения на середине тела — не успех, даже если заголовки были с кодом 200. Код 403 от целевого сайта — тоже не успех, но это, вероятно, не вина прокси, и мы это различаем через классификацию исхода.

Практическое правило: классифицируйте исход каждого запроса в одну из небольшого числа категорий. Например: ok (получен ожидаемый ответ), proxy_connect_error (не удалось установить соединение с прокси), proxy_auth_error (прокси отклонил авторизацию), upstream_timeout (прокси ответил, но целевой ресурс не ответил вовремя), upstream_4xx, upstream_5xx, truncated (соединение разорвано до конца тела). Эта классификация — основа для всего дальнейшего анализа. Доля успеха считается как ok ко всему, но отдельно вы смотрите на доли каждой категории ошибок, потому что они указывают на разные виновники.

Сигнал 2: задержка по перцентилям

Задержку для прокси имеет смысл измерять в трёх точках: время установления соединения с прокси, время до первого байта ответа целевого ресурса (TTFB, time to first byte) и полное время запроса. Первое говорит о доступности самого прокси-узла. Второе — о качестве маршрута от прокси до цели и скорости ответа цели. Третье — о пропускной способности канала. Если растёт только третье, а первые два стабильны, проблема в полосе, а не в доступности. Если растёт первое, проблема между вами и прокси. Считать задержку нужно перцентилями, а не средним, и этому посвящён отдельный раздел ниже.

Сигнал 3: доля повторных попыток

Доля ретраев (retry rate) — самый недооценённый сигнал. Большинство клиентов написаны так, что при ошибке автоматически повторяют запрос через другой адрес. Это правильно с точки зрения надёжности и ужасно с точки зрения наблюдаемости: доля успешных запросов остаётся 99%, а под капотом каждый запрос выполняется трижды. Вы платите за трафик втрое, целевой ресурс получает тройную нагрузку, а на дашборде всё зелёное. Доля ретраев вскрывает скрытую деградацию до того, как она станет видимой деградацией. Считается как отношение числа попыток к числу логических запросов: значение 1,0 означает, что ретраев нет, 1,5 — что каждый второй запрос повторялся.

Сигнал 4: расход трафика

Расход трафика — сигнал, который напрямую связан с деньгами, потому что многие тарифы, включая тарифы Proxeon, учитывают объём переданных данных. Но у него есть и диагностическая роль. Резкий рост байт на один успешный запрос означает, что целевой ресурс стал отдавать другие страницы: тяжёлые страницы-заглушки, редиректы на другие разделы, изменённую вёрстку. Резкое падение — что вместо полных страниц вы получаете короткие ответы об ошибке. Считайте трафик в двух направлениях и обязательно нормализуйте на число успешных запросов: «килобайт на успешный запрос» — гораздо более информативная метрика, чем «гигабайт в час».

Как это выглядит в коде

Ниже минимальный набор метрик для Prometheus на Python. Обратите внимание на набор меток: пул, страна, оператор, тип прокси. Адрес конкретного прокси в метках отсутствует намеренно, и мы объясним это в разделе про измерения.

from prometheus_client import Counter, Histogram

LABELS = ["pool", "country", "operator", "proxy_type"]

REQUESTS = Counter("proxy_requests_total", "Логические запросы через пул", LABELS + ["outcome"])
ATTEMPTS = Counter("proxy_attempts_total", "Попытки, включая ретраи", LABELS + ["attempt_kind"])
TTFB = Histogram("proxy_ttfb_seconds", "Время до первого байта", LABELS, buckets=(0.1, 0.25, 0.5, 1, 2, 4, 8, 16, 30))
CONNECT = Histogram("proxy_connect_seconds", "Установление соединения с прокси", LABELS, buckets=(0.05, 0.1, 0.25, 0.5, 1, 2, 5))
BYTES = Counter("proxy_bytes_total", "Байты через пул", LABELS + ["direction"])

def observe(ctx, outcome, ttfb, connect, bytes_in, bytes_out, attempt):
base = dict(pool=ctx.pool, country=ctx.country, operator=ctx.operator, proxy_type=ctx.proxy_type)
kind = "first" if attempt == 1 else "retry"
ATTEMPTS.labels(**base, attempt_kind=kind).inc()
if outcome in ("ok", "upstream_4xx", "upstream_5xx", "truncated", "upstream_timeout"):
TTFB.labels(**base).observe(ttfb)
CONNECT.labels(**base).observe(connect)
BYTES.labels(**base, direction="in").inc(bytes_in)
BYTES.labels(**base, direction="out").inc(bytes_out)
if outcome == "ok" or attempt == ctx.max_attempts:
REQUESTS.labels(**base, outcome=outcome).inc()

Заметьте деталь: счётчик логических запросов увеличивается только при успехе или при исчерпании попыток. Так доля успеха отражает результат для пользователя вашего кода, а счётчик попыток — реальную работу пула. Разница между ними и есть доля ретраев.

Таблица: метрика, что означает её рост, первое действие

МетрикаЧто означает её ростПервое действие
Доля proxy_connect_errorПрокси-узлы недоступны или перегружены; проблема между вами и провайдеромПроверить свой исходящий канал и DNS, затем сравнить по срезам: если весь пул — искать причину у себя, если один срез — уведомить провайдера с данными
Доля proxy_auth_errorИстёк срок доступа, сменились креды, превышен лимит подключенийПроверить актуальность реквизитов и лимиты в панели провайдера, не ретраить такие запросы
Доля upstream_timeoutЦелевой ресурс медленно отвечает через данный маршрут либо прокси не успеваетСравнить TTFB по целевым хостам: если один хост — проблема цели, если все — маршрут прокси
Доля upstream_4xx (кроме 404)Целевой ресурс ограничивает интенсивность или изменил правила доступаСнизить частоту запросов к ресурсу, проверить соответствие его правилам, не увеличивать ретраи
Доля upstream_5xxПроблема на стороне целевого ресурсаПодождать, снизить нагрузку; прокси, скорее всего, не при чём
Доля truncatedСоединения обрываются на середине: нестабильный канал, слишком короткие таймауты чтенияПроверить таймауты чтения и размер ответов; сравнить по операторам
p95 времени соединения с проксиДеградация доступности узлов прокси или вашего сетевого стекаПроверить число одновременных соединений и исчерпание портов у себя
p95 TTFB при стабильном времени соединенияМаршрут от прокси до цели стал медленнее или цель замедлиласьРазложить по целевым хостам и по срезам пула
p99 полного времени при стабильном TTFBУпала пропускная способность канала прокси или выросли ответыПосмотреть байты на запрос; если размер не менялся — проблема полосы
Доля ретраевСкрытая деградация: успех держится за счёт повторов, растёт стоимостьНайти, какая категория ошибок вызывает повторы, и работать с ней
Байты на успешный запросЦелевой ресурс отдаёт другой контент: заглушки, редиректы, изменённая структураПроверить содержимое ответов на выборке, обновить парсер и валидацию
Общий трафик при стабильном числе запросовРост ретраев или размера ответов; прямой удар по бюджетуРазложить на два фактора: число попыток и байты на попытку

Что писать в лог на каждый запрос — и чего писать нельзя

Метрики отвечают на вопрос «что происходит», логи — на вопрос «с чем именно». Когда дашборд показывает рост ошибок в срезе «страна X, оператор Y», вам нужно понять, все ли адреса этого среза виноваты или два из тридцати. Метрики с меткой по адресу взорвут хранилище, а логи — нет, потому что логи вы не агрегируете постоянно, а фильтруете по запросу.

Обязательные поля строки лога

Каждая попытка запроса через пул должна порождать одну структурированную запись, желательно в формате JSON. Вот набор полей, который мы считаем минимально достаточным:

  • ts — метка времени в UTC с миллисекундами. Не локальное время. Никогда.
  • request_id — идентификатор логического запроса, общий для всех его попыток. Позволяет собрать всю историю одного запроса одной командой.
  • attempt и max_attempts — номер попытки и лимит. Без номера попытки вы не отличите первую ошибку от третьей.
  • proxy_id — внутренний идентификатор прокси. Не адрес с портом и не строка подключения. Внутренний идентификатор, который вы можете сопоставить с реальным адресом через отдельную защищённую таблицу.
  • proxy_type, country, operator, pool — те же измерения, что в метриках. Это позволяет прыгать между дашбордом и логами без пересчёта.
  • target_host — доменное имя цели. Не полный URL: в строках запроса бывают токены и персональные данные.
  • method — HTTP-метод.
  • status — HTTP-код ответа, если он был получен, иначе null.
  • outcome — та самая классификация исхода, о которой шла речь выше.
  • error_class — класс исключения или короткий код ошибки транспорта, если исход не ok.
  • connect_ms, ttfb_ms, total_ms — три задержки в миллисекундах.
  • bytes_in, bytes_out — размер тела ответа и размер запроса.
  • session_id — если вы используете липкие сессии, идентификатор сессии; так вы увидите, что ошибки концентрируются в долгих сессиях.
  • retry_reason — для попыток начиная со второй: исход предыдущей попытки.

Пример строки:

{"ts":"2026-03-14T11:32:07.412Z","request_id":"7f3a9c1e","attempt":2,"max_attempts":3,"retry_reason":"upstream_timeout","proxy_id":"px-2841","proxy_type":"mobile","country":"RU","operator":"op_a","pool":"pricing","target_host":"shop.example","method":"GET","status":200,"outcome":"ok","error_class":null,"connect_ms":184,"ttfb_ms":1420,"total_ms":2310,"bytes_in":48211,"bytes_out":612,"session_id":"s-19b"}

Одна такая строка весит около 400 байт. При 50 запросах в секунду это примерно 1,7 гигабайта в сутки без сжатия и 200–300 мегабайт со сжатием. Вполне посильно даже для скромного бюджета.

Чего в логах быть не должно

Здесь мы переходим от инженерии к ответственности. Список коротко, но каждый пункт важен:

  1. Реквизиты доступа к прокси. Самая частая утечка — строка подключения вида http://логин:пароль@хост:порт, которая попадает в лог целиком, потому что разработчик залогировал конфиг «для отладки». Логи копируются в тикеты, пересылаются коллегам, хранятся годами. Реквизиты в них — это реквизиты, доступные всем, кто когда-либо видел лог.
  2. Заголовок Proxy-Authorization и любые другие заголовки авторизации, включая заголовки к целевому ресурсу.
  3. Тело ответа. Оно огромное, оно может содержать персональные данные третьих лиц, и оно почти никогда не нужно для диагностики прокси. Если вам нужно понять, что отдал целевой ресурс, сохраните хеш тела и его длину, а сам ответ — в отдельное хранилище с коротким сроком жизни и только по выборке.
  4. Cookies в обе стороны.
  5. Полный URL с параметрами запроса. Достаточно хоста и, при необходимости, пути без параметров.
  6. Реальный адрес прокси в открытом виде, если лог уходит в общее хранилище. Внутреннего идентификатора достаточно.

Практика, которую мы рекомендуем: реализуйте редактирование на уровне единой функции, через которую проходит любая запись, связанная с прокси. Не полагайтесь на дисциплину разработчиков.

import re

CRED_IN_URL = re.compile(r"(https?|socks5h?)://[^/@:]+:[^/@]+@")
SENSITIVE_HEADERS = {"proxy-authorization", "authorization", "cookie", "set-cookie"}

def redact(record: dict) -> dict:
for key, value in list(record.items()):
if key.lower() in SENSITIVE_HEADERS:
record[key] = "[redacted]"
elif isinstance(value, str):
record[key] = CRED_IN_URL.sub(lambda m: m.group(1) + "://[redacted]@", value)
record.pop("body", None)
return record

Семплирование и хранение

Нужно ли писать каждую попытку? Ошибки — да, все. Успешные попытки — можно семплировать: при интенсивности выше сотни запросов в секунду достаточно 5–10% успешных строк для оценки задержек, потому что метрики уже содержат полную картину. Важно, чтобы решение о семплировании принималось по request_id, а не случайно на каждую попытку: иначе вы получите вторую попытку без первой. Срок хранения детальных логов — обычно 7–14 дней; агрегаты и метрики живут дольше.

Перцентили вместо среднего: почему среднее скрывает проблему

Представьте пул из ста адресов. Девяносто из них отвечают за 0,8 секунды. Десять — за 9 секунд, потому что попали на перегруженную базовую станцию. Среднее время ответа — 1,62 секунды. Неделю назад, когда всё было хорошо, среднее было 1,4. Рост на 15%. Кто на такое отреагирует? Никто. Между тем каждый десятый ваш запрос ждёт девять секунд, и если у вас есть таймаут в восемь, каждый десятый запрос падает с ошибкой.

Среднее арифметическое хорошо описывает распределения, похожие на колокол. Задержки через прокси на колокол не похожи никогда. Это распределение с длинным правым хвостом: большинство запросов быстрые, но некоторые — очень медленные, и именно они определяют пользовательский опыт и стоимость. Более того, задержки через пул часто бимодальны: есть «нормальный» горб и есть «горб проблемных адресов». Среднее ложится между ними, в точку, где реальных запросов почти нет.

Что такое p50, p95, p99

Перцентиль — это значение, ниже которого лежит заданная доля наблюдений. p50 (медиана) — половина запросов быстрее этого значения. p95 — 95% быстрее, 5% медленнее. p99 — только один процент медленнее. Для пула прокси мы рекомендуем смотреть на три перцентиля с разными целями:

  • p50 показывает «типичный» запрос. Если растёт p50, деградировало большинство. Это редкий и серьёзный сигнал.
  • p95 — рабочая метрика для алертов. Она чувствительна к проблемным сегментам (в примере выше p95 сразу покажет 9 секунд), но устойчива к единичным выбросам.
  • p99 — метрика для таймаутов и планирования. Ваш таймаут должен быть чуть выше «здорового» p99, иначе вы будете обрывать нормальные запросы и превращать их в ретраи.

Как считать точно

Если данных немного (тысячи значений в окне), считайте точно: отсортируйте и возьмите элемент по индексу.

def percentile(values, p):
if not values:
return None
ordered = sorted(values)
k = (len(ordered) - 1) * p / 100.0
lo = int(k)
hi = min(lo + 1, len(ordered) - 1)
return ordered[lo] + (ordered[hi] - ordered[lo]) * (k - lo)

ttfb_window = [0.8, 0.9, 0.7, 9.1, 0.85, 8.7, 0.78, 0.92, 0.81, 0.79]
print(percentile(ttfb_window, 50), percentile(ttfb_window, 95))

Точный подсчёт не масштабируется: чтобы посчитать p95 за неделю по миллионам запросов, нужно хранить все значения. Поэтому промышленные системы используют приближённые структуры.

Как считать в масштабе: гистограммы

Prometheus и совместимые системы хранят гистограмму как набор счётчиков по корзинам: сколько наблюдений было меньше 0,1 с, меньше 0,25 с, меньше 0,5 с и так далее. Перцентиль восстанавливается линейной интерполяцией внутри корзины. Точность зависит от того, насколько удачно выбраны границы корзин. Для задержек через прокси хорошо работает логарифмическая шкала от 100 миллисекунд до 30 секунд: 0,1; 0,25; 0,5; 1; 2; 4; 8; 16; 30. Если ваш здоровый p95 около 2 секунд, а таймаут 15, такая сетка даст погрешность в пределах 20–30% на хвосте, чего для алертов и диагностики достаточно.

histogram_quantile(0.95,
sum by (le, country, operator) (
rate(proxy_ttfb_seconds_bucket{pool="pricing"}[5m])
)
)

Обратите внимание на sum by (le, ...) внутри. Это критически важная деталь: гистограммы разных инстансов вашего клиента нужно сначала сложить, а потом вычислять квантиль. Обратная операция — посчитать p95 на каждом инстансе и усреднить — математически бессмысленна. Среднее перцентилей не является перцентилем. Это одна из самых распространённых ошибок, и она способна спрятать проблему так же надёжно, как обычное среднее.

Если вам нужна высокая точность на хвосте (например, вы продаёте SLA на задержку внутренним заказчикам), используйте структуры типа t-digest или HDR-гистограмм, которые дают точность в доли процента при фиксированном объёме памяти. Для большинства команд стандартных гистограмм достаточно.

Разметка по измерениям: страна, оператор, тип прокси

Вот сценарий, который иллюстрирует, зачем нужны измерения. У вас пул из 400 адресов, 5% из них — мобильные адреса одного оператора в одном регионе. Этот сегмент полностью деградировал: доля успеха там упала до 10%. Что покажет общий график доли успеха по пулу? Падение с 99% до 94,5%. Заметно, но не критично. Дежурный подумает «сеть шалит» и пойдёт дальше. А заказчик данных из этого региона получит пустоту.

Разметка по измерениям (в терминологии Prometheus — меткам, labels) решает ровно эту задачу: она позволяет разложить агрегат на срезы и увидеть, что проблема локализована. Но у разметки есть цена, и её нужно понимать.

Какие измерения выбрать

Хорошее измерение обладает двумя свойствами: у него небольшое, ограниченное число значений, и разбивка по нему отвечает на реальный вопрос диагностики. Для пула прокси мы рекомендуем четыре измерения в метриках:

  • proxy_type — мобильные, резидентные, датацентровые. Разные типы деградируют по-разному и имеют разную стоимость.
  • country — страна выходного адреса. Обычно от 1 до 30 значений.
  • operator — оператор связи или автономная система, через которую выходит адрес. Проблемы часто локализованы именно здесь: одна перегруженная базовая станция или один плохой пиринг у оператора.
  • pool — логическое назначение: какой продукт или задача использует этот сегмент. Позволяет разделять инциденты по влиянию на бизнес.

Пятое измерение, о котором стоит подумать — target_host, целевой хост. Оно нужно, чтобы отличать проблемы прокси от проблем цели. Но если вы ходите на тысячи разных доменов, кардинальность взорвётся. Компромисс: оставляйте в метке только топ-20 хостов по объёму, а остальные схлопывайте в значение other.

Арифметика кардинальности

Каждая уникальная комбинация значений меток — это отдельный временной ряд. Посчитаем: 3 типа × 10 стран × 15 операторов × 4 пула × 7 исходов = 12 600 рядов только для счётчика запросов. Гистограмма с 9 корзинами умножит своё число рядов на 10. Итого около 50–60 тысяч рядов. Для Prometheus это комфортно. Теперь представьте, что вы добавили метку с адресом прокси (400 значений). Число рядов вырастает в 400 раз — до 20 миллионов. Это уже не «комфортно», это смерть хранилища.

Отсюда правило: адрес конкретного прокси живёт в логах, а не в метриках. Метрики отвечают на вопрос «какой срез деградировал», логи — «какие именно адреса внутри среза». Такое разделение труда даёт и скорость, и детализацию без взрывного роста затрат.

Запросы, которые находят срез

Главный вопрос диагностики: «где хуже всего?» На него отвечает сортировка срезов по доле успеха:

bottomk(5,
sum by (country, operator, proxy_type) (rate(proxy_requests_total{outcome="ok"}[10m]))
/
sum by (country, operator, proxy_type) (rate(proxy_requests_total[10m]))
)

Второй вопрос: «это новая проблема или так было всегда?» На него отвечает сравнение с прошлым:

(
sum by (operator) (rate(proxy_requests_total{outcome="ok"}[10m])) / sum by (operator) (rate(proxy_requests_total[10m]))
)
-
(
sum by (operator) (rate(proxy_requests_total{outcome="ok"}[10m] offset 1d)) / sum by (operator) (rate(proxy_requests_total[10m] offset 1d))
)

Отрицательное значение показывает, насколько срез стал хуже, чем сутки назад. Такой график на дашборде превращает «где-то что-то не так» в «оператор B в стране C минус 12 пунктов относительно вчера».

Откуда берутся значения измерений

Страна и тип прокси обычно известны из конфигурации пула. Оператора можно получить из метаданных провайдера: например, в панели и API Proxeon адреса снабжены информацией о географии и операторе, которую вы подтягиваете при инициализации пула и кешируете рядом с внутренним идентификатором прокси. Определять оператора в момент запроса через внешние базы не стоит: это добавит задержку и создаст лишнюю зависимость.

Алерты, которые не шумят: пороги, окна, гистерезис

Худшее, что может случиться с системой оповещений, — не пропущенный инцидент. Худшее — это когда дежурный получает 40 уведомлений в день, из которых 39 ничего не значат, и перестаёт читать сороковое. Оповещение, которое игнорируют, хуже отсутствия оповещения, потому что создаёт иллюзию контроля.

Принцип 1: алерт на симптом, а не на причину

Не ставьте алерт на «прокси px-2841 вернул ошибку». Отдельный адрес имеет право болеть; за это отвечает механизм карантина внутри пула, который мы здесь не обсуждаем. Ставьте алерт на то, что чувствует потребитель ваших данных: доля успеха по пулу ниже цели, p95 выше цели, ретраи выше цели. Причину вы найдёте по дашборду за минуту, если правильно сделали измерения. Задача алерта — разбудить, а не объяснить.

Принцип 2: минимальный объём трафика

Доля успеха 50% при двух запросах за пять минут — это один неудачный запрос, а не инцидент. Любое правило на долю должно содержать условие на минимальное число запросов в окне. Иначе ночью, когда трафик падает, вы утонете в ложных срабатываниях.

Принцип 3: окна и скорость сгорания бюджета

Если вы определили целевой уровень надёжности (например, 98% успешных запросов за месяц), у вас есть бюджет ошибок: 2% запросов. Хороший алерт реагирует на скорость расходования бюджета, а не на мгновенное значение. Практика, ставшая стандартом в индустрии надёжности, — многооконные алерты: короткое окно (5 минут) ловит резкие падения, длинное (1 час) — медленную деградацию, и оба должны быть превышены одновременно, чтобы исключить кратковременные всплески.

Принцип 4: гистерезис

Гистерезис — это разные пороги для срабатывания и снятия оповещения. Если алерт срабатывает при доле успеха ниже 95% и снимается при возврате выше 95%, метрика, колеблющаяся вокруг 95%, породит серию уведомлений «сработал — снялся — сработал» каждые несколько минут. Решение: срабатывать при 95%, снимать при 97%. Или, проще, требовать, чтобы условие держалось непрерывно заданное время (в Prometheus это ключевое слово for), а также задавать задержку на снятие.

Принцип 5: уровни серьёзности и маршрутизация

Не всё нужно доставлять звонком. Разделите оповещения на два-три уровня: критические (влияние на потребителей данных прямо сейчас — звонок или пуш дежурному), предупреждения (деградация среза, рост ретраев, аномалия трафика — сообщение в канал, разберётся в рабочее время), информационные (отсутствие данных от инстанса, приближение к лимиту трафика — ежедневная сводка).

Пример набора правил

groups:
- name: proxy-pool
rules:
- record: pool:success_ratio:5m
expr: sum by (pool) (rate(proxy_requests_total{outcome="ok"}[5m])) / sum by (pool) (rate(proxy_requests_total[5m]))
- record: pool:success_ratio:1h
expr: sum by (pool) (rate(proxy_requests_total{outcome="ok"}[1h])) / sum by (pool) (rate(proxy_requests_total[1h]))
- record: pool:request_rate:5m
expr: sum by (pool) (rate(proxy_requests_total[5m]))

- alert: ProxyPoolSuccessLow
expr: pool:success_ratio:5m < 0.95 and pool:success_ratio:1h < 0.97 and pool:request_rate:5m > 0.5
for: 5m
labels: { severity: critical }
annotations:
summary: "Доля успеха по пулу {{ $labels.pool }} ниже цели"

- alert: ProxyPoolLatencyHigh
expr: histogram_quantile(0.95, sum by (le, pool) (rate(proxy_ttfb_seconds_bucket[10m]))) > 4 and pool:request_rate:5m > 0.5
for: 10m
labels: { severity: warning }

- alert: ProxyPoolRetryRateHigh
expr: sum by (pool) (rate(proxy_attempts_total[15m])) / sum by (pool) (rate(proxy_requests_total[15m])) > 1.4
for: 15m
labels: { severity: warning }

- alert: ProxyPoolSliceDegraded
expr: (sum by (pool, country, operator) (rate(proxy_requests_total{outcome="ok"}[15m])) / sum by (pool, country, operator) (rate(proxy_requests_total[15m]))) < 0.85 and sum by (pool, country, operator) (rate(proxy_requests_total[15m])) > 0.2
for: 15m
labels: { severity: warning }

- alert: ProxyPoolNoData
expr: absent(proxy_requests_total) or pool:request_rate:5m == 0
for: 10m
labels: { severity: warning }

Обратите внимание на последнее правило. Отсутствие данных — самый коварный сбой: клиент упал, метрик нет, графики плоские и зелёные, все спокойны. Алерт на тишину обязателен.

Чек-лист для каждого нового алерта

  1. Написано ли в аннотации, что делать получателю? Если нет — правило не готово.
  2. Есть ли условие на минимальный трафик?
  3. Есть ли задержка for или гистерезис?
  4. Проверено ли правило на истории за последний месяц: сколько раз оно бы сработало и сколько из них были реальными инцидентами?
  5. Понятно ли, к какому уровню серьёзности оно относится и куда маршрутизируется?
  6. Заведён ли обратный процесс: если алерт сработал и оказался ложным, кто и когда его пересмотрит?

Быстрая диагностика по дашборду: три типовые картины

Дашборд — не галерея графиков, а инструмент принятия решения. Хороший дашборд для пула прокси устроен как воронка. Верхняя строка: четыре сигнала по всему пулу за последние 6 часов с наложением вчерашнего дня тонкой линией. Вторая строка: те же сигналы, разложенные по стране, оператору и типу — в виде таблицы или тепловой карты, отсортированной по худшему значению. Третья строка: доля успеха и TTFB по топ-10 целевым хостам. Четвёртая: соотношение времени соединения, TTFB и полного времени. Плюс вертикальные маркеры деплоев и изменений конфигурации пула.

С таким дашбордом диагностика сводится к ответу на три вопроса: «деградировал весь пул или срез?», «деградировали все цели или одна?», «какая из трёх задержек выросла?». Комбинации ответов дают три типовые картины.

Картина 1: деградировали все срезы одновременно

Все страны, все операторы, все типы прокси показывают рост ошибок или задержек синхронно, с точностью до минуты. Внешние провайдеры и операторы так не ломаются: у них нет общей точки отказа с вашей стороны. Значит, проблема у вас: ваш исходящий канал, ваш DNS, исчерпание файловых дескрипторов или локальных портов, недавний деплой, изменивший таймауты или размер пула соединений, конкуренция за процессор с другим процессом на той же машине. Первое действие: посмотреть на маркер деплоя и на время соединения с прокси. Если время соединения выросло, а TTFB (за вычетом соединения) стабилен — проблема между вами и прокси-узлами, и почти наверняка это ваша сторона или ваш сетевой провайдер.

Картина 2: деградировал один срез

Один оператор в одной стране, или один тип прокси, показывает падение, а остальные срезы стабильны. Это самая частая картина. Причина — у оператора связи (перегрузка сегмента сети, регламентные работы) или у сегмента инфраструктуры провайдера прокси. Первое действие: временно снизить вес этого среза в распределении нагрузки (механизм — за пределами этой статьи) и написать в поддержку провайдера сообщение с конкретными данными: срез, время начала, метрики до и после, примеры request_id. Поддержка Proxeon реагирует на такие обращения быстрее, чем на «у вас всё плохо», просто потому, что получает готовую локализацию. Второе действие: посмотреть логи по срезу, чтобы понять, все ли адреса внутри деградировали или несколько.

Картина 3: упала доля успеха для одного целевого хоста, задержки в норме

Все срезы пула по-прежнему хорошо ходят на все хосты, кроме одного. Для этого хоста растёт доля upstream_4xx или изменился объём байт на запрос. Прокси здесь не виноваты вообще. Целевой ресурс изменил поведение: ввёл ограничение частоты, изменил структуру страницы, начал отдавать другой контент для части запросов. Первое действие: снизить интенсивность запросов к этому ресурсу и проверить, соответствует ли ваш сбор его правилам использования. Второе: проверить содержимое на выборке и обновить валидацию ответов. Категорически не стоит в этой ситуации увеличивать число ретраев или менять адреса чаще — это только увеличит нагрузку на ресурс и ваши затраты на трафик.

Картина 4 (бонус): ретраи растут, успех стоит

Доля успеха 99%, задержки терпимые, но доля ретраев выросла с 1,1 до 1,8. Это скрытая деградация. Она стоит вам денег уже сейчас и станет видимой деградацией, как только исчерпается запас попыток. Первое действие: посмотреть, какой исход стал причиной повторов (поле retry_reason в логах), и дальше идти по одной из первых трёх картин.

Чек-лист диагностики за 60 секунд

  1. Секунды 0–10: верхняя строка дашборда. Какой из четырёх сигналов ушёл от вчерашней линии? Когда именно?
  2. Секунды 10–25: таблица по срезам. Один срез красный или все? Если все — картина 1, идти к маркерам деплоя и своей инфраструктуре.
  3. Секунды 25–40: таблица по целевым хостам. Один хост или все? Если один — картина 3, прокси не при чём.
  4. Секунды 40–50: соотношение задержек. Выросло соединение, TTFB или полное время?
  5. Секунды 50–60: сформулировать одно предложение: «с [время] в срезе [срез] для [цель] выросла [метрика] с [было] до [стало], предположительно из-за [гипотеза]». Это предложение уходит в канал инцидента и в тикет провайдеру.

Типичные ошибки, которые обнуляют пользу от наблюдаемости

Мы видели десятки внедрений мониторинга вокруг прокси. Ошибки повторяются с удивительным постоянством.

  • Среднее время ответа как главная метрика. Уже обсудили: среднее прячет хвост, а хвост — это и есть проблема.
  • Усреднение перцентилей между инстансами. Складывайте гистограммы, а не готовые p95.
  • Адрес прокси как метка метрики. Взрыв кардинальности и падение хранилища через месяц.
  • Реквизиты доступа в логах. Одна строка «for debug» — и пароль лежит в общем хранилище логов на год.
  • Ретраи считаются как успех. Если считать только финальный исход, вы никогда не увидите, что пул работает втрое интенсивнее, чем нужно.
  • Код 429 и 403 от целевого ресурса записываются как ошибка прокси. Это ошибка классификации, которая уводит расследование в неверную сторону и портит отношения с провайдером.
  • Одна задержка вместо трёх. Без раздельного измерения соединения, TTFB и полного времени вы не отличите проблему доступности от проблемы полосы.
  • Алерт без минимального трафика. Ночной шум и потеря доверия к оповещениям.
  • Алерт на каждый отдельный адрес. Это задача карантина, а не оповещения человека.
  • Нет алерта на отсутствие данных. Клиент упал — графики зелёные.
  • Локальное время в логах. Сопоставление с данными провайдера и коллег в другом часовом поясе превращается в пытку.
  • Дашборд без маркеров деплоя. Половина инцидентов совпадает с изменением кода или конфигурации. Без маркера вы будете искать причину снаружи, когда она внутри.
  • Хранение детальных логов вечно. Дорого и бессмысленно: через две недели детали не нужны, а агрегаты уже есть в метриках.

Минимальный стек без переусложнения

Наблюдаемость легко превратить в проект ради проекта. Команда из трёх человек разворачивает распределённую очередь сообщений, кластер колоночной базы и систему трассировок — для клиента, который делает 20 запросов в секунду. Не надо так. Стек должен соответствовать масштабу.

Уровень 1: один сервис, до 50 запросов в секунду

Достаточно структурированных JSON-логов в файл или стандартный вывод и одного скрипта, который раз в минуту агрегирует их и пишет четыре сигнала в простую базу или даже в тот же лог. Дашборд — любой инструмент, который умеет строить график по таблице. Алерты — cron-скрипт, который сравнивает агрегаты с порогами и шлёт сообщение в мессенджер. Такой стек поднимается за день и закрывает 80% потребностей.

Уровень 2: несколько сервисов, 50–1000 запросов в секунду

Стандартный набор: Prometheus для метрик, Grafana для дашбордов, Alertmanager для маршрутизации оповещений, Loki или аналогичное хранилище для логов с индексом по меткам. Клиенты экспортируют метрики через встроенный HTTP-эндпоинт и пишут JSON-логи в стандартный вывод; агент собирает их. Правила записи (recording rules) заранее считают доли успеха, чтобы дашборды открывались мгновенно. Это конфигурация, описанная в примерах выше, и она покрывает подавляющее большинство команд.

Уровень 3: десятки сервисов, тысячи запросов в секунду

Здесь появляется смысл в OpenTelemetry как едином слое сбора, в колоночном хранилище для логов с возможностью произвольных аналитических запросов по миллиардам строк и, возможно, в трассировках для сложных цепочек. Но даже на этом уровне четыре сигнала и структура логов не меняются. Меняется только транспорт и хранение.

Данные провайдера как дополнение

Провайдер прокси видит свою сторону: расход трафика по каждому порту, число активных соединений, время последней ротации. Панель и API Proxeon отдают эту статистику, и её полезно подтягивать рядом с собственными метриками — хотя бы для сверки расхода трафика: если ваш счётчик байт и счётчик провайдера расходятся более чем на 10–15%, у вас проблема с измерением (чаще всего не учитываются заголовки, TLS-рукопожатия или оборванные ответы). Но данные провайдера не заменяют ваши: только вы знаете, какой запрос был успешным с точки зрения задачи.

Обёртка клиента как единая точка измерения

Независимо от уровня стека, все измерения должны проходить через одну обёртку над HTTP-клиентом. Никаких «здесь измерим, а здесь забыли». Схематично:

import time, json, sys, requests

def fetch_via_proxy(ctx, url, timeout=(5, 20)):
t0 = time.perf_counter()
outcome, status, err = "ok", None, None
ttfb = connect = total = 0.0
bytes_in = 0
try:
session = ctx.session # уже настроена на прокси по внутреннему proxy_id
resp = session.get(url, timeout=timeout, stream=True)
ttfb = time.perf_counter() - t0
status = resp.status_code
body = resp.content
bytes_in = len(body)
total = time.perf_counter() - t0
if status >= 500: outcome = "upstream_5xx"
elif status >= 400: outcome = "upstream_4xx"
elif not ctx.validate(body): outcome = "invalid_content"
except requests.exceptions.ProxyError as e:
outcome, err = "proxy_connect_error", type(e).__name__
except requests.exceptions.ReadTimeout as e:
outcome, err = "upstream_timeout", type(e).__name__
except requests.exceptions.ChunkedEncodingError as e:
outcome, err = "truncated", type(e).__name__
finally:
total = total or (time.perf_counter() - t0)
observe(ctx, outcome, ttfb, connect, bytes_in, len(url), ctx.attempt)
record = redact({"ts": ctx.now_utc(), "request_id": ctx.request_id, "attempt": ctx.attempt,
"proxy_id": ctx.proxy_id, "proxy_type": ctx.proxy_type, "country": ctx.country,
"operator": ctx.operator, "pool": ctx.pool, "target_host": ctx.host(url),
"status": status, "outcome": outcome, "error_class": err,
"ttfb_ms": int(ttfb * 1000), "total_ms": int(total * 1000), "bytes_in": bytes_in})
sys.stdout.write(json.dumps(record, ensure_ascii=False) + chr(10))
return outcome, status

Обратите внимание на исход invalid_content: ответ с кодом 200, который не прошёл вашу валидацию (например, страница без ожидаемого блока данных), — это не успех. Именно такие ответы чаще всего маскируют изменение поведения целевого ресурса.

Кейсы: как выглядит наблюдаемость в деле

Кейс 1: один оператор, одна страна, минус два часа простоя в неделю

Команда мониторинга цен на публичных торговых площадках работала через пул из примерно 300 мобильных и резидентных адресов при интенсивности около 40 запросов в секунду. До внедрения разметки по операторам они видели одно: общий p95 TTFB время от времени подскакивал с 1,6 до 3–4 секунд на несколько часов, доля успеха проседала на 3–5 пунктов. Расследование каждого такого эпизода занимало от часа до двух, и чаще всего заканчивалось словами «само прошло». После добавления меток country и operator первый же эпизод локализовался за две минуты: мобильные адреса одного оператора в одном регионе, p95 там был 11 секунд при норме 2. Оставшиеся 85% пула были в полном порядке. Команда снизила вес среза в распределении, направила провайдеру отчёт с временными рамками и метриками, и через несколько часов получила подтверждение регламентных работ у оператора. Итог за квартал: время диагностики упало с 90 минут до 5, число эпизодов не изменилось, но их влияние на общий p95 стало неразличимым.

Кейс 2: скрытые ретраи и +38% к счёту за трафик

Другая команда обнаружила рост расхода трафика на 38% за месяц при неизменном числе задач и стабильной доле успеха 98,5%. Первая гипотеза — целевые страницы стали тяжелее. Метрика байт на успешный запрос это опровергла: 46 килобайт, как и раньше. Зато доля ретраев, которую до этого никто не смотрел, выросла с 1,08 до 1,52. Разбор поля retry_reason в логах показал: почти все повторы вызывал исход upstream_timeout для одного целевого хоста, который стал отвечать медленнее, но укладывался в таймаут со второй попытки. Прокси не были причиной вообще. Команда увеличила таймаут чтения для этого хоста с 8 до 15 секунд (здоровый p99 был 11), снизила интенсивность запросов к нему и вернула расход трафика к прежнему уровню за три дня. Мораль: доля ретраев — это метрика про деньги.

Кейс 3: от 200 уведомлений в неделю до шести

Третья команда унаследовала систему оповещений с правилом «доля успеха по любому срезу за 1 минуту ниже 90%» без ограничения на трафик. Ночью, когда через некоторые срезы шло два-три запроса в минуту, правило срабатывало десятки раз. Дежурные выключили звук. Один реальный инцидент — падение доступности целого сегмента на четыре часа — был замечен утром по жалобам. Переработка правил заняла один день: добавлено условие на минимальный трафик, введены два окна (5 минут и 1 час), добавлена задержка for на 10 минут, срезы вынесены в уровень warning, а на pool-уровне оставлен единственный critical. Результат: около шести уведомлений в неделю, из которых четыре-пять требовали реального действия. Дежурные снова включили звук.

FAQ: частые вопросы о наблюдаемости пула прокси

Нужны ли трассировки, если есть метрики и логи?

Для большинства команд — нет. Запрос через прокси не проходит через десять микросервисов; у него один переход. Роль трассировки здесь выполняет request_id в логах: по нему вы собираете все попытки одного запроса. Трассировки становятся полезны, когда прокси-клиент — лишь часть длинной цепочки (очередь задач, парсер, валидатор, загрузчик в базу) и вам нужно понять, где именно время теряется.

Как измерять TTFB, если HTTP-клиент его не отдаёт напрямую?

Большинство клиентов возвращают управление после получения заголовков, если попросить потоковое чтение (stream=True или аналог). Момент возврата и есть приближение TTFB. Время соединения точнее измеряется на уровне транспорта: у многих библиотек есть хуки на события соединения, у некоторых — готовая информация о фазах запроса. Если хуков нет, можно измерять время до отправки первого байта запроса отдельным низкоуровневым замером, но для практических целей приближения через потоковое чтение достаточно.

Как отличить ошибку прокси от ошибки целевого ресурса?

По месту возникновения. Если соединение с прокси не установилось или прокси вернул собственный код (например, 407 при проблемах авторизации либо специфический ответ шлюза), это ошибка прокси. Если прокси принял запрос, а дальше вы получили код от целевого сервера или таймаут ожидания ответа, это ошибка между прокси и целью или самой цели. Классифицируйте на этой границе, а затем сравнивайте по срезам: ошибки цели видны для всех срезов одновременно, ошибки прокси — для конкретных.

Каким должен быть здоровый уровень доли ретраев?

Зависит от целей и типа прокси. Для мобильных адресов нормой можно считать 1,05–1,15: беспроводная сеть по природе менее предсказуема. Для датацентровых — 1,01–1,05. Важнее не абсолютное значение, а его стабильность: рост на 30% относительно недельного базового уровня — повод разобраться, даже если абсолютное значение всё ещё выглядит скромным.

Как выбрать пороги алертов, если нет истории?

Начните с наблюдения без оповещений: соберите две недели данных, посмотрите на распределение доли успеха и p95 по дням и часам. Порог для critical ставьте так, чтобы за эти две недели он сработал бы не более одного-двух раз, и оба раза вы бы согласились, что это инцидент. Порог для warning — чтобы срабатывал несколько раз в неделю. Затем каждый месяц пересматривайте: алерт, который ни разу не привёл к действию, либо ужесточается, либо удаляется.

Стоит ли хранить тело ответа хотя бы для ошибок?

Для ошибок транспорта тела нет. Для ответов с неожиданным содержимым (invalid_content) полезно сохранить первые несколько сотен байт или хеш и длину, в отдельное хранилище с сроком жизни один-два дня, с обязательной очисткой от персональных данных. Полные тела хранить не нужно: они тяжёлые, они могут содержать данные третьих лиц, а для диагностики прокси почти всегда достаточно понять, какой тип страницы пришёл.

Как сверять свой учёт трафика с данными провайдера?

Считайте байты в обе стороны, включая заголовки, и сравнивайте суточные суммы по портам или сессиям с тем, что показывает панель провайдера. Расхождение в 5–10% нормально: у вас не учитываются накладные расходы TLS и оборванные соединения. Расхождение выше 20% — повод проверить, не считаете ли вы только тело ответа и не теряете ли попытки, завершившиеся исключением до чтения тела.

Сколько времени занимает внедрение описанного подхода с нуля?

Для одного клиента с существующей обёрткой над HTTP: день на метрики и структуру логов, день на дашборд, день на правила оповещений и ещё две недели на наблюдение перед включением critical-алертов. Если у вас несколько клиентов на разных языках, основной срок уходит на согласование единого набора исходов и меток — здесь важно не торопиться, потому что переименовать метрику после года накопления истории очень больно.

Что делать, если данных от провайдера об операторе нет для части адресов?

Заводите значение unknown и следите за его долей. Если unknown больше 10–15% пула, ваши срезы по оператору теряют смысл — запросите у провайдера полные метаданные или разметьте адреса вручную один раз при добавлении в пул.

Заключение: одна минута вместо часа

Вернёмся к сцене из начала. «Прокси тормозят» — фраза, которая не содержит информации. Всё, что описано в этом руководстве, служит одной цели: превратить её в предложение с числами, срезом, временем и гипотезой. Четыре сигнала — доля успеха, задержка по перцентилям, доля ретраев, расход трафика — дают картину состояния пула. Структурированный лог с идентификатором запроса, идентификатором прокси, номером попытки, кодом, тремя задержками и размером — детали для расследования. Перцентили вместо среднего показывают хвост, где живут реальные проблемы. Разметка по стране, оператору и типу локализует деградацию до среза. Алерты с минимальным трафиком, двумя окнами и гистерезисом срабатывают редко и по делу. Дашборд-воронка отвечает на три вопроса за минуту.

С чего начать завтра? Первый шаг — обёртка над клиентом с единой классификацией исходов и одной функцией записи. Второй — гистограмма TTFB и счётчик попыток с метками по стране, оператору и типу. Третий — функция редактирования логов, через которую проходит всё без исключений. Четвёртый — дашборд из четырёх графиков и одной таблицы по срезам. Пятый — две недели наблюдения и только потом алерты. Через месяц вы посмотрите на график доли ретраев и, вполне возможно, удивитесь: пул работал заметно интенсивнее, чем вам казалось. А через квартал фраза «у нас всё тормозит» исчезнет из вашего чата — не потому, что проблем не станет, а потому, что каждую из них вы будете называть по имени.