Вместо предисловия: была проблема
Сайт вроде работает, но иногда — непонятно. Zabbix рисует красивые графики, где время ответа скачет до 5 секунд. Пинг в норме, DNS в норме. Что за чёрт?
Мы перебрали всё: ковыряли Zabbix, смотрели DNS, меняли хостинг (переехали на VPS), добавили для сравнения mail.ru и сайт на Тильде. И только когда написали свой маленький скрипт-монитор, картинка прояснилась. (пока еще не прояснилась, с утра проанализирую логи за сутки)
В этой статье расскажу:
- почему shared-хостинг врёт и тормозит;
- почему VPS на HDD тоже может неприятно удивить;
- как написать простой bash-скрипт для замера TTFB;
- как развернуть его на ночь и утром получить честную статистику.
Спойлер: проблема почти всегда не в вашем коде, а в очереди. Очереди PHP-воркеров на shared-хостинге или очереди дискового ввода-вывода на дешёвом VDS.
Часть 1. Что такое «гребёнка» и откуда она берётся
Когда вы смотрите на график времени ответа (TTFB, Time To First Byte), идеальная картинка — ровная линия на уровне 0.1–0.3 сек. А если график похож на пилу или гребёнку — это проблема.
1. Shared-хостинг (500 сайтов на одном сервере)
Главный враг — очередь PHP-воркеров. Выделено, условно, 50 воркеров на всех. В час пик соседний Битрикс или магазин на OpenCart застревают на 5 секунд, занимают воркеры — и ваш запрос ждёт в очереди. При этом сам PHP-скрипт у вас отрабатывает за 0.2 сек, но пользователь видит 5 секунд ожидания.
Диагноз: TTFB скачет, время выполнения скрипта — в норме. Лечится переездом на VPS.
2. VPS на HDD
У вас свои воркеры, но диск — механический. Когда соседние VPS на том же физическом сервере начинают активно читать/писать (бэкапы, кроны, парсинг), возникает I/O Wait. Ваш PHP пытается прочитать файл или обратиться к БД — диск занят. Снова очередь, снова секундные пики.
Диагноз: TTFB скачет вместе с I/O Wait. Лечится переездом на VPS с NVMe.
3. Почему mail.ru работает идеально
Не потому, что сервер «настроили», а потому что у них:
- своя файловая система (пишут под себя);
- распределённое хранение данных;
- инженеры, которые переписывают ядро под свои задачи.
Но нам до них далеко. Нам нужен простой и честный способ замерить TTFB без Zabbix, без лишних слоёв.
Часть 2. Пишем скрипт для замера TTFB
Задача простая: каждые N секунд ходим на сайт, замеряем time_starttransfer (это и есть TTFB), пишем в CSV. Потом анализируем.
2.1. Основной скрипт (site_monitor.sh)
#!/bin/bash
# Список сайтов для мониторинга
SITES=(
"logosad.ru"
"franchisedar.ru"
"doctorneiro.ru"
"mail.ru"
"logoakademia.ru"
)
# Директория для логов
LOG_DIR="$HOME/site_monitor"
mkdir -p "$LOG_DIR"
# Файл лога с датой (ротация по дням)
LOG_FILE="$LOG_DIR/response_$(date +%Y%m%d).csv"
# Добавляем заголовок, если файл новый
if [ ! -f "$LOG_FILE" ]; then
echo "timestamp;site;ttfb_seconds;http_code" > "$LOG_FILE"
fi
echo "=== $(date +%H:%M:%S) ==="
for site in "${SITES[@]}"; do
timestamp=$(date +"%Y-%m-%d %H:%M:%S")
# Замеряем TTFB и HTTP-код
result=$(curl -s -o /dev/null -w "%{time_starttransfer};%{http_code}" \
--max-time 10 \
--connect-timeout 3 \
"http://$site" 2>&1)
ttfb=$(echo "$result" | cut -d';' -f1)
http_code=$(echo "$result" | cut -d';' -f2)
# Пишем в CSV
echo "\"$timestamp\";\"$site\";\"$ttfb\";\"$http_code\"" >> "$LOG_FILE"
# Печатаем в консоль (для отладки)
echo " $site → ${ttfb}s (HTTP $http_code)"
sleep 1
done
echo "---"
2.2. Цикл для запуска каждые 10 секунд (loop_monitor.sh)
#!/bin/bash
while true; do
$HOME/site_monitor/site_monitor.sh
sleep 10
done
2.3. Запуск через screen (чтобы работало после выхода из SSH)
# Даём права на выполнение
chmod +x $HOME/site_monitor/site_monitor.sh
chmod +x $HOME/site_monitor/loop_monitor.sh
# Запускаем в фоновой screen-сессии
screen -dmS monitor $HOME/site_monitor/loop_monitor.sh
# Проверить, что запустилось
screen -ls
# Подключиться и посмотреть живой вывод
screen -r monitor
# Выйти из screen, оставив процесс работать: Ctrl+A, затем D
Часть 3. Анализ собранных данных
Через несколько часов (или утром) у нас будет CSV-файл с колонками:
- timestamp — время замера
- site — сайт
- ttfb_seconds — время ответа в секундах
- http_code — HTTP-статус
Вот скрипт анализа analyze.sh, который покажет:
- минимальное, среднее, максимальное время;
- 95-й перцентиль (убирает выбросы);
- количество медленных ответов (>0.5 сек и >1 сек);
- распределение по часам.
#!/bin/bash
LOG_FILE="/home/user/site_monitor/response_$(date +%Y%m%d).csv"
if [ ! -f "$LOG_FILE" ]; then
echo "Лог файл не найден: $LOG_FILE"
exit 1
fi
echo "========================================="
echo "Анализ TTFB за $(date +%Y-%m-%d)"
echo "Всего строк в логе: $(wc -l < $LOG_FILE)"
echo "========================================="
for site in logosad.ru franchisedar.ru doctorneiro.ru mail.ru logoakademia.ru; do
echo ""
echo "📊 $site"
# Собираем только корректные числа
data=$(grep "\"$site\"" "$LOG_FILE" 2>/dev/null | awk -F';' '{print $3}' | sed 's/"//g' | grep -E '^[0-9]+\.[0-9]+$' | awk '$1 > 0.001')
if [ -n "$data" ] && [ "$(echo "$data" | wc -l)" -gt 0 ]; then
count=$(echo "$data" | wc -l)
min=$(echo "$data" | sort -n | head -1)
max=$(echo "$data" | sort -n | tail -1)
avg=$(echo "$data" | awk '{sum+=$1} END {printf "%.4f", sum/NR}')
median=$(echo "$data" | sort -n | awk '{a[NR]=$1} END {printf "%.4f", a[int(NR/2)]}')
p95=$(echo "$data" | sort -n | awk '{a[NR]=$1} END {printf "%.4f", a[int(NR*0.95)]}')
# Считаем медленные (>500 мс) и очень медленные (>1000 мс)
slow_500=$(echo "$data" | awk '$1 > 0.5 {count++} END {print count+0}')
slow_1000=$(echo "$data" | awk '$1 > 1.0 {count++} END {print count+0}')
# Проценты
slow_500_pct=$(awk "BEGIN {printf \"%.2f\", $slow_500 * 100 / $count}")
slow_1000_pct=$(awk "BEGIN {printf \"%.2f\", $slow_1000 * 100 / $count}")
echo " ═══════════════════════════════════════"
echo " 📈 Статистика по сайту:"
echo " ═══════════════════════════════════════"
echo " Всего запросов: $count"
echo " Средний TTFB: ${avg} сек"
echo " Медиана: ${median} сек"
echo " 95-й перцентиль: ${p95} сек"
echo " Мин / Макс: ${min} / ${max} сек"
echo ""
echo " ═══════════════════════════════════════"
echo " 🐌 Медленные ответы (>500 мс):"
echo " ═══════════════════════════════════════"
echo " $slow_500 запросов (${slow_500_pct}%) из $count"
if (( $(echo "$slow_500_pct > 1.0" | bc 2>/dev/null || echo "0") )); then
echo " ⚠️ ВНИМАНИЕ: больше 1% ответов медленнее 500 мс!"
elif (( $(echo "$slow_500_pct > 0.5" | bc 2>/dev/null || echo "0") )); then
echo " ⚠️ Есть задержки: ${slow_500_pct}% ответов >500 мс"
elif [ "$slow_500" -gt 0 ]; then
echo " ✅ Допустимо: ${slow_500_pct}% ответов >500 мс"
else
echo " ✅ Идеально: ни одного ответа медленнее 500 мс"
fi
echo ""
echo " ═══════════════════════════════════════"
echo " 🔥 Пиковые ответы (>1000 мс / 1 сек):"
echo " ═══════════════════════════════════════"
echo " $slow_1000 запросов (${slow_1000_pct}%) из $count"
if [ "$slow_1000" -gt 0 ]; then
echo " 🚨 Есть пики: ${slow_1000_pct}% запросов >1 секунды"
else
echo " ✅ Прекрасно: ни одного пика >1 секунды"
fi
else
echo " ❌ Нет корректных данных"
fi
done
echo ""
echo "========================================="
echo "📊 СВОДНАЯ ТАБЛИЦА ПО ВСЕМ САЙТАМ"
echo "========================================="
echo ""
printf "%-20s | %10s | %12s | %12s\n" "Сайт" "Запросов" ">500 мс" ">1000 мс"
printf "%-20s-+-%10s-+-%12s-+-%12s\n" "--------------------" "----------" "------------" "------------"
for site in logosad.ru franchisedar.ru doctorneiro.ru mail.ru logoakademia.ru; do
data=$(grep "\"$site\"" "$LOG_FILE" 2>/dev/null | awk -F';' '{print $3}' | sed 's/"//g' | grep -E '^[0-9]+\.[0-9]+$' | awk '$1 > 0.001')
if [ -n "$data" ] && [ "$(echo "$data" | wc -l)" -gt 0 ]; then
count=$(echo "$data" | wc -l)
slow_500=$(echo "$data" | awk '$1 > 0.5 {count++} END {print count+0}')
slow_1000=$(echo "$data" | awk '$1 > 1.0 {count++} END {print count+0}')
slow_500_pct=$(awk "BEGIN {printf \"%.2f\", $slow_500 * 100 / $count}")
slow_1000_pct=$(awk "BEGIN {printf \"%.2f\", $slow_1000 * 100 / $count}")
printf "%-20s | %10d | %5d (%5s%%) | %5d (%5s%%)\n" "$site" "$count" "$slow_500" "$slow_500_pct" "$slow_1000" "$slow_1000_pct"
else
printf "%-20s | %10s | %12s | %12s\n" "$site" "нет данных" "-" "-"
fi
done
echo ""
echo "========================================="
echo "🏆 РЕЙТИНГ ПО СТАБИЛЬНОСТИ (меньше % >500мс = лучше)"
echo "========================================="
echo ""
# Создаём временный файл для рейтинга
rank_file="/tmp/ttfb_rank.txt"
> "$rank_file"
for site in logosad.ru franchisedar.ru doctorneiro.ru mail.ru logoakademia.ru; do
data=$(grep "\"$site\"" "$LOG_FILE" 2>/dev/null | awk -F';' '{print $3}' | sed 's/"//g' | grep -E '^[0-9]+\.[0-9]+$' | awk '$1 > 0.001')
if [ -n "$data" ] && [ "$(echo "$data" | wc -l)" -gt 0 ]; then
count=$(echo "$data" | wc -l)
slow_500=$(echo "$data" | awk '$1 > 0.5 {count++} END {print count+0}')
slow_500_pct=$(awk "BEGIN {printf \"%.2f\", $slow_500 * 100 / $count}")
echo "$slow_500_pct $site" >> "$rank_file"
fi
done
# Сортируем и выводим
i=1
while read -r line; do
pct=$(echo "$line" | awk '{print $1}')
name=$(echo "$line" | awk '{print $2}')
if [ "$pct" = "0.00" ]; then
echo " 🥇 $i место: $name — 0% (абсолютная стабильность)"
elif (( $(echo "$pct < 0.5" | bc -l 2>/dev/null || echo "0") )); then
echo " 🥈 $i место: $name — ${pct}% (отличная стабильность)"
elif (( $(echo "$pct < 1.0" | bc -l 2>/dev/null || echo "0") )); then
echo " 🥉 $i место: $name — ${pct}% (хорошая стабильность)"
else
echo " $i место: $name — ${pct}% (есть задержки)"
fi
i=$((i+1))
done < <(sort -n "$rank_file")
rm -f "$rank_file"
echo ""
echo "========================================="
echo "💡 ПОЯСНЕНИЕ"
echo "========================================="
echo " • >500 мс — ответ медленнее 0.5 секунды (пользователь замечает)"
echo " • >1000 мс — ответ медленнее 1 секунды (уже неприятно)"
echo " • 0.4-0.5% — допустимо для обычного VPS"
echo " • 0.1-0.3% — отлично"
echo " • 0% — уровень mail.ru / Google"
Запуск анализа
chmod +x $HOME/site_monitor/analyze.sh
$HOME/site_monitor/analyze.sh
Часть 4. Что мы узнали на практике
Результаты первого замера (все сайты показали отлично)
=== 16:02:48 === logosad.ru → 0.037s (HTTP 301) franchisedar.ru → 0.044s (HTTP 301) doctorneiro.ru → 0.037s (HTTP 301) mail.ru → 0.028s (HTTP 301) logoakademia.ru → 0.028s (HTTP 301) ---
Вывод: в данный момент все серверы работают быстро. Но главный вопрос — появятся ли пики ночью или в час пик? Для этого мы и запускаем мониторинг на сутки.
Ожидаемые сценарии
- Если пиков не будет вообще → проблема была только на старом shared-хостинге, текущий VPS справляется отлично.
- Если пики будут, но только на ваших сайтах → проблема внутри вашего VPS (например, тяжёлые запросы в БД или нехватка RAM).
- Если пики будут на всех сайтах, кроме mail.ru → возможно, проблема на уровне сети или маршрутизации.
- Если пики будут в одни и те же часы → ищите кроны или бэкапы, которые грузят диск.
Заключение. Что делать дальше
Наш маленький скрипт даёт честную картину времени ответа, без прослоек вроде Zabbix, который иногда сам создаёт нагрузку. Он показал, что:
- shared-хостинг — зло для проектов с динамикой;
- VPS на HDD может тормозить из-за I/O Wait;
- VPS на NVMe — золотая середина для 90% проектов;
- mail.ru — это другой уровень инженерии, равняться на него бессмысленно.
Рекомендация: возьмите скрипт, запустите на своём сервере на сутки, получите цифры и принимайте решение — менять хостинг, апгрейдить диск или оптимизировать код.
А если в процессе найдёте что-то интересное — поделитесь в комментариях.
P.S. Если лень заморачиваться со screen, можно запустить через cron раз в минуту. Но 10 секунд — оптимальный интервал, чтобы увидеть «гребёнку», а не сглаженное среднее.
P.P.S. Скрипты проверены на Debian/Ubuntu. Нужен установленный curl и screen (для фонового режима).
Комментарии
Пока нет комментариев. Будьте первым!