Jump to content
View in the app

A better way to browse. Learn more.

T.M.I IThub

A full-screen app on your home screen with push notifications, badges and more.

To install this app on iOS and iPadOS
  1. Tap the Share icon in Safari
  2. Scroll the menu and tap Add to Home Screen.
  3. Tap Add in the top-right corner.
To install this app on Android
  1. Tap the 3-dot menu (⋮) in the top-right corner of the browser.
  2. Tap Add to Home screen or Install app.
  3. Confirm by tapping Install.

С чего начинается тюнинг

Nginx в дефолтной конфигурации — это как спортивный автомобиль с заводскими настройками для езды по бездорожью: едет, но не так быстро, как мог бы. Хорошая новость: большинство важных оптимизаций достигается правкой конфига, а не покупкой более мощного железа.

Плохая новость: многие "гайды по тюнингу" в интернете — копипаста десятилетней давности, без понимания что и зачем. Параметры для Nginx 1.8 на 2-ядерном сервере копируют на 32-ядерный продакшен под highload — и удивляются что не помогает или становится хуже.

Этот материал — о том, как думать о тюнинге Nginx: что делает каждый параметр, какие компромиссы он несёт, и как проверить что оптимизация действительно работает.

Версии в статье: Nginx 1.24+ / 1.25+ (mainline). Большинство конфигов работают с 1.18+.


Диагностика перед тюнингом: что измерять

Тюнинг без метрик — гадание на кофейной гуще. Сначала измеряем, потом меняем, потом снова измеряем.

Текущее состояние Nginx

# Версия и скомпилированные модули
nginx -V 2>&1 | tr ' ' '\n' | grep -E 'version|with-|without-'

# Активная конфигурация (проверка синтаксиса)
nginx -t

# Рабочие процессы и их нагрузка
ps aux | grep nginx
top -p $(pgrep -d',' nginx)

# Открытые соединения
ss -s
ss -tnp | grep nginx | wc -l

# Статус (если включён stub_status)
curl -s http://127.0.0.1/nginx_status
# Active connections: 847
# server accepts handled requests
#  12340582 12340582 28473910
# Reading: 12 Writing: 84 Waiting: 751

# Лимиты файловых дескрипторов
cat /proc/$(pgrep -f 'nginx: master')/limits | grep 'open files'
ulimit -n

Нагрузочное тестирование

# wrk — современный HTTP benchmarker
# Установка: apt install wrk / brew install wrk
wrk -t12 -c400 -d30s --latency http://your-server/api/endpoint

# Параметры:
# -t12   — 12 потоков (= число ядер)
# -c400  — 400 конкурентных соединений
# -d30s  — 30 секунд
# --latency — показать перцентили задержек

# Вывод:
# Running 30s test @ http://your-server/
#   12 threads and 400 connections
#   Thread Stats   Avg    Stdev   Max   +/- Stdev
#     Latency    23.45ms  8.12ms  890ms  92.34%
#     Req/Sec    1.45k   312.45   2.10k  68.23%
#   Latency Distribution
#      50%   21.23ms
#      75%   28.45ms
#      90%   35.67ms
#      99%   78.90ms    ← 99-й перцентиль важнее среднего!
#   521245 requests in 30.00s, 2.34GB read
#   Requests/sec: 17374.83
#   Transfer/sec: 79.92MB

# ab (Apache Benchmark) — встроен везде, но хуже wrk
ab -n 10000 -c 100 http://your-server/

# hey — ещё один вариант (Go)
hey -n 50000 -c 200 http://your-server/

Мониторинг в реальном времени

# Топ запросов по времени ответа (из access_log)
awk '{print $NF, $7}' /var/log/nginx/access.log | sort -rn | head -20

# Количество запросов в секунду (live)
tail -f /var/log/nginx/access.log | pv -l -i 1 > /dev/null

# Распределение кодов ответов за последний час
awk -v d="$(date -d '1 hour ago' '+%d/%b/%Y:%H')" \
  '$4 ~ d {print $9}' /var/log/nginx/access.log | sort | uniq -c | sort -rn

# Топ IP по количеству запросов
awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -20

Уровень 1: Системные настройки Linux

Nginx ограничен операционной системой. Без правильной настройки Linux все оптимизации Nginx упрутся в системный потолок.

Файловые дескрипторы

# /etc/security/limits.conf
nginx   soft    nofile  65535
nginx   hard    nofile  65535
root    soft    nofile  65535
root    hard    nofile  65535

# Для systemd (приоритет над limits.conf):
# /etc/systemd/system/nginx.service.d/override.conf
[Service]
LimitNOFILE=65535

sudo systemctl daemon-reload
sudo systemctl restart nginx

# Проверка:
cat /proc/$(cat /var/run/nginx.pid)/limits | grep 'open files'

Сетевой стек (sysctl)

# /etc/sysctl.d/99-nginx.conf

# ===== TCP буферы =====
# Увеличиваем буферы приёма/передачи
net.core.rmem_default = 262144
net.core.wmem_default = 262144
net.core.rmem_max     = 16777216
net.core.wmem_max     = 16777216
net.ipv4.tcp_rmem     = 4096 87380 16777216
net.ipv4.tcp_wmem     = 4096 65536 16777216

# ===== Очередь соединений =====
# Размер очереди для accept() — важно при всплесках трафика
net.core.somaxconn     = 65535
net.ipv4.tcp_max_syn_backlog = 65535

