Сайт вроде работает, но иногда — непонятно. Zabbix рисует красивые графики, где время ответа скачет до 5 секунд. Пинг в норме, DNS в норме. Что за чёрт?
Мы перебрали всё: ковыряли Zabbix, смотрели DNS, меняли хостинг (переехали на VPS), добавили для сравнения mail.ru и сайт на Тильде. И только когда написали свой маленький скрипт-монитор, картинка прояснилась. (пока еще не прояснилась, с утра проанализирую логи за сутки)
В этой статье расскажу:
Спойлер: проблема почти всегда не в вашем коде, а в очереди. Очереди PHP-воркеров на shared-хостинге или очереди дискового ввода-вывода на дешёвом VDS.
Когда вы смотрите на график времени ответа (TTFB, Time To First Byte), идеальная картинка — ровная линия на уровне 0.1–0.3 сек. А если график похож на пилу или гребёнку — это проблема.
Главный враг — очередь PHP-воркеров. Выделено, условно, 50 воркеров на всех. В час пик соседний Битрикс или магазин на OpenCart застревают на 5 секунд, занимают воркеры — и ваш запрос ждёт в очереди. При этом сам PHP-скрипт у вас отрабатывает за 0.2 сек, но пользователь видит 5 секунд ожидания.
Диагноз: TTFB скачет, время выполнения скрипта — в норме. Лечится переездом на VPS.
У вас свои воркеры, но диск — механический. Когда соседние VPS на том же физическом сервере начинают активно читать/писать (бэкапы, кроны, парсинг), возникает I/O Wait. Ваш PHP пытается прочитать файл или обратиться к БД — диск занят. Снова очередь, снова секундные пики.
Диагноз: TTFB скачет вместе с I/O Wait. Лечится переездом на VPS с NVMe.
Не потому, что сервер «настроили», а потому что у них:
Но нам до них далеко. Нам нужен простой и честный способ замерить TTFB без Zabbix, без лишних слоёв.
Задача простая: каждые N секунд ходим на сайт, замеряем time_starttransfer (это и есть TTFB), пишем в CSV. Потом анализируем.
#!/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 "---"
#!/bin/bash
while true; do
$HOME/site_monitor/site_monitor.sh
sleep 10
done
# Даём права на выполнение
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
Через несколько часов (или утром) у нас будет CSV-файл с колонками:
Вот скрипт анализа analyze.sh, который покажет:
#!/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
=== 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) ---
Вывод: в данный момент все серверы работают быстро. Но главный вопрос — появятся ли пики ночью или в час пик? Для этого мы и запускаем мониторинг на сутки.
Наш маленький скрипт даёт честную картину времени ответа, без прослоек вроде Zabbix, который иногда сам создаёт нагрузку. Он показал, что:
Рекомендация: возьмите скрипт, запустите на своём сервере на сутки, получите цифры и принимайте решение — менять хостинг, апгрейдить диск или оптимизировать код.
А если в процессе найдёте что-то интересное — поделитесь в комментариях.
P.S. Если лень заморачиваться со screen, можно запустить через cron раз в минуту. Но 10 секунд — оптимальный интервал, чтобы увидеть «гребёнку», а не сглаженное среднее.
P.P.S. Скрипты проверены на Debian/Ubuntu. Нужен установленный curl и screen (для фонового режима).
Комментарии
Пока нет комментариев. Будьте первым!