Системный журнал — практичная база знаний по VPS и Linux
monitoring • 10 минут

Как масштабировать проверки безопасности на VPS без нового железа

Регулярные проверки безопасности на VPS часто выглядят просто: cron или systemd-таймер запускает скрипт, скрипт проверяет DNS-записи, nginx-конфигурацию, открытые порты, SSH, TLS, резервные копии и записывает найденные проблемы в базу. Пока серверов мало, такая схема работает. Когда количество объектов растёт, начинают всплывать типичные проблемы: очередь забивается, API отвечает слишком долго, PostgreSQL захлёбывается от множества коротких запросов, а планировщик создаёт пики нагрузки. В публичном разборе Cloudflare команда Security Insights увеличила пропускную способность сканов более чем в 10 раз и вышла на показатель свыше 120 сканов в секунду без простого добавления железа. Для администратора Linux/VPS из этого кейса важнее не конкретный масштаб, а метод поиска узких мест.

Что именно нужно масштабировать

Любая система регулярных проверок состоит из одних и тех же частей. Планировщик решает, что и когда проверять. Очередь принимает задания и распределяет их между исполнителями. Рабочие процессы выполняют проверки: например, читают DNS, подключаются к API, анализируют nginx-конфигурацию или проверяют настройки firewall. Результаты уходят в API или напрямую в базу данных. Отдельный слой отвечает за хранение, поиск и отображение инсайтов.

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

  • очередь растёт быстрее, чем её разбирают workers;
  • длинные задачи блокируют короткие;
  • API держит соединения к базе слишком долго;
  • база получает сотни тысяч мелких запросов;
  • планировщик запускает проверки пачками;
  • таймауты увеличены, но реальная задержка не устранена.

Практический вывод прост: перед масштабированием инфраструктуры соберите метрики. Нужны не только CPU и RAM, но и lag очереди, количество активных соединений к PostgreSQL, время ответа API, p95/p99 задержки, число повторов задач и распределение заданий по времени. Если вы используете systemd, первые команды для обзора могут быть такими:

systemctl list-timers --all
journalctl -u scanner-worker --since "1 hour ago" | grep -Ei 'timeout|panic|deadline|connection refused'
ss -s
psql -c "SELECT count(*) FROM pg_stat_activity;"

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

Очередь: параллельность не равна бесконечной скорости

Очереди часто воспринимают как обычный список заданий: положили задачу, worker забрал, выполнил. С Kafka всё иначе. Это не просто queue, а потоковая система с партициями. Внутри одной партиции сообщения должны обрабатываться в порядке следования, и в рамках одной consumer group активным потребителем обычно является один consumer на партицию. Поэтому простое добавление workers не всегда ускоряет обработку. Если у вас 30 партиций, то один checker не сможет честно распараллелиться на 100 одинаково полезных процессов.

Отсюда два важных ограничения. Во-первых, медленное сообщение внутри партиции задерживает движение к следующим сообщениям. Во-вторых, количество потребителей ограничено количеством партиций, а добавление партиций увеличивает нагрузку на брокер и усложняет балансировку. На VPS с небольшой нагрузкой это может звучать чрезмерно сложно, но принцип применим к любой очереди: порядок обработки и размер партиций напрямую влияют на параллельность.

Полезная техника — пакетная обработка. Worker забирает не одно сообщение, а небольшую пачку и обрабатывает элементы пачки параллельно. Это снижает накладные расходы на ожидание следующего задания и повышает утилизацию CPU. Но есть цена: если процесс упадёт в середине пачки, часть работы придётся выполнить повторно. Поэтому размер пачки должен быть таким, чтобы ускорять обработку, но не превращать сбой в дорогую повторную операцию.

Разделите быстрые и медленные задачи

Не все проверки одинаковы. Один маленький сайт можно проверить за миллисекунды, а крупный аккаунт с тысячами DNS-записей, несколькими nginx-виртуалами и большим количеством TLS-сертификатов — за минуты. Если такие задачи попадают в одну очередь, короткие проверки начинают ждать длинные. Это называется head-of-line blocking: впереди стоит тяжёлая операция, и всё, что за ней, вынуждено простаивать.