# ===== TCP оптимизации =====
# Быстрое переиспользование TIME_WAIT соединений
net.ipv4.tcp_tw_reuse    = 1

# Алгоритм управления перегрузкой
# BBR — лучший выбор для большинства продакшен-сценариев (ядро 4.9+)
net.core.default_qdisc    = fq
net.ipv4.tcp_congestion_control = bbr

# Уменьшаем время FIN_WAIT2 (2 минуты по умолчанию — слишком долго)
net.ipv4.tcp_fin_timeout  = 15

# Максимальное число открытых TCP соединений
net.ipv4.tcp_max_tw_buckets = 1440000

# Разрешаем bind на порт без TIME_WAIT
net.ipv4.tcp_timestamps  = 1

# ===== Очередь обработки пакетов =====
net.core.netdev_max_backlog = 65535

# ===== Локальный диапазон портов =====
# Для upstream keepalive нужно много эфемерных портов
net.ipv4.ip_local_port_range = 1024 65535

# Применить:
sudo sysctl -p /etc/sysctl.d/99-nginx.conf

# Проверить BBR:
sysctl net.ipv4.tcp_congestion_control
# должно быть: net.ipv4.tcp_congestion_control = bbr

Прозрачные hugepages и планировщик I/O

# Для высоконагруженных серверов — отключить transparent hugepages
# (могут вызывать latency spikes)
echo never > /sys/kernel/mm/transparent_hugepage/enabled
echo never > /sys/kernel/mm/transparent_hugepage/defrag

# В /etc/rc.local для постоянства:
echo 'echo never > /sys/kernel/mm/transparent_hugepage/enabled' >> /etc/rc.local
echo 'echo never > /sys/kernel/mm/transparent_hugepage/defrag'  >> /etc/rc.local

# Планировщик I/O для SSD (none или mq-deadline быстрее cfq)
echo mq-deadline > /sys/block/sda/queue/scheduler
# Проверить: cat /sys/block/sda/queue/scheduler

Уровень 2: Базовый конфиг Nginx — Worker и Events

# /etc/nginx/nginx.conf

# ===== WORKER PROCESSES =====
# Правило: 1 воркер на 1 физическое ядро CPU
# auto — Nginx сам определяет количество ядер (рекомендуется)
worker_processes auto;

# Привязка воркеров к ядрам (CPU affinity)
# Уменьшает cache miss у процессора, повышает производительность ~5-10%
# Для 4 ядер:
# worker_cpu_affinity 0001 0010 0100 1000;
# Для auto (Nginx 1.9+):
worker_cpu_affinity auto;

# Приоритет процесса (от -20 до 20, меньше = выше приоритет)
# -5 даёт небольшое преимущество без ущерба системе
worker_priority -5;

# Файловые дескрипторы на воркер (должно совпадать с ulimit -n)
worker_rlimit_nofile 65535;

# PID файл
pid /var/run/nginx.pid;

# ===== ERROR LOG =====
# warn в продакшене (info/debug — слишком verbose для highload)
error_log /var/log/nginx/error.log warn;

# ===== EVENTS =====
events {
    # Максимум соединений на воркер
    # Итого соединений = worker_processes × worker_connections
    # Не ставьте больше 65535 — ограничение Linux
    # Реально для highload: 4096-16384
    worker_connections 10240;

    # epoll — единственный правильный выбор на Linux
    # (Nginx выбирает автоматически, но явно лучше)
    use epoll;

    # Принимать все ожидающие соединения за один вызов accept()
    # ОБЯЗАТЕЛЬНО для highload! Без этого воркер обрабатывает по 1 соединению
    multi_accept on;

    # Принимать мьютекс для accept() (устарело в современных ядрах,
    # но оставляем для совместимости)
    # accept_mutex off;  # Можно отключить на ядрах 3.9+
}

Уровень 3: HTTP блок — основные оптимизации

