Трафик в день запуска выглядит пугающе, а на деле это обычно самый лёгкий трафик, который вам придётся обслуживать: очень много людей просят одни и те же несколько страниц в течение одного часа. Ловушка не в объёме. Она в том, что каждый из этих одинаковых запросов доходит до вашего приложения, приложение идёт в базу данных, а база десять тысяч раз делает одну и ту же работу, чтобы выдать одни и те же байты. Исправьте это, и запуск, для которого понадобился бы тариф вчетверо больше, спокойно проходит на том тарифе, который у вас уже есть.
Последовательность, которая работает, коротка, и порядок в ней важнее любого отдельного шага. Переведите догадку о трафике в запросы в секунду. Прикиньте, сколько одновременных воркеров это означает. Закэшируйте всё, что не зависит от того, кто спрашивает. Найдите один медленный запрос раньше, чем он найдёт вас. Проведите нагрузочный тест. И только потом думайте о тарифе. Большинство сбоев в день запуска - это один запрос без индекса или один незакэшированный шаблон, а не один недостающий гигабайт.
Переведите оценку трафика в запросы в секунду#
«Мы ждём двадцать тысяч посетителей» - это не число, под которое можно рассчитывать ресурсы. Переведите его в запросы в секунду, и оно им станет.
Возьмите ожидаемое число визитов за день, решите, какая доля приходится на самый загруженный час, и предположите, что примерно половина этого часа приходится на самые загруженные десять минут. Это достаточно пессимистично, чтобы быть полезным, и достаточно оптимистично, чтобы не быть глупым.
20,000 visits, 60% in the launch hour = 12,000 visitshalf of those in the busiest 10 minutes = 6,000 visits6,000 / 600 seconds = 10 page views per secondЗатем умножьте на число запросов на один просмотр страницы. Просмотр страницы - это не один запрос: это документ плюс его стили, скрипты, шрифты, изображения и всё, что вы подключили для аналитики. Пятнадцать-сорок дополнительных запросов - обычное дело. При десяти просмотрах в секунду и двадцати дополнительных запросах вы отдаёте 200 запросов в секунду в сумме, из которых десять касаются вашего приложения, а 190 - статические файлы.
Это разделение и есть вся суть. Десять динамических запросов в секунду - пустяк для любого фреймворка на любом тарифе. Двести запросов в секунду за статикой - пустяк для любого веб-сервера. Запуск убивает то, что это разделение неверно: когда изображения ужимаются на лету или «статическая» рекламная страница каждый раз собирается из базы данных.
| Ожидаемых визитов в час пик | Просмотров страниц/с | Динамических запросов/с (без кэша) | Динамических запросов/с (кэш 60 с) |
|---|---|---|---|
| 1,000 | 0.5 | 0.5 | меньше 0.1 |
| 10,000 | 5 | 5 | меньше 0.1 |
| 50,000 | 25 | 25 | меньше 0.1 |
| 200,000 | 100 | 100 | меньше 0.1 |
Последний столбец - не фокус с округлением. Страница, закэшированная на шестьдесят секунд, собирается не чаще раза в минуту, сколько бы людей о ней ни просили. В этом разница между необходимостью в тарифе и необходимостью в директиве.
Сколько воркеров на самом деле нужно#
Формула - закон Литтла, и это единственный кусочек теории очередей, который нужен веб-разработчику:
concurrency = requests per second x average response timeДесять динамических запросов в секунду по 200 мс каждый требуют двух воркеров, обрабатывающих запросы в любой момент. Десять в секунду по 2 секунды каждый - двадцати. Число воркеров определяет время ответа, а не трафик, и поэтому исправить медленный эндпоинт полезнее, чем удвоить тариф.
Добавьте запас, потому что средние значения скрывают хвост. Считайте по 95-му перцентилю времени ответа и прибавьте 50 процентов. В примере выше, если p95 составляет 600 мс, а не 200 мс, вам нужно шесть воркеров, поэтому выделите девять или десять.
Для PHP-FPM это число - pm.max_children, и у него есть ещё и ограничение по памяти:
pm = dynamicpm.max_children = 12pm.start_servers = 4pm.min_spare_servers = 2pm.max_spare_servers = 6pm.max_requests = 500pm.status_path = /fpm-statusЗадайте pm.max_children равным памяти, которую вы можете выделить, делённой на реальный резидентный размер воркера. Измерьте его через ps, а не гадайте: воркер WordPress с типичным набором плагинов занимает 60-120 МБ, поэтому на тарифе с 2 ГБ, где под PHP доступно 1,2 ГБ, получится десять-двенадцать дочерних процессов, а не пятьдесят. Если поставить больше, пропускная способность не вырастет, вы получите своп, а затем остановку из-за нехватки памяти.
pm.max_requests = 500 перезапускает каждый воркер после 500 запросов, что прикрывает медленные утечки в плагинах, которые писали не вы. А pm.status_path стоит включить до запуска: показатель listen queue, который он выдаёт, - самый чистый сигнал того, что воркеры кончились. Всё, что выше нуля и держится долго, означает, что запросы ждут свободный дочерний процесс, а не выполнения вашего кода.
Node и Python - это другие формы той же проблемы. Один процесс Node использует одно ядро, поэтому параллелизм получают, запуская несколько экземпляров за прокси или через cluster. Практическое правило Gunicorn для синхронных воркеров - (2 x cores) + 1, а асинхронный класс воркеров полностью меняет расчёт. В любом случае воркеров больше, чем у вас есть памяти, хуже, чем меньше. Случай PHP подробно разобран в статье сколько RAM нужно WordPress, а потолок, который процесс Node ставит себе сам, - в статье лимиты памяти в Node.
Кэшируйте страницы, которые не меняются#
Рекламная страница, собираемая на каждый запрос, - это одинаковая работа, повторённая тысячи раз ради одинакового результата. Закэшированная хотя бы на шестьдесят секунд, она превращается в одну сборку и тысячи чтений из памяти. Одно это изменение стоит больше, чем любое повышение тарифа, которое вы могли бы купить утром.
Три уровня, от самого дешёвого. Используйте все.
Браузер и CDN - для всего, у чего в имени есть хэш. Сборка, выдающая app.4f9c2b.js, может кэшироваться навсегда, потому что новая сборка даёт новое имя:
Cache-Control: public, max-age=31536000, immutableКороткий кэш прокси для HTML. Десяти секунд достаточно, чтобы сгладить всплеск, и достаточно мало, чтобы устаревание никто не заметил. В nginx:
proxy_cache_path /var/cache/nginx keys_zone=micro:10m max_size=1g inactive=10m;map $http_cookie $bypass_cache { default 0; ~*session|logged_in 1;}location / { proxy_pass http://127.0.0.1:3000; proxy_cache micro; proxy_cache_valid 200 301 302 10s; proxy_cache_lock on; proxy_cache_use_stale updating error timeout http_500 http_502 http_503; proxy_cache_background_update on; proxy_cache_bypass $bypass_cache; proxy_no_cache $bypass_cache; add_header X-Cache-Status $upstream_cache_status;}Две из этих директив решают дело под нагрузкой. proxy_cache_lock on означает, что когда закэшированная страница истекает, один запрос пересоздаёт её, а остальные ждут эту копию, вместо того чтобы тысяча запросов одновременно ринулась на ваше приложение. proxy_cache_use_stale означает, что если приложение упадёт, посетители продолжат получать последнюю рабочую копию, а не 502. Вместе они превращают плохие пять минут в незаметные. Перед запуском посмотрите X-Cache-Status на вкладке сети в браузере: если при каждой перезагрузке там MISS, что-то в вашем ответе мешает кэшированию, и обычно это заголовок Set-Cookie на странице, которой он не нужен.
Кэширование на уровне приложения для дорогих фрагментов. Число товаров, таблица лидеров, список «недавно присоединились». Обычно основной вред наносит горстка запросов, а шестидесятисекундное запоминание убирает их полностью.
Что нельзя кэшировать: всё, что связано с авторизацией. Это нормально, потому что страницы для вошедших пользователей составляют гораздо меньшую долю трафика запуска, чем вы ожидаете. Правило - строго разделять кэшируемое и персонализированное и никогда не помещать персональное приветствие в оболочку страницы, потому что из-за одной строки текста весь документ становится некэшируемым. Семантика заголовков разобрана в статье заголовки HTTP-кэширования простыми словами, включая stale-while-revalidate и то, почему неправильно заданный ETag тихо отключает всё.
Падает база данных#
Когда запуск проваливается, приложение обычно в порядке и просто ждёт. Ресурсы кончились у базы данных, и почти всегда одним из трёх способов.
Запрос без индекса. Быстрый на вашем ноутбуке с 200 строками, катастрофический с 200 000, потому что это последовательное сканирование. Найдите такие до этого дня. В PostgreSQL включите журнал медленных запросов, а затем прочитайте план:
-- postgresql.conflog_min_duration_statement = 200 -- millisecondsshared_preload_libraries = 'pg_stat_statements'SELECT calls, round(mean_exec_time::numeric, 1) AS avg_ms, queryFROM pg_stat_statementsORDER BY mean_exec_time * calls DESCLIMIT 10;Сортируйте по суммарному времени, а не по среднему, потому что запрос, который выполняется десять тысяч раз по 40 мс, вредит больше отчёта, который запускается один раз и работает 4 секунды. Столбец называется mean_exec_time в PostgreSQL 13 и новее и mean_time в более старых. Затем выполните EXPLAIN (ANALYZE, BUFFERS) для худшего нарушителя и поищите последовательное сканирование большой таблицы. Что означает вывод, разобрано в статье как читать EXPLAIN ANALYZE.
Исчерпание соединений. Каждый фреймворк открывает пул, у каждого воркера свой пул, а max_connections конечно. Десять экземпляров приложения с пулом по 20 - это 200 запрошенных соединений при значении по умолчанию, которое часто равно 100, а каждый бэкенд PostgreSQL стоит несколько мегабайт памяти ещё до того, как начинает работать. Симптом - FATAL: sorry, too many clients already и приложение, которое будто зависло. Размер пулов подбирайте под базу, а не под приложение: суммарный размер пулов по всем экземплярам должен уверенно оставаться ниже max_connections, с запасом под вашу собственную сессию psql. Полная история - в статье пулы соединений и лимиты, а настройки, которые важны при 1-8 ГБ, - в статье настройка PostgreSQL на небольшом сервере.
Записи, которые выстраиваются в очередь. Столбец-счётчик, который увеличивает каждый посетитель, строка сессии, обновляемая на каждый запрос, вставка аналитики прямо в цепочке запроса. При тесте с одним пользователем они незаметны, а при тысяче превращаются в конкуренцию за блокировки. Выносите из запроса всё, что не обязано быть синхронным: в очередь, в фоновый воркер или в журнал только для добавления, который вы агрегируете позже. Как сделать это без новой инфраструктуры, разобрано в статье фоновые задачи на небольшом сервере.
Ассеты, изображения и байты, которые никто не закладывает в бюджет#
Самая частая причина медленной страницы запуска - не сервер. Это несжатое изображение-баннер в 4 МБ, отдаваемое телефону в полном разрешении. Двадцать тысяч посетителей на 4 МБ - это 80 ГБ трафика ради одной декоративной фотографии, и каждый из этих посетителей её ждал.
До этого дня:
- Уменьшите изображения до наибольшего размера, в котором они реально отображаются, и отдавайте современные форматы.
- Включите Gzip или Brotli для текстовых ответов. Это одна директива, и она обычно сокращает HTML, CSS и JavaScript на 70-80 процентов.
- Дайте каждому артефакту сборки хэш содержимого в имени файла, чтобы его можно было кэшировать навсегда.
- Держите шрифты на собственном домене с
font-display: swap, а не блокируйте отрисовку из-за стороннего сервиса. - Уберите виджеты аналитики и чата, которыми не пользуетесь. Каждый из них - это DNS-запрос, соединение и скрипт на критическом пути.
Ничто из этого не относится к хостингу, и именно поэтому об этом стоит сказать здесь: повышение тарифа не делает изображение в 4 МБ меньше. Какие из этих показателей хостинг двигает, а какие нет, честно сказано в статье TTFB, Core Web Vitals и хостинг.
Проведите нагрузочный тест заранее#
Тест настоящим инструментом занимает двадцать минут и остаётся единственным способом узнать, сработало ли всё вышесказанное. Тестируйте на копии для staging с данными, похожими на боевые, а не на пустой базе, потому что весь смысл - найти запрос, который вредит только в масштабе.
Для быстрой проверки одного эндпоинта:
$ oha -z 60s -c 50 https://staging.example.com/$ hey -z 60s -c 50 https://staging.example.com/Для реалистичного нарастания стоит потратиться на лишний файл и взять k6:
import http from "k6/http";import { check, sleep } from "k6";export const options = { stages: [ { duration: "1m", target: 50 }, { duration: "3m", target: 200 }, { duration: "1m", target: 0 }, ], thresholds: { http_req_duration: ["p(95)<500"] },};export default function () { const res = http.get("https://staging.example.com/"); check(res, { "status is 200": (r) => r.status === 200 }); sleep(1);}Из результата читайте три вещи, в таком порядке: долю ошибок, 95-й перцентиль длительности и точку, где кривая изгибается. Интересна не максимальная пропускная способность. Интересна одновременность, при которой p95 начинает расти, потому что это и есть ваш реальный потолок, и он обычно далеко ниже точки, где запросы начинают падать. Держать копию для staging ради этого недорого, а как это делать, не путая её с боевой средой, описано в статье staging и production на одном аккаунте.
Подготовьте путь к повышению, а не само повышение#
Покупать тариф вчетверо больше ради дня, который вы не можете предсказать, - значит тратить деньги в обе стороны: вы платите за мощность, которая не понадобилась, а запрос без индекса у вас всё равно остаётся.
Лучше точно знать, что вы сделаете и сколько это займёт. Запишите это до этого дня:
- На какой тариф вы перейдёте и сколько он стоит.
- Сколько времени занимает переход и перезапускает ли он процесс. Смена тарифа не пересобирает сервер, поэтому файлы, слот базы данных и домен остаются ровно на месте.
- Что вы отключите первым, если придётся сбросить нагрузку: эндпоинт поиска, живой счётчик, виджет рекомендаций. Флаг функции, который переключается за десять секунд, ценнее тарифа, который нельзя купить быстрее минуты.
- Как выглядит ваша статическая страница на крайний случай.
error_page 502 503 504 /maintenance.html;с настоящей страницей за ним - лучший исход, чем страница nginx по умолчанию.
Тарифы для приложений и сайтов включают слот обратного прокси, поэтому домен и его сертификат уже направлены на сервер и продлеваются сами; настоящий адрес клиента приходит в X-Forwarded-For, что важно, если вы ограничиваете частоту запросов по IP. В Express это означает app.set("trust proxy", 1) до любого ограничителя частоты, иначе каждый посетитель будет выглядеть как прокси. Остальная работа с заголовками объяснена в статье что делает обратный прокси.
За чем следить в этот день, по порядку#
Откройте это всё до того, как придёт первый посетитель, и читайте сверху вниз, а не смотрите на то, что красивее выглядит.
| Сигнал | Где | Что значит, если он сдвинулся |
|---|---|---|
| Доля ошибок | Журнал приложения, журнал прокси | Единственное число, которое всегда плохо |
| Время ответа p95 | Журнал прокси или ваш APM | Растёт раньше ошибок; ваше раннее предупреждение |
| Насыщение воркеров | pm.status_path, очередь listen | Запросы ждут процесс, а не выполнения кода |
| Соединения с базой | Число из pg_stat_activity | Приближение к max_connections означает зависание следом |
| CPU | График в панели | Упёрся в вашу долю - это медленно, а не сломано |
| Память | График в панели | Тревожен рост нижней границы, а не пика |
| Диск | График в панели | Журналы и загрузки изображений заполняют его быстрее, чем вы ждёте |
CPU на 100 процентах сам по себе не авария. Это жёсткое ограничение до купленной вами доли, поэтому сервер, стоящий там, медленный, а не сломанный, и из-за этого его никогда не приостанавливают. Достижение лимита памяти - другое дело: контейнер останавливается и чисто перезапускается, а не уходит в своп, а для веб-приложения это значит потерянные запросы, находившиеся в обработке. Следите за нижней границей памяти между всплесками, потому что именно это число говорит, растёте ли вы на самом деле или просто заняты. Формы, на которые стоит реагировать, описаны в статье как читать график нагрузки сервера.
FAQ#
Сколько трафика выдержит небольшой тариф?
Больше, чем ожидает большинство, когда страницы кэшируются, и намного меньше, чем ожидают, когда нет. Тариф на 1 ГБ, отдающий рекламную страницу с десятисекундным кэшем, выдержит сотни запросов в секунду. Тот же тариф, собирающий эту страницу из базы на каждый запрос, начнёт задыхаться после двадцати.
Стоит ли повышать тариф перед запуском на всякий случай?
Только если вы что-то измерили. Сначала проведите нагрузочный тест: если p95 остаётся ровным при тройной ожидаемой одновременности, тариф - не ваш риск. Если вы действительно не уверены, а запуск важен, месяц на более высоком тарифе - недорогая страховка, но кэширование всё равно сделайте, иначе просто перенесёте сбой.
Почему сайт замедляется раньше, чем начинает выдавать ошибки?
Потому что запросы стоят в очереди. Когда все воркеры заняты, новые запросы ждут, а не падают, поэтому задержка растёт, а доля ошибок остаётся нулевой. Этот промежуток - ваше окно для предупреждения, и поэтому время ответа p95 - тот сигнал, за которым надо следить, а доля ошибок - сигнал, что вы опоздали.
Нужен ли CDN для запуска?
Не обязателен, но это самая дешёвая мощность, которую можно купить на всплеск, потому что он снимает почти все запросы за статикой до того, как они дойдут до вас. Если ваша аудитория разбросана по континентам, он к тому же убирает кусок задержки, до которого не дотянется никакое повышение тарифа.
Что ломается первым, когда веб-приложению не хватает памяти?
В контейнере, который упёрся в лимит, процесс останавливается и чисто перезапускается, поэтому всё, что было в обработке, теряется, а хранилище сессий в памяти опустошается. Это довод в пользу того, чтобы хранить сессии в базе данных или в подписанном cookie до дня запуска, а не после него.
Можно ли выкатить исправление во время запуска?
Можно, и плавный перезапуск за прокси делает это незаметным, но меняйте что-то одно за раз и держите наготове предыдущую версию, чтобы вернуться. Механика на одной машине описана в статье деплой без простоя на небольшом сервере.




Комментарии
Полностью анонимно: без аккаунта, без почты, без cookie. Мы храним имя, которое вы ввели, текст и время - больше ничего. Количество ссылок ограничено, разметка не отображается.