Нагрузочное тестирование Go до первого найма бэкенд-разработчика
Используйте нагрузочное тестирование Go, чтобы смоделировать реалистичный трафик, измерить p95 задержки, нагрузку на базу, память и ошибки, а затем принять решение о найме.

Десять тысяч активных пользователей в месяц - не требование к ёмкости. Это показатель для биллинга или аналитики. Сервис Go может выдержать гораздо больше, если запросы лёгкие и распределены по месяцу, или упасть при нескольких сотнях пользователей, если каждая сессия запускает медленные запросы, загрузки файлов и обращения к сторонним сервисам.
Полезный вопрос в другом: выполняет ли созданный бэкенд заданную цель по обслуживанию при ожидаемом пиковом трафике, с достаточным запасом на рост и обычный сбой. Ответ можно получить до найма бэкенд-разработчика, но только если нагрузочный тест похож на ваш продукт и одновременно фиксирует, что происходит с API, рантаймом Go и PostgreSQL. Зелёный график средней задержки почти ничего не доказывает.
Именно такой тест я бы потребовал, прежде чем сказать основателю, что созданный Go-бэкенд готов к 10 000 активных пользователей в месяц. Он даёт воспроизводимый результат «пройдено» или «не пройдено», выявляет первое узкое место и отделяет проблему ёмкости от проблемы корректности.
Месячных пользователей нужно перевести в пиковые запросы
Прежде чем выбирать уровень нагрузки, переведите прогноз пользователей в запросы в секунду. Месячная активная аудитория скрывает две переменные, от которых зависит бэкенд: сколько сессий приходит в самое загруженное окно и сколько работы создаёт каждая сессия.
Начните с наблюдаемых данных, если есть закрытая бета. Посчитайте сессии за самые загруженные 15 минут, запросы на сессию и состав маршрутов. Если трафика ещё нет, запишите допущения так, чтобы их мог оспорить любой участник команды. Например, 10 000 активных пользователей создают восемь сессий в месяц, 15 API-запросов на сессию, а 20 процентов дневного трафика приходится на самый загруженный час. Это даёт примерно 8 запросов в секунду в средний загруженный день. Запуски, уведомления, сроки выплаты зарплаты или общий часовой пояс могут сделать реальный пик в несколько раз выше.
Не превращайте эти расчёты в ложную точность. Используйте их, чтобы определить три уровня теста:
- Ожидаемый пик: самая высокая нагрузка, которую вы сейчас прогнозируете.
- Пик роста: вдвое выше ожидаемого пика, если у бизнеса нет более точного прогноза.
- Стрессовый уровень: повышайте трафик, пока не нарушится цель по обслуживанию или не насытится ресурс.
Тесты ожидаемого пика и пика роста отвечают, есть ли запас для планируемого запуска. Стрессовый тест показывает, что сломается первым и как сбой увидят пользователи. Это важно, поскольку сервис, который быстро отклоняет лишнюю работу, проще эксплуатировать, чем сервис, который забирает все соединения с базой и тормозит несвязанные маршруты.
Для проверки ёмкости используйте открытую модель нагрузки. В открытой модели запросы начинаются с фиксированной частотой, даже если предыдущие запросы замедлились. Закрытая модель с фиксированным числом виртуальных пользователей часто скрывает коллапс: более медленные ответы заставляют пользователей отправлять меньше новых запросов, поэтому предлагаемая нагрузка падает именно тогда, когда сервису тяжело. Документация Grafana по k6 показывает это различие через исполнители с интенсивностью поступления и сообщает dropped_iterations, когда генератор не может запустить запланированную работу. Считайте пропущенные итерации сбоем генератора теста, а не успехом сервера.
После прогрева запускайте каждый стабильный уровень минимум на 30 минут. Пятиминутные тесты не замечают смену соединений, циклы сборки мусора, вытеснение кеша, фоновые задачи и постепенный рост памяти. Когда короткий тест пройдёт, добавьте отдельный двухчасовой soak-тест на ожидаемом пике.
Оформите пересчёт в небольшой таблице и не скрывайте единицы измерения. Месячные пользователи, умноженные на сессии на пользователя и запросы на сессию, дают месячные запросы. Делите только после распределения трафика по рабочим дням и самому загруженному часу. Затем добавьте повторные попытки, фоновые задачи, вебхуки и опросы, которых может не быть в пользовательской аналитике. Фронтенд, опрашивающий сервер каждые десять секунд, может создать больше работы для API, чем клики, открывшие страницу.
Отдельно моделируйте всплески и стабильный пик. Вход после уведомления, завершение импорта или повторные попытки клиентов после короткого сбоя могут сконцентрировать работу в одной минуте. Добавьте этап всплеска: быстро достигайте ожидаемой частоты, держите её достаточно долго, чтобы заполнить очереди, и возвращайтесь к норме. Сервис должен восстановиться без растущего бэклога и ручного перезапуска. Зафиксируйте время восстановления как результат. Система может пройти стабильный тест, но оставаться небезопасной, если краткий всплеск оставляет пул или воркеры в зависшем состоянии.
Не умножайте итоговую частоту на произвольный коэффициент запаса и не называйте результат реалистичным. Свяжите запас с бизнес-неопределённостью: ошибкой прогноза, запланированной кампанией, недоступностью одной реплики или временем, нужным для добавления мощности. Проверяйте каждое допущение, на которое собираетесь опираться. Храните таблицу рядом с результатом, потому что успешный прогон теряет смысл, когда никто не помнит, из какого прогноза и предположений о всплесках получилась целевая нагрузка.
Состав трафика должен быть похож на реальную сессию
Реалистичный нагрузочный тест сохраняет частоту маршрутов, размер полезной нагрузки, аутентификацию, распределение данных, паузы между действиями и конкуренцию за записи. Сто запросов в секунду к /health измеряют обработчик проверки состояния, а не приложение.
По возможности составьте распределение по журналам доступа. Группируйте маршруты по бизнес-действию, а не по исходному URL, поскольку /projects/123 и /projects/456 имеют одну форму. Для раннего SaaS правдоподобное распределение может дать 45 процентов чтению списков и деталей, 20 процентов поиску, 15 процентов созданию или обновлению, 10 процентов входу и обновлению токена, ещё 10 процентов экспорту или другой тяжёлой работе. Ваши числа должны исходить из пути пользователя в продукте, а не из этого примера.
Используйте много тестовых аккаунтов и записей. Повторное использование одного аккаунта может создать нереалистично горячий кеш, последовательно выполнять обновления одной строки или включить ограничения частоты, которые настоящий трафик распределил бы. Подготовьте небольших, средних и крупных клиентов. Добавьте отсутствующие записи, некорректный ввод и отказы в авторизации, потому что пути обработки ошибок часто иначе обращаются к базе или выделяют память под тело ответа, чем успешные пути.
Крупные загрузки и долгий экспорт вынесите в отдельный сценарий, если у них другая цель по обслуживанию. Но запускайте их одновременно с обычным трафиком. Иначе тест не заметит инцидент, который увидят пользователи: один класс экспорта занимает пул, пока простая страница настроек ждёт за ним.
В финальном прогоне ёмкости не подменяйте PostgreSQL, объектное хранилище, очереди или внешние сервисы моками. Мок полезен, чтобы изолировать стоимость обработчика, но он убирает зависимости, которые скорее всего определят ёмкость. Направьте тест на staging-стек с такими же размерами инстансов, настройками базы, индексами, лимитами соединений и сетевым путём, как в production. Очищенные данные, похожие на production, лучше тысячи одинаковых тестовых строк.
Не проводите тест через кеш доставки контента, если API обычно не кешируется. И наоборот, оставьте настоящий кеш в цепочке, когда он есть в production. Цель не в том, чтобы бэкенд выглядел загруженным. Цель в том, чтобы воспроизвести работу, которую действительно вызывает запрос пользователя.
Рабочий тест k6 должен выражать контракт
Храните пороги и этапы трафика в системе контроля версий, чтобы прогон теста не превращался в скриншот, который потом трактуют задним числом. k6 считает пороги критериями «пройдено» или «не пройдено» и завершает работу с ненулевым кодом при их нарушении, поэтому результат подходит для проверки релиза.
Следующий шаблон запускает смешанную сессию с заданной интенсивностью, проверяет смысл ответа и задаёт отдельные ограничения задержки для обычного чтения и тяжёлого экспорта. Замените маршруты, полезную нагрузку и цели значениями, согласованными для вашего продукта. Код внутри теста намеренно простой, чтобы неудачная проверка соответствовала действию пользователя.
import http from 'k6/http';
import { check, sleep } from 'k6';
import { Rate } from 'k6/metrics';
const businessErrors = new Rate('business_errors');
export const options = {
scenarios: {
expected_peak: {
executor: 'ramping-arrival-rate',
startRate: 5,
timeUnit: '1s',
preAllocatedVUs: 40,
maxVUs: 200,
stages: [
{ target: 10, duration: '5m' },
{ target: 10, duration: '30m' },
{ target: 20, duration: '10m' },
{ target: 20, duration: '30m' },
],
},
},
thresholds: {
'http_req_duration{name:project_list}': ['p(95)<300'],
'http_req_duration{name:project_create}': ['p(95)<500'],
'http_req_duration{name:export}': ['p(95)<2000'],
http_req_failed: ['rate<0.01'],
business_errors: ['rate<0.005'],
dropped_iterations: ['count==0'],
},
};
export function setup() {
const response = http.post(`${__ENV.BASE_URL}/api/login`, JSON.stringify({
email: __ENV.TEST_EMAIL,
password: __ENV.TEST_PASSWORD,
}), { headers: { 'Content-Type': 'application/json' } });
check(response, { 'login succeeds': r => r.status === 200 });
return { token: response.json('token') };
}
export default function (data) {
const headers = { Authorization: `Bearer ${data.token}`, 'Content-Type': 'application/json' };
const list = http.get(`${__ENV.BASE_URL}/api/projects?limit=25`, {
headers,
tags: { name: 'project_list' },
});
businessErrors.add(!check(list, {
'list status is 200': r => r.status === 200,
'list has items': r => Array.isArray(r.json('items')),
}));
if (Math.random() < 0.25) {
const create = http.post(`${__ENV.BASE_URL}/api/projects`, JSON.stringify({
name: `load-${__VU}-${__ITER}`,
}), { headers, tags: { name: 'project_create' } });
businessErrors.add(!check(create, { 'create status is 201': r => r.status === 201 }));
}
if (Math.random() < 0.03) {
const runExport = http.post(`${__ENV.BASE_URL}/api/exports`, '{}', {
headers,
tags: { name: 'export' },
});
businessErrors.add(!check(runExport, { 'export accepted': r => r.status === 202 }));
}
sleep(Math.random() * 2 + 1);
}
Примерные цели - отправная точка, а не универсальные обещания. Устанавливайте p95 для каждого класса маршрутов по задержке, которую пользователь готов терпеть, и требованиям самого продукта. Не используйте один общий порог 300 мс, чтобы оценивать асинхронный экспорт и запрос автодополнения.
Запускайте скрипт с машины, на которой не работает приложение. Убедитесь, что у генератора есть запас CPU и нет пропущенных итераций. Сохраняйте точный коммит, конфигурацию окружения, идентификатор снимка данных, команду и необработанный вывод теста. Без этого позднее сравнение в основном будет строиться на памяти и оптимизме.
Прежде чем доверять долгому прогону, откалибруйте генератор. Направьте его на маленький обработчик без работы с базой, поднимите запрошенную интенсивность выше запланированного теста и убедитесь, что генератор поддерживает её, не исчерпывая собственные CPU, сокеты или сеть. Когда k6 добавляет виртуальных пользователей или сообщает о пропущенных итерациях, ограничением может быть машина нагрузки. Распределяйте генерацию по нескольким машинам только тогда, когда одной не хватает для создания нужной нагрузки, и синхронизируйте их часы, чтобы серверные и клиентские графики совпадали.
Для каждого прогона обеспечьте спокойную базовую среду. Остановите миграции, импорт данных и несвязанные задачи staging, если эти задачи не запускаются во время реального пика. Затем запланируйте второй тест с включённой реальной фоновой работой. Вместе эти тесты покажут чистую ёмкость API и эксплуатационную ёмкость, которую получат пользователи. Если проходит только тихий прогон, план запуска опирается на фикцию staging.
Сделайте второй скрипт для одного пути пользователя и запустите его с одним виртуальным пользователем до добавления нагрузки. Проверьте каждый ответ, созданную запись и действие по очистке. Так вы поймаете плохой токен, проверку, которая всегда возвращает true, или тестовые данные, конфликтующие после первой итерации. Результат ёмкости бессмыслен, если скрипт тестирует страницу ошибки или снова и снова читает один кешированный объект.
p95 требует контекста на уровне маршрута
Используйте p95 задержки, потому что средние значения скрывают медленное меньшинство, но никогда не смотрите только на p95. При p95 в 800 мс как минимум каждый двадцатый запрос занимает столько времени или больше, из-за чего страница с несколькими запросами может ощущаться стабильно медленной. Процентиль также становится шумным на маршрутах с очень малым числом образцов, поэтому указывайте рядом число запросов.
Записывайте p50, p95, p99, максимум, пропускную способность и долю ошибок для каждого названного бизнес-действия. p50 показывает обычное поведение, p95 служит практическим порогом обслуживания, а p99 показывает хвост, не позволяя одному максимуму доминировать в обсуждении. Разделяйте результаты по коду статуса. Быстрый ответ 500 не должен улучшать картину задержки.
Измеряйте серверную длительность обработчика наряду с наблюдаемой клиентом длительностью. Разница включает установку соединения, прокси, сетевое время и передачу ответа. Если клиентский p95 растёт, а p95 обработчика не меняется, ищите проблему вне обработчика. Если растут оба и увеличивается время ожидания базы, запрос, вероятно, стоит в очереди за соединением или запросом.
Результаты прогрева и стабильного состояния должны оставаться раздельными. В развёрнутом бинарном файле Go компиляция не происходит, но холодные кеши, новые соединения с базой, отложенная инициализация и автомасштабирование могут исказить первые минуты. Пользователи всё равно сталкиваются с холодным поведением, поэтому сохраняйте его отдельным результатом, а не удаляйте.
Средние значения всё ещё полезны для учёта ресурсов. Общее время работы с базой, делённое на число вызовов, может выявить запрос, который умеренно медленный и очень частый. Но оно не заменяет цель по процентилю обслуживания. Здесь часто смешивают задержку и ёмкость: задержка описывает, сколько заняла завершённая работа, а ёмкость - сколько предлагаемой работы сервис выдерживает без роста очередей или ошибок. Хорошая задержка при низкой достигнутой частоте запросов не доказывает ёмкость.
Определите сбой до прогона. Я бы счёл тест запуска неудачным, если любой критический маршрут не достигает цели p95, непредвиденные HTTP-сбои превышают согласованную долю, не проходят бизнес-проверки, пропускаются запланированные итерации или ресурс остаётся насыщенным. Четыре пройденных порога из пяти означают неудачный прогон с полезными диагностическими данными.
Ожидание соединений показывает скрытые очереди базы данных
Добавьте инструменты измерения database/sql до теста, поскольку задержка приложения не скажет, PostgreSQL работает медленно или приложение ждёт доступа к нему. DB.Stats() в Go сообщает OpenConnections, InUse, Idle, WaitCount и WaitDuration, а также счётчики закрытий. Экспортируйте их в систему метрик каждые несколько секунд.
func recordDBStats(ctx context.Context, db *sql.DB, g GaugeSet) {
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
s := db.Stats()
g.Set("db_open_connections", float64(s.OpenConnections))
g.Set("db_in_use_connections", float64(s.InUse))
g.Set("db_idle_connections", float64(s.Idle))
g.Set("db_wait_count_total", float64(s.WaitCount))
g.Set("db_wait_seconds_total", s.WaitDuration.Seconds())
}
}
}
Считайте изменение накопительных счётчиков за стабильное окно. Рост WaitCount означает, что запросам пришлось ждать свободного соединения. Изменение WaitDuration, делённое на изменение WaitCount, даёт среднее ожидание пула за этот интервал. Сопоставьте InUse с MaxOpenConnections: ровная линия на лимите вместе с растущим ожиданием означает насыщение пула.
Не отвечайте на это увеличением SetMaxOpenConns, пока график не станет лучше. Это популярное исправление переносит очередь в PostgreSQL и может увеличить конкуренцию, потребление памяти и задержку запросов. Сначала выясните, почему соединения остаются занятыми: медленные запросы, транзакции, удерживаемые во время сетевых вызовов, построчное чтение или забытые вызовы Rows.Close(). Затем подберите размер пула в рамках бюджета соединений базы для каждой реплики приложения и воркера.
В документации Go сказано, что неположительное значение SetMaxOpenConns оставляет пул неограниченным. Неограниченный пул - опасная настройка production по умолчанию, когда несколько реплик могут открыть соединения одновременно. Укажите явный лимит, осознанно настройте поведение незанятых соединений и время жизни, оставьте базе запас для миграций, администрирования и фоновой работы.
Отдельно отслеживайте длительность транзакций. Обработчик может вернуть ответ за 200 мс, пока отложенная очистка или утёкшая транзакция удерживает соединение гораздо дольше. Метрики пула показывают давление, а трассировки или измерение транзакций выявляют владельца.
Доказательства медленных запросов должны идти из PostgreSQL
Включите pg_stat_statements в тестовом окружении и делайте снимки до и после каждого прогона. Документация PostgreSQL описывает его как инструмент учёта статистики планирования и выполнения нормализованных выражений. Его представление содержит число вызовов, строк, суммарное и среднее время выполнения, активность блоков и временных блоков. Это доказательство гораздо надёжнее догадок на основе запроса, который попался в одной трассировке.
Используйте разницу между снимками, поскольку представление накапливает данные. Сбрасывайте его только в изолированной тестовой базе, потому что сброс уничтожает свидетельства другой работы. Этот запрос находит выражения, которые израсходовали больше всего времени выполнения за чистое тестовое окно:
SELECT
queryid,
calls,
round(total_exec_time::numeric, 1) AS total_ms,
round(mean_exec_time::numeric, 2) AS mean_ms,
rows,
shared_blks_read,
temp_blks_written
FROM pg_stat_statements
WHERE calls > 20
ORDER BY total_exec_time DESC
LIMIT 20;
Суммарное время находит частые запросы, которые доминируют в работе базы. Среднее время находит отдельные медленные выражения. Ни один показатель не даёт p95 запроса, поскольку pg_stat_statements агрегирует вызовы. Когда важна хвостовая задержка одного выражения, используйте трассировки или гистограммы длительности. Здесь тоже часто смешивают разные вещи: медленный запрос может означать высокое среднее время выполнения, большое время в хвосте или огромную суммарную стоимость из-за частоты. Для каждого случая нужно своё исправление.
Для ведущих выражений запустите EXPLAIN (ANALYZE, BUFFERS) с безопасными репрезентативными параметрами вне нагрузочного теста с измерением времени. ANALYZE выполняет выражение, поэтому оборачивайте изменяющие данные выражения в транзакцию с откатом или проверяйте их на одноразовой копии. Ищите оценки, сильно отличающиеся от фактического числа строк, повторяющиеся циклы, последовательное сканирование больших таблиц с избирательным запросом, сортировки со сбросом на диск и множество прочитанных общих блоков.
Созданный код часто формирует паттерн N+1, который выглядит нормально на тестовых данных: получить 25 проектов, затем выполнить по одному запросу владельца и подсчёта для каждого проекта. При десяти запросах в секунду одна эта конечная точка может создать более 500 выражений базы в секунду до запуска любого другого маршрута. Исправлением может быть join, пакетный WHERE id = ANY($1) или заранее вычисленное число. Увеличение лимита пула оставляет лишнюю работу на месте.
Фиксируйте и ожидания блокировок. Быстрый изолированный запрос может остановиться при параллельных обновлениях одного клиента, аккаунта или последовательности. Когда задержка растёт только в сценарии записи, изучайте активные ожидания и границы транзакций вместо рефлекторного добавления индекса.
Память должна стабилизироваться под постоянной нагрузкой
Оценивайте память по форме графика во времени, а не по одному пику. Процесс Go, который растёт при прогреве, а затем колеблется вокруг стабильного уровня, ведёт себя иначе, чем процесс, у которого базовый уровень после сборки мусора продолжает расти весь двухчасовой soak-тест.
Записывайте резидентную память процесса, выделение кучи Go, объекты кучи, число горутин, частоту сборки мусора, время пауз и скорость выделения памяти. Память контейнера важна, потому что операционная система завершает процесс по его лимиту, а не только по размеру кучи Go. Сравнивайте память после сборок мусора при похожей нагрузке. Так вы уберёте большую часть обычной пилообразности и увидите удержание памяти.
Откройте стандартные метрики рантайма Go или защищённую конечную точку профилирования в staging. Снимите профиль кучи в начале и конце soak-теста, затем сравните места удерживаемых выделений через go tool pprof. Снимите и профиль горутин. Растущее число горутин может показать запросы, застрявшие на каналах, тела ответов, которые никто не закрыл, или фоновую работу, запущенную без отмены.
Не задавайте GOMEMLIMIT точно равным лимиту контейнера. Процессу также нужна память для стеков горутин, отображений исполняемого файла, буферов драйвера базы и других выделений вне кучи. Оставьте запас и подтвердите его на самых крупных полезных нагрузках. Тест с крошечными телами JSON почти ничего не говорит о конечной точке, которая читает в память загрузку размером 20 МБ.
Принудительно проверьте неудобные случаи: максимально допустимые тела запросов, большие результаты запросов, отменённых клиентов, таймауты и повторные экспорты. Убедитесь, что память возвращается после завершения работы. Следите и за CPU, потому что активная сборка мусора может удерживать память ниже лимита, разрушая при этом задержку.
Практическое условие прохождения сочетает потолок и тренд. Требуйте, чтобы резидентная память на пике роста оставалась с безопасным запасом ниже лимита развёртывания, а базовый уровень после сборки и число горутин перестали расти во время soak-теста. Универсального безопасного процента нет. Выбирайте запас с учётом поведения платформы при перезапуске, колебаний трафика и возможности другой реплики принять нагрузку после перезапуска.
Сбои должны включать неправильные ответы и поведение при перегрузке
Считайте отдельно ошибки транспорта, HTTP-статуса, таймауты, паники и неправильные ответы. http_req_failed учитывает неудачные HTTP-запросы согласно callback ответа k6, но ответ 200 с пустым списком, повторным списанием или отсутствующей записью тоже означает сбой. Поэтому пример создаёт business_errors из проверок содержимого.
Помечайте ожидаемые отказы, например некорректный ввод или намеренное ограничение частоты, чтобы они не попадали в долю непредвиденных ошибок. Затем проверяйте их контракт: статус верный, тело ограничено, отказ приходит быстро. Система под перегрузкой не должна тратить 30 секунд, прежде чем вернуть 503.
Следите в логах сервера за восстановлением после паник, ошибками контекстного дедлайна, задержками получения соединения, ошибками сериализации PostgreSQL и отменёнными запросами. Группируйте ошибки по стабильной причине, а не по полному сообщению, чтобы идентификаторы не создавали тысячи категорий. Сохраняйте репрезентативные трассировки первого случая и хвоста высокой задержки.
После чистого теста ёмкости проведите один тест деградации. Уменьшите доступное число соединений базы, добавьте контролируемую задержку одной внешней зависимости или перезапустите одну реплику приложения при продолжающемся трафике. Делайте это только в изолированном тестовом окружении. Цель - подтвердить, что таймауты, отмена и проверки состояния сдерживают сбой, а не позволяют очередям занять все ресурсы.
Явно настройте HTTP-сервер. Документация Go по net/http говорит, что нулевые или отрицательные ReadTimeout, WriteTimeout и IdleTimeout могут означать отсутствие таймаута в зависимости от поля. Созданные сервисы часто вызывают http.ListenAndServe с настройками по умолчанию и никогда не принимают это решение. Свой http.Server, дедлайны в области запроса и ограниченный размер тела не дают медленным клиентам и зависшим зависимостям удерживать ресурсы бесконечно.
Проверьте корректность после прогона. Посчитайте созданные записи, проверьте идемпотентность там, где клиенты повторяли попытки, убедитесь, что фоновые задачи завершились один раз, и подтвердите отсутствие частичного состояния после неудачных запросов. Нагрузочные тесты находили в моих проектах больше ошибок с дублированием работы, чем самый тщательный ревью кода.
Решение о найме зависит от первого ограничения
Можно запускаться без бэкенд-разработчика, если система стабильно проходит тесты ожидаемого пика и пика роста, soak-тест достигает стабильного базового уровня памяти, очереди базы остаются под контролем, а команда может объяснить первый сбой в стрессовом тесте. Один удачный зелёный прогон ничего не доказывает. Запустите один и тот же коммит минимум три раза и разберитесь с большой вариативностью.
Для каждого прогона храните краткую запись результата:
- Коммит и окружение, включая размеры реплик и конфигурацию базы данных.
- Масштаб набора данных, состав трафика, интенсивность поступления и длительность теста.
- p95 и p99 маршрутов, достигнутую пропускную способность, бизнес-сбои и пропущенные итерации.
- Пиковые CPU и память, тренд памяти после сборки и тренд горутин.
- Ожидания пула, ведущие SQL по суммарному времени, ожидания блокировок и наблюдаемую точку отказа.
Наймите штатного или контрактного бэкенд-специалиста до запуска, если никто не может объяснить растущее ожидание пула, удерживаемую память, конкуренцию блокировок или непоследовательные записи. Нанимайте, если единственный человек, способный запустить тест, не может безопасно менять созданный код. Это пробел ответственности, а не порог запросов в секунду.
Неудачный тест сам по себе не оправдывает штатный найм. Отсутствующий индекс, N+1-запрос или неограниченный экспорт могут оказаться локальным исправлением. Повторные проблемы в проектировании транзакций, наблюдаемости, отмене и поведении развёртывания указывают на постоянную инженерную работу. Разница в том, нашли ли вы один дефект или обнаружили, что никто не отвечает за поведение системы.
Koder.ai может создать и экспортировать Go-бэкенд, развернуть его и сохранять снимки для отката, но генерация не отменяет планирование ёмкости. Храните скрипт нагрузки и изменения наблюдаемости вместе с исходниками, чтобы каждое значимое изменение бэкенда проходило тот же контракт.
Не обещайте бизнесу, что 10 000 пользователей в месяц безопасны. Обещайте измеренную интенсивность поступления, состав маршрутов, целевую задержку, бюджет ошибок и пределы ресурсов. Когда продукт меняется, меняйте эти входные данные и запускайте тест снова.
FAQ
Справится ли бэкенд на Go с 10 000 активных пользователей в месяц?
Часто да, но месячная активная аудитория не описывает нагрузку на бэкенд. Переведите прогноз в пиковые запросы в секунду и набор маршрутов, затем проверьте эту нагрузку по явным ограничениям задержки, ошибок, базы данных и памяти.
Сколько запросов в секунду соответствует 10 000 пользователям в месяц?
Фиксированного пересчёта нет. Нужны число сессий на пользователя, запросов на сессию, доля трафика в самое загруженное окно и события, из-за которых пользователи приходят одновременно.
Какое значение p95 задержки приемлемо для Go API?
Задавайте цели по действию пользователя, а не по языку или фреймворку. Интерактивному чтению может требоваться несколько сотен миллисекунд, у принятой фоновой задачи может быть другая цель, но команда должна выбрать числа до просмотра результатов теста.
Как долго должен идти нагрузочный тест бэкенда?
После прогрева держите каждый стабильный уровень нагрузки не менее 30 минут, затем проведите более длительный soak-тест на ожидаемом пике. Короткие тесты могут не заметить смену соединений, удерживаемую память, фоновую работу и постепенный рост очередей.
Что использовать для нагрузочного теста: виртуальных пользователей или интенсивность запросов?
Для заявления о ёмкости используйте модель с интенсивностью поступления запросов, поскольку она продолжает подавать работу, когда сервис замедляется. Модель с фиксированным числом виртуальных пользователей может снизить частоту запросов при замедлении и скрыть момент, когда начинают расти очереди.
Как обнаружить исчерпание пула соединений с базой данных в Go?
Экспортируйте DB.Stats() и следите за InUse, OpenConnections, WaitCount и WaitDuration. Рост счётчиков ожидания, когда занятые соединения остаются на настроенном максимуме, показывает, что запросы стоят в очереди к пулу.
Стоит ли увеличивать пул SQL-соединений Go, если запросы ждут?
Не увеличивайте его, пока не поймёте, почему соединения заняты и какой запас у PostgreSQL. Более крупный пул может перенести очередь в базу данных и усилить конкуренцию.
Как найти медленные запросы PostgreSQL во время нагрузочного теста?
Сделайте снимки pg_stat_statements до и после теста, затем ранжируйте разницу по суммарному и среднему времени выполнения. Для хвостовой задержки используйте репрезентативные трассировки или гистограммы, поскольку агрегированное представление не даёт p95 по отдельному запросу.
Как понять, есть ли утечка памяти в сервисе Go?
Проведите стабильный soak-тест и сравнивайте память после сборок мусора при похожей нагрузке. Базовый уровень, который продолжает расти, особенно вместе с числом объектов в куче или горутин, требует сравнения профилей кучи и горутин.
Когда стартапу стоит нанять бэкенд-разработчика?
Нанимайте, когда проблемы с производительностью показывают постоянную потребность в ответственности за проектирование базы данных, наблюдаемость, конкурентный доступ, корректность или эксплуатацию. Одна локальная правка индекса или запроса может не требовать штатной роли, но необъяснимое поведение под нагрузкой требует её.