Узкое место файлового архива вычисляется за один замер, если заранее снять baseline по пяти величинам: пропускная способность отдачи, время отклика (p50 и p95), загрузка диска (await и %util), сетевой throughput, число одновременных соединений. Baseline на здоровом архиве становится точкой отсчёта. Отклонение больше 10% после смены конфига или деплоя сразу показывает, какой ресурс просел.
Типовой архив в 2026 году устроен так: Nginx 1.26 или новее отдаёт статику, файлы лежат на локальном NVMe или на сетевом хранилище, тяжёлое сжатие выполняется на этапе сборки, TLS завершается на том же узле. Prometheus 2.x с node_exporter 1.x собирает метрики с хоста, nginx-prometheus-exporter отдаёт статистику соединений. ОС: Linux 5.x или 6.x с systemd.
Настройка производительности не должна открывать доступ к служебным файлам. Держите autoindex выключенным, блокируйте .env и .git, проверяйте конфиг через nginx -t перед каждым reload.
Почему файловый архив замедляется: базовые точки контроля
Архив тормозит по четырём причинам, и все они измеряются стандартными утилитами Linux. Ниже отправные точки, с которых начинаем диагностику.
Четыре ресурса, которые чаще всего становятся bottleneck
| Ресурс | Симптом | Команда первичной проверки | Порог внимания |
|---|---|---|---|
| Диск | растёт await, утилизация близка к пределу | iostat -x 1 | await больше 20 мс, %util больше 80% |
| Сеть | throughput упирается в канал, появляются ретрансмиты | sar -n DEV 1, ss -s | канал занят больше чем на 70% |
| CPU | растут %sys и %softirq, nginx греет gzip | mpstat -P ALL 1, top | больше 70% на ядро |
| Память и дескрипторы | 502 и 503, в логе too many open files | ulimit -n, cat /proc/sys/fs/file-nr | больше 80% от лимита |
Для файлового архива первым подозреваемым выступает диск или сеть. CPU выходит на первый план, только если сжатие делается на лету. При отдаче предсжатых файлов он почти не участвует, а нагрузку даёт разве что TLS-рукопожатие при большом числе новых соединений.
Что считать нормой для файлового архива
| Метрика | Ориентир | Когда это проблема |
|---|---|---|
| Время отклика статики | меньше 100 мс в LAN, меньше 300 мс через WAN | p95 выше 500 мс |
| Пропускная способность файлового сервера | не ниже 70% от полосы канала | падение ниже 50% от baseline |
| await на чтение | 10-20 мс для SSD, 20-40 мс для HDD | стабильно выше 30 мс |
| %util диска | меньше 70% | больше 85% в течение 10 минут |
| Одновременные соединения | не выше worker_connections x worker_processes с запасом 20-30% | CurrEstab подошёл к лимиту |
Эти цифры задают отправную точку, а не абсолют. Норма зависит от железа, файловой системы и профиля трафика, поэтому baseline снимается на вашем стенде или на проде в спокойные часы. Пошаговая база по раздаче файлов, MIME-типам и заголовкам собрана в статье про раздачу статического контента через Nginx.
Ключевые метрики файлового архива и как их снимать
Пять групп метрик закрывают почти все инциденты: пропускная способность, время отклика, нагрузка на диск, нагрузка на сеть и число одновременных соединений. Снимать их можно как утилитами командной строки, так и через node_exporter.
Пропускная способность и время отклика
Скорость отдачи одного файла и задержку измеряет curl. Флаг -w выводит обе величины сразу, включая время до первого байта.
curl -o /dev/null -s -w 'ttfb=%{time_starttransfer} time_total=%{time_total} speed=%{speed_download}\n' \
https://archive.example/file.tar.gz
Нагрузочный тест на реальном профиле трафика даёт wrk, а для простых сценариев хватает ab.
wrk -t4 -c100 -d30s https://archive.example/file.tar.gz ab -n 1000 -c 100 https://archive.example/file.tar.gz
Разложить задержку по этапам помогает расширенный формат лога Nginx. $request_time показывает полное время обработки запроса, $upstream_response_time нужен, если часть файлов приходит от бэкенда.
log_format archive '$remote_addr $request $status '
'request_time=$request_time '
'upstream_time=$upstream_response_time '
'bytes=$bytes_sent';
В node_exporter пропускную способность дают счётчики node_network_receive_bytes_total и node_network_transmit_bytes_total, а также node_disk_read_bytes_total для чтения с диска. Как отличить медленный диск от медленной сети: если speed_download низкий, а iostat показывает await в норме и %util низкий, упор в канал или в клиента. Если await высокий и растёт очередь чтения, виноват диск.
Нагрузка на диск: IOPS, await, %util
Для архива важны чтение и задержка чтения, запись почти не участвует. Смотрим r/s, rkB/s, await и %util.
iostat -x 1 iotop -o pidstat -d 1
Если %util держится выше 80% и await растёт, диск упирается в IOPS или в полосу. Показатель rkB/s на пределе паспортного значения подтверждает это. В node_exporter те же данные лежат в node_disk_read_bytes_total, node_disk_reads_completed_total и node_disk_read_time_seconds_total, а утилизация считается из node_disk_io_time_seconds_total.
Нагрузка на сеть и число одновременных соединений
Сетевую полосу и ошибки показывает sar, состояние сокетов берём из ss.
sar -n DEV 1 sar -n EDEV 1 ss -s ss -lnt
Внутреннюю статистику Nginx отдаёт stub_status. Директиву включаем только для localhost или внутренней подсети.
location /nginx_status {
stub_status;
allow 127.0.0.1;
deny all;
}
curl -s http://127.0.0.1/nginx_status
Вывод содержит Active connections, Reading, Writing и Waiting. Рост Waiting означает, что соединения ждут ответа и держат keepalive-слоты: помогает увеличение worker_connections, open_file_cache или уменьшение времени ответа. node_exporter отдаёт node_netstat_Tcp_CurrEstab и node_sockstat_TCP_inuse, по ним удобно строить график занятости слотов.
Настройка мониторинга: Prometheus и node_exporter для файлового архива
Связка Prometheus и node_exporter даёт историю метрик и алерты раньше, чем приходят жалобы. Настройка занимает около получаса и не требует правок приложения.
Установка и настройка node_exporter
Распакуйте архив релиза node_exporter 1.x, положите бинарник в /usr/local/bin и создайте отдельного пользователя без прав на запись в архив.
useradd --no-create-home --shell /usr/sbin/nologin node_exporter install -o node_exporter -g node_exporter -m 0755 node_exporter /usr/local/bin/node_exporter
Юнит systemd с нужными коллекторами выглядит так.
[Unit] Description=Node Exporter After=network.target [Service] User=node_exporter Group=node_exporter Type=simple ExecStart=/usr/local/bin/node_exporter --collector.diskstats --collector.netdev --collector.sockstat --collector.filesystem [Install] WantedBy=multi-user.target
systemctl daemon-reload systemctl enable --now node_exporter curl -s http://localhost:9100/metrics | grep -c '^node_disk'
Порт 9100 наружу не открываем: метрики содержат имена устройств, точки монтирования и топологию хоста. Доступ даём из внутренней сети или через reverse proxy с аутентификацией и TLS.
prometheus.yml: scrape_configs для архива
Подключаем хост архива и экспортер Nginx отдельными заданиями.
scrape_configs:
- job_name: file_archive
scrape_interval: 15s
static_configs:
- targets: ['archive-01:9100']
- job_name: nginx
scrape_interval: 15s
static_configs:
- targets: ['archive-01:9113']
promtool check config prometheus.yml
Интервал 15 секунд подходит для файлового архива: пики отдачи заметны и без перегрузки хранилища метрик. Переходите на 5 секунд, если разбираете короткие всплески или тестируете изменения и нужно видеть результат сразу. Базовые PromQL-запросы для панелей Grafana:
rate(node_network_transmit_bytes_total{instance="archive-01:9100",device="eth0"}[5m]) * 8
rate(node_disk_read_bytes_total{device="nvme0n1"}[5m])
rate(node_disk_read_time_seconds_total{device="nvme0n1"}[5m]) / rate(node_disk_reads_completed_total{device="nvme0n1"}[5m])
node_netstat_Tcp_CurrEstab
Первый запрос переводит байты в биты и показывает занятость канала. Третий считает среднее await на чтение, это самая полезная панель при разборе дисковых проблем.
Правила алертов: что и когда срабатывает
Ниже набор правил, который закрывает деградацию до появления жалоб пользователей. Для LowThroughput подставьте свой baseline в битах в секунду вместо многоточия.
groups:
- name: file_archive
rules:
- alert: HighDiskAwait
expr: rate(node_disk_read_time_seconds_total[5m]) / rate(node_disk_reads_completed_total[5m]) > 0.03
for: 5m
labels:
severity: warning
annotations:
summary: "Задержка чтения на {{ $labels.device }} выше 30 мс"
- alert: HighDiskUtil
expr: rate(node_disk_io_time_seconds_total[10m]) > 0.85
for: 10m
labels:
severity: warning
- alert: LowThroughput
expr: rate(node_network_transmit_bytes_total{device="eth0"}[10m]) < ...
for: 10m
labels:
severity: warning
- alert: HighConnections
expr: node_netstat_Tcp_CurrEstab > 26000
for: 5m
labels:
severity: warning
- alert: HighLatency
expr: histogram_quantile(0.95, sum(rate(nginx_http_request_duration_seconds_bucket[5m])) by (le)) > 0.5
for: 5m
labels:
severity: critical
Порог HighConnections считайте от своих значений: для worker_processes auto на 8 ядрах и worker_connections 8192 запас заканчивается в районе 26 тысяч установленных соединений. Alertmanager маршрутизирует срабатывания в Telegram и Slack, группировка по alertname убирает шторм сообщений при массовой деградации.
Оптимизация: кэширование, сжатие, CDN и тюнинг Nginx
Четыре направления дают основной прирост: клиентский и прокси-кэш, предсжатые файлы, CDN для удалённой аудитории и настройка самого Nginx под статику. Меняем по одному пункту и после каждого снимаем замер, иначе эффект не отделить от шума.
Кэширование: Cache-Control, ETag, immutable
Файл с версией в имени не меняется никогда, поэтому ему выдаём длинный max-age и immutable. Такой ответ запрещает браузеру перепроверять файл даже при перезагрузке страницы.
location ~* \.(tar\.gz|zip|deb|rpm|iso)$ {
add_header Cache-Control "public, max-age=2592000, immutable";
access_log off;
}
location ~* \.html$ {
add_header Cache-Control "public, max-age=3600, must-revalidate";
}
curl -I https://archive.example/file-2.4.1.tar.gz | grep -i cache-control
Директива expires сама формирует Cache-Control с одним лишь max-age, поэтому для immutable надёжнее задать заголовок явно. Файлам без версии в имени immutable не выдаём: клиенты перестанут видеть обновления. Подробная матрица заголовков, ETag, Last-Modified и версионирования разобрана в материале про кэширование статики: gzip, brotli, Cache-Control и ETag.
Сжатие файлов на сервере: gzip, brotli, precompressed
Сжатие файлов на сервере имеет смысл только для текста: HTML-гайды, JSON-метаданные, индексные страницы, CSS-описания. Для tar.gz, zip и образов повторное сжатие бесполезно, а CPU съедает.
gzip on; gzip_comp_level 5; gzip_min_length 1024; gzip_vary on; gzip_types text/plain text/css application/json application/javascript text/xml application/xml image/svg+xml; gzip_static on; brotli_static on;
Директивы brotli требует модуль ngx_brotli; если он собран, добавляем brotli on с уровнем 5-6. Уровни выше 6 заметно греют CPU и дают выигрыш в доли процента. Предсжатые версии файлов через gzip_static и brotli_static снимают нагрузку полностью: Nginx отдаёт готовый .br, не тратя ядра на компрессию.
curl -H 'Accept-Encoding: br' -I https://archive.example/guide.html | grep -i content-encoding mpstat -P ALL 1
Замер mpstat до и после перехода на предсжатые файлы показывает, сколько ядер освободилось. На текстовой нагрузке разница обычно кратная, на архивах её не будет вовсе.
CDN для файлового архива
CDN оправдан при географически распределённой аудитории, крупных файлах и пиковых нагрузках: origin перестаёт быть единственной точкой отдачи. Схема простая: origin pull, длинный TTL для версионированных архивов, сброс кэша по смене версии в имени.
curl -I https://archive.example/file-2.4.1.tar.gz | grep -iE 'x-cache|age|cf-cache-status'
Заголовки X-Cache, Age и CF-Cache-Status показывают попадание в кэш. Приватные файлы с токенами доступа через публичный CDN не отдаём, иначе ссылка утечёт через чужой edge-узел. Таймауты и работу с крупными файлами удобно сверить со статьёй про тонкую настройку Nginx для больших файлов.
Тюнинг Nginx под статику
Параметры ниже рассчитаны на отдачу статики с локального NVMe.
worker_processes auto; worker_rlimit_nofile 65535; worker_connections 8192; multi_accept on; sendfile on; tcp_nopush on; tcp_nodelay on; keepalive_timeout 65; keepalive_requests 1000; open_file_cache max=10000 inactive=30s; open_file_cache_valid 60s; open_file_cache_min_uses 2; open_file_cache_errors on; aio threads; directio 8m; output_buffers 4 256k;
worker_rlimit_nofile поднимает лимит дескрипторов на процесс, worker_connections задаёт потолок соединений. sendfile и tcp_nopush отдают файл без копирования в пользовательское пространство и собирают заголовки в один пакет, tcp_nodelay убирает задержку на мелких ответах. open_file_cache держит дескрипторы и метаданные горячих файлов. Пара aio threads и directio 8m отправляет файлы крупнее 8 МБ через асинхронный ввод-вывод мимо страничного кэша, чтобы один большой архив не вытеснял метаданные остальных.
nginx -t systemctl reload nginx wrk -t4 -c200 -d30s https://archive.example/file.tar.gz
aio и directio тестируйте на своём железе и файловой системе: на части конфигураций эти параметры дают регресс. Развёрнутый набор параметров ядра и Nginx для 10k-50k RPS есть в гайде по тюнингу Nginx для высоких нагрузок.
Типовые узкие места: примеры с диагностикой и исправлением
Ниже четыре сценария, которые закрывают большинство инцидентов с файловым архивом. Логика одна: сначала подтверждаем причину замером, потом меняем конфиг, потом сравниваем с baseline.
| Симптом | Причина | Действие |
|---|---|---|
| await выше 30 мс, %util выше 90% | диск на пределе IOPS | NVMe, open_file_cache, readahead, RAID |
| Throughput на пределе канала, ретрансмиты | не хватает полосы | CDN, сжатие текста, limit_rate |
| Высокий %sys, nginx в топе по CPU | сжатие на лету | gzip_static, brotli_static, уровень 5 |
| 502 и 503, ошибки в error.log | лимиты соединений и дескрипторов | worker_connections, worker_rlimit_nofile, sysctl |
Кейс 1: диск, высокий await и %util
Симптом: iostat показывает await больше 30 мс, %util выше 90%, rkB/s упирается в паспортную полосу накопителя.
iostat -x 1 iotop -o pidstat -d 1 lsblk -o NAME,ROTA,SIZE
Если чтение на пределе, вариантов два: поднять пропускную способность носителя или сократить число обращений к нему. Перенос архива на NVMe и включение open_file_cache закрывает большую часть случаев. Для последовательного чтения крупных файлов помогает увеличение readahead (значение в 512-байтных секторах, то есть 8192 - это 4 МБ).
blockdev --setra 8192 /dev/nvme0n1
Проверка результата: повторный iostat под нагрузкой и прогон wrk, сравнение rkB/s и await с baseline.
Кейс 2: сеть, канал забит и есть ретрансмиты
Симптом: sar показывает throughput на пределе канала, ss -s и ss -ti обнаруживают повторы передач и растущие очереди.
sar -n DEV 1 sar -n EDEV 1 ss -ti
Первое действие - снизить объём передаваемых данных: включить сжатие текстовых форматов и предсжатые файлы. Второе - разгрузить origin через CDN. Третье - ограничить скорость для некритичных клиентов, чтобы один качающий не съедал полосу у остальных.
location /downloads/ {
limit_rate_after 10m;
limit_rate 5m;
}
Проверка: sar -n DEV 1 после изменения и curl -w speed_download с внешней точки. Ограничение действует на соединение, поэтому его подбирают по реальному распределению клиентов.
Кейс 3: CPU просел на сжатии
Симптом: mpstat показывает высокий %sys и %softirq, top выводит nginx поверх остальных процессов, время ответа растёт при неизменной нагрузке.
mpstat -P ALL 1 perf top
Исправление: перейти на предсжатые файлы (gzip_static, brotli_static), снизить brotli до уровня 5, отключить сжатие для уже сжатых форматов. Проверка: mpstat до и после, а также curl с заголовком Accept-Encoding: br, чтобы убедиться, что brotli действительно отдаётся.
Кейс 4: лимиты соединений и файловых дескрипторов
Симптом: в nginx error.log появляются записи worker_connections are not enough и too many open files, клиенты получают 502 и 503.
ss -s cat /proc/sys/fs/file-nr ulimit -n sysctl net.core.somaxconn net.ipv4.tcp_max_syn_backlog
Поднимаем лимиты на уровне Nginx и ядра. Lимит worker_connections считает и клиентские, и проксируемые соединения, поэтому запас в 20-30% обязателен.
fs.file-max = 200000 net.core.somaxconn = 65535 net.ipv4.tcp_max_syn_backlog = 65535 net.ipv4.ip_local_port_range = 10240 65000 net.ipv4.tcp_fin_timeout = 15
sysctl -p nginx -t systemctl reload nginx ss -lnt
Проверка: ab -n 5000 -c 500 без ошибок соединения и чистый error.log. Если 502 остаются, смотрите в сторону бэкенда, а не Nginx.
Безопасность при оптимизации: что нельзя ломать
Ускорение не оправдывает открытый доступ к служебным файлам. autoindex держите выключенным: листинг каталога раскрывает структуру архива целиком.
location ~ /\. {
deny all;
access_log off;
log_not_found off;
}
location ~* \.(env|git|htaccess|sql|bak)$ {
deny all;
}
Права на файлы 644, на каталоги 755, владелец - пользователь без прав на запись в каталоги с контентом. Immutable-кэш выдаём только файлам с хешем или версией в имени. CDN не используем для приватных материалов. Для HTML-гайдов добавляем базовые заголовки безопасности.
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; add_header Content-Security-Policy "default-src 'self'; img-src 'self' data:" always; add_header X-Content-Type-Options "nosniff" always;
curl -I https://archive.example/.env
Ответ должен быть 403 или 404, а не содержимое файла. Этот тест стоит прогонять после каждой правки server-блока.
Чек-лист регулярной проверки производительности
Регулярные проверки удерживают архив быстрым без ручного контроля. Ниже распределение по периодичности с командами и ожидаемым результатом.
Ежедневные, еженедельные и ежемесячные проверки
| Периодичность | Что проверяем | Команда | Ожидаемый результат |
|---|---|---|---|
| Ежедневно | горящие алерты и ошибки в логе | tail -n 200 error.log | grep -E '502|503|too many' | пустой вывод |
| Ежедневно | состояние соединений | curl -s http://127.0.0.1/nginx_status | Waiting без устойчивого роста |
| Еженедельно | диск и сеть против baseline | iostat -x 1, sar -n DEV 1, ss -s | отклонение меньше 10% |
| Ежемесячно | нагрузочный тест и валидация конфига | wrk -t4 -c200 -d60s, nginx -t | p95 в пределах SLO |
| Ежеквартально | SLO, кэш-политики, CDN | пересмотр правил и TTL | актуальные пороги и TTL |
Как зафиксировать baseline и отслеживать регресс
Baseline снимается на здоровом архиве и хранится рядом с конфигами. Минимальный набор: пропускная способность отдачи, p50 и p95 времени отклика, средний await на чтение, %util, CurrEstab. Замеры удобно писать в CSV и складывать в тот же Grafana-дашборд.
wrk -t4 -c200 -d60s --latency https://archive.example/file.tar.gz | tee baseline-$(date +%F).txt
curl -o /dev/null -s -w '%{time_starttransfer},%{time_total},%{speed_download}\n' https://archive.example/file.tar.gz >> latency.csv
После каждого изменения прогоняем тот же набор и сравниваем. Регресс больше 10% - повод откатить правку до разбора причин. Baseline устаревает при смене железа, версии Nginx или профиля трафика, тогда его снимают заново.
Перед любой правкой сохраняйте конфиг: cp nginx.conf nginx.conf.bak-$(date +%F), затем nginx -t и только после успешной проверки systemctl reload nginx. Такой порядок оставляет рабочий архив на связи даже при ошибке в директиве, а мониторинг покажет, принесла ли правка ожидаемый прирост.