- itishkin_tmi - девопс mp3
- itishkin_tmi - Катим в прод mp3
- itishkin_tmi - От лампочки до директора mp3
- itishkin_tmi - Удали ВПН 2 mp3
-
itishkin_tmi - А ведь вчера работало mp3
Вчера Работало(1).mp3 Вчера Работало.mp3
- itishkin_tmi - Код ... Баг ... Фикс mp3
- itishkin_tmi - Деплой ночью mp3
- itishkin_tmi - Код ... Баг ... Фикс mp3
- itishkin_tmi - Нельзя в продакшен mp3
- itishkin_tmi - PHP живой mp3
- itishkin_tmi - Айти Воины mp3
- itishkin_tmi - УдОли ВПН mp3
-
Linux в продакшене: какой дистрибутив выбрать, как настроить надёжность и выжать максимум
Введение: Linux в продакшене — это не просто установить и забытьПоставить Ubuntu, запустить приложение и назвать это "продакшеном" — рецепт будущей катастрофы. Настоящий production Linux — это слоёный пирог из правильного выбора дистрибутива, hardening-а, тюнинга ядра, настроенного мониторинга и чётких процедур обслуживания. Статья написана для тех, кто уже умеет работать с Linux и хочет понять почему нужно делать именно так, а не просто скопировать команды. Часть 1: Выбор дистрибутиваКритерии для production-выбораПрежде чем смотреть на дистрибутивы, определитесь с приоритетами: Стабильность vs свежесть: старые проверенные пакеты или последние версии? Коммерческая поддержка: нужен SLA от вендора или достаточно community? Цикл жизни: как долго дистрибутив будет получать security-патчи? Экосистема: есть ли готовые пакеты для вашего стека? Команда: что знает ваша команда? Переучивание дорого стоит. Ubuntu LTS ServerЦикл поддержки: 5 лет standard, 10 лет ESM (Extended Security Maintenance). Текущий LTS: 24.04 (Noble Numbat), следующий — 26.04. Ubuntu — безусловный лидер по распространённости в cloud-среде. AWS, GCP, Azure — везде Ubuntu является первым выбором по умолчанию. Огромная документация, большинство DevOps-инструментов тестируются на Ubuntu в первую очередь. Подходит для: Startups и компании без выделенного Linux-администратора Kubernetes workers и cloud-native окружения Быстрое развёртывание новых стеков Осторожно: Canonical меняет политики (snap-пакеты вместо deb без предупреждения) Обновления между LTS требуют тщательного тестирования ESM платный для > 5 машин в организации # Проверка версии и EOL даты lsb_release -a ubuntu-advantage status # Статус ESM подписки # Отключить snap если не нужен (спорно, но часто делают) sudo systemctl disable snapd --now sudo apt purge snapd Debian StableЦикл поддержки: ~3 года основная поддержка + 2 года LTS. Текущий: Debian 12 "Bookworm" (до 2028). Debian — "бабушка" большинства дистрибутивов. Её репутация: консервативная, предсказуемая, надёжная. Пакеты в Stable могут быть на 1–2 года старше апстрима, но зато они досконально протестированы. Никаких сюрпризов в 3 часа ночи. Подходит для: Серверы с длинным жизненным циклом (БД, хранилища) Инфраструктура, где стабильность важнее новых фич Встраиваемые и промышленные серверы Осторожно: Старые пакеты могут не поддерживать новые возможности (TLS 1.3 и т.п. уже везде есть, но крайние версии — нет) community-поддержка без коммерческого SLA # Debian: правильные sources.list для продакшена # Только stable, никакого testing/sid! cat /etc/apt/sources.list # deb http://deb.debian.org/debian bookworm main contrib non-free-firmware # deb http://security.debian.org/debian-security bookworm-security main # deb http://deb.debian.org/debian bookworm-updates main # Backports только для конкретных пакетов, не массово # deb http://deb.debian.org/debian bookworm-backports main RHEL / Rocky Linux / AlmaLinuxRHEL (Red Hat Enterprise Linux) — корпоративный стандарт в Enterprise-сегменте, особенно там где есть compliance требования (PCI DSS, HIPAA, FedRAMP). Платная подписка, но включает полный enterprise support от Red Hat. Rocky Linux и AlmaLinux — бинарно-совместимые клоны RHEL, бесплатные. После смерти CentOS 8 (2021) это лучшая замена для тех, кто хочет RHEL-совместимость без платы. Цикл поддержки: RHEL 9 → до 2032 (10 лет!). Rocky/Alma — аналогично. Подходит для: Enterprise-окружения с compliance требованиями Компании с контрактами Red Hat (получают support + Satellite) Там где нужен SELinux из коробки в полном объёме Серверы баз данных Oracle (Oracle Linux — тоже RHEL-клон) # Rocky Linux: подписка на обновления безопасности (бесплатно) sudo dnf install epel-release sudo dnf update # Проверка SELinux статуса getenforce # Enforcing / Permissive / Disabled sestatus # RHEL-специфика: subscription-manager sudo subscription-manager status sudo subscription-manager repos --list-enabled Alpine LinuxЦикл: rolling release (stable branches с ~2 годами поддержки). Alpine — минималистичный дистрибутив: musl libc вместо glibc, busybox вместо GNU coreutils, базовый образ 5 МБ. Создан для контейнеров и встраиваемых систем. Подходит для: Docker-образы (но есть нюансы с musl!) Edge-узлы и шлюзы с ограниченными ресурсами Безопасные "голые" серверы с минимальной поверхностью атаки Осторожно: musl libc != glibc: некоторые Go/C программы ведут себя иначе Не для тех, кто не знает что делает: меньше инструментов отладки Не для продакшена с незнакомым стеком Итоговая таблица выбора Ubuntu LTS Debian Stable Rocky/Alma Alpine Cloud-native ★★★★★ ★★★★ ★★★ ★★★★ Enterprise ★★★ ★★★ ★★★★★ ★★ Стабильность пакетов ★★★★ ★★★★★ ★★★★★ ★★★ Свежесть пакетов ★★★★ ★★★ ★★★ ★★★★★ Безопасность (OOB) ★★★★ ★★★★ ★★★★★ ★★★★ Поддержка сообщества ★★★★★ ★★★★★ ★★★★ ★★★ Простота обслуживания ★★★★★ ★★★★ ★★★ ★★ Рекомендация: для большинства команд без специфических требований — Ubuntu 24.04 LTS или Debian 12. Для enterprise с compliance — Rocky Linux 9. Для контейнеров — Alpine или Distroless. Часть 2: Первичная настройка надёжностиБазовое обновление и автоматические security-патчи# === Ubuntu / Debian === sudo apt update && sudo apt upgrade -y # Автоматические security-обновления (unattended-upgrades) sudo apt install unattended-upgrades apt-listchanges sudo dpkg-reconfigure unattended-upgrades # Конфиг /etc/apt/apt.conf.d/50unattended-upgrades # Оставляем только security, НЕ автоустанавливаем всё подряд sudo tee /etc/apt/apt.conf.d/50unattended-upgrades > /dev/null <<'EOF' Unattended-Upgrade::Allowed-Origins { "${distro_id}:${distro_codename}-security"; "${distro_id}ESMApps:${distro_codename}-apps-security"; "${distro_id}ESM:${distro_codename}-infra-security"; }; Unattended-Upgrade::AutoFixInterruptedDpkg "true"; Unattended-Upgrade::MinimalSteps "true"; Unattended-Upgrade::Remove-Unused-Dependencies "true"; Unattended-Upgrade::Automatic-Reboot "false"; // НЕ перезагружаем автоматически! Unattended-Upgrade::Mail "ops@yourcompany.com"; EOF # === Rocky / AlmaLinux === sudo dnf install dnf-automatic sudo systemctl enable --now dnf-automatic-install.timer # /etc/dnf/automatic.conf: apply_updates = yes, upgrade_type = security Правильная настройка NTPВремя — фундамент для логов, TLS-сертификатов, Kerberos, баз данных. Неправильное время = необъяснимые баги. # Современный стандарт: systemd-timesyncd (простой) или chrony (сложные сети) # chronyd рекомендован для серверов — точнее и поддерживает аппаратные часы sudo apt install chrony # Ubuntu/Debian # или: sudo dnf install chrony # RHEL-based # /etc/chrony.conf sudo tee /etc/chrony.conf > /dev/null <<'EOF' # Используем несколько источников из разных пулов pool 0.ru.pool.ntp.org iburst pool 1.ru.pool.ntp.org iburst pool 2.europe.pool.ntp.org iburst pool 3.pool.ntp.org iburst # Внутренний NTP-сервер (если есть) — даём максимальный приоритет # server 192.168.1.1 prefer iburst # Разрешаем step при большом расхождении (только при старте) makestep 1.0 3 # Дрейф файла driftfile /var/lib/chrony/drift # Логирование logdir /var/log/chrony # Синхронизация RTC (hardware clock) rtcsync EOF sudo systemctl restart chrony chronyc tracking # Статус синхронизации chronyc sources -v # Источники времени systemd: сделать сервисы по-настоящему надёжнымиБольшинство используют systemd только для start/stop/enable. Но в нём есть мощные механизмы для production-надёжности. # /etc/systemd/system/myapp.service — production-grade unit [Unit] Description=My Production Application Documentation=https://docs.mycompany.com/myapp # Зависимости — стартуем ПОСЛЕ того как сеть полностью готова After=network-online.target postgresql.service redis.service Wants=network-online.target Requires=postgresql.service # Условие запуска — только если файл конфига существует ConditionPathExists=/etc/myapp/config.yaml [Service] Type=notify # Приложение сигнализирует о готовности через sd_notify NotifyAccess=main User=myapp Group=myapp WorkingDirectory=/opt/myapp # Переменные окружения из защищённого файла (права 600, владелец root) EnvironmentFile=/etc/myapp/environment ExecStartPre=/opt/myapp/bin/validate-config # Валидация конфига перед стартом ExecStart=/opt/myapp/bin/myapp --config /etc/myapp/config.yaml ExecReload=/bin/kill -HUP $MAINPID # Graceful reload по сигналу HUP # ── Restart политика ────────────────────────────────────────────────────────── Restart=on-failure # Перезапускаем только при сбое (не при systemctl stop) RestartSec=5s # Ждём 5 секунд перед перезапуском StartLimitIntervalSec=120s # Окно для подсчёта неудачных стартов StartLimitBurst=5 # Максимум 5 перезапусков за 120 секунд # После исчерпания — юнит в Failed. Оповестить алертинг! # ── Watchdog ───────────────────────────────────────────────────────────────── # Приложение ДОЛЖНО вызывать sd_notify("WATCHDOG=1") каждые WatchdogSec/2 WatchdogSec=30s # ── Таймауты ───────────────────────────────────────────────────────────────── TimeoutStartSec=60s # Максимальное время на инициализацию TimeoutStopSec=30s # Максимальное время на graceful shutdown KillMode=mixed # Сначала SIGTERM главному процессу, потом SIGKILL всей группе KillSignal=SIGTERM # ── Лимиты ресурсов ────────────────────────────────────────────────────────── LimitNOFILE=65536 # Максимум открытых файлов LimitNPROC=4096 # Максимум процессов/потоков LimitMEMLOCK=infinity # Для приложений с mlock (JVM, некоторые БД) # ── Безопасность (sandboxing) ───────────────────────────────────────────────── NoNewPrivileges=true # Запрет повышения привилегий PrivateTmp=true # Изолированный /tmp PrivateDevices=true # Запрет доступа к /dev (кроме basic) ProtectSystem=strict # /usr, /boot, /etc только на чтение ProtectHome=true # Нет доступа к /home, /root ReadWritePaths=/var/lib/myapp /var/log/myapp # Разрешаем запись только сюда CapabilityBoundingSet=CAP_NET_BIND_SERVICE # Только если нужен порт < 1024 # ── Логирование ────────────────────────────────────────────────────────────── StandardOutput=journal StandardError=journal SyslogIdentifier=myapp [Install] WantedBy=multi-user.target # Анализ конфигурации unit (покажет ошибки и предупреждения) systemd-analyze verify /etc/systemd/system/myapp.service # Просмотр "дерева" зависимостей systemd-analyze critical-chain myapp.service # Лимиты запущенного сервиса cat /proc/$(systemctl show -p MainPID --value myapp)/limits # История перезапусков journalctl -u myapp --since "7 days ago" | grep -E "Started|Stopped|Failed" Настройка ulimits и системных лимитов# /etc/security/limits.conf — лимиты для пользователей # Для высоконагруженного приложения (например, веб-сервер, БД) myapp soft nofile 65536 myapp hard nofile 65536 myapp soft nproc 65536 myapp hard nproc 65536 # Для Elasticsearch / Kafka / других Java-приложений elasticsearch soft memlock unlimited elasticsearch hard memlock unlimited elasticsearch soft nofile 65536 elasticsearch hard nofile 65536 # /etc/systemd/system.conf — глобальные лимиты для systemd-процессов # DefaultLimitNOFILE=65536 # DefaultLimitNPROC=65536 # Проверка текущих лимитов процесса cat /proc/$(pgrep myapp | head -1)/limits Часть 3: Тюнинг ядра для продакшенаПараметры sysctl: сетьЭто самое важное для highload-серверов. По умолчанию Linux оптимизирован для десктопа, не для сервера. # /etc/sysctl.d/99-production.conf sudo tee /etc/sysctl.d/99-production.conf > /dev/null <<'EOF' # ════════════════════════════════════════════════════════════════════ # СЕТЬ — TCP/IP стек # ════════════════════════════════════════════════════════════════════ # Буферы сокетов (receive/send) # Для highload: 128 МБ (default ~212 КБ — катастрофически мало!) net.core.rmem_max = 134217728 # 128 МБ net.core.wmem_max = 134217728 net.core.rmem_default = 31457280 # 30 МБ net.core.wmem_default = 31457280 # TCP-специфичные буферы (min, default, max) net.ipv4.tcp_rmem = 4096 87380 134217728 net.ipv4.tcp_wmem = 4096 65536 134217728 # Очередь подключений (backlog) # Должна совпадать с параметром backlog в listen() вашего приложения net.core.somaxconn = 65535 net.core.netdev_max_backlog = 65535 # SYN-очередь (защита от SYN flood + highload accept) net.ipv4.tcp_max_syn_backlog = 65535 # TIME_WAIT: ускоряем переиспользование сокетов net.ipv4.tcp_tw_reuse = 1 # Переиспользовать TIME_WAIT сокеты (безопасно) net.ipv4.tcp_fin_timeout = 15 # Сократить FIN_WAIT таймаут (default 60 сек) # Диапазон эфемерных портов (для outbound соединений) net.ipv4.ip_local_port_range = 1024 65535 # Keepalive — обнаружение мёртвых соединений net.ipv4.tcp_keepalive_time = 120 # Начинаем проверку после 120 сек простоя net.ipv4.tcp_keepalive_intvl = 10 # Интервал между пробами net.ipv4.tcp_keepalive_probes = 6 # Количество проб # Алгоритм управления перегрузкой (congestion control) # BBR — современный алгоритм Google, значительно лучше для WAN и highload net.core.default_qdisc = fq net.ipv4.tcp_congestion_control = bbr # Быстрый открытый TCP (TFO) — уменьшает latency на 1 RTT # (убедитесь что клиенты поддерживают) net.ipv4.tcp_fastopen = 3 # 1=клиент, 2=сервер, 3=оба # Защита от мусорных RST пакетов net.ipv4.tcp_rfc1337 = 1 # ════════════════════════════════════════════════════════════════════ # ПАМЯТЬ # ════════════════════════════════════════════════════════════════════ # vm.swappiness: насколько агрессивно использовать swap # 0 = swap только при крайней необходимости (для БД и latency-критичных) # 10 = рекомендуется для production-серверов # 60 = default (слишком агрессивный для сервера) vm.swappiness = 10 # Dirty pages: когда сбрасывать на диск # Уменьшаем для предсказуемой latency (не даём накопиться большому flush) vm.dirty_ratio = 10 # Начинаем sync при 10% RAM dirty (default 20%) vm.dirty_background_ratio = 3 # Фоновый flush начинается при 3% (default 10%) vm.dirty_writeback_centisecs = 500 # Интервал фонового flush (5 сек, default 5 сек) vm.dirty_expire_centisecs = 3000 # Страницы считаются "старыми" через 30 сек # Overcommit (для Java/JVM и других приложений с большими heap) # 0 = эвристика ядра (default) # 1 = всегда overcommit (опасно, но нужно для некоторых БД) # 2 = строгий лимит (CommitLimit = swap + vm.overcommit_ratio% от RAM) vm.overcommit_memory = 0 # OOM Killer: штраф для критических процессов # Устанавливается через /proc или systemd OOMPolicy # (ниже — пример для PostgreSQL) # ════════════════════════════════════════════════════════════════════ # ФАЙЛОВАЯ СИСТЕМА # ════════════════════════════════════════════════════════════════════ # Максимум открытых файлов в системе (не путать с per-process ulimit) fs.file-max = 2097152 # Inotify: для приложений, следящих за файлами (Docker, K8s, IDE) fs.inotify.max_user_watches = 524288 fs.inotify.max_user_instances = 512 # AIO: асинхронный ввод-вывод (PostgreSQL, некоторые БД) fs.aio-max-nr = 1048576 # ════════════════════════════════════════════════════════════════════ # БЕЗОПАСНОСТЬ # ════════════════════════════════════════════════════════════════════ # Запрет разыменования символических ссылок в /tmp (защита от атак) fs.protected_symlinks = 1 fs.protected_hardlinks = 1 # Запрет записи в память выполняемых файлов fs.protected_regular = 2 fs.protected_fifos = 2 # ASLR (Address Space Layout Randomization) kernel.randomize_va_space = 2 # Полная рандомизация # Запрет SysRq (кроме sync/reboot — оставляем для экстренного случая) kernel.sysrq = 16 # 16 = только sync # Защита от Spectre/Meltdown через ptrace kernel.yama.ptrace_scope = 1 # Core dumps только в определённую директорию kernel.core_pattern = /var/crash/core.%e.%p.%t kernel.core_uses_pid = 1 EOF # Применить без перезагрузки sudo sysctl --system # Проверить конкретный параметр sysctl net.ipv4.tcp_congestion_control Hugepages: критично для баз данныхHugePages уменьшают нагрузку на TLB (Translation Lookaside Buffer) при работе с большими объёмами памяти. PostgreSQL, Oracle, MySQL InnoDB, Elasticsearch, Redis — все выигрывают от HugePages. # Проверить текущее состояние cat /proc/meminfo | grep -i huge # HugePages_Total: 0 ← не настроены # Hugepagesize: 2048 kB ← 2 МБ каждая страница # Рассчитать сколько нужно: # Для PostgreSQL: shared_buffers + прочая SHM / 2 МБ # Например: 16 ГБ shared_buffers → 16384 МБ / 2 МБ = 8192 страниц # /etc/sysctl.d/99-hugepages.conf echo "vm.nr_hugepages = 8192" | sudo tee /etc/sysctl.d/99-hugepages.conf # Или прозрачные HugePages (THP) — автоматически, но с latency spikes! # Для БД (особенно Redis) — ОТКЛЮЧИТЬ THP: echo never | sudo tee /sys/kernel/mm/transparent_hugepage/enabled echo never | sudo tee /sys/kernel/mm/transparent_hugepage/defrag # Сделать постоянным через rc.local или systemd service: sudo tee /etc/systemd/system/disable-thp.service > /dev/null <<'EOF' [Unit] Description=Disable Transparent Huge Pages DefaultDependencies=false After=sysinit.target local-fs.target [Service] Type=oneshot ExecStart=/bin/sh -c 'echo never > /sys/kernel/mm/transparent_hugepage/enabled' ExecStart=/bin/sh -c 'echo never > /sys/kernel/mm/transparent_hugepage/defrag' RemainAfterExit=yes [Install] WantedBy=multi-user.target EOF sudo systemctl enable --now disable-thp Планировщик I/O: под задачу# Посмотреть текущий планировщик для диска cat /sys/block/sda/queue/scheduler # [mq-deadline] kyber bfq none # Выбор планировщика: # none (noop) — для NVMe SSD и виртуальных дисков (SSD сами управляют очередью) # mq-deadline — универсальный, хорош для смешанных нагрузок # bfq — для десктопов и интерактивных задач (не для продакшена) # kyber — для NVMe с очень низкой latency # Для NVMe SSD — none: echo none | sudo tee /sys/block/nvme0n1/queue/scheduler # Для SATA SSD — mq-deadline: echo mq-deadline | sudo tee /sys/block/sda/queue/scheduler # Через udev (постоянно): sudo tee /etc/udev/rules.d/60-ioschedulers.rules > /dev/null <<'EOF' # NVMe ACTION=="add|change", KERNEL=="nvme[0-9]*", ATTR{queue/scheduler}="none" # SATA/SAS SSD (по ROTATIONAL=0) ACTION=="add|change", KERNEL=="sd[a-z]|xvd[a-z]", ATTR{queue/rotational}=="0", ATTR{queue/scheduler}="mq-deadline" # HDD (по ROTATIONAL=1) ACTION=="add|change", KERNEL=="sd[a-z]", ATTR{queue/rotational}=="1", ATTR{queue/scheduler}="bfq" EOF # Дополнительные параметры очереди диска: # Глубина очереди запросов (для NVMe можно больше) cat /sys/block/nvme0n1/queue/nr_requests # default 64 echo 1024 | sudo tee /sys/block/nvme0n1/queue/nr_requests # Read-ahead (предварительное чтение) # Для последовательного чтения (логи, backup): увеличить # Для random I/O (OLTP БД): уменьшить blockdev --setra 256 /dev/sda # 128 КБ (256 × 512 байт) OOM Killer: кого убивать в последнюю очередь# OOM killer убивает процессы когда RAM исчерпана. # Значение oom_score_adj: от -1000 (никогда не убивать) до +1000 (убить первым) # Защитить критические процессы (PostgreSQL, основное приложение): echo -1000 | sudo tee /proc/$(pgrep postgres | head -1)/oom_score_adj # В systemd unit — правильный способ: # [Service] # OOMScoreAdjust=-900 # -1000 зарезервирован для ядра # Для throaway-процессов (worker, task runner) — пусть убивают первыми: # OOMScoreAdjust=500 # Включить расширенное логирование OOM kill: echo 1 | sudo tee /proc/sys/vm/oom_dump_tasks # Посмотреть oom_score всех процессов for p in /proc/[0-9]*/; do pid=$(basename $p) comm=$(cat $p/comm 2>/dev/null) score=$(cat $p/oom_score 2>/dev/null) adj=$(cat $p/oom_score_adj 2>/dev/null) [ -n "$score" ] && echo "$score $adj $pid $comm" done | sort -rn | head -20 Часть 4: Файловые системы и хранилищеВыбор файловой системыext4 — проверенный стандарт, но без поддержки checksums данных (только метаданных). XFS — отличная производительность на больших файлах и параллельном I/O. Default в RHEL. Нельзя уменьшить (только увеличить). Btrfs — copy-on-write, встроенные checksums, снапшоты. Хорош для систем где нужны снапшоты без LVM. В продакшене требует RAID-конфигурации для надёжности. ZFS — самая надёжная ФС с end-to-end checksums, встроенным RAID-Z, дедупликацией. Требует отдельной установки на Linux. Рекомендуется для хранилищ данных. # Монтирование ext4 с оптимальными опциями для продакшена: # /etc/fstab: # /dev/sdb1 /data ext4 defaults,noatime,lazytime,errors=remount-ro 0 2 # noatime — не обновлять atime при каждом чтении (значительно снижает I/O) # lazytime — обновлять временны́е метки только при flush (компромисс) # errors=remount-ro — при ошибке ФС переходит в read-only вместо паники # XFS с journalling только на metadata (для производительности): # /dev/sdb1 /data xfs defaults,noatime,logbsize=256k 0 2 # Проверка здоровья файловой системы: sudo tune2fs -l /dev/sdb1 | grep -E "Last checked|Mount count|Max mount count" sudo xfs_info /data sudo xfs_repair -n /data # Проверка XFS без исправления LVM: гибкое управление томамиLVM (Logical Volume Manager) — обязателен для production-серверов. Позволяет расширять разделы без downtime, создавать снапшоты для бэкапов. # Создание LVM структуры: # 1. Physical Volumes (PV) sudo pvcreate /dev/sdb /dev/sdc # 2. Volume Group (VG) sudo vgcreate data_vg /dev/sdb /dev/sdc sudo vgs # Проверка # 3. Logical Volumes (LV) sudo lvcreate -L 100G -n postgres_lv data_vg # 100 ГБ для PostgreSQL sudo lvcreate -L 50G -n logs_lv data_vg # 50 ГБ для логов sudo lvcreate -l 100%FREE -n backup_lv data_vg # Остаток под бэкапы # Форматирование sudo mkfs.xfs /dev/data_vg/postgres_lv sudo mkfs.ext4 /dev/data_vg/logs_lv # Расширение без downtime (XFS умеет расти онлайн): sudo lvextend -L +50G /dev/data_vg/postgres_lv # Добавить 50 ГБ к LV sudo xfs_growfs /var/lib/postgresql # Расширить ФС # Снапшот для онлайн-бэкапа: sudo lvcreate -L 10G -s -n postgres_snap /dev/data_vg/postgres_lv sudo mount -o ro /dev/data_vg/postgres_snap /mnt/backup_snap # rsync с /mnt/backup_snap → бэкап без остановки БД sudo umount /mnt/backup_snap sudo lvremove -f /dev/data_vg/postgres_snap Часть 5: Мониторинг и observabilityВстроенные инструменты диагностики# ── Производительность системы ─────────────────────────────────────────────── # top на стероидах: htop sudo apt install htop # Статистика I/O по процессам sudo iotop -o # -o = только активные процессы # Сетевые соединения (замена netstat) ss -tunap # TCP/UDP, номера, приложения, pid ss -s # Сводная статистика ss 'state established' # Только ESTABLISHED # Дисковый I/O в реальном времени iostat -xz 1 # Расширенная статистика, без нулей, обновление 1с # Нагрузка на сеть по интерфейсам sar -n DEV 1 # Из пакета sysstat # Сколько памяти реально свободно free -h cat /proc/meminfo | grep -E "MemAvailable|MemFree|Cached|SwapUsed" # ── Анализ производительности ──────────────────────────────────────────────── # perf: профилировщик ядра (какие системные вызовы тормозят) sudo perf top -g # Онлайн profiling sudo perf stat -p $(pgrep myapp) sleep 10 # Статистика за 10 секунд # strace: что делает процесс (дорого, только для диагностики) sudo strace -p $(pgrep myapp) -e trace=network,file -T 2>&1 | head -50 # lsof: открытые файлы и сокеты sudo lsof -p $(pgrep myapp) | wc -l # Сколько открыто sudo lsof -i :8080 # Кто слушает порт 8080 # ── Диагностика сети ───────────────────────────────────────────────────────── # Статистика TCP ошибок (ретрансмиты, dropped пакеты) netstat -s | grep -E "retransmit|failed|overflow|listen" # Или через nstat (более детально): nstat -az | grep -i -E "retrans|drop|overflow|fail" # Потери пакетов на интерфейсе ip -s link show eth0 # RX errors/dropped/overrun — проблемы приёма # TX errors/dropped — проблемы передачи # Traceroute с временами (для диагностики latency) mtr --report --report-cycles 20 8.8.8.8 # TCP-дамп для анализа (осторожно с нагрузкой!) sudo tcpdump -i eth0 -n port 5432 -c 1000 -w /tmp/postgres.pcap # Анализ в Wireshark Настройка централизованного логирования# journald — настройка хранения логов sudo tee /etc/systemd/journald.conf.d/99-production.conf > /dev/null <<'EOF' [Journal] Storage=persistent # Хранить на диске (не только в RAM) Compress=yes # Сжатие SystemMaxUse=2G # Максимум 2 ГБ для системных логов SystemKeepFree=500M # Оставлять 500 МБ свободными MaxRetentionSec=30day # Хранить не дольше 30 дней MaxFileSec=1day # Ротация ежедневно ForwardToSyslog=no # Не дублировать в rsyslog (если не нужно) RateLimitBurst=1000 # Лимит: 1000 сообщений RateLimitIntervalSec=30s # за 30 секунд на единицу EOF sudo systemctl restart systemd-journald # Полезные запросы journalctl: journalctl --since "1 hour ago" -p err # Ошибки за последний час journalctl -u myapp -f --output=json # Поток логов в JSON journalctl --disk-usage # Сколько занимают логи journalctl --vacuum-time=7d # Удалить старше 7 дней # rsyslog → файлы (для совместимости с legacy-инструментами): # /etc/rsyslog.d/99-production.conf sudo tee /etc/rsyslog.d/99-production.conf > /dev/null <<'EOF' # Высокопроизводительный режим $ActionQueueType LinkedList $ActionQueueSize 10000 $ActionResumeRetryCount -1 # Бесконечный retry при недоступности цели # Отправка в centralized syslog (Loki, Graylog, Splunk) *.* @@syslog.internal.example.com:514 # @@ = TCP (надёжнее UDP) EOF Prometheus Node Exporter: метрики для Grafana# Установка Node Exporter (собирает 1000+ метрик системы) wget https://github.com/prometheus/node_exporter/releases/latest/download/node_exporter-*.linux-amd64.tar.gz tar xf node_exporter-*.tar.gz sudo mv node_exporter-*/node_exporter /usr/local/bin/ sudo chmod +x /usr/local/bin/node_exporter # systemd unit для Node Exporter sudo tee /etc/systemd/system/node_exporter.service > /dev/null <<'EOF' [Unit] Description=Prometheus Node Exporter After=network.target [Service] Type=simple User=nobody ExecStart=/usr/local/bin/node_exporter \ --collector.systemd \ --collector.processes \ --collector.interrupts \ --collector.tcpstat \ --collector.diskstats \ --web.listen-address=:9100 \ --web.telemetry-path=/metrics Restart=on-failure RestartSec=5s NoNewPrivileges=true PrivateTmp=true [Install] WantedBy=multi-user.target EOF sudo systemctl enable --now node_exporter # Проверка curl -s http://localhost:9100/metrics | head -20 Ключевые метрики для алертов (Prometheus rules): # /etc/prometheus/rules/node-alerts.yml groups: - name: node-alerts rules: - alert: HighCPULoad expr: 100 - (avg by(instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 85 for: 10m labels: severity: warning annotations: summary: "CPU > 85% на {{ $labels.instance }} в течение 10 мин" - alert: LowMemory expr: node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes * 100 < 10 for: 5m labels: severity: critical annotations: summary: "Доступно < 10% RAM на {{ $labels.instance }}" - alert: DiskSpaceCritical expr: node_filesystem_avail_bytes{fstype!~"tmpfs|fuse.lxcfs"} / node_filesystem_size_bytes * 100 < 10 for: 5m labels: severity: critical annotations: summary: "Диск {{ $labels.mountpoint }} заполнен на 90%+" - alert: HighDiskIOUtilization expr: rate(node_disk_io_time_seconds_total[5m]) * 100 > 80 for: 10m labels: severity: warning annotations: summary: "Диск {{ $labels.device }} загружен на 80%+" - alert: HighNetworkErrorRate expr: rate(node_network_receive_errs_total[5m]) + rate(node_network_transmit_errs_total[5m]) > 10 for: 5m labels: severity: warning - alert: SystemdServiceFailed expr: node_systemd_unit_state{state="failed"} == 1 for: 1m labels: severity: critical annotations: summary: "Сервис {{ $labels.name }} в состоянии Failed" Часть 6: Безопасность в продакшенеSSH: только так и никак иначе# /etc/ssh/sshd_config.d/99-hardening.conf sudo tee /etc/ssh/sshd_config.d/99-hardening.conf > /dev/null <<'EOF' # Только ключи, никаких паролей PasswordAuthentication no ChallengeResponseAuthentication no KbdInteractiveAuthentication no PermitEmptyPasswords no # Запрет root-логина PermitRootLogin no # Только конкретные пользователи или группы AllowGroups sshusers sudo # Современные алгоритмы (убираем слабые) KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org,ecdh-sha2-nistp256 HostKeyAlgorithms ssh-ed25519,rsa-sha2-512,rsa-sha2-256 Ciphers aes256-gcm@openssh.com,aes128-gcm@openssh.com,chacha20-poly1305@openssh.com MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com # Таймаут неактивной сессии (30 минут) ClientAliveInterval 300 ClientAliveCountMax 6 # Защита от брутфорса: максимум 3 попытки аутентификации MaxAuthTries 3 MaxSessions 10 LoginGraceTime 30s # Отключить X11 forwarding и агент forwarding на серверах X11Forwarding no AllowAgentForwarding no # Логировать все подключения LogLevel VERBOSE EOF sudo sshd -t # Проверка синтаксиса sudo systemctl reload sshd UFW / nftables: файрвол# UFW — простой и читаемый интерфейс к iptables/nftables sudo apt install ufw # Политика по умолчанию: всё запрещено входящее, всё разрешено исходящее sudo ufw default deny incoming sudo ufw default allow outgoing # Разрешаем только нужное sudo ufw allow from 10.0.0.0/8 to any port 22 # SSH только из внутренней сети sudo ufw allow 80/tcp sudo ufw allow 443/tcp sudo ufw allow from 10.0.0.0/8 to any port 9100 # Node Exporter только изнутри # Rate limiting для SSH (защита от брутфорса) sudo ufw limit ssh sudo ufw enable sudo ufw status verbose # Логирование отклонённых пакетов sudo ufw logging on fail2ban: автоматическая блокировка атакsudo apt install fail2ban # /etc/fail2ban/jail.local sudo tee /etc/fail2ban/jail.local > /dev/null <<'EOF' [DEFAULT] bantime = 3600 # Блокировка на 1 час findtime = 600 # Окно анализа: 10 минут maxretry = 5 # Максимум попыток # Уведомление по email destemail = ops@yourcompany.com sender = fail2ban@yourserver.example.com action = %(action_mwl)s # Ban + email с логами # Белый список (никогда не блокировать) ignoreip = 127.0.0.1/8 ::1 10.0.0.0/8 192.168.0.0/16 [sshd] enabled = true port = ssh logpath = %(sshd_log)s maxretry = 3 # SSH: только 3 попытки [nginx-http-auth] enabled = true maxretry = 5 [nginx-limit-req] enabled = true maxretry = 10 [nginx-botsearch] enabled = true maxretry = 2 EOF sudo systemctl enable --now fail2ban sudo fail2ban-client status # Общий статус sudo fail2ban-client status sshd # Статус SSH jail auditd: аудит действий на сервереsudo apt install auditd # /etc/audit/rules.d/99-production.rules sudo tee /etc/audit/rules.d/99-production.rules > /dev/null <<'EOF' # Удаляем все текущие правила -D # Максимальный буфер (для highload) -b 8192 # Отказоустойчивость: при ошибке — продолжать (не паниковать) -f 1 # ── Критические файлы ──────────────────────────────────────────────────────── -w /etc/passwd -p wa -k identity -w /etc/shadow -p wa -k identity -w /etc/sudoers -p wa -k sudo_changes -w /etc/ssh/ -p wa -k ssh_config # Системные бинарники -w /usr/bin/sudo -p x -k privileged -w /usr/bin/su -p x -k privileged -w /bin/chmod -p x -k privileged -w /bin/chown -p x -k privileged # Изменения конфигурации cron -w /etc/cron.d/ -p wa -k cron -w /var/spool/cron/ -p wa -k cron # Загрузка/выгрузка модулей ядра -w /sbin/insmod -p x -k kernel_modules -w /sbin/rmmod -p x -k kernel_modules -a always,exit -F arch=b64 -S init_module -k kernel_modules # Сетевые изменения -a always,exit -F arch=b64 -S sethostname -S setdomainname -k system-locale -w /etc/hosts -p wa -k hosts -w /etc/network/ -p wa -k network # Неудачные системные вызовы (попытки эскалации привилегий) -a always,exit -F arch=b64 -S open -F exit=-EACCES -k access_denied -a always,exit -F arch=b64 -S open -F exit=-EPERM -k access_denied # Мониторинг директории приложения -w /opt/myapp/bin/ -p x -k app_exec EOF sudo augenrules --load # Загрузить правила sudo auditctl -l # Проверить активные правила ausearch -k sudo_changes --start today # Посмотреть события sudo aureport --summary # Сводный отчёт Часть 7: Процедуры и автоматизация обслуживанияСкрипт проверки здоровья сервера#!/bin/bash # /usr/local/bin/health-check.sh — ежедневный health check # Запускать через cron: 0 7 * * * /usr/local/bin/health-check.sh set -euo pipefail REPORT_FILE="/var/log/health-check-$(date +%Y%m%d).log" ALERT_EMAIL="ops@yourcompany.com" ISSUES=0 log() { echo "[$(date '+%H:%M:%S')] $*" | tee -a "$REPORT_FILE"; } warn() { log "⚠️ WARN: $*"; ((ISSUES++)); } err() { log "❌ ERROR: $*"; ((ISSUES++)); } ok() { log "✅ OK: $*"; } log "=== Health Check: $(hostname) === $(date) ===" # ── Свободное место на дисках ──────────────────────────────────────────────── log "--- Дисковое пространство ---" while IFS= read -r line; do usage=$(echo "$line" | awk '{print $5}' | tr -d '%') mount=$(echo "$line" | awk '{print $6}') [ "$usage" -ge 90 ] && err "Диск $mount заполнен на $usage%" && continue [ "$usage" -ge 80 ] && warn "Диск $mount заполнен на $usage%" && continue ok "Диск $mount: $usage%" done < <(df -h --output=pcent,target -x tmpfs -x devtmpfs | tail -n +2) # ── Память ─────────────────────────────────────────────────────────────────── log "--- Память ---" available_mb=$(awk '/MemAvailable/ {printf "%.0f", $2/1024}' /proc/meminfo) total_mb=$(awk '/MemTotal/ {printf "%.0f", $2/1024}' /proc/meminfo) used_pct=$(( (total_mb - available_mb) * 100 / total_mb )) swap_used=$(awk '/SwapTotal/{t=$2} /SwapFree/{f=$2} END{printf "%.0f", (t-f)/1024}' /proc/meminfo) [ "$used_pct" -ge 90 ] && err "RAM: использовано $used_pct%" [ "$used_pct" -ge 80 ] && warn "RAM: использовано $used_pct%" ok "RAM: $used_pct% использовано ($available_mb МБ свободно)" [ "$swap_used" -gt 100 ] && warn "Swap: используется ${swap_used} МБ" # ── Failed сервисы ─────────────────────────────────────────────────────────── log "--- Systemd сервисы ---" failed=$(systemctl list-units --state=failed --no-legend --no-pager | awk '{print $1}') if [ -n "$failed" ]; then for svc in $failed; do err "Сервис в Failed: $svc" done else ok "Все сервисы работают" fi # ── Нагрузка ───────────────────────────────────────────────────────────────── log "--- CPU загрузка ---" cpu_cores=$(nproc) load_avg=$(cut -d' ' -f1 /proc/loadavg) load_int=$(echo "$load_avg" | cut -d. -f1) [ "$load_int" -ge "$((cpu_cores * 2))" ] && err "Load average $load_avg при $cpu_cores ядрах" [ "$load_int" -ge "$cpu_cores" ] && warn "Load average $load_avg при $cpu_cores ядрах" ok "Load average: $load_avg (ядер: $cpu_cores)" # ── SSL-сертификаты ────────────────────────────────────────────────────────── log "--- SSL сертификаты ---" for cert_file in /etc/letsencrypt/live/*/cert.pem /etc/ssl/certs/*.pem; do [ -f "$cert_file" ] || continue expiry=$(openssl x509 -in "$cert_file" -noout -enddate 2>/dev/null \ | cut -d= -f2) expiry_epoch=$(date -d "$expiry" +%s 2>/dev/null || continue) days_left=$(( (expiry_epoch - $(date +%s)) / 86400 )) [ "$days_left" -le 7 ] && err "Сертификат $cert_file истекает через $days_left дней!" [ "$days_left" -le 30 ] && warn "Сертификат $cert_file истекает через $days_left дней" [ "$days_left" -gt 30 ] && ok "Сертификат $cert_file: осталось $days_left дней" done # ── Итог ───────────────────────────────────────────────────────────────────── log "" log "=== Итог: $ISSUES проблем(а) ===" if [ "$ISSUES" -gt 0 ]; then mail -s "⚠️ Health Check FAILED: $(hostname) — $ISSUES проблем" \ "$ALERT_EMAIL" < "$REPORT_FILE" fi exit $((ISSUES > 0 ? 1 : 0)) Автоматизированный бэкап с проверкой целостности#!/bin/bash # /usr/local/bin/backup.sh — ежедневный инкрементальный бэкап set -euo pipefail BACKUP_DIR="/var/backup" REMOTE="backup-user@backup-server.example.com:/backups/$(hostname)" DATE=$(date +%Y%m%d-%H%M) LOG="/var/log/backup.log" RETENTION_DAYS=30 log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG"; } log "=== Бэкап начат: $DATE ===" # ── Конфигурация ───────────────────────────────────────────────────────────── rsync -avz --delete \ --exclude='*.tmp' \ --exclude='*.log' \ --exclude='/proc' \ --exclude='/sys' \ --exclude='/dev' \ --exclude='/run' \ --exclude='/tmp' \ --link-dest="$REMOTE/latest" \ / \ "$REMOTE/$DATE/" \ --log-file="$LOG" \ 2>&1 # Обновляем симлинк на последний бэкап ssh backup-user@backup-server.example.com \ "ln -sfn /backups/$(hostname)/$DATE /backups/$(hostname)/latest" # ── Проверка целостности (checksums) ───────────────────────────────────────── log "Генерация checksums..." find /etc /opt/myapp /var/lib -type f -newer /var/backup/.last_backup \ -exec sha256sum {} \; > "/var/backup/checksums-$DATE.txt" 2>/dev/null || true rsync -avz "/var/backup/checksums-$DATE.txt" \ "$REMOTE/checksums/$DATE.txt" touch /var/backup/.last_backup # ── Очистка старых бэкапов ─────────────────────────────────────────────────── log "Очистка бэкапов старше $RETENTION_DAYS дней..." ssh backup-user@backup-server.example.com \ "find /backups/$(hostname) -maxdepth 1 -type d -mtime +$RETENTION_DAYS \ ! -name 'latest' -exec rm -rf {} + 2>/dev/null; echo done" log "=== Бэкап завершён успешно ===" Быстрая шпаргалка: что сделать на каждом новом сервере#!/bin/bash # Минимальный чеклист для нового production-сервера echo "=== 1. Обновить систему ===" apt update && apt upgrade -y # или dnf update -y echo "=== 2. Создать admin-пользователя ===" adduser deploy usermod -aG sudo deploy mkdir -p /home/deploy/.ssh cp ~/.ssh/authorized_keys /home/deploy/.ssh/ chown -R deploy:deploy /home/deploy/.ssh chmod 700 /home/deploy/.ssh chmod 600 /home/deploy/.ssh/authorized_keys echo "=== 3. Настроить SSH ===" sed -i 's/#PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config sed -i 's/PermitRootLogin yes/PermitRootLogin no/' /etc/ssh/sshd_config systemctl reload sshd echo "=== 4. Файрвол ===" ufw --force enable ufw default deny incoming ufw allow from 10.0.0.0/8 to any port 22 ufw allow 80 443 echo "=== 5. NTP ===" apt install -y chrony systemctl enable --now chrony echo "=== 6. Применить sysctl тюнинг ===" # (скопировать /etc/sysctl.d/99-production.conf из шаблона) sysctl --system echo "=== 7. Node Exporter ===" # (установить и включить — см. раздел выше) echo "=== 8. fail2ban ===" apt install -y fail2ban systemctl enable --now fail2ban echo "=== 9. Unattended upgrades ===" apt install -y unattended-upgrades dpkg-reconfigure -f noninteractive unattended-upgrades echo "=== 10. Базовый мониторинг диска ===" # Добавить в cron: 0 7 * * * /usr/local/bin/health-check.sh echo "✅ Базовая настройка завершена. Не забыть:" echo " - Настроить бэкап" echo " - Подключить к Prometheus/Grafana" echo " - Добавить в систему конфигурационного управления (Ansible/Salt)" echo " - Задокументировать сервер в CMDB" ЗаключениеProduction Linux — это не дистрибутив, это дисциплина. Правильный выбор дистрибутива даёт фундамент. Тюнинг ядра и sysctl — производительность. systemd с правильными политиками — надёжность. Мониторинг — видимость. Безопасность — защиту. А автоматизация обслуживания — предсказуемость. Самые частые провалы в продакшене: Не настроены лимиты (ulimit, systemd LimitNOFILE) — приложение падает при нагрузке Нет мониторинга диска — о заполнении узнают по жалобам пользователей SSH доступен по паролям — вопрос не "взломают ли", а "когда" Не тестируются бэкапы — они есть, но не работают когда нужны Нет процедуры обновления — серверы не обновляются годами Используйте Ansible или Terraform для воспроизводимости: каждая настройка из этой статьи должна быть в коде, а не только в голове у одного администратора. Сервер должен разворачиваться автоматически — это единственная гарантия того, что в 3 часа ночи вы сможете его поднять заново.
-
Тюнинг производительности Nginx в продакшене: полное руководство
С чего начинается тюнинг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: Системные настройки LinuxNginx ограничен операционной системой. Без правильной настройки 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 cacheProxy 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 — защита и QoShttp { # ===== ЗОНЫ 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 limitinghttp { # Карта: 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 — это итеративный процесс. Хороший порядок: Системный уровень: sysctl, ulimit, BBR — без этого упрётесь в ОС Workers и events: worker_processes auto, multi_accept on, epoll Буферы и таймауты: адаптируйте под характер трафика (размер ответов, скорость клиентов) Кэш: proxy_cache или fastcgi_cache — самый большой прирост производительности Upstream keepalive: критично для высоких RPS SSL оптимизация: session cache + stapling + HTTP/2 Сжатие: gzip + brotli_static для предкомпрессии Rate limiting: защита без ущерба для легитимного трафика Измеряйте до и после каждого изменения. Доверяйте цифрам, а не интуиции. И помните: лучший тюнинг — тот, который решает вашу конкретную проблему, а не скопированный из статьи.
-
PostgreSQL 16/17/18: администрирование и тюнинг производительности в продакшене
Введение: PostgreSQL в продакшене — другой зверьПоднять PostgreSQL локально — просто. Запустить его в продакшене под реальной нагрузкой так, чтобы он не падал, не тормозил и не раздувался до потери диска — это уже инженерия. PostgreSQL 16, 17 и 18 принесли серьёзные улучшения производительности: логическая репликация стала намного мощнее, параллельные запросы умнее, планировщик научился большему. Но дефолтная конфигурация по-прежнему рассчитана на «запустить на ноутбуке с 256 МБ RAM», а не на production-сервер с 128 ГБ памяти. Эта статья — системный разбор всего, что нужно сделать, чтобы PostgreSQL работал быстро, надёжно и предсказуемо. Никакой воды: только параметры, SQL, реальные кейсы. Глава 1. Конфигурация: postgresql.conf с нуляПамять: самые важные параметры# postgresql.conf # shared_buffers — основной кэш PostgreSQL в памяти. # Правило: 25-40% от RAM сервера. # На 64 ГБ RAM: 16 ГБ shared_buffers = 16GB # effective_cache_size — подсказка планировщику, сколько памяти # доступно для кэширования (shared_buffers + OS page cache). # Правило: 50-75% от RAM. # На 64 ГБ RAM: 48 ГБ effective_cache_size = 48GB # work_mem — память для одной операции сортировки/хэширования # ВНИМАНИЕ: умножается на число параллельных запросов × число операций в плане! # На сервере с 500 соединениями и work_mem=256MB → потенциально 128 ГБ! # Разумно: 4-64 МБ для OLTP, 256 МБ-1 ГБ для аналитики work_mem = 32MB # maintenance_work_mem — для VACUUM, CREATE INDEX, ALTER TABLE # Больше = быстрее индексы и вакуум. Безопасно давать больше, чем work_mem. maintenance_work_mem = 2GB # huge_pages — используем hugepages Linux для shared_buffers # Обязательно для shared_buffers > 8 ГБ huge_pages = on WAL и checkpoint: баланс между скоростью и надёжностью# wal_level — минимальный уровень для репликации # replica — для физической репликации # logical — для логической репликации (больше overhead) wal_level = replica # Размер WAL буфера (с PostgreSQL 16 wal_buffers=auto работает хорошо) wal_buffers = 64MB # checkpoint_completion_target — размазываем запись checkpoint во времени # 0.9 означает: записать грязные страницы за 90% интервала между checkpoint checkpoint_completion_target = 0.9 # max_wal_size — максимальный объём WAL между checkpoint # При большой нагрузке на запись увеличьте до 4-16 ГБ # Это НЕ размер хранилища WAL, а порог для инициации checkpoint max_wal_size = 4GB # min_wal_size — минимальный резерв WAL файлов min_wal_size = 1GB # wal_compression — сжатие WAL (PostgreSQL 15+: поддержка lz4, zstd) # Снижает I/O, небольшой CPU overhead wal_compression = lz4 Параллелизм (PostgreSQL 16+)# max_worker_processes — общий пул фоновых процессов max_worker_processes = 16 # max_parallel_workers_per_gather — параллельные воркеры на один запрос # Правило: не более числа физических ядер / 2 max_parallel_workers_per_gather = 4 # max_parallel_workers — суммарно параллельных воркеров max_parallel_workers = 8 # max_parallel_maintenance_workers — для CREATE INDEX CONCURRENTLY, VACUUM max_parallel_maintenance_workers = 4 # parallel_tuple_cost, parallel_setup_cost — влияют на решение планировщика # использовать параллельность. Снизить если параллельные планы не строятся. parallel_tuple_cost = 0.1 parallel_setup_cost = 100 Соединения# max_connections — ОСТОРОЖНО! Каждое соединение ≈ 5-10 МБ памяти. # При PgBouncer: достаточно 100-200 серверных соединений. # Без пула: реальное число ≤ 200-300 max_connections = 200 # superuser_reserved_connections — резерв для DBA superuser_reserved_connections = 5 Планировщик: тонкая настройка# random_page_cost — стоимость случайного чтения страницы. # Для SSD: 1.1-1.5 (против дефолта 4.0 для HDD). # Занижение → планировщик чаще выбирает Index Scan. random_page_cost = 1.1 # seq_page_cost — стоимость последовательного чтения (база = 1.0) seq_page_cost = 1.0 # effective_io_concurrency — параллельных I/O для Bitmap Heap Scan # SSD: 200-300, HDD: 2-4, NVMe: 500+ effective_io_concurrency = 200 # default_statistics_target — точность статистики для планировщика # Дефолт 100. Для колонок с высокой кардинальностью — до 500. default_statistics_target = 200 # enable_partitionwise_join — важно для партиционированных таблиц enable_partitionwise_join = on enable_partitionwise_aggregate = on Глава 2. Индексная стратегияПравильные индексы — половина успеха. Неправильные — гарантированный bloat и тормоза на INSERT/UPDATE. Типы индексов: когда что использоватьB-Tree — дефолт, для равенства и диапазонов: -- Стандартный случай CREATE INDEX idx_orders_user_id ON orders(user_id); -- Частичный индекс — только активные записи -- Занимает меньше места, быстрее обновляется CREATE INDEX idx_orders_active ON orders(created_at) WHERE status = 'active'; -- Покрывающий индекс (INCLUDE) — избегаем обращения к таблице -- PostgreSQL 11+, активно улучшен в 16/17 CREATE INDEX idx_orders_cover ON orders(user_id) INCLUDE (total_amount, status, created_at); -- Составной: порядок имеет значение! -- Ставьте впереди колонки с высокой кардинальностью -- и те, по которым фильтрация точнее CREATE INDEX idx_orders_composite ON orders(user_id, status, created_at); GIN — для массивов, JSONB, полнотекстового поиска: -- JSONB поиск CREATE INDEX idx_products_attrs ON products USING GIN(attributes); -- Полнотекстовый поиск CREATE INDEX idx_articles_fts ON articles USING GIN(to_tsvector('russian', title || ' ' || body)); -- Поиск в массивах CREATE INDEX idx_tags ON posts USING GIN(tags); BRIN — для очень больших таблиц с естественной сортировкой: -- Для таблиц логов, временных рядов — экономия места 99%+ -- BRIN не хранит каждое значение, только мин/макс по блокам CREATE INDEX idx_events_time_brin ON events USING BRIN(created_at) WITH (pages_per_range = 128); -- PostgreSQL 14+: bloom filter в BRIN CREATE INDEX idx_events_bloom ON events USING BRIN(device_id, created_at) WITH (pages_per_range = 64); Hash — только для точного равенства, быстрее B-Tree: CREATE INDEX idx_sessions_token ON sessions USING HASH(session_token); Найти неиспользуемые и дублирующие индексы-- Неиспользуемые индексы (кандидаты на удаление) SELECT schemaname, tablename, indexname, pg_size_pretty(pg_relation_size(indexrelid)) AS index_size, idx_scan, idx_tup_read, idx_tup_fetch FROM pg_stat_user_indexes WHERE idx_scan = 0 AND indexrelname NOT LIKE 'pg_%' ORDER BY pg_relation_size(indexrelid) DESC; -- Дублирующие индексы SELECT indrelid::regclass AS table_name, array_agg(indexrelid::regclass) AS indexes, array_agg(indkey) AS index_keys FROM pg_index GROUP BY indrelid, indkey HAVING count(*) > 1; -- Индексы vs размер таблицы: раздутые индексы SELECT t.tablename, pg_size_pretty(pg_total_relation_size(t.tablename::regclass)) AS total, pg_size_pretty(pg_relation_size(t.tablename::regclass)) AS table_size, pg_size_pretty( pg_total_relation_size(t.tablename::regclass) - pg_relation_size(t.tablename::regclass) ) AS indexes_size, round( (pg_total_relation_size(t.tablename::regclass) - pg_relation_size(t.tablename::regclass))::numeric / nullif(pg_total_relation_size(t.tablename::regclass), 0) * 100, 1 ) AS index_ratio_pct FROM pg_tables t WHERE t.schemaname = 'public' ORDER BY pg_total_relation_size(t.tablename::regclass) DESC LIMIT 30; Глава 3. EXPLAIN ANALYZE: читаем план запроса как профессионалEXPLAIN ANALYZE — главный инструмент оптимизации. Без него — гадание на кофейной гуще. -- Всегда используйте все опции EXPLAIN ( ANALYZE, -- Реально выполнить и показать время BUFFERS, -- Показать попадания/промахи кэша FORMAT TEXT, -- или JSON для авто-анализа TIMING ON, -- Время каждого узла SETTINGS ON, -- Показать изменённые параметры WAL ON -- PostgreSQL 13+: WAL активность ) SELECT ...; Анатомия плана: на что смотретьEXPLAIN (ANALYZE, BUFFERS) SELECT o.id, o.total, u.email FROM orders o JOIN users u ON u.id = o.user_id WHERE o.created_at > NOW() - INTERVAL '7 days' AND o.status = 'completed'; -- Типичный вывод: -- QUERY PLAN -- Hash Join (cost=1250.00..8934.21 rows=1523 width=48) (actual time=45.231..189.443 rows=1287 loops=1) -- Buffers: shared hit=4521 read=2341 ← read > 0 = данных нет в кэше -- Hash Cond: (o.user_id = u.id) -- -> Bitmap Heap Scan on orders o (cost=87.3..7512.4 rows=1523 width=32) -- (actual time=2.341..145.231 rows=1287 loops=1) -- Recheck Cond: (created_at > (now() - '7 days'::interval)) -- Filter: (status = 'completed') -- Rows Removed by Filter: 4521 ← КРАСНЫЙ ФЛАГ: фильтруем 4521 строк! -- Heap Blocks: exact=1823 -- Buffers: shared hit=123 read=1823 -- -> Bitmap Index Scan on idx_orders_created_at -- Index Cond: (created_at > (now() - '7 days'::interval)) -- -> Hash (cost=890.00..890.00 rows=21000 width=24) (actual time=42.3..42.3 rows=21000 loops=1) -- Buckets: 32768 Batches: 1 Memory Usage: 1856kB -- Buffers: shared hit=4398 read=518 -- -> Seq Scan on users u (cost=0.00..890.00 rows=21000 width=24) -- Planning Time: 1.234 ms -- Execution Time: 190.123 ms ← Реальное время! Красные флаги в плане: Признак Проблема Решение Rows Removed by Filter >> возвращаемых строк Индекс не покрывает все условия Добавить колонку status в индекс actual rows >> estimated rows (×10+) Устаревшая статистика ANALYZE table или повысить default_statistics_target Seq Scan на большой таблице Нет подходящего индекса Создать индекс Batches: N (N > 1) в Hash Join Хэш-таблица не помещается в work_mem Увеличить work_mem или оптимизировать запрос loops=N при N×cost = огромно Вложенный цикл на большом наборе Рассмотреть Hash Join / Merge Join shared read >> shared hit Данные не в кэше Увеличить shared_buffers или прогреть кэш Автоматический поиск медленных запросов-- pg_stat_statements: топ-20 самых дорогих запросов -- Требует: shared_preload_libraries = 'pg_stat_statements' -- postgresql.conf: pg_stat_statements.track = all SELECT round(total_exec_time::numeric, 2) AS total_ms, calls, round(mean_exec_time::numeric, 2) AS mean_ms, round(stddev_exec_time::numeric, 2) AS stddev_ms, round((total_exec_time / sum(total_exec_time) OVER () * 100)::numeric, 2) AS pct_total, round(rows::numeric / calls, 1) AS avg_rows, -- Соотношение кэш-попаданий round( 100.0 * shared_blks_hit / nullif(shared_blks_hit + shared_blks_read, 0), 2 ) AS cache_hit_pct, -- Нормализованный текст запроса (без значений параметров) left(query, 100) AS query_snippet FROM pg_stat_statements WHERE calls > 10 ORDER BY total_exec_time DESC LIMIT 20; -- Запросы с самым высоким среднем временем (не суммарным!) SELECT calls, round(mean_exec_time::numeric, 2) AS mean_ms, round(max_exec_time::numeric, 2) AS max_ms, round(stddev_exec_time::numeric, 2) AS stddev_ms, left(query, 120) AS query FROM pg_stat_statements WHERE calls > 5 AND mean_exec_time > 100 -- Больше 100 мс в среднем ORDER BY mean_exec_time DESC LIMIT 20; -- Запросы с плохим cache hit ratio (много disk reads) SELECT calls, round(mean_exec_time::numeric, 2) AS mean_ms, shared_blks_read, shared_blks_hit, round(100.0 * shared_blks_hit / nullif(shared_blks_hit + shared_blks_read, 0), 2) AS cache_hit_pct, left(query, 120) AS query FROM pg_stat_statements WHERE calls > 10 AND (shared_blks_hit + shared_blks_read) > 0 AND shared_blks_read > shared_blks_hit -- Больше промахов чем попаданий ORDER BY shared_blks_read DESC LIMIT 20; Глава 4. Autovacuum: настройка, а не молитваAutovacuum — не враг, а друг. Но дефолтные настройки рассчитаны на небольшие таблицы. На больших таблицах он либо не успевает, либо тормозит рабочую нагрузку. Понять текущее состояние vacuum-- Таблицы с наибольшим dead tuple bloat SELECT schemaname, relname AS tablename, n_live_tup, n_dead_tup, round(n_dead_tup::numeric / nullif(n_live_tup + n_dead_tup, 0) * 100, 2) AS dead_pct, last_vacuum, last_autovacuum, last_analyze, last_autoanalyze, autovacuum_count, pg_size_pretty(pg_total_relation_size(schemaname||'.'||relname)) AS total_size FROM pg_stat_user_tables WHERE n_dead_tup > 1000 ORDER BY n_dead_tup DESC LIMIT 20; -- Таблицы, которым скоро нужен vacuum (по счётчику транзакций) -- age() показывает сколько транзакций прошло с последнего freeze SELECT schemaname, relname, pg_size_pretty(pg_total_relation_size(oid)) AS size, age(relfrozenxid) AS xid_age, round(age(relfrozenxid)::numeric / 2000000000 * 100, 2) AS freeze_pct, -- Когда автовакуум сделает freeze (по умолчанию при 150M транзакций) (200000000 - age(relfrozenxid)) AS txids_until_freeze FROM pg_class JOIN pg_namespace ON pg_namespace.oid = pg_class.relnamespace WHERE relkind = 'r' AND nspname NOT IN ('pg_catalog', 'information_schema') ORDER BY age(relfrozenxid) DESC LIMIT 20; -- Текущие процессы autovacuum SELECT pid, now() - xact_start AS duration, query, state, wait_event_type, wait_event FROM pg_stat_activity WHERE query LIKE 'autovacuum:%' ORDER BY duration DESC; Оптимальная настройка autovacuum# postgresql.conf — глобальные настройки autovacuum # Число процессов autovacuum autovacuum_max_workers = 6 # Дефолт 3; на активном сервере — 4-8 # Стоимостной лимит для autovacuum (throttling) # Дефолт 200 — очень агрессивное ограничение скорости. # На SSD можно поднять до 800-2000. autovacuum_vacuum_cost_limit = 800 # Задержка между "порциями" vacuum (cooldown) # При cost_limit=800 и delay=2ms → ~400 МБ/с максимальная скорость vacuum autovacuum_vacuum_cost_delay = 2ms # Порог запуска VACUUM: n_dead_tup > autovacuum_vacuum_threshold + n_live_tup * scale_factor autovacuum_vacuum_threshold = 50 autovacuum_vacuum_scale_factor = 0.02 # 2% от таблицы (дефолт 20%) # Порог запуска ANALYZE autovacuum_analyze_threshold = 50 autovacuum_analyze_scale_factor = 0.01 # 1% (дефолт 20%) # Для больших таблиц scale_factor делает vacuum очень редким: # Таблица 100M строк × 0.02 = 2M dead tuples до запуска vacuum — МНОГО Настройка per-table (лучше глобальных для горячих таблиц):-- Для высокоактивных таблиц: vacuum чаще, агрессивнее ALTER TABLE orders SET ( autovacuum_vacuum_scale_factor = 0.005, -- Запуск при 0.5% dead tuples autovacuum_analyze_scale_factor = 0.002, -- Analyze при 0.2% autovacuum_vacuum_cost_limit = 1600, -- Более высокий лимит I/O autovacuum_vacuum_cost_delay = 1 -- Меньше пауз ); -- Для append-only таблиц (логи, временные ряды): -- Vacuum не нужен часто, но freeze — важен ALTER TABLE event_log SET ( autovacuum_vacuum_scale_factor = 0.2, -- Редкий vacuum (мало UPDATE/DELETE) autovacuum_freeze_max_age = 500000000, -- Freeze через 500M транзакций autovacuum_vacuum_cost_limit = 2000 -- Быстрый когда запустился ); -- Проверить что настройки применились: SELECT relname, reloptions FROM pg_class WHERE relname IN ('orders', 'event_log'); Обнаружение table bloat (раздутых таблиц)-- Скрипт оценки bloat (не требует сторонних расширений) WITH constants AS ( SELECT current_setting('block_size')::numeric AS bs, 23 AS hdr, 8 AS ma ), columns_per_table AS ( SELECT att.attrelid, count(*) AS cols, -- Байт nullmap на строку (count(*) + 7) / 8 AS nullhdr FROM pg_attribute att WHERE att.attnum > 0 AND NOT att.attisdropped GROUP BY 1 ), rows_estimate AS ( SELECT c.oid, CASE WHEN c.reltuples < 0 THEN 0 ELSE c.reltuples END AS est_rows, c.relpages, c.relname, n.nspname FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace WHERE c.relkind = 'r' AND n.nspname NOT IN ('pg_catalog', 'information_schema') ) SELECT re.nspname || '.' || re.relname AS table_name, re.est_rows, re.relpages AS current_pages, pg_size_pretty(re.relpages * 8192) AS current_size, -- Оценочный реальный размер pg_size_pretty( ceil(re.est_rows * 30 / 8192.0)::bigint * 8192 ) AS estimated_real_size, round( 100.0 * (re.relpages - ceil(re.est_rows * 30 / 8192.0)) / nullif(re.relpages, 0), 1 ) AS bloat_pct FROM rows_estimate re WHERE re.relpages > 100 ORDER BY (re.relpages - ceil(re.est_rows * 30 / 8192.0)) DESC LIMIT 20; -- Для точного bloat используйте расширение pgstattuple: -- CREATE EXTENSION pgstattuple; SELECT * FROM pgstattuple('orders'); -- Поля: table_len, live_tuple_count, dead_tuple_count, dead_tuple_percent, free_space Глава 5. Connection Pooling с PgBouncerКаждое соединение с PostgreSQL — это отдельный процесс (~5 МБ памяти + overhead планировщика). 1000 соединений = 5 ГБ памяти только на процессы. PgBouncer решает эту проблему. Режимы PgBouncerРежим Как работает Подходит для Ограничения session 1 клиент = 1 серверное соединение на всю сессию Совместимость Нет экономии transaction Серверное соединение занято только на время транзакции OLTP, большинство приложений SET, LISTEN, prepared statements statement Одно серверное соединение на один SQL-оператор Агрессивная экономия Нет транзакций! Конфигурация PgBouncer# /etc/pgbouncer/pgbouncer.ini [databases] # Синтаксис: alias = host=... dbname=... port=... user=... myapp = host=127.0.0.1 port=5432 dbname=myapp_db # Для чтения — отдельный пул на реплику myapp_ro = host=replica.internal port=5432 dbname=myapp_db [pgbouncer] # Режим пула pool_mode = transaction # Адрес и порт PgBouncer listen_addr = 0.0.0.0 listen_port = 5432 # Максимум клиентских соединений (к PgBouncer) max_client_conn = 2000 # Размер серверного пула на базу (к PostgreSQL) # PostgreSQL: max_connections = 200 # PgBouncer: default_pool_size = 80 (на каждую базу) default_pool_size = 80 # Минимальный пул (держим готовые соединения) min_pool_size = 10 # Резерв для суперпользователя (аналог reserved_connections) reserve_pool_size = 5 reserve_pool_timeout = 3 # Таймауты server_idle_timeout = 600 # Закрыть серверное соединение через 10 мин idle client_idle_timeout = 0 # Не закрывать клиентские (0 = infinite) server_connect_timeout = 5 # Таймаут подключения к PostgreSQL query_timeout = 0 # 0 = нет лимита на запрос (лучше ставить в app) query_wait_timeout = 120 # Ждать свободного соединения до 120 с # Проверка соединений server_check_query = select 1 server_check_delay = 30 # Аутентификация (scram-sha-256 — стандарт PG 14+) auth_type = scram-sha-256 auth_file = /etc/pgbouncer/userlist.txt # Логирование (не слишком подробное — влияет на производительность) log_connections = 0 log_disconnections = 0 log_pooler_errors = 1 # Admin интерфейс admin_users = pgbouncer_admin stats_users = monitoring_user # Производительность tcp_keepalive = 1 tcp_keepidle = 60 tcp_keepintvl = 10 tcp_keepcnt = 5 Мониторинг PgBouncer-- Подключиться к admin БД PgBouncer: -- psql -h localhost -p 5432 -U pgbouncer_admin pgbouncer -- Состояние пулов SHOW POOLS; -- cl_active — клиентов с активным серверным соединением -- cl_waiting — клиентов в очереди (ждут свободного соединения!) -- sv_active — серверных соединений в работе -- sv_idle — серверных соединений в ожидании (пул) -- sv_used — только что освобождённые (не проверены ещё) -- maxwait — максимальное время ожидания клиента (критический параметр!) -- Статистика SHOW STATS; -- total_query_time — суммарное время выполнения запросов -- avg_query_time — среднее время запроса -- total_wait_time — суммарное время ожидания в очереди -- Список клиентов SHOW CLIENTS; -- Перезагрузить конфиг без перезапуска RELOAD; -- Сбросить статистику RESET STATS; Интеграция с PostgreSQL 17: встроенный connection shardPostgreSQL 17 улучшил max_connections по производительности и добавил механизм connection_obeys_lc_messages — мелочь, но полезная. Работа над встроенным пулингом (connection pooling) ведётся активно, следите за PostgreSQL 18. Глава 6. Партиционирование: когда таблица растёт до сотен ГБПартиционирование делит одну логическую таблицу на несколько физических. PostgreSQL 16/17 значительно улучшили работу с партициями: умный pruning, параллельные операции, partition-wise joins. RANGE партиционирование (самое частое — по дате)-- Создание партиционированной таблицы CREATE TABLE events ( id BIGSERIAL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), device_id INT NOT NULL, event_type TEXT NOT NULL, payload JSONB, PRIMARY KEY (id, created_at) -- created_at обязательна в PK для партиций! ) PARTITION BY RANGE (created_at); -- Создание партиций (вручную или автоматически) CREATE TABLE events_2024_01 PARTITION OF events FOR VALUES FROM ('2024-01-01') TO ('2024-02-01'); CREATE TABLE events_2024_02 PARTITION OF events FOR VALUES FROM ('2024-02-01') TO ('2024-03-01'); -- DEFAULT партиция для данных вне диапазона CREATE TABLE events_default PARTITION OF events DEFAULT; -- Индексы создаются на каждой партиции отдельно -- (или глобально через CREATE INDEX на родительской — PG 11+) CREATE INDEX idx_events_device ON events(device_id, created_at); -- Автоматически создаст индекс на каждой партиции! Автоматическое создание партиций (pg_partman)Ручное создание партиций — путь к ошибкам. Используйте pg_partman: -- Установка pg_partman -- Добавить в postgresql.conf: shared_preload_libraries = 'pg_partman_bgw' -- Настройка автоматического управления партициями SELECT partman.create_parent( p_parent_table => 'public.events', p_control => 'created_at', p_interval => 'monthly', -- или 'weekly', 'daily', 'yearly' p_premake => 3, -- Создавать 3 будущих партиции заранее p_start_partition => '2024-01-01' ); -- Настройка retention (удаление старых партиций) UPDATE partman.part_config SET retention = '12 months', -- Хранить 12 месяцев retention_keep_table = false, -- Удалять партицию физически retention_keep_index = false -- Удалять и индексы WHERE parent_table = 'public.events'; -- Запуск обслуживания вручную (обычно pg_partman_bgw делает это сам) CALL partman.run_maintenance_proc(); Partition Pruning: проверяем что планировщик умный-- Планировщик должен сканировать только нужные партиции EXPLAIN SELECT count(*) FROM events WHERE created_at BETWEEN '2024-03-01' AND '2024-03-31'; -- Ищем в плане: "Partitions: events_2024_03" -- НЕ должно быть: "Append (всех партиций)" -- Если pruning не работает — проверьте что условие на колонку партиционирования -- и её тип совпадают (нет неявных каст) -- Partition pruning во время выполнения (runtime pruning, PG 11+) -- Работает даже для параметров ($1, $2) если enable_partition_pruning = on SET enable_partition_pruning = on; -- Дефолт on в PG 16+ LIST партиционирование (по типу/региону)CREATE TABLE orders ( id BIGSERIAL, region TEXT NOT NULL, created_at TIMESTAMPTZ DEFAULT NOW(), total DECIMAL(10,2) ) PARTITION BY LIST (region); CREATE TABLE orders_eu PARTITION OF orders FOR VALUES IN ('DE', 'FR', 'NL', 'PL'); CREATE TABLE orders_us PARTITION OF orders FOR VALUES IN ('US', 'CA', 'MX'); CREATE TABLE orders_asia PARTITION OF orders FOR VALUES IN ('CN', 'JP', 'KR', 'IN'); CREATE TABLE orders_rest PARTITION OF orders DEFAULT; HASH партиционирование (равномерное распределение)-- Для таблиц без естественного ключа партиционирования -- Гарантирует примерно равный размер партиций CREATE TABLE sessions ( id UUID DEFAULT gen_random_uuid(), user_id INT NOT NULL, data JSONB ) PARTITION BY HASH (user_id); -- Создаём N партиций (степень двойки — хорошая практика) CREATE TABLE sessions_0 PARTITION OF sessions FOR VALUES WITH (MODULUS 8, REMAINDER 0); CREATE TABLE sessions_1 PARTITION OF sessions FOR VALUES WITH (MODULUS 8, REMAINDER 1); -- ... и т.д. до sessions_7 Глава 7. Репликация: PostgreSQL 16/17/18Физическая репликация (Streaming Replication)# На Primary: postgresql.conf wal_level = replica max_wal_senders = 10 # Максимум одновременных реплик wal_keep_size = 1GB # Буфер WAL для реплик (PG 13+, заменил wal_keep_segments) hot_standby = on # Разрешить запросы на реплике hot_standby_feedback = on # Реплика сообщает Primary о своих транзакциях # pg_hba.conf на Primary — разрешаем репликацию с адреса реплики: # host replication replicator 10.0.0.2/32 scram-sha-256 # На Standby: создание базовой копии pg_basebackup \ -h primary.host \ -U replicator \ -D /var/lib/postgresql/17/main \ -P \ --wal-method=stream \ --checkpoint=fast \ --write-recovery-conf # Создаёт standby.signal и postgresql.auto.conf # postgresql.auto.conf на Standby (создаётся pg_basebackup): # primary_conninfo = 'host=primary.host port=5432 user=replicator password=...' -- Мониторинг репликации на Primary: SELECT client_addr, usename, application_name, state, sent_lsn, write_lsn, flush_lsn, replay_lsn, -- Лаг в байтах pg_wal_lsn_diff(sent_lsn, replay_lsn) AS replay_lag_bytes, -- Лаг во времени (PG 10+) write_lag, flush_lag, replay_lag, sync_state FROM pg_stat_replication ORDER BY replay_lag DESC; -- На Standby — проверка своего лага: SELECT now() - pg_last_xact_replay_timestamp() AS replication_lag, pg_is_in_recovery() AS is_replica, pg_last_wal_receive_lsn() AS received_lsn, pg_last_wal_replay_lsn() AS replayed_lsn; Логическая репликация (PostgreSQL 16/17: серьёзно улучшена)Логическая репликация в PostgreSQL 16 получила: Двунаправленная (bidirectional) репликация — обе стороны могут принимать запись Streaming больших транзакций в реальном времени (без ожидания COMMIT) Параллельное применение изменений на подписчике -- На Publisher (источник): -- postgresql.conf: wal_level = logical -- Создание публикации CREATE PUBLICATION my_pub FOR TABLE orders, users, products WITH (publish = 'insert, update, delete', publish_via_partition_root = true); -- Для всех таблиц: CREATE PUBLICATION all_tables FOR ALL TABLES; -- На Subscriber (назначение): -- Создание подписки CREATE SUBSCRIPTION my_sub CONNECTION 'host=primary.host port=5432 dbname=mydb user=replicator password=secret' PUBLICATION my_pub WITH ( connect = true, slot_name = 'my_sub_slot', synchronous_commit = 'off', -- Более быстрая репликация streaming = on -- PG 14+: stream больших транзакций ); -- Мониторинг логической репликации на Publisher: SELECT slot_name, plugin, slot_type, database, active, active_pid, -- КРИТИЧНО: wal_status = 'lost' означает что слот отстал и WAL удалён wal_status, pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), confirmed_flush_lsn)) AS subscriber_lag FROM pg_replication_slots; -- ОПАСНОСТЬ: неактивный logical slot держит WAL! Диск кончится. -- Если слот не используется > 24ч — проверить и при необходимости удалить: -- SELECT pg_drop_replication_slot('my_sub_slot'); Глава 8. Мониторинг: что смотреть в продакшенеСистемные вьюшки — источник правды-- ===== АКТИВНЫЕ ЗАПРОСЫ И БЛОКИРОВКИ ===== -- Запросы дольше 30 секунд — потенциальные проблемы SELECT pid, now() - pg_stat_activity.query_start AS duration, query, state, wait_event_type, wait_event, client_addr, usename, application_name FROM pg_stat_activity WHERE (now() - pg_stat_activity.query_start) > INTERVAL '30 seconds' AND state != 'idle' ORDER BY duration DESC; -- Граф блокировок: кто кого блокирует WITH RECURSIVE lock_graph AS ( -- Базовый случай: запросы, ожидающие блокировку SELECT blocked.pid AS blocked_pid, blocked.query AS blocked_query, blocked.query_start AS blocked_start, blocker.pid AS blocker_pid, blocker.query AS blocker_query, 0 AS depth FROM pg_stat_activity blocked JOIN pg_stat_activity blocker ON blocker.pid = ANY(pg_blocking_pids(blocked.pid)) WHERE blocked.wait_event_type = 'Lock' UNION ALL -- Рекурсивный случай: цепочки блокировок SELECT lg.blocked_pid, lg.blocked_query, lg.blocked_start, blocker.pid, blocker.query, lg.depth + 1 FROM lock_graph lg JOIN pg_stat_activity blocker ON blocker.pid = ANY(pg_blocking_pids(lg.blocker_pid)) WHERE lg.depth < 10 ) SELECT blocked_pid, left(blocked_query, 80) AS blocked_query, now() - blocked_start AS wait_duration, blocker_pid, left(blocker_query, 80) AS blocker_query, depth FROM lock_graph ORDER BY wait_duration DESC; -- Если нужно убить зависший запрос (мягко): -- SELECT pg_cancel_backend(pid); -- Отмена запроса, транзакция откатывается -- Если не реагирует (жёстко): -- SELECT pg_terminate_backend(pid); -- Завершение процесса -- ===== КЭШ И I/O ===== -- Cache hit ratio (цель: > 99%) SELECT sum(heap_blks_hit) AS heap_hit, sum(heap_blks_read) AS heap_read, round( sum(heap_blks_hit)::numeric / nullif(sum(heap_blks_hit) + sum(heap_blks_read), 0) * 100, 3 ) AS cache_hit_ratio FROM pg_statio_user_tables; -- По каждой таблице: SELECT relname AS table_name, heap_blks_hit, heap_blks_read, round(heap_blks_hit::numeric / nullif(heap_blks_hit + heap_blks_read, 0) * 100, 2) AS cache_hit_pct, idx_blks_hit, idx_blks_read, round(idx_blks_hit::numeric / nullif(idx_blks_hit + idx_blks_read, 0) * 100, 2) AS idx_cache_hit_pct FROM pg_statio_user_tables WHERE heap_blks_read + heap_blks_hit > 0 ORDER BY heap_blks_read DESC LIMIT 20; -- ===== CHECKPOINT СТАТИСТИКА ===== SELECT checkpoints_timed, checkpoints_req, -- Если часто req >> timed: увеличить max_wal_size checkpoint_write_time / 1000 AS write_sec, checkpoint_sync_time / 1000 AS sync_sec, buffers_checkpoint, buffers_clean, buffers_backend, -- Если много: bgwriter не успевает → тюнинг bgwriter buffers_backend_fsync, -- НЕ ноль = ПРОБЛЕМА: backend делает fsync сам buffers_alloc FROM pg_stat_bgwriter; -- Если buffers_backend > 0 — увеличить bgwriter_lru_maxpages: -- bgwriter_lru_maxpages = 200 (дефолт 100) -- bgwriter_lru_multiplier = 4.0 (дефолт 2.0) -- bgwriter_delay = 50ms (дефолт 200ms) Скрипт ежедневного health-check-- Сохранить как daily_healthcheck.sql и запускать через cron \echo '=== PostgreSQL Daily Health Check ===' \echo '' \echo '--- Database Sizes ---' SELECT datname, pg_size_pretty(pg_database_size(datname)) AS size FROM pg_database WHERE datname NOT IN ('postgres', 'template0', 'template1') ORDER BY pg_database_size(datname) DESC; \echo '' \echo '--- Top 10 Largest Tables ---' SELECT schemaname || '.' || tablename AS table, pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) AS total_size FROM pg_tables WHERE schemaname = 'public' ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC LIMIT 10; \echo '' \echo '--- Tables with High Dead Tuple Ratio (> 10%) ---' SELECT relname, n_live_tup, n_dead_tup, round(n_dead_tup::numeric / nullif(n_live_tup + n_dead_tup, 0) * 100, 1) AS dead_pct, last_autovacuum FROM pg_stat_user_tables WHERE n_dead_tup::numeric / nullif(n_live_tup + n_dead_tup, 0) > 0.1 AND n_live_tup > 1000 ORDER BY dead_pct DESC; \echo '' \echo '--- Replication Lag ---' SELECT application_name, replay_lag, sync_state FROM pg_stat_replication; \echo '' \echo '--- Long-Running Transactions (> 1 hour) ---' SELECT pid, usename, now() - xact_start AS duration, left(query, 100) AS query FROM pg_stat_activity WHERE xact_start IS NOT NULL AND now() - xact_start > INTERVAL '1 hour' AND pid != pg_backend_pid() ORDER BY duration DESC; \echo '' \echo '--- Unused Indexes (0 scans) ---' SELECT schemaname, tablename, indexname, pg_size_pretty(pg_relation_size(indexrelid)) AS size FROM pg_stat_user_indexes WHERE idx_scan = 0 AND pg_relation_size(indexrelid) > 10 * 1024 * 1024 -- > 10 МБ ORDER BY pg_relation_size(indexrelid) DESC LIMIT 10; Глава 9. Новинки PostgreSQL 16/17/18PostgreSQL 16 (2023)Логическая репликация от standby — теперь можно публиковать изменения не только с primary, разгружая мастер. Параллельный COPY — загрузка данных через COPY стала параллельной. Улучшения планировщика для GROUP BY с параллелизмом. pg_stat_io — новая системная вьюшка для детальной статистики I/O: -- PostgreSQL 16+: детальная I/O статистика SELECT backend_type, object, context, reads, writes, extends, op_bytes, hits, evictions, reuses, fsyncs, read_time, write_time FROM pg_stat_io ORDER BY reads + writes DESC; -- Особенно полезно: сравнить hits vs reads для разных backend_type COPY FROM ... WHERE — фильтрация при загрузке данных: -- Загружаем только нужные строки COPY orders FROM '/tmp/orders.csv' CSV HEADER WHERE status = 'completed' AND total > 100; PostgreSQL 17 (2024)MERGE стал намного мощнее — поддержка RETURNING, DO NOTHING: -- PostgreSQL 17: MERGE с RETURNING MERGE INTO inventory AS target USING incoming_stock AS source ON target.product_id = source.product_id WHEN MATCHED THEN UPDATE SET quantity = target.quantity + source.quantity WHEN NOT MATCHED THEN INSERT (product_id, quantity) VALUES (source.product_id, source.quantity) RETURNING target.product_id, target.quantity, merge_action(); -- merge_action() → 'INSERT' или 'UPDATE' Incremental sorting улучшен — быстрее для DISTINCT и ORDER BY. pg_stat_statements получил toplevel — разделение top-level vs вложенных запросов. Vacuum improvements — улучшена скорость заморозки (freeze), меньше I/O. VACUUM (SKIP_DATABASE_STATS) — ускорение вакуума многих мелких таблиц. Размер WAL записей уменьшен — меньше I/O при интенсивной записи. PostgreSQL 18 (2025, в разработке / ранние беты)Встроенный асинхронный I/O (io_method = io_uring) — огромный прирост для NVMe SSD, особенно при высоком параллелизме: # postgresql.conf (PostgreSQL 18 при использовании Linux io_uring) io_method = io_uring # Дефолт: sync; альтернатива: worker Планировщик с ML-hints — работа над улучшением кардинальности оценок. GRANT/REVOKE для роли по умолчанию — улучшена система безопасности. Глава 10. Практические кейсы: реальные проблемы и их решенияКейс 1: «Запросы стали медленнее после VACUUM»-- Симптом: autovacuum отработал, но запросы стали медленнее. -- Причина: устаревшая статистика. VACUUM не обновляет статистику! -- Решение 1: Принудительный ANALYZE ANALYZE VERBOSE orders; -- Или для всей БД: -- vacuumdb --analyze-only --all -- Решение 2: Увеличить точность статистики для проблемных колонок ALTER TABLE orders ALTER COLUMN status SET STATISTICS 500, ALTER COLUMN region SET STATISTICS 500; ANALYZE orders; -- Проверить статистику после: SELECT attname, n_distinct, correlation FROM pg_stats WHERE tablename = 'orders' AND attname IN ('status', 'region', 'created_at'); Кейс 2: «Диск заполнился WAL файлами»-- Причина 1: Зависший логический слот репликации SELECT slot_name, active, wal_status, pg_size_pretty( pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn) ) AS retained_wal FROM pg_replication_slots WHERE wal_status != 'reserved'; -- Если слот неактивен и держит WAL — удалить после согласования с командой: SELECT pg_drop_replication_slot('stale_slot_name'); -- Причина 2: archive_command не успевает -- Проверить: SELECT last_archived_wal, last_failed_wal, last_failed_time FROM pg_stat_archiver; -- Временная мера: уменьшить max_wal_size -- Постоянная: починить archive_command или увеличить место -- Причина 3: Слишком агрессивные checkpoint -- Уменьшить wal_keep_size если репликация живая Кейс 3: «Connection pool переполнен, приложение не может подключиться»-- Диагноз: смотрим pg_stat_activity SELECT state, count(*), left(query, 50) AS sample_query FROM pg_stat_activity WHERE datname = 'myapp_db' GROUP BY state, left(query, 50) ORDER BY count(*) DESC; -- Частая причина: idle in transaction (транзакция открыта и забыта) SELECT pid, now() - xact_start AS idle_duration, query FROM pg_stat_activity WHERE state = 'idle in transaction' AND now() - xact_start > INTERVAL '5 minutes' ORDER BY idle_duration DESC; -- Быстрое решение: убить зависшие idle in transaction SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE state = 'idle in transaction' AND now() - xact_start > INTERVAL '10 minutes'; -- Постоянное решение: idle_in_transaction_session_timeout -- postgresql.conf: -- idle_in_transaction_session_timeout = 5min -- idle_session_timeout = 30min (PG 14+) Кейс 4: «Таблица растёт несмотря на DELETE»-- Table bloat: место от удалённых строк не возвращается OS. -- PostgreSQL помечает строки как "мёртвые", VACUUM освобождает их -- для ПОВТОРНОГО ИСПОЛЬЗОВАНИЯ, но не возвращает OS (кроме pg_toast). -- Проверить bloat: SELECT relname, pg_size_pretty(pg_total_relation_size(oid)) AS total_size, n_dead_tup, n_live_tup FROM pg_stat_user_tables JOIN pg_class USING (relid) WHERE relname = 'your_table'; -- Решение 1: VACUUM FULL (блокирует таблицу! Используйте в окно обслуживания) VACUUM FULL ANALYZE your_table; -- Решение 2: pg_repack (без блокировки!) -- Устанавливается отдельно: https://github.com/reorg/pg_repack -- pg_repack -d mydb -t your_table -- Решение 3: для партиционированных таблиц — просто удалить старую партицию -- ALTER TABLE events DETACH PARTITION events_2022_01; -- DROP TABLE events_2022_01; -- Мгновенное освобождение места! Заключение: чеклист production PostgreSQLКОНФИГУРАЦИЯ □ shared_buffers = 25-40% RAM □ effective_cache_size = 50-75% RAM □ work_mem настроен с учётом max_connections × parallel_workers □ random_page_cost = 1.1-1.5 для SSD/NVMe □ huge_pages = on (при shared_buffers > 8 ГБ, настроен в Linux) □ max_wal_size = 2-8 ГБ (зависит от нагрузки) □ wal_compression = lz4 (PG 15+) □ idle_in_transaction_session_timeout = 5min □ statement_timeout = установлен разумный лимит МОНИТОРИНГ □ pg_stat_statements включён и регулярно анализируется □ Алерт на cache hit ratio < 95% □ Алерт на replication lag > 60s □ Алерт на bloat > 30% для критичных таблиц □ Алерт на неактивные replication slots □ Ежедневный health check запрос AUTOVACUUM □ autovacuum_max_workers = 4-6 □ autovacuum_vacuum_cost_delay = 2ms (SSD) □ autovacuum_vacuum_cost_limit = 800-2000 □ Scale factor снижен для горячих таблиц □ Мониторинг n_dead_tup и xid_age СОЕДИНЕНИЯ □ PgBouncer в transaction mode □ max_connections ≤ 300 (больше — через пул) □ Настроен pool_size в PgBouncer □ Мониторинг cl_waiting в PgBouncer ИНДЕКСЫ □ Аудит неиспользуемых индексов (pg_stat_user_indexes) □ Составные индексы с правильным порядком колонок □ INCLUDE для покрывающих индексов □ BRIN для append-only больших таблиц РЕПЛИКАЦИЯ □ Мониторинг replay_lag □ Мониторинг pg_replication_slots на утечку WAL □ Проверка wal_status всех слотов □ hot_standby_feedback = on на репликах БЕЗОПАСНОСТЬ □ scram-sha-256 в pg_hba.conf □ Минимальные привилегии для каждого пользователя □ ssl = on + проверка сертификатов □ log_connections/log_disconnections для аудита PostgreSQL — невероятно мощная система, которая «из коробки» даёт примерно 20% своего потенциала. Правильная конфигурация, индексная стратегия и мониторинг превращают её в продукт, который выдерживает тысячи транзакций в секунду на десятках терабайт данных — без дорогостоящих «облачных» альтернатив.