http {
    # ===== БАЗОВЫЕ MIME ТИПЫ =====
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    # ===== SENDFILE: ZERO-COPY ПЕРЕДАЧА ФАЙЛОВ =====
    # Передаёт файлы напрямую из файловой системы в сокет
    # минуя user space — экономит копирование данных в памяти
    # ОБЯЗАТЕЛЬНО для статики!
    sendfile on;

    # Отправлять заголовки и начало файла в одном TCP-пакете
    # Работает только совместно с sendfile on
    tcp_nopush on;

    # Отключить алгоритм Nagle — не буферизировать маленькие пакеты
    # Уменьшает latency для интерактивных запросов
    # tcp_nopush и tcp_nodelay вместе: сначала накапливаем (nopush),
    # потом сразу отправляем последний пакет (nodelay)
    tcp_nodelay on;

    # ===== KEEPALIVE =====
    # Время жизни keepalive соединения с клиентом
    # 65s стандарт, 75s — максимум до таймаута прокси (Cloudflare = 90s)
    keepalive_timeout 65;

    # Максимум запросов через одно keepalive соединение
    # После этого — закрываем и открываем новое
    # 1000 — хороший баланс между переиспользованием и памятью
    keepalive_requests 1000;

    # ===== БУФЕРЫ =====
    # Эти настройки критически важны для производительности proxy!

    # Размер хэш-таблицы имён серверов
    server_names_hash_bucket_size 128;
    server_names_hash_max_size    1024;

    # Буфер для чтения заголовков запроса клиента
    # 16k достаточно для большинства запросов, включая большие cookie
    client_header_buffer_size 16k;
    large_client_header_buffers 4 32k;

    # Максимальный размер тела запроса (загрузка файлов)
    # 0 — отключить ограничение (не рекомендуется!)
    client_max_body_size 64m;

    # Таймаут на чтение тела запроса
    client_body_timeout 30s;

    # Таймаут на чтение заголовков запроса
    client_header_timeout 15s;

    # Таймаут на отправку ответа клиенту
    # (между двумя последовательными операциями send)
    send_timeout 30s;

    # Буфер для тела запроса в памяти (если больше — пишем на диск)
    client_body_buffer_size 256k;

    # ===== ТИПЫ ХЭШЕЙ =====
    types_hash_max_size 4096;
    types_hash_bucket_size 128;

    # ===== БЕЗОПАСНОСТЬ: УБИРАЕМ ВЕРСИЮ NGINX =====
    server_tokens off;

    # ===== ЛОГИ =====
    # Формат логов с временем обработки запроса — важно для анализа!
    log_format main '$remote_addr - $remote_user [$time_local] '
                    '"$request" $status $body_bytes_sent '
                    '"$http_referer" "$http_user_agent" '
                    'rt=$request_time uct=$upstream_connect_time '
                    'uht=$upstream_header_time urt=$upstream_response_time';

    # Расширенный формат для детальной диагностики:
    log_format detailed '$remote_addr - $remote_user [$time_local] '
                        '"$request" $status $body_bytes_sent '
                        'rt=$request_time '
                        'urt="$upstream_response_time" '
                        'uct="$upstream_connect_time" '
                        'uht="$upstream_header_time" '
                        'cs=$upstream_cache_status '
                        'host=$host '
                        'xff="$http_x_forwarded_for"';

    access_log /var/log/nginx/access.log main buffer=64k flush=5s;
    # buffer=64k  — буферизация логов (не пишем на диск каждую строку)
    # flush=5s    — сбрасываем буфер каждые 5 секунд

    # Для максимальной производительности — отключить access_log на статике
    # (настраивается в location блоках)

    # ===== ВКЛЮЧАЕМ ПОДКОНФИГИ =====
    include /etc/nginx/conf.d/*.conf;
}

Уровень 4: Gzip и Brotli — сжатие ответов

http {
    # ===== GZIP =====
    gzip on;

    # Не сжимать ответы для IE6 (исторический артефакт, можно убрать)
    gzip_disable "msie6";

    # Сжимать ответы для всех клиентов, в т.ч. через прокси
    # any — сжимать независимо от заголовка Via
    gzip_proxied any;

    # Уровень сжатия: 1-9
    # 1 — быстро, мало сжатие (~60%)
    # 6 — баланс (рекомендуется для продакшена ~70%)
    # 9 — максимум, но тратит значительно больше CPU (~72%, но в 3-5 раз медленнее 6)
    gzip_comp_level 6;

    # Минимальный размер для сжатия (не сжимать маленькие файлы — смысла нет)
    gzip_min_length 1024;

    # Буферы для сжатия
    gzip_buffers 16 8k;

    # HTTP версия (сжимать и для HTTP/1.0 клиентов — редко нужно)
    gzip_http_version 1.1;

    # Типы контента для сжатия
    # text/html сжимается всегда (по умолчанию)
    gzip_types
        text/plain
        text/css
        text/xml
        text/javascript
        application/json
        application/javascript
        application/xml
        application/xml+rss
        application/x-javascript
        application/x-font-ttf
        application/vnd.ms-fontobject
        font/opentype
        image/svg+xml
        image/x-icon;

    # Добавить заголовок Vary: Accept-Encoding
    # Указывает прокси-серверам хранить сжатые и несжатые версии отдельно
    gzip_vary on;

    # ===== BROTLI (требует модуль ngx_brotli) =====
    # Brotli на 15-25% эффективнее gzip при тех же CPU-затратах
    # Поддерживается всеми современными браузерами

    # Установка модуля (Ubuntu/Debian):
    # apt install libnginx-mod-http-brotli-filter
    # Или из исходников: https://github.com/google/ngx_brotli

    # load_module modules/ngx_http_brotli_filter_module.so;
    # load_module modules/ngx_http_brotli_static_module.so;

    brotli on;
    brotli_comp_level 6;       # 0-11, 6 — хороший баланс
    brotli_min_length 1024;
    brotli_types
        text/plain
        text/css
        text/xml
        text/javascript
        application/json
        application/javascript
        application/xml
        application/xml+rss
        image/svg+xml;

    # Brotli static — отдавать предкомпрессированные .br файлы
    # Нужно сгенерировать: find /var/www -name "*.js" | xargs -I{} brotli {}
    brotli_static on;

    # ===== GZIP STATIC — предкомпрессированные .gz файлы =====
    # Если файл app.js.gz существует — отдаём его без CPU на сжатие
    gzip_static on;
}

Предварительное сжатие статики (экономит CPU)

#!/bin/bash
# Скрипт для предкомпрессии статических файлов
STATIC_DIR="/var/www/html"

find "$STATIC_DIR" \
    \( -name "*.js" -o -name "*.css" -o -name "*.html" -o -name "*.json" \
       -o -name "*.xml" -o -name "*.svg" \) \
    -type f | while read file; do

    # gzip (только если .gz не существует или файл новее)
    if [ ! -f "${file}.gz" ] || [ "$file" -nt "${file}.gz" ]; then
        gzip -9 -k "$file"
    fi

    # brotli
    if command -v brotli &> /dev/null; then
        if [ ! -f "${file}.br" ] || [ "$file" -nt "${file}.br" ]; then
            brotli -q 11 -k "$file"
        fi
    fi
done

echo "Предкомпрессия завершена: $(find $STATIC_DIR -name '*.gz' | wc -l) gz файлов"

Уровень 5: Кэширование — proxy_cache и FastCGI cache

Proxy Cache (для проксирования на upstream)

http {
    # ===== ЗОНА КЭША =====
    # keys_zone=cache_name:10m  — зона памяти для хранения ключей
    # levels=1:2                — структура директорий (более эффективный поиск)
    # inactive=60m              — удалять неиспользуемые файлы через 60 минут
    # max_size=10g              — максимальный размер кэша на диске
    # use_temp_path=off         — не использовать временный путь (быстрее)
    proxy_cache_path /var/cache/nginx/proxy
        levels=1:2
        keys_zone=proxy_cache:50m
        inactive=60m
        max_size=10g
        use_temp_path=off;

    server {
        location /api/ {
            proxy_pass http://backend;

            # ===== PROXY БУФЕРЫ =====
            # Буферизировать ответ от upstream в памяти
            # Важно: без буферизации Nginx держит соединение с upstream
            # пока клиент не скачает весь ответ (медленные клиенты = занятые воркеры)
            proxy_buffering on;

            # Количество и размер буферов для тела ответа
            # proxy_buffers × proxy_buffer_size = RAM на соединение
            # 32 × 16k = 512k на соединение
            proxy_buffers         32 16k;
            proxy_buffer_size     16k;   # Для заголовков ответа

            # Если ответ не помещается в proxy_buffers — пишем во временный файл
            proxy_max_temp_file_size 0;   # 0 = отключить (пишем всё в память)
            # или установить лимит: proxy_max_temp_file_size 1024m;

            # Буфер для занятых соединений (busy = клиент читает медленно)
            proxy_busy_buffers_size 64k;

            # ===== PROXY ТАЙМАУТЫ =====
            # Таймаут установки соединения с upstream
            proxy_connect_timeout 5s;

            # Таймаут между двумя последовательными операциями чтения от upstream
            proxy_read_timeout 60s;

            # Таймаут передачи данных к upstream
            proxy_send_timeout 60s;

            # ===== ЗАГОЛОВКИ К UPSTREAM =====
            proxy_set_header Host               $host;
            proxy_set_header X-Real-IP          $remote_addr;
            proxy_set_header X-Forwarded-For    $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto  $scheme;
            proxy_http_version 1.1;             # HTTP/1.1 для keepalive!
            proxy_set_header Connection "";     # Убрать заголовок Connection для keepalive

            # ===== КЭШИРОВАНИЕ =====
            proxy_cache            proxy_cache;
            proxy_cache_valid      200 302  10m;  # Кэшировать 200/302 на 10 минут
            proxy_cache_valid      404      1m;   # 404 — на 1 минуту
            proxy_cache_valid      any      30s;  # Остальное — 30 секунд

            # Ключ кэша (что уникально идентифицирует запрос)
            proxy_cache_key "$scheme$request_method$host$request_uri";

            # Методы для кэширования (по умолчанию только GET и HEAD)
            proxy_cache_methods GET HEAD;

            # Кэшировать ответы с заголовком Set-Cookie
            # (осторожно — персональные данные!)
            # proxy_ignore_headers Set-Cookie;

            # Stale cache — отдавать устаревший кэш пока upstream недоступен
            # Спасает от "пятистотки" при временных проблемах с бэкендом
            proxy_cache_use_stale error timeout updating
                                   http_500 http_502 http_503 http_504;

            # Блокировка одновременных запросов к upstream (coalescing)
            # Один запрос идёт к upstream, остальные ждут результата в кэше
            proxy_cache_lock on;
            proxy_cache_lock_timeout 5s;
            proxy_cache_lock_age     5s;

            # Фоновое обновление кэша (stale-while-revalidate)
            # Отдаём устаревший кэш и одновременно запускаем фоновое обновление
            proxy_cache_background_update on;

            # Добавляем заголовок X-Cache-Status для отладки
            add_header X-Cache-Status $upstream_cache_status always;
        }

        # ===== СТАТИКА: МАКСИМАЛЬНЫЙ КЭШ =====
        location ~* \.(jpg|jpeg|png|gif|ico|svg|webp|woff|woff2|ttf|eot)$ {
            expires     1y;
            add_header  Cache-Control "public, immutable, max-age=31536000";
            access_log  off;  # Не логировать статику (экономит I/O)
            tcp_nodelay off;  # Для больших файлов nopush важнее nodelay
            sendfile    on;
            aio         on;   # Асинхронный I/O для больших файлов
        }

        location ~* \.(js|css)$ {
            expires     1y;
            add_header  Cache-Control "public, immutable, max-age=31536000";
            access_log  off;
        }

        location ~* \.(html|htm)$ {
            expires     1h;
            add_header  Cache-Control "public, max-age=3600, must-revalidate";
        }
    }
}

FastCGI Cache (для PHP-FPM)

http {
    # Зона FastCGI кэша
    fastcgi_cache_path /var/cache/nginx/fastcgi
        levels=1:2
        keys_zone=fastcgi_cache:20m
        inactive=60m
        max_size=5g
        use_temp_path=off;

    # Глобальная переменная для определения статуса кэша
    map $request_method $no_cache_method {
        default 0;
        POST    1;
        PUT     1;
        DELETE  1;
        PATCH   1;
    }

    server {
        set $skip_cache 0;

        # Не кэшировать авторизованных пользователей (например, WordPress)
        if ($http_cookie ~* "wordpress_logged_in|woocommerce_cart") {
            set $skip_cache 1;
        }

        # Не кэшировать POST запросы
        if ($request_method = POST) {
            set $skip_cache 1;
        }

        # Не кэшировать URL с query string (можно убрать если кэш по полному URL)
        # if ($query_string != "") {
        #     set $skip_cache 1;
        # }

        # Не кэшировать admin/личный кабинет
        if ($request_uri ~* "^/admin|^/wp-admin|^/login") {
            set $skip_cache 1;
        }

        location ~ \.php$ {
            include fastcgi_params;
            fastcgi_pass unix:/run/php/php8.2-fpm.sock;
            fastcgi_index index.php;
            fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;

            # ===== FastCGI БУФЕРЫ =====
            fastcgi_buffers         16 16k;
            fastcgi_buffer_size     32k;
            fastcgi_busy_buffers_size 64k;

            # Таймауты
            fastcgi_connect_timeout  5s;
            fastcgi_read_timeout    60s;
            fastcgi_send_timeout    60s;

            # ===== КЭШ =====
            fastcgi_cache            fastcgi_cache;
            fastcgi_cache_key        "$scheme$request_method$host$request_uri";
            fastcgi_cache_valid       200  5m;
            fastcgi_cache_valid       301 302  1m;
            fastcgi_cache_valid       404  30s;
            fastcgi_cache_bypass     $skip_cache;
            fastcgi_no_cache         $skip_cache;
            fastcgi_cache_use_stale  error timeout updating
                                     http_500 http_503;
            fastcgi_cache_lock       on;
            fastcgi_cache_background_update on;

            add_header X-FastCGI-Cache $upstream_cache_status;
        }
    }
}

Инвалидация кэша

# Очистить весь кэш proxy
find /var/cache/nginx/proxy -type f -delete

# Очистить конкретный URL (через модуль ngx_cache_purge):
# location ~ /purge(/.*) {
#     fastcgi_cache_purge fastcgi_cache "$scheme$request_method$host$1";
# }
# curl -X PURGE http://your-server/api/products/123

# Мониторинг кэша:
# X-Cache-Status значения:
# HIT       — ответ из кэша 

# MISS — кэш не нашёл, запрос к upstream

# BYPASS — кэш пропущен (skip_cache = 1)

# EXPIRED — кэш устарел, запрошен свежий ответ

# STALE — отдан устаревший кэш (upstream недоступен)

# UPDATING — отдан устаревший кэш пока фоновое обновление

# REVALIDATED — кэш подтверждён (304 от upstream)


Уровень 6: Upstream Keepalive и балансировка

http {
    # ===== UPSTREAM С KEEPALIVE =====
    upstream backend {
        # Алгоритм балансировки
        # least_conn — меньше соединений = меньше задержка (лучше для long-poll)
        # ip_hash    — один клиент всегда на один сервер (сессии)
        # По умолчанию: round-robin
        least_conn;

        server 10.0.0.10:8080 weight=3;  # Тройной вес (мощнее)
        server 10.0.0.11:8080 weight=1;
        server 10.0.0.12:8080 weight=1 backup;  # Резервный (включается при падении основных)

        # Параметры health check (требует nginx plus или upstream_check_module):
        # server 10.0.0.10:8080 max_fails=3 fail_timeout=30s;

        # ===== KEEPALIVE ПУЛА К UPSTREAM =====
        # Количество keepalive соединений в пуле (на воркер!)
        # НЕ максимальное число соединений — это пул переиспользуемых!
        # Правило: (RPS / worker_count) × avg_response_time_sec × 1.5
        # При 10000 RPS, 4 воркерах, 20мс ответе: 10000/4 × 0.02 × 1.5 = 75
        keepalive 128;

        # Таймаут keepalive соединения с upstream
        keepalive_timeout 60s;

        # Максимум запросов через одно keepalive соединение к upstream
        keepalive_requests 10000;
    }

    server {
        location / {
            proxy_pass http://backend;

            # КРИТИЧЕСКИ ВАЖНО для upstream keepalive!
            # HTTP/1.1 поддерживает keepalive (1.0 — нет)
            proxy_http_version 1.1;

            # Убираем заголовок Connection: close (стандарт для HTTP/1.0 прокси)
            proxy_set_header Connection "";
        }
    }
}

Уровень 7: SSL/TLS — производительность без потери безопасности

http {
    # ===== SSL СЕССИИ =====
    # Кэш SSL сессий (повторное использование TLS handshake)
    # 1m ≈ 4000 сессий. Для highload: 50m-100m
    ssl_session_cache   shared:SSL:50m;

    # Время жизни кэшированной SSL сессии
    ssl_session_timeout 1d;   # 24 часа — максимум рекомендуемый

    # SSL Session Tickets (альтернатива session cache, статeful у клиента)
    # Для идеальной forward secrecy — отключить
    # Для максимальной производительности — включить
    ssl_session_tickets off;   # Безопаснее, но чуть медленнее

    # ===== ПРОТОКОЛЫ И ШИФРЫ =====
    # Только TLS 1.2 и 1.3 (1.0 и 1.1 — уязвимы и устарели)
    ssl_protocols TLSv1.2 TLSv1.3;

    # Шифры (Mozilla Modern конфигурация)
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;

    # Сервер выбирает шифр (не клиент) — важно для безопасности
    ssl_prefer_server_ciphers off;   # off для TLS 1.3 (там нет выбора шифра)

    # ===== ECDH КРИВАЯ =====
    ssl_ecdh_curve X25519:prime256v1:secp384r1;

    # ===== STAPLING =====
    # OCSP Stapling: сервер сам проверяет сертификат и включает ответ в TLS handshake
    # Клиенту не нужно делать отдельный запрос к CA — быстрее!
    ssl_stapling on;
    ssl_stapling_verify on;
    resolver 8.8.8.8 8.8.4.4 1.1.1.1 valid=300s;
    resolver_timeout 5s;

    # ===== DH параметры (для DHE шифров) =====
    # Генерировать: openssl dhparam -out /etc/nginx/dhparam.pem 2048
    # ssl_dhparam /etc/nginx/dhparam.pem;

    server {
        listen 443 ssl;

        # ===== HTTP/2 =====
        # Мультиплексирование запросов — один TCP для множества запросов
        # Сжатие заголовков (HPACK) — экономит трафик
        # Server Push — отправка ресурсов до запроса (редко нужен)
        listen 443 ssl http2;

        ssl_certificate     /etc/nginx/ssl/fullchain.pem;
        ssl_certificate_key /etc/nginx/ssl/privkey.pem;

        # ===== HSTS =====
        # Браузер не будет делать HTTP запросы — сразу HTTPS
        # Включать только когда уверены что HTTPS работает корректно!
        add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;

        # ===== SECURITY ЗАГОЛОВКИ =====
        add_header X-Frame-Options           "SAMEORIGIN"   always;
        add_header X-Content-Type-Options    "nosniff"      always;
        add_header X-XSS-Protection          "1; mode=block" always;
        add_header Referrer-Policy           "strict-origin-when-cross-origin" always;
        add_header Permissions-Policy        "geolocation=(), microphone=()" always;
    }

    # Редирект HTTP → HTTPS
    server {
        listen 80;
        server_name example.com www.example.com;

        # 301 для всего кроме .well-known (Let's Encrypt)
        location /.well-known/acme-challenge/ {
            root /var/www/html;
        }

        location / {
            return 301 https://$host$request_uri;
        }
    }
}

Измерение времени TLS handshake

# Проверка SSL без кэша (первое соединение)
curl -w "\n=== Timing ===\nDNS: %{time_namelookup}s\nConnect: %{time_connect}s\nTLS: %{time_appconnect}s\nTTFB: %{time_starttransfer}s\nTotal: %{time_total}s\n" \
     --no-keepalive -s -o /dev/null https://your-server/

# С session resumption (второе соединение)
curl -w "TLS Resumption Total: %{time_total}s\n" \
     --no-keepalive -s -o /dev/null https://your-server/ \
     --tls-session-info

# Проверка OCSP Stapling:
echo QUIT | openssl s_client -connect your-server:443 -status 2>/dev/null | \
    grep -A 17 'OCSP response'

Уровень 8: Rate Limiting — защита и QoS

http {
    # ===== ЗОНЫ RATE LIMITING =====

    # Ограничение по IP адресу
    # zone=name:10m — 10МБ памяти (~160 000 IP адресов)
    # rate=100r/s — 100 запросов в секунду с одного IP
    limit_req_zone $binary_remote_addr zone=api_limit:20m rate=100r/s;

    # Ограничение для авторизации (жёстче!)
    limit_req_zone $binary_remote_addr zone=auth_limit:10m rate=5r/m;

    # Ограничение по URL + IP (для конкретных эндпоинтов)
    limit_req_zone "$binary_remote_addr$uri" zone=upload_limit:10m rate=5r/m;

    # Ограничение одновременных соединений
    limit_conn_zone $binary_remote_addr zone=conn_limit:10m;

    # Лог уровень для rejected запросов (warn — не засорять error.log)
    limit_req_log_level warn;

    # Код ответа при превышении лимита (429 = Too Many Requests)
    limit_req_status 429;
    limit_conn_status 429;

    server {
        # ===== API ENDPOINT =====
        location /api/ {
            # burst=200  — разрешить всплески до 200 запросов сверх лимита
            # nodelay    — не задерживать burst запросы, обрабатывать немедленно
            #              (без nodelay — запросы ставятся в очередь и задерживаются)
            limit_req zone=api_limit burst=200 nodelay;

            # Максимум 100 одновременных соединений с одного IP
            limit_conn conn_limit 100;

            proxy_pass http://backend;
        }

        # ===== АВТОРИЗАЦИЯ: СТРОГИЙ ЛИМИТ =====
        location /api/auth/ {
            limit_req zone=auth_limit burst=10 nodelay;
            limit_conn conn_limit 10;
            proxy_pass http://backend;
        }

        # ===== ЗАГРУЗКА ФАЙЛОВ =====
        location /upload/ {
            limit_req zone=upload_limit burst=2 nodelay;
            limit_conn conn_limit 5;
            client_max_body_size 100m;
            proxy_pass http://backend;
        }
    }
}

Белые списки для rate limiting

http {
    # Карта: 0 = применять limit, 1 = пропустить
    geo $limit {
        default         1;       # Ограничивать всех
        10.0.0.0/8      0;       # Доверенная внутренняя сеть — без ограничений
        192.168.0.0/16  0;       # Локальная сеть — без ограничений
        1.2.3.4         0;       # Конкретный IP (мониторинг, партнёры)
    }

    # Если $limit = 0 — пустой ключ, limit_req не применяется
    map $limit $limit_key {
        0 "";
        1 $binary_remote_addr;
    }

    limit_req_zone $limit_key zone=api_limit:20m rate=100r/s;
}

Уровень 9: Open File Cache и другие детали

http {
    # ===== OPEN FILE CACHE =====
    # Кэшировать информацию об открытых файлах:
    # файловые дескрипторы, размеры, время модификации, ошибки
    # Особенно важно при большом количестве файлов статики!

    # max=10000 — максимум 10000 записей в кэше
    # inactive=30s — удалять если не обращались 30 секунд
    open_file_cache max=10000 inactive=30s;

    # Сколько раз файл должен быть запрошен за inactive период
    # чтобы остаться в кэше
    open_file_cache_min_uses 2;

    # Проверять актуальность кэша каждые 60 секунд
    open_file_cache_valid 60s;

    # Кэшировать ошибки (файл не найден, нет прав)
    open_file_cache_errors on;

    # ===== SENDFILE + AIO для больших файлов =====
    # Для файлов > 8MB — асинхронный I/O эффективнее
    aio threads;  # AIO через thread pool (Nginx 1.7.11+)
    # или aio on; # POSIX AIO (старый вариант, хуже)

    directio 8m;  # Файлы > 8MB: читать напрямую, минуя page cache
                  # Полезно для больших видеофайлов которые не нужно кэшировать

    # ===== OUTPUT BUFFERS =====
    # Размер буфера вывода (используется с sendfile)
    output_buffers 2 512k;

    # ===== ПЕРЕМЕННЫЕ =====
    # Кэш переменных (для complex map и geo директив)
    variables_hash_max_size 2048;
    variables_hash_bucket_size 128;

    # ===== MAP HASH =====
    map_hash_max_size 2048;
    map_hash_bucket_size 128;
}

Уровень 10: Полный production конфиг сервера

# /etc/nginx/conf.d/example.com.conf

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name example.com www.example.com;

    root /var/www/html;
    index index.html index.php;

    # SSL
    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    # Логи с детальным форматом
    access_log /var/log/nginx/example.com.access.log detailed buffer=64k flush=5s;
    error_log  /var/log/nginx/example.com.error.log warn;

    # Скрываем .git, .env и другие служебные файлы
    location ~ /\. {
        deny all;
        access_log off;
        log_not_found off;
    }

    location ~* \.(env|log|sh|sql|conf|config|bak|backup|swp|tmp)$ {
        deny all;
    }

    # ===== СТАТИКА: МАКСИМАЛЬНАЯ ОТДАЧА =====
    location ~* \.(jpg|jpeg|png|gif|ico|svg|webp|avif|woff|woff2|ttf|eot|otf)$ {
        expires     1y;
        add_header  Cache-Control "public, immutable";
        add_header  Vary Accept-Encoding;
        access_log  off;
        log_not_found off;
        gzip_static on;
        brotli_static on;
    }

    location ~* \.(js|css|map)$ {
        expires     1y;
        add_header  Cache-Control "public, immutable";
        access_log  off;
        gzip_static on;
        brotli_static on;
    }

    # ===== FAVICON И ROBOTS =====
    location = /favicon.ico {
        access_log off;
        log_not_found off;
        expires 1y;
    }

    location = /robots.txt {
        access_log off;
        log_not_found off;
    }

    # ===== API =====
    location /api/ {
        limit_req zone=api_limit burst=200 nodelay;
        limit_conn conn_limit 100;

        proxy_pass         http://backend;
        proxy_http_version 1.1;
        proxy_set_header   Connection "";
        proxy_set_header   Host               $host;
        proxy_set_header   X-Real-IP          $remote_addr;
        proxy_set_header   X-Forwarded-For    $proxy_add_x_forwarded_for;
        proxy_set_header   X-Forwarded-Proto  $scheme;

        proxy_cache            proxy_cache;
        proxy_cache_valid      200 5m;
        proxy_cache_valid      404 30s;
        proxy_cache_use_stale  error timeout updating http_500 http_502 http_503 http_504;
        proxy_cache_lock       on;
        proxy_cache_background_update on;

        add_header X-Cache-Status $upstream_cache_status always;
    }

    # ===== PHP =====
    location ~ \.php$ {
        try_files $uri =404;
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass unix:/run/php/php8.2-fpm.sock;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;

        fastcgi_buffers          16 16k;
        fastcgi_buffer_size      32k;
        fastcgi_read_timeout     60s;

        fastcgi_cache            fastcgi_cache;
        fastcgi_cache_valid      200 5m;
        fastcgi_cache_bypass     $skip_cache;
        fastcgi_no_cache         $skip_cache;
        fastcgi_cache_use_stale  error timeout updating http_500 http_503;
        fastcgi_cache_lock       on;

        add_header X-FastCGI-Cache $upstream_cache_status;
    }

    # ===== КОРЕНЬ =====
    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }
}

Диагностика и мониторинг в продакшене

Stub Status модуль

server {
    listen 127.0.0.1:8080;

    location /nginx_status {
        stub_status;
        allow 127.0.0.1;
        allow 10.0.0.0/8;      # Сеть мониторинга
        deny all;
    }
}
# Парсинг nginx_status:
curl -s http://127.0.0.1:8080/nginx_status
# Active connections: 1247
# server accepts handled requests
#  87354291 87354291 245912847
# Reading: 12 Writing: 847 Waiting: 388

# Интерпретация:
# Active    = Reading + Writing + Waiting
# Waiting   = keepalive соединения (ждут следующего запроса)
# Writing   = активно пишем ответ клиенту
# Reading   = читаем запрос от клиента

# Если Waiting >> Writing — много keepalive соединений, это нормально
# Если Reading >> 0 постоянно — клиенты медленно отправляют запросы
# Если Writing = worker_processes × worker_connections — всё занято!

# accepts == handled — нет dropped connections. Если differs — проблема!

Анализ логов

# Медленные запросы (> 1 секунды)
awk '$NF > 1' /var/log/nginx/access.log | \
    awk '{print $NF, $7}' | sort -rn | head -20

# HIT rate кэша
grep -o 'cs=[A-Z]*' /var/log/nginx/access.log | \
    sort | uniq -c | sort -rn
# 94521 cs=HIT      ← 87% HIT rate — хорошо!
# 12305 cs=MISS
# 1823  cs=BYPASS
# 289   cs=EXPIRED

# Ошибки upstream
grep 'upstream timed out\|connect() failed\|upstream prematurely' \
    /var/log/nginx/error.log | tail -50

# Топ 5xx ошибок
awk '$9 ~ /^5/' /var/log/nginx/access.log | \
    awk '{print $9, $7}' | sort | uniq -c | sort -rn | head -20

Чеклист тюнинга: финальная проверка

#!/bin/bash
# Быстрая проверка ключевых параметров

echo "=== NGINX PERFORMANCE CHECKLIST ==="

# 1. Worker processes
WP=$(nginx -T 2>/dev/null | grep 'worker_processes' | tail -1 | awk '{print $2}')
CORES=$(nproc)
echo "Worker processes: $WP (cores: $CORES)"

# 2. Worker connections
WC=$(nginx -T 2>/dev/null | grep 'worker_connections' | tail -1 | awk '{print $2}')
echo "Worker connections: $WC"

# 3. File descriptors
FD=$(cat /proc/$(cat /var/run/nginx.pid 2>/dev/null || echo 1)/limits \
     2>/dev/null | grep 'open files' | awk '{print $4}')
echo "File descriptors limit: $FD"

# 4. sendfile
SF=$(nginx -T 2>/dev/null | grep -E '^\s+sendfile' | tail -1)
echo "Sendfile: $SF"

# 5. gzip
GZ=$(nginx -T 2>/dev/null | grep -E '^\s+gzip ' | tail -1)
echo "Gzip: $GZ"

# 6. SSL session cache
SSL=$(nginx -T 2>/dev/null | grep 'ssl_session_cache' | tail -1)
echo "SSL session cache: $SSL"

# 7. BBR
echo "TCP congestion: $(sysctl -n net.ipv4.tcp_congestion_control)"

# 8. Тест конфигурации
nginx -t && echo "Config: OK" || echo "Config: ERROR!"

echo ""
echo "=== ТЕКУЩАЯ НАГРУЗКА ==="
curl -s http://127.0.0.1:8080/nginx_status 2>/dev/null || echo "stub_status недоступен"

Типичные ошибки и мифы

Миф 1: "worker_processes 4096 увеличит производительность" Нет. Оптимум — по одному воркеру на ядро. Больше воркеров = больше переключений контекста = хуже.

Миф 2: "worker_connections 65535 — максимум соединений" Нет. Это максимум на один воркер. Итого: worker_processes × worker_connections. При 4 воркерах и 10240 соединениях = 40960 одновременных соединений.

Миф 3: "keepalive_timeout 0 ускорит сервер" Наоборот. Keepalive экономит TLS handshake и TCP установку соединения. Отключение keepalive нагрузит сервер больше.

Миф 4: "gzip_comp_level 9 — лучше" Нет. Разница в размере между уровнями 6 и 9 — 1-3%. Разница в CPU — в 3-5 раз. Используйте gzip_comp_level 6.

Ошибка: proxy_cache без proxy_cache_use_stale При недоступности upstream без use_stale клиенты получат 502. С use_stale error timeout — получат устаревший кэш пока upstream восстанавливается. Всегда включайте!

Ошибка: не настроен upstream keepalive Без keepalive в блоке upstream каждый запрос создаёт новое TCP соединение к backend. При 1000 RPS — 1000 новых TCP handshake в секунду. С keepalive 64 — переиспользуются 64 соединения.


Заключение

Тюнинг Nginx — это итеративный процесс. Хороший порядок:

  1. Системный уровень: sysctl, ulimit, BBR — без этого упрётесь в ОС

  2. Workers и events: worker_processes auto, multi_accept on, epoll

  3. Буферы и таймауты: адаптируйте под характер трафика (размер ответов, скорость клиентов)

  4. Кэш: proxy_cache или fastcgi_cache — самый большой прирост производительности

  5. Upstream keepalive: критично для высоких RPS

  6. SSL оптимизация: session cache + stapling + HTTP/2

  7. Сжатие: gzip + brotli_static для предкомпрессии

  8. Rate limiting: защита без ущерба для легитимного трафика

Измеряйте до и после каждого изменения. Доверяйте цифрам, а не интуиции. И помните: лучший тюнинг — тот, который решает вашу конкретную проблему, а не скопированный из статьи.

User Feedback

Create an account or sign in to leave a review

There are no reviews to display.

Configure browser push notifications

Chrome (Android)
  1. Tap the lock icon next to the address bar.
  2. Tap Permissions → Notifications.
  3. Adjust your preference.
Chrome (Desktop)
  1. Click the padlock icon in the address bar.
  2. Select Site settings.
  3. Find Notifications and adjust your preference.