Логи на нагруженном сервере растут быстрее, чем их успевает кто-то читать. Журнал доступа веб-сервера за сутки набирает миллионы строк, и вопрос "что именно происходило ночью" не решается прокруткой текста в редакторе. Нужен инструмент, который за один проход превратит сырой поток записей в компактную таблицу: самые активные адреса, распределение кодов ответа, трафик по часам, верхушка запрашиваемых страниц.
Awk стоит первым кандидатом на такую работу. Утилита входит в базовый состав почти любого дистрибутива Linux и умеет то, ради чего обычно поднимают отдельную систему аналитики: группировку по значению поля, счёт, суммы и средние прямо в потоке. Ниже разобраны механика инструмента и команды, которые копируют в терминал и запускают на реальных журналах без подготовки.
Построчное чтение журнала и разбор каждой строки на поля
Awk читает ввод построчно. К каждой строке применяется правило, заключённое в фигурные скобки, а сама строка автоматически режется на поля. Разделителем по умолчанию служит любой пробельный символ, включая табуляцию, причём подряд идущие пробелы считаются одним разделителем. К полям обращаются по номеру: $1 возвращает первое поле, $2 второе, а $0 выдаёт всю строку целиком.
Встроенные переменные экономят ручной счёт. NF хранит число полей в текущей строке, NR ведёт порядковый номер строки с начала ввода, а конструкция $NF выбирает последнее поле, когда его номер плавает от строки к строке. Типовая строка журнала доступа выглядит так:
192.0.2.15 - - [12/Feb/2025:13:05:01 +0300] "GET /api/v1/status HTTP/1.1" 200 512 "-" "Mozilla/5.0"
Кавычки не защищают содержимое: запрос "GET /api/v1/status HTTP/1.1" распадается на три поля, и нумерация съезжает. При разделении по пробелам код ответа оказывается девятым полем, объём ответа в байтах десятым, а час лежит внутри четвёртого. Номера полей верны, пока формат журнала не меняют, поэтому перед разбором его стоит зафиксировать. Базовые команды выделения:
awk '{print $1}' /var/log/nginx/access.log
awk '{print $NF}' /var/log/nginx/access.log
awk '{print NR, NF, $1, $9, $10}' /var/log/nginx/access.log
awk 'NF < 10 {next} {print $1, $9}' /var/log/nginx/access.log
Первая команда печатает адреса, вторая собирает последние поля строк, третья выводит номер строки, число полей и ключевые поля доступа, четвёртая пропускает обрезанные записи, в которых полей меньше десяти. Битые строки лучше отсекать до счёта, иначе они искажают любые итоги.
Ключ -F задаёт другой разделитель для журналов с необычной разметкой:
awk -F'\t' '{print $2, $4}' events.log
awk -F'|' '{print $3}' events.log
Первая команда читает файл с табуляцией, вторая работает со знаком | в роли разделителя.
Ассоциативные массивы awk для счёта записей, сумм и средних
Главная сила awk - ассоциативные массивы: индексом служит не число, а произвольная строка. Счётчик по значению поля пишется одной конструкцией:
awk '{c[$9]++} END {for (k in c) print k, c[k]}' /var/log/nginx/access.log
Команда проходит журнал, для каждой строки увеличивает счётчик с индексом, равным коду ответа, а в конце печатает накопленное. Результат - частотная таблица кодов: сколько раз сервер вернул 200, сколько 404, сколько 503. Тот же приём работает по любому измерению: адресам, путям, именам служб в системном журнале.
Массивы накапливают не только счётчики, но и суммы:
awk '{s[$1]+=$10} END {for (a in s) print a, s[a]}' /var/log/nginx/access.log
awk '{sum+=$10} END {print "всего байт:", sum}' /var/log/nginx/access.log
awk '{sum+=$10} END {print "средний ответ:", sum/NR}' /var/log/nginx/access.log
Первая команда собирает трафик по каждому адресу, вторая считает общий объём за файл, третья делит объём на число строк и получает средний размер ответа. Несколько метрик считаются за один проход: правила c[$9]++, s+=$10 и h[substr($4,2,14)]++ спокойно живут в одном блоке. Это и есть агрегация в чистом виде: миллион строк сжимается до пары десятков итогов.
Ограничение одно: массив живёт в памяти, и его размер равен числу уникальных ключей. Счёт по кодам ответа занимает десятки ячеек, счёт по адресам при огромной аудитории способен занять миллионы, и тогда выручает связка сортировки и подсчёта повторов из следующего раздела.
Блоки BEGIN и END для заголовков отчёта и итоговых строк
Скрипт awk выполняется в три этапа. Блок BEGIN срабатывает один раз до чтения первой строки: в нём задают разделитель, готовят переменные и печатают шапку отчёта. Основное правило выполняется на каждой строке. Блок END срабатывает после последней строки и выводит итоги. Минимальный отчёт с шапкой:
awk 'BEGIN {print "код", "число"} {c[$9]++} END {for (k in c) print k, c[k]}' /var/log/nginx/access.log
Для ровных колонок служит printf с явным форматом:
awk 'BEGIN {printf "%-6s %8s %12s\n", "код", "число", "среднее"}
{c[$9]++; s[$9]+=$10}
END {for (k in c) printf "%-6s %8d %12.0f\n", k, c[k], s[k]/c[k]}' /var/log/nginx/access.log
Формат %s печатает строку, %d целое число, %.0f округляет дробное до целого, а цифры перед буквами задают ширину колонок и выравнивание. Шапка, аккуратные столбцы и итоговая строка превращают вывод из простыни текста в отчёт, который не стыдно вставить в письмо коллеге. Полезная деталь напоследок: перебор for (k in c) не обещает порядка ключей, поэтому за awk ставят сортировку, если порядок в отчёте важен.
Классические конвейеры с sort и uniq для верхушки списка
Часто задача сводится к простому: взять одно поле, посчитать повторы, показать верхушку. Для этого существует каноническая связка из пяти команд:
awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -20
awk выделяет первое поле каждой строки, первый sort упорядочивает значения, uniq -c заменяет подряд идущие одинаковые строки одной строкой с числом повторов, sort -rn сортирует итог как числа по убыванию, head оставляет двадцать первых позиций. На выходе - список адресов с наибольшим числом запросов. Связка держится на одной детали: uniq считает повторы только у соседних строк, поэтому сортировка перед ним обязательна.
Ключ -n у второй сортировки включает сравнение чисел, а не текста: без него 9 вставало бы выше 100, ведь при алфавитном порядке сравниваются первые символы.
Той же связкой находят популярные страницы и проблемные адреса:
awk '{print $7}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -20
awk '$9 >= 400 {print $7}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -20
Первая команда строит топ путей, вторая сначала оставляет строки с кодами от 400 и показывает страницы, чаще других вернувшие отказ. Условие перед фигурными скобками - обычный приём awk: правило применяется только к строкам, прошедшим проверку. Для подсчёта уникальных значений без частот связка сокращается:
awk '{print $1}' /var/log/nginx/access.log | sort -u | wc -l
Команда отвечает на вопрос "сколько разных адресов обращалось к серверу за сутки".
Группировка событий по часам через срез метки времени
Для анализа динамики нужна не только частота, но и распределение по времени. В журнале доступа метка стоит в квадратных скобках и при разбиении по пробелам делится на два поля: дату с часом и смещение зоны. Срез substr($4, 2, 14) вырезает из четвёртого поля фрагмент от второго символа длиной четырнадцать знаков: день, месяц, год и час без минут и секунд. Счёт по часам пишется так:
awk '{c[substr($4,2,14)]++} END {for (h in c) print h, c[h]}' /var/log/nginx/access.log | sort
Каждая строка увеличивает счётчик своего часа, блок END печатает накопленное, sort выстраивает часы по возрастанию. Результат читается как готовый график: видно, в какой час подскочил трафик и когда сервер начал возвращать отказы. Срезы комбинируются с суммами и фильтрами:
awk '{h=substr($4,2,14); t[h]++; if ($9>=500) e[h]++}
END {for (k in t) print k, t[k], e[k]+0}' /var/log/nginx/access.log | sort
За один проход считаются общее число запросов и количество ответов с кодами от 500. Прибавка нуля в e[k]+0 нужна, чтобы часы без отказов печатались нулём, а не пустым местом. В системном журнале час лежит в начале третьего поля:
awk '{c[substr($3,1,2)]++} END {for (h in c) print h, c[h]}' /var/log/syslog | sort
Нюанс сортировки: часы от 00 до 23 упорядочиваются верно, а ключ с названием месяца сортируется по алфавиту, а не по календарю. Для длинных периодов ключ перегруппировывают, чтобы старшая часть времени стояла слева, или ограничивают отчёт одними сутками.
Скорость обработки на миллионах строк и подготовка к прогону
Awk работает в один проход: читает строку, обновляет счётчики, забывает её и берёт следующую. Файл целиком в памяти не лежит, память уходит только на массивы и переменные. На больших объёмах заметный выигрыш дают несколько практических приёмов.
Первый приём - считать все метрики одним запуском: десять прогонов по журналу дороже одного прогона с десятью счётчиками.
awk '{c[$9]++; s+=$10; h[substr($4,2,14)]++; if ($7=="/health") p++}
END {print "строк:", NR; print "байт:", s+0; print "health:", p+0}' /var/log/nginx/access.log
Второй приём - предварительная фильтрация. Когда нужны строки с конкретным признаком, их дешевле выбрать до awk: поиск фиксированной строки работает быстрее, чем разбор каждой строки на поля:
grep -F " 500 " /var/log/nginx/access.log | awk '{c[$1]++} END {for (a in c) print c[a], a}' | sort -rn | head -10
Ключ -F заставляет grep искать буквально, без регулярных выражений. Третий приём - нейтральная локаль. В локали с правилами естественного языка команды сравнивают строки медленнее, чем побайтово. Префикс LC_ALL=C переводит grep, sort и awk в побайтовый режим:
LC_ALL=C awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -20
Для чисел и адресов порядок остаётся прежним, а сортировка на больших объёмах работает быстрее. Четвёртый приём - архивы. Ротация журналов упаковывает старые файлы, и читать их можно без распаковки на диск:
zcat /var/log/nginx/access.log.*.gz | awk '{c[$9]++} END {for (k in c) print k, c[k]}' | sort -rn
Пятый приём - разбиение: файл режут командой split с ключом -l по числу строк, части считают отдельно и складывают промежуточные итоги. Для большинства задач хватает одного прохода.
Сценарии применения от разбора сбоя до регулярных отчётов
Однострочники awk окупаются в трёх типовых ситуациях: разовый разбор сбоя, регулярная отчётность и подготовка полей для следующих конвейеров.
При разборе сбоя порядок действий почти всегда одинаков:
- вырезать из журнала окно времени с началом проблемы, чтобы не гонять весь файл;
- оставить строки с кодами ответов от 400 и выше, чтобы не смешивать их с общим потоком;
- посчитать события по часам, адресам и путям за один проход;
- отсортировать каждый список по убыванию и оставить верхушку;
- сохранить итоги в файл и приложить их к материалам разбора.
Быстрая проверка масштаба выглядит так: сколько отказов вернул сервер за конкретный час и какие адреса их получили.
awk 'substr($4,2,14) == "12/Feb/2025:14" && $9 >= 500 {c[$1]++}
END {for (a in c) print c[a], a}' /var/log/nginx/access.log | sort -rn | head -10
Для регулярных отчётов команды ставят в планировщик cron и пишут итоги в файл:
7 6 * * * awk '{c[$9]++} END {for (k in c) print k, c[k]}' /var/log/nginx/access.log >> /root/reports/codes.txt
Строка запускает сводку по кодам ответа каждое утро и дописывает её в журнал отчётов. Такие команды лучше хранить в небольшом скрипте с комментариями: полгода спустя однострочник без пояснений читается хуже, чем абзац обычного текста.
Третий сценарий - извлечение полей для следующих конвейеров. awk отбирает нужный столбец и приводит его к удобному виду, дальше подключаются uniq, wc -l и sort -u.
awk -v OFS=';' '{print $1, $9, $10}' /var/log/nginx/access.log | head -100
awk -v path="/api" '$7 ~ path {print $1}' /var/log/nginx/access.log
Awk редко заменяет полноценные системы сбора статистики, но на сервере без лишнего софта он закрывает разовые вопросы за минуты и экономит часы возни с выгрузками. Проверенный набор однострочников стоит держать под рукой в личном файле заметок: при следующем наплыве ошибок не придётся собирать конвейер заново.