Анализ7 мин чтения

Что измерять в бенчмарке прокси

Сравнивайте прокси по успешности получения контента, времени ответа и поведению сессий с помощью Python-обвязки на curl, которая фиксирует сбои и считает результаты.

На этой странице

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

Определите успех до измерения скорости

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

Успеху нужно определение для каждого целевого сайта: ожидаемый статус, обязательное содержимое и приемлемый исход редиректа. Ответ HTTP 200 с CAPTCHA может этому определению не соответствовать. Отсутствующее поле или неполная загрузка — тоже. Простой текстовый маркер полезен для стартовой проверки; боевой нагрузке могут понадобиться разобранные записи, валидация по схеме или полный многошаговый поток.

Фиксируйте успешные ответы и каждую категорию сбоев по отдельности, затем разбирайтесь в причине. Таймаут может произойти на нескольких участках пути. Один лишь 403 не говорит, какой посредник или origin применил ограничение. Ни время ответа, ни статус недостаточны, чтобы назначить виновного.

Записывайте время до первого байта и общее время

curl сообщает временные вехи, измеренные от начала передачи. Время до первого байта (TTFB) показывает, когда начался ответ; общее время включает получение тела. Записывайте обе величины, потому что быстрый первый байт всё ещё может предшествовать медленной или неполной загрузке.

11 сентября 2026 года три последовательных прямых запроса к https://example.com/ с VPS в Хельсинки через curl 8.18 дали следующие значения. Прокси не использовался. Подписи ниже делают накопительный смысл явным:

code
http=200 tcp_complete=0.009780s tls_complete=0.023224s ttfb=0.044821s total=0.044983s
http=200 tcp_complete=0.008289s tls_complete=0.023851s ttfb=0.039902s total=0.039966s
http=200 tcp_complete=0.008149s tls_complete=0.031935s ttfb=0.061614s total=0.061688s

Это наблюдаемая база примерно 40–62 мс до первого байта, а не нижняя граница для будущих запросов. Три наблюдения не устанавливают устойчивого распределения. Соберите свежий базовый замер на той же машине и том же целевом сайте незадолго до сравнения прокси и сохраните дату прогона, версию клиента и конфигурацию.

Эта ограниченная по времени команда собирает те же поля для нового прямого запроса:

sh
curl --disable --silent --show-error --noproxy '*' \
  --connect-timeout 5 --max-time 20 --retry 0 \
  --output /dev/null \
  --write-out 'http=%{http_code} tcp_complete=%{time_connect}s tls_complete=%{time_appconnect}s ttfb=%{time_starttransfer}s total=%{time_total}s exit=%{exitcode}\n' \
  'https://example.com/'

Для того же запроса через HTTP(S)-прокси используйте curl 8.3 или новее. Задайте в PROXY_URL шлюз провайдера без учётных данных, включая поддерживаемую схему и порт. Пусть менеджер секретов передаст PROXY_USERNAME и PROXY_PASSWORD в окружении. Этот пример следует руководству по настройке curl: curl импортирует учётные данные сам, поэтому раскрытый пароль не становится аргументом оболочки. Не включайте трассировку оболочки.

sh
: "${PROXY_URL:?Set a credential-free HTTP(S) gateway URL}"
curl --disable --silent --show-error --noproxy '' \
  --proxy "$PROXY_URL" --proxy-basic \
  --variable %PROXY_USERNAME --variable %PROXY_PASSWORD \
  --expand-proxy-user '{{PROXY_USERNAME}}:{{PROXY_PASSWORD}}' \
  --connect-timeout 5 --max-time 20 --retry 0 \
  --output /dev/null \
  --write-out 'http=%{http_code} tunnel=%{http_connect} tcp_complete=%{time_connect}s tls_complete=%{time_appconnect}s ttfb=%{time_starttransfer}s total=%{time_total}s exit=%{exitcode}\n' \
  'https://example.com/'

--disable стоит первым, чтобы игнорировать личную конфигурацию curl. --noproxy '*' в прямой команде обходит унаследованные прокси; пустой список обхода в проксированной команде не даёт NO_PROXY исключить целевой сайт. Эти команды используют одно свежее соединение на процесс curl, не следуют редиректам и не делают повторов. При HTTP-ошибке код выхода curl всё равно может быть нулевым, потому что команды намеренно не содержат --fail; проверяйте HTTP-статус отдельно. О выборе маршрута см. руководство curl, о дедлайнах — руководство по таймаутам.

Понимайте, что входит в тайминги соединения

Для HTTPS-сайта через HTTP-прокси curl устанавливает соединение с прокси, запрашивает туннель CONNECT, затем согласовывает TLS с целевым сервером через этот туннель. CONNECT определён в RFC 9110.

Интервал между time_connect и time_appconnect включает установку туннеля и установление TLS. Вычитание соответствующего прямого интервала не изолирует накладные расходы CONNECT: условия маршрутизации и рукопожатия тоже меняются. HTTPS-прокси добавляет ещё одно TLS-рукопожатие. Статический объект в CDN эти различия не убирает.

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

%{http_connect} сообщает код ответа на HTTP CONNECT отдельно от HTTP-статуса передачи. --proxy-header отправляет заголовок прокси; статус туннеля он не сообщает. Предпочитайте выбранные поля статуса и тайминга подробным трассировкам, которые могут содержать данные аутентификации. Эти интерпретации следуют документированным полям тайминга curl.

Проверяйте ротацию по обещанной политике

Исходящий IP-адрес — это публичный адрес, который наблюдает целевой сайт. Обращайтесь к поддерживаемой провайдером диагностической конечной точке или к контролируемой вами конечной точке, которая сообщает адрес вызывающего. ipify — один из публичных вариантов; проверьте его актуальные правила использования, прежде чем автоматизировать запросы.

Сохраняйте последовательность результатов с метками времени, прежде чем сводить их к числу уникальных адресов. Для каждой попытки записывайте выбранный регион, настройку сессии, транспортный результат и валидный IPv4- или IPv6-адрес. Исключайте неудачные запросы и невалидные тела из подсчёта адресов, но сообщайте об этих сбоях отдельно. В Python ответ с адресом в виде простого текста можно проверить через ipaddress.ip_address(value.strip()).

Сравните эту последовательность с политикой ротации провайдера. Ротация может происходить на каждый запрос, соединение или интервал времени, и пул может переиспользовать адрес. Десять запросов не требуют десяти различных исходящих узлов, если продукт явно этого не обещает. Sticky-сессия призвана сохранять исходящий узел на документированный срок; проверьте её истечение и поведение при отключении, прежде чем считать переназначение неисправностью.

Используйте настройки страны или региона и сессии, которые нужны вашей нагрузке. Повторите тест на протяжении соответствующего окна сессии. Руководство по ротируемым и sticky-прокси объясняет, какие виды поведения оценивать. Бенчмарк содержимого ниже ротацию не измеряет.

Выберите выборку и окно наблюдения

Десять запросов могут выявить базовые сбои, но дают мало свидетельств о распределении задержек. Медленные запросы могут задерживать очередь; одно лишь среднее может скрывать медленные выбросы.

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

Сравнивая провайдеров, чередуйте их запросы в одном и том же окне. Это уменьшает различия, вызванные меняющейся нагрузкой на целевой сайт, не гарантируя идентичных условий. Держите содержимое запросов, настройки клиента и критерии успеха одинаковыми. Переиспользование соединений и боевую конкурентность тестируйте отдельно от последовательного примера со свежим процессом ниже.

Небольшой бенчмарк содержимого со сводкой

Сохраните это как proxy-bench.py и запустите python3 proxy-bench.py. Требуются Python 3 и curl 8.3+. Используются те же закрытые переменные окружения прокси, что и выше. Задайте BENCH_MODE=direct для прямого прогона или оставьте proxy. Каждый прогон создаёт новый каталог результатов и печатает его путь.

Целевой сайт по умолчанию — небольшая иллюстративная страница. Для реального сравнения задайте в BENCH_URL разрешённую HTTPS-конечную точку и замените BENCH_MARKER на содержательную ожидаемую строку. Не используйте URL с учётными данными или чувствительными параметрами. Адаптируйте проверку содержимого под реальный формат ответа, прежде чем ссылаться на успех на уровне приложения.

python
import collections
import datetime
import json
import math
import os
from pathlib import Path
import statistics
import subprocess
import tempfile
import time

url = os.environ.get("BENCH_URL", "https://example.com/")
marker = os.environ.get("BENCH_MARKER", "<h1>Example Domain</h1>").encode()
mode = os.environ.get("BENCH_MODE", "proxy")
if not url.startswith("https://") or not marker or mode not in {"direct", "proxy"}:
    raise SystemExit("Use an HTTPS URL, a nonempty marker and direct or proxy mode")
route = ["--noproxy", "*"]
if mode == "proxy":
    route = ["--noproxy", "", "--proxy", os.environ["PROXY_URL"], "--proxy-basic",
             "--variable", "%PROXY_USERNAME", "--variable", "%PROXY_PASSWORD",
             "--expand-proxy-user", "{{PROXY_USERNAME}}:{{PROXY_PASSWORD}}"]
fields = ["http", "tunnel", "tcp_complete", "tls_complete", "ttfb", "total"]
write_out = "%{http_code} %{http_connect} %{time_connect} %{time_appconnect} %{time_starttransfer} %{time_total}"
count, window = 200, 3600.0
directory = Path(tempfile.mkdtemp(prefix="proxy-bench-", dir="."))
rows = []
started = time.monotonic()
with (directory / "results.jsonl").open("x") as log:
    for i in range(count):
        due = started + i * window / (count - 1)
        time.sleep(max(0, due - time.monotonic()))
        timestamp = datetime.datetime.now(datetime.timezone.utc).isoformat()
        with tempfile.TemporaryDirectory() as scratch:
            body = Path(scratch) / "body"
            result = subprocess.run(
                ["curl", "--disable", "--silent", "--connect-timeout", "5",
                 "--max-time", "20", "--retry", "0", *route,
                 "--output", str(body), "--write-out", write_out, "--url", url],
                capture_output=True, text=True)
            values = result.stdout.split()
            timings = dict(zip(fields, values)) if len(values) == len(fields) else {}
            if result.returncode:
                outcome = f"curl_{result.returncode}"
            elif not timings:
                outcome = "missing_measurements"
            elif timings["http"] != "200":
                outcome = "http_" + timings["http"]
            elif not body.exists() or marker not in body.read_bytes():
                outcome = "content_mismatch"
            else:
                outcome = "success"
        row = dict(attempt=i + 1, timestamp=timestamp, mode=mode,
                   exit_code=result.returncode, outcome=outcome, **timings)
        rows.append(row)
        log.write(json.dumps(row) + "\n")
        log.flush()

successful = [row for row in rows if row["outcome"] == "success"]
summary = {"attempts": len(rows), "successes": len(successful),
           "success_rate": len(successful) / len(rows),
           "outcomes": dict(collections.Counter(row["outcome"] for row in rows))}
for metric in ("ttfb", "total"):
    samples = sorted(float(row[metric]) for row in successful)
    summary[metric] = ({"median_seconds": statistics.median(samples),
                        "p95_seconds": samples[math.ceil(0.95 * len(samples)) - 1]}
                       if samples else None)
(directory / "summary.json").write_text(json.dumps(summary, indent=2) + "\n")
print(directory)
print(json.dumps(summary, indent=2))

Первый запрос стартует сразу; 200-й запланирован через 3 600 секунд после первого. Запросы остаются последовательными. Если запрос занимает больше, чем интервал между ними, последующие старты сдвигаются, и прогон занимает больше времени. Фактические метки времени старта сохраняются в логе. Автоматических повторов и следования редиректам нет, поэтому ответ 3xx в этом примере — HTTP-сбой. Если вашей нагрузке нужны редиректы, определите, какие целевые адреса и итоговое содержимое приемлемы, прежде чем расширять обвязку.

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

Знаменатель доли успеха — все предпринятые запросы, включая таймауты и сбои содержимого. Коды выхода curl, такие как 28 (таймаут), 7 (сбой соединения) и 56 (сбой приёма), описывают широкие категории сбоев, а не подтверждённый неисправный компонент. При расследовании пользуйтесь официальным справочником кодов выхода.

Статистика задержек использует только успешные ответы, а сводка держит рядом с ней число сбоев. Медиана — среднее по положению значение, при необходимости усредняющее среднюю пару. 95-й процентиль использует метод ближайшего ранга: отсортируйте успешные образцы и выберите ранг ceil(0.95 × sample_count), считая с единицы. Без успехов обе сводки задержек равны null.

Например, четыре успеха из пяти попыток дают долю успеха 80%. Если их значения TTFB равны 0,10, 0,12, 0,20 и 0,40 секунды, медиана — 0,16 секунды, а p95 по ближайшему рангу — 0,40 секунды. Это выдуманные значения для объяснения расчёта, а не измеренные результаты прокси.

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

Источники

  1. curl manual: --write-out variables and proxy configuration
  2. everything curl: exit codes
  3. RFC 9110: HTTP Semantics, section 9.3.6 CONNECT
  4. ipify: a simple public IP address API

Теги:ProxiesChoosing proxiesTroubleshooting