Решение не обязательно должно быть сложным. Можно заранее классифицировать задачи по ожидаемой длительности и направить их в разные потоки обработки. Например, быстрые проверки nginx-конфигурации, SSH-настроек и firewall-правил идут в один worker pool, а тяжёлые проверки с большим числом DNS-записей — в другой. На уровне Linux это могут быть отдельные systemd-сервисы, разные контейнеры или просто разные процессы с собственными лимитами.

Важно не только разделить очереди, но и уметь определять тяжёлые задачи до их выполнения. Признаками могут быть количество зон, число записей, размер конфигурации, количество доменов в сертификате или число правил firewall. Если система заранее понимает, что задача тяжёлая, она не должна заставлять быстрый поток ждать её завершения.

API и база данных: не маскируйте задержку таймаутами

Одна из самых опасных ошибок при масштабировании проверок — увеличивать client-side timeouts. Если API отвечает за 5 секунд вместо 200 миллисекунд, увеличение таймаута до 30 секунд не делает систему быстрее. Оно лишь дольше удерживает соединения, быстрее исчерпывает connection pool и создаёт видимость, что проблема исчезла. На практике очередь начинает расти, workers простаивают, а база получает длинные транзакции.

В разборе Cloudflare была показательная ситуация: основное хранилище находилось в одном регионе, а API работал в двух. Запросы из удалённого региона шли дольше, соединения удерживались, pool быстро заканчивался, и часть процессов начинала упираться в ожидание свободного соединения. Среднее время вызова API в одном месте было порядка 10 мс, а в другом могло доходить почти до 3 секунд. Решение оказалось архитектурным: активный API нужно держать ближе к основной базе данных, а не пытаться победить географию большим таймаутом.

Для обычного VPS это означает следующее:

  • не разносите API и PostgreSQL по удалённым серверам без необходимости;
  • измеряйте не только статус 200, но и время ответа;
  • если API проходит через nginx, смотрите разницу между request_time и upstream_response_time;
  • не завышайте proxy_read_timeout только для того, чтобы скрыть медленную базу;
  • отслеживайте количество активных соединений к PostgreSQL;
  • проверяйте, не держат ли workers соединения открытыми во время долгих проверок.

Если nginx используется как reverse proxy для внутреннего API, задержка на стороне upstream почти всегда важнее, чем задержка на самом proxy. Увеличенный proxy_read_timeout полезен только тогда, когда операция действительно должна длиться долго: например, большой импорт или тяжёлый отчёт. Для массовых security scans это чаще всего симптом неправильного распределения нагрузки.

PostgreSQL: меньше кругов до базы — выше пропускная способность

Каждая найденная проблема должна где-то сохраняться. Если система находит 100 инсайтов и делает 100 отдельных INSERT или UPDATE, база выполняет 100 обменов с приложением. Если таких задач тысячи, задержка становится заметной даже на мощном сервере. Особенно плохо, когда каждый INSERT идёт в отдельной транзакции или каждый конфликт уникальности обрабатывается отдельным запросом.

Типичный антипаттерн выглядит так: worker нашёл список проблем, а затем в цикле записывает каждую проблему отдельным SQL-запросом. Для небольших объёмов это удобно писать, но плохо масштабируется. На больших наборах лучше использовать пакетную загрузку, multi-row INSERT, временные таблицы или COPY. Однако COPY в временную таблицу тоже не всегда идеален: при частом использовании он может создавать дополнительную нагрузку на системные таблицы PostgreSQL.

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

EXPLAIN (ANALYZE, BUFFERS) SELECT count(*) FROM pg_stat_activity;

Этот пример не решает задачу хранения, но показывает привычку: перед оптимизацией SQL нужно видеть план выполнения и реальное время. Для production-таблиц с результатами сканов полезны индексы по account_id, zone_id, типу проверки, времени обнаружения и статусу. Без индексов даже нормальная вставка может сопровождаться медленным поиском конфликтов и долгими SELECT на панели мониторинга.

Планировщик: равномерность важнее мгновенного старта

Самая тихая проблема регулярных проверок — планировщик. Он может месяцами работать нормально, а потом создать шторм задач. Например, все объекты были добавлены в один день, у них одинаковое поле last_scheduled_at, и при изменении частоты сканирования половина базы внезапно становится due for scan. В результате очередь получает сотни тысяч заданий за короткое время.

Если очередь имеет временное хранение сообщений, такой шторм особенно опасен: задачи могут накопиться быстрее, чем их обработают, а старые сообщения начнут удаляться по retention policy. Поэтому планировщик должен не просто выбирать всё, что пора проверить, а распределять работу во времени.

Что помогает на практике:

  • планировать проверки зон или объектов независимо друг от друга, а не только аккаунтами;
  • рандомизировать начальное время следующей проверки;
  • не запускать все cron-задачи в одно и то же время;
  • использовать RandomizedDelaySec для systemd timers;
  • ограничивать скорость постановки заданий в очередь;
  • пересчитывать лимит скорости при росте числа объектов;
  • не допускать каскада задач от одного крупного клиента или одного большого аккаунта.

Адаптивный rate limit полезен не только для огромных платформ. Если у вас 500 VPS и вы хотите проверять каждую раз в неделю, простой расчёт показывает, сколько задач в секунду нужно ставить в очередь, чтобы успеть за семь дней. Если завтра объектов станет 1000, старый лимит уже не подойдёт. Поэтому лимит лучше считать от текущего количества объектов и выбранной частоты, а не хранить как магическую константу.

Метрики, без которых масштабирование превращается в гадание

Хорошая система проверок должна отвечать не на вопрос «сервер жив?», а на вопрос «где именно застревает безопасность?». Минимальный набор метрик для Linux/VPS-окружения:

  • количество заданий в очереди;
  • возраст самого старого необработанного задания;
  • скорость постановки заданий в очередь;
  • скорость успешной обработки;
  • число повторных запусков;
  • количество таймаутов API;
  • p95/p99 времени ответа API;
  • число активных соединений к PostgreSQL;
  • время самых долгих SQL-запросов;
  • распределение задач по часам;
  • доля быстрых и медленных проверок;
  • количество найденных проблем по типам.

Отдельно стоит отслеживать не только ошибки, но и успешные проверки. Если скорость обработки внезапно выросла, это не всегда хорошо: возможно, часть проверок стала пропускаться из-за неправильной логики или слишком агрессивного rate limit. Если количество найденных проблем резко упало, возможно, сломался конкретный checker.

Что делать на небольшом VPS

Не каждая команда нуждается в Kafka и микросервисах. Для одного сервера или небольшой группы VPS можно начать проще: systemd timers для расписания, PostgreSQL-таблица заданий для очереди, отдельный worker-процесс для проверок и nginx для доступа к внутреннему API. Принципы останутся теми же.

Начните с трёх изменений:

  • перестаньте запускать все проверки одновременно;
  • заменяйте множество мелких записей в базу пакетными операциями;
  • отдельно учитывайте тяжёлые объекты, чтобы они не блокировали лёгкие.

Затем добавьте метрики. Без них вы будете видеть только последствия: сайт с панелью открывается медленно, cron иногда не успевает, в логах появляются таймауты. С метриками вы увидите причину: например, очередь растёт по вторникам после массового запуска, API держит 200 соединений, а 80% времени уходит на ожидание PostgreSQL.

Главный урок

Масштабирование регулярных проверок безопасности — это не только вопрос мощности сервера. Чаще всего узкое место находится в порядке обработки, распределении задач, количестве запросов к базе и задержках между компонентами. Добавление железа может помочь, но сначала стоит разобрать код, SQL, логи и метрики. Увеличение таймаутов, рост числа партиций или запуск новых workers без понимания системы могут временно скрыть проблему, но не сделают её устойчивой.

Хорошая архитектура проверок должна быть предсказуемой: задания поступают равномерно, быстрые проверки не ждут медленные, база получает данные пакетами, API не зависит от удалённой задержки, а планировщик умеет адаптироваться к росту числа объектов. Именно это позволяет ускорить security scans без постоянного увеличения инфраструктуры и делает мониторинг действительно полезным инструментом, а не источником новых аварий.