<?xml version="1.0"?>
<rss version="2.0"><channel><title>Articles: IT Infrastructure: Servers, Cloud, DevOps, Networks | IThub</title><link>https://ithub.uno/statiarticles/9_infrastructure/?d=1</link><description>Articles: IT Infrastructure: Servers, Cloud, DevOps, Networks | IThub</description><language>en</language><item><title>&#x41B;&#x438;&#x43C;&#x438;&#x442;&#x44B; &#x432; NGINX: &#x43A;&#x430;&#x43A; &#x437;&#x430;&#x449;&#x438;&#x442;&#x438;&#x442;&#x44C; &#x441;&#x435;&#x440;&#x432;&#x435;&#x440; &#x43E;&#x442; &#x43F;&#x435;&#x440;&#x435;&#x433;&#x440;&#x443;&#x437;&#x43E;&#x43A; &#x438; DoS&#x2011;&#x430;&#x442;&#x430;&#x43A;</title><link>https://ithub.uno/statiarticles/9_infrastructure/%D0%BB%D0%B8%D0%BC%D0%B8%D1%82%D1%8B-%D0%B2-nginx-%D0%BA%D0%B0%D0%BA-%D0%B7%D0%B0%D1%89%D0%B8%D1%82%D0%B8%D1%82%D1%8C-%D1%81%D0%B5%D1%80%D0%B2%D0%B5%D1%80-%D0%BE%D1%82-%D0%BF%D0%B5%D1%80%D0%B5%D0%B3%D1%80%D1%83%D0%B7%D0%BE%D0%BA-%D0%B8-dos%E2%80%91%D0%B0%D1%82%D0%B0%D0%BA-r12/</link><description><![CDATA[
<p><img src="https://ithub.uno/uploads/tmi_files_2026_02/how-to-rate-limit-in-nginx-feature-image-1024x647.webp.7176420e29b00d43b825388dfea73b50.webp" /></p>
<h2>1. Сколько соединений может обработать один воркер</h2><p>В NGINX каждый воркер‑процесс способен обслуживать определённое число одновременных соединений. Это задаётся директивой:</p><pre spellcheck="" class="tmiCode language-nginx" data-language="Nginx"><code>worker_connections 1024;
</code></pre><ul><li><p>Здесь учитываются <strong>все дескрипторы</strong>, включая клиентские соединения и прокси‑сессии к бэкендам.</p></li><li><p>По умолчанию NGINX использует <strong>768 соединений</strong>, но для серьёзных нагрузок лучше поднять до <strong>1024+</strong>, не забывая про лимит открытых файлов в ОС (<code>ulimit -n</code>).</p></li></ul><p><strong>Расчёт максимального числа клиентов:</strong></p><p>max_clients=worker_processes×worker_connections\text{max\_clients} = \text{worker\_processes} \times \text{worker\_connections}max_clients=worker_processes×worker_connections</p><p>Пример:</p><pre spellcheck="" class="tmiCode" data-language="text"><code>worker_processes = 4
worker_connections = 1024
max_clients = 4 × 1024 = 4096
</code></pre><p>То есть сервер может обслуживать до 4096 соединений одновременно (минус коннекты к upstream).</p><p><strong>Пример конфигурации </strong><code>events.conf</code><strong>:</strong></p><pre spellcheck="" class="tmiCode language-nginx" data-language="Nginx"><code>events {
    worker_connections 1024;  # максимум соединений на воркер
    # accept_mutex on;        # равномерное принятие коннектов (опционально)
}
</code></pre><hr><h2>2. Ограничение одновременных соединений: <code>limit_conn</code></h2><p>Чтобы защитить сервер от «дружелюбной» перегрузки, например, когда бот‑краулер открывает сотни соединений, используется <strong>ngx_http_limit_conn_module</strong>:</p><ul><li><p><code>limit_conn_zone</code> — создаёт область памяти для хранения текущих соединений (обычно по IP).</p></li><li><p><code>limit_conn</code> — задаёт лимит одновременных соединений.</p></li></ul><p><strong>Пример настройки:</strong></p><pre spellcheck="" class="tmiCode language-nginx" data-language="Nginx"><code>http {
    limit_conn_zone $binary_remote_addr zone=perip:10m;

    server {
        location / {
            limit_conn perip 10;         # не больше 10 соединений на IP
            limit_conn_status 429;       # выдаём 429 вместо дефолтного 503
            limit_conn_log_level info;   # уровень логирования при отказе
        }
    }
}
</code></pre><ul><li><p><strong>10m зоны на 64-битной платформе</strong> хранят примерно 16 000 уникальных IP.</p></li><li><p>При переполнении NGINX сразу отдаёт ошибку (503/429).</p></li></ul><p><strong>Dry-run и логирование:</strong></p><pre spellcheck="" class="tmiCode language-nginx" data-language="Nginx"><code>limit_conn_dry_run on;       # не блокирует, а только логирует
limit_conn_log_level notice;
</code></pre><p>Так можно тестировать лимиты, не блокируя клиентов.</p><hr><h2>3. Ограничение скорости запросов: <code>limit_req</code></h2><p>Если нужно контролировать <strong>частоту запросов</strong>, применяется <strong>ngx_http_limit_req_module</strong>.</p><p>Механизм основан на <strong>«leaky bucket»</strong> — запросы попадают в бакет и «вытекают» с заданной скоростью.</p><pre spellcheck="" class="tmiCode language-nginx" data-language="Nginx"><code>http {
    limit_req_zone $binary_remote_addr zone=login:10m rate=5r/s;

    server {
        location /login {
            limit_req zone=login burst=10 nodelay;
            limit_req_status 429;
            limit_req_log_level warn;
        }
    }
}
</code></pre><ul><li><p><strong>burst</strong> — пиковое количество запросов, которое сервер пропускает сверх лимита.</p></li><li><p><strong>nodelay</strong> — отказ сразу при превышении лимита, без задержки.</p></li><li><p>Без nodelay лишние запросы будут ожидать своей очереди.</p></li></ul><hr><h2>4. Где хранится состояние</h2><p>В NGINX есть <strong>shared memory zones</strong> — специальная общая память для лимитов:</p><ul><li><p><strong>Slab-пул</strong> разбивает память на одинаковые блоки для быстрого выделения и освобождения.</p></li><li><p>Для <strong>limit_conn</strong> используется хеш-таблица или сбалансированное дерево по IP.</p></li><li><p>Для <strong>limit_req</strong> — красно‑чёрное дерево + очередь «протекающего ведра».</p></li><li><p>Доступ защищён встроенным мьютексом — нет гонок даже при сотнях воркеров.</p></li></ul><p><strong>Механизм работы:</strong></p><ol><li><p>При новом запросе NGINX берёт ключ (IP), ищет запись в зоне.</p></li><li><p>Если записи нет — создаёт новую.</p></li><li><p>Проверяет счётчики и решает, пропускать запрос или отказать.</p></li></ol><hr><h2>5. Реакция сервера при превышении лимита</h2><ul><li><p>По умолчанию:</p><ul><li><p><code>limit_conn</code> → 503 Service Unavailable</p></li><li><p><code>limit_req</code> → 503 Service Unavailable</p></li></ul></li><li><p>Лучше отдавать <strong>429 Too Many Requests</strong>, чтобы клиенты понимали причину.</p></li></ul><p><strong>Пример логов</strong></p><pre spellcheck="" class="tmiCode" data-language="text"><code>2025/04/04 13:45:12 [warn] 12345#0: *67890 limiting requests, excess: 5 by zone "login"
2025/04/04 13:45:12 [info] 12345#0: *67890 a client request is temporarily blocked by zone "perip"
</code></pre><hr><h2>6. Практические нюансы</h2><ul><li><p><strong>Размер зоны:</strong> 1 MB ≈ 16 000 записей; для 100 000 IP/сутки потребуется 10–12 MB.</p></li><li><p><strong>burst vs delay:</strong> для login/API можно применять разные подходы:</p><ul><li><p>login: <code>burst=1; nodelay</code></p></li><li><p>API: <code>burst=10; delay 10</code></p></li></ul></li><li><p><strong>Разные зоны для разных endpoint:</strong> чтобы лимиты не конфликтовали.</p></li><li><p><strong>Исключения:</strong> через <code>map</code> или <code>geo</code> можно исключить внутренние IP:</p></li></ul><pre spellcheck="" class="tmiCode language-nginx" data-language="Nginx"><code>map $remote_addr $limit {
    10.0.0.0/8         "";
    default            $binary_remote_addr;
}

limit_req_zone $limit zone=api:20m rate=20r/s;
</code></pre><hr><h2>7. Сторонние модули и NGINX Plus</h2><h3>7.1 ngx_brotli — снижение трафика</h3><ul><li><p>Brotli‑сжатие уменьшает объём передаваемых данных, разгружая лимиты по трафику:</p></li></ul><pre spellcheck="" class="tmiCode language-nginx" data-language="Nginx"><code>http {
    brotli on;
    brotli_comp_level 6;
    brotli_types text/html text/css application/javascript;
}
</code></pre><h3>7.2 ngx_http_limit_traffic_ratefilter_module — лимит по байтам</h3><ul><li><p>Позволяет ограничить скорость передачи, например 100 KB/s на IP:</p></li></ul><pre spellcheck="" class="tmiCode language-nginx" data-language="Nginx"><code>http {
    limit_traffic_rate_zone $binary_remote_addr zone=bytetraf:10m;

    server {
        location /downloads/ {
            limit_traffic_rate zone=bytetraf rate=100k;
        }
    }
}
</code></pre><h3>7.3 NGINX Plus — расширенные возможности</h3><ul><li><p><strong>Синхронизация зон между нодами</strong> — zone_sync.</p></li><li><p><strong>Адаптивное ограничение</strong>: скорость передачи зависит от метрик, например:</p></li></ul><pre spellcheck="" class="tmiCode language-nginx" data-language="Nginx"><code>map $upstream_response_time $dyn_rate {
    "~^[0-9]\.[0-1]" 200k;   # быстрые ответы — больше скорости
    default           50k;    # медленные — ограничение
}

server {
    location /stream/ {
        limit_rate $dyn_rate;
    }
}
</code></pre>]]></description><guid isPermaLink="false">12</guid><pubDate>Fri, 06 Feb 2026 17:39:00 +0000</pubDate></item><item><title>&#x423;&#x437;&#x43D;&#x430;&#x439;&#x442;&#x435; &#x43E; &#x441;&#x430;&#x43C;&#x44B;&#x445; &#x440;&#x430;&#x441;&#x43F;&#x440;&#x43E;&#x441;&#x442;&#x440;&#x430;&#x43D;&#x451;&#x43D;&#x43D;&#x44B;&#x445; &#x43E;&#x448;&#x438;&#x431;&#x43A;&#x430;&#x445; &#x43F;&#x440;&#x438; &#x43F;&#x440;&#x43E;&#x435;&#x43A;&#x442;&#x438;&#x440;&#x43E;&#x432;&#x430;&#x43D;&#x438;&#x438; &#x438; &#x44D;&#x43A;&#x441;&#x43F;&#x43B;&#x443;&#x430;&#x442;&#x430;&#x446;&#x438;&#x438; &#x441;&#x435;&#x442;&#x435;&#x439;, &#x43E;&#x442; &#x43A;&#x430;&#x431;&#x435;&#x43B;&#x44C;&#x43D;&#x43E;&#x439; &#x438;&#x43D;&#x444;&#x440;&#x430;&#x441;&#x442;&#x440;&#x443;&#x43A;&#x442;&#x443;&#x440;&#x44B; &#x434;&#x43E; &#x43C;&#x430;&#x440;&#x448;&#x440;&#x443;&#x442;&#x438;&#x437;&#x430;&#x442;&#x43E;&#x440;&#x43E;&#x432;. &#x41F;&#x440;&#x430;&#x43A;&#x442;&#x438;&#x447;&#x435;&#x441;&#x43A;&#x438;&#x435; &#x441;&#x43E;&#x432;&#x435;&#x442;&#x44B;, &#x43F;&#x440;&#x438;&#x43C;&#x435;&#x440;&#x44B; &#x438; &#x43D;&#x430;&#x433;&#x43B;&#x44F;&#x434;&#x43D;&#x44B;&#x435; &#x440;&#x435;&#x43A;&#x43E;&#x43C;&#x435;&#x43D;&#x434;&#x430;&#x446;&#x438;&#x438; &#x434;&#x43B;&#x44F; IT-&#x441;&#x43F;&#x435;&#x446;&#x438;&#x430;&#x43B;&#x438;&#x441;&#x442;&#x43E;&#x432; &#x438; &#x430;&#x434;&#x43C;&#x438;&#x43D;&#x438;&#x441;&#x442;&#x440;&#x430;&#x442;&#x43E;&#x440;&#x43E;&#x432;.</title><link>https://ithub.uno/statiarticles/9_infrastructure/%D1%83%D0%B7%D0%BD%D0%B0%D0%B9%D1%82%D0%B5-%D0%BE-%D1%81%D0%B0%D0%BC%D1%8B%D1%85-%D1%80%D0%B0%D1%81%D0%BF%D1%80%D0%BE%D1%81%D1%82%D1%80%D0%B0%D0%BD%D1%91%D0%BD%D0%BD%D1%8B%D1%85-%D0%BE%D1%88%D0%B8%D0%B1%D0%BA%D0%B0%D1%85-%D0%BF%D1%80%D0%B8-%D0%BF%D1%80%D0%BE%D0%B5%D0%BA%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B8-%D0%B8-%D1%8D%D0%BA%D1%81%D0%BF%D0%BB%D1%83%D0%B0%D1%82%D0%B0%D1%86%D0%B8%D0%B8-%D1%81%D0%B5%D1%82%D0%B5%D0%B9-%D0%BE%D1%82-%D0%BA%D0%B0%D0%B1%D0%B5%D0%BB%D1%8C%D0%BD%D0%BE%D0%B9-%D0%B8%D0%BD%D1%84%D1%80%D0%B0%D1%81%D1%82%D1%80%D1%83%D0%BA%D1%82%D1%83%D1%80%D1%8B-%D0%B4%D0%BE-%D0%BC%D0%B0%D1%80%D1%88%D1%80%D1%83%D1%82%D0%B8%D0%B7%D0%B0%D1%82%D0%BE%D1%80%D0%BE%D0%B2-%D0%BF%D1%80%D0%B0%D0%BA%D1%82%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%B8%D0%B5-%D1%81%D0%BE%D0%B2%D0%B5%D1%82%D1%8B-%D0%BF%D1%80%D0%B8%D0%BC%D0%B5%D1%80%D1%8B-%D0%B8-%D0%BD%D0%B0%D0%B3%D0%BB%D1%8F%D0%B4%D0%BD%D1%8B%D0%B5-%D1%80%D0%B5%D0%BA%D0%BE%D0%BC%D0%B5%D0%BD%D0%B4%D0%B0%D1%86%D0%B8%D0%B8-%D0%B4%D0%BB%D1%8F-it-%D1%81%D0%BF%D0%B5%D1%86%D0%B8%D0%B0%D0%BB%D0%B8%D1%81%D1%82%D0%BE%D0%B2-%D0%B8-%D0%B0%D0%B4%D0%BC%D0%B8%D0%BD%D0%B8%D1%81%D1%82%D1%80%D0%B0%D1%82%D0%BE%D1%80%D0%BE%D0%B2-r18/</link><description><![CDATA[
<p><img src="https://ithub.uno/uploads/tmi_files_2026_02/before_and_after_01.jpg.ce87463843307bbadcc0d20e7c14f835.jpg" /></p>
<h3>Ошибки в сетевой инфраструктуре: от планирования до эксплуатации</h3><p>Ошибки, которые могут допустить IT-специалисты, практически неисчерпаемы. Некоторые из них незаметны обычным пользователям — например, отсутствие настроенного журналирования событий. Даже взлом, произошедший из-за этого, может остаться незамеченным, пока об этом не напишут в новостях.</p><p>Другие ошибки становятся очевидными сразу: проблемы в работе сети замечает каждый сотрудник. В этой статье разберём наиболее частые ошибки при проектировании и эксплуатации сетей и дадим практические рекомендации, как их избежать.</p><hr><h3>1. Планирование сети: превыше всего</h3><p>Часто сети строятся без должного планирования. Причины бывают разные: экономия средств, отсутствие компетенций у проектировщиков, поспешные решения руководства.</p><p><strong>Пример:</strong> интегратор готовит детальную спецификацию сети, а заказчик «подрезает» её, чтобы сэкономить — в итоге сеть строится с недочётами, которые потом приходится исправлять дорого и долго.</p><p>Правильное планирование включает:</p><ul><li><p>оценку количества рабочих мест и будущего расширения;</p></li><li><p>разработку документации и схем сети;</p></li><li><p>расчёт емкости портов на коммутаторах и маршрутизаторах;</p></li><li><p>планирование отказоустойчивости и резервирования.</p></li></ul><hr><h3>2. Кабельная инфраструктура: детали имеют значение</h3><p>Даже при массовом использовании Wi‑Fi проводные соединения остаются критически важными.</p><p><strong>Ошибки:</strong></p><ul><li><p>Недостаток розеток и кабелей.</p></li><li><p>Плохая организация кабельных каналов.</p></li><li><p>Попытка «все на Wi‑Fi», что может привести к перегрузке сети.</p></li></ul><p><strong>Решение:</strong></p><ul><li><p>Планируйте количество розеток с запасом (+4–6 на кабинет).</p></li><li><p>Разделяйте кабели по цветам:</p><ul><li><p><strong>Жёлтые</strong> — кроссы,</p></li><li><p><strong>Красные</strong> — серверы и NAS,</p></li><li><p><strong>Синие</strong> — настенные разъёмы и коммутаторы,</p></li><li><p><strong>Чёрные</strong> — инфраструктурные соединения,</p></li><li><p><strong>Зелёные</strong> — временные подключения.</p></li></ul></li><li><p>Маркируйте оба конца кабеля, чтобы быстро находить подключение к устройству.</p></li></ul><hr><h3>3. Коммутаторы: порты не должны заканчиваться</h3><p>Ошибка многих сетевых администраторов — неправильный расчёт портовой емкости. Если все порты заняты «впритык», в критический момент новые устройства подключить будет невозможно.</p><p><strong>Совет:</strong></p><ul><li><p>Для этажных коммутаторов рассчитывайте запас портов.</p></li><li><p>Для коммутаторов ядра — учитывайте отказоустойчивость и настройку Spanning Tree.</p></li></ul><hr><h3>4. Маршрутизаторы: не пытайтесь сделать всё</h3><p>Современные маршрутизаторы умеют почти всё: VPN, DHCP, файрволл, межсетевой экран.<br>Но перегружать маршрутизатор всеми функциями не стоит:</p><ul><li><p>Он станет точкой отказа.</p></li><li><p>Пропускная способность упадёт.</p></li><li><p>Риски безопасности увеличатся.</p></li></ul><p><strong>Рекомендация:</strong> оставьте маршрутизатор только для маршрутизации, VPN и файрволл — на отдельные устройства.</p><hr><h3>5. Серверная: порядок и скорость</h3><p><strong>Ошибки:</strong></p><ul><li><p>Перепутанные кабели, отсутствие маркировки.</p></li><li><p>Серверы, размещённые далеко от магистралей сети, что замедляет работу.</p></li></ul><p><strong>Пример:</strong><br>Представьте автостраду и деревенскую дорогу. Магистраль позволяет двигаться быстро без остановок, а локальные дороги замедляют движение. Так же работает и сеть: чем больше устройств между пользователем и сервером, тем медленнее передача данных.</p><p><strong>Решение:</strong></p><ul><li><p>Сразу организуйте кабели аккуратно и с маркировкой.</p></li><li><p>Размещайте серверы так, чтобы минимизировать количество промежуточных устройств.</p></li></ul><hr><h3>6. Документирование: бумажки — это важно</h3><p>Без документации можно потерять контроль над сетью: свободные порты окажутся заняты, кабели не будут подписаны, схемы станут нечитаемыми.</p><p><strong>Советы:</strong></p><ul><li><p>Ведите журнал портов на коммутаторах.</p></li><li><p>Создавайте схемы сети на уровнях L1/L2/L3, не перегружая одну схему всеми деталями.</p></li><li><p>Обновляйте документацию регулярно.</p></li></ul><hr><h3>Заключение</h3><p>Правильное проектирование и эксплуатация сети — это не только вопрос техники, но и организации процессов. Соблюдение этих рекомендаций помогает минимизировать ошибки, повышает стабильность и скорость работы сети.</p><p>Даже если что-то осталось за кадром — избегая этих типичных ошибок, вы уже делаете сеть более надёжной.</p>]]></description><guid isPermaLink="false">18</guid><pubDate>Fri, 06 Feb 2026 18:32:07 +0000</pubDate></item><item><title>&#x41A;&#x430;&#x43A; &#x43F;&#x440;&#x43E;&#x435;&#x43A;&#x442;&#x438;&#x440;&#x43E;&#x432;&#x430;&#x442;&#x44C; &#x43E;&#x442;&#x43A;&#x430;&#x437;&#x43E;&#x443;&#x441;&#x442;&#x43E;&#x439;&#x447;&#x438;&#x432;&#x44B;&#x435; &#x438; &#x43C;&#x430;&#x441;&#x448;&#x442;&#x430;&#x431;&#x438;&#x440;&#x443;&#x435;&#x43C;&#x44B;&#x435; &#x441;&#x43E;&#x431;&#x44B;&#x442;&#x438;&#x439;&#x43D;&#x43E;-&#x43E;&#x440;&#x438;&#x435;&#x43D;&#x442;&#x438;&#x440;&#x43E;&#x432;&#x430;&#x43D;&#x43D;&#x44B;&#x435; &#x441;&#x438;&#x441;&#x442;&#x435;&#x43C;&#x44B; (EDA)</title><link>https://ithub.uno/statiarticles/9_infrastructure/%D0%BA%D0%B0%D0%BA-%D0%BF%D1%80%D0%BE%D0%B5%D0%BA%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D1%82%D1%8C-%D0%BE%D1%82%D0%BA%D0%B0%D0%B7%D0%BE%D1%83%D1%81%D1%82%D0%BE%D0%B9%D1%87%D0%B8%D0%B2%D1%8B%D0%B5-%D0%B8-%D0%BC%D0%B0%D1%81%D1%88%D1%82%D0%B0%D0%B1%D0%B8%D1%80%D1%83%D0%B5%D0%BC%D1%8B%D0%B5-%D1%81%D0%BE%D0%B1%D1%8B%D1%82%D0%B8%D0%B9%D0%BD%D0%BE-%D0%BE%D1%80%D0%B8%D0%B5%D0%BD%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%BD%D1%8B%D0%B5-%D1%81%D0%B8%D1%81%D1%82%D0%B5%D0%BC%D1%8B-eda-r21/</link><description><![CDATA[
<p><img src="https://ithub.uno/uploads/tmi_files_2026_02/bd35seb9lw8v3bs4rhzuth2ucyk1knvp.webp.f86bf5104093a47cbecc2e7b27947a1b.webp" /></p>
<h3>Введение</h3><p>Событийно-ориентированные архитектуры (EDA) на бумаге выглядят идеальными: продюсеры и консюмеры отделены друг от друга, потоки асинхронны, а система легко масштабируется. Но реальность часто оказывается сложнее.</p><p>Представьте распродажу на «Чёрную пятницу»: ваша система обработки платежей получает в 5 раз больше трафика. В этот момент серверлесс-функции запускаются «холодно», очереди SQS переполняются, а DynamoDB начинает троттлить. Результат: сбои заказов клиентов. И это не гипотетический сценарий — с этим сталкиваются многие команды eCommerce, SaaS и FinTech.</p><p>Система EDA в высокоуровневом виде состоит из трёх компонентов: продюсер → буфер/очередь → консюмер. При проектировании важно учитывать не только непрерывную работу, но и предсказуемость системы под нагрузкой. Пиковые нагрузки могут быть вызваны интеграциями, узкими местами потребителей или бесконечными повторными попытками сообщений — всё это проверяет архитектуру на прочность.</p><hr><h3>Задержка — не единственная проблема</h3><p>Когда говорят о производительности EDA, обычно имеют в виду задержку. Но для отказоустойчивых систем важны также:</p><ul><li><p>Пропускная способность</p></li><li><p>Эффективное использование ресурсов</p></li><li><p>Надёжная передача данных между компонентами</p></li></ul><p><strong>Пример:</strong><br>Если сервис зависит от SQS и трафик резко возрастает, downstream-системы могут перегрузиться. Это приводит к повторным попыткам, росту задержек и искажению метрик мониторинга. Даже продуманный DLQ, экспоненциальное затухание и троттлинг не решат проблему, если не учитывать контракты между компонентами.</p><p><strong>Вывод:</strong> задержка — это сигнал о «давлении» в системе. Её нужно воспринимать как индикатор накопления нагрузки, а не только минимизировать.</p><hr><h3>Паттерны проектирования для масштабируемости и отказоустойчивости</h3><h4>1. Шардирование и перемешивающее шардирование</h4><p>Разделяйте клиентов или события на несколько шардов, чтобы шумный клиент не перегружал всю систему.</p><p><strong>Пример:</strong><br>В очереди SQS несколько клиентов могут быть хэшированы на одну очередь. Если один клиент начинает генерировать пик событий, он влияет на всех остальных. Перемешивающее шардирование уменьшает вероятность этого, распределяя клиентов случайным образом по разным очередям.</p><h4>2. Предварительное выделение ресурсов для критических задач</h4><p>Для задач с высокой чувствительностью к задержке (например, обнаружение мошенничества в FinTech) заранее выделяйте ресурсы.</p><p><strong>Пример:</strong><br>Для AWS Lambda используйте provisioned concurrency или авто-масштабирование с выделенной параллельностью. Это гарантирует быструю обработку критических событий, сохраняя экономичность при изменении нагрузки.</p><hr><h3>Паттерны инфраструктуры</h3><h4>1. Очереди и буферы</h4><p>Очереди SQS, Kafka, Kinesis и EventBridge действуют как буферы между продюсерами и консюмерами, поглощая резкие всплески нагрузки.</p><p><strong>Пример:</strong></p><ul><li><p>Реальное время кликов на рекламной платформе → Kinesis (шардирование по региону)</p></li><li><p>Выставление счетов → FIFO SQS для гарантии порядка и предотвращения дублирования</p></li></ul><h4>2. Быстрый сбой и предсказуемый отказ</h4><p>Если консюмер не может обработать событие (например, база данных недоступна), лучше завершить операцию с ошибкой сразу, чем блокировать очередь на длительное время.</p><p><strong>Пример:</strong><br>Контейнер Lambda зависал на аутентификации 30 секунд → добавили тайм-аут 5 секунд и явное завершение с ошибкой → очередь перестала накапливать сообщения.</p><hr><h3>Распространённые ошибки и как их избежать</h3><ol><li><p><strong>Переоценка средней нагрузки:</strong><br>Систему нужно тестировать под резкие пики (p95, p99), а не под средние значения.</p></li><li><p><strong>Повторные попытки как панацея:</strong><br>Бесконтрольные повторные попытки могут создать петли трафика и троттлинг. Используйте экспоненциальное затухание с джиттером и разделяйте ошибки на повторяемые и нет.</p></li><li><p><strong>Недостаточная наблюдаемость:</strong><br>Метрики должны показывать не только ошибки и время отклика, но и глубину очередей, повторные попытки и масштабируемость компонентов.</p></li><li><p><strong>Одинаковое обращение со всеми событиями:</strong><br>Событие оплаты ≠ событие логирования. Разделяйте критические и низкоприоритетные события с помощью отдельных очередей или маршрутизации в разные Lambdas.</p></li></ol><hr><h3>Заключение</h3><p>Отказоустойчивость — это не попытка создать «идеальную систему», а способность выдерживать удары и продолжать работу. Основные принципы:</p><ul><li><p>Эластичность и буферы, поглощающие пики нагрузки</p></li><li><p>Умные повторные попытки</p></li><li><p>Предсказуемые режимы отказа</p></li><li><p>Наблюдаемость, позволяющая подтверждать работоспособность системы</p></li></ul><p><strong>С чего начать:</strong><br>Создайте простое событийно-ориентированное приложение на SQS и Lambda. Попробуйте DLQ, обработку сбоев и маршрутизацию событий через EventBridge. Постепенно добавляйте шардирование, авто-масштабирование и сложные паттерны.</p><p>Отказоустойчивость — это подход, который строится шаг за шагом. Начните с малого, изучайте поведение системы и постепенно добавляйте сложность.</p>]]></description><guid isPermaLink="false">21</guid><pubDate>Fri, 06 Feb 2026 18:34:15 +0000</pubDate></item><item><title>&#x41A;&#x430;&#x43A; &#x431;&#x435;&#x437;&#x43E;&#x43F;&#x430;&#x441;&#x43D;&#x43E; &#x43F;&#x435;&#x440;&#x435;&#x43D;&#x435;&#x441;&#x442;&#x438; &#x431;&#x430;&#x437;&#x443; &#x434;&#x430;&#x43D;&#x43D;&#x44B;&#x445; PostgreSQL: &#x43F;&#x43E;&#x43B;&#x43D;&#x43E;&#x435; &#x440;&#x443;&#x43A;&#x43E;&#x432;&#x43E;&#x434;&#x441;&#x442;&#x432;&#x43E; &#x43F;&#x43E; &#x43B;&#x43E;&#x433;&#x438;&#x447;&#x435;&#x441;&#x43A;&#x43E;&#x439; &#x440;&#x435;&#x43F;&#x43B;&#x438;&#x43A;&#x430;&#x446;&#x438;&#x438; &#x438; &#x434;&#x440;&#x443;&#x433;&#x438;&#x43C; &#x43C;&#x435;&#x442;&#x43E;&#x434;&#x430;&#x43C;</title><link>https://ithub.uno/statiarticles/9_infrastructure/%D0%BA%D0%B0%D0%BA-%D0%B1%D0%B5%D0%B7%D0%BE%D0%BF%D0%B0%D1%81%D0%BD%D0%BE-%D0%BF%D0%B5%D1%80%D0%B5%D0%BD%D0%B5%D1%81%D1%82%D0%B8-%D0%B1%D0%B0%D0%B7%D1%83-%D0%B4%D0%B0%D0%BD%D0%BD%D1%8B%D1%85-postgresql-%D0%BF%D0%BE%D0%BB%D0%BD%D0%BE%D0%B5-%D1%80%D1%83%D0%BA%D0%BE%D0%B2%D0%BE%D0%B4%D1%81%D1%82%D0%B2%D0%BE-%D0%BF%D0%BE-%D0%BB%D0%BE%D0%B3%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%BE%D0%B9-%D1%80%D0%B5%D0%BF%D0%BB%D0%B8%D0%BA%D0%B0%D1%86%D0%B8%D0%B8-%D0%B8-%D0%B4%D1%80%D1%83%D0%B3%D0%B8%D0%BC-%D0%BC%D0%B5%D1%82%D0%BE%D0%B4%D0%B0%D0%BC-r24/</link><description><![CDATA[
<p><img src="https://ithub.uno/uploads/tmi_files_2026_02/MD-989.webp.ebac0bcc749f27933f24faa78c86ccdc.webp" /></p>
<h2>Введение</h2><p>Перенос базы данных PostgreSQL — задача непростая, особенно для больших проектов. Часто это один из самых крупных и ответственных процессов для разработчиков и администраторов. Основные сценарии переноса включают:</p><ul><li><p>обновление до новой версии PostgreSQL;</p></li><li><p>перенос базы на другой сервер или хостинг;</p></li><li><p>миграция с минимальным временем простоя.</p></li></ul><p>В зависимости от размера базы и ограничений инфраструктуры есть три основных подхода.</p><hr><h2>1. Перенос с помощью pg_dump и pg_restore</h2><p><code>pg_dump</code> позволяет создать дамп всей базы, включая схемы, таблицы и специальные объекты. Для небольших баз (50–150 ГБ) это часто самый простой вариант.</p><p><strong>Пример использования:</strong></p><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code>pg_dump -Fc $SOURCE_DB_URI &gt; dump_file.dump
pg_restore --no-acl --no-owner -d $TARGET_DB_URI dump_file.dump
</code></pre><p><strong>Плюсы:</strong></p><ul><li><p>Надёжно и просто;</p></li><li><p>Полный дамп базы, включая схему и данные.</p></li></ul><p><strong>Минусы:</strong></p><ul><li><p>При больших базах (сотни ГБ и выше) процесс может занять часы;</p></li><li><p>Требуется время на восстановление и минимизация простоя.</p></li></ul><hr><h2>2. Использование WAL (Write-Ahead Logging)</h2><p>Если у вас настроено резервное копирование на основе WAL, например через <strong>pgBackRest</strong>, <strong>WAL-G</strong> или <strong>WAL-E</strong>, можно выполнить масштабную миграцию:</p><ol><li><p>Создаётся полная резервная копия базы;</p></li><li><p>Настраивается потоковая передача WAL на новый сервер;</p></li><li><p>После завершения первичной синхронизации можно переключить приложение на новую базу с минимальным простоем.</p></li></ol><p><strong>Плюсы:</strong></p><ul><li><p>Подходит для терабайтных баз;</p></li><li><p>Минимизирует простой.</p></li></ul><p><strong>Минусы:</strong></p><ul><li><p>Требует доступа к WAL (не поддерживается, например, в Amazon RDS).</p></li></ul><hr><h2>3. Логическая миграция PostgreSQL</h2><p>Логическая репликация позволяет переносить данные на новый сервер без доступа к WAL.</p><ul><li><p><strong>Принцип работы:</strong> текущая база (publisher) передаёт изменения новой базе (subscriber);</p></li><li><p>Репликация распространяется на данные таблиц, но <strong>не переносит схему, индексы и последовательности</strong>;</p></li><li><p>С помощью дополнительных шагов можно выполнить полную миграцию.</p></li></ul><h3>Основные шаги логической миграции</h3><h4>Шаг 1: Перенос схемы</h4><p>Сначала необходимо создать на новом сервере структуру базы:</p><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code>pg_dump -Fc -s $SOURCE_DB_URI | pg_restore --no-acl --no-owner -d $TARGET_DB_URI
</code></pre><ul><li><p>При активной разработке изменений схемы: синхронизируйте изменения и на подписчике.</p></li></ul><h4>Шаг 2: Настройка издателя (старый сервер)</h4><ol><li><p>Включите логическую репликацию:</p></li></ol><pre spellcheck="" class="tmiCode language-sql" data-language="SQL"><code>ALTER SYSTEM SET wal_level = logical;
</code></pre><ol start="2"><li><p>Настройте параметры слотов репликации:</p></li></ol><pre spellcheck="" class="tmiCode" data-language="text"><code>max_replication_slots
max_wal_senders
max_logical_replication_workers
max_worker_processes
max_sync_workers_per_subscription
</code></pre><ol start="3"><li><p>Убедитесь, что сеть разрешает подключения с нового сервера.</p></li><li><p>Создайте пользователя для репликации:</p></li></ol><pre spellcheck="" class="tmiCode language-sql" data-language="SQL"><code>CREATE ROLE elizabeth WITH REPLICATION LOGIN PASSWORD 'my_password';
GRANT SELECT ON ALL TABLES IN SCHEMA public TO elizabeth;
</code></pre><ol start="5"><li><p>Определите таблицы без первичных ключей:</p></li></ol><pre spellcheck="" class="tmiCode language-sql" data-language="SQL"><code>select tab.table_schema, tab.table_name
from information_schema.tables tab
left join information_schema.table_constraints tco
  on tab.table_schema = tco.table_schema
  and tab.table_name = tco.table_name
  and tco.constraint_type = 'PRIMARY KEY'
where tab.table_type = 'BASE TABLE'
  and tab.table_schema not in ('pg_catalog', 'information_schema')
  and tco.constraint_name is null
order by table_schema, table_name;
</code></pre><ul><li><p>Для таких таблиц используйте уникальный индекс или REPLICA IDENTITY FULL:</p></li></ul><pre spellcheck="" class="tmiCode language-sql" data-language="SQL"><code>ALTER TABLE tablename REPLICA IDENTITY USING INDEX idx_unique_index;
-- или
ALTER TABLE tablename REPLICA IDENTITY FULL;
</code></pre><ol start="6"><li><p>Создайте публикацию всех таблиц:</p></li></ol><pre spellcheck="" class="tmiCode language-sql" data-language="SQL"><code>CREATE PUBLICATION bridge_migration FOR ALL TABLES;
SELECT * FROM pg_publication_tables;
</code></pre><h4>Шаг 3: Настройка подписчика (новый сервер)</h4><p>Создаём подписку на публикацию:</p><pre spellcheck="" class="tmiCode language-sql" data-language="SQL"><code>CREATE SUBSCRIPTION bridge_migration
CONNECTION 'host={host} port=5432 dbname={database} user={login} password={password}'
PUBLICATION bridge_migration;
</code></pre><ul><li><p>Для больших баз можно ограничить число одновременно синхронизируемых таблиц через <code>max_sync_workers_per_subscription</code>.</p></li></ul><h4>Шаг 4: Мониторинг первичной загрузки</h4><p>Проверяем прогресс через:</p><pre spellcheck="" class="tmiCode language-sql" data-language="SQL"><code>SELECT * FROM pg_stat_subscription;
SELECT * FROM pg_subscription_rel;
</code></pre><ul><li><p>Состояния таблиц:</p><ul><li><p><code>i</code> — инициализация</p></li><li><p><code>d</code> — копирование данных</p></li><li><p><code>f</code> — копирование завершено</p></li><li><p><code>s</code> — синхронизация выполнена</p></li><li><p><code>r</code> — обычная репликация</p></li></ul></li></ul><h4>Шаг 5: Тестирование и переключение</h4><ol><li><p>Остановите запись на исходной базе;</p></li><li><p>Проверьте данные на новом сервере;</p></li><li><p>Переключите приложение на новую базу.</p></li></ol><h4>Шаг 6: Синхронизация последовательностей</h4><p>Логическая репликация не переносит последовательности. Используйте команды <code>setval</code>:</p><pre spellcheck="" class="tmiCode language-sql" data-language="SQL"><code>SELECT
  'SELECT setval(' || quote_literal(quote_ident(n.nspname) || '.' || quote_ident(c.relname)) || ', ' || s.last_value || ');'
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
JOIN pg_sequences s ON s.schemaname = n.nspname AND s.sequencename = c.relname
WHERE c.relkind = 'S';
</code></pre><ul><li><p>Выполните результат на новом сервере, чтобы синхронизировать все последовательности.</p></li></ul><hr><h2>Заключение</h2><p>Логическая репликация — безопасный и эффективный способ миграции PostgreSQL, особенно при крупных базах и ограничениях по доступу к WAL.</p><ul><li><p>Данные остаются согласованными, если схема подписчика идентична схеме издателя;</p></li><li><p>Репликация однонаправленная, без конфликтующих записей на подписчике;</p></li><li><p>Можно минимизировать простой при переходе на новый сервер.</p></li></ul>]]></description><guid isPermaLink="false">24</guid><pubDate>Fri, 06 Feb 2026 18:39:53 +0000</pubDate></item><item><title>&#x41C;&#x43E;&#x43D;&#x438;&#x442;&#x43E;&#x440;&#x438;&#x43D;&#x433; Linux-&#x441;&#x435;&#x440;&#x432;&#x435;&#x440;&#x43E;&#x432;: Prometheus, Grafana &#x438; &#x43F;&#x440;&#x430;&#x432;&#x438;&#x43B;&#x44C;&#x43D;&#x44B;&#x435; &#x430;&#x43B;&#x435;&#x440;&#x442;&#x44B;</title><link>https://ithub.uno/statiarticles/9_infrastructure/%D0%BC%D0%BE%D0%BD%D0%B8%D1%82%D0%BE%D1%80%D0%B8%D0%BD%D0%B3-linux-%D1%81%D0%B5%D1%80%D0%B2%D0%B5%D1%80%D0%BE%D0%B2-prometheus-grafana-%D0%B8-%D0%BF%D1%80%D0%B0%D0%B2%D0%B8%D0%BB%D1%8C%D0%BD%D1%8B%D0%B5-%D0%B0%D0%BB%D0%B5%D1%80%D1%82%D1%8B-r43/</link><description><![CDATA[
<p><img src="https://ithub.uno/uploads/tmi_files_2026_02/hq720.jpg.37967727eb5b5a07ae7f7c7150d4c9fc.jpg" /></p>
<p>«Как вы узнали о проблеме?» — «Пожаловались пользователи» — так работать нельзя. Правильный мониторинг означает, что вы знаете о проблеме раньше, чем её заметят пользователи. Эта статья о построении полноценного стека мониторинга для Linux-инфраструктуры: от сбора метрик до умных алертов.</p><hr><h2>Архитектура: что и зачем</h2><pre spellcheck="" class="tmiCode language-plaintext" data-language="Простой текст"><code>Серверы                   Мониторинг             Визуализация
[node_exporter] ──────► [Prometheus] ──────► [Grafana]
[php-fpm_exporter]          │                    │
[mysql_exporter]            │ алерты         дашборды
[nginx_exporter]            ▼
[redis_exporter]       [Alertmanager]
                            │
                    [Email/Slack/PagerDuty]
</code></pre><p>Prometheus — это time-series база данных с pull-моделью сбора данных. Exporters на серверах открывают HTTP endpoint с метриками в формате Prometheus, и сервер Prometheus их периодически «скрейпит».</p><hr><h2>Node Exporter: метрики операционной системы</h2><h3>Установка</h3><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># Через пакет
apt install prometheus-node-exporter  # Ubuntu
# или скачиваем бинарник

# Проверяем endpoint
curl http://localhost:9100/metrics | head -50
</code></pre><h3>Что собирает node_exporter</h3><pre spellcheck="" class="tmiCode language-plaintext" data-language="Простой текст"><code># CPU
node_cpu_seconds_total{cpu="0",mode="idle"}
node_cpu_seconds_total{cpu="0",mode="user"}
node_cpu_seconds_total{cpu="0",mode="system"}
node_cpu_seconds_total{cpu="0",mode="iowait"}

# Память
node_memory_MemTotal_bytes
node_memory_MemAvailable_bytes
node_memory_SwapUsed_bytes

# Диски
node_disk_read_bytes_total{device="sda"}
node_disk_written_bytes_total{device="sda"}
node_disk_io_time_seconds_total{device="sda"}

# Сеть
node_network_receive_bytes_total{device="eth0"}
node_network_transmit_bytes_total{device="eth0"}
node_network_receive_errs_total{device="eth0"}

# Файловая система
node_filesystem_avail_bytes{mountpoint="/"}
node_filesystem_size_bytes{mountpoint="/"}

# Нагрузка
node_load1   # средняя нагрузка за 1 минуту
node_load5
node_load15
</code></pre><h3>Кастомные метрики через textfile collector</h3><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># Создаём директорию для textfile
mkdir -p /var/lib/node_exporter/textfile_collector

# Запускаем node_exporter с collector
/usr/bin/prometheus-node-exporter \
    --collector.textfile.directory=/var/lib/node_exporter/textfile_collector

# Скрипт для метрик приложения (запускаем по cron)
cat &gt; /usr/local/bin/app-metrics.sh &lt;&lt; 'EOF'
#!/bin/bash

METRICS_FILE="/var/lib/node_exporter/textfile_collector/app.prom"

# Количество PHP-FPM процессов
fpm_workers=$(ps aux | grep php-fpm | grep -v grep | wc -l)

# Количество MySQL соединений
mysql_connections=$(mysql -u monitoring -ppassword -e "SHOW STATUS LIKE 'Threads_connected';" | awk 'NR==2{print $2}')

# Место в очереди Redis
redis_queue_size=$(redis-cli llen myapp:jobs)

cat &gt; "$METRICS_FILE" &lt;&lt; METRICS
# HELP myapp_fpm_workers Number of PHP-FPM worker processes
# TYPE myapp_fpm_workers gauge
myapp_fpm_workers $fpm_workers

# HELP myapp_mysql_connections Active MySQL connections
# TYPE myapp_mysql_connections gauge
myapp_mysql_connections $mysql_connections

# HELP myapp_queue_size Redis job queue size
# TYPE myapp_queue_size gauge
myapp_queue_size $redis_queue_size
METRICS
EOF
chmod +x /usr/local/bin/app-metrics.sh

# Добавляем в cron (каждую минуту)
echo "* * * * * root /usr/local/bin/app-metrics.sh" &gt; /etc/cron.d/app-metrics
</code></pre><hr><h2>Установка Prometheus</h2><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># Создаём пользователя
useradd --no-create-home --shell /bin/false prometheus

# Создаём директории
mkdir -p /etc/prometheus /var/lib/prometheus
chown prometheus:prometheus /var/lib/prometheus

# Скачиваем (проверьте актуальную версию)
cd /tmp
wget https://github.com/prometheus/prometheus/releases/download/v2.50.1/prometheus-2.50.1.linux-amd64.tar.gz
tar xvf prometheus-*.tar.gz
cp prometheus-*/prometheus /usr/local/bin/
cp prometheus-*/promtool /usr/local/bin/
cp -r prometheus-*/consoles /etc/prometheus/
cp -r prometheus-*/console_libraries /etc/prometheus/
chown prometheus:prometheus /usr/local/bin/prometheus /usr/local/bin/promtool
</code></pre><h3>Конфигурация Prometheus</h3><p><code>/etc/prometheus/prometheus.yml</code>:</p><pre spellcheck="" class="tmiCode language-yaml" data-language="YAML"><code>global:
  scrape_interval: 15s      # как часто собираем метрики
  evaluation_interval: 15s  # как часто оцениваем правила алертов
  scrape_timeout: 10s

# Правила алертов
rule_files:
  - /etc/prometheus/rules/*.yml

# Куда отправлять алерты
alerting:
  alertmanagers:
    - static_configs:
        - targets:
          - localhost:9093

# Источники метрик
scrape_configs:
  # Сам Prometheus
  - job_name: 'prometheus'
    static_configs:
      - targets: ['localhost:9090']

  # Node exporters — наши серверы
  - job_name: 'node'
    static_configs:
      - targets:
          - 'web01:9100'
          - 'web02:9100'
          - 'db01:9100'
    # Добавляем метки для группировки
    relabel_configs:
      - source_labels: [__address__]
        target_label: instance
    # Статические метки
    static_configs:
      - targets: ['web01:9100']
        labels:
          env: production
          role: web
      - targets: ['db01:9100']
        labels:
          env: production
          role: database

  # MySQL exporter
  - job_name: 'mysql'
    static_configs:
      - targets: ['localhost:9104']

  # Nginx exporter
  - job_name: 'nginx'
    static_configs:
      - targets: ['localhost:9113']

  # Redis exporter
  - job_name: 'redis'
    static_configs:
      - targets: ['localhost:9121']

  # PHP-FPM — через статус страницу
  - job_name: 'php-fpm'
    static_configs:
      - targets: ['localhost:9253']

  # Service discovery через файлы (удобно для динамической инфраструктуры)
  - job_name: 'dynamic-servers'
    file_sd_configs:
      - files:
          - /etc/prometheus/targets/*.yml
        refresh_interval: 30s
</code></pre><h3>Systemd unit для Prometheus</h3><pre spellcheck="" class="tmiCode language-ini" data-language="ini"><code>[Unit]
Description=Prometheus Monitoring
Wants=network-online.target
After=network-online.target

[Service]
User=prometheus
Group=prometheus
Type=simple
ExecStart=/usr/local/bin/prometheus \
    --config.file=/etc/prometheus/prometheus.yml \
    --storage.tsdb.path=/var/lib/prometheus \
    --storage.tsdb.retention.time=30d \
    --storage.tsdb.retention.size=10GB \
    --web.enable-lifecycle \
    --web.enable-admin-api

Restart=on-failure
RestartSec=5s

[Install]
WantedBy=multi-user.target
</code></pre><hr><h2>PromQL: язык запросов</h2><p>PromQL — мощный язык для работы с time-series. Основные паттерны:</p><pre spellcheck="" class="tmiCode" data-language="promql"><code># Мгновенные значения
node_memory_MemAvailable_bytes

# Использование памяти в %
(1 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)) * 100

# CPU usage (rate нужен для счётчиков)
100 - (avg by (instance) (
    rate(node_cpu_seconds_total{mode="idle"}[5m])
) * 100)

# Disk I/O latency
rate(node_disk_io_time_seconds_total[5m])

# Свободное место на диске в %
(node_filesystem_avail_bytes / node_filesystem_size_bytes) * 100

# Количество TCP соединений по состоянию
node_netstat_Tcp_CurrEstab

# Nginx requests per second
rate(nginx_http_requests_total[5m])

# 95-й перцентиль времени ответа
histogram_quantile(0.95, 
    rate(http_request_duration_seconds_bucket[5m])
)

# Агрегация по серверам
sum by (instance) (rate(node_cpu_seconds_total{mode!="idle"}[5m]))

# Топ 5 серверов по CPU
topk(5, 
    100 - avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100
)
</code></pre><hr><h2>Правила алертов</h2><p><code>/etc/prometheus/rules/linux.yml</code>:</p><pre spellcheck="" class="tmiCode language-yaml" data-language="YAML"><code>groups:
  - name: linux_nodes
    rules:
      # CPU
      - alert: HighCPUUsage
        expr: |
          100 - (avg by (instance) (
            rate(node_cpu_seconds_total{mode="idle"}[5m])
          ) * 100) &gt; 85
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "High CPU on {{ $labels.instance }}"
          description: "CPU usage is {{ printf \"%.1f\" $value }}% on {{ $labels.instance }}"

      - alert: CriticalCPUUsage
        expr: |
          100 - (avg by (instance) (
            rate(node_cpu_seconds_total{mode="idle"}[5m])
          ) * 100) &gt; 95
        for: 2m
        labels:
          severity: critical
        annotations:
          summary: "CRITICAL CPU on {{ $labels.instance }}"

      # Память
      - alert: HighMemoryUsage
        expr: |
          (1 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)) * 100 &gt; 90
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "High memory on {{ $labels.instance }}"
          description: "Memory usage is {{ printf \"%.1f\" $value }}%"

      # Диск
      - alert: DiskSpaceLow
        expr: |
          (node_filesystem_avail_bytes{mountpoint="/"} / node_filesystem_size_bytes{mountpoint="/"}) * 100 &lt; 15
        for: 2m
        labels:
          severity: warning
        annotations:
          summary: "Low disk space on {{ $labels.instance }}"
          description: "Only {{ printf \"%.1f\" $value }}% disk space remaining"

      - alert: DiskSpaceCritical
        expr: |
          (node_filesystem_avail_bytes{mountpoint="/"} / node_filesystem_size_bytes{mountpoint="/"}) * 100 &lt; 5
        for: 1m
        labels:
          severity: critical
        annotations:
          summary: "CRITICAL: Disk almost full on {{ $labels.instance }}"

      # Инод
      - alert: DiskInodesLow
        expr: |
          (node_filesystem_files_free / node_filesystem_files) * 100 &lt; 10
        for: 2m
        labels:
          severity: warning

      # Сервер недоступен
      - alert: InstanceDown
        expr: up == 0
        for: 1m
        labels:
          severity: critical
        annotations:
          summary: "Instance {{ $labels.instance }} is DOWN"

      # Load average
      - alert: HighLoadAverage
        expr: node_load1 &gt; (count by (instance)(node_cpu_seconds_total{mode="idle"}) * 2)
        for: 5m
        labels:
          severity: warning

      # OOM Killer
      - alert: OOMKillerActive
        expr: increase(node_vmstat_oom_kill[5m]) &gt; 0
        labels:
          severity: critical
        annotations:
          summary: "OOM Killer active on {{ $labels.instance }}"

      # Много TIME_WAIT соединений
      - alert: HighTimeWaitConnections
        expr: node_sockstat_TCP_tw &gt; 10000
        for: 5m
        labels:
          severity: warning
</code></pre><hr><h2>Alertmanager: умная маршрутизация уведомлений</h2><p><code>/etc/alertmanager/alertmanager.yml</code>:</p><pre spellcheck="" class="tmiCode language-yaml" data-language="YAML"><code>global:
  smtp_smarthost: 'smtp.gmail.com:587'
  smtp_from: 'alerts@example.com'
  smtp_auth_username: 'alerts@example.com'
  smtp_auth_password: 'password'
  
  slack_api_url: 'https://hooks.slack.com/services/YOUR/SLACK/WEBHOOK'

# Шаблоны уведомлений
templates:
  - /etc/alertmanager/templates/*.tmpl

# Маршрутизация
route:
  group_by: ['alertname', 'instance']
  group_wait: 30s       # ждём перед первым уведомлением
  group_interval: 5m    # интервал между повторными уведомлениями группы
  repeat_interval: 4h   # когда повторить если не решено
  
  receiver: 'slack-warnings'
  
  routes:
    # Критические — немедленно в PagerDuty
    - match:
        severity: critical
      receiver: 'pagerduty-critical'
      group_wait: 0s
      repeat_interval: 1h
    
    # Ночью тишина для warnings
    - match:
        severity: warning
      receiver: 'slack-warnings'
      mute_time_intervals:
        - nights-and-weekends
    
    # Отдельный канал для базы данных
    - match:
        job: mysql
      receiver: 'slack-dba-channel'

# Время тишины
time_intervals:
  - name: nights-and-weekends
    time_intervals:
      - weekdays: [saturday, sunday]
      - times:
          - start_time: '22:00'
            end_time: '08:00'

# Получатели
receivers:
  - name: 'slack-warnings'
    slack_configs:
      - channel: '#alerts'
        icon_emoji: ':warning:'
        title: '{{ .GroupLabels.alertname }}'
        text: |
          {{ range .Alerts }}
          *Instance:* {{ .Labels.instance }}
          *Description:* {{ .Annotations.description }}
          {{ end }}
        send_resolved: true

  - name: 'pagerduty-critical'
    pagerduty_configs:
      - service_key: 'YOUR_PAGERDUTY_KEY'

  - name: 'slack-dba-channel'
    slack_configs:
      - channel: '#dba-alerts'
</code></pre><hr><h2>Grafana: визуализация</h2><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># Установка
apt-get install -y apt-transport-https software-properties-common
wget -q -O - https://packages.grafana.com/gpg.key | gpg --dearmor | \
    tee /usr/share/keyrings/grafana.gpg &gt; /dev/null
echo "deb [signed-by=/usr/share/keyrings/grafana.gpg] \
    https://packages.grafana.com/oss/deb stable main" | \
    tee /etc/apt/sources.list.d/grafana.list
apt-get update &amp;&amp; apt-get install grafana -y
systemctl enable --now grafana-server
</code></pre><h3>Provisioning дашбордов через код</h3><p><code>/etc/grafana/provisioning/datasources/prometheus.yaml</code>:</p><pre spellcheck="" class="tmiCode language-yaml" data-language="YAML"><code>apiVersion: 1
datasources:
  - name: Prometheus
    type: prometheus
    access: proxy
    url: http://localhost:9090
    isDefault: true
    jsonData:
      timeInterval: "15s"
</code></pre><p><code>/etc/grafana/provisioning/dashboards/default.yaml</code>:</p><pre spellcheck="" class="tmiCode language-yaml" data-language="YAML"><code>apiVersion: 1
providers:
  - name: default
    orgId: 1
    folder: ''
    type: file
    disableDeletion: false
    updateIntervalSeconds: 30
    options:
      path: /var/lib/grafana/dashboards
</code></pre><h3>Готовые дашборды</h3><p>На grafana.com/dashboards есть тысячи готовых дашбордов. Популярные ID для импорта:</p><ul><li><p><code>1860</code> — Node Exporter Full</p></li><li><p><code>7362</code> — MySQL Overview</p></li><li><p><code>763</code> — Redis Dashboard</p></li><li><p><code>12708</code> — PHP-FPM Dashboard</p></li><li><p><code>11074</code> — Node Exporter for Prometheus</p></li></ul><hr><h2>blackbox_exporter: мониторинг снаружи</h2><p>Для мониторинга HTTP, TCP, DNS, ICMP с внешней точки зрения:</p><pre spellcheck="" class="tmiCode language-yaml" data-language="YAML"><code># /etc/blackbox_exporter/config.yml
modules:
  http_2xx:
    prober: http
    timeout: 5s
    http:
      valid_http_versions: ["HTTP/1.1", "HTTP/2.0"]
      valid_status_codes: []  # 2xx
      follow_redirects: true
      tls_config:
        insecure_skip_verify: false

  http_post_2xx:
    prober: http
    http:
      method: POST
      headers:
        Content-Type: application/json
      body: '{"probe": "check"}'

  tcp_connect:
    prober: tcp
    timeout: 5s

  ssl_expiry:
    prober: http
    timeout: 5s
    http:
      fail_if_ssl: false
      fail_if_not_ssl: true
      tls_config:
        insecure_skip_verify: false
</code></pre><p>В prometheus.yml добавляем:</p><pre spellcheck="" class="tmiCode language-yaml" data-language="YAML"><code>- job_name: 'blackbox'
  metrics_path: /probe
  params:
    module: [http_2xx]
  static_configs:
    - targets:
        - https://myapp.example.com/health
        - https://api.example.com/status
  relabel_configs:
    - source_labels: [__address__]
      target_label: __param_target
    - source_labels: [__param_target]
      target_label: instance
    - target_label: __address__
      replacement: localhost:9115

# Алерт на SSL
- alert: SSLCertExpiringSoon
  expr: probe_ssl_earliest_cert_expiry - time() &lt; 86400 * 30
  labels:
    severity: warning
  annotations:
    summary: "SSL cert expires in {{ $value | humanizeDuration }}"
</code></pre><hr><p>Правильный мониторинг — это инвестиция, которая окупается при первом же инциденте, когда вы знаете о проблеме за 10 минут до того, как позвонят пользователи. Начните с node_exporter и базовых алертов, постепенно добавляйте экспортеры для ваших сервисов.</p>]]></description><guid isPermaLink="false">43</guid><pubDate>Sun, 22 Feb 2026 13:20:18 +0000</pubDate></item><item><title>Enterprise Linux: &#x440;&#x435;&#x437;&#x435;&#x440;&#x432;&#x43D;&#x43E;&#x435; &#x43A;&#x43E;&#x43F;&#x438;&#x440;&#x43E;&#x432;&#x430;&#x43D;&#x438;&#x435;, &#x442;&#x44E;&#x43D;&#x438;&#x43D;&#x433; Nginx, &#x446;&#x435;&#x43D;&#x442;&#x440;&#x430;&#x43B;&#x438;&#x437;&#x43E;&#x432;&#x430;&#x43D;&#x43D;&#x43E;&#x435; &#x43B;&#x43E;&#x433;&#x438;&#x440;&#x43E;&#x432;&#x430;&#x43D;&#x438;&#x435; &#x438; &#x43F;&#x440;&#x43E;&#x434;&#x432;&#x438;&#x43D;&#x443;&#x442;&#x430;&#x44F; &#x434;&#x438;&#x430;&#x433;&#x43D;&#x43E;&#x441;&#x442;&#x438;&#x43A;&#x430; &#x441;&#x435;&#x440;&#x432;&#x435;&#x440;&#x43E;&#x432;</title><link>https://ithub.uno/statiarticles/9_infrastructure/enterprise-linux-%D1%80%D0%B5%D0%B7%D0%B5%D1%80%D0%B2%D0%BD%D0%BE%D0%B5-%D0%BA%D0%BE%D0%BF%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5-%D1%82%D1%8E%D0%BD%D0%B8%D0%BD%D0%B3-nginx-%D1%86%D0%B5%D0%BD%D1%82%D1%80%D0%B0%D0%BB%D0%B8%D0%B7%D0%BE%D0%B2%D0%B0%D0%BD%D0%BD%D0%BE%D0%B5-%D0%BB%D0%BE%D0%B3%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5-%D0%B8-%D0%BF%D1%80%D0%BE%D0%B4%D0%B2%D0%B8%D0%BD%D1%83%D1%82%D0%B0%D1%8F-%D0%B4%D0%B8%D0%B0%D0%B3%D0%BD%D0%BE%D1%81%D1%82%D0%B8%D0%BA%D0%B0-%D1%81%D0%B5%D1%80%D0%B2%D0%B5%D1%80%D0%BE%D0%B2-r49/</link><description><![CDATA[
<p><img src="https://ithub.uno/uploads/tmi_files_2026_02/cp.png.ff29caa5a3850c6f2889b6b9689fca09.png" /></p>
<h1>Резервное копирование в Linux: стратегии и инструменты</h1><h2>Правило 3-2-1</h2><p>Любая стратегия резервного копирования должна начинаться с правила 3-2-1:</p><ul><li><p><strong>3</strong> копии данных</p></li><li><p><strong>2</strong> разных типа носителей</p></li><li><p><strong>1</strong> копия вне офиса</p></li></ul><h2>Rsync: умное инкрементальное копирование</h2><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code>#!/usr/bin/env bash
# Скрипт резервного копирования с ротацией

BACKUP_SOURCE="/var/www"
BACKUP_DEST="/mnt/backup"
RETAIN_DAYS=30
DATE=$(date +%Y%m%d_%H%M%S)

# Создаём снэпшот через hard links (не дублируем неизменённые файлы)
rsync -avz --delete \
    --link-dest="$BACKUP_DEST/latest" \
    --exclude="*.log" \
    --exclude="cache/" \
    --exclude="tmp/" \
    "$BACKUP_SOURCE/" \
    "$BACKUP_DEST/$DATE/"

# Обновляем симлинк на последний бэкап
ln -sfn "$BACKUP_DEST/$DATE" "$BACKUP_DEST/latest"

# Удаляем старые бэкапы
find "$BACKUP_DEST" -maxdepth 1 -type d -mtime +$RETAIN_DAYS -exec rm -rf {} +

echo "Backup completed: $BACKUP_DEST/$DATE"
du -sh "$BACKUP_DEST/$DATE"
</code></pre><h2>Borg: дедупликация и шифрование</h2><p>Borg — продвинутый инструмент с дедупликацией (одинаковые блоки хранятся один раз):</p><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># Установка
apt install borgbackup

# Инициализация репозитория с шифрованием
borg init --encryption=repokey-blake2 user@backup-server:/backups/myserver

# Создание бэкапа
borg create \
    --verbose \
    --filter AME \
    --list \
    --stats \
    --show-rc \
    --compression lz4 \
    --exclude-caches \
    --exclude '/home/*/.cache/*' \
    --exclude '/var/cache/*' \
    --exclude '/var/tmp/*' \
    user@backup-server:/backups/myserver::myserver-$(date +%Y%m%d_%H%M) \
    /etc \
    /var/www \
    /home \
    /var/lib/mysql  # осторожно с активной БД!

# Список архивов
borg list user@backup-server:/backups/myserver

# Проверка целостности
borg check user@backup-server:/backups/myserver

# Восстановление
cd /tmp/restore
borg extract user@backup-server:/backups/myserver::myserver-20240115_0300 \
    var/www/myapp/public  # только конкретная директория

# Ротация (хранить: 7 ежедневных, 4 недельных, 12 ежемесячных)
borg prune \
    --keep-daily=7 \
    --keep-weekly=4 \
    --keep-monthly=12 \
    user@backup-server:/backups/myserver
</code></pre><h2>Бэкап MySQL без блокировок</h2><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code>#!/usr/bin/env bash
# Бэкап MySQL с минимальным влиянием на продакшн

DB_USER="backup"
DB_PASS="backup_password"
BACKUP_DIR="/var/backups/mysql"
DATE=$(date +%Y%m%d_%H%M)

mkdir -p "$BACKUP_DIR"

# Создаём пользователя для бэкапа (только необходимые права)
# GRANT SELECT, LOCK TABLES, SHOW VIEW, EVENT, TRIGGER, PROCESS ON *.* TO 'backup'@'localhost';

# Бэкап всех баз
mysqldump \
    --user="$DB_USER" \
    --password="$DB_PASS" \
    --single-transaction \
    --routines \
    --triggers \
    --events \
    --all-databases \
    --master-data=2 \
    | gzip &gt; "$BACKUP_DIR/full-$DATE.sql.gz"

# Проверяем что файл не пустой
size=$(stat -c%s "$BACKUP_DIR/full-$DATE.sql.gz")
if [[ $size -lt 1000 ]]; then
    echo "ERROR: Backup file too small ($size bytes)" &gt;&amp;2
    rm "$BACKUP_DIR/full-$DATE.sql.gz"
    exit 1
fi

echo "Backup created: $BACKUP_DIR/full-$DATE.sql.gz ($size bytes)"

# Ротация — удаляем старше 7 дней
find "$BACKUP_DIR" -name "full-*.sql.gz" -mtime +7 -delete

# XtraBackup для горячего бэкапа InnoDB (без --single-transaction ограничений)
# apt install percona-xtrabackup-80
# xtrabackup --backup --user="$DB_USER" --password="$DB_PASS" \
#     --target-dir="$BACKUP_DIR/xtrabackup-$DATE"
</code></pre><h2>Проверка восстановления — самое важное</h2><p>Бэкап без проверки восстановления — не бэкап. Автоматизируйте:</p><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code>#!/usr/bin/env bash
# Тест восстановления MySQL (запускать еженедельно)

BACKUP_FILE=$(ls -t /var/backups/mysql/full-*.sql.gz | head -1)
TEST_DB="restore_test_$(date +%s)"

echo "Testing restore of $BACKUP_FILE"

# Создаём тестовую базу
mysql -e "CREATE DATABASE $TEST_DB"

# Восстанавливаем
zcat "$BACKUP_FILE" | mysql "$TEST_DB"

# Проверяем количество таблиц
table_count=$(mysql -sN -e "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='$TEST_DB'")
echo "Tables restored: $table_count"

# Удаляем тестовую базу
mysql -e "DROP DATABASE $TEST_DB"

if [[ $table_count -gt 0 ]]; then
    echo "Restore test PASSED"
else
    echo "Restore test FAILED!" &gt;&amp;2
    exit 1
fi
</code></pre><hr><h1>Nginx: тюнинг и продвинутая конфигурация</h1><h2>Производительность nginx</h2><pre spellcheck="" class="tmiCode language-nginx" data-language="Nginx"><code># /etc/nginx/nginx.conf

user www-data;
# Одни worker per CPU core
worker_processes auto;
# Привязываем к ядрам (снижаем context switch)
worker_cpu_affinity auto;

# Максимум соединений = worker_processes * worker_connections
events {
    worker_connections 4096;
    use epoll;           # лучший I/O multiplexer для Linux
    multi_accept on;     # принимаем все соединения за один раз
}

http {
    # Базовые оптимизации
    sendfile on;
    tcp_nopush on;       # отправлять заголовки и начало файла вместе
    tcp_nodelay on;      # отключить Nagle для активных соединений

    # Таймауты
    keepalive_timeout 65;
    keepalive_requests 1000;
    client_header_timeout 15;
    client_body_timeout 15;
    send_timeout 15;

    # Буферы
    client_body_buffer_size 128k;
    client_max_body_size 50M;
    client_header_buffer_size 1k;
    large_client_header_buffers 4 16k;

    # Сжатие
    gzip on;
    gzip_vary on;
    gzip_min_length 1024;
    gzip_comp_level 5;
    gzip_types
        text/plain
        text/css
        text/javascript
        application/javascript
        application/json
        application/xml
        image/svg+xml;

    # Кэширование статики
    open_file_cache max=10000 inactive=20s;
    open_file_cache_valid 30s;
    open_file_cache_min_uses 2;
    open_file_cache_errors on;

    # Безопасность
    server_tokens off;
    more_clear_headers Server;  # если установлен nginx-extras

    # Rate limiting
    limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
    limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;
    limit_conn_zone $binary_remote_addr zone=perip:10m;
}
</code></pre><h2>Virtual host для PHP-приложения</h2><pre spellcheck="" class="tmiCode language-nginx" data-language="Nginx"><code># /etc/nginx/sites-available/myapp.conf

# Upstream pool с health checks
upstream php_fpm {
    least_conn;  # балансировка по наименее загруженному
    server 127.0.0.1:9000 weight=5 max_fails=3 fail_timeout=30s;
    server 127.0.0.1:9001 weight=5 max_fails=3 fail_timeout=30s;
    keepalive 32;  # постоянные соединения к FPM
}

# Кэш для FastCGI ответов
fastcgi_cache_path /var/cache/nginx/fastcgi
    levels=1:2
    keys_zone=php_cache:100m
    max_size=2g
    inactive=60m
    use_temp_path=off;

server {
    listen 80;
    server_name myapp.example.com;
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name myapp.example.com;
    root /var/www/myapp/public;
    index index.php;

    # SSL
    ssl_certificate /etc/letsencrypt/live/myapp.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/myapp.example.com/privkey.pem;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 10m;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384;
    ssl_prefer_server_ciphers off;
    ssl_stapling on;
    ssl_stapling_verify on;

    # Безопасность
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header Referrer-Policy "no-referrer-when-downgrade" always;
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

    # Логи
    access_log /var/log/nginx/myapp-access.log combined buffer=512k flush=1m;
    error_log /var/log/nginx/myapp-error.log warn;

    # Ограничения
    limit_conn perip 20;

    # Статика с долгим кешированием
    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
        log_not_found off;
        access_log off;
    }

    # API с rate limiting
    location /api/ {
        limit_req zone=api burst=20 nodelay;
        try_files $uri $uri/ /index.php?$query_string;
    }

    location /api/auth {
        limit_req zone=login burst=5 nodelay;
        try_files $uri $uri/ /index.php?$query_string;
    }

    # PHP-FPM
    location ~ \.php$ {
        fastcgi_pass php_fpm;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        include fastcgi_params;

        # Кеширование (осторожно — только для некэшируемого поставьте X-Cache-Bypass)
        fastcgi_cache php_cache;
        fastcgi_cache_key "$scheme$request_method$host$request_uri";
        fastcgi_cache_valid 200 302 60m;
        fastcgi_cache_valid 404 1m;
        fastcgi_cache_bypass $http_pragma $http_authorization $cookie_PHPSESSID;
        fastcgi_no_cache $http_pragma $http_authorization;
        add_header X-Cache-Status $upstream_cache_status;

        # Буферизация
        fastcgi_buffer_size 128k;
        fastcgi_buffers 4 256k;
        fastcgi_busy_buffers_size 256k;

        # Таймаут для долгих запросов
        fastcgi_read_timeout 300;
    }

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    # Запрещаем служебные файлы
    location ~ /\.(ht|git|env) {
        deny all;
        return 404;
    }
}
</code></pre><hr><h1>Централизованное логирование: rsyslog, loki, ELK</h1><h2>rsyslog: маршрутизация логов</h2><pre spellcheck="" class="tmiCode language-plaintext" data-language="Простой текст"><code># /etc/rsyslog.conf — продвинутая конфигурация

# Шаблоны
template(name="FileFormat" type="string"
    string="%TIMESTAMP:::date-rfc3339% %HOSTNAME% %syslogtag%%msg:::sp-if-no-1st-sp%%msg:::drop-last-lf%\n"
)

# JSON формат для Logstash/Loki
template(name="JSONFormat" type="list") {
    constant(value="{")
    constant(value="\"timestamp\":\"")
    property(name="timereported" dateFormat="rfc3339")
    constant(value="\",\"host\":\"")
    property(name="hostname")
    constant(value="\",\"severity\":\"")
    property(name="syslogseverity-text")
    constant(value="\",\"facility\":\"")
    property(name="syslogfacility-text")
    constant(value="\",\"program\":\"")
    property(name="programname")
    constant(value="\",\"pid\":\"")
    property(name="procid")
    constant(value="\",\"message\":\"")
    property(name="msg" format="json")
    constant(value="\"}\n")
}

# Маршрутизация по приоритету
*.emerg  :omusrmsg:*                    # все терминалы при критической ошибке
auth,authpriv.*  /var/log/auth.log
mail.*   -/var/log/mail.log             # дефис = буферизованная запись
cron.*   /var/log/cron.log
*.warn   /var/log/warnings.log

# Отдельный файл для nginx
if $programname == 'nginx' then {
    action(type="omfile" file="/var/log/nginx/error.log" template="FileFormat")
    stop
}

# Пересылка на центральный сервер
*.* action(type="omfwd"
    target="log-server.internal"
    port="514"
    protocol="tcp"
    template="JSONFormat"
    action.resumeRetryCount="-1"
    queue.type="linkedList"
    queue.size="50000"
    queue.filename="rsyslog_queue"
    queue.saveonshutdown="on"
)
</code></pre><h2>Loki + Promtail: современный стек</h2><p>Loki — это "Prometheus для логов", хранит логи как метрики с метками:</p><pre spellcheck="" class="tmiCode language-yaml" data-language="YAML"><code># /etc/promtail/promtail-config.yaml
server:
  http_listen_port: 9080

positions:
  filename: /var/log/positions.yaml

clients:
  - url: http://loki:3100/loki/api/v1/push

scrape_configs:
  - job_name: nginx
    static_configs:
      - targets: [localhost]
        labels:
          job: nginx
          env: production
          __path__: /var/log/nginx/access.log
    
    pipeline_stages:
      - regex:
          expression: '^(?P&lt;remote_addr&gt;\S+) - (?P&lt;remote_user&gt;\S+) \[(?P&lt;time&gt;[^\]]+)\] "(?P&lt;method&gt;\S+) (?P&lt;path&gt;[^\s"]+)[^"]*" (?P&lt;status&gt;\d+) (?P&lt;body_bytes&gt;\d+)'
      
      - labels:
          method:
          status:
      
      - metrics:
          http_requests_total:
            type: Counter
            description: "Total HTTP requests"
            source: status
            config:
              action: inc

  - job_name: php-app
    static_configs:
      - targets: [localhost]
        labels:
          job: php-app
          __path__: /var/www/myapp/storage/logs/*.log
    
    pipeline_stages:
      - multiline:
          firstline: '^\[\d{4}-\d{2}-\d{2}'
          max_wait_time: 3s
      
      - regex:
          expression: '^\[(?P&lt;time&gt;[^\]]+)\] (?P&lt;env&gt;\w+)\.(?P&lt;level&gt;[A-Z]+): (?P&lt;message&gt;.+)'
      
      - labels:
          level:
          env:
</code></pre><h3>Запросы LogQL (язык Loki)</h3><pre spellcheck="" class="tmiCode language-plaintext" data-language="Простой текст"><code># Все ошибки nginx
{job="nginx"} |= "error"

# HTTP 500 ошибки за последний час
{job="nginx"} | regex `status=(?P&lt;status&gt;\d+)` | status="500"

# Медленные запросы (&gt;1 секунды)
{job="nginx"} | regex `request_time=(?P&lt;rt&gt;[0-9.]+)` | rt &gt; 1.0

# Топ URL по количеству запросов
topk(10, sum by (path) (rate({job="nginx"} | json [5m])))

# Уровень ошибок в приложении
sum(rate({job="php-app", level="ERROR"}[5m])) by (level)
</code></pre><hr><h1>Ansible: управление конфигурациями Linux-серверов</h1><h2>Структура Ansible-проекта</h2><pre spellcheck="" class="tmiCode language-plaintext" data-language="Простой текст"><code>ansible/
├── ansible.cfg
├── inventory/
│   ├── production/
│   │   ├── hosts.yml
│   │   └── group_vars/
│   │       ├── all.yml
│   │       ├── web.yml
│   │       └── db.yml
│   └── staging/
│       └── hosts.yml
├── roles/
│   ├── common/
│   ├── nginx/
│   ├── php/
│   └── mysql/
└── playbooks/
    ├── site.yml
    ├── deploy.yml
    └── update.yml
</code></pre><h3>ansible.cfg</h3><pre spellcheck="" class="tmiCode language-ini" data-language="ini"><code>[defaults]
inventory = inventory/production
remote_user = deploy
private_key_file = ~/.ssh/id_ed25519
host_key_checking = False
retry_files_enabled = False
stdout_callback = yaml
callback_whitelist = timer, profile_tasks
forks = 20

[ssh_connection]
pipelining = True
ssh_args = -o ControlMaster=auto -o ControlPersist=60s
</code></pre><h2>Роль для hardening</h2><pre spellcheck="" class="tmiCode language-yaml" data-language="YAML"><code># roles/common/tasks/main.yml
---
- name: Update and upgrade apt packages
  apt:
    upgrade: dist
    update_cache: yes
    cache_valid_time: 3600

- name: Install required packages
  apt:
    name:
      - ufw
      - fail2ban
      - unattended-upgrades
      - logrotate
      - htop
      - curl
      - git
    state: present

- name: Configure sysctl security settings
  sysctl:
    name: "{{ item.key }}"
    value: "{{ item.value }}"
    state: present
    reload: yes
  loop: "{{ sysctl_settings }}"

- name: Configure UFW
  ufw:
    state: enabled
    policy: deny
    direction: incoming

- name: Allow SSH
  ufw:
    rule: allow
    port: "{{ ssh_port }}"
    proto: tcp

- name: Configure fail2ban
  template:
    src: jail.local.j2
    dest: /etc/fail2ban/jail.local
    owner: root
    group: root
    mode: '0644'
  notify: restart fail2ban

- name: Configure SSH
  template:
    src: sshd_config.j2
    dest: /etc/ssh/sshd_config
    validate: 'sshd -t -f %s'
    owner: root
    group: root
    mode: '0600'
  notify: restart sshd
</code></pre><h2>Идемпотентность: делаем правильно</h2><pre spellcheck="" class="tmiCode language-yaml" data-language="YAML"><code># Создание пользователя (идемпотентно)
- name: Create deploy user
  user:
    name: deploy
    groups: www-data
    shell: /bin/bash
    create_home: yes
    state: present

# Копируем SSH ключ
- name: Set authorized keys
  authorized_key:
    user: deploy
    state: present
    key: "{{ lookup('file', 'files/deploy_key.pub') }}"
    exclusive: yes  # удалить другие ключи

# Изменение конфига только если нужно
- name: Configure PHP-FPM
  template:
    src: php-fpm-pool.conf.j2
    dest: /etc/php/8.2/fpm/pool.d/www.conf
    owner: root
    group: root
    mode: '0644'
  notify: reload php-fpm

# Handlers (выполняются только если что-то изменилось)
# roles/php/handlers/main.yml
- name: reload php-fpm
  service:
    name: php8.2-fpm
    state: reloaded

- name: restart php-fpm
  service:
    name: php8.2-fpm
    state: restarted
</code></pre><h2>Деплой приложения через Ansible</h2><pre spellcheck="" class="tmiCode language-yaml" data-language="YAML"><code># playbooks/deploy.yml
---
- name: Deploy MyApp
  hosts: web
  serial: "30%"  # Rolling update: 30% серверов одновременно
  vars:
    app_dir: /var/www/myapp
    git_repo: git@github.com:company/myapp.git
    git_branch: "{{ branch | default('main') }}"

  tasks:
    - name: Pull latest code
      git:
        repo: "{{ git_repo }}"
        dest: "{{ app_dir }}"
        version: "{{ git_branch }}"
        force: yes

    - name: Install Composer dependencies
      composer:
        command: install
        working_dir: "{{ app_dir }}"
        no_dev: yes
        optimize_autoloader: yes

    - name: Run migrations
      command: php spark migrate --all
      args:
        chdir: "{{ app_dir }}"
      run_once: true  # только на одном сервере

    - name: Clear application cache
      command: php spark cache:clear
      args:
        chdir: "{{ app_dir }}"

    - name: Reload PHP-FPM (graceful)
      service:
        name: php8.2-fpm
        state: reloaded

    - name: Warm up cache
      uri:
        url: "https://{{ inventory_hostname }}/health"
        status_code: 200
      retries: 5
      delay: 2
</code></pre><hr><h1>Диагностика Linux: алгоритм поиска проблем</h1><h2>Методология USE</h2><p><strong>USE Method</strong> (Brendan Gregg): для каждого ресурса проверяем:</p><ul><li><p><strong>U</strong>tilization — использование (в %)</p></li><li><p><strong>S</strong>aturation — насыщение (очереди, ожидание)</p></li><li><p><strong>E</strong>rrors — ошибки</p></li></ul><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># CPU Utilization
mpstat -P ALL 1 3

# CPU Saturation (очередь на выполнение)
vmstat 1 | awk '{print $1}'  # r - run queue

# Memory Utilization
free -h

# Memory Saturation (swapping)
vmstat 1 | awk '{print $7, $8}'  # si/so - swap in/out

# Disk Utilization
iostat -xz 1 | grep -E "Device|sd|nvme"

# Disk Saturation (await &gt; service time)
iostat -xz 1 | awk 'NR&gt;3 {print $1, $16}'  # %util

# Network Utilization
sar -n DEV 1 5
</code></pre><h2>60-секундный анализ сервера</h2><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># Быстрый обзор за 60 секунд (по Brendan Gregg)

uptime                    # load average
dmesg -T | tail -5        # ошибки ядра
vmstat -SM 1 3            # VM, CPU, I/O обзор
mpstat -P ALL 1 3         # CPU по ядрам
pidstat 1 3               # процессы
iostat -xz 1 3            # I/O дисков
free -m                   # память
sar -n DEV 1 3            # сеть
sar -n TCP,ETCP 1 3       # TCP метрики
top                       # интерактивно
</code></pre><h2>Диагностика "сервер завис"</h2><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># 1. Можем ли мы что-то делать?
# Если не отвечает по SSH - физический доступ или IPMI/iLO

# 2. Что не отвечает?
ping server-ip             # сеть живая?
nc -zv server-ip 22        # SSH порт открыт?
nc -zv server-ip 80        # HTTP открыт?

# 3. Загрузка
uptime
# load: 0.5 — норма
# load: = CPU cores — занят
# load: &gt; CPU cores * 2 — перегружен

# 4. Кто виноват?
top -bn1 | head -20
ps auxwf | head -30

# 5. Есть ли OOM?
dmesg | grep -i "oom\|killed process"
journalctl -k --since "1 hour ago" | grep -i oom

# 6. Диск переполнен?
df -h
du -sh /var /tmp /home    # кто занял место

# 7. Иноды кончились?
df -i

# 8. Что происходит с сетью?
ss -s                      # статистика сокетов
ss -tnp state time-wait | wc -l  # TIME_WAIT
netstat -i                 # ошибки на интерфейсах

# 9. Дисковые проблемы
dmesg | grep -i "error\|fail\|i/o"
smartctl -H /dev/sda       # здоровье диска

# 10. Полная картина за последний час
sar -A 1 10               # всё что собрал sar
</code></pre><h2>strace и ltrace: что делает процесс</h2><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># Что делает процесс прямо сейчас
strace -p $(pgrep nginx | head -1)

# Только конкретные системные вызовы
strace -e trace=open,read,write,network -p PID

# Статистика системных вызовов за 5 секунд
strace -c -p PID -e trace=all &amp;
sleep 5
kill %1

# ltrace — вызовы библиотечных функций
ltrace -p PID

# Запустить и трейсить
strace -e trace=network curl google.com 2&gt;&amp;1 | grep connect

# Дочерние процессы тоже
strace -f -p PID -o /tmp/strace.log
</code></pre><h2>Анализ производительности с perf</h2><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># Профиль за 10 секунд (нужен linux-tools-generic)
perf record -F 99 -g -p $(pgrep php-fpm | head -1) -- sleep 10
perf report --stdio | head -50

# Hotspot функции
perf top -K -p $(pgrep nginx | head -1)

# Счётчики производительности
perf stat -p PID -- sleep 5
# cache-misses, branch-misses, context-switches

# Flame graph (установить FlameGraph от Brendan Gregg)
perf record -F 99 -ag -- sleep 10
perf script | stackcollapse-perf.pl | flamegraph.pl &gt; flame.svg
</code></pre><h2>Диагностика сетевых задержек</h2><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># Измеряем задержки на разных уровнях
# 1. ICMP (сеть)
ping -c 100 server-ip | tail -3

# 2. TCP handshake (OS + сеть)
hping3 -S -c 100 -p 80 server-ip | tail -5

# 3. HTTP time_to_first_byte (приложение)
curl -o /dev/null -s -w "
dns:      %{time_namelookup}s
connect:  %{time_connect}s
tls:      %{time_appconnect}s
ttfb:     %{time_starttransfer}s
total:    %{time_total}s
" https://myapp.example.com

# 4. Детальная трассировка HTTP
curl -v --trace-time https://myapp.example.com 2&gt;&amp;1 | head -50

# 5. Tcpdump для анализа конкретного запроса
tcpdump -i eth0 -w request.pcap host client-ip and port 443
# Открываем в Wireshark для детального анализа

# 6. Статистика задержки на уровне сокета
ss -ti  # socket timing info
</code></pre><h2>Инструменты для экстренной диагностики: шпаргалка</h2><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># Процессы
ps auxwf               # дерево процессов
pstree -pu             # красивое дерево
pgrep -a nginx         # найти процессы
lsof -p PID            # файлы процесса
lsof -i :80            # кто слушает порт 80
fuser -n tcp 80        # pid процесса на порту

# Файловая система
lsof +D /var/log       # кто держит файлы в директории
inotifywait -m /etc/passwd  # слежка за изменениями
find / -newer /tmp/stamp -type f 2&gt;/dev/null  # что изменилось с timestamp

# Сеть
tcpdump -i any port 80 -nn -q
conntrack -L | wc -l   # количество трекируемых соединений
nmap -sV localhost     # сканируем себя

# История команд в случае инцидента
history | grep -i "rm\|mv\|chmod\|dd" | tail -20
last | head -20        # последние логины
lastb | head -10       # неудачные логины
who                    # кто сейчас залогинен
w                      # что они делают
</code></pre><p>Диагностика — это смесь знаний, методологии и опыта. Самые ценные навыки: не паниковать, следовать методологии USE, измерять прежде чем делать выводы, и помнить что 90% проблем с производительностью — это диск, память или сеть, а не код.</p>]]></description><guid isPermaLink="false">49</guid><pubDate>Sun, 22 Feb 2026 13:27:10 +0000</pubDate></item><item><title>Ceph &#x447;&#x430;&#x441;&#x442;&#x44C; #1 - Ceph &#x434;&#x43B;&#x44F; &#x432;&#x437;&#x440;&#x43E;&#x441;&#x43B;&#x44B;&#x445;: &#x447;&#x442;&#x43E; &#x44D;&#x442;&#x43E; &#x442;&#x430;&#x43A;&#x43E;&#x435;, &#x43A;&#x430;&#x43A; &#x440;&#x430;&#x431;&#x43E;&#x442;&#x430;&#x435;&#x442; &#x438; &#x437;&#x430;&#x447;&#x435;&#x43C; &#x432;&#x430;&#x43C; &#x44D;&#x442;&#x43E; &#x43D;&#x443;&#x436;&#x43D;&#x43E;</title><link>https://ithub.uno/statiarticles/9_infrastructure/ceph-%D1%87%D0%B0%D1%81%D1%82%D1%8C-1-ceph-%D0%B4%D0%BB%D1%8F-%D0%B2%D0%B7%D1%80%D0%BE%D1%81%D0%BB%D1%8B%D1%85-%D1%87%D1%82%D0%BE-%D1%8D%D1%82%D0%BE-%D1%82%D0%B0%D0%BA%D0%BE%D0%B5-%D0%BA%D0%B0%D0%BA-%D1%80%D0%B0%D0%B1%D0%BE%D1%82%D0%B0%D0%B5%D1%82-%D0%B8-%D0%B7%D0%B0%D1%87%D0%B5%D0%BC-%D0%B2%D0%B0%D0%BC-%D1%8D%D1%82%D0%BE-%D0%BD%D1%83%D0%B6%D0%BD%D0%BE-r52/</link><description><![CDATA[
<p><img src="https://ithub.uno/uploads/tmi_files_2026_02/Ceph-1.webp.28e886df17c6cd054b7b7e88e36d3c17.webp" /></p>
<h2>Почему Ceph, а не просто NAS или SAN?</h2><p>Представьте ситуацию: у вас 50 серверов, каждый с несколькими терабайтами данных, виртуальные машины, S3-хранилище для бэкапов, общий файловый ресурс для кластера Kubernetes — и всё это нужно хранить надёжно, быстро и так, чтобы смерть одного (или нескольких) серверов не привела к потере данных и даунтайму.</p><p>Традиционные решения здесь ломаются. NAS — единая точка отказа. SAN — дорого, сложно, проприетарно. RAID — не масштабируется за пределы одной машины. Ceph решает эту задачу радикально иначе: он распределяет данные по всем дискам всех серверов одновременно, и любой узел может умереть прямо сейчас, пока вы это читаете, — вы ничего не потеряете.</p><p>Ceph используют CERN (те самые, что ищут бозон Хиггса), крупнейшие облачные провайдеры, Proxmox, OpenStack — в общем, люди, которым нельзя терять данные. Давайте разберёмся, как это устроено.</p><hr><h2>Три уровня хранения в одном кластере</h2><p>Ceph — это не одна технология, это три совершенно разных интерфейса хранения, построенных поверх одного движка:</p><pre spellcheck="" class="tmiCode language-plaintext" data-language="Простой текст"><code>┌─────────────────────────────────────────────────┐
│              Приложения и клиенты               │
├──────────────┬──────────────┬───────────────────┤
│   RBD        │   CephFS     │   RGW (S3/Swift)  │
│  Блочное     │  Файловая    │  Объектное         │
│  хранилище   │  система     │  хранилище         │
├──────────────┴──────────────┴───────────────────┤
│                    RADOS                        │
│        (Reliable Autonomic Distributed          │
│              Object Store)                      │
├─────────────────────────────────────────────────┤
│         OSD OSD OSD OSD OSD OSD OSD             │
│         (физические диски/SSD/NVMe)             │
└─────────────────────────────────────────────────┘
</code></pre><p><strong>RBD (RADOS Block Device)</strong> — виртуальный блочный диск. С точки зрения виртуальной машины или Kubernetes pod — это просто диск. Внутри он разбит на объекты по 4 МБ (по умолчанию) и размазан по всему кластеру. Размер — до 16 эксабайт.</p><p><strong>CephFS</strong> — POSIX-совместимая распределённая файловая система. Монтируется как обычная папка, понимает права доступа, символические ссылки, всё как у людей. Метаданные хранит отдельно от данных через специальный демон MDS.</p><p><strong>RGW (RADOS Gateway)</strong> — HTTP-интерфейс объектного хранилища, совместимый с Amazon S3 и OpenStack Swift. Загружаете файлы через API, получаете бакеты, версионирование, lifecycle-политики — всё как в S3.</p><p>Самое красивое: всё три интерфейса используют один и тот же кластер RADOS. Вы можете одновременно монтировать CephFS на NFS-сервере, раздавать RBD-диски виртуалкам Proxmox и гонять бэкапы в RGW — и все они делят одни и те же физические диски.</p><hr><h2>Архитектура: четыре типа демонов</h2><p>Ceph-кластер — это набор демонов, каждый со своей ролью. Никаких монолитов, никакого единого «сервера хранилища».</p><h3>MON — Monitor (мозг кластера)</h3><pre spellcheck="" class="tmiCode language-plaintext" data-language="Простой текст"><code>MON1  MON2  MON3
  \     |     /
   \    |    /
    кластерная карта
    (cluster map)
</code></pre><p>Мониторы хранят <strong>карту кластера</strong> — полное описание топологии: какие OSD существуют, где они физически расположены, здоровы ли они. Это не данные, это метаданные. Мониторы работают по протоколу Paxos и требуют кворума: нужно нечётное число, минимум 3 в продакшне.</p><p>Без кворума мониторов — нет записи (но чтение может работать). Мониторы не хранят пользовательские данные вообще — они лёгкие, их можно держать даже на небольших VM.</p><h3>OSD — Object Storage Daemon (мышцы кластера)</h3><p>Один OSD = один физический диск (или раздел). OSD хранит данные, обслуживает запросы чтения/записи, участвует в репликации, сам находит соседей для репликации по карте кластера.</p><p>Типичный сервер в кластере: 12 дисков = 12 OSD-процессов + небольшой SSD для BlueStore WAL/DB.</p><p>OSD <strong>общаются напрямую</strong> — без центрального сервера хранения. Если клиент пишет данные в pool с репликацией 3x, primary OSD сам синхронно реплицирует на двух соседей и только потом отвечает клиенту «записано».</p><h3>MDS — Metadata Server</h3><p>Нужен только для CephFS. Хранит иерархию директорий и метаданные файлов (права, размеры, время). Данные файлов хранятся в обычных RADOS-объектах — MDS только помогает по пути <code>/my/dir/file.txt</code> найти нужные объекты.</p><p>Можно запустить несколько MDS для параллелизма — активный-активный режим (multi-MDS).</p><h3>MGR — Manager</h3><p>Менеджер собирает статистику, запускает модули (dashboard, prometheus-экспортер, балансировщик), обрабатывает оркестровку через cephadm. Нужно минимум 2 для отказоустойчивости (один active, один standby).</p><hr><h2>CRUSH: как Ceph решает, куда положить данные</h2><p>Вот где начинается самое интересное. В обычном RAID контроллер знает: «диск 1, 2, 3». В Ceph нет центрального индекса «где лежит файл» — это было бы узким местом в огромном кластере.</p><p>Вместо этого используется алгоритм <strong>CRUSH (Controlled Replication Under Scalable Hashing)</strong>. Зная только имя объекта и карту кластера, CRUSH <strong>детерминированно вычисляет</strong>, на каких OSD хранить данные — без запросов к какому-либо серверу метаданных.</p><pre spellcheck="" class="tmiCode language-plaintext" data-language="Простой текст"><code>object "my_file_chunk_0042" 
    │
    ▼
pg_id = hash(object_name) % pg_count
    │
    ▼
CRUSH(pg_id, crush_map) → [OSD.7, OSD.23, OSD.41]
</code></pre><p>Когда приходит запрос «где лежит объект X» — любой клиент, зная карту кластера, сам вычисляет ответ и идёт напрямую к нужному OSD. Без промежуточных серверов. Это и есть причина масштабируемости.</p><h3>Placement Groups (PG): промежуточный уровень</h3><p>Объектов в кластере могут быть миллиарды. Если бы каждый объект CRUSH маппил напрямую на OSD — карта кластера была бы гигантской. Поэтому объекты сначала группируются в <strong>Placement Groups</strong> (PG), а уже PG маппятся на OSD.</p><pre spellcheck="" class="tmiCode language-plaintext" data-language="Простой текст"><code>Объект → PG (группа объектов) → OSD
</code></pre><p>Число PG на pool — важный параметр настройки. Слишком мало — неравномерное распределение, узкое место. Слишком много — накладные расходы. Золотое правило: ~100 PG на OSD в pool.</p><h3>CRUSH Map: физическая топология</h3><p>CRUSH знает физику вашего датацентра:</p><pre spellcheck="" class="tmiCode language-plaintext" data-language="Простой текст"><code>datacenter DC1
├── rack Rack-A
│   ├── host server-01
│   │   ├── osd.0 (weight 1.0)
│   │   ├── osd.1 (weight 1.0)
│   │   └── osd.2 (weight 1.0)
│   └── host server-02
│       ├── osd.3
│       └── osd.4
└── rack Rack-B
    └── host server-03
        ├── osd.5
        └── osd.6
</code></pre><p>Правило репликации может звучать так: «три копии, каждая на отдельном rack'е». Тогда при смерти целого стойки ни одна PG не потеряет больше одной копии данных.</p><hr><h2>BlueStore: почему Ceph не использует ext4 или XFS</h2><p>До Ceph 12 OSD хранил данные на обычной файловой системе (FileStore). Это работало, но было медленно: каждая запись проходила через XFS/ext4 со всеми их накладными расходами, двойным кешированием, лишними syscall'ами.</p><p>С Ceph 12 появился <strong>BlueStore</strong> — кастомный бэкенд хранения, который работает напрямую с блочным устройством, минуя файловую систему. FileStore официально удалён начиная с Reef (18.x).</p><h3>Архитектура BlueStore</h3><pre spellcheck="" class="tmiCode language-plaintext" data-language="Простой текст"><code>OSD Process
├── BlueStore
│   ├── RocksDB (метаданные объектов, omap)
│   │   └── хранится на быстром SSD/NVMe (BlueFS)
│   ├── WAL (write-ahead log)
│   │   └── тоже лучше на SSD
│   └── данные объектов
│       └── на основном диске (HDD или SSD)
└── BlueFS (микрофайловая система для RocksDB)
</code></pre><p>В Tentacle (20.x) BlueStore получил улучшенное сжатие и новый, более быстрый WAL — это не маркетинг, а реальные измеримые улучшения для workload'ов с частой записью.</p><p><strong>Ключевые преимущества BlueStore:</strong></p><ul><li><p>Полный контроль над I/O без лишних слоёв</p></li><li><p>Атомарные транзакции без двойного буферирования</p></li><li><p>Встроенное сжатие (zlib, snappy, zstd, lz4)</p></li><li><p>Checksums для данных и метаданных (обнаружение битрот)</p></li><li><p>Эффективный omap для небольших значений ключ-значение</p></li></ul><hr><h2>Репликация vs. Erasure Coding: выбираем стратегию</h2><h3>Репликация (Replication)</h3><p>Простейший вариант: каждый объект хранится в N копиях на N разных OSD.</p><pre spellcheck="" class="tmiCode language-plaintext" data-language="Простой текст"><code>Запись "hello.txt":
    [OSD.5 — первичная копия]
        ├── реплицирует → [OSD.12 — копия 2]
        └── реплицирует → [OSD.31 — копия 3]
</code></pre><p><strong>Плюсы:</strong> простота, низкая latency, любой OSD может обслужить чтение. <strong>Минусы:</strong> 3x overhead по дисковому пространству.</p><p>Для продакшна стандарт — <code>size=3, min_size=2</code>. Это значит: нормальный режим — 3 копии, деградированный (когда один OSD умер) — 2 копии, меньше 2 — запись заблокирована.</p><h3>Erasure Coding (EC)</h3><p>EC — это как RAID 5/6, но распределённый. Данные разбиваются на K кусков, добавляются M паритетных кусков. Всего K+M кусков на K+M OSD. Для восстановления нужно любые K из K+M кусков.</p><pre spellcheck="" class="tmiCode language-plaintext" data-language="Простой текст"><code>Пример EC 4+2:
  chunk0 chunk1 chunk2 chunk3 | parity0 parity1
   OSD.1  OSD.2  OSD.3  OSD.4    OSD.5   OSD.6

При смерти OSD.2 и OSD.5 — данные восстанавливаются из оставшихся 4 из 6.
</code></pre><p><strong>Плюсы:</strong> экономия места. EC 4+2 даёт overhead 1.5x против 3x для репликации. <strong>Минусы:</strong> сложнее, выше latency, CPU overhead на кодирование/декодирование.</p><p>EC оптимально для холодного хранилища, S3-бэкапов, больших объектов. Для горячих IOPS-нагруженных данных (БД, VM) — репликация.</p><h3>FastEC в Tentacle: революция для Erasure Coding</h3><p>В Ceph Tentacle (20.2.0) появилась долгожданная функция <strong>FastEC</strong> — принципиально новая реализация I/O для EC пулов с поддержкой partial reads и partial writes.</p><p>До FastEC: запись небольшого объекта в EC-пул требовала читать все K кусков, обновлять данные, пересчитывать все паритеты и писать всё обратно. Это называется Read-Modify-Write (RMW) — катастрофа для производительности при мелких записях.</p><p>FastEC оптимизирует именно этот случай. По словам разработчиков и независимым тестам, на определённых workload'ах FastEC обгоняет даже репликацию 3x по производительности — при вдвое меньшем расходе места.</p><p>Важно: FastEC включается явно на уровне пула командой <code>allow_ec_optimizations</code>:</p><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code>ceph osd pool set mypool allow_ec_optimizations true
</code></pre><p>Существующие пулы можно мигрировать без пересоздания данных — достаточно обновить OSD и MON до Tentacle.</p><hr><h2>Что нового в Ceph Tentacle (20.2.0)</h2><p>Tentacle вышел 18 ноября 2025 года и является <strong>20-м стабильным релизом</strong> Ceph. Это значительный релиз, не косметический. Вот главное:</p><h3>FastEC — новый движок Erasure Coding</h3><p>Уже разобрали выше. Переключение плагина по умолчанию с устаревшего Jerasure на <strong>ISA-L</strong> (Intel ISA-L library) — более быстрый, активно поддерживаемый. Jerasure больше не обслуживается авторами.</p><h3>SMB-поддержка через Ceph</h3><p>Ceph теперь умеет создавать SMB-шары прямо из кластера через новый модуль mgr. Технически это Samba поверх CephFS с автоматическим управлением через cephadm. Поддерживает Active Directory и standalone. Работает в кластерном режиме через CTDB.</p><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code>ceph smb cluster create mysmb active-directory DC=corp,DC=example,DC=com \
  --domain-realm corp.example.com
</code></pre><h3>mgmt-gateway: единая точка входа для управления</h3><p>Новый сервис <code>mgmt-gateway</code> — nginx reverse proxy с TLS, который объединяет Dashboard, Prometheus, Grafana, Alertmanager под одним адресом. Никаких «зайди на порт 8443 для дашборда, 9090 для Prometheus, 3000 для Grafana».</p><p>Плюс интеграция с OAuth 2.0/OIDC для SSO. Настраивается через cephadm в пару команд.</p><h3>certmgr: автоматические TLS-сертификаты</h3><p>Подсистема управления сертификатами. Ceph теперь сам выступает корневым CA, выпускает сертификаты для своих сервисов, обновляет их автоматически, предупреждает об истечении. Никаких самоподписанных сертификатов вручную.</p><h3>Data Availability Score</h3><p>Новая команда для мониторинга доступности данных:</p><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code>ceph osd pool availability-status
</code></pre><p>Показывает «score» для каждого пула — сколько данных доступно прямо сейчас. Пул считается недоступным если любая PG не в состоянии <code>active</code> или есть <code>unfound</code> объекты.</p><h3>Crimson OSD + SeaStore (Tech Preview)</h3><p>Crimson — полностью переписанный OSD на основе Seastar (асинхронный, без блокирующих операций). В Tentacle к нему добавили развёртывание SeaStore — нового бэкенда хранения рядом с Crimson. Это всё ещё tech preview, в продакшне не используем — но прогресс виден.</p><h3>Удаление устаревших модулей</h3><p>Модули <code>mgr/restful</code> и <code>mgr/zabbix</code> официально удалены. Они были deprecated с 2020 года и имели уязвимости в зависимостях (CVE-2023-46136). Переходите на Dashboard API и Prometheus.</p><hr><h2>Когда Ceph — правильный выбор</h2><p>Ceph имеет смысл когда у вас:</p><ul><li><p>Минимум 3 физических сервера (иначе нет смысла в распределённости)</p></li><li><p>Объём данных от нескольких терабайт</p></li><li><p>Потребность в нескольких типах хранилища одновременно (block + object + file)</p></li><li><p>Нужна горизонтальная масштабируемость: добавил серверы → ёмкость и производительность выросли</p></li><li><p>Нужна отказоустойчивость без дорогого проприетарного железа</p></li></ul><p>Когда Ceph — <strong>не правильный выбор</strong>:</p><ul><li><p>Один сервер или только два — берите ZFS/BTRFS</p></li><li><p>Небольшой проект: overhead на управление не окупится</p></li><li><p>Нужна очень низкая latency (&lt; 1ms) для транзакционной БД — NVMe All-Flash Array или local SSD в приоритете</p></li></ul><hr><h2>Итог: ключевые концепции для запоминания</h2><div class="tmiRichText__table-wrapper"><table style="width: 717px;"><colgroup><col style="width:155px;"><col style="width:562px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>Концепция</p></th><th colspan="1" rowspan="1"><p>Коротко</p></th></tr><tr><td colspan="1" rowspan="1"><p>RADOS</p></td><td colspan="1" rowspan="1"><p>Нижний уровень — distributed object store</p></td></tr><tr><td colspan="1" rowspan="1"><p>CRUSH</p></td><td colspan="1" rowspan="1"><p>Алгоритм распределения данных без метасервера</p></td></tr><tr><td colspan="1" rowspan="1"><p>OSD</p></td><td colspan="1" rowspan="1"><p>1 демон = 1 диск</p></td></tr><tr><td colspan="1" rowspan="1"><p>PG</p></td><td colspan="1" rowspan="1"><p>Группа объектов, единица репликации</p></td></tr><tr><td colspan="1" rowspan="1"><p>MON</p></td><td colspan="1" rowspan="1"><p>Кворумный регистр карты кластера</p></td></tr><tr><td colspan="1" rowspan="1"><p>BlueStore</p></td><td colspan="1" rowspan="1"><p>Нативный бэкенд OSD без ФС</p></td></tr><tr><td colspan="1" rowspan="1"><p>RBD</p></td><td colspan="1" rowspan="1"><p>Блочный диск поверх RADOS</p></td></tr><tr><td colspan="1" rowspan="1"><p>CephFS</p></td><td colspan="1" rowspan="1"><p>POSIX-ФС поверх RADOS + MDS</p></td></tr><tr><td colspan="1" rowspan="1"><p>RGW</p></td><td colspan="1" rowspan="1"><p>S3/Swift API поверх RADOS</p></td></tr><tr><td colspan="1" rowspan="1"><p>FastEC</p></td><td colspan="1" rowspan="1"><p>Быстрый Erasure Coding в Tentacle</p></td></tr></tbody></table></div><p>В следующей статье мы разворачиваем реальный кластер с нуля через cephadm, настраиваем пулы и подключаем RBD к Proxmox.</p><p></p><p><a rel="" href="https://ithub.uno/statiarticles/9_infrastructure/ceph-%D1%87%D0%B0%D1%81%D1%82%D1%8C-2-%D1%80%D0%B0%D0%B7%D0%B2%D0%BE%D1%80%D0%B0%D1%87%D0%B8%D0%B2%D0%B0%D0%B5%D0%BC-ceph-tentacle-%D1%81-%D0%BD%D1%83%D0%BB%D1%8F-%D0%BE%D1%82-%D1%87%D0%B8%D1%81%D1%82%D1%8B%D1%85-%D1%81%D0%B5%D1%80%D0%B2%D0%B5%D1%80%D0%BE%D0%B2-%D0%B4%D0%BE-%D1%80%D0%B0%D0%B1%D0%BE%D1%87%D0%B5%D0%B3%D0%BE-%D0%BA%D0%BB%D0%B0%D1%81%D1%82%D0%B5%D1%80%D0%B0-r55/">Далее читай - Часть #2</a></p>]]></description><guid isPermaLink="false">52</guid><pubDate>Sun, 22 Feb 2026 13:37:00 +0000</pubDate></item><item><title>Ceph &#x447;&#x430;&#x441;&#x442;&#x44C; #2 - &#x420;&#x430;&#x437;&#x432;&#x43E;&#x440;&#x430;&#x447;&#x438;&#x432;&#x430;&#x435;&#x43C; Ceph Tentacle &#x441; &#x43D;&#x443;&#x43B;&#x44F;: &#x43E;&#x442; &#x447;&#x438;&#x441;&#x442;&#x44B;&#x445; &#x441;&#x435;&#x440;&#x432;&#x435;&#x440;&#x43E;&#x432; &#x434;&#x43E; &#x440;&#x430;&#x431;&#x43E;&#x447;&#x435;&#x433;&#x43E; &#x43A;&#x43B;&#x430;&#x441;&#x442;&#x435;&#x440;&#x430;</title><link>https://ithub.uno/statiarticles/9_infrastructure/ceph-%D1%87%D0%B0%D1%81%D1%82%D1%8C-2-%D1%80%D0%B0%D0%B7%D0%B2%D0%BE%D1%80%D0%B0%D1%87%D0%B8%D0%B2%D0%B0%D0%B5%D0%BC-ceph-tentacle-%D1%81-%D0%BD%D1%83%D0%BB%D1%8F-%D0%BE%D1%82-%D1%87%D0%B8%D1%81%D1%82%D1%8B%D1%85-%D1%81%D0%B5%D1%80%D0%B2%D0%B5%D1%80%D0%BE%D0%B2-%D0%B4%D0%BE-%D1%80%D0%B0%D0%B1%D0%BE%D1%87%D0%B5%D0%B3%D0%BE-%D0%BA%D0%BB%D0%B0%D1%81%D1%82%D0%B5%D1%80%D0%B0-r55/</link><description><![CDATA[
<p><img src="https://ithub.uno/uploads/tmi_files_2026_02/ceph-install-and-configure.webp.91490a2e4b4208ffb91f29063e975f83.webp" /></p>
<p>В прошлой статье мы разобрались с теорией — теперь руки в землю. Будем разворачивать минимальный продакшн-кластер Ceph Tentacle (20.2.x) через <strong>cephadm</strong> — официальный инструмент оркестровки, который умеет всё: установку, конфигурирование, обновление, добавление узлов.</p><hr><h2>Что мы будем строить</h2><p><strong>Минимальная продакшн-конфигурация:</strong></p><pre spellcheck="" class="tmiCode language-plaintext" data-language="Простой текст"><code>┌─────────────────────────────────────────────────────┐
│  ceph-node1  │  ceph-node2  │  ceph-node3           │
│              │              │                       │
│  MON + MGR   │  MON + MGR   │  MON                 │
│  OSD.0       │  OSD.3       │  OSD.6               │
│  OSD.1       │  OSD.4       │  OSD.7               │
│  OSD.2       │  OSD.5       │  OSD.8               │
│              │              │                       │
│  /dev/sdb    │  /dev/sdb    │  /dev/sdb            │
│  /dev/sdc    │  /dev/sdc    │  /dev/sdc            │
│  /dev/sdd    │  /dev/sdd    │  /dev/sdd            │
└─────────────────────────────────────────────────────┘
</code></pre><p><strong>Требования к каждому узлу:</strong></p><ul><li><p>OS: Ubuntu 22.04 LTS или Debian 12 (рекомендуется), RHEL 9 тоже ок</p></li><li><p>RAM: минимум 16 GB (рекомендуется 32+ GB для продакшна)</p></li><li><p>CPU: 4+ ядра</p></li><li><p>Сеть: минимум 1 GbE, лучше 10 GbE; отдельная сеть для репликации — хорошая идея</p></li><li><p>Диски: минимум 1 диск для OSD (не системный!), лучше SSD или NVMe</p></li></ul><p><strong>Важно:</strong> диски для OSD должны быть <strong>пустыми</strong> — без разделов, без файловых систем. BlueStore сам их форматирует.</p><hr><h2>Шаг 1: Подготовка всех узлов</h2><p>Выполняем на <strong>каждом</strong> из трёх узлов.</p><h3>Обновление системы и базовые пакеты</h3><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code>apt update &amp;&amp; apt upgrade -y
apt install -y \
    chrony \
    curl \
    python3 \
    python3-pip \
    lvm2 \
    podman \   # или docker
    ntp
</code></pre><p><strong>Почему chrony важен:</strong> Ceph очень чувствителен к рассинхронизации времени. Разница &gt; 5 секунд между узлами вызывает предупреждения и может дестабилизировать кластер. Убедитесь что NTP работает:</p><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code>timedatectl status
chronyc tracking
</code></pre><h3>Настройка hostname и /etc/hosts</h3><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># На ceph-node1:
hostnamectl set-hostname ceph-node1

# На всех трёх узлах добавляем в /etc/hosts:
cat &gt;&gt; /etc/hosts &lt;&lt; 'EOF'
192.168.10.11  ceph-node1
192.168.10.12  ceph-node2
192.168.10.13  ceph-node3
EOF
</code></pre><h3>SSH ключи: cephadm общается через SSH</h3><p>Генерируем ключ на <strong>первом узле</strong> (bootstrap узел) и распространяем:</p><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># На ceph-node1:
ssh-keygen -t ed25519 -N "" -f /root/.ssh/id_ed25519

# Копируем на все узлы (включая node1 самого себя):
for node in ceph-node1 ceph-node2 ceph-node3; do
    ssh-copy-id -i /root/.ssh/id_ed25519.pub root@$node
done

# Проверяем:
for node in ceph-node1 ceph-node2 ceph-node3; do
    echo "=== $node ==="
    ssh root@$node "hostname &amp;&amp; uname -r"
done
</code></pre><h3>Подготовка дисков: убеждаемся что они чистые</h3><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># Проверяем состояние дисков
lsblk
fdisk -l /dev/sdb
wipefs -a /dev/sdb  # если нужно очистить

# cephadm сам зачистит диски при добавлении — если они "чистые"
# (без LVM, без партиций, без файловой системы)
# Принудительно зачистить:
ceph-volume lvm zap /dev/sdb --destroy  # после установки ceph
</code></pre><hr><h2>Шаг 2: Bootstrap первого узла</h2><h3>Устанавливаем cephadm</h3><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># На ceph-node1:
curl --silent --remote-name --location \
    https://github.com/ceph/ceph/raw/reef/src/cephadm/cephadm

chmod +x cephadm

# Устанавливаем в систему
./cephadm install

# Добавляем репозиторий Tentacle
cephadm add-repo --release tentacle

# Устанавливаем ceph-common (для команды ceph)
cephadm install ceph-common
</code></pre><h3>Bootstrap кластера</h3><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code>cephadm bootstrap \
    --mon-ip 192.168.10.11 \
    --cluster-network 192.168.20.0/24 \
    --initial-dashboard-user admin \
    --initial-dashboard-password 'YourStrongPassword!123' \
    --allow-fqdn-hostname \
    --skip-monitoring-stack  # добавим мониторинг позже отдельно
</code></pre><p>Что делает эта команда за кулисами:</p><ol><li><p>Создаёт директории конфигурации <code>/etc/ceph/</code></p></li><li><p>Генерирует ключи аутентификации</p></li><li><p>Поднимает первый MON в контейнере</p></li><li><p>Поднимает MGR</p></li><li><p>Активирует модуль Dashboard</p></li><li><p>Пишет <code>/etc/ceph/ceph.conf</code> и <code>/etc/ceph/ceph.client.admin.keyring</code></p></li></ol><p>После успешного выполнения вы увидите URL дашборда:</p><pre spellcheck="" class="tmiCode language-plaintext" data-language="Простой текст"><code>Ceph Dashboard is now available at:
             URL: https://ceph-node1:8443/
            User: admin
        Password: YourStrongPassword!123
</code></pre><p><strong>Параметр </strong><code>--cluster-network</code><strong>:</strong> Это сеть для трафика репликации между OSD. Если у вас только одна сеть — уберите этот параметр. Но если есть выделенная сеть — обязательно используйте её, это критично для производительности публичной сети.</p><hr><h2>Шаг 3: Добавляем узлы в кластер</h2><h3>Проверяем первый узел</h3><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code>ceph status
# Должны увидеть: mon: 1 mons at quorum...
# health: HEALTH_WARN (это нормально на старте)

ceph orch status
# Оркестратор должен быть активен
</code></pre><h3>Добавляем ceph-node2 и ceph-node3</h3><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># На ceph-node1 — добавляем public SSH ключ cephadm в авторизованные на узлах
ceph cephadm get-pub-key &gt; /tmp/ceph.pub

ssh root@ceph-node2 "mkdir -p /root/.ssh &amp;&amp; \
    cat &gt;&gt; /root/.ssh/authorized_keys" &lt; /tmp/ceph.pub

ssh root@ceph-node3 "mkdir -p /root/.ssh &amp;&amp; \
    cat &gt;&gt; /root/.ssh/authorized_keys" &lt; /tmp/ceph.pub

# Добавляем хосты в кластер
ceph orch host add ceph-node2 192.168.10.12
ceph orch host add ceph-node3 192.168.10.13

# Проверяем
ceph orch host ls
</code></pre><hr><h2>Шаг 4: Добавляем MON и MGR</h2><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># По умолчанию cephadm хочет 5 MON — для нас 3 достаточно
ceph orch apply mon 3

# Проверяем что MON есть на всех трёх узлах
ceph orch ps --daemon-type mon

# Добавляем второй MGR (для failover)
ceph orch apply mgr 2
</code></pre><p>Ждём пока cephadm автоматически запустит MON на node2 и node3. Следим:</p><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code>watch ceph status
# Ждём: mon: 3 mons at quorum ceph-node1,ceph-node2,ceph-node3
</code></pre><hr><h2>Шаг 5: Добавляем OSD — сердце кластера</h2><h3>Инвентаризация доступных дисков</h3><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># Смотрим что cephadm видит на всех узлах
ceph orch device ls

# Вывод покажет диски и их статус:
# HOST        PATH      TYPE  SIZE  AVAILABLE  REFRESHED
# ceph-node1  /dev/sdb  hdd   2TiB  Yes        12s ago
# ceph-node1  /dev/sdc  hdd   2TiB  Yes        12s ago
# ...
</code></pre><p>Диск помечен как <code>AVAILABLE</code> если он полностью пустой. Если нет — смотрим причину в колонке <code>REJECT REASONS</code>.</p><h3>Автоматическое добавление всех доступных дисков</h3><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># Самый простой способ — использовать все доступные диски
ceph orch apply osd --all-available-devices

# Следим за прогрессом
watch ceph osd tree
</code></pre><h3>Ручное добавление конкретных дисков (рекомендуется для продакшна)</h3><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># Добавляем по одному — больше контроля
ceph orch daemon add osd ceph-node1:/dev/sdb
ceph orch daemon add osd ceph-node1:/dev/sdc
ceph orch daemon add osd ceph-node1:/dev/sdd

ceph orch daemon add osd ceph-node2:/dev/sdb
ceph orch daemon add osd ceph-node2:/dev/sdc
ceph orch daemon add osd ceph-node2:/dev/sdd

ceph orch daemon add osd ceph-node3:/dev/sdb
ceph orch daemon add osd ceph-node3:/dev/sdc
ceph orch daemon add osd ceph-node3:/dev/sdd
</code></pre><h3>OSD Service Spec для воспроизводимой конфигурации</h3><p>Для инфраструктуры-как-код создаём spec-файл:</p><pre spellcheck="" class="tmiCode language-yaml" data-language="YAML"><code># osd-spec.yaml
service_type: osd
service_id: default
placement:
  host_pattern: 'ceph-node*'
data_devices:
  paths:
    - /dev/sdb
    - /dev/sdc
    - /dev/sdd
# Если есть отдельные SSD для WAL/DB:
# db_devices:
#   paths:
#     - /dev/nvme0n1
# wal_devices:
#   paths:
#     - /dev/nvme1n1
</code></pre><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code>ceph orch apply -i osd-spec.yaml
</code></pre><hr><h2>Шаг 6: Проверяем здоровье кластера</h2><p>После добавления OSD кластер начнёт балансировку данных (backfill). Это нормально и займёт время. Следим:</p><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># Общий статус
ceph status

# Подробный статус OSD
ceph osd stat
ceph osd df  # использование дискового пространства

# Статус PG
ceph pg stat

# Потребление ресурсов
ceph df detail

# Дерево OSD с весами
ceph osd tree
</code></pre><p>Хорошее состояние:</p><pre spellcheck="" class="tmiCode language-plaintext" data-language="Простой текст"><code>cluster:
  id:     a7f64266-0894-4f1e-a635-d0aeaca0e993
  health: HEALTH_OK

services:
  mon: 3 daemons, quorum ceph-node1,ceph-node2,ceph-node3
  mgr: ceph-node1.xxx(active), ceph-node2.xxx(standby)
  osd: 9 osds: 9 up (since 5m), 9 in (since 5m)

data:
  pools: 1 pools, 1 pgs
  objects: 0 objects, 0 B
  usage:   450 MiB used, 54 TiB / 54 TiB avail
  pgs:     1 active+clean
</code></pre><hr><h2>Шаг 7: Создаём пулы хранения</h2><p>Пул — логический контейнер для данных. Каждый пул имеет свою политику репликации/EC, количество PG и другие параметры.</p><h3>Пул с репликацией 3x (для VM, баз данных)</h3><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># Создаём пул
ceph osd pool create vmpool 32 32  # 32 PG

# Настраиваем репликацию
ceph osd pool set vmpool size 3      # 3 копии
ceph osd pool set vmpool min_size 2  # минимум 2 для записи

# Тип пула - для RBD
ceph osd pool application enable vmpool rbd

# Инициализируем для RBD
rbd pool init vmpool

# Проверяем
ceph osd pool ls detail
</code></pre><h3>Сколько PG нужно?</h3><p>Формула: <code>PG = (OSDs * 100) / pool_size</code></p><p>Для нашего кластера (9 OSD, репликация 3):</p><ul><li><p><code>PG = (9 * 100) / 3 = 300</code> — но возьмём ближайшую степень 2 = 256</p></li></ul><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># Изменить количество PG (только увеличение)
ceph osd pool set vmpool pg_num 64
ceph osd pool set vmpool pgp_num 64
</code></pre><p>С Ceph Luminous появился PG autoscaler — он сам подбирает оптимальное число PG:</p><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># Включаем автоскалер для пула
ceph osd pool set vmpool pg_autoscale_mode on

# Глобально включить автоскалер
ceph mgr module enable pg_autoscaler
ceph config set global osd_pool_default_pg_autoscale_mode on
</code></pre><h3>Пул с Erasure Coding (для S3/бэкапов)</h3><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># Создаём EC-профиль
# k=4 data chunks, m=2 parity chunks = 6 OSD минимум
# overhead = 1.5x против 3x у репликации
ceph osd erasure-code-profile set myec \
    k=4 m=2 \
    plugin=isa \
    crush-failure-domain=host

# Просматриваем профиль
ceph osd erasure-code-profile get myec

# Создаём пул с EC
ceph osd pool create ecpool 32 32 erasure myec

# Включаем FastEC оптимизации (Tentacle 20.x+)
ceph osd pool set ecpool allow_ec_optimizations true

# Для работы RGW с EC нужен overlay pool
ceph osd pool create ecpool-index 16  # репликация для индексов
ceph osd pool application enable ecpool rgw
ceph osd pool application enable ecpool-index rgw
</code></pre><hr><h2>Шаг 8: Подключаем RBD — блочное хранилище</h2><h3>Создаём RBD-образ</h3><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># Создаём образ диска 100 GB
rbd create --size 102400 vmpool/myvm-disk01

# Смотрим информацию
rbd info vmpool/myvm-disk01

# Листинг образов в пуле
rbd ls vmpool

# Размер всех образов
rbd du vmpool
</code></pre><h3>Монтируем на Linux через kernel driver</h3><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># Маппируем образ как блочное устройство
rbd device map vmpool/myvm-disk01 --id admin \
    --keyring /etc/ceph/ceph.client.admin.keyring

# Видим устройство
rbd device list
# /dev/rbd0 → vmpool/myvm-disk01

# Форматируем и монтируем
mkfs.xfs /dev/rbd0
mkdir /mnt/rbd-data
mount /dev/rbd0 /mnt/rbd-data

# Авто-монтирование через /etc/fstab через rbdmap
# /etc/ceph/rbdmap:
# vmpool/myvm-disk01 id=admin,keyring=/etc/ceph/ceph.client.admin.keyring
systemctl enable rbdmap
</code></pre><h3>RBD для Proxmox VE</h3><p>Proxmox имеет встроенную поддержку Ceph. Добавляем через GUI или:</p><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># На Proxmox хосте устанавливаем ceph-клиент
apt install ceph-common

# Копируем конфиг и ключ с Ceph кластера
scp root@ceph-node1:/etc/ceph/ceph.conf /etc/pve/ceph.conf
scp root@ceph-node1:/etc/ceph/ceph.client.admin.keyring \
    /etc/pve/priv/ceph/

# Или создаём отдельного пользователя с ограниченными правами
ceph auth get-or-create client.proxmox \
    mon 'profile rbd' \
    osd 'profile rbd pool=vmpool' \
    mgr 'profile rbd pool=vmpool' \
    &gt; /tmp/ceph.client.proxmox.keyring

# Добавляем Ceph storage в Proxmox
pveceph pool create vmpool --pg_num 64 --pg_autoscale_mode on
</code></pre><hr><h2>Шаг 9: CephFS — распределённая файловая система</h2><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># Создаём CephFS (автоматически создаёт metadata и data пулы)
ceph fs volume create myfs --placement="3"

# Проверяем статус MDS
ceph mds stat
ceph fs status myfs

# Монтируем через FUSE (для тестов)
apt install ceph-fuse
mkdir /mnt/cephfs
ceph-fuse /mnt/cephfs -m ceph-node1:6789

# Или через kernel driver (лучше производительность)
# Получаем ключ
ceph auth get-key client.admin | base64
# Монтируем:
mount -t ceph ceph-node1:6789:/ /mnt/cephfs \
    -o name=admin,secret=&lt;base64-key&gt;

# В /etc/fstab:
# ceph-node1:6789,ceph-node2:6789:/ /mnt/cephfs ceph \
#   name=admin,secretfile=/etc/ceph/admin.secret,noatime 0 0
</code></pre><h3>Subvolumes для Kubernetes</h3><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># Создаём subvolume group
ceph fs subvolumegroup create myfs k8s

# Создаём subvolume (persistent volume)
ceph fs subvolume create myfs pvc-001 --group-name k8s --size 10G

# Получаем путь
ceph fs subvolume getpath myfs pvc-001 --group-name k8s
# /volumes/k8s/pvc-001/...
</code></pre><hr><h2>Шаг 10: RGW — S3-совместимое объектное хранилище</h2><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># Разворачиваем RGW через cephadm
ceph orch apply rgw myrgw --placement="2 ceph-node1 ceph-node2" \
    --port=8080

# Проверяем статус
ceph orch ps --daemon-type rgw

# Создаём пользователя
radosgw-admin user create \
    --uid=s3user \
    --display-name="S3 User" \
    --email=s3user@example.com

# Получаем ключи
radosgw-admin user info --uid=s3user
# access_key и secret_key для S3 клиентов

# Тестируем через s3cmd или mc (MinIO client)
apt install s3cmd
s3cmd --configure  # вводим access_key, secret_key, endpoint

# Или через AWS CLI
aws configure  # вводим ключи
aws --endpoint-url http://ceph-node1:8080 s3 mb s3://mybucket
aws --endpoint-url http://ceph-node1:8080 s3 ls
aws --endpoint-url http://ceph-node1:8080 s3 cp /tmp/test.txt s3://mybucket/
</code></pre><hr><h2>Шаг 11: Стек мониторинга</h2><p>Ceph Tentacle поставляет готовый стек мониторинга через cephadm. В Tentacle появился новый <code>mgmt-gateway</code> — единая точка входа:</p><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># Разворачиваем полный стек мониторинга
ceph orch apply prometheus
ceph orch apply grafana
ceph orch apply alertmanager
ceph orch apply node-exporter

# Новый в Tentacle: mgmt-gateway (nginx reverse proxy + TLS)
cat &gt; mgmt-gateway.yaml &lt;&lt; 'EOF'
service_type: mgmt-gateway
placement:
  count: 2  # HA — два инстанса
spec:
  port: 443
  enable_auth: true  # требовать аутентификацию
EOF

ceph orch apply -i mgmt-gateway.yaml

# Проверяем
ceph orch ps --daemon-type mgmt-gateway
</code></pre><p>Теперь Dashboard, Grafana, Prometheus — всё доступно через один HTTPS endpoint на порту 443.</p><p>Встроенные Grafana-дашборды Ceph показывают:</p><ul><li><p>OSD latency и throughput</p></li><li><p>Pool utilization</p></li><li><p>MON quorum status</p></li><li><p>PG состояния</p></li><li><p>Алерты</p></li></ul><hr><h2>Полезные команды для ежедневной работы</h2><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># === КЛАСТЕР ===
ceph status              # общий статус
ceph health detail       # детали о проблемах
ceph df                  # использование пространства
ceph versions            # версии всех демонов

# === OSD ===
ceph osd tree            # топология
ceph osd df              # место по OSD
ceph osd perf            # latency метрики
ceph osd dump            # полный дамп карты OSD

# Вывести из эксплуатации OSD (graceful)
ceph osd out osd.5
ceph osd drain osd.5     # ждём пока PG переедут

# === PG ===
ceph pg stat             # статус всех PG
ceph pg dump | grep -v active+clean  # проблемные PG
ceph pg repair 1.a3      # принудительный repair конкретной PG

# === Логи ===
ceph log last 20         # последние записи кластерного лога
journalctl -u ceph-osd@0 -f  # лог конкретного OSD

# === Оркестратор ===
ceph orch ls             # список сервисов
ceph orch ps             # список демонов с состоянием
ceph orch events         # события оркестратора
</code></pre><hr><h2>Типичные проблемы при развёртывании</h2><h3>HEALTH_WARN: too few PGs</h3><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code>ceph health detail
# HEALTH_WARN too few PGs per OSD

# Увеличиваем PG для затронутых пулов
ceph osd pool set vmpool pg_num 64
ceph osd pool set vmpool pgp_num 64
</code></pre><h3>OSD не добавляется: диск не определяется как доступный</h3><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># Смотрим причины отказа
ceph orch device ls --wide

# Часто причина: старые сигнатуры на диске
# Зачищаем через ceph-volume
cephadm shell -- ceph-volume lvm zap /dev/sdb --destroy

# Или более агрессивно
wipefs -a /dev/sdb
dd if=/dev/zero of=/dev/sdb bs=4M count=10
</code></pre><h3>clock skew detected</h3><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># Проверяем время на всех узлах
for node in ceph-node1 ceph-node2 ceph-node3; do
    echo "$node: $(ssh root@$node date)"
done

# На проблемном узле — синхронизируем немедленно
chronyc makestep
timedatectl set-ntp true
</code></pre><h3>Кластер застрял в rebalancing надолго</h3><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># Смотрим прогресс
ceph progress

# Ускоряем (только во время обслуживания, не в бою)
ceph tell osd.* injectargs --osd-max-backfills 8
ceph tell osd.* injectargs --osd-recovery-max-active 8

# После — возвращаем в норму
ceph tell osd.* injectargs --osd-max-backfills 3
</code></pre><hr><p>В следующей, финальной статье: производительность и тюнинг кластера, стратегии апгрейда с предыдущих версий, disaster recovery и продвинутые сценарии использования.</p><p></p><p><a rel="" href="https://ithub.uno/statiarticles/9_infrastructure/ceph-%D1%87%D0%B0%D1%81%D1%82%D1%8C-3-ceph-%D0%B2-%D0%BF%D1%80%D0%BE%D0%B4%D0%B0%D0%BA%D1%88%D0%BD%D0%B5-%D1%82%D1%8E%D0%BD%D0%B8%D0%BD%D0%B3-%D0%B0%D0%BF%D0%B3%D1%80%D0%B5%D0%B9%D0%B4-%D0%B4%D0%BE-tentacle-%D0%B8-%D0%B2%D0%BE%D1%81%D1%81%D1%82%D0%B0%D0%BD%D0%BE%D0%B2%D0%BB%D0%B5%D0%BD%D0%B8%D0%B5-%D0%BF%D0%BE%D1%81%D0%BB%D0%B5-%D0%BA%D0%B0%D1%82%D0%B0%D1%81%D1%82%D1%80%D0%BE%D1%84-r58/">Далее читай - Часть #3</a></p>]]></description><guid isPermaLink="false">55</guid><pubDate>Sun, 22 Feb 2026 13:40:00 +0000</pubDate></item><item><title>Ceph &#x447;&#x430;&#x441;&#x442;&#x44C; #3 - Ceph &#x432; &#x43F;&#x440;&#x43E;&#x434;&#x430;&#x43A;&#x448;&#x43D;&#x435;: &#x442;&#x44E;&#x43D;&#x438;&#x43D;&#x433;, &#x430;&#x43F;&#x433;&#x440;&#x435;&#x439;&#x434; &#x434;&#x43E; Tentacle &#x438; &#x432;&#x43E;&#x441;&#x441;&#x442;&#x430;&#x43D;&#x43E;&#x432;&#x43B;&#x435;&#x43D;&#x438;&#x435; &#x43F;&#x43E;&#x441;&#x43B;&#x435; &#x43A;&#x430;&#x442;&#x430;&#x441;&#x442;&#x440;&#x43E;&#x444;</title><link>https://ithub.uno/statiarticles/9_infrastructure/ceph-%D1%87%D0%B0%D1%81%D1%82%D1%8C-3-ceph-%D0%B2-%D0%BF%D1%80%D0%BE%D0%B4%D0%B0%D0%BA%D1%88%D0%BD%D0%B5-%D1%82%D1%8E%D0%BD%D0%B8%D0%BD%D0%B3-%D0%B0%D0%BF%D0%B3%D1%80%D0%B5%D0%B9%D0%B4-%D0%B4%D0%BE-tentacle-%D0%B8-%D0%B2%D0%BE%D1%81%D1%81%D1%82%D0%B0%D0%BD%D0%BE%D0%B2%D0%BB%D0%B5%D0%BD%D0%B8%D0%B5-%D0%BF%D0%BE%D1%81%D0%BB%D0%B5-%D0%BA%D0%B0%D1%82%D0%B0%D1%81%D1%82%D1%80%D0%BE%D1%84-r58/</link><description><![CDATA[
<p><img src="https://ithub.uno/uploads/tmi_files_2026_02/4kB_Random_Read_IOPS_Latency_20230414.webp.5b38fb4cc973d358d5181030eff9f387.webp" /></p>
<p>Кластер работает. Теперь начинается настоящая работа: выжать из него максимум производительности, не потерять данные при апгрейде и знать, что делать когда (не «если») что-то сломается.</p><hr><h2>Часть 1: Планирование железа — правильный старт</h2><p>Перед тем как тюнить — убедитесь что железо подобрано правильно. Никакой тюнинг не исправит плохую архитектуру.</p><h3>Сети: разделяйте публичную и кластерную</h3><p>Худшее что можно сделать — смешать пользовательский трафик и репликацию в одну сеть.</p><pre spellcheck="" class="tmiCode language-plaintext" data-language="Простой текст"><code>Публичная сеть (client network): клиенты → MON/OSD
Кластерная сеть (cluster network): OSD → OSD (репликация)

Рекомендация:
- Публичная: 10 GbE minimum
- Кластерная: 25 GbE или bond из двух 10 GbE
</code></pre><p>Конфигурируем при bootstrap:</p><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code>cephadm bootstrap \
    --mon-ip 192.168.10.11 \
    --cluster-network 192.168.20.0/24
# или после:
ceph config set global cluster_network 192.168.20.0/24
</code></pre><h3>Размещение BlueStore компонентов</h3><p>BlueStore — три уровня данных с разными требованиями:</p><div class="tmiRichText__table-wrapper"><table style="min-width: 80px;"><colgroup><col style="min-width:20px;"><col style="min-width:20px;"><col style="min-width:20px;"><col style="min-width:20px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>Компонент</p></th><th colspan="1" rowspan="1"><p>Что хранит</p></th><th colspan="1" rowspan="1"><p>Требования</p></th><th colspan="1" rowspan="1"><p>Рекомендация</p></th></tr><tr><td colspan="1" rowspan="1"><p>DATA</p></td><td colspan="1" rowspan="1"><p>Тела объектов</p></td><td colspan="1" rowspan="1"><p>Ёмкость</p></td><td colspan="1" rowspan="1"><p>HDD или SSD</p></td></tr><tr><td colspan="1" rowspan="1"><p>DB (RocksDB)</p></td><td colspan="1" rowspan="1"><p>Метаданные объектов</p></td><td colspan="1" rowspan="1"><p>IOPS, latency</p></td><td colspan="1" rowspan="1"><p>NVMe SSD</p></td></tr><tr><td colspan="1" rowspan="1"><p>WAL</p></td><td colspan="1" rowspan="1"><p>Write-Ahead Log</p></td><td colspan="1" rowspan="1"><p>Высокие IOPS</p></td><td colspan="1" rowspan="1"><p>NVMe SSD</p></td></tr></tbody></table></div><p>Если все компоненты на одном диске — они конкурируют за I/O. Выносим DB и WAL на NVMe:</p><pre spellcheck="" class="tmiCode language-yaml" data-language="YAML"><code># osd-spec-nvme.yaml
service_type: osd
service_id: nvme-optimized
placement:
  host_pattern: 'ceph-node*'
data_devices:
  paths:
    - /dev/sdb  # HDD для данных
    - /dev/sdc
db_devices:
  paths:
    - /dev/nvme0n1  # NVMe для DB (разделяется между несколькими OSD)
wal_devices:
  paths:
    - /dev/nvme1n1  # отдельный NVMe для WAL
</code></pre><p><strong>Золотое правило:</strong> один NVMe может обслуживать WAL/DB для 4-6 HDD OSD.</p><h3>Расчёт оптимального числа OSD на сервер</h3><p>Больше OSD — больше параллелизма, но больше RAM. Один OSD потребляет:</p><ul><li><p>~1 GB RAM (HDD OSD, небольшие данные)</p></li><li><p>~2-4 GB RAM (SSD/NVMe OSD под нагрузкой)</p></li><li><p></p></li></ul><ul><li><p>BlueStore cache: по умолчанию 1/4 RAM на все OSD</p></li></ul><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># Проверяем потребление памяти OSD
ceph daemon osd.0 dump_mempools
ceph daemon osd.0 perf dump | grep -i mem
</code></pre><hr><h2>Часть 2: Тюнинг производительности</h2><h3>BlueStore: кэш и компрессия</h3><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># Размер BlueStore кэша — главный параметр
# По умолчанию: 1/4 от общей RAM (авто)
# Можно задать явно для SSD/NVMe (они меньше нуждаются в кэше)
ceph config set osd bluestore_cache_size_ssd 1073741824  # 1 GB для SSD OSD

# HDD нуждаются в бОльшем кэше
ceph config set osd bluestore_cache_size_hdd 536870912   # 512 MB для HDD OSD

# Компрессия — включаем для cold data
ceph osd pool set mypool compression_mode aggressive  # сжимать всегда
# или
ceph osd pool set mypool compression_mode passive     # сжимать если выгодно

ceph osd pool set mypool compression_algorithm zstd   # лучший ratio
# или snappy — быстрее, но меньше сжимает
# или lz4   — самый быстрый, минимальное сжатие

ceph osd pool set mypool compression_min_blob_size 8192  # мин. размер для сжатия
</code></pre><h3>Настройка очереди I/O — mclock</h3><p>С Ceph Pacific появился планировщик mclock, дающий QoS на уровне OSD:</p><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># Проверяем текущий планировщик
ceph config get osd osd_op_queue
# должен быть: mclock_scheduler

# Приоритеты для workload'ов:
# client — пользовательские операции
# recovery — восстановление данных
# scrub — фоновая проверка

# Для HDD-кластера снижаем агрессивность recovery
ceph config set osd osd_mclock_scheduler_client_res 1
ceph config set osd osd_mclock_scheduler_recovery_res 1
ceph config set osd osd_mclock_scheduler_scrub_res 1
</code></pre><p>В Tentacle добавили защиту от нереалистичных значений IOPS capacity для mclock — теперь если измеренное значение IOPS слишком низкое (&lt; 50 для HDD, &lt; 1000 для SSD), планировщик использует последнее валидное значение.</p><h3>Оптимизация для конкретных workload'ов</h3><p><strong>Для виртуальных машин (много случайных мелких I/O):</strong></p><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># Увеличиваем число OSD threads
ceph config set osd osd_op_num_shards 8
ceph config set osd osd_op_num_threads_per_shard 2

# Write pipeline
ceph config set osd bluestore_throttle_bytes 67108864  # 64 MB
ceph config set osd bluestore_throttle_deferred_bytes 134217728  # 128 MB

# Для NVMe — отключаем оверхед на большие буферы
ceph config set osd bluestore_max_blob_size_ssd 65536
</code></pre><p><strong>Для больших последовательных записей (S3, медиа):</strong></p><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># Увеличиваем объект для EC
ceph config set osd osd_max_write_size 512  # MB

# RGW chunk size
ceph config set client rgw_obj_stripe_size 8388608  # 8 MB
</code></pre><p><strong>Для read-heavy workload'ов:</strong></p><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># Увеличиваем BlueStore cache для чтения
ceph config set osd bluestore_cache_meta_ratio 0.4   # 40% для метаданных
ceph config set osd bluestore_cache_kv_ratio 0.4     # 40% для RocksDB

# Readahead на уровне BlueStore
ceph config set osd bluestore_default_buffered_read true
</code></pre><h3>Настройка RBD для Kubernetes / Proxmox</h3><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># Включаем RBD кеширование на стороне клиента
cat &gt;&gt; /etc/ceph/ceph.conf &lt;&lt; 'EOF'
[client]
rbd cache = true
rbd cache size = 134217728      # 128 MB
rbd cache max dirty = 100663296 # 96 MB
rbd cache target dirty = 67108864  # 64 MB
rbd cache max dirty age = 5.0
rbd cache writethrough until flush = true
EOF

# Для diskless систем — через librbd
rbd config image set vmpool/myvm-disk01 rbd_cache true
rbd config image set vmpool/myvm-disk01 rbd_cache_size 134217728
</code></pre><hr><h2>Часть 3: FastEC — как правильно использовать</h2><p>FastEC — главная фича Tentacle для EC пулов. Разберём как мигрировать и что получаем.</p><h3>Создаём новый EC пул с FastEC</h3><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># Профиль с ISA-L (новый дефолт в Tentacle)
ceph osd erasure-code-profile set fastec-profile \
    k=4 m=2 \
    plugin=isa \
    technique=reed_sol_van \
    crush-failure-domain=host

# Создаём пул
ceph osd pool create fastec-pool 64 64 erasure fastec-profile

# Включаем FastEC оптимизации
ceph osd pool set fastec-pool allow_ec_optimizations true

# Проверяем что включилось
ceph osd pool get fastec-pool allow_ec_optimizations
</code></pre><h3>Миграция существующего EC пула</h3><p>Если у вас был EC пул с Jerasure — миграция возможна без пересоздания данных:</p><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># Обновляем OSD и MON до Tentacle (см. раздел Upgrade)

# После апгрейда — включаем оптимизации
ceph osd pool set oldpool allow_ec_optimizations true

# Следим за состоянием пула во время активации
watch ceph pg stat
</code></pre><h3>Бенчмарк: насколько быстрее FastEC?</h3><p>Сравниваем производительность:</p><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># Устанавливаем инструменты
apt install ceph-common

# Тест записи в EC пул с FastEC
rados bench -p fastec-pool 60 write --no-cleanup

# Тест чтения
rados bench -p fastec-pool 60 seq

# Сравниваем с репликацией
rados bench -p vmpool 60 write --no-cleanup
rados bench -p vmpool 60 seq

# Очищаем после теста
rados bench -p fastec-pool 60 cleanup
rados bench -p vmpool 60 cleanup
</code></pre><p>По данным разработчиков и независимым тестам (blog <a rel="external nofollow" href="https://nuvotex.de">nuvotex.de</a>, <a rel="external nofollow" href="https://42on.com">42on.com</a>): FastEC при workload'е с преобладанием чтения и объектами среднего размера (1-4 MB) может превысить производительность репликации 3x при вдвое меньшем расходе места.</p><hr><h2>Часть 4: Апгрейд с Squid (19.x) до Tentacle (20.x)</h2><h3>Подготовка к апгрейду</h3><p>Это самый важный раздел. Апгрейд Ceph — процедура, требующая внимания.</p><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># 1. Проверяем здоровье — ОБЯЗАТЕЛЬНО перед началом
ceph status
ceph health detail

# Кластер ДОЛЖЕН быть в HEALTH_OK или HEALTH_WARN (без critical)
# НЕ начинайте при: OSD down, degraded PGs, incomplete PGs

# 2. Проверяем версии клиентов
ceph features  # показывает connected clients и их версии

# 3. Делаем снэпшот всех RBD образов (опционально, но разумно)
for pool in $(ceph osd pool ls); do
    for image in $(rbd ls $pool 2&gt;/dev/null); do
        rbd snap create $pool/$image@pre-upgrade-$(date +%Y%m%d)
    done
done

# 4. Отключаем PG autoscaler на время апгрейда
ceph osd pool set noautoscale

# 5. Устанавливаем noout флаг (предотвращает rebalancing при рестарте OSD)
ceph osd set noout
</code></pre><h3>Апгрейд через cephadm (рекомендуется)</h3><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># Запускаем апгрейд — cephadm делает всё сам, rolling update
ceph orch upgrade start --image quay.io/ceph/ceph:v20.2.0

# Мониторим прогресс
ceph orch upgrade status

# Детальный лог
ceph -W cephadm

# В реальном времени
watch ceph versions
</code></pre><p>Cephadm обновляет в правильном порядке:</p><ol><li><p>MGR (сначала standby, потом active)</p></li><li><p>MON (по одному, ждёт quorum)</p></li><li><p>OSD (по одному, ждёт чистых PG после каждого)</p></li><li><p>MDS, RGW, другие сервисы</p></li></ol><p>Вы можете поставить на паузу и возобновить:</p><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code>ceph orch upgrade pause
ceph orch upgrade resume
</code></pre><h3>Апгрейд вручную (для не-cephadm кластеров)</h3><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># Порядок строго важен!
# 1. MON
for mon_host in ceph-node1 ceph-node2 ceph-node3; do
    echo "Upgrading MON on $mon_host"
    ssh root@$mon_host "apt update &amp;&amp; apt install -y ceph-mon"
    ssh root@$mon_host "systemctl restart ceph-mon.target"
    
    # Ждём возврата quorum
    sleep 30
    ceph mon stat
done

# Проверяем что все MON обновились
ceph mon dump | grep min_mon_release
# Должно показать: min_mon_release 20 (tentacle)

# 2. MGR
for mgr_host in ceph-node1 ceph-node2; do
    ssh root@$mgr_host "apt install -y ceph-mgr"
    ssh root@$mgr_host "systemctl restart ceph-mgr.target"
    sleep 10
done

# 3. OSD (по одному за раз!)
for osd_id in $(ceph osd ls); do
    osd_host=$(ceph osd find $osd_id | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['crush_location']['host'])")
    
    echo "Upgrading OSD.$osd_id on $osd_host"
    
    # Устанавливаем новый пакет
    ssh root@$osd_host "apt install -y ceph-osd"
    
    # Рестартуем OSD
    ssh root@$osd_host "systemctl restart ceph-osd@$osd_id"
    
    # Ждём пока OSD поднимется
    sleep 30
    
    # Проверяем что OSD up и PGs чистые
    while ceph pg stat | grep -q "degraded\|recovering"; do
        echo "Waiting for PGs to recover..."
        sleep 30
    done
    
    echo "OSD.$osd_id upgraded successfully"
done

# 4. После всех OSD — финализация
ceph osd require-osd-release tentacle
</code></pre><h3>Финализация апгрейда</h3><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># Снимаем noout
ceph osd unset noout

# Включаем PG autoscaler обратно
ceph osd pool unset noautoscale

# Проверяем что все демоны на новой версии
ceph versions

# Убеждаемся что все фичи Tentacle включены
ceph osd dump | grep require_osd_release

# Включаем новые возможности Tentacle
ceph osd pool set mypool allow_ec_optimizations true  # если EC пул
</code></pre><hr><h2>Часть 5: Disaster Recovery — что делать когда всё плохо</h2><h3>Сценарий 1: OSD упал</h3><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># Смотрим что произошло
ceph health detail
ceph osd tree | grep -i down

# Оценка: сколько времени OSD уже down?
ceph osd info osd.5 | grep "last_clean_epoch"

# Быстрый рестарт (если проблема временная)
systemctl restart ceph-osd@5
# или через cephadm:
ceph orch daemon restart osd.5

# Если OSD не стартует — смотрим логи
journalctl -u ceph-osd@5 -n 100 --no-pager

# OSD сломан физически — нужно заменить
# Помечаем как out (начнётся rebalancing)
ceph osd out osd.5

# Ждём завершения rebalancing
watch ceph pg stat  # ждём active+clean

# Удаляем из кластера
ceph osd purge osd.5 --yes-i-really-mean-it

# Меняем диск, зачищаем и добавляем обратно
ceph orch daemon add osd ceph-node2:/dev/sdc
</code></pre><h3>Сценарий 2: Целый хост упал</h3><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># Если хост не вернётся — убираем его OSD
# Для примера: умер ceph-node2 с OSD 3,4,5

# Помечаем все OSD хоста как out
ceph osd host-down-out ceph-node2  # если есть команда
# или вручную:
for osd in 3 4 5; do ceph osd out osd.$osd; done

# После rebalancing — удаляем
for osd in 3 4 5; do
    ceph osd purge osd.$osd --yes-i-really-mean-it
done

# Удаляем MON если он был на этом хосте
ceph mon remove ceph-node2

# Удаляем хост из оркестратора
ceph orch host drain ceph-node2
ceph orch host rm ceph-node2 --force

# Проверяем здоровье после
ceph status
</code></pre><h3>Сценарий 3: PG застряла в inconsistent/corrupt</h3><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># Находим проблемные PG
ceph pg dump | grep -v "active+clean"

# Запускаем repair
ceph pg repair 3.1a

# Если repair не помогает — более агрессивно
ceph osd set nodeep-scrub  # временно отключаем deep-scrub

# Смотрим детали PG
ceph pg 3.1a query

# OSD с повреждёнными данными
ceph osd tree
ceph pg 3.1a get  # какие OSD участвуют

# Принудительное восстановление из другой реплики
# (осторожно! только если уверены что данные на primary повреждены)
ceph pg force-recovery 3.1a
</code></pre><h3>Сценарий 4: MON потерял quorum</h3><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># Проверяем статус MON
ceph mon stat
ceph mon dump

# Если 1 из 3 MON не отвечает — quorum ещё есть (2 из 3)
# Рестартуем проблемный
systemctl restart ceph-mon@ceph-node2

# Если quorum потерян (0 из 3 доступны) — режим аварийного восстановления
# Это серьёзная ситуация

# На одном живом MON:
ceph-mon -i ceph-node1 --extract-monmap /tmp/monmap
monmaptool --print /tmp/monmap

# Удаляем недостижимые MON из карты
monmaptool --rm ceph-node2 /tmp/monmap
monmaptool --rm ceph-node3 /tmp/monmap

# Инжектируем исправленную monmap
ceph-mon -i ceph-node1 --inject-monmap /tmp/monmap

# Запускаем с одним MON
ceph-mon -i ceph-node1

# Добавляем новые MON после стабилизации
ceph orch apply mon ceph-node1,ceph-node2,ceph-node3
</code></pre><h3>Сценарий 5: Восстановление удалённого RBD образа</h3><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># Если образ удалён — проверяем trash
rbd trash ls vmpool

# Восстанавливаем из trash (образы там держатся delay_seconds)
rbd trash restore vmpool/trash-id

# Если включён rbd-mirror с журналированием — восстановление из журнала
# В крайнем случае — восстановление из снэпшота
rbd snap ls vmpool/myvm-disk01
rbd snap rollback vmpool/myvm-disk01@pre-upgrade-20241201

# Восстановление из бэкапа через export
rbd export vmpool/myvm-disk01 /mnt/backup/myvm-disk01.raw
# Восстановление:
rbd import /mnt/backup/myvm-disk01.raw vmpool/myvm-disk01-restored
</code></pre><hr><h2>Часть 6: Продвинутые возможности Tentacle</h2><h3>SMB shares из CephFS</h3><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># Создаём SMB кластер (Active Directory интеграция)
ceph smb cluster create mysmb \
    active-directory \
    --domain DC=corp,DC=example,DC=com \
    --realm CORP.EXAMPLE.COM \
    --dns-server 192.168.1.10

# Добавляем CephFS share
ceph smb share create mysmb myshare \
    --cephfs-volume myfs \
    --cephfs-path /shares/myshare

# Проверяем
ceph smb cluster ls
ceph smb share ls

# Через Dashboard — аналогично с GUI
</code></pre><h3>RBD Live Migration — новинка Tentacle</h3><p>Мгновенный импорт образов из других кластеров без копирования данных:</p><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># Импорт из другого Ceph кластера (native format)
rbd migration prepare \
    --source-spec '{"type":"native","cluster_name":"src-cluster","pool_name":"vmpool","image_name":"myvm"}' \
    dstpool/myvm-imported

# Импорт через NBD (из любого источника)
rbd migration prepare \
    --source-spec '{"type":"nbd","uri":"nbd://192.168.1.100:10809/disk"}' \
    dstpool/imported-disk

# Запускаем миграцию (фоновая копия данных)
rbd migration execute dstpool/myvm-imported

# Когда завершится — фиксируем
rbd migration commit dstpool/myvm-imported
</code></pre><p>Магия в том, что образ доступен для чтения и записи немедленно — пока данные копируются в фоне, читаются напрямую с источника.</p><h3>Data Availability Score — новый инструмент мониторинга</h3><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># Включаем tracking
ceph config set global enable_availability_tracking true

# Проверяем score для каждого пула
ceph osd pool availability-status

# Вывод:
# POOL    AVAILABLE  SCORE
# vmpool  yes        1.00
# ecpool  yes        0.99   ← одна PG в не-clean состоянии

# Очищаем статус для пула после устранения проблемы
ceph osd pool clear-availability-status vmpool
</code></pre><h3>Scrub: планирование глубоких проверок</h3><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># Принудительный scrub для конкретной PG
ceph pg scrub 1.a3
ceph pg deep-scrub 1.a3

# Scrub всего пула
ceph osd pool scrub vmpool

# Планирование — ограничиваем scrub нерабочим временем
ceph config set osd osd_scrub_begin_hour 1    # с 1:00
ceph config set osd osd_scrub_end_hour 6      # до 6:00
ceph config set osd osd_scrub_min_interval 86400   # не чаще раза в день
ceph config set osd osd_deep_scrub_interval 604800  # deep-scrub раз в неделю

# Статус scrub
ceph pg dump | awk '{print $1, $16, $17}' | head -30
# PG_ID | LAST_SCRUB | LAST_DEEP_SCRUB
</code></pre><hr><h2>Часть 7: Capacity planning и масштабирование</h2><h3>Добавление нового хоста и OSD</h3><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># Добавляем хост
ceph orch host add ceph-node4 192.168.10.14

# Добавляем OSD
ceph orch daemon add osd ceph-node4:/dev/sdb
ceph orch daemon add osd ceph-node4:/dev/sdc

# Автоматическая ребалансировка начнётся сразу
# Следим за прогрессом
ceph progress
watch ceph df
</code></pre><h3>Расчёт сырого хранилища</h3><pre spellcheck="" class="tmiCode language-plaintext" data-language="Простой текст"><code>Полезное место = (Общий объём) / overhead_factor

Репликация 3x: overhead = 3.0
EC 4+2:         overhead = 1.5
EC 6+3:         overhead = 1.5
EC 8+3:         overhead = 1.375

Для кластера с 9 × 4TB HDD и репликацией 3x:
- Сырое: 36 TB
- Полезное: 36 / 3 = 12 TB (минус ~10% overhead Ceph = ~10.8 TB)

Правило большого пальца: не заполняйте более 80% полезного места!
При 80%+ производительность падает из-за фрагментации и задержек recovery.
</code></pre><h3>Мониторинг через Prometheus</h3><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># ceph exporter уже встроен, prometheus конечные точки:
# http://ceph-node1:9283/metrics  - MGR prometheus module

# Ключевые метрики для алертов:
# ceph_health_status != 0          — нездоровый кластер
# ceph_osd_in == 0                 — OSD out
# ceph_pg_degraded &gt; 0             — деградированные PG
# ceph_osd_available_bytes &lt; 20%   — заканчивается место
# ceph_osd_apply_latency_ms &gt; 50   — высокая задержка записи

# Пример alertmanager rule:
cat &gt;&gt; /etc/prometheus/rules/ceph.yml &lt;&lt; 'EOF'
groups:
- name: ceph
  rules:
  - alert: CephHealthError
    expr: ceph_health_status == 2
    for: 1m
    labels:
      severity: critical
    annotations:
      summary: "Ceph cluster is in ERROR state"

  - alert: CephOSDDown  
    expr: ceph_osd_up == 0
    for: 2m
    labels:
      severity: warning

  - alert: CephDiskAlmostFull
    expr: (ceph_osd_stat_bytes_used / ceph_osd_stat_bytes) &gt; 0.80
    for: 5m
    labels:
      severity: warning
    annotations:
      summary: "Ceph OSD {{ $labels.ceph_daemon }} is {{ $value | humanizePercentage }} full"
EOF
</code></pre><hr><h2>Чеклист: Ceph в продакшне</h2><p><strong>До разворачивания:</strong></p><ul><li><p>[ ] Минимум 3 физических хоста (лучше 5+ для отказоустойчивости)</p></li><li><p>[ ] Отдельные сети для публичного и кластерного трафика (10 GbE+)</p></li><li><p>[ ] NTP синхронизирован на всех узлах</p></li><li><p>[ ] SSD/NVMe для BlueStore DB и WAL</p></li><li><p>[ ] Резервные диски наготове для горячей замены</p></li><li><p>[ ] CRUSH map настроен с учётом физической топологии (стойки, ЦОД)</p></li></ul><p><strong>Оперативный мониторинг:</strong></p><ul><li><p>[ ] Prometheus + Grafana с Ceph дашбордами</p></li><li><p>[ ] Алерты на HEALTH_ERR, OSD down, PG degraded, диск &gt;80%</p></li><li><p>[ ] mgmt-gateway настроен (Tentacle 20.x)</p></li><li><p>[ ] certmgr управляет TLS сертификатами</p></li></ul><p><strong>Регулярные процедуры:</strong></p><ul><li><p>[ ] Ежедневная проверка <code>ceph status</code></p></li><li><p>[ ] Еженедельный deep-scrub (автоматически через cron)</p></li><li><p>[ ] Тестирование восстановления из снэпшотов раз в квартал</p></li><li><p>[ ] Обновления безопасности: следим за <code>ceph-announce</code></p></li></ul><p><strong>Перед апгрейдом:</strong></p><ul><li><p>[ ] Кластер в HEALTH_OK</p></li><li><p>[ ] Снэпшоты критичных RBD образов</p></li><li><p>[ ] <code>ceph osd set noout</code></p></li><li><p>[ ] Тест в staging среде</p></li><li><p>[ ] Откат-план: как вернуться на предыдущую версию (downgrade невозможен, нужен rollback через снэпшоты)</p></li></ul><hr><h2>Где учиться дальше</h2><p><strong>Официальная документация:</strong></p><ul><li><p><a rel="external nofollow" href="https://docs.ceph.com">docs.ceph.com</a> — эталонная документация, всегда актуальная</p></li><li><p>ceph.io/en/news/blog — официальный блог с release notes и углублёнными техническими статьями</p></li></ul><p><strong>Сообщество:</strong></p><ul><li><p><a rel="" href="mailto:ceph-users@ceph.io">ceph-users@ceph.io</a> — рассылка для пользователей</p></li><li><p><a rel="external nofollow" href="https://irc.oftc.net">irc.oftc.net</a> #ceph — IRC канал</p></li><li><p>Cephalocon — ежегодная конференция сообщества</p></li></ul><p><strong>Практика:</strong></p><ul><li><p>Vagrant + VirtualBox: поднимите тестовый кластер на ноутбуке (cephadm работает в VM)</p></li><li><p>Rook — Ceph оператор для Kubernetes, хороший способ изучить интеграцию</p></li><li><p>Proxmox VE имеет встроенный Ceph — отличная песочница</p></li></ul><p>Ceph — это не инструмент «поставил и забыл». Это живая система, требующая понимания и регулярного внимания. Но когда вы научитесь с ней работать — получаете petabyte-scale хранилище корпоративного уровня на обычном commodity железе. Это стоит вложенных усилий.</p>]]></description><guid isPermaLink="false">58</guid><pubDate>Sun, 22 Feb 2026 13:43:08 +0000</pubDate></item><item><title>&#x41A;&#x43E;&#x440;&#x440;&#x435;&#x43A;&#x442;&#x43D;&#x44B;&#x439; &#x437;&#x430;&#x43F;&#x443;&#x441;&#x43A; &#x43F;&#x440;&#x43E;&#x446;&#x435;&#x441;&#x441;&#x43E;&#x432; &#x432; Docker-&#x43A;&#x43E;&#x43D;&#x442;&#x435;&#x439;&#x43D;&#x435;&#x440;&#x435;: ENTRYPOINT, CMD, PID 1, exec &#x438; tini</title><link>https://ithub.uno/statiarticles/9_infrastructure/%D0%BA%D0%BE%D1%80%D1%80%D0%B5%D0%BA%D1%82%D0%BD%D1%8B%D0%B9-%D0%B7%D0%B0%D0%BF%D1%83%D1%81%D0%BA-%D0%BF%D1%80%D0%BE%D1%86%D0%B5%D1%81%D1%81%D0%BE%D0%B2-%D0%B2-docker-%D0%BA%D0%BE%D0%BD%D1%82%D0%B5%D0%B9%D0%BD%D0%B5%D1%80%D0%B5-entrypoint-cmd-pid-1-exec-%D0%B8-tini-r64/</link><description><![CDATA[
<p><img src="https://ithub.uno/uploads/tmi_files_2026_02/Docker_1_1.webp.138a2a60ea69455cc51df95b621db2ea.webp" /></p>
<h1>Введение</h1><p>Корректный запуск процессов внутри контейнера — одна из ключевых тем при разработке Docker-образов. Формально всё описано в документации Docker, однако на практике регулярно возникают неоднозначные ситуации:</p><ul><li><p>контейнер не останавливается корректно;</p></li><li><p>сигналы не доходят до приложения;</p></li><li><p>появляются zombie-процессы;</p></li><li><p>PID 1 ведёт себя неожиданно.</p></li></ul><p>В этой статье разберём:</p><ol><li><p>Разницу между <code>ENTRYPOINT</code> и <code>CMD</code>.</p></li><li><p>Отличие exec и shell форм.</p></li><li><p>Почему критически важно, какой процесс имеет PID 1.</p></li><li><p>Как правильно писать <code>docker-entrypoint.sh</code>.</p></li><li><p>Когда и зачем использовать <code>tini</code>.</p></li></ol><p>Материал ориентирован на практическое применение и реальные сценарии.</p><hr><h1>1. ENTRYPOINT и CMD: фундаментальная разница</h1><p>В Dockerfile существуют две директивы для запуска процессов:</p><ul><li><p><code>ENTRYPOINT</code></p></li><li><p><code>CMD</code></p></li></ul><p>Обе участвуют в формировании итоговой команды запуска контейнера, но выполняют разные роли.</p><h2>Логическая модель</h2><p>Можно представить их так:</p><pre spellcheck="" class="tmiCode language-plaintext" data-language="Простой текст"><code>ENTRYPOINT + CMD = финальная команда контейнера</code></pre><h3>Рекомендуемая практика</h3><ul><li><p><code>ENTRYPOINT</code> — фиксированная команда (исполняемый файл или скрипт).</p></li><li><p><code>CMD</code> — аргументы по умолчанию, которые можно переопределить.</p></li></ul><hr><h2>Exec-форма и Shell-форма</h2><p>Docker поддерживает два синтаксиса.</p><h3><span class="tmiEmoji" title="">1️⃣</span> Exec-форма (рекомендуется)</h3><pre spellcheck="" class="tmiCode language-plaintext" data-language="Простой текст"><code>ENTRYPOINT ["/bin/ping"]
CMD ["it-lux.ru"]</code></pre><p>Особенности:</p><ul><li><p>Не используется shell.</p></li><li><p>Нет подстановки переменных.</p></li><li><p>Процесс запускается напрямую.</p></li><li><p>Корректная обработка сигналов.</p></li></ul><p>После сборки:</p><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code>docker run ping</code></pre><p>Внутри контейнера выполнится:</p><pre spellcheck="" class="tmiCode language-plaintext" data-language="Простой текст"><code>/bin/ping it-lux.ru</code></pre><p>Переопределение аргументов:</p><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code>docker run ping google.com</code></pre><p>Теперь выполнится:</p><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code>/bin/ping google.com</code></pre><p>Это правильная архитектура: один образ — разные параметры запуска.</p><hr><h3><span class="tmiEmoji" title="">2️⃣</span> Shell-форма (менее предпочтительна)</h3><pre spellcheck="" class="tmiCode language-plaintext" data-language="Простой текст"><code>ENTRYPOINT ping it-lux.ru</code></pre><p>Фактически Docker запустит:</p><pre spellcheck="" class="tmiCode language-plaintext" data-language="Простой текст"><code>/bin/sh -c "ping it-lux.ru"</code></pre><p>Минусы:</p><ul><li><p>Появляется промежуточный shell.</p></li><li><p>Сигналы могут не дойти до целевого процесса.</p></li><li><p>PID 1 становится shell.</p></li></ul><p>Shell-форма допустима, но требует понимания последствий.</p><hr><h1>2. Проблема PID 1</h1><p>В Linux процесс с PID 1 — особый.</p><p>Особенности:</p><ul><li><p>Он не имеет обработчиков сигналов по умолчанию.</p></li><li><p>Он ответственен за "усыновление" осиротевших процессов.</p></li><li><p>Он должен корректно обрабатывать SIGTERM.</p></li></ul><p>Docker при остановке контейнера выполняет:</p><pre spellcheck="" class="tmiCode language-plaintext" data-language="Простой текст"><code>docker stop → отправляет SIGTERM → PID 1</code></pre><p>Если PID 1:</p><ul><li><p>не обрабатывает сигнал,</p></li><li><p>не передаёт его дочерним процессам,</p></li></ul><p>то контейнер завершится некорректно (force kill через SIGKILL спустя timeout).</p><hr><h1>3. Ошибка с docker-entrypoint.sh</h1><p>Типичный пример:</p><pre spellcheck="" class="tmiCode language-plaintext" data-language="Простой текст"><code>FROM centos:7

COPY docker-entrypoint.sh /usr/bin
ENTRYPOINT ["/usr/bin/docker-entrypoint.sh"]</code></pre><p>Содержимое:</p><pre spellcheck="" class="tmiCode language-plaintext" data-language="Простой текст"><code>#!/bin/bash
ping ya.ru</code></pre><p>Что происходит?</p><p>PID 1 — это:</p><pre spellcheck="" class="tmiCode language-plaintext" data-language="Простой текст"><code>/bin/bash /usr/bin/docker-entrypoint.sh</code></pre><p>А <code>ping</code> — дочерний процесс.</p><p>При <code>docker stop</code>:</p><ul><li><p>SIGTERM получает bash</p></li><li><p>bash может не передать сигнал дальше</p></li><li><p>ping зависает</p></li><li><p>появляются zombie-процессы</p></li></ul><p>Это <strong>некорректная архитектура контейнера</strong>.</p><hr><h1>4. Правильное решение — exec</h1><p>В bash существует встроенная команда <code>exec</code>.</p><p>Она:</p><ul><li><p>заменяет текущий процесс</p></li><li><p>передаёт ему PID</p></li><li><p>не создаёт дополнительный уровень</p></li></ul><p>Правильный вариант:</p><pre spellcheck="" class="tmiCode language-plaintext" data-language="Простой текст"><code>#!/bin/bash
exec ping ya.ru</code></pre><p>Теперь:</p><pre spellcheck="" class="tmiCode language-plaintext" data-language="Простой текст"><code>PID 1 → ping</code></pre><p>Контейнер завершится корректно.</p><hr><h1>5. Использование CMD внутри entrypoint</h1><p>Более гибкий вариант:</p><pre spellcheck="" class="tmiCode language-plaintext" data-language="Простой текст"><code>ENTRYPOINT ["/usr/bin/docker-entrypoint.sh"]
CMD ["ya.ru"]</code></pre><p>Скрипт:</p><pre spellcheck="" class="tmiCode language-plaintext" data-language="Простой текст"><code>#!/bin/bash

# подготовительные действия

set -- ping "$@"
exec "$@"</code></pre><p>Разбор:</p><ul><li><p><code>$@</code> — все аргументы контейнера.</p></li><li><p><code>set --</code> — формирует новую команду.</p></li><li><p><code>exec "$@"</code> — запускает её как PID 1.</p></li></ul><p>Запуск:</p><pre spellcheck="" class="tmiCode language-plaintext" data-language="Простой текст"><code>docker run ping google.com</code></pre><p>Результат:</p><pre spellcheck="" class="tmiCode language-plaintext" data-language="Простой текст"><code>PID 1 → ping google.com</code></pre><p>Это production-подход.</p><hr><h1>6. Когда одного exec недостаточно</h1><p>Теперь усложним сценарий.</p><p>Допустим, запускается:</p><ul><li><p>Jenkins</p></li><li><p>Apache</p></li><li><p>Zabbix server</p></li></ul><p>Такие системы активно создают дочерние процессы.</p><p>Примеры:</p><ul><li><p>Jenkins</p></li><li><p>Zabbix</p></li><li><p>Apache HTTP Server</p></li></ul><p>Если дочерние процессы:</p><ul><li><p>завершаются некорректно,</p></li><li><p>остаются "осиротевшими",</p></li></ul><p>то PID 1 должен их "подчищать".</p><p>Но большинство приложений:</p><ul><li><p>не реализуют init-поведение,</p></li><li><p>не умеют корректно reaping zombie-процессов.</p></li></ul><hr><h1>7. Решение — tini</h1><p>Здесь используется <code>tini</code>.</p><ul><li><p>Минималистичный init для контейнеров.</p></li><li><p>Корректно проксирует сигналы.</p></li><li><p>Убирает zombie-процессы.</p></li><li><p>Работает как PID 1.</p></li></ul><p>Название — это "init" наоборот.</p><h2>Как подключить tini</h2><p>Пример Dockerfile:</p><pre spellcheck="" class="tmiCode language-plaintext" data-language="Простой текст"><code>FROM debian:stable

RUN apt-get update &amp;&amp; apt-get install -y tini

ENTRYPOINT ["/usr/bin/tini", "--"]
CMD ["your-app"]</code></pre><p>Теперь:</p><pre spellcheck="" class="tmiCode language-plaintext" data-language="Простой текст"><code>PID 1 → tini
PID 7 → your-app</code></pre><p>Что делает tini:</p><ol><li><p>Получает SIGTERM.</p></li><li><p>Передаёт сигнал дочернему процессу.</p></li><li><p>Reap'ит zombie-процессы.</p></li><li><p>Корректно завершает контейнер.</p></li></ol><p>Это production best practice.</p><hr><h1>8. Почему bash ≠ tini</h1><div class="tmiRichText__table-wrapper"><table style="min-width: 384px;"><colgroup><col style="width:364px;"><col style="min-width:20px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>Bash как PID 1</p></th><th colspan="1" rowspan="1"><p>tini как PID 1</p></th></tr><tr><td colspan="1" rowspan="1"><p>Не проксирует сигналы корректно</p></td><td colspan="1" rowspan="1"><p>Проксирует</p></td></tr><tr><td colspan="1" rowspan="1"><p>Не предназначен как init</p></td><td colspan="1" rowspan="1"><p>Предназначен</p></td></tr><tr><td colspan="1" rowspan="1"><p>Может терять SIGTERM</p></td><td colspan="1" rowspan="1"><p>Корректно передаёт</p></td></tr><tr><td colspan="1" rowspan="1"><p>Не чистит zombie</p></td><td colspan="1" rowspan="1"><p>Чистит</p></td></tr></tbody></table></div><p>Это принципиально разные роли.</p><hr><h1>9. Итоговые рекомендации (Best Practices)</h1><ol><li><p>Используйте exec-форму всегда, когда возможно.</p></li><li><p>В <code>docker-entrypoint.sh</code> обязательно применяйте <code>exec</code>.</p></li><li><p>Разделяйте:</p><ul><li><p>ENTRYPOINT — исполняемый файл</p></li><li><p>CMD — аргументы по умолчанию</p></li></ul></li><li><p>Если приложение создаёт дочерние процессы — используйте <code>tini</code>.</p></li><li><p>Проверяйте, кто имеет PID 1:</p><p></p></li></ol><pre spellcheck="" class="tmiCode language-plaintext" data-language="Простой текст"><code>docker exec -it container ps aux</code></pre><hr><h1>Заключение</h1><p>На простых примерах всё работает и без этих нюансов. Однако при усложнении логики контейнера:</p><ul><li><p>появляются проблемы с остановкой,</p></li><li><p>теряются сигналы,</p></li><li><p>возникают zombie-процессы,</p></li><li><p>контейнер завершает работу некорректно.</p></li></ul><p>Docker упрощает деплой, но не отменяет фундаментальные принципы работы процессов в Linux.</p><p>Понимание:</p><ul><li><p>роли PID 1,</p></li><li><p>различий exec и shell,</p></li><li><p>корректного построения entrypoint,</p></li><li><p>необходимости tini</p></li></ul><p>позволяет создавать production-ready Docker-образы, которые ведут себя предсказуемо и корректно в любой среде.</p>]]></description><guid isPermaLink="false">64</guid><pubDate>Fri, 27 Feb 2026 18:05:39 +0000</pubDate></item><item><title>Systemd: &#x41F;&#x43E;&#x43B;&#x43D;&#x43E;&#x435; &#x440;&#x443;&#x43A;&#x43E;&#x432;&#x43E;&#x434;&#x441;&#x442;&#x432;&#x43E; &#x43E;&#x442; &#x43D;&#x43E;&#x432;&#x438;&#x447;&#x43A;&#x430; &#x434;&#x43E; &#x44D;&#x43A;&#x441;&#x43F;&#x435;&#x440;&#x442;&#x430; &#x2014; &#x410;&#x440;&#x445;&#x438;&#x442;&#x435;&#x43A;&#x442;&#x443;&#x440;&#x430;, &#x42E;&#x43D;&#x438;&#x442;&#x44B;, cgroups, &#x41B;&#x43E;&#x433;&#x438;&#x440;&#x43E;&#x432;&#x430;&#x43D;&#x438;&#x435; &#x438; &#x420;&#x435;&#x430;&#x43B;&#x44C;&#x43D;&#x44B;&#x435; &#x41F;&#x440;&#x438;&#x43C;&#x435;&#x440;&#x44B;</title><link>https://ithub.uno/statiarticles/9_infrastructure/systemd-%D0%BF%D0%BE%D0%BB%D0%BD%D0%BE%D0%B5-%D1%80%D1%83%D0%BA%D0%BE%D0%B2%D0%BE%D0%B4%D1%81%D1%82%D0%B2%D0%BE-%D0%BE%D1%82-%D0%BD%D0%BE%D0%B2%D0%B8%D1%87%D0%BA%D0%B0-%D0%B4%D0%BE-%D1%8D%D0%BA%D1%81%D0%BF%D0%B5%D1%80%D1%82%D0%B0-%D0%B0%D1%80%D1%85%D0%B8%D1%82%D0%B5%D0%BA%D1%82%D1%83%D1%80%D0%B0-%D1%8E%D0%BD%D0%B8%D1%82%D1%8B-cgroups-%D0%BB%D0%BE%D0%B3%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5-%D0%B8-%D1%80%D0%B5%D0%B0%D0%BB%D1%8C%D0%BD%D1%8B%D0%B5-%D0%BF%D1%80%D0%B8%D0%BC%D0%B5%D1%80%D1%8B-r82/</link><description><![CDATA[
<p><img src="https://ithub.uno/uploads/tmi_files_2026_03/ce611e15bd9a44acbaa6c52d14110969.webp.3e7497b442b66792716e985320e5bcde.webp" /></p>
<h2>Введение: Почему SysVinit умер, и при чём тут Systemd</h2><p>Представьте себе повара, который готовит ужин на 10 человек, но строго по одному блюду за раз — сначала суп, потом только начинает нарезать салат. Именно так работала старая система инициализации <strong>SysVinit</strong>: запускала службы строго по одной, в заранее заданном порядке. Независимо от того, зависят ли они друг от друга.</p><p>С усложнением Linux-систем это стало болью:</p><ul><li><p><strong>Медленная загрузка.</strong> Служба A ждёт завершения службы B, даже если между ними нет никакой реальной зависимости.</p></li><li><p><strong>Нет контроля за процессами.</strong> Запустил — и забыл. Упал дочерний процесс? SysVinit об этом не знает.</p></li><li><p><strong>Хаос в логах.</strong> Каждый сервис пишет куда хочет: один в <code>/var/log/nginx/</code>, другой в syslog, третий в <code>/tmp/</code>. Никакой единой точки входа.</p></li><li><p><strong>Хрупкие скрипты.</strong> Shell-скрипты в <code>/etc/init.d/</code> — это огромное поле для ошибок и несовместимостей между дистрибутивами.</p></li></ul><p>В 2010 году Леннарт Поттеринг (Lennart Poettering) представил <strong>systemd</strong> — систему, которая решала все эти проблемы разом. Параллельный запуск, граф зависимостей, контрольные группы, централизованные логи. Сообщество поначалу встретило его в штыки (споры были жаркими), но сегодня systemd — стандарт де-факто в Fedora, Debian, Ubuntu, Arch, RHEL, CentOS и большинстве других дистрибутивов.</p><p>Давайте разберём его по косточкам.</p><hr><h2>Часть 1. Архитектура systemd — что происходит под капотом</h2><h3>1.1 PID 1 — главный процесс системы</h3><p>Когда ядро Linux загружается, оно запускает самый первый пользовательский процесс с <strong>PID 1</strong>. В системах с systemd это и есть демон <code>systemd</code>. Он — прямой родитель всех остальных процессов в системе.</p><p>Это важно по двум причинам:</p><ol><li><p>Если PID 1 упадёт — система паникует. Поэтому systemd написан максимально надёжно.</p></li><li><p>Все процессы, которые становятся «сиротами» (их родитель умер), автоматически переходят под крыло PID 1.</p></li></ol><pre spellcheck="" class="tmiCode language-plaintext" data-language="Простой текст"><code>Ядро Linux
    └── systemd (PID 1)
            ├── journald (логирование)
            ├── udevd (устройства)
            ├── networkd (сеть)
            ├── nginx.service (ваш веб-сервер)
            ├── postgresql.service (БД)
            └── ... все остальные сервисы</code></pre><h3>1.2 Ключевые компоненты системы</h3><p><code>systemd</code><strong> (PID 1)</strong> Главный дирижёр. Читает юнит-файлы, строит граф зависимостей, запускает процессы в нужном порядке, следит за их состоянием через cgroups.</p><p><code>systemctl</code> Ваш пульт управления. Когда вы пишете <code>systemctl start nginx</code>, эта утилита НЕ запускает nginx напрямую. Она отправляет сообщение по <strong>D-Bus</strong> демону systemd, который и выполняет работу. Это ключевое отличие от простого вызова скрипта.</p><p><code>journald</code> Централизованная система логирования. Перехватывает <code>stdout</code> и <code>stderr</code> всех сервисов, обогащает каждую запись метаданными (PID, UID, имя юнита, хостнейм) и сохраняет в структурированном бинарном формате. Это позволяет делать сложные запросы к логам — как SQL к базе данных.</p><p><code>udevd</code> Менеджер устройств. Когда вы подключаете USB-флешку, именно udevd создаёт <code>/dev/sdb</code>, загружает нужные модули ядра и может запустить определённый сервис.</p><p><code>networkd</code><strong>, </strong><code>timedated</code><strong>, </strong><code>logind</code> Специализированные демоны для управления сетью, системным временем и пользовательскими сессиями. Они общаются с PID 1 через D-Bus.</p><h3>1.3 D-Bus — как компоненты разговаривают друг с другом</h3><p>D-Bus — это системная шина сообщений, аналог внутренней корпоративной почты между процессами. Вместо того чтобы процессы вызывали функции друг друга напрямую (что небезопасно), они отправляют структурированные сообщения через шину.</p><p>Практический пример: <code>systemctl start nginx</code></p><ol><li><p><code>systemctl</code> формирует D-Bus-сообщение: «Вызови метод <code>StartUnit</code> с аргументом <code>nginx.service</code>»</p></li><li><p>Сообщение уходит в системную шину</p></li><li><p>Демон <code>systemd</code> получает его и выполняет</p></li><li><p>Возвращает ответ через ту же шину</p></li></ol><p>Это даёт безопасность (права проверяются на уровне D-Bus), гибкость (любая программа может управлять сервисами) и расширяемость.</p><hr><h2>Часть 2. Юниты — строительные блоки systemd</h2><p>Юнит (unit) — это описание любого системного ресурса в виде декларативного конфигурационного файла. Думайте о нём как о «паспорте» для сервиса, сокета, таймера или точки монтирования.</p><h3>2.1 Где хранятся юниты</h3><div class="tmiRichText__table-wrapper"><table style="min-width: 1126px;"><colgroup><col style="min-width:20px;"><col style="width:519px;"><col style="width:587px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>Путь</p></th><th colspan="1" rowspan="1"><p>Назначение</p></th><th colspan="1" rowspan="1"><p>Приоритет</p></th></tr><tr><td colspan="1" rowspan="1"><p><code>/usr/lib/systemd/system/</code></p></td><td colspan="1" rowspan="1"><p>Юниты, установленные пакетным менеджером</p></td><td colspan="1" rowspan="1"><p>Низкий</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>/etc/systemd/system/</code></p></td><td colspan="1" rowspan="1"><p>Ваши кастомные юниты и переопределения</p></td><td colspan="1" rowspan="1"><p>Высокий</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>/run/systemd/system/</code></p></td><td colspan="1" rowspan="1"><p>Временные юниты (исчезают после перезагрузки)</p></td><td colspan="1" rowspan="1"><p>Высший</p></td></tr></tbody></table></div><blockquote class="tmiQuote" cite="" data-tmiquote=""><div class="tmiQuote_contents" data-tmitruncate=""><p><strong>Важно:</strong> Никогда не редактируйте файлы в <code>/usr/lib/systemd/system/</code> напрямую — они перезапишутся при обновлении пакетов. Для изменения стандартного юнита используйте <code>systemctl edit &lt;имя&gt;</code>, который создаст drop-in файл в <code>/etc/systemd/system/&lt;имя&gt;.d/override.conf</code>.</p></div></blockquote><h3>2.2 Типы юнитов</h3><h4><code>.service</code> — сервисы (самый частый тип)</h4><p>Описывает демон или процесс. Именно с ним вы работаете в 90% случаев.</p><p>ini</p><pre spellcheck="" class="tmiCode language-ini" data-language="ini"><code># /etc/systemd/system/myapp.service
[Unit]
Description=My Awesome Application
After=network.target postgresql.service
Requires=postgresql.service

[Service]
Type=simple
ExecStart=/usr/bin/myapp --config /etc/myapp/config.yml
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
RestartSec=5s
User=myapp
Group=myapp

[Install]
WantedBy=multi-user.target</code></pre><p><strong>Параметр </strong><code>Type=</code><strong> — это важно понимать правильно:</strong></p><div class="tmiRichText__table-wrapper"><table style="min-width: 1099px;"><colgroup><col style="min-width:20px;"><col style="width:378px;"><col style="width:701px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>Тип</p></th><th colspan="1" rowspan="1"><p>Поведение</p></th><th colspan="1" rowspan="1"><p>Когда использовать</p></th></tr><tr><td colspan="1" rowspan="1"><p><code>simple</code></p></td><td colspan="1" rowspan="1"><p>systemd считает сервис запущенным сразу после старта ExecStart</p></td><td colspan="1" rowspan="1"><p>Большинство современных программ</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>forking</code></p></td><td colspan="1" rowspan="1"><p>Программа делает fork() и завершает родительский процесс. systemd ждёт этого.</p></td><td colspan="1" rowspan="1"><p>Старые Unix-демоны (nginx, apache)</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>notify</code></p></td><td colspan="1" rowspan="1"><p>Программа сама сигнализирует systemd через <code>sd_notify()</code>, что готова</p></td><td colspan="1" rowspan="1"><p>Программы с поддержкой systemd API</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>oneshot</code></p></td><td colspan="1" rowspan="1"><p>Программа выполняется и завершается. systemd ждёт завершения.</p></td><td colspan="1" rowspan="1"><p>Скрипты, одноразовые задачи</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>dbus</code></p></td><td colspan="1" rowspan="1"><p>Сервис считается запущенным, когда занял имя на D-Bus</p></td><td colspan="1" rowspan="1"><p>Демоны, использующие D-Bus</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>idle</code></p></td><td colspan="1" rowspan="1"><p>Запуск откладывается до завершения всех остальных задач</p></td><td colspan="1" rowspan="1"><p>Фоновые задачи с низким приоритетом</p></td></tr></tbody></table></div><p><strong>Жизненный цикл и перезапуск:</strong></p><p>ini</p><pre spellcheck="" class="tmiCode language-ini" data-language="ini"><code>[Service]
# Варианты для Restart=:
# no          — не перезапускать никогда
# on-success  — только при коде выхода 0
# on-failure  — при любом ненулевом коде, сигнале или таймауте (самый частый выбор)
# on-abnormal — при сигнале или таймауте (не при коде выхода)
# always      — перезапускать всегда (даже при systemctl stop!)
Restart=on-failure
RestartSec=5s

# Ограничение на количество перезапусков:
# Максимум 5 попыток за 30 секунд, потом сдаться
StartLimitIntervalSec=30s
StartLimitBurst=5</code></pre><h4><code>.socket</code> — socket-based activation (ленивый запуск)</h4><p>Это одна из самых мощных и недооценённых фич systemd. Идея простая: зачем держать 20 сервисов запущенными, если большинство из них обращаются раз в час?</p><p>Socket-based activation работает так:</p><ol><li><p>systemd открывает и слушает сокет (порт, Unix socket, FIFO)</p></li><li><p>Сам сервис <strong>не запущен</strong></p></li><li><p>Приходит первое подключение</p></li><li><p>systemd запускает сервис и передаёт ему уже установленное соединение</p></li><li><p>Клиент не замечает разницы — соединение не потеряно!</p></li></ol><p>ini</p><pre spellcheck="" class="tmiCode language-ini" data-language="ini"><code># /etc/systemd/system/echo.socket
[Unit]
Description=Echo Server Socket

[Socket]
ListenStream=12345
Accept=no

[Install]
WantedBy=sockets.target</code></pre><p>ini</p><pre spellcheck="" class="tmiCode language-ini" data-language="ini"><code># /etc/systemd/system/echo.service
[Unit]
Description=Echo Server Service

[Service]
Type=simple
ExecStart=/usr/local/bin/echo-server
# Сервис получит сокет через файловый дескриптор 3
StandardInput=socket</code></pre><p>Активация: <code>sudo systemctl enable --now echo.socket</code> — и сервис будет запускаться автоматически при первом подключении.</p><h4><code>.timer</code> — замена cron с суперспособностями</h4><p>Таймеры systemd мощнее cron по нескольким причинам:</p><ul><li><p>Поддерживают зависимости (запустить только если работает такой-то сервис)</p></li><li><p>Логируются в journald как обычные юниты</p></li><li><p>Могут «догнать» пропущенные запуски после перезагрузки (<code>Persistent=true</code>)</p></li><li><p>Точность до секунды и поддержка случайных задержек для распределения нагрузки</p></li></ul><p>ini</p><pre spellcheck="" class="tmiCode language-ini" data-language="ini"><code># /etc/systemd/system/backup.timer
[Unit]
Description=Daily Backup Timer

[Timer]
# Запускать каждый день в 02:30
OnCalendar=*-*-* 02:30:00
# Случайная задержка до 10 минут (не всё одновременно в 02:30!)
RandomizedDelaySec=10m
# Запустить задачу, если она была пропущена (например, система была выключена)
Persistent=true

[Install]
WantedBy=timers.target</code></pre><p>ini</p><pre spellcheck="" class="tmiCode language-ini" data-language="ini"><code># /etc/systemd/system/backup.service
[Unit]
Description=Daily Backup Job

[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
User=backup</code></pre><p>Активация: <code>sudo systemctl enable --now backup.timer</code></p><p>Проверить все активные таймеры: <code>systemctl list-timers --all</code></p><p><strong>Синтаксис OnCalendar:</strong></p><div class="tmiRichText__table-wrapper"><table style="width: 702px;"><colgroup><col style="width:258px;"><col style="width:444px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>Выражение</p></th><th colspan="1" rowspan="1"><p>Значение</p></th></tr><tr><td colspan="1" rowspan="1"><p><code>daily</code></p></td><td colspan="1" rowspan="1"><p>Каждый день в 00:00</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>weekly</code></p></td><td colspan="1" rowspan="1"><p>Каждый понедельник в 00:00</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>monthly</code></p></td><td colspan="1" rowspan="1"><p>1-го числа каждого месяца</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>*-*-* 09:00:00</code></p></td><td colspan="1" rowspan="1"><p>Каждый день в 09:00</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>Mon-Fri *-*-* 08:30:00</code></p></td><td colspan="1" rowspan="1"><p>По будням в 08:30</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>*-*-1,15 00:00:00</code></p></td><td colspan="1" rowspan="1"><p>1-го и 15-го каждого месяца</p></td></tr></tbody></table></div><p>Проверить выражение: <code>systemd-analyze calendar "Mon-Fri *-*-* 08:30:00"</code></p><h4><code>.target</code> — группы юнитов (замена runlevel)</h4><p>Target — это не сервис, а точка синхронизации. Думайте об этом как о «состоянии системы», которого нужно достичь.</p><div class="tmiRichText__table-wrapper"><table style="min-width: 940px;"><colgroup><col style="width:239px;"><col style="min-width:20px;"><col style="width:681px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>Target</p></th><th colspan="1" rowspan="1"><p>Аналог runlevel</p></th><th colspan="1" rowspan="1"><p>Значение</p></th></tr><tr><td colspan="1" rowspan="1"><p><code>poweroff.target</code></p></td><td colspan="1" rowspan="1"><p>0</p></td><td colspan="1" rowspan="1"><p>Выключение</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>rescue.target</code></p></td><td colspan="1" rowspan="1"><p>1</p></td><td colspan="1" rowspan="1"><p>Однопользовательский режим</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>multi-user.target</code></p></td><td colspan="1" rowspan="1"><p>3</p></td><td colspan="1" rowspan="1"><p>Многопользовательский без GUI</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>graphical.target</code></p></td><td colspan="1" rowspan="1"><p>5</p></td><td colspan="1" rowspan="1"><p>С графическим интерфейсом</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>reboot.target</code></p></td><td colspan="1" rowspan="1"><p>6</p></td><td colspan="1" rowspan="1"><p>Перезагрузка</p></td></tr></tbody></table></div><p>bash</p><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># Узнать текущий target (аналог текущего runlevel)
systemctl get-default

# Сменить target (аналог init 3)
sudo systemctl isolate multi-user.target

# Установить target по умолчанию
sudo systemctl set-default multi-user.target</code></pre><h4><code>.path</code> — реакция на события файловой системы</h4><p>Аналог <code>incron</code>. Запускает сервис при изменениях в файловой системе.</p><p>ini</p><pre spellcheck="" class="tmiCode language-ini" data-language="ini"><code># /etc/systemd/system/watch-uploads.path
[Unit]
Description=Watch for new uploads

[Path]
# Запустить связанный .service когда появится этот файл
PathExists=/var/spool/uploads/trigger.flag
# Или мониторить директорию на изменения
DirectoryNotEmpty=/var/spool/uploads/

[Install]
WantedBy=multi-user.target</code></pre><h3>2.3 Зависимости между юнитами — граф, а не очередь</h3><p>Это одно из ключевых отличий systemd от SysVinit. Вместо фиксированного порядка — направленный граф зависимостей.</p><p><strong>Директивы зависимостей:</strong></p><div class="tmiRichText__table-wrapper"><table style="min-width: 859px;"><colgroup><col style="min-width:20px;"><col style="min-width:20px;"><col style="width:819px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>Директива</p></th><th colspan="1" rowspan="1"><p>Тип</p></th><th colspan="1" rowspan="1"><p>Поведение</p></th></tr><tr><td colspan="1" rowspan="1"><p><code>Requires=</code></p></td><td colspan="1" rowspan="1"><p>Жёсткая</p></td><td colspan="1" rowspan="1"><p>Если зависимость не запустилась — этот юнит тоже не стартует и останавливается вместе с ней</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>Wants=</code></p></td><td colspan="1" rowspan="1"><p>Мягкая</p></td><td colspan="1" rowspan="1"><p>Пробует запустить зависимость, но если та упадёт — не останавливается</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>BindsTo=</code></p></td><td colspan="1" rowspan="1"><p>Очень жёсткая</p></td><td colspan="1" rowspan="1"><p>Как Requires, но юнит останавливается если зависимость остановится в любой момент</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>PartOf=</code></p></td><td colspan="1" rowspan="1"><p>Односторонняя</p></td><td colspan="1" rowspan="1"><p>Останавливается/перезапускается вместе с зависимостью, но не запускается вместе с ней</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>Conflicts=</code></p></td><td colspan="1" rowspan="1"><p>Конфликт</p></td><td colspan="1" rowspan="1"><p>Не может работать одновременно с указанным юнитом</p></td></tr></tbody></table></div><p><strong>Директивы порядка:</strong></p><div class="tmiRichText__table-wrapper"><table style="min-width: 460px;"><colgroup><col style="min-width:20px;"><col style="width:440px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>Директива</p></th><th colspan="1" rowspan="1"><p>Поведение</p></th></tr><tr><td colspan="1" rowspan="1"><p><code>After=</code></p></td><td colspan="1" rowspan="1"><p>Этот юнит запускается ПОСЛЕ указанного</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>Before=</code></p></td><td colspan="1" rowspan="1"><p>Этот юнит запускается ДО указанного</p></td></tr></tbody></table></div><blockquote class="tmiQuote" cite="" data-tmiquote=""><div class="tmiQuote_contents" data-tmitruncate=""><p><strong>Важный нюанс:</strong> <code>After=</code> и <code>Before=</code> задают только <strong>порядок</strong>, но не зависимость! Если вы напишете только <code>After=postgresql.service</code>, но не <code>Requires=postgresql.service</code>, то ваш сервис стартует после PostgreSQL, но запустится даже если PostgreSQL упал. Обычно нужно использовать оба.</p></div></blockquote><p>ini</p><pre spellcheck="" class="tmiCode language-ini" data-language="ini"><code>[Unit]
# Правильная комбинация: сначала БД, потом мы, и мы не работаем без БД
After=postgresql.service
Requires=postgresql.service</code></pre><hr><h2>Часть 3. cgroups — почему systemd знает всё о ваших процессах</h2><h3>3.1 Проблема, которую решают cgroups</h3><p>Представьте: nginx запущен. Он форкает 4 воркера. Один воркер форкает ещё процесс для CGI. Тот форкает что-то ещё. Итого 10 процессов, и все они «принадлежат» nginx, но в SysVinit это было невозможно отследить.</p><p><strong>Control Groups (cgroups)</strong> — механизм ядра Linux, который позволяет объединять процессы в иерархические группы и управлять ими совместно.</p><p>Systemd автоматически создаёт cgroup для каждого сервиса. Все дочерние процессы — внутри этой группы. Всегда.</p><pre spellcheck="" class="tmiCode language-plaintext" data-language="Простой текст"><code>/sys/fs/cgroup/
├── system.slice/
│   ├── nginx.service/        ← все процессы nginx здесь
│   │   ├── pid: 1234 (master)
│   │   ├── pid: 1235 (worker 1)
│   │   ├── pid: 1236 (worker 2)
│   │   └── pid: 1237 (cache loader)
│   ├── postgresql.service/   ← и postgres здесь
│   └── redis.service/
└── user.slice/
    └── user-1000.slice/      ← процессы пользователя</code></pre><h3>3.2 Что даёт cgroup на практике</h3><p><strong>Точный kill без зомби-процессов:</strong> При <code>systemctl stop nginx</code> systemd отправляет сигнал всей cgroup — умирают все 10 процессов, включая те, о которых вы не знали. Больше никаких «phantom workers».</p><p><strong>Мониторинг:</strong></p><p>bash</p><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># Показать дерево процессов cgroup сервиса
systemd-cgls /system.slice/nginx.service
# Вывод:
# /system.slice/nginx.service
# └─ 1234 /usr/sbin/nginx -g daemon off;
#    ├─ 1235 nginx: worker process
#    ├─ 1236 nginx: worker process
#    └─ 1237 nginx: cache loader process

# Мониторинг ресурсов в реальном времени (как top, но для cgroups)
systemd-cgtop</code></pre><h3>3.3 Ограничение ресурсов через юнит-файлы</h3><p>Это магия. Вместо сложных настроек cgroups вручную — просто добавляете строки в секцию <code>[Service]</code>:</p><p>ini</p><pre spellcheck="" class="tmiCode language-ini" data-language="ini"><code>[Service]
# === ПАМЯТЬ ===
# Мягкий лимит: systemd начнёт агрессивно освобождать память
MemoryHigh=400M
# Жёсткий лимит: OOM Killer убьёт процесс если превысит
MemoryMax=512M
# Гарантированная память (не будет отдана другим)
MemoryMin=100M

# === CPU ===
# 50% от одного ядра
CPUQuota=50%
# Или: вес CPU (1-10000, default=100)
CPUWeight=200

# === ДИСК (I/O) ===
# Ограничение чтения/записи для конкретного устройства
IOReadBandwidthMax=/dev/sda 50M
IOWriteBandwidthMax=/dev/sda 20M

# === СЕТЬ (через IP accounting) ===
IPAccounting=yes
IPAddressAllow=192.168.0.0/24
IPAddressDeny=any</code></pre><p><strong>Проверка текущих лимитов:</strong></p><p>bash</p><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># Посмотреть параметры cgroup напрямую в файловой системе
cat /sys/fs/cgroup/system.slice/nginx.service/memory.max
# 536870912 (512 МБ в байтах)

# Или через systemctl
systemctl show nginx.service | grep -E 'Memory|CPU|IO'</code></pre><hr><h2>Часть 4. journald — логи как база данных</h2><h3>4.1 Почему journald лучше текстовых логов</h3><p>Обычный syslog — это текстовый файл. Хочешь найти все ошибки nginx за последний час? Пишешь <code>grep "error" /var/log/nginx/error.log | grep "$(date +%b\ %d)"</code> и молишься.</p><p>journald — структурированное хранилище с индексами. Каждая запись — не строчка текста, а объект с полями:</p><pre spellcheck="" class="tmiCode language-plaintext" data-language="Простой текст"><code>_SYSTEMD_UNIT=nginx.service    ← какой сервис
_PID=1234                       ← какой процесс
_UID=www-data                   ← от какого пользователя
_HOSTNAME=web-01                ← на каком хосте
PRIORITY=3                      ← уровень важности (err)
MESSAGE=connection refused...   ← само сообщение
_SOURCE_REALTIME_TIMESTAMP=...  ← точное время</code></pre><h3>4.2 Полное руководство по journalctl</h3><p>bash</p><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># === БАЗОВЫЕ ЗАПРОСЫ ===

# Все логи конкретного сервиса
sudo journalctl -u nginx.service

# Последние 50 строк
sudo journalctl -u nginx.service -n 50

# Следить в реальном времени (как tail -f)
sudo journalctl -u nginx.service -f

# С определённого момента
sudo journalctl -u nginx.service --since "2024-01-15 10:00:00"
sudo journalctl -u nginx.service --since "1 hour ago"
sudo journalctl -u nginx.service --since today
sudo journalctl -u nginx.service --since yesterday --until "2024-01-14 23:59:59"

# === ФИЛЬТРАЦИЯ ПО УРОВНЮ ВАЖНОСТИ ===
# 0=emerg, 1=alert, 2=crit, 3=err, 4=warning, 5=notice, 6=info, 7=debug
sudo journalctl -p err                      # только err
sudo journalctl -p err..warning             # от err до warning
sudo journalctl -u nginx -p warning         # предупреждения nginx

# === ФИЛЬТРАЦИЯ ПО ЗАГРУЗКЕ ===
sudo journalctl -b                          # текущая загрузка
sudo journalctl -b -1                       # предыдущая загрузка
sudo journalctl -b -2                       # позапрошлая загрузка
sudo journalctl --list-boots                # список всех загрузок

# === ФОРМАТЫ ВЫВОДА ===
sudo journalctl -u nginx -o json            # JSON (для парсинга)
sudo journalctl -u nginx -o json-pretty     # JSON с форматированием
sudo journalctl -u nginx -o verbose         # Все поля записи
sudo journalctl -u nginx -o cat             # Только текст сообщений

# === ПРОДВИНУТЫЕ ЗАПРОСЫ ===

# Логи конкретного процесса
sudo journalctl _PID=1234

# Логи от конкретного пользователя
sudo journalctl _UID=1000

# Комбинирование условий (OR)
sudo journalctl _SYSTEMD_UNIT=nginx.service + _SYSTEMD_UNIT=php-fpm.service

# Экспорт в файл
sudo journalctl -u nginx --since today -o json &gt; nginx-today.json

# === УПРАВЛЕНИЕ ЖУРНАЛОМ ===

# Размер журнала на диске
sudo journalctl --disk-usage

# Очистка старых логов (оставить только за последние 2 недели)
sudo journalctl --vacuum-time=2weeks

# Очистка до определённого размера
sudo journalctl --vacuum-size=500M</code></pre><h3>4.3 Настройка journald</h3><p>ini</p><pre spellcheck="" class="tmiCode language-ini" data-language="ini"><code># /etc/systemd/journald.conf
[Journal]
# Максимальный размер журнала на диске
SystemMaxUse=1G

# Максимальный размер одного файла журнала
SystemMaxFileSize=100M

# Хранить журналы не дольше
MaxRetentionSec=1month

# Сжатие (по умолчанию включено)
Compress=yes

# Перенаправить в syslog (для совместимости)
ForwardToSyslog=no

# Уровень логирования по умолчанию
MaxLevelStore=debug
MaxLevelSyslog=warning</code></pre><p>После изменения: <code>sudo systemctl restart systemd-journald</code></p><hr><h2>Часть 5. Практика — реальные сценарии</h2><h3>5.1 Создание production-ready сервиса с нуля</h3><p>Задача: создать сервис для Go-приложения с полной изоляцией и автоматическим перезапуском.</p><p>ini</p><pre spellcheck="" class="tmiCode language-ini" data-language="ini"><code># /etc/systemd/system/api-server.service
[Unit]
Description=API Server
Documentation=https://github.com/company/api-server
After=network.target
Wants=network-online.target
After=network-online.target

# Если зависит от БД:
Requires=postgresql.service
After=postgresql.service

[Service]
Type=notify
# Путь к бинарнику
ExecStart=/usr/local/bin/api-server
# Путь к конфигу через переменную окружения
EnvironmentFile=/etc/api-server/env
# Или напрямую:
Environment="PORT=8080"
Environment="LOG_LEVEL=info"

# Перезапуск
Restart=on-failure
RestartSec=5s
StartLimitIntervalSec=60s
StartLimitBurst=3

# Пользователь и группа
User=api
Group=api

# Рабочая директория
WorkingDirectory=/opt/api-server

# === БЕЗОПАСНОСТЬ ===
# Запрет повышения привилегий
NoNewPrivileges=yes
# Изолированный /tmp
PrivateTmp=yes
# Только чтение для /usr, /boot, /etc
ProtectSystem=strict
# Запрет доступа к домашним директориям
ProtectHome=yes
# Изолированная сеть для системных вызовов
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes
# Список разрешённых системных вызовов
SystemCallFilter=@system-service
# Разрешить запись только в эти директории
ReadWritePaths=/var/lib/api-server /var/log/api-server

# === РЕСУРСЫ ===
MemoryMax=512M
CPUQuota=200%
# Ограничение открытых файлов
LimitNOFILE=65536

[Install]
WantedBy=multi-user.target</code></pre><p>Применение:</p><p>bash</p><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code>sudo systemctl daemon-reload
sudo systemctl enable --now api-server.service
sudo systemctl status api-server.service</code></pre><h3>5.2 Drop-in файлы — переопределение без изменения оригинала</h3><p>Золотое правило: никогда не редактируйте файлы в <code>/usr/lib/systemd/system/</code>. Используйте drop-in файлы.</p><p>bash</p><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># Удобный способ — systemctl edit сам создаст файл
sudo systemctl edit nginx.service</code></pre><p>Создастся файл <code>/etc/systemd/system/nginx.service.d/override.conf</code>:</p><p>ini</p><pre spellcheck="" class="tmiCode language-ini" data-language="ini"><code>[Service]
# Добавим лимит памяти к стандартному nginx
MemoryMax=256M
# Переопределим тип перезапуска
Restart=always
# Добавим переменную окружения
Environment="NGINX_ENVSUBST_OUTPUT_DIR=/etc/nginx"</code></pre><p>bash</p><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># После сохранения:
sudo systemctl daemon-reload
sudo systemctl restart nginx.service

# Посмотреть итоговую конфигурацию (оригинал + drop-ins)
sudo systemctl cat nginx.service</code></pre><h3>5.3 Анализ и оптимизация времени загрузки</h3><p>bash</p><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># Общее время загрузки
systemd-analyze
# Startup finished in 2.134s (kernel) + 8.643s (userspace) = 10.777s

# Топ «тормозов» при загрузке
systemd-analyze blame
# 4.123s NetworkManager-wait-online.service
# 2.456s plymouth-quit-wait.service
# 1.234s dev-sda1.device
# 0.987s apparmor.service

# Критический путь загрузки — что тормозит конкретный target
systemd-analyze critical-chain graphical.target

# Визуальная диаграмма в SVG (откройте в браузере!)
systemd-analyze plot &gt; boot-plot.svg

# Проверить юнит-файл на ошибки
systemd-analyze verify /etc/systemd/system/myapp.service</code></pre><p><strong>Частые причины медленной загрузки и как их лечить:</strong></p><div class="tmiRichText__table-wrapper"><table style="width: 1103px;"><colgroup><col style="width:280px;"><col style="width:244px;"><col style="width:579px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>Проблема</p></th><th colspan="1" rowspan="1"><p>Симптом</p></th><th colspan="1" rowspan="1"><p>Решение</p></th></tr><tr><td colspan="1" rowspan="1"><p><code>NetworkManager-wait-online.service</code></p></td><td colspan="1" rowspan="1"><p>20-30 секунд ожидания сети</p></td><td colspan="1" rowspan="1"><p><code>sudo systemctl disable NetworkManager-wait-online.service</code> (если сеть не нужна при загрузке)</p></td></tr><tr><td colspan="1" rowspan="1"><p>Сервис висит на старте</p></td><td colspan="1" rowspan="1"><p>Долгий timeout</p></td><td colspan="1" rowspan="1"><p>Проверить <code>TimeoutStartSec=</code> и зависимости</p></td></tr><tr><td colspan="1" rowspan="1"><p>Много последовательных зависимостей</p></td><td colspan="1" rowspan="1"><p>Длинный critical chain</p></td><td colspan="1" rowspan="1"><p>Заменить <code>Requires=</code> на <code>Wants=</code> где возможно</p></td></tr></tbody></table></div><h3>5.4 Диагностика падающего сервиса — пошаговый алгоритм</h3><p>bash</p><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># Шаг 1: Статус сервиса
sudo systemctl status myapp.service
# Ищем: статус (failed/active), последние строки лога, код выхода

# Шаг 2: Последние логи с подробными метаданными
sudo journalctl -u myapp.service -n 100 --no-pager

# Шаг 3: Логи с момента последней загрузки (для проблем при старте)
sudo journalctl -u myapp.service -b

# Шаг 4: Все ошибки в системе в момент падения
sudo journalctl -p err --since "10 min ago" --no-pager

# Шаг 5: Проверить зависимости
systemctl list-dependencies myapp.service
# Все зависимости должны быть зелёными

# Шаг 6: Запустить вручную под тем же пользователем (для воспроизведения)
sudo -u myapp /usr/local/bin/myapp --config /etc/myapp/config.yml

# Шаг 7: Посмотреть все переменные окружения сервиса
sudo systemctl show myapp.service -p Environment

# Шаг 8: Проверить права доступа к файлам
sudo systemctl cat myapp.service | grep -E 'ExecStart|WorkingDirectory|User'
sudo ls -la /usr/local/bin/myapp</code></pre><hr><h2>Часть 6. Продвинутые техники</h2><h3>6.1 Шаблонные юниты — один файл для многих экземпляров</h3><p>Если нужно запустить один и тот же сервис с разными параметрами (например, несколько воркеров), используйте шаблоны.</p><p>ini</p><pre spellcheck="" class="tmiCode language-ini" data-language="ini"><code># /etc/systemd/system/worker@.service
# Обратите внимание на @ в имени файла!
[Unit]
Description=Worker Instance %i
After=network.target

[Service]
Type=simple
ExecStart=/usr/local/bin/worker --id=%i --config=/etc/worker/config.yml
User=worker
Restart=on-failure

[Install]
WantedBy=multi-user.target</code></pre><p>Запуск нескольких экземпляров:</p><p>bash</p><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># %i заменится на значение после @
sudo systemctl start worker@1.service
sudo systemctl start worker@2.service
sudo systemctl start worker@3.service

# Или всех сразу:
sudo systemctl enable worker@{1..5}.service
sudo systemctl start worker@{1..5}.service

# Посмотреть все запущенные экземпляры
systemctl list-units 'worker@*'</code></pre><h3>6.2 Временные сервисы через systemd-run</h3><p>bash</p><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># Запустить команду как временный сервис (исчезнет после завершения)
sudo systemd-run --unit=my-task /usr/bin/python3 /opt/scripts/heavy_task.py

# С ограничением ресурсов
sudo systemd-run --unit=cpu-heavy --property=CPUQuota=50% --property=MemoryMax=256M \
    /usr/bin/python3 /opt/scripts/heavy_task.py

# Следить за прогрессом
journalctl -u my-task -f</code></pre><h3>6.3 Полезные команды, о которых мало кто знает</h3><p>bash</p><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># Проверить юнит-файл на синтаксические ошибки ДО применения
systemd-analyze verify /etc/systemd/system/myapp.service

# Показать все переопределения (drop-in файлы) для сервиса
systemctl cat nginx.service

# Показать все свойства юнита
systemctl show nginx.service

# Показать конкретное свойство
systemctl show nginx.service -p MainPID
systemctl show nginx.service -p MemoryCurrent

# Перезагрузить конфиги без daemon-reload (для drop-in файлов)
sudo systemctl daemon-reload

# Перечитать конфиги всех сервисов (более мягкий вариант)
sudo systemctl reload-or-restart nginx.service

# Узнать, какой пакет установил юнит
systemctl cat nginx.service | head -1
# # /lib/systemd/system/nginx.service
dpkg -S /lib/systemd/system/nginx.service   # Debian/Ubuntu
rpm -qf /lib/systemd/system/nginx.service  # RHEL/Fedora

# Блокировка: сервис не запустится даже вручную
sudo systemctl mask dangerous-service.service
# Разблокировка
sudo systemctl unmask dangerous-service.service</code></pre><hr><h2>Шпаргалка — все команды в одном месте</h2><h3>Управление сервисами</h3><div class="tmiRichText__table-wrapper"><table style="width: 1152px;"><colgroup><col style="width:352px;"><col style="width:800px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>Задача</p></th><th colspan="1" rowspan="1"><p>Команда</p></th></tr><tr><td colspan="1" rowspan="1"><p>Запустить</p></td><td colspan="1" rowspan="1"><p><code>sudo systemctl start &lt;name&gt;</code></p></td></tr><tr><td colspan="1" rowspan="1"><p>Остановить</p></td><td colspan="1" rowspan="1"><p><code>sudo systemctl stop &lt;name&gt;</code></p></td></tr><tr><td colspan="1" rowspan="1"><p>Перезапустить</p></td><td colspan="1" rowspan="1"><p><code>sudo systemctl restart &lt;name&gt;</code></p></td></tr><tr><td colspan="1" rowspan="1"><p>Перечитать конфиг (без остановки)</p></td><td colspan="1" rowspan="1"><p><code>sudo systemctl reload &lt;name&gt;</code></p></td></tr><tr><td colspan="1" rowspan="1"><p>Reload или restart</p></td><td colspan="1" rowspan="1"><p><code>sudo systemctl reload-or-restart &lt;name&gt;</code></p></td></tr><tr><td colspan="1" rowspan="1"><p>Статус</p></td><td colspan="1" rowspan="1"><p><code>systemctl status &lt;name&gt;</code></p></td></tr><tr><td colspan="1" rowspan="1"><p>Включить автозапуск</p></td><td colspan="1" rowspan="1"><p><code>sudo systemctl enable &lt;name&gt;</code></p></td></tr><tr><td colspan="1" rowspan="1"><p>Выключить автозапуск</p></td><td colspan="1" rowspan="1"><p><code>sudo systemctl disable &lt;name&gt;</code></p></td></tr><tr><td colspan="1" rowspan="1"><p>Включить И запустить</p></td><td colspan="1" rowspan="1"><p><code>sudo systemctl enable --now &lt;name&gt;</code></p></td></tr><tr><td colspan="1" rowspan="1"><p>Заблокировать навсегда</p></td><td colspan="1" rowspan="1"><p><code>sudo systemctl mask &lt;name&gt;</code></p></td></tr></tbody></table></div><h3>Просмотр состояния</h3><div class="tmiRichText__table-wrapper"><table style="width: 1416px;"><colgroup><col style="width:396px;"><col></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>Задача</p></th><th colspan="1" rowspan="1"><p>Команда</p></th></tr><tr><td colspan="1" rowspan="1"><p>Все запущенные сервисы</p></td><td colspan="1" rowspan="1"><p><code>systemctl list-units --type=service --state=running</code></p></td></tr><tr><td colspan="1" rowspan="1"><p>Все упавшие</p></td><td colspan="1" rowspan="1"><p><code>systemctl --failed</code></p></td></tr><tr><td colspan="1" rowspan="1"><p>Проверить автозапуск</p></td><td colspan="1" rowspan="1"><p><code>systemctl is-enabled &lt;name&gt;</code></p></td></tr><tr><td colspan="1" rowspan="1"><p>Проверить активность</p></td><td colspan="1" rowspan="1"><p><code>systemctl is-active &lt;name&gt;</code></p></td></tr><tr><td colspan="1" rowspan="1"><p>Дерево зависимостей</p></td><td colspan="1" rowspan="1"><p><code>systemctl list-dependencies &lt;name&gt;</code></p></td></tr><tr><td colspan="1" rowspan="1"><p>Кто зависит от этого</p></td><td colspan="1" rowspan="1"><p><code>systemctl list-dependencies --reverse &lt;name&gt;</code></p></td></tr><tr><td colspan="1" rowspan="1"><p>Все таймеры</p></td><td colspan="1" rowspan="1"><p><code>systemctl list-timers</code></p></td></tr></tbody></table></div><h3>Логи (journalctl)</h3><div class="tmiRichText__table-wrapper"><table style="width: 1045px;"><colgroup><col style="width:396px;"><col style="width:649px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>Задача</p></th><th colspan="1" rowspan="1"><p>Команда</p></th></tr><tr><td colspan="1" rowspan="1"><p>Логи сервиса</p></td><td colspan="1" rowspan="1"><p><code>sudo journalctl -u &lt;name&gt;</code></p></td></tr><tr><td colspan="1" rowspan="1"><p>Последние N строк</p></td><td colspan="1" rowspan="1"><p><code>sudo journalctl -u &lt;name&gt; -n 50</code></p></td></tr><tr><td colspan="1" rowspan="1"><p>В реальном времени</p></td><td colspan="1" rowspan="1"><p><code>sudo journalctl -u &lt;name&gt; -f</code></p></td></tr><tr><td colspan="1" rowspan="1"><p>За текущую загрузку</p></td><td colspan="1" rowspan="1"><p><code>sudo journalctl -u &lt;name&gt; -b</code></p></td></tr><tr><td colspan="1" rowspan="1"><p>Только ошибки</p></td><td colspan="1" rowspan="1"><p><code>sudo journalctl -u &lt;name&gt; -p err</code></p></td></tr><tr><td colspan="1" rowspan="1"><p>С определённого времени</p></td><td colspan="1" rowspan="1"><p><code>sudo journalctl -u &lt;name&gt; --since "1h ago"</code></p></td></tr><tr><td colspan="1" rowspan="1"><p>Размер журнала</p></td><td colspan="1" rowspan="1"><p><code>sudo journalctl --disk-usage</code></p></td></tr><tr><td colspan="1" rowspan="1"><p>Очистить старые</p></td><td colspan="1" rowspan="1"><p><code>sudo journalctl --vacuum-time=2weeks</code></p></td></tr></tbody></table></div><h3>Диагностика производительности</h3><div class="tmiRichText__table-wrapper"><table style="width: 1050px;"><colgroup><col style="width:444px;"><col style="width:606px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>Задача</p></th><th colspan="1" rowspan="1"><p>Команда</p></th></tr><tr><td colspan="1" rowspan="1"><p>Время загрузки</p></td><td colspan="1" rowspan="1"><p><code>systemd-analyze</code></p></td></tr><tr><td colspan="1" rowspan="1"><p>Что грузилось дольше всех</p></td><td colspan="1" rowspan="1"><p><code>systemd-analyze blame</code></p></td></tr><tr><td colspan="1" rowspan="1"><p>Критический путь</p></td><td colspan="1" rowspan="1"><p><code>systemd-analyze critical-chain &lt;target&gt;</code></p></td></tr><tr><td colspan="1" rowspan="1"><p>Визуальная диаграмма</p></td><td colspan="1" rowspan="1"><p><code>systemd-analyze plot &gt; boot.svg</code></p></td></tr><tr><td colspan="1" rowspan="1"><p>Проверить юнит-файл</p></td><td colspan="1" rowspan="1"><p><code>systemd-analyze verify /path/to/unit</code></p></td></tr><tr><td colspan="1" rowspan="1"><p>Дерево cgroups</p></td><td colspan="1" rowspan="1"><p><code>systemd-cgls</code></p></td></tr><tr><td colspan="1" rowspan="1"><p>Ресурсы cgroups в реальном времени</p></td><td colspan="1" rowspan="1"><p><code>systemd-cgtop</code></p></td></tr></tbody></table></div><hr><h2>Заключение</h2><p>Systemd — это не монстр, которого стоит бояться. Это мощный инструмент, понимание которого делает вас значительно эффективнее как системного администратора или разработчика. Несколько ключевых идей, которые стоит унести из этой статьи:</p><ol><li><p><strong>Юниты</strong> — декларативные описания ресурсов. Пишите их правильно, используя все доступные настройки безопасности.</p></li><li><p><strong>cgroups</strong> — системd всегда знает, где ваши процессы. Используйте это для мониторинга и ограничения ресурсов.</p></li><li><p><strong>journald</strong> — это база данных, а не текстовый файл. Учитесь делать правильные запросы.</p></li><li><p><strong>Drop-in файлы</strong> — никогда не редактируйте оригинальные юнит-файлы из пакетов.</p></li><li><p><strong>systemd-analyze</strong> — ваш первый инструмент при проблемах с загрузкой.</p></li></ol>]]></description><guid isPermaLink="false">82</guid><pubDate>Sat, 07 Mar 2026 04:06:37 +0000</pubDate></item><item><title>Systemd: The Complete Guide from Zero to Hero &#x2014; Architecture, Units, cgroups, Logging, and Real-World Examples</title><link>https://ithub.uno/statiarticles/9_infrastructure/systemd-the-complete-guide-from-zero-to-hero-architecture-units-cgroups-logging-and-real-world-examples-r85/</link><description><![CDATA[
<p><img src="https://ithub.uno/uploads/tmi_files_2026_03/systemd-linux.png.6efd164210cc01359928da5b537d997e.png" /></p>
<h2>Introduction: Why SysVinit Died and What Systemd Fixed</h2><p>Imagine a chef cooking dinner for 10 guests but making each dish completely from scratch, one at a time — starting the salad only after the soup is fully served. That's essentially how <strong>SysVinit</strong> worked: it started services one by one, in a fixed order, regardless of whether they were actually dependent on each other.</p><p>As Linux systems grew more complex, this became a serious bottleneck:</p><ul><li><p><strong>Slow boot times.</strong> Service A waits for Service B to finish, even if there's zero dependency between them.</p></li><li><p><strong>No process tracking.</strong> Init launched a script and moved on. A child process crashed? SysVinit had no idea.</p></li><li><p><strong>Log chaos.</strong> Every service wrote logs wherever it wanted — <code>/var/log/nginx/</code>, syslog, <code>/tmp/</code> — no unified interface.</p></li><li><p><strong>Brittle shell scripts.</strong> The <code>/etc/init.d/</code> scripts were fragile, hard to maintain, and inconsistent across distros.</p></li></ul><p>In 2010, <strong>Lennart Poettering</strong> introduced systemd to solve all of these problems simultaneously: parallel startup, dependency graphs, control groups, and centralized logging. The community response was controversial (to put it mildly), but today systemd is the de-facto standard on Fedora, Debian, Ubuntu, Arch, RHEL, and most other major distributions.</p><p>Let's break it down piece by piece.</p><hr><h2>Part 1. Systemd Architecture — What's Under the Hood</h2><h3>1.1 PID 1 — The Ruler of All Processes</h3><p>When the Linux kernel boots, it launches the very first user-space process with <strong>PID 1</strong>. On systemd systems, that process IS the <code>systemd</code> daemon. It's the direct parent of everything else in the system.</p><p>This matters for two reasons:</p><ol><li><p>If PID 1 crashes, the system panics. Hence systemd is written to be extremely robust.</p></li><li><p>All orphaned processes (whose parent died) are automatically reparented to PID 1.</p></li></ol><pre spellcheck="" class="tmiCode language-plaintext" data-language="Простой текст"><code>Linux Kernel
    └── systemd (PID 1)
            ├── journald (logging)
            ├── udevd (device management)
            ├── networkd (networking)
            ├── nginx.service (your web server)
            ├── postgresql.service (database)
            └── ... all other services</code></pre><h3>1.2 Key Components</h3><p><code>systemd</code><strong> (PID 1)</strong> The conductor of the whole orchestra. It reads unit files, builds a dependency graph, launches processes in the right order, and tracks them via cgroups.</p><p><code>systemctl</code> Your control panel. When you type <code>systemctl start nginx</code>, this tool does NOT start nginx directly. It sends a <strong>D-Bus message</strong> to the systemd daemon, which does the actual work. This is a fundamental difference from running a script.</p><p><code>journald</code> Centralized logging daemon. It captures <code>stdout</code> and <code>stderr</code> from all services, enriches each entry with structured metadata (PID, UID, unit name, hostname), and stores everything in a binary format that supports complex queries — think SQL for logs.</p><p><code>udevd</code> Device manager. When you plug in a USB drive, udevd creates <code>/dev/sdb</code>, loads the appropriate kernel modules, and can trigger specific services.</p><p><code>networkd</code><strong>, </strong><code>timedated</code><strong>, </strong><code>logind</code> Specialized daemons for network management, system time, and user sessions. They all communicate with PID 1 via D-Bus.</p><h3>1.3 D-Bus — The Communication Backbone</h3><p>D-Bus is an inter-process communication (IPC) system bus — think of it as an internal messaging platform between processes. Instead of processes calling each other's functions directly (which is unsafe), they send structured messages through the bus.</p><p><strong>Example flow for </strong><code>systemctl start nginx</code><strong>:</strong></p><ol><li><p><code>systemctl</code> forms a D-Bus message: "Call the <code>StartUnit</code> method with argument <code>nginx.service</code>"</p></li><li><p>The message goes onto the system bus</p></li><li><p>The <code>systemd</code> daemon receives and processes it</p></li><li><p>Returns the result through the same bus</p></li></ol><p>This provides security (permissions checked at the D-Bus level), flexibility (any program can manage services), and extensibility.</p><hr><h2>Part 2. Units — The Building Blocks of Systemd</h2><p>A unit is a description of any system resource as a declarative configuration file. Think of it as the "passport" for a service, socket, timer, or mount point.</p><h3>2.1 Where Units Live</h3><div class="tmiRichText__table-wrapper"><table style="min-width: 640px;"><colgroup><col style="min-width:20px;"><col style="width:600px;"><col style="min-width:20px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>Path</p></th><th colspan="1" rowspan="1"><p>Purpose</p></th><th colspan="1" rowspan="1"><p>Priority</p></th></tr><tr><td colspan="1" rowspan="1"><p><code>/usr/lib/systemd/system/</code></p></td><td colspan="1" rowspan="1"><p>Units installed by package manager</p></td><td colspan="1" rowspan="1"><p>Lowest</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>/etc/systemd/system/</code></p></td><td colspan="1" rowspan="1"><p>Your custom units and overrides</p></td><td colspan="1" rowspan="1"><p>High</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>/run/systemd/system/</code></p></td><td colspan="1" rowspan="1"><p>Temporary units (gone after reboot)</p></td><td colspan="1" rowspan="1"><p>Highest</p></td></tr></tbody></table></div><blockquote class="tmiQuote" cite="" data-tmiquote=""><div class="tmiQuote_contents" data-tmitruncate=""><p><strong>Important:</strong> Never edit files in <code>/usr/lib/systemd/system/</code> directly — they'll be overwritten on package updates. To modify a stock unit, use <code>systemctl edit &lt;name&gt;</code>, which creates a drop-in override at <code>/etc/systemd/system/&lt;name&gt;.d/override.conf</code>.</p></div></blockquote><h3>2.2 Unit Types</h3><h4><code>.service</code> — Service Units (Most Common)</h4><p>Describes a daemon or process. This is what you'll use in 90% of cases.</p><p>ini</p><pre spellcheck="" class="tmiCode language-ini" data-language="ini"><code># /etc/systemd/system/myapp.service
[Unit]
Description=My Awesome Application
After=network.target postgresql.service
Requires=postgresql.service

[Service]
Type=simple
ExecStart=/usr/bin/myapp --config /etc/myapp/config.yml
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
RestartSec=5s
User=myapp
Group=myapp

[Install]
WantedBy=multi-user.target</code></pre><p><strong>The </strong><code>Type=</code><strong> parameter — get this right:</strong></p><div class="tmiRichText__table-wrapper"><table style="min-width: 1108px;"><colgroup><col style="min-width:20px;"><col style="width:537px;"><col style="width:551px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>Type</p></th><th colspan="1" rowspan="1"><p>Behavior</p></th><th colspan="1" rowspan="1"><p>When to Use</p></th></tr><tr><td colspan="1" rowspan="1"><p><code>simple</code></p></td><td colspan="1" rowspan="1"><p>Service is considered started immediately after ExecStart launches</p></td><td colspan="1" rowspan="1"><p>Most modern applications</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>forking</code></p></td><td colspan="1" rowspan="1"><p>Program calls fork() and the parent exits. Systemd waits for this.</p></td><td colspan="1" rowspan="1"><p>Classic Unix daemons (nginx, apache)</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>notify</code></p></td><td colspan="1" rowspan="1"><p>Program signals systemd via <code>sd_notify()</code> when ready</p></td><td colspan="1" rowspan="1"><p>Programs with native systemd API support</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>oneshot</code></p></td><td colspan="1" rowspan="1"><p>Program runs and exits. Systemd waits for completion.</p></td><td colspan="1" rowspan="1"><p>Scripts, one-off tasks</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>dbus</code></p></td><td colspan="1" rowspan="1"><p>Service is considered started when it claims a D-Bus name</p></td><td colspan="1" rowspan="1"><p>Daemons using D-Bus</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>idle</code></p></td><td colspan="1" rowspan="1"><p>Start delayed until all other jobs complete</p></td><td colspan="1" rowspan="1"><p>Low-priority background tasks</p></td></tr></tbody></table></div><p><strong>Restart policy:</strong></p><p>ini</p><pre spellcheck="" class="tmiCode language-ini" data-language="ini"><code>[Service]
# Restart= options:
# no          — never restart
# on-success  — only on exit code 0
# on-failure  — on non-zero exit, signal, or timeout (most common choice)
# on-abnormal — on signal or timeout (not normal exit)
# always      — always restart (even after systemctl stop!)
Restart=on-failure
RestartSec=5s

# Limit restart attempts:
# Max 5 attempts within 30 seconds, then give up
StartLimitIntervalSec=30s
StartLimitBurst=5</code></pre><h4><code>.socket</code> — Socket-Based Activation (Lazy Launch)</h4><p>This is one of the most powerful and underappreciated features of systemd. The idea: why keep 20 services running when most of them get called once an hour?</p><p>Socket-based activation works like this:</p><ol><li><p>systemd opens and listens on a socket (port, Unix socket, or FIFO)</p></li><li><p>The actual service is <strong>not running</strong></p></li><li><p>The first connection arrives</p></li><li><p>systemd launches the service and passes the established connection to it</p></li><li><p>The client never notices — the connection isn't lost!</p></li></ol><p>ini</p><pre spellcheck="" class="tmiCode language-ini" data-language="ini"><code># /etc/systemd/system/echo.socket
[Unit]
Description=Echo Server Socket

[Socket]
ListenStream=12345
Accept=no

[Install]
WantedBy=sockets.target</code></pre><p>ini</p><pre spellcheck="" class="tmiCode language-ini" data-language="ini"><code># /etc/systemd/system/echo.service
[Unit]
Description=Echo Server

[Service]
Type=simple
ExecStart=/usr/local/bin/echo-server
# Service receives the socket via file descriptor 3
StandardInput=socket</code></pre><p>Enable: <code>sudo systemctl enable --now echo.socket</code> — and the service starts on the first connection.</p><h4><code>.timer</code> — Cron Replacement with Superpowers</h4><p>Systemd timers beat cron on several fronts:</p><ul><li><p>Support dependencies (run only if some service is running)</p></li><li><p>Logged in journald like any other unit</p></li><li><p>Can "catch up" on missed runs after reboot (<code>Persistent=true</code>)</p></li><li><p>Support random delays to spread load across the hour</p></li></ul><p>ini</p><pre spellcheck="" class="tmiCode language-ini" data-language="ini"><code># /etc/systemd/system/backup.timer
[Unit]
Description=Daily Backup Timer

[Timer]
# Run every day at 02:30
OnCalendar=*-*-* 02:30:00
# Random delay up to 10 minutes (don't hammer the server at exactly 02:30!)
RandomizedDelaySec=10m
# Run the task if it was missed (e.g. system was off)
Persistent=true

[Install]
WantedBy=timers.target</code></pre><p>ini</p><pre spellcheck="" class="tmiCode language-ini" data-language="ini"><code># /etc/systemd/system/backup.service
[Unit]
Description=Daily Backup Job

[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
User=backup</code></pre><p>Enable: <code>sudo systemctl enable --now backup.timer</code></p><p>Check all active timers: <code>systemctl list-timers --all</code></p><p><strong>OnCalendar syntax cheatsheet:</strong></p><div class="tmiRichText__table-wrapper"><table style="width: 785px;"><colgroup><col style="width:279px;"><col style="width:506px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>Expression</p></th><th colspan="1" rowspan="1"><p>Meaning</p></th></tr><tr><td colspan="1" rowspan="1"><p><code>daily</code></p></td><td colspan="1" rowspan="1"><p>Every day at 00:00</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>weekly</code></p></td><td colspan="1" rowspan="1"><p>Every Monday at 00:00</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>monthly</code></p></td><td colspan="1" rowspan="1"><p>1st of every month</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>*-*-* 09:00:00</code></p></td><td colspan="1" rowspan="1"><p>Every day at 09:00</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>Mon-Fri *-*-* 08:30:00</code></p></td><td colspan="1" rowspan="1"><p>Weekdays at 08:30</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>*-*-1,15 00:00:00</code></p></td><td colspan="1" rowspan="1"><p>1st and 15th of every month</p></td></tr></tbody></table></div><p>Validate an expression: <code>systemd-analyze calendar "Mon-Fri *-*-* 08:30:00"</code></p><h4><code>.target</code> — Unit Groups (Replacing Runlevels)</h4><p>A target is not a service — it's a synchronization point. Think of it as a "system state" to be reached.</p><div class="tmiRichText__table-wrapper"><table style="min-width: 732px;"><colgroup><col style="width:244px;"><col style="min-width:20px;"><col style="width:468px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>Target</p></th><th colspan="1" rowspan="1"><p>SysV Runlevel</p></th><th colspan="1" rowspan="1"><p>Meaning</p></th></tr><tr><td colspan="1" rowspan="1"><p><code>poweroff.target</code></p></td><td colspan="1" rowspan="1"><p>0</p></td><td colspan="1" rowspan="1"><p>Shutdown</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>rescue.target</code></p></td><td colspan="1" rowspan="1"><p>1</p></td><td colspan="1" rowspan="1"><p>Single-user mode</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>multi-user.target</code></p></td><td colspan="1" rowspan="1"><p>3</p></td><td colspan="1" rowspan="1"><p>Multi-user, no GUI</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>graphical.target</code></p></td><td colspan="1" rowspan="1"><p>5</p></td><td colspan="1" rowspan="1"><p>With graphical interface</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>reboot.target</code></p></td><td colspan="1" rowspan="1"><p>6</p></td><td colspan="1" rowspan="1"><p>Reboot</p></td></tr></tbody></table></div><p>bash</p><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># Check current default target (like current runlevel)
systemctl get-default

# Switch target (like init 3)
sudo systemctl isolate multi-user.target

# Set default target
sudo systemctl set-default multi-user.target</code></pre><h3>2.3 Unit Dependencies — A Graph, Not a Queue</h3><p>This is one of the key differentiators from SysVinit. Instead of a fixed sequence, systemd builds a directed dependency graph.</p><p><strong>Dependency directives:</strong></p><div class="tmiRichText__table-wrapper"><table style="width: 1350px;"><colgroup><col style="width:141px;"><col style="width:145px;"><col></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>Directive</p></th><th colspan="1" rowspan="1"><p>Type</p></th><th colspan="1" rowspan="1"><p>Behavior</p></th></tr><tr><td colspan="1" rowspan="1"><p><code>Requires=</code></p></td><td colspan="1" rowspan="1"><p>Hard</p></td><td colspan="1" rowspan="1"><p>If the dependency fails to start, this unit also fails and stops with it</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>Wants=</code></p></td><td colspan="1" rowspan="1"><p>Soft</p></td><td colspan="1" rowspan="1"><p>Tries to start the dependency, but won't stop if it fails</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>BindsTo=</code></p></td><td colspan="1" rowspan="1"><p>Very hard</p></td><td colspan="1" rowspan="1"><p>Like Requires, but this unit stops whenever the dependency stops</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>PartOf=</code></p></td><td colspan="1" rowspan="1"><p>One-way</p></td><td colspan="1" rowspan="1"><p>Stops/restarts together with the dependency, but doesn't start with it</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>Conflicts=</code></p></td><td colspan="1" rowspan="1"><p>Conflict</p></td><td colspan="1" rowspan="1"><p>Cannot run simultaneously with the specified unit</p></td></tr></tbody></table></div><p><strong>Ordering directives:</strong></p><div class="tmiRichText__table-wrapper"><table style="min-width: 506px;"><colgroup><col style="min-width:20px;"><col style="width:486px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>Directive</p></th><th colspan="1" rowspan="1"><p>Behavior</p></th></tr><tr><td colspan="1" rowspan="1"><p><code>After=</code></p></td><td colspan="1" rowspan="1"><p>This unit starts AFTER the specified one</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>Before=</code></p></td><td colspan="1" rowspan="1"><p>This unit starts BEFORE the specified one</p></td></tr></tbody></table></div><blockquote class="tmiQuote" cite="" data-tmiquote=""><div class="tmiQuote_contents" data-tmitruncate=""><p><strong>Critical nuance:</strong> <code>After=</code> and <code>Before=</code> only define <strong>ordering</strong>, NOT dependency! If you write only <code>After=postgresql.service</code> without <code>Requires=postgresql.service</code>, your service will start after PostgreSQL but will also start even if PostgreSQL failed. You almost always need both.</p></div></blockquote><hr><h2>Part 3. cgroups — Why Systemd Always Knows Your Processes</h2><h3>3.1 The Problem cgroups Solve</h3><p>Consider: nginx is running. It forks 4 workers. One worker forks a CGI process. That forks something else. Now there are 10 processes, all "belonging" to nginx, but in SysVinit there was no way to track this.</p><p><strong>Control Groups (cgroups)</strong> are a Linux kernel mechanism that lets you group processes hierarchically and manage them collectively.</p><p>Systemd automatically creates a cgroup for every service. All child processes live inside that group. Always.</p><pre spellcheck="" class="tmiCode language-plaintext" data-language="Простой текст"><code>/sys/fs/cgroup/
├── system.slice/
│   ├── nginx.service/        ← all nginx processes here
│   │   ├── pid: 1234 (master)
│   │   ├── pid: 1235 (worker 1)
│   │   ├── pid: 1236 (worker 2)
│   │   └── pid: 1237 (cache loader)
│   ├── postgresql.service/
│   └── redis.service/
└── user.slice/
    └── user-1000.slice/      ← user processes</code></pre><h3>3.2 What cgroups Give You in Practice</h3><p><strong>Clean process termination — no zombie processes:</strong> When you run <code>systemctl stop nginx</code>, systemd sends the signal to the entire cgroup — all 10 processes die, including ones you didn't know existed. No more phantom workers.</p><p><strong>Monitoring:</strong></p><p>bash</p><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># Show process tree for a service's cgroup
systemd-cgls /system.slice/nginx.service

# Real-time resource monitoring (like top, but for cgroups)
systemd-cgtop</code></pre><h3>3.3 Resource Limits via Unit Files</h3><p>Instead of manually configuring cgroups, just add lines to your <code>[Service]</code> section:</p><p>ini</p><pre spellcheck="" class="tmiCode language-ini" data-language="ini"><code>[Service]
# === MEMORY ===
# Soft limit: systemd will aggressively reclaim memory
MemoryHigh=400M
# Hard limit: OOM Killer will kill the process if exceeded
MemoryMax=512M
# Guaranteed memory (won't be given to others)
MemoryMin=100M

# === CPU ===
# 50% of a single core
CPUQuota=50%
# Or: CPU weight (1-10000, default=100)
CPUWeight=200

# === DISK I/O ===
IOReadBandwidthMax=/dev/sda 50M
IOWriteBandwidthMax=/dev/sda 20M

# === NETWORK ===
IPAccounting=yes
IPAddressAllow=192.168.0.0/24
IPAddressDeny=any</code></pre><p><strong>Verify current limits:</strong></p><p>bash</p><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># Check cgroup filesystem directly
cat /sys/fs/cgroup/system.slice/nginx.service/memory.max
# 536870912 (512 MB in bytes)

# Or via systemctl
systemctl show nginx.service | grep -E 'Memory|CPU|IO'</code></pre><hr><h2>Part 4. journald — Logs as a Database</h2><h3>4.1 Why journald Beats Plain Text Logs</h3><p>A plain syslog is a text file. Want to find all nginx errors from the last hour? You write <code>grep "error" /var/log/nginx/error.log | grep "$(date +%b\ %d)"</code> and hope for the best.</p><p>journald is a structured store with indexes. Every entry is not a text string but an object with fields:</p><pre spellcheck="" class="tmiCode language-plaintext" data-language="Простой текст"><code>_SYSTEMD_UNIT=nginx.service    ← which service
_PID=1234                       ← which process
_UID=www-data                   ← which user
_HOSTNAME=web-01                ← which host
PRIORITY=3                      ← severity level (err)
MESSAGE=connection refused...   ← the message itself
_SOURCE_REALTIME_TIMESTAMP=...  ← precise timestamp</code></pre><h3>4.2 Complete journalctl Reference</h3><p>bash</p><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># === BASIC QUERIES ===

# All logs for a service
sudo journalctl -u nginx.service

# Last 50 lines
sudo journalctl -u nginx.service -n 50

# Follow in real time (like tail -f)
sudo journalctl -u nginx.service -f

# From a specific time
sudo journalctl -u nginx.service --since "2024-01-15 10:00:00"
sudo journalctl -u nginx.service --since "1 hour ago"
sudo journalctl -u nginx.service --since today
sudo journalctl -u nginx.service --since yesterday --until "2024-01-14 23:59:59"

# === FILTERING BY SEVERITY ===
# 0=emerg, 1=alert, 2=crit, 3=err, 4=warning, 5=notice, 6=info, 7=debug
sudo journalctl -p err                      # only errors
sudo journalctl -p err..warning             # err through warning
sudo journalctl -u nginx -p warning         # nginx warnings only

# === FILTERING BY BOOT ===
sudo journalctl -b                          # current boot
sudo journalctl -b -1                       # previous boot
sudo journalctl -b -2                       # two boots ago
sudo journalctl --list-boots                # list all boots

# === OUTPUT FORMATS ===
sudo journalctl -u nginx -o json            # JSON (for parsing)
sudo journalctl -u nginx -o json-pretty     # Formatted JSON
sudo journalctl -u nginx -o verbose         # All metadata fields
sudo journalctl -u nginx -o cat             # Message text only

# === ADVANCED QUERIES ===

# Logs for a specific process
sudo journalctl _PID=1234

# Logs from a specific user
sudo journalctl _UID=1000

# Combine conditions (OR)
sudo journalctl _SYSTEMD_UNIT=nginx.service + _SYSTEMD_UNIT=php-fpm.service

# Export to file
sudo journalctl -u nginx --since today -o json &gt; nginx-today.json

# === JOURNAL MANAGEMENT ===

# Disk usage of the journal
sudo journalctl --disk-usage

# Clean up logs older than 2 weeks
sudo journalctl --vacuum-time=2weeks

# Clean up to a specific size
sudo journalctl --vacuum-size=500M</code></pre><hr><h2>Part 5. Real-World Scenarios</h2><h3>5.1 Creating a Production-Ready Service from Scratch</h3><p>ini</p><pre spellcheck="" class="tmiCode language-ini" data-language="ini"><code># /etc/systemd/system/api-server.service
[Unit]
Description=API Server
Documentation=https://github.com/company/api-server
After=network-online.target
Wants=network-online.target
Requires=postgresql.service
After=postgresql.service

[Service]
Type=notify
ExecStart=/usr/local/bin/api-server
EnvironmentFile=/etc/api-server/env
Environment="PORT=8080"
Environment="LOG_LEVEL=info"

Restart=on-failure
RestartSec=5s
StartLimitIntervalSec=60s
StartLimitBurst=3

User=api
Group=api
WorkingDirectory=/opt/api-server

# === SECURITY HARDENING ===
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
ProtectHome=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes
SystemCallFilter=@system-service
ReadWritePaths=/var/lib/api-server /var/log/api-server

# === RESOURCE LIMITS ===
MemoryMax=512M
CPUQuota=200%
LimitNOFILE=65536

[Install]
WantedBy=multi-user.target</code></pre><h3>5.2 Drop-in Files — Override Without Touching Originals</h3><p>bash</p><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># systemctl edit creates the override file automatically
sudo systemctl edit nginx.service
# Creates: /etc/systemd/system/nginx.service.d/override.conf</code></pre><p>ini</p><pre spellcheck="" class="tmiCode language-ini" data-language="ini"><code>[Service]
MemoryMax=256M
Restart=always
Environment="NGINX_ENVSUBST_OUTPUT_DIR=/etc/nginx"</code></pre><p>bash</p><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code>sudo systemctl daemon-reload
sudo systemctl restart nginx.service

# View the full effective config (original + drop-ins)
sudo systemctl cat nginx.service</code></pre><h3>5.3 Boot Time Analysis and Optimization</h3><p>bash</p><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># Total boot time
systemd-analyze
# Startup finished in 2.134s (kernel) + 8.643s (userspace) = 10.777s

# Top boot-time offenders
systemd-analyze blame

# Critical path to a specific target
systemd-analyze critical-chain graphical.target

# Generate visual timeline (open in browser!)
systemd-analyze plot &gt; boot-plot.svg

# Validate a unit file for errors
systemd-analyze verify /etc/systemd/system/myapp.service</code></pre><h3>5.4 Diagnosing a Failing Service — Step by Step</h3><p>bash</p><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># Step 1: Service status
sudo systemctl status myapp.service

# Step 2: Recent logs with full detail
sudo journalctl -u myapp.service -n 100 --no-pager

# Step 3: Logs since last boot (for startup issues)
sudo journalctl -u myapp.service -b

# Step 4: All errors in the system at the time of failure
sudo journalctl -p err --since "10 min ago" --no-pager

# Step 5: Check dependencies
systemctl list-dependencies myapp.service

# Step 6: Run manually as the service user (to reproduce)
sudo -u myapp /usr/local/bin/myapp --config /etc/myapp/config.yml

# Step 7: Check environment variables
sudo systemctl show myapp.service -p Environment

# Step 8: Check file permissions
sudo systemctl cat myapp.service | grep -E 'ExecStart|WorkingDirectory|User'
sudo ls -la /usr/local/bin/myapp</code></pre><hr><h2>Quick Reference Cheatsheet</h2><h3>Service Control</h3><div class="tmiRichText__table-wrapper"><table style="width: 783px;"><colgroup><col style="width:209px;"><col style="width:574px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>Task</p></th><th colspan="1" rowspan="1"><p>Command</p></th></tr><tr><td colspan="1" rowspan="1"><p>Start</p></td><td colspan="1" rowspan="1"><p><code>sudo systemctl start &lt;name&gt;</code></p></td></tr><tr><td colspan="1" rowspan="1"><p>Stop</p></td><td colspan="1" rowspan="1"><p><code>sudo systemctl stop &lt;name&gt;</code></p></td></tr><tr><td colspan="1" rowspan="1"><p>Restart</p></td><td colspan="1" rowspan="1"><p><code>sudo systemctl restart &lt;name&gt;</code></p></td></tr><tr><td colspan="1" rowspan="1"><p>Reload config (no stop)</p></td><td colspan="1" rowspan="1"><p><code>sudo systemctl reload &lt;name&gt;</code></p></td></tr><tr><td colspan="1" rowspan="1"><p>Status</p></td><td colspan="1" rowspan="1"><p><code>systemctl status &lt;name&gt;</code></p></td></tr><tr><td colspan="1" rowspan="1"><p>Enable autostart</p></td><td colspan="1" rowspan="1"><p><code>sudo systemctl enable &lt;name&gt;</code></p></td></tr><tr><td colspan="1" rowspan="1"><p>Disable autostart</p></td><td colspan="1" rowspan="1"><p><code>sudo systemctl disable &lt;name&gt;</code></p></td></tr><tr><td colspan="1" rowspan="1"><p>Enable AND start</p></td><td colspan="1" rowspan="1"><p><code>sudo systemctl enable --now &lt;name&gt;</code></p></td></tr><tr><td colspan="1" rowspan="1"><p>Block permanently</p></td><td colspan="1" rowspan="1"><p><code>sudo systemctl mask &lt;name&gt;</code></p></td></tr></tbody></table></div><h3>Viewing State</h3><div class="tmiRichText__table-wrapper"><table style="width: 884px;"><colgroup><col style="width:205px;"><col style="width:679px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>Task</p></th><th colspan="1" rowspan="1"><p>Command</p></th></tr><tr><td colspan="1" rowspan="1"><p>All running services</p></td><td colspan="1" rowspan="1"><p><code>systemctl list-units --type=service --state=running</code></p></td></tr><tr><td colspan="1" rowspan="1"><p>All failed</p></td><td colspan="1" rowspan="1"><p><code>systemctl --failed</code></p></td></tr><tr><td colspan="1" rowspan="1"><p>Check autostart</p></td><td colspan="1" rowspan="1"><p><code>systemctl is-enabled &lt;name&gt;</code></p></td></tr><tr><td colspan="1" rowspan="1"><p>Check active</p></td><td colspan="1" rowspan="1"><p><code>systemctl is-active &lt;name&gt;</code></p></td></tr><tr><td colspan="1" rowspan="1"><p>Dependency tree</p></td><td colspan="1" rowspan="1"><p><code>systemctl list-dependencies &lt;name&gt;</code></p></td></tr><tr><td colspan="1" rowspan="1"><p>Who depends on this</p></td><td colspan="1" rowspan="1"><p><code>systemctl list-dependencies --reverse &lt;name&gt;</code></p></td></tr><tr><td colspan="1" rowspan="1"><p>All timers</p></td><td colspan="1" rowspan="1"><p><code>systemctl list-timers</code></p></td></tr></tbody></table></div><h3>Logs (journalctl)</h3><div class="tmiRichText__table-wrapper"><table style="width: 795px;"><colgroup><col style="width:197px;"><col style="width:598px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>Task</p></th><th colspan="1" rowspan="1"><p>Command</p></th></tr><tr><td colspan="1" rowspan="1"><p>Service logs</p></td><td colspan="1" rowspan="1"><p><code>sudo journalctl -u &lt;name&gt;</code></p></td></tr><tr><td colspan="1" rowspan="1"><p>Last N lines</p></td><td colspan="1" rowspan="1"><p><code>sudo journalctl -u &lt;name&gt; -n 50</code></p></td></tr><tr><td colspan="1" rowspan="1"><p>Real-time</p></td><td colspan="1" rowspan="1"><p><code>sudo journalctl -u &lt;name&gt; -f</code></p></td></tr><tr><td colspan="1" rowspan="1"><p>Current boot</p></td><td colspan="1" rowspan="1"><p><code>sudo journalctl -u &lt;name&gt; -b</code></p></td></tr><tr><td colspan="1" rowspan="1"><p>Errors only</p></td><td colspan="1" rowspan="1"><p><code>sudo journalctl -u &lt;name&gt; -p err</code></p></td></tr><tr><td colspan="1" rowspan="1"><p>Since time</p></td><td colspan="1" rowspan="1"><p><code>sudo journalctl -u &lt;name&gt; --since "1h ago"</code></p></td></tr><tr><td colspan="1" rowspan="1"><p>Journal size</p></td><td colspan="1" rowspan="1"><p><code>sudo journalctl --disk-usage</code></p></td></tr><tr><td colspan="1" rowspan="1"><p>Cleanup</p></td><td colspan="1" rowspan="1"><p><code>sudo journalctl --vacuum-time=2weeks</code></p></td></tr></tbody></table></div><h3>Performance Diagnostics</h3><div class="tmiRichText__table-wrapper"><table style="width: 1078px;"><colgroup><col style="width:333px;"><col style="width:745px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>Task</p></th><th colspan="1" rowspan="1"><p>Command</p></th></tr><tr><td colspan="1" rowspan="1"><p>Boot time</p></td><td colspan="1" rowspan="1"><p><code>systemd-analyze</code></p></td></tr><tr><td colspan="1" rowspan="1"><p>Boot bottlenecks</p></td><td colspan="1" rowspan="1"><p><code>systemd-analyze blame</code></p></td></tr><tr><td colspan="1" rowspan="1"><p>Critical chain</p></td><td colspan="1" rowspan="1"><p><code>systemd-analyze critical-chain &lt;target&gt;</code></p></td></tr><tr><td colspan="1" rowspan="1"><p>Visual timeline</p></td><td colspan="1" rowspan="1"><p><code>systemd-analyze plot &gt; boot.svg</code></p></td></tr><tr><td colspan="1" rowspan="1"><p>Validate unit file</p></td><td colspan="1" rowspan="1"><p><code>systemd-analyze verify /path/to/unit</code></p></td></tr><tr><td colspan="1" rowspan="1"><p>cgroup tree</p></td><td colspan="1" rowspan="1"><p><code>systemd-cgls</code></p></td></tr><tr><td colspan="1" rowspan="1"><p>cgroup resource usage</p></td><td colspan="1" rowspan="1"><p><code>systemd-cgtop</code></p></td></tr></tbody></table></div><hr><h2>Conclusion</h2><p>Systemd is not a monster to be feared — it's a powerful tool that makes you dramatically more effective as a sysadmin or developer. Key takeaways from this guide:</p><ol><li><p><strong>Units</strong> are declarative resource descriptions. Write them properly and use available security directives.</p></li><li><p><strong>cgroups</strong> mean systemd always knows where your processes are. Use this for monitoring and resource constraints.</p></li><li><p><strong>journald</strong> is a database, not a text file. Learn to query it properly.</p></li><li><p><strong>Drop-in files</strong> — never edit original package-installed unit files.</p></li><li><p><strong>systemd-analyze</strong> — your first tool when diagnosing boot problems.</p></li></ol>]]></description><guid isPermaLink="false">85</guid><pubDate>Sat, 07 Mar 2026 04:13:04 +0000</pubDate></item><item><title>&#x411;&#x430;&#x437;&#x44B; &#x434;&#x430;&#x43D;&#x43D;&#x44B;&#x445; &#x432;&#x440;&#x435;&#x43C;&#x435;&#x43D;&#x43D;&#x44B;&#x445; &#x440;&#x44F;&#x434;&#x43E;&#x432;: InfluxDB, TimescaleDB &#x438; &#x43F;&#x440;&#x43E;&#x43C;&#x44B;&#x448;&#x43B;&#x435;&#x43D;&#x43D;&#x44B;&#x439; historian</title><link>https://ithub.uno/statiarticles/9_infrastructure/%D0%B1%D0%B0%D0%B7%D1%8B-%D0%B4%D0%B0%D0%BD%D0%BD%D1%8B%D1%85-%D0%B2%D1%80%D0%B5%D0%BC%D0%B5%D0%BD%D0%BD%D1%8B%D1%85-%D1%80%D1%8F%D0%B4%D0%BE%D0%B2-influxdb-timescaledb-%D0%B8-%D0%BF%D1%80%D0%BE%D0%BC%D1%8B%D1%88%D0%BB%D0%B5%D0%BD%D0%BD%D1%8B%D0%B9-historian-r119/</link><description><![CDATA[
<p><img src="https://ithub.uno/uploads/tmi_files_2026_03/tsfnn0.png.4d59c51892c3907b7590b41d2e411097.png" /></p>
<h2>Зачем специализированная БД для временных рядов</h2><p>Технологические данные — это всегда временной ряд: температура каждую секунду, давление каждые 100 мс, состояние оборудования каждые 10 мс. PostgreSQL или MySQL могут хранить такие данные. Но при миллионах записей в день начинаются проблемы.</p><p><strong>Почему реляционные БД плохо справляются:</strong></p><ul><li><p>Индексы B-Tree неэффективны для временных запросов ("за последний час")</p></li><li><p>Запись строк в таблицу с индексами — медленно при высоком темпе</p></li><li><p>GROUP BY time_interval требует дорогих вычислений</p></li><li><p>Партиционирование по времени нужно настраивать вручную</p></li><li><p>Хранение тысяч тегов → тысячи колонок или плохая схема</p></li></ul><p><strong>Что умеют Time-Series TSDB:</strong></p><ul><li><p>Оптимизированная запись: 100 000+ точек/сек на скромном железе</p></li><li><p>Встроенное сжатие (delta-delta, XOR float compression)</p></li><li><p>Автоматические retention policies (TTL данных)</p></li><li><p>Downsampling: автоматически агрегируем "горячие" данные в "холодные"</p></li><li><p>Встроенные временны́е функции: moving average, rate, derivative</p></li></ul><hr><h2>InfluxDB 2.x: промышленный стандарт IoT</h2><h3>Основные концепции</h3><p><strong>Measurement</strong> — аналог таблицы:</p><pre spellcheck="" class="tmiCode language-plaintext" data-language="Простой текст"><code>measurement: "telemetry"
</code></pre><p><strong>Tags</strong> — индексированные метаданные (строки):</p><pre spellcheck="" class="tmiCode language-plaintext" data-language="Простой текст"><code>tags: device="conveyor1", location="line1", area="factory"
</code></pre><p><strong>Fields</strong> — неиндексированные данные (числа, строки, bool):</p><pre spellcheck="" class="tmiCode language-plaintext" data-language="Простой текст"><code>fields: temperature=87.3, current=15.5, running=true
</code></pre><p><strong>Timestamp</strong> — время с нано-точностью.</p><p><strong>Точка данных (Point):</strong></p><pre spellcheck="" class="tmiCode language-plaintext" data-language="Простой текст"><code>measurement,tags fields timestamp
telemetry,device=conveyor1,location=line1 temperature=87.3,current=15.5 1710000000000000000
</code></pre><h3>Почему Tags vs Fields важно</h3><pre spellcheck="" class="tmiCode language-plaintext" data-language="Простой текст"><code>Tags:    ИНДЕКСИРОВАНЫ → используйте для группировки/фильтрации
         device, location, sensor_type, unit_id

Fields:  НЕ индексированы → используйте для числовых данных
         temperature, pressure, current, voltage
         
ОШИБКА: положить temperature в Tag — поиск по значению работает,
        но карданальность огромная → индекс разрастётся → InfluxDB замедлится.
ОШИБКА: положить device_id в Field — нельзя эффективно фильтровать по устройству.
</code></pre><h3>Python клиент InfluxDB 2.x:</h3><pre spellcheck="" class="tmiCode language-python" data-language="Python"><code>from influxdb_client import InfluxDBClient, Point, WritePrecision
from influxdb_client.client.write_api import SYNCHRONOUS, WriteOptions
from datetime import datetime, timezone
import time

INFLUX_URL    = "http://localhost:8086"
INFLUX_TOKEN  = "your-api-token-here"
INFLUX_ORG    = "factory"
INFLUX_BUCKET = "process_data"

# Клиент с батчевой записью
client = InfluxDBClient(url=INFLUX_URL, token=INFLUX_TOKEN, org=INFLUX_ORG)

write_api = client.write_api(write_options=WriteOptions(
    batch_size        = 1000,    # Накапливаем до 1000 точек
    flush_interval    = 5_000,   # Или сбрасываем каждые 5 секунд
    jitter_interval   = 500,     # ±500мс для сглаживания нагрузки
    retry_interval    = 5_000,   # Retry при ошибке через 5с
    max_retry_time    = 180_000, # Максимум 3 минуты retry
))

query_api = client.query_api()

# ===== ЗАПИСЬ =====

def write_single_point(device: str, location: str,
                        temperature: float, current: float, running: bool):
    """Запись одной точки"""
    point = (
        Point("telemetry")
        .tag("device",   device)
        .tag("location", location)
        .field("temperature", temperature)
        .field("current",     current)
        .field("running",     int(running))  # bool → int (InfluxDB лучше хранит)
        .time(datetime.now(timezone.utc))
    )
    write_api.write(bucket=INFLUX_BUCKET, record=point)


def write_batch(measurements: list[dict]):
    """
    Эффективная пакетная запись.
    measurements: [{'device': 'pump1', 'temp': 25.3, 'current': 12.1}, ...]
    """
    points = []
    for m in measurements:
        p = (
            Point("telemetry")
            .tag("device",   m['device'])
            .tag("location", m.get('location', 'unknown'))
            .field("temperature", float(m.get('temp',    0)))
            .field("current",     float(m.get('current', 0)))
            .field("pressure",    float(m.get('pressure', 0)))
        )
        points.append(p)
    
    write_api.write(bucket=INFLUX_BUCKET, record=points)


# Запись в нативном line protocol (максимальная производительность):
def write_line_protocol(lines: list[str]):
    """
    Прямая запись в line protocol — самый быстрый способ.
    Формат: measurement[,tag=value...] field=value[,field=value...] [timestamp]
    """
    write_api.write(bucket=INFLUX_BUCKET, record='\n'.join(lines),
                    write_precision=WritePrecision.NANOSECONDS)

# Пример:
lines = [
    "telemetry,device=pump1,location=line1 temperature=87.3,current=15.5 1710000000000000000",
    "telemetry,device=pump2,location=line1 temperature=72.1,current=8.2  1710000000000000000",
    "telemetry,device=valve1,location=line2 position=75.0               1710000000000000000",
]
write_line_protocol(lines)


# ===== ЗАПРОСЫ (Flux) =====

def query_last_hour(device: str) -&gt; list[dict]:
    """Последний час данных устройства"""
    flux = f'''
    from(bucket: "{INFLUX_BUCKET}")
        |&gt; range(start: -1h)
        |&gt; filter(fn: (r) =&gt; r._measurement == "telemetry")
        |&gt; filter(fn: (r) =&gt; r.device == "{device}")
        |&gt; filter(fn: (r) =&gt; r._field == "temperature" or r._field == "current")
        |&gt; pivot(rowKey: ["_time"], columnKey: ["_field"], valueColumn: "_value")
        |&gt; sort(columns: ["_time"])
    '''
    
    tables = query_api.query(flux)
    results = []
    for table in tables:
        for record in table.records:
            results.append({
                'time':        record.get_time().isoformat(),
                'temperature': record.values.get('temperature'),
                'current':     record.values.get('current'),
            })
    return results


def query_aggregated_stats(device: str, window: str = "5m",
                            range_start: str = "-24h") -&gt; list[dict]:
    """
    Агрегированная статистика по временным окнам.
    window: "1m", "5m", "1h", "1d"
    """
    flux = f'''
    from(bucket: "{INFLUX_BUCKET}")
        |&gt; range(start: {range_start})
        |&gt; filter(fn: (r) =&gt; r._measurement == "telemetry" and r.device == "{device}")
        |&gt; filter(fn: (r) =&gt; r._field == "temperature")
        |&gt; aggregateWindow(
            every: {window},
            fn: (tables=&lt;-, column) =&gt; tables |&gt; reduce(
                identity: {{mean: 0.0, min: 99999.0, max: -99999.0, count: 0}},
                fn: (r, accumulator) =&gt; ({{
                    mean:  accumulator.mean + r._value,
                    min:   if r._value &lt; accumulator.min  then r._value else accumulator.min,
                    max:   if r._value &gt; accumulator.max  then r._value else accumulator.max,
                    count: accumulator.count + 1,
                }})
            ),
            createEmpty: false
        )
    '''
    # Для простого avg/min/max лучше использовать встроенные функции:
    flux_simple = f'''
    from(bucket: "{INFLUX_BUCKET}")
        |&gt; range(start: {range_start})
        |&gt; filter(fn: (r) =&gt; r._measurement == "telemetry" 
                          and r.device == "{device}"
                          and r._field == "temperature")
        |&gt; aggregateWindow(every: {window}, fn: mean, createEmpty: false)
        |&gt; yield(name: "mean")
    '''
    
    tables = query_api.query(flux_simple)
    return [{'time': r.get_time().isoformat(), 'mean_temp': r.get_value()}
            for table in tables for r in table.records]


def query_anomalies(threshold_high: float = 85.0,
                     range_start: str = "-7d") -&gt; list[dict]:
    """Поиск аномалий — превышений порога"""
    flux = f'''
    from(bucket: "{INFLUX_BUCKET}")
        |&gt; range(start: {range_start})
        |&gt; filter(fn: (r) =&gt; r._measurement == "telemetry" and r._field == "temperature")
        |&gt; filter(fn: (r) =&gt; r._value &gt; {threshold_high})
        |&gt; group(columns: ["device"])
        |&gt; sort(columns: ["_time"], desc: true)
    '''
    
    tables = query_api.query(flux)
    return [{
        'device': r.values.get('device'),
        'time':   r.get_time().isoformat(),
        'value':  r.get_value(),
        'excess': round(r.get_value() - threshold_high, 2),
    } for table in tables for r in table.records]


def query_device_availability(range_start: str = "-30d") -&gt; list[dict]:
    """Доступность (availability) по устройствам за период"""
    flux = f'''
    import "math"
    
    total = from(bucket: "{INFLUX_BUCKET}")
        |&gt; range(start: {range_start})
        |&gt; filter(fn: (r) =&gt; r._measurement == "telemetry" and r._field == "running")
        |&gt; group(columns: ["device"])
        |&gt; count()
        |&gt; rename(columns: {{_value: "total_count"}})
    
    running = from(bucket: "{INFLUX_BUCKET}")
        |&gt; range(start: {range_start})
        |&gt; filter(fn: (r) =&gt; r._measurement == "telemetry" and r._field == "running")
        |&gt; filter(fn: (r) =&gt; r._value == 1)
        |&gt; group(columns: ["device"])
        |&gt; count()
        |&gt; rename(columns: {{_value: "running_count"}})
    
    join(tables: {{total, running}}, on: ["device"])
        |&gt; map(fn: (r) =&gt; ({{ r with availability_pct: 
            math.round(x: r.running_count / r.total_count * 1000.0) / 10.0
        }}))
    '''
    
    tables = query_api.query(flux)
    return [{'device': r.values.get('device'),
             'availability': r.values.get('availability_pct')}
            for table in tables for r in table.records]
</code></pre><hr><h2>Retention Policies и Downsampling</h2><p>Хранить сырые данные с секундным разрешением 10 лет — безумно дорого. Правильная стратегия:</p><pre spellcheck="" class="tmiCode language-plaintext" data-language="Простой текст"><code>"Горячие" данные:  1 секунда, 30 дней    → быстрый SSD
"Тёплые" данные:   1 минута,  1 год      → обычный SSD
"Холодные" данные: 1 час,     10 лет     → HDD/объектное хранилище
</code></pre><h3>Конфигурация в InfluxDB 2.x:</h3><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># Создание bucket с retention 30 дней (сырые данные)
influx bucket create \
    --name process_data_raw \
    --retention 30d \
    --org factory

# Bucket для агрегированных данных (бессрочно)
influx bucket create \
    --name process_data_aggregated \
    --retention 0 \
    --org factory
</code></pre><h3>Задача downsampling (Flux):</h3><pre spellcheck="" class="tmiCode language-python" data-language="Python"><code>def setup_downsampling_task():
    """
    Создаём задачу InfluxDB для автоматического downsampling.
    Каждые 5 минут агрегируем сырые данные в минутные.
    """
    
    flux_task = '''
    option task = {
        name: "Downsampling: raw→1min",
        every: 5m,          // Запускать каждые 5 минут
        offset: 1m,         // Смещение (ждём пока данные придут)
    }
    
    // Читаем сырые данные за последние 5 минут
    data = from(bucket: "process_data_raw")
        |&gt; range(start: -task.every)
        |&gt; filter(fn: (r) =&gt; r._measurement == "telemetry")
    
    // Агрегируем каждую числовую метрику
    data
        |&gt; filter(fn: (r) =&gt; r._field == "temperature" or 
                              r._field == "current"     or
                              r._field == "pressure")
        |&gt; aggregateWindow(every: 1m, fn: mean, createEmpty: false)
        |&gt; set(key: "_measurement", value: "telemetry_1m")
        |&gt; to(bucket: "process_data_aggregated")
    
    // Для бинарных данных (running) — используем last
    data
        |&gt; filter(fn: (r) =&gt; r._field == "running")
        |&gt; aggregateWindow(every: 1m, fn: last, createEmpty: false)
        |&gt; set(key: "_measurement", value: "telemetry_1m")
        |&gt; to(bucket: "process_data_aggregated")
    '''
    
    # Создание задачи через API
    tasks_api = client.tasks_api()
    task = tasks_api.create_task_every(
        name="Downsampling: raw→1min",
        flux=flux_task,
        every="5m",
        organization=INFLUX_ORG
    )
    print(f"Задача создана: {task.id}")
</code></pre><hr><h2>TimescaleDB: PostgreSQL для временных рядов</h2><p>TimescaleDB — расширение PostgreSQL. Если вы уже используете PostgreSQL и знаете SQL — это лучший выбор. Вы получаете TSDB-оптимизации при сохранении полного SQL.</p><pre spellcheck="" class="tmiCode language-sql" data-language="SQL"><code>-- Установка расширения
CREATE EXTENSION IF NOT EXISTS timescaledb;

-- Обычная таблица PostgreSQL
CREATE TABLE telemetry (
    time        TIMESTAMPTZ NOT NULL,
    device      TEXT        NOT NULL,
    location    TEXT        NOT NULL,
    temperature FLOAT,
    current     FLOAT,
    pressure    FLOAT,
    running     BOOLEAN,
    quality     TEXT DEFAULT 'GOOD'
);

-- Превращаем в hypertable (TimescaleDB магия!)
SELECT create_hypertable('telemetry', 'time',
    chunk_time_interval =&gt; INTERVAL '1 day'  -- Партиция = 1 день
);

-- Индекс на часто используемые теги
CREATE INDEX ON telemetry (device, time DESC);
CREATE INDEX ON telemetry (location, time DESC);

-- Compression (сжатие старых данных)
ALTER TABLE telemetry SET (
    timescaledb.compress,
    timescaledb.compress_segmentby = 'device',
    timescaledb.compress_orderby = 'time DESC'
);

-- Автоматическое сжатие данных старше 7 дней
SELECT add_compression_policy('telemetry', INTERVAL '7 days');

-- Автоматическое удаление старых данных (30 дней)
SELECT add_retention_policy('telemetry', INTERVAL '30 days');
</code></pre><h3>Запросы (обычный SQL!):</h3><pre spellcheck="" class="tmiCode language-sql" data-language="SQL"><code>-- Последний час данных с устройства
SELECT time, temperature, current, running
FROM telemetry
WHERE device = 'pump1'
  AND time &gt; NOW() - INTERVAL '1 hour'
ORDER BY time DESC;

-- Среднее по 5-минутным окнам
SELECT 
    time_bucket('5 minutes', time) AS bucket,
    device,
    ROUND(AVG(temperature)::numeric, 2) AS avg_temp,
    ROUND(MIN(temperature)::numeric, 2) AS min_temp,
    ROUND(MAX(temperature)::numeric, 2) AS max_temp,
    COUNT(*) AS samples
FROM telemetry
WHERE device = 'pump1'
  AND time &gt; NOW() - INTERVAL '24 hours'
GROUP BY bucket, device
ORDER BY bucket DESC;

-- Обнаружение аномалий (значение &gt; avg + 2*stddev)
WITH stats AS (
    SELECT 
        device,
        AVG(temperature) AS avg_temp,
        STDDEV(temperature) AS std_temp
    FROM telemetry
    WHERE time &gt; NOW() - INTERVAL '7 days'
    GROUP BY device
)
SELECT 
    t.time, t.device, t.temperature,
    s.avg_temp, s.std_temp,
    (t.temperature - s.avg_temp) / NULLIF(s.std_temp, 0) AS z_score
FROM telemetry t
JOIN stats s ON t.device = s.device
WHERE t.time &gt; NOW() - INTERVAL '24 hours'
  AND ABS((t.temperature - s.avg_temp) / NULLIF(s.std_temp, 0)) &gt; 2.0
ORDER BY ABS((t.temperature - s.avg_temp) / NULLIF(s.std_temp, 0)) DESC
LIMIT 50;

-- Доступность оборудования за месяц
SELECT 
    device,
    COUNT(*) FILTER (WHERE running = true)  AS running_count,
    COUNT(*) AS total_count,
    ROUND(
        COUNT(*) FILTER (WHERE running = true)::numeric / COUNT(*) * 100, 1
    ) AS availability_pct,
    SUM(CASE WHEN running THEN 1 ELSE 0 END) * 
        EXTRACT(EPOCH FROM INTERVAL '1 second') / 3600.0 AS running_hours
FROM telemetry
WHERE time &gt; NOW() - INTERVAL '30 days'
GROUP BY device
ORDER BY availability_pct DESC;
</code></pre><h3>Непрерывные агрегации (Continuous Aggregates):</h3><pre spellcheck="" class="tmiCode language-sql" data-language="SQL"><code>-- Создаём материализованное представление с автообновлением
CREATE MATERIALIZED VIEW telemetry_5min
WITH (timescaledb.continuous) AS
SELECT 
    time_bucket('5 minutes', time) AS bucket,
    device,
    location,
    AVG(temperature)  AS avg_temp,
    MIN(temperature)  AS min_temp,
    MAX(temperature)  AS max_temp,
    AVG(current)      AS avg_current,
    MAX(current)      AS max_current,
    BOOL_OR(running)  AS any_running,
    COUNT(*)          AS sample_count
FROM telemetry
GROUP BY bucket, device, location
WITH NO DATA;

-- Автоматическое обновление каждые 5 минут
SELECT add_continuous_aggregate_policy('telemetry_5min',
    start_offset =&gt; INTERVAL '15 minutes',
    end_offset   =&gt; INTERVAL '5 minutes',
    schedule_interval =&gt; INTERVAL '5 minutes'
);

-- Запрос к агрегированным данным (мгновенно!)
SELECT * FROM telemetry_5min
WHERE device = 'pump1'
  AND bucket &gt; NOW() - INTERVAL '24 hours'
ORDER BY bucket DESC;
</code></pre><hr><h2>Python + SQLAlchemy + TimescaleDB:</h2><pre spellcheck="" class="tmiCode language-python" data-language="Python"><code>from sqlalchemy import create_engine, text
from sqlalchemy.orm import Session
import pandas as pd
from datetime import datetime, timedelta, timezone

DATABASE_URL = "postgresql://user:password@localhost:5432/factory_db"
engine = create_engine(DATABASE_URL, pool_size=10, max_overflow=20)

class TelemetryRepository:
    
    def write_batch(self, records: list[dict]) -&gt; int:
        """Пакетная запись телеметрии"""
        if not records:
            return 0
        
        with engine.begin() as conn:
            result = conn.execute(
                text("""
                    INSERT INTO telemetry (time, device, location,
                                          temperature, current, pressure, running)
                    VALUES (:time, :device, :location,
                            :temperature, :current, :pressure, :running)
                    ON CONFLICT DO NOTHING
                """),
                records
            )
            return result.rowcount
    
    def get_latest(self, device: str, fields: list[str] = None) -&gt; dict | None:
        """Последнее значение устройства"""
        field_list = ', '.join(fields or ['temperature', 'current', 'pressure', 'running'])
        
        with engine.connect() as conn:
            row = conn.execute(
                text(f"""
                    SELECT time, {field_list}
                    FROM telemetry
                    WHERE device = :device
                    ORDER BY time DESC
                    LIMIT 1
                """),
                {'device': device}
            ).fetchone()
            
            return dict(row._mapping) if row else None
    
    def get_as_dataframe(self, device: str, hours: int = 24) -&gt; pd.DataFrame:
        """Загрузка данных в Pandas DataFrame для анализа"""
        query = text("""
            SELECT time, temperature, current, pressure, running
            FROM telemetry
            WHERE device = :device
              AND time &gt; :since
            ORDER BY time
        """)
        
        with engine.connect() as conn:
            df = pd.read_sql(
                query,
                conn,
                params={'device': device,
                        'since': datetime.now(timezone.utc) - timedelta(hours=hours)},
                parse_dates=['time'],
                index_col='time'
            )
        
        return df
    
    def detect_anomalies_zscore(self, device: str, 
                                  field: str = 'temperature',
                                  threshold: float = 2.5) -&gt; pd.DataFrame:
        """Обнаружение аномалий методом z-score"""
        df = self.get_as_dataframe(device, hours=24)
        
        if df.empty or field not in df.columns:
            return pd.DataFrame()
        
        mean = df[field].mean()
        std  = df[field].std()
        
        if std == 0:
            return pd.DataFrame()
        
        df['z_score'] = (df[field] - mean) / std
        anomalies = df[df['z_score'].abs() &gt; threshold].copy()
        anomalies['is_high'] = anomalies['z_score'] &gt; 0
        
        return anomalies[['z_score', field, 'is_high']]
    
    def get_equipment_report(self, days: int = 30) -&gt; pd.DataFrame:
        """Отчёт по оборудованию за период"""
        query = text("""
            SELECT 
                device,
                COUNT(*) as total_records,
                COUNT(*) FILTER (WHERE running) as running_records,
                ROUND((COUNT(*) FILTER (WHERE running)::numeric / COUNT(*) * 100)::numeric, 1) as availability_pct,
                ROUND(AVG(temperature)::numeric, 1) as avg_temp,
                ROUND(MAX(temperature)::numeric, 1) as max_temp,
                ROUND(AVG(current)::numeric, 2) as avg_current
            FROM telemetry
            WHERE time &gt; NOW() - MAKE_INTERVAL(days =&gt; :days)
            GROUP BY device
            ORDER BY device
        """)
        
        with engine.connect() as conn:
            return pd.read_sql(query, conn, params={'days': days})
</code></pre><hr><h2>Выбор TSDB: сравнительная таблица</h2><div class="tmiRichText__table-wrapper"><table style="width: 1036px;"><colgroup><col style="width:266px;"><col style="width:314px;"><col style="width:236px;"><col style="width:220px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>Критерий</p></th><th colspan="1" rowspan="1"><p>InfluxDB 2.x</p></th><th colspan="1" rowspan="1"><p>TimescaleDB</p></th><th colspan="1" rowspan="1"><p>Prometheus</p></th></tr><tr><td colspan="1" rowspan="1"><p>Основа</p></td><td colspan="1" rowspan="1"><p>Собственный движок</p></td><td colspan="1" rowspan="1"><p>PostgreSQL</p></td><td colspan="1" rowspan="1"><p>Собственный</p></td></tr><tr><td colspan="1" rowspan="1"><p>Язык запросов</p></td><td colspan="1" rowspan="1"><p>Flux (мощный, непривычный)</p></td><td colspan="1" rowspan="1"><p>SQL</p></td><td colspan="1" rowspan="1"><p>PromQL</p></td></tr><tr><td colspan="1" rowspan="1"><p>Производительность записи</p></td><td colspan="1" rowspan="1"><p>★★★★★</p></td><td colspan="1" rowspan="1"><p>★★★★</p></td><td colspan="1" rowspan="1"><p>★★★</p></td></tr><tr><td colspan="1" rowspan="1"><p>SQL-совместимость</p></td><td colspan="1" rowspan="1"><p><span class="tmiEmoji" title="">❌</span></p></td><td colspan="1" rowspan="1"><p><span class="tmiEmoji" title="">✅</span> (полная)</p></td><td colspan="1" rowspan="1"><p><span class="tmiEmoji" title="">❌</span></p></td></tr><tr><td colspan="1" rowspan="1"><p>Сжатие</p></td><td colspan="1" rowspan="1"><p>★★★★★</p></td><td colspan="1" rowspan="1"><p>★★★★</p></td><td colspan="1" rowspan="1"><p>★★★</p></td></tr><tr><td colspan="1" rowspan="1"><p>Масштабирование</p></td><td colspan="1" rowspan="1"><p>InfluxDB Enterprise</p></td><td colspan="1" rowspan="1"><p>TimescaleDB</p></td><td colspan="1" rowspan="1"><p>Thanos/Cortex</p></td></tr><tr><td colspan="1" rowspan="1"><p>Лицензия</p></td><td colspan="1" rowspan="1"><p>BSL (OSS ограничен)</p></td><td colspan="1" rowspan="1"><p>Apache 2</p></td><td colspan="1" rowspan="1"><p>Apache 2</p></td></tr><tr><td colspan="1" rowspan="1"><p>Интеграция с Grafana</p></td><td colspan="1" rowspan="1"><p>★★★★★</p></td><td colspan="1" rowspan="1"><p>★★★★★</p></td><td colspan="1" rowspan="1"><p>★★★★★</p></td></tr><tr><td colspan="1" rowspan="1"><p>Лучше для</p></td><td colspan="1" rowspan="1"><p>IoT, большой объём тегов</p></td><td colspan="1" rowspan="1"><p>Существующий PostgreSQL-стек</p></td><td colspan="1" rowspan="1"><p>DevOps мониторинг</p></td></tr></tbody></table></div><hr><h2>Заключение</h2><p>Выбор TSDB зависит от контекста. <strong>InfluxDB</strong> — лучший выбор для чистых IoT/телеметрия проектов: максимальная производительность, мощный Flux для временны́х вычислений, отличная экосистема. <strong>TimescaleDB</strong> — если уже есть PostgreSQL инфраструктура, нужны JOINs с другими данными или разработчики лучше знают SQL.</p><p>Ключевые принципы для production: всегда настраивайте retention policies (данные должны автоматически удаляться), используйте downsampling для долгосрочного хранения агрегатов, настройте сжатие (экономия 90%+ дискового пространства), мониторьте производительность самой TSDB.</p><p>Deadband-фильтрация на уровне edge-узла (не писать если значение не изменилось существенно) снижает нагрузку на БД в 5–50 раз для медленно меняющихся процессов. Это первое что нужно сделать перед любой оптимизацией TSDB.</p>]]></description><guid isPermaLink="false">119</guid><pubDate>Sat, 21 Mar 2026 17:33:04 +0000</pubDate></item><item><title>PostgreSQL 16/17/18: &#x430;&#x434;&#x43C;&#x438;&#x43D;&#x438;&#x441;&#x442;&#x440;&#x438;&#x440;&#x43E;&#x432;&#x430;&#x43D;&#x438;&#x435; &#x438; &#x442;&#x44E;&#x43D;&#x438;&#x43D;&#x433; &#x43F;&#x440;&#x43E;&#x438;&#x437;&#x432;&#x43E;&#x434;&#x438;&#x442;&#x435;&#x43B;&#x44C;&#x43D;&#x43E;&#x441;&#x442;&#x438; &#x432; &#x43F;&#x440;&#x43E;&#x434;&#x430;&#x43A;&#x448;&#x435;&#x43D;&#x435;</title><link>https://ithub.uno/statiarticles/9_infrastructure/postgresql-161718-%D0%B0%D0%B4%D0%BC%D0%B8%D0%BD%D0%B8%D1%81%D1%82%D1%80%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5-%D0%B8-%D1%82%D1%8E%D0%BD%D0%B8%D0%BD%D0%B3-%D0%BF%D1%80%D0%BE%D0%B8%D0%B7%D0%B2%D0%BE%D0%B4%D0%B8%D1%82%D0%B5%D0%BB%D1%8C%D0%BD%D0%BE%D1%81%D1%82%D0%B8-%D0%B2-%D0%BF%D1%80%D0%BE%D0%B4%D0%B0%D0%BA%D1%88%D0%B5%D0%BD%D0%B5-r155/</link><description><![CDATA[
<p><img src="https://ithub.uno/uploads/tmi_files_2026_03/0seizy-vxfjf3uv8g3n_bijcwb0.jpeg.7ea9b66c99849502cfdfbe935e4eb589.jpeg" /></p>
<h2>Введение: PostgreSQL в продакшене — другой зверь</h2><p>Поднять PostgreSQL локально — просто. Запустить его в продакшене под реальной нагрузкой так, чтобы он не падал, не тормозил и не раздувался до потери диска — это уже инженерия.</p><p>PostgreSQL 16, 17 и 18 принесли серьёзные улучшения производительности: логическая репликация стала намного мощнее, параллельные запросы умнее, планировщик научился большему. Но дефолтная конфигурация по-прежнему рассчитана на «запустить на ноутбуке с 256 МБ RAM», а не на production-сервер с 128 ГБ памяти.</p><p>Эта статья — системный разбор всего, что нужно сделать, чтобы PostgreSQL работал быстро, надёжно и предсказуемо. Никакой воды: только параметры, SQL, реальные кейсы.</p><hr><h2>Глава 1. Конфигурация: postgresql.conf с нуля</h2><h3>Память: самые важные параметры</h3><pre spellcheck="" class="tmiCode language-ini" data-language="ini"><code># 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 &gt; 8 ГБ
huge_pages = on
</code></pre><h3>WAL и checkpoint: баланс между скоростью и надёжностью</h3><pre spellcheck="" class="tmiCode language-ini" data-language="ini"><code># 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
</code></pre><h3>Параллелизм (PostgreSQL 16+)</h3><pre spellcheck="" class="tmiCode language-ini" data-language="ini"><code># 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
</code></pre><h3>Соединения</h3><pre spellcheck="" class="tmiCode language-ini" data-language="ini"><code># max_connections — ОСТОРОЖНО! Каждое соединение ≈ 5-10 МБ памяти.
# При PgBouncer: достаточно 100-200 серверных соединений.
# Без пула: реальное число ≤ 200-300
max_connections = 200

# superuser_reserved_connections — резерв для DBA
superuser_reserved_connections = 5
</code></pre><h3>Планировщик: тонкая настройка</h3><pre spellcheck="" class="tmiCode language-ini" data-language="ini"><code># 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
</code></pre><hr><h2>Глава 2. Индексная стратегия</h2><p>Правильные индексы — половина успеха. Неправильные — гарантированный bloat и тормоза на INSERT/UPDATE.</p><h3>Типы индексов: когда что использовать</h3><p><strong>B-Tree</strong> — дефолт, для равенства и диапазонов:</p><pre spellcheck="" class="tmiCode language-sql" data-language="SQL"><code>-- Стандартный случай
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);
</code></pre><p><strong>GIN</strong> — для массивов, JSONB, полнотекстового поиска:</p><pre spellcheck="" class="tmiCode language-sql" data-language="SQL"><code>-- 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);
</code></pre><p><strong>BRIN</strong> — для очень больших таблиц с естественной сортировкой:</p><pre spellcheck="" class="tmiCode language-sql" data-language="SQL"><code>-- Для таблиц логов, временных рядов — экономия места 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);
</code></pre><p><strong>Hash</strong> — только для точного равенства, быстрее B-Tree:</p><pre spellcheck="" class="tmiCode language-sql" data-language="SQL"><code>CREATE INDEX idx_sessions_token ON sessions
USING HASH(session_token);
</code></pre><h3>Найти неиспользуемые и дублирующие индексы</h3><pre spellcheck="" class="tmiCode language-sql" data-language="SQL"><code>-- Неиспользуемые индексы (кандидаты на удаление)
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(*) &gt; 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;
</code></pre><hr><h2>Глава 3. EXPLAIN ANALYZE: читаем план запроса как профессионал</h2><p><code>EXPLAIN ANALYZE</code> — главный инструмент оптимизации. Без него — гадание на кофейной гуще.</p><pre spellcheck="" class="tmiCode language-sql" data-language="SQL"><code>-- Всегда используйте все опции
EXPLAIN (
    ANALYZE,           -- Реально выполнить и показать время
    BUFFERS,           -- Показать попадания/промахи кэша
    FORMAT TEXT,       -- или JSON для авто-анализа
    TIMING ON,         -- Время каждого узла
    SETTINGS ON,       -- Показать изменённые параметры
    WAL ON             -- PostgreSQL 13+: WAL активность
)
SELECT ...;
</code></pre><h3>Анатомия плана: на что смотреть</h3><pre spellcheck="" class="tmiCode language-sql" data-language="SQL"><code>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 &gt; 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 &gt; 0 = данных нет в кэше
--   Hash Cond: (o.user_id = u.id)
--   -&gt;  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 &gt; (now() - '7 days'::interval))
--         Filter: (status = 'completed')
--         Rows Removed by Filter: 4521   ← КРАСНЫЙ ФЛАГ: фильтруем 4521 строк!
--         Heap Blocks: exact=1823
--         Buffers: shared hit=123 read=1823
--         -&gt;  Bitmap Index Scan on idx_orders_created_at
--               Index Cond: (created_at &gt; (now() - '7 days'::interval))
--   -&gt;  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
--         -&gt;  Seq Scan on users u  (cost=0.00..890.00 rows=21000 width=24)
-- Planning Time: 1.234 ms
-- Execution Time: 190.123 ms   ← Реальное время!
</code></pre><p><strong>Красные флаги в плане:</strong></p><div class="tmiRichText__table-wrapper"><table style="min-width: 60px;"><colgroup><col style="min-width:20px;"><col style="min-width:20px;"><col style="min-width:20px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>Признак</p></th><th colspan="1" rowspan="1"><p>Проблема</p></th><th colspan="1" rowspan="1"><p>Решение</p></th></tr><tr><td colspan="1" rowspan="1"><p><code>Rows Removed by Filter</code> &gt;&gt; возвращаемых строк</p></td><td colspan="1" rowspan="1"><p>Индекс не покрывает все условия</p></td><td colspan="1" rowspan="1"><p>Добавить колонку status в индекс</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>actual rows</code> &gt;&gt; <code>estimated rows</code> (×10+)</p></td><td colspan="1" rowspan="1"><p>Устаревшая статистика</p></td><td colspan="1" rowspan="1"><p><code>ANALYZE table</code> или повысить <code>default_statistics_target</code></p></td></tr><tr><td colspan="1" rowspan="1"><p><code>Seq Scan</code> на большой таблице</p></td><td colspan="1" rowspan="1"><p>Нет подходящего индекса</p></td><td colspan="1" rowspan="1"><p>Создать индекс</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>Batches: N</code> (N &gt; 1) в Hash Join</p></td><td colspan="1" rowspan="1"><p>Хэш-таблица не помещается в work_mem</p></td><td colspan="1" rowspan="1"><p>Увеличить work_mem или оптимизировать запрос</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>loops=N</code> при N×cost = огромно</p></td><td colspan="1" rowspan="1"><p>Вложенный цикл на большом наборе</p></td><td colspan="1" rowspan="1"><p>Рассмотреть Hash Join / Merge Join</p></td></tr><tr><td colspan="1" rowspan="1"><p><code>shared read</code> &gt;&gt; <code>shared hit</code></p></td><td colspan="1" rowspan="1"><p>Данные не в кэше</p></td><td colspan="1" rowspan="1"><p>Увеличить shared_buffers или прогреть кэш</p></td></tr></tbody></table></div><h3>Автоматический поиск медленных запросов</h3><pre spellcheck="" class="tmiCode language-sql" data-language="SQL"><code>-- 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 &gt; 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 &gt; 5
  AND mean_exec_time &gt; 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 &gt; 10
  AND (shared_blks_hit + shared_blks_read) &gt; 0
  AND shared_blks_read &gt; shared_blks_hit  -- Больше промахов чем попаданий
ORDER BY shared_blks_read DESC
LIMIT 20;
</code></pre><hr><h2>Глава 4. Autovacuum: настройка, а не молитва</h2><p>Autovacuum — не враг, а друг. Но дефолтные настройки рассчитаны на небольшие таблицы. На больших таблицах он либо не успевает, либо тормозит рабочую нагрузку.</p><h3>Понять текущее состояние vacuum</h3><pre spellcheck="" class="tmiCode language-sql" data-language="SQL"><code>-- Таблицы с наибольшим 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 &gt; 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;
</code></pre><h3>Оптимальная настройка autovacuum</h3><pre spellcheck="" class="tmiCode language-ini" data-language="ini"><code># 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 &gt; 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 — МНОГО
</code></pre><h3>Настройка per-table (лучше глобальных для горячих таблиц):</h3><pre spellcheck="" class="tmiCode language-sql" data-language="SQL"><code>-- Для высокоактивных таблиц: 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');
</code></pre><h3>Обнаружение table bloat (раздутых таблиц)</h3><pre spellcheck="" class="tmiCode language-sql" data-language="SQL"><code>-- Скрипт оценки 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 &gt; 0 AND NOT att.attisdropped
    GROUP BY 1
),
rows_estimate AS (
    SELECT c.oid,
           CASE WHEN c.reltuples &lt; 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 &gt; 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
</code></pre><hr><h2>Глава 5. Connection Pooling с PgBouncer</h2><p>Каждое соединение с PostgreSQL — это отдельный процесс (~5 МБ памяти + overhead планировщика). 1000 соединений = 5 ГБ памяти только на процессы. PgBouncer решает эту проблему.</p><h3>Режимы PgBouncer</h3><div class="tmiRichText__table-wrapper"><table style="min-width: 80px;"><colgroup><col style="min-width:20px;"><col style="min-width:20px;"><col style="min-width:20px;"><col style="min-width:20px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>Режим</p></th><th colspan="1" rowspan="1"><p>Как работает</p></th><th colspan="1" rowspan="1"><p>Подходит для</p></th><th colspan="1" rowspan="1"><p>Ограничения</p></th></tr><tr><td colspan="1" rowspan="1"><p><strong>session</strong></p></td><td colspan="1" rowspan="1"><p>1 клиент = 1 серверное соединение на всю сессию</p></td><td colspan="1" rowspan="1"><p>Совместимость</p></td><td colspan="1" rowspan="1"><p>Нет экономии</p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>transaction</strong></p></td><td colspan="1" rowspan="1"><p>Серверное соединение занято только на время транзакции</p></td><td colspan="1" rowspan="1"><p>OLTP, большинство приложений</p></td><td colspan="1" rowspan="1"><p><code>SET</code>, <code>LISTEN</code>, prepared statements</p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>statement</strong></p></td><td colspan="1" rowspan="1"><p>Одно серверное соединение на один SQL-оператор</p></td><td colspan="1" rowspan="1"><p>Агрессивная экономия</p></td><td colspan="1" rowspan="1"><p>Нет транзакций!</p></td></tr></tbody></table></div><h3>Конфигурация PgBouncer</h3><pre spellcheck="" class="tmiCode language-ini" data-language="ini"><code># /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
</code></pre><h3>Мониторинг PgBouncer</h3><pre spellcheck="" class="tmiCode language-sql" data-language="SQL"><code>-- Подключиться к 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;
</code></pre><h3>Интеграция с PostgreSQL 17: встроенный connection shard</h3><p>PostgreSQL 17 улучшил <code>max_connections</code> по производительности и добавил механизм <code>connection_obeys_lc_messages</code> — мелочь, но полезная. Работа над встроенным пулингом (<code>connection pooling</code>) ведётся активно, следите за PostgreSQL 18.</p><hr><h2>Глава 6. Партиционирование: когда таблица растёт до сотен ГБ</h2><p>Партиционирование делит одну логическую таблицу на несколько физических. PostgreSQL 16/17 значительно улучшили работу с партициями: умный pruning, параллельные операции, partition-wise joins.</p><h3>RANGE партиционирование (самое частое — по дате)</h3><pre spellcheck="" class="tmiCode language-sql" data-language="SQL"><code>-- Создание партиционированной таблицы
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);
-- Автоматически создаст индекс на каждой партиции!
</code></pre><h3>Автоматическое создание партиций (pg_partman)</h3><p>Ручное создание партиций — путь к ошибкам. Используйте pg_partman:</p><pre spellcheck="" class="tmiCode language-sql" data-language="SQL"><code>-- Установка pg_partman
-- Добавить в postgresql.conf: shared_preload_libraries = 'pg_partman_bgw'

-- Настройка автоматического управления партициями
SELECT partman.create_parent(
    p_parent_table   =&gt; 'public.events',
    p_control        =&gt; 'created_at',
    p_interval       =&gt; 'monthly',       -- или 'weekly', 'daily', 'yearly'
    p_premake        =&gt; 3,               -- Создавать 3 будущих партиции заранее
    p_start_partition =&gt; '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();
</code></pre><h3>Partition Pruning: проверяем что планировщик умный</h3><pre spellcheck="" class="tmiCode language-sql" data-language="SQL"><code>-- Планировщик должен сканировать только нужные партиции
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+
</code></pre><h3>LIST партиционирование (по типу/региону)</h3><pre spellcheck="" class="tmiCode language-sql" data-language="SQL"><code>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;
</code></pre><h3>HASH партиционирование (равномерное распределение)</h3><pre spellcheck="" class="tmiCode language-sql" data-language="SQL"><code>-- Для таблиц без естественного ключа партиционирования
-- Гарантирует примерно равный размер партиций
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
</code></pre><hr><h2>Глава 7. Репликация: PostgreSQL 16/17/18</h2><h3>Физическая репликация (Streaming Replication)</h3><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># На 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
</code></pre><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># На 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=...'
</code></pre><pre spellcheck="" class="tmiCode language-sql" data-language="SQL"><code>-- Мониторинг репликации на 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;
</code></pre><h3>Логическая репликация (PostgreSQL 16/17: серьёзно улучшена)</h3><p>Логическая репликация в PostgreSQL 16 получила:</p><ul><li><p><strong>Двунаправленная (bidirectional) репликация</strong> — обе стороны могут принимать запись</p></li><li><p><strong>Streaming больших транзакций</strong> в реальном времени (без ожидания COMMIT)</p></li><li><p><strong>Параллельное применение</strong> изменений на подписчике</p></li></ul><pre spellcheck="" class="tmiCode language-sql" data-language="SQL"><code>-- На 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! Диск кончится.
-- Если слот не используется &gt; 24ч — проверить и при необходимости удалить:
-- SELECT pg_drop_replication_slot('my_sub_slot');
</code></pre><hr><h2>Глава 8. Мониторинг: что смотреть в продакшене</h2><h3>Системные вьюшки — источник правды</h3><pre spellcheck="" class="tmiCode language-sql" data-language="SQL"><code>-- ===== АКТИВНЫЕ ЗАПРОСЫ И БЛОКИРОВКИ =====

-- Запросы дольше 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) &gt; 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 &lt; 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 (цель: &gt; 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 &gt; 0
ORDER BY heap_blks_read DESC
LIMIT 20;


-- ===== CHECKPOINT СТАТИСТИКА =====
SELECT
    checkpoints_timed,
    checkpoints_req,            -- Если часто req &gt;&gt; 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 &gt; 0 — увеличить bgwriter_lru_maxpages:
-- bgwriter_lru_maxpages = 200     (дефолт 100)
-- bgwriter_lru_multiplier = 4.0   (дефолт 2.0)
-- bgwriter_delay = 50ms           (дефолт 200ms)
</code></pre><h3>Скрипт ежедневного health-check</h3><pre spellcheck="" class="tmiCode language-sql" data-language="SQL"><code>-- Сохранить как 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 (&gt; 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) &gt; 0.1
  AND n_live_tup &gt; 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 (&gt; 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 &gt; 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) &gt; 10 * 1024 * 1024  -- &gt; 10 МБ
ORDER BY pg_relation_size(indexrelid) DESC
LIMIT 10;
</code></pre><hr><h2>Глава 9. Новинки PostgreSQL 16/17/18</h2><h3>PostgreSQL 16 (2023)</h3><p><strong>Логическая репликация от standby</strong> — теперь можно публиковать изменения не только с primary, разгружая мастер.</p><p><strong>Параллельный </strong><code>COPY</code> — загрузка данных через COPY стала параллельной.</p><p><strong>Улучшения планировщика</strong> для <code>GROUP BY</code> с параллелизмом.</p><p><code>pg_stat_io</code> — новая системная вьюшка для детальной статистики I/O:</p><pre spellcheck="" class="tmiCode language-sql" data-language="SQL"><code>-- 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
</code></pre><p><code>COPY FROM ... WHERE</code> — фильтрация при загрузке данных:</p><pre spellcheck="" class="tmiCode language-sql" data-language="SQL"><code>-- Загружаем только нужные строки
COPY orders FROM '/tmp/orders.csv' CSV HEADER
WHERE status = 'completed' AND total &gt; 100;
</code></pre><h3>PostgreSQL 17 (2024)</h3><p><code>MERGE</code><strong> стал намного мощнее</strong> — поддержка <code>RETURNING</code>, <code>DO NOTHING</code>:</p><pre spellcheck="" class="tmiCode language-sql" data-language="SQL"><code>-- 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'
</code></pre><p><strong>Incremental sorting</strong> улучшен — быстрее для DISTINCT и ORDER BY.</p><p><code>pg_stat_statements</code><strong> получил </strong><code>toplevel</code> — разделение top-level vs вложенных запросов.</p><p><strong>Vacuum improvements</strong> — улучшена скорость заморозки (freeze), меньше I/O.</p><p><code>VACUUM (SKIP_DATABASE_STATS)</code> — ускорение вакуума многих мелких таблиц.</p><p><strong>Размер WAL записей уменьшен</strong> — меньше I/O при интенсивной записи.</p><h3>PostgreSQL 18 (2025, в разработке / ранние беты)</h3><p><strong>Встроенный асинхронный I/O (io_method = io_uring)</strong> — огромный прирост для NVMe SSD, особенно при высоком параллелизме:</p><pre spellcheck="" class="tmiCode language-ini" data-language="ini"><code># postgresql.conf (PostgreSQL 18 при использовании Linux io_uring)
io_method = io_uring     # Дефолт: sync; альтернатива: worker
</code></pre><p><strong>Планировщик с ML-hints</strong> — работа над улучшением кардинальности оценок.</p><p><code>GRANT</code><strong>/</strong><code>REVOKE</code><strong> для роли по умолчанию</strong> — улучшена система безопасности.</p><hr><h2>Глава 10. Практические кейсы: реальные проблемы и их решения</h2><h3>Кейс 1: «Запросы стали медленнее после VACUUM»</h3><pre spellcheck="" class="tmiCode language-sql" data-language="SQL"><code>-- Симптом: 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');
</code></pre><h3>Кейс 2: «Диск заполнился WAL файлами»</h3><pre spellcheck="" class="tmiCode language-sql" data-language="SQL"><code>-- Причина 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 если репликация живая
</code></pre><h3>Кейс 3: «Connection pool переполнен, приложение не может подключиться»</h3><pre spellcheck="" class="tmiCode language-sql" data-language="SQL"><code>-- Диагноз: смотрим 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 &gt; 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 &gt; INTERVAL '10 minutes';

-- Постоянное решение: idle_in_transaction_session_timeout
-- postgresql.conf:
-- idle_in_transaction_session_timeout = 5min
-- idle_session_timeout = 30min  (PG 14+)
</code></pre><h3>Кейс 4: «Таблица растёт несмотря на DELETE»</h3><pre spellcheck="" class="tmiCode language-sql" data-language="SQL"><code>-- 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;  -- Мгновенное освобождение места!
</code></pre><hr><h2>Заключение: чеклист production PostgreSQL</h2><pre spellcheck="" class="tmiCode language-plaintext" data-language="Простой текст"><code>КОНФИГУРАЦИЯ
□ 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 &gt; 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 &lt; 95%
□ Алерт на replication lag &gt; 60s
□ Алерт на bloat &gt; 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 для аудита
</code></pre><p>PostgreSQL — невероятно мощная система, которая «из коробки» даёт примерно 20% своего потенциала. Правильная конфигурация, индексная стратегия и мониторинг превращают её в продукт, который выдерживает тысячи транзакций в секунду на десятках терабайт данных — без дорогостоящих «облачных» альтернатив.</p>]]></description><guid isPermaLink="false">155</guid><pubDate>Sat, 21 Mar 2026 21:49:53 +0000</pubDate></item><item><title>&#x422;&#x44E;&#x43D;&#x438;&#x43D;&#x433; &#x43F;&#x440;&#x43E;&#x438;&#x437;&#x432;&#x43E;&#x434;&#x438;&#x442;&#x435;&#x43B;&#x44C;&#x43D;&#x43E;&#x441;&#x442;&#x438; Nginx &#x432; &#x43F;&#x440;&#x43E;&#x434;&#x430;&#x43A;&#x448;&#x435;&#x43D;&#x435;: &#x43F;&#x43E;&#x43B;&#x43D;&#x43E;&#x435; &#x440;&#x443;&#x43A;&#x43E;&#x432;&#x43E;&#x434;&#x441;&#x442;&#x432;&#x43E;</title><link>https://ithub.uno/statiarticles/9_infrastructure/%D1%82%D1%8E%D0%BD%D0%B8%D0%BD%D0%B3-%D0%BF%D1%80%D0%BE%D0%B8%D0%B7%D0%B2%D0%BE%D0%B4%D0%B8%D1%82%D0%B5%D0%BB%D1%8C%D0%BD%D0%BE%D1%81%D1%82%D0%B8-nginx-%D0%B2-%D0%BF%D1%80%D0%BE%D0%B4%D0%B0%D0%BA%D1%88%D0%B5%D0%BD%D0%B5-%D0%BF%D0%BE%D0%BB%D0%BD%D0%BE%D0%B5-%D1%80%D1%83%D0%BA%D0%BE%D0%B2%D0%BE%D0%B4%D1%81%D1%82%D0%B2%D0%BE-r158/</link><description><![CDATA[
<p><img src="https://ithub.uno/uploads/tmi_files_2026_03/NGINX-sendfile-tcp_nopush-and-tcp_nodelay-Explained-ai-1769550907-1024x683.jpg.b2865b381fc6b3ac52ed6188d0ac35c4.jpg" /></p>
<h2>С чего начинается тюнинг</h2><p>Nginx в дефолтной конфигурации — это как спортивный автомобиль с заводскими настройками для езды по бездорожью: едет, но не так быстро, как мог бы. Хорошая новость: большинство важных оптимизаций достигается правкой конфига, а не покупкой более мощного железа.</p><p>Плохая новость: многие "гайды по тюнингу" в интернете — копипаста десятилетней давности, без понимания что и зачем. Параметры для Nginx 1.8 на 2-ядерном сервере копируют на 32-ядерный продакшен под highload — и удивляются что не помогает или становится хуже.</p><p>Этот материал — о том, как думать о тюнинге Nginx: что делает каждый параметр, какие компромиссы он несёт, и как проверить что оптимизация действительно работает.</p><p><strong>Версии в статье:</strong> Nginx 1.24+ / 1.25+ (mainline). Большинство конфигов работают с 1.18+.</p><hr><h2>Диагностика перед тюнингом: что измерять</h2><p>Тюнинг без метрик — гадание на кофейной гуще. Сначала измеряем, потом меняем, потом снова измеряем.</p><h3>Текущее состояние Nginx</h3><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># Версия и скомпилированные модули
nginx -V 2&gt;&amp;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
</code></pre><h3>Нагрузочное тестирование</h3><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># 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/
</code></pre><h3>Мониторинг в реальном времени</h3><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># Топ запросов по времени ответа (из 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 &gt; /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
</code></pre><hr><h2>Уровень 1: Системные настройки Linux</h2><p>Nginx ограничен операционной системой. Без правильной настройки Linux все оптимизации Nginx упрутся в системный потолок.</p><h3>Файловые дескрипторы</h3><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># /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'
</code></pre><h3>Сетевой стек (sysctl)</h3><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># /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
</code></pre><h3>Прозрачные hugepages и планировщик I/O</h3><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># Для высоконагруженных серверов — отключить transparent hugepages
# (могут вызывать latency spikes)
echo never &gt; /sys/kernel/mm/transparent_hugepage/enabled
echo never &gt; /sys/kernel/mm/transparent_hugepage/defrag

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

# Планировщик I/O для SSD (none или mq-deadline быстрее cfq)
echo mq-deadline &gt; /sys/block/sda/queue/scheduler
# Проверить: cat /sys/block/sda/queue/scheduler
</code></pre><hr><h2>Уровень 2: Базовый конфиг Nginx — Worker и Events</h2><pre spellcheck="" class="tmiCode language-nginx" data-language="Nginx"><code># /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+
}
</code></pre><hr><h2>Уровень 3: HTTP блок — основные оптимизации</h2><pre spellcheck="" class="tmiCode language-nginx" data-language="Nginx"><code>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;
}
</code></pre><hr><h2>Уровень 4: Gzip и Brotli — сжатие ответов</h2><pre spellcheck="" class="tmiCode language-nginx" data-language="Nginx"><code>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;
}
</code></pre><h3>Предварительное сжатие статики (экономит CPU)</h3><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code>#!/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 &amp;&gt; /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 файлов"
</code></pre><hr><h2>Уровень 5: Кэширование — proxy_cache и FastCGI cache</h2><h3>Proxy Cache (для проксирования на upstream)</h3><pre spellcheck="" class="tmiCode language-nginx" data-language="Nginx"><code>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";
        }
    }
}
</code></pre><h3>FastCGI Cache (для PHP-FPM)</h3><pre spellcheck="" class="tmiCode language-nginx" data-language="Nginx"><code>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;
        }
    }
}
</code></pre><h3>Инвалидация кэша</h3><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># Очистить весь кэш 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       — ответ из кэша </code></pre><p><code><span class="tmiEmoji" title="">✅</span> </code></p><p><code># MISS      — кэш не нашёл, запрос к upstream </code></p><p><code># BYPASS    — кэш пропущен (skip_cache = 1) </code></p><p><code># EXPIRED   — кэш устарел, запрошен свежий ответ </code></p><p><code># STALE     — отдан устаревший кэш (upstream недоступен) </code></p><p><code># UPDATING  — отдан устаревший кэш пока фоновое обновление </code></p><p><code># REVALIDATED — кэш подтверждён (304 от upstream) </code></p><hr><h2>Уровень 6: Upstream Keepalive и балансировка</h2><pre spellcheck="" class="tmiCode language-nginx" data-language="Nginx"><code>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 "";
        }
    }
}
</code></pre><hr><h2>Уровень 7: SSL/TLS — производительность без потери безопасности</h2><pre spellcheck="" class="tmiCode language-nginx" data-language="Nginx"><code>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;
        }
    }
}
</code></pre><h3>Измерение времени TLS handshake</h3><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># Проверка 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&gt;/dev/null | \
    grep -A 17 'OCSP response'
</code></pre><hr><h2>Уровень 8: Rate Limiting — защита и QoS</h2><pre spellcheck="" class="tmiCode language-nginx" data-language="Nginx"><code>http {
    # ===== ЗОНЫ RATE LIMITING =====

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

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

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

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

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

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

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

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

            proxy_pass http://backend;
        }

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

        # ===== ЗАГРУЗКА ФАЙЛОВ =====
        location /upload/ {
            limit_req zone=upload_limit burst=2 nodelay;
            limit_conn conn_limit 5;
            client_max_body_size 100m;
            proxy_pass http://backend;
        }
    }
}
</code></pre><h3>Белые списки для rate limiting</h3><pre spellcheck="" class="tmiCode language-nginx" data-language="Nginx"><code>http {
    # Карта: 0 = применять limit, 1 = пропустить
    geo $limit {
        default         1;       # Ограничивать всех
        10.0.0.0/8      0;       # Доверенная внутренняя сеть — без ограничений
        192.168.0.0/16  0;       # Локальная сеть — без ограничений
        1.2.3.4         0;       # Конкретный IP (мониторинг, партнёры)
    }

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

    limit_req_zone $limit_key zone=api_limit:20m rate=100r/s;
}
</code></pre><hr><h2>Уровень 9: Open File Cache и другие детали</h2><pre spellcheck="" class="tmiCode language-nginx" data-language="Nginx"><code>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 для больших файлов =====
    # Для файлов &gt; 8MB — асинхронный I/O эффективнее
    aio threads;  # AIO через thread pool (Nginx 1.7.11+)
    # или aio on; # POSIX AIO (старый вариант, хуже)

    directio 8m;  # Файлы &gt; 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;
}
</code></pre><hr><h2>Уровень 10: Полный production конфиг сервера</h2><pre spellcheck="" class="tmiCode language-nginx" data-language="Nginx"><code># /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;
    }
}
</code></pre><hr><h2>Диагностика и мониторинг в продакшене</h2><h3>Stub Status модуль</h3><pre spellcheck="" class="tmiCode language-nginx" data-language="Nginx"><code>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;
    }
}
</code></pre><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># Парсинг 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 &gt;&gt; Writing — много keepalive соединений, это нормально
# Если Reading &gt;&gt; 0 постоянно — клиенты медленно отправляют запросы
# Если Writing = worker_processes × worker_connections — всё занято!

# accepts == handled — нет dropped connections. Если differs — проблема!
</code></pre><h3>Анализ логов</h3><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code># Медленные запросы (&gt; 1 секунды)
awk '$NF &gt; 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
</code></pre><hr><h2>Чеклист тюнинга: финальная проверка</h2><pre spellcheck="" class="tmiCode language-bash" data-language="Bash"><code>#!/bin/bash
# Быстрая проверка ключевых параметров

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

# 1. Worker processes
WP=$(nginx -T 2&gt;/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&gt;/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&gt;/dev/null || echo 1)/limits \
     2&gt;/dev/null | grep 'open files' | awk '{print $4}')
echo "File descriptors limit: $FD"

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

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

# 6. SSL session cache
SSL=$(nginx -T 2&gt;/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 &amp;&amp; echo "Config: OK" || echo "Config: ERROR!"

echo ""
echo "=== ТЕКУЩАЯ НАГРУЗКА ==="
curl -s http://127.0.0.1:8080/nginx_status 2&gt;/dev/null || echo "stub_status недоступен"
</code></pre><hr><h2>Типичные ошибки и мифы</h2><p><strong>Миф 1: "worker_processes 4096 увеличит производительность"</strong> Нет. Оптимум — по одному воркеру на ядро. Больше воркеров = больше переключений контекста = хуже.</p><p><strong>Миф 2: "worker_connections 65535 — максимум соединений"</strong> Нет. Это максимум на один воркер. Итого: <code>worker_processes × worker_connections</code>. При 4 воркерах и 10240 соединениях = 40960 одновременных соединений.</p><p><strong>Миф 3: "keepalive_timeout 0 ускорит сервер"</strong> Наоборот. Keepalive экономит TLS handshake и TCP установку соединения. Отключение keepalive нагрузит сервер больше.</p><p><strong>Миф 4: "gzip_comp_level 9 — лучше"</strong> Нет. Разница в размере между уровнями 6 и 9 — 1-3%. Разница в CPU — в 3-5 раз. Используйте gzip_comp_level 6.</p><p><strong>Ошибка: proxy_cache без proxy_cache_use_stale</strong> При недоступности upstream без <code>use_stale</code> клиенты получат 502. С <code>use_stale error timeout</code> — получат устаревший кэш пока upstream восстанавливается. Всегда включайте!</p><p><strong>Ошибка: не настроен upstream keepalive</strong> Без <code>keepalive</code> в блоке upstream каждый запрос создаёт новое TCP соединение к backend. При 1000 RPS — 1000 новых TCP handshake в секунду. С keepalive 64 — переиспользуются 64 соединения.</p><hr><h2>Заключение</h2><p>Тюнинг Nginx — это итеративный процесс. Хороший порядок:</p><ol><li><p><strong>Системный уровень</strong>: sysctl, ulimit, BBR — без этого упрётесь в ОС</p></li><li><p><strong>Workers и events</strong>: <code>worker_processes auto</code>, <code>multi_accept on</code>, <code>epoll</code></p></li><li><p><strong>Буферы и таймауты</strong>: адаптируйте под характер трафика (размер ответов, скорость клиентов)</p></li><li><p><strong>Кэш</strong>: proxy_cache или fastcgi_cache — самый большой прирост производительности</p></li><li><p><strong>Upstream keepalive</strong>: критично для высоких RPS</p></li><li><p><strong>SSL оптимизация</strong>: session cache + stapling + HTTP/2</p></li><li><p><strong>Сжатие</strong>: gzip + brotli_static для предкомпрессии</p></li><li><p><strong>Rate limiting</strong>: защита без ущерба для легитимного трафика</p></li></ol><p>Измеряйте до и после каждого изменения. Доверяйте цифрам, а не интуиции. И помните: лучший тюнинг — тот, который решает вашу конкретную проблему, а не скопированный из статьи.</p>]]></description><guid isPermaLink="false">158</guid><pubDate>Sat, 21 Mar 2026 22:09:46 +0000</pubDate></item></channel></rss>
