opened image

Глубокая диагностика через curl: от «одного запроса» до мини‑бенчмарка

В первой главе мы научились пользоваться curl как стетоскопом: один запрос — и сразу видно DNS, подключение, TLS, TTFB и общее время ответа.

 

Но в реальной жизни одного замера почти никогда недостаточно. Сегодня сайт может «стрельнуть» быстро один раз и провалиться на следующем. Нагрузка скачет, кэш прогревается, база иногда думает дольше. Поэтому во второй главе мы научимся:

  • прогонять много запросов подряд и считать среднее/проценты;

  • сравнивать IPv4 и IPv6;

  • стрелять в конкретный бэкенд за балансировщиком;

  • смотреть, как ведут себя редиректы и разные версии HTTP;

  • собирать из этого уже похожий на правду мини‑бенчмарк.

 

При этом я буду держать курс на то, чтобы даже начинающему админу было понятно, что и зачем делаем.

 

 

Мини‑бенчмарк: многократные замеры и p95

 

Один запуск curl — это фотография. Нам нужна маленькая «видеозапись» поведения сайта.

 

Сделаем простой bash‑скрипт, который:

  • много раз дергает URL;

  • печатает только time_total (полное время);

  • в конце считает среднее и p95 (95‑й процент).

 

Создадим файл, например curl-bench.sh:

#!/usr/bin/env bash

URL="$1"
COUNT="${2:-20}"  # default 20 requests

if [[ -z "$URL" ]]; then
  echo "Usage: $0 <url> [count]"
  exit 1
fi

# collect timings into an array
results=()

for i in $(seq 1 "$COUNT"); do
  t=$(curl -s -o /dev/null -w '%{time_total}' "$URL")
  results+=("$t")
  echo "#${i}: ${t}s"
done

# sorting and calculating statistics
sorted=$(printf '%s
' "${results[@]}" | sort -n)

sum=0
n=0

while read -r v; do
  sum=$(awk -v s="$sum" -v v="$v" 'BEGIN {printf "%.6f", s+v}')
  n=$((n+1))
  vals+=("$v")
done <<< "$sorted"

avg=$(awk -v s="$sum" -v n="$n" 'BEGIN {printf "%.4f", s/n}')

# p95 — element with index floor(0.95*n)
idx=$(awk -v n="$n" 'BEGIN {printf "%d", (n*0.95)}')
(( idx < 1 )) && idx=1

p95=${vals[$((idx-1))]}

echo "-------------------------"
echo "requests: $n"
echo "avg:      ${avg}s"
echo "p95:      ${p95}s"

 

Делаем исполняемым:

chmod +x curl-bench.sh

 

И запускаем:

./curl-bench.sh https://example.com/ 30

 

На выходе вы получите 30 строк вида #N: 0.423s и внизу краткую сводку:

-------------------------
requests: 30
avg:      0.4100s
p95:      0.6500s

 

Как это интерпретировать

  • avg (среднее) — средняя температура по больнице. Хорошо показывает общее состояние, но его может испортить один очень медленный запрос.

  • p95 — 95‑й процент. 95% запросов быстрее этого значения. Именно p95 ближе всего к тому, «как сайт ощущается» пользователям.

 

Если avg 0.3s, а p95 1.2s — иногда у вас происходят всплески задержки (скорее всего, база/очереди/соседняя нагрузка).

 

 

IPv4 vs IPv6: кто быстрее

 

Многие сайты уже отдают контент как по IPv4, так и по IPv6. Иногда один стек работает заметно медленнее другого.

Сравним:

# IPv4
curl -4 -s -o /dev/null -w @curl-format.txt https://example.com/

# IPv6
curl -6 -s -o /dev/null -w @curl-format.txt https://example.com/

 

На что смотрим:

  • если time_connect по IPv6 существенно больше — возможно, проблемы маршрутизации или фильтры по пути;

  • если time_namelookup сильно отличается — проверьте AAAA‑записи и настройки DNS;

  • если time_total растёт только по IPv6, а TTFB примерно одинаковый — подозрение на сеть (MTU, потери, асимметричный маршрут).

 

При необходимости можно прогнать наш curl-bench.sh отдельно с ключами -4 и -6, слегка доработав скрипт (добавить параметр для передачи этих опций).

 

 

Стреляем по конкретному бэкенду за балансировщиком

 

В продакшене у вас чаще всего не один сервер, а несколько бэкендов за балансировщиком. Иногда «тормозит не сайт, а конкретная нода».

--resolve: фиксируем IP для домена

 

Если вы знаете IP конкретного сервера, можно обойти DNS и балансировщик:

curl -s -o /dev/null -w @curl-format.txt \
  --resolve site.com:443:203.0.113.10 \
  https://site.com/

 

Что делает --resolve:

  • говорит curl: «для site.com:443 используй IP 203.0.113.10, не трогай DNS»;

  • при этом в HTTP‑запросе остаётся Host: site.com, то есть виртуальный хост на бэкенде работает как обычно.

 

Так можно сравнить тайминги разных бэкендов:

curl -s -o /dev/null -w @curl-format.txt \
  --resolve site.com:443:203.0.113.10 https://site.com/

curl -s -o /dev/null -w @curl-format.txt \
  --resolve site.com:443:203.0.113.11 https://site.com/

 

Если один сервер стабильно медленнее другого по TTFB — есть повод искать проблему именно на нём (нагрузка, медленный диск, локальные логи, кривые кэш‑правила и т.п.).

 

--connect-to: перекидываем трафик на другой хост/порт

 

Иногда нужен более гибкий вариант: изменить и хост, и порт назначения. Для этого есть --connect-to:

curl -s -o /dev/null -w @curl-format.txt \
  --connect-to site.com:443:10.0.0.5:8443 \
  https://site.com/

 

Смысл такой:

  • логически мы идём на https://site.com:443;

  • физически curl соединяется с 10.0.0.5:8443;

  • в заголовке Host остаётся site.com.

 

Этим удобно ходить на тестовые бэкенды, нестандартные порты, локальные dev‑окружения, не меняя URL и не трогая DNS.

 

 

HTTP/1.1, HTTP/2, HTTP/3: влияние протокола

 

Если ваш curl собран с поддержкой HTTP/2 и HTTP/3, можно посмотреть, как меняется время ответа при разных версиях протокола.

 

Примеры:

# Force HTTP/1.1
curl --http1.1 -s -o /dev/null -w @curl-format.txt https://site.com/

# HTTP/2
curl --http2 -s -o /dev/null -w @curl-format.txt https://site.com/

# HTTP/3 (if the server and curl support)
curl --http3 -s -o /dev/null -w @curl-format.txt https://site.com/

 

Что здесь важно понимать начинающему админу:

  • для одной простой страницы без кучи ресурсов разница может быть почти незаметна;

  • преимущество HTTP/2/3 особенно видно, когда много параллельных запросов (картинки, CSS, JS);

  • иногда HTTPS + HTTP/2 на старом железе или под специфичным прокси может, наоборот, ухудшать ситуацию.

 

Задача curl в этом контексте — дать вам «голое» время ответа по каждому протоколу.

 

 

Редиректы: декомпозируем цепочку

 

В первой главе мы включали опцию -L, чтобы curl следовал редиректам. Но как посмотреть, где именно в цепочке мы теряем время?  Одна из простых тактик — пройти цепочку по шагам вручную.

 

Сначала смотрим только заголовки (-I)

curl -I https://site.com/

 

Вы увидите что‑то вроде:

HTTP/2 301
location: https://www.site.com/
...

 

Дальше можно пройтись по каждому шагу отдельно:

curl -s -o /dev/null -w @curl-format.txt https://site.com/

curl -s -o /dev/null -w @curl-format.txt https://www.site.com/

 

Если первый шаг быстрый, а второй тормозит — проблема явно не в редиректе, а уже в конечной точке.

 

Автоматическое следование (-L) с отображением кода ответа

 

Можно добавить в формат вывода переменные %{url_effective} и %{http_code}, чтобы после всех редиректов увидеть итоговый URL и статус:

curl -s -L -o /dev/null -w 'code: %{http_code}\nurl: %{url_effective}\n' https://site.com/

 

Это не даст тайминги по каждому отдельному шагу, но покажет итоговую комбинацию «куда пришли» + «чем закончилось».

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

 

 

Измеряем разные этапы отдельно: TTFB vs размер ответа

 

Иногда важно разделить:

  • время, пока сервер думает;

  • время, пока контент докачивается.

Напомним ключевые моменты:

  • time_starttransfer — примерно TTFB, время до первого байта;

  • time_total — полное время до получения всего тела.

 

Если вы хотите прямо видеть разницу, добавьте в формат строку вроде:

Server processing (TTFB): %{time_starttransfer}s
Download tail:            %{time_total} - %{time_starttransfer}s

 

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

 

Пример грубого скрипта:

curl -s -o /dev/null -w '%{time_starttransfer} %{time_total}\n' https://site.com/ \
  | awk '{ttfb=$1; total=$2; tail=total-ttfb; \
         printf "TTFB: %.3fs\nTail: %.3fs\n", ttfb, tail}'

 

Если TTFB большой — смотрим в сторону бэкенда и базы. Если Tail странно большой — возможно, ответ очень тяжёлый или где‑то по пути проблемы с пропускной способностью.

 

 

Дополнительные заголовки и дебаг: что сервер ответил на самом деле

 

Иногда число секунд — это ещё не всё. Хотите понять, попали ли вы в кэш, в какой датацентр, на какой ноде оказались? Смотрим заголовки.

Пример:

curl -s -D - -o /dev/null https://site.com/

 

Опция -D - выводит заголовки ответа. Часто там есть служебные поля:

  • X-Cache: HIT/MISS — попали ли мы в кэш;

  • X-Served-By, X-Backend — имя бэкенда или датацентра;

  • Server-Timing — иногда там лежат внутренние тайминги приложения.

 

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

 

 

Мини‑итог 

 

Во второй главе мы:

  • превратили curl из разового выстрела в мини‑бенчмарк с несколькими запросами, средним и p95;

  • научились сравнивать IPv4 и IPv6;

  • научились стрелять в конкретный бэкенд за балансировщиком с помощью --resolve и --connect-to;

  • посмотрели, как влияет версия протокола (HTTP/1.1, HTTP/2, HTTP/3);

  • научились разбирать цепочки редиректов и отделять TTFB от времени докачки тела ответа;

  • увидели, как вытягивать заголовки для дополнительного контекста (кэш, датацентр, нода).

 

На практике всё это позволяет вам не просто говорить «сайт медленный», а чётко формулировать: «по IPv6 из Нидерландов p95 вырос до 900 мс, при этом TTFB нормальный, а хвост докачки большой — подозреваю ограничение полосы на конкретном узле или по пути». А пока можно уже использовать скрипт curl-bench.sh в повседневной жизни, чтобы перестать спорить «на глаз» и начать оперировать цифрами.