8 lines
19 KiB
JSON
8 lines
19 KiB
JSON
{
|
||
"index": 222,
|
||
"slug": "editorial-2021-11-practice-load-testing",
|
||
"title": "Нагрузочное тестирование: как связать профиль, измерение и решение",
|
||
"excerpt": "Нагрузочный тест даёт полезный ответ только тогда, когда команда заранее описала сценарий, среду, данные и критерий успеха. Разбираем практическую схему, пример на k6 и отрицательный путь, который не позволяет выдать учебный счётчик за capacity.",
|
||
"contentHtml": "<p>Проблема: после запуска команда видит растущую задержку и ошибки, но не может сказать, какой сценарий их вызвал; цена ошибки — неверный фикс, повторный прогон на другой среде и пропущенный отказ в production.</p>\n<p>Нагрузочное тестирование проверяет не абстрактное «быстрее или медленнее», а поведение системы при заданном потоке работы. Поэтому сначала описывают workload, затем выбирают инструмент и метрики. Workload отвечает на четыре вопроса: какой запрос выполняется, с какими данными, с какой моделью поступления и до какой границы продолжается проверка. Если хотя бы один ответ потерян, итоговый RPS мало что объясняет.</p>\n<p>Тезис простой: нагрузочный тест становится инженерным доказательством только тогда, когда его вход, наблюдение и решение связаны одним контрактом. Число запросов без контекста не показывает ни причину задержки, ни безопасный следующий шаг.</p>\n<h2>Механизм: профиль меняет условия проверки</h2>\n<p>Запросы создают конкуренцию за ограниченные ресурсы. Это могут быть соединения к базе, worker-потоки, CPU, память, дисковый ввод-вывод или лимит внешнего API. Пока запас ресурса велик, время ответа растёт слабо. Когда очередь становится длиннее, система начинает накапливать работу. Затем появляются таймауты, ретраи и ошибки. Ретраи увеличивают поток и могут ускорить отказ.</p>\n<p>Один итоговый счётчик скрывает переходы между этими состояниями. Профиль сохраняет их: короткий прогрев проверяет сам сценарий, стабильный участок даёт повторяемую базу, ступенька проверяет заранее выбранную границу, а восстановление показывает, уходит ли очередь после снижения входа. Названия сегментов не важны сами по себе. Важно объяснить роль каждого сегмента и его условия.</p>\n<p>Нужно также различать закрытую и открытую модели. В закрытой модели следующий шаг обычно начинается после завершения предыдущего. Замедление системы уменьшает фактический поток. В открытой модели новые итерации стартуют по расписанию независимо от ответа системы. Замедление увеличивает число одновременно работающих итераций. Эти модели отвечают на разные вопросы. Их нельзя сравнивать только по числу виртуальных пользователей.</p>\n<table><caption>Что должно быть видно в нагрузочном тесте</caption><thead><tr><th>Поле</th><th>Пример</th><th>Зачем оно нужно</th></tr></thead><tbody><tr><td>Сценарий</td><td><code>GET /catalog/items</code>, доля чтений 80%</td><td>Показывает, какую работу моделирует тест</td></tr><tr><td>Данные</td><td>Версия набора, размер ответа, правило очистки</td><td>Отделяет эффект данных от эффекта кода</td></tr><tr><td>Модель входа</td><td>30 итераций в секунду в течение 5 минут</td><td>Объясняет, что означает скорость</td></tr><tr><td>Наблюдение</td><td>p95, p99, доля ошибок, насыщение CPU</td><td>Связывает симптом с ресурсом</td></tr><tr><td>Порог</td><td>p95 < 300 мс, ошибки < 1%</td><td>Превращает график в проверяемое решение</td></tr></tbody></table>\n<h2>Пример: сначала договор, потом запуск</h2>\n<p>Допустим, нужно проверить чтение каталога перед изменением индекса. Учебный сценарий ниже показывает форму проверки. Он не обращается к реальному сервису и не подтверждает его производительность. Адрес, длительность, скорость и пороги здесь демонстрационные. В рабочем тесте их заменяют условиями конкретного сервиса.</p>\n<pre><code>import http from 'k6/http';\nimport { check, sleep } from 'k6';\n\nexport const options = {\n scenarios: {\n catalog_read: {\n executor: 'ramping-arrival-rate',\n startRate: 5,\n timeUnit: '1s',\n preAllocatedVUs: 10,\n maxVUs: 50,\n stages: [\n { target: 5, duration: '30s' },\n { target: 30, duration: '2m' },\n { target: 5, duration: '30s' }\n ]\n }\n },\n thresholds: {\n http_req_failed: ['rate<0.01'],\n http_req_duration: ['p(95)<300']\n }\n};\n\nexport default function () {\n const response = http.get(`${__ENV.BASE_URL}/catalog/items`);\n check(response, {\n 'status is 200': (r) => r.status === 200,\n 'body is not empty': (r) => r.body.length > 0\n });\n sleep(1);\n}</code></pre>\n<p>В этом примере <code>ramping-arrival-rate</code> задаёт изменение скорости старта итераций, а не обещает, что сервер завершит их с той же скоростью. Параметр <code>maxVUs</code> ограничивает ресурс самого генератора. Если виртуальных пользователей не хватает, тест может не поддержать заданный arrival rate. Это отдельный сигнал о конфигурации теста, а не доказательство отказа приложения.</p>\n<p>Порог тоже не равен факту. Запись <code>p(95)<300</code> задаёт правило pass/fail для конкретной метрики. Она не говорит, почему порог нарушен. Причину ищут по временной связи с очередями, CPU, базой, внешними вызовами и лимитами. Если порог не был согласован до запуска, команда легко выбирает удобное объяснение уже после графика.</p>\n<h2>Как читать симптом</h2>\n<table><caption>Симптом → причина → проверка → действие</caption><thead><tr><th>Симптом</th><th>Возможная причина</th><th>Проверка</th><th>Действие</th></tr></thead><tbody><tr><td>p95 растёт, RPS почти не меняется</td><td>Очередь внутри сервиса или зависимость отвечает медленнее</td><td>Сопоставить latency с queue depth, CPU, pool и временем внешних вызовов</td><td>Найти первую насыщенную очередь; не увеличивать таймаут вслепую</td></tr><tr><td>Ошибки появляются только на ступени</td><td>Пересечён лимит соединений, workers или внешнего API</td><td>Проверить лимиты и распределение ошибок по endpoint и коду</td><td>Изменить лимит или сценарий только после подтверждения владельца ресурса</td></tr><tr><td>График выглядит лучше после увеличения VU генератора</td><td>Генератор был bottleneck и не подавал заявленный поток</td><td>Сравнить запланированный и фактически начатый arrival rate</td><td>Увеличить ресурс генератора и повторить тот же сценарий</td></tr><tr><td>Тест стабилен, но production падает</td><td>Не совпали данные, зависимости, размер ответа или модель входа</td><td>Сравнить версии среды и workload по карточке запуска</td><td>Закрыть различия; не объявлять стенд доказательством production capacity</td></tr><tr><td>После снижения входа ошибки продолжаются</td><td>Очередь не успевает опустеть или ретраи продолжают поток</td><td>Наблюдать recovery, backlog и число повторных попыток</td><td>Остановить тест по лимиту и разобрать остаточную работу отдельно</td></tr></tbody></table>\n<h2>Иллюстрация профиля</h2>\n<figure><img src=\"/assets/editorial/2021/load-test-profile-2021.svg\" alt=\"Профиль нагрузочного теста из сегментов прогрева, стабильной нагрузки, ступеньки и восстановления\"><figcaption>Учебная схема показывает порядок сегментов. Красная граница в ступеньке — условие примера, а не найденный предел реальной системы. Схема не содержит измерений RPS, latency или throughput.</figcaption></figure>\n<p>Иллюстрация полезна как проверка полноты. Если на ней нельзя подписать источник входа, границу сегмента и условие остановки, этих данных, скорее всего, нет и в описании запуска. Сегменты должны быть достаточно длинными для выбранного наблюдения. Короткая ступенька может показать только разогрев клиента, кэша или соединительного пула.</p>\n<h2>Порядок действий</h2>\n<ol><li>Выберите один пользовательский сценарий и запишите метод, путь, доли операций и ожидаемый результат. Не начинайте с общего «нагрузить сервис».</li><li>Зафиксируйте среду: версии приложения и зависимостей, размер инстансов, число реплик, лимиты, сеть и внешние системы. Для каждой зависимости укажите, настоящая она или заменённая.</li><li>Подготовьте данные. Запишите версию набора, распределение размеров, правила уникальности, очистку и влияние кэша. Не используйте обезличенный набор, если его форма отличается от рабочей.</li><li>Выберите модель входа. Укажите закрытую или открытую модель, начальную скорость, ступени, длительность и ресурс генератора.</li><li>Назначьте метрики до запуска. Минимум: процент ошибок, p95 и p99, фактически начатые итерации, CPU, память, очереди и состояние зависимостей.</li><li>Сформулируйте порог и остановку. Запишите, какое нарушение прекращает тест, какие данные сохраняются и кто принимает решение о повторе.</li><li>Проведите короткий smoke-запуск. Проверьте статус, тело ответа, авторизацию, корреляционные идентификаторы и отсутствие утечки данных.</li><li>Запустите профиль и сохраните конфигурацию вместе с сырыми результатами. Не округляйте длительность и не переписывайте параметры вручную после запуска.</li><li>Разберите отрицательный путь. Проверьте, что происходит при исчерпании пула, ответе 5xx, таймауте и недоступности зависимости. Убедитесь, что ретраи не превращают отказ в неконтролируемый поток.</li><li>Сделайте вывод только по совпадающему набору условий. Если изменилась среда, данные, версия сценария или модель входа, создайте новый запуск и не сравнивайте его с прежним как одну серию.</li></ol>\n<h2>Ограничения и отрицательный путь</h2>\n<p>Нагрузочный тест не измеряет «мощность приложения» вообще. Он измеряет ответ системы на конкретный workload в конкретной среде. Учебная модель с четырьмя сегментами не моделирует сеть, TLS, DNS, планировщик, базу, кэш, балансировщик, реальные пользовательские данные или распределённый генератор. Искусственно отклонённая итерация в такой модели не является HTTP-ошибкой и не превращается в error rate.</p>\n<p>Даже реальный запуск может дать ложное спокойствие. Кэш прогрелся, база использовала другой план, тестовый набор меньше рабочего, а внешняя система не участвовала. Обратная ситуация тоже возможна: генератор упёрся в CPU, и сервис получил меньше входа, чем было заявлено. Поэтому отрицательный результат нужно сохранять с контекстом, а положительный — ограничивать теми же условиями.</p>\n<p>Не лечите симптом увеличением таймаута, числа ретраев или VU, пока не проверили источник задержки. Таймаут может уменьшить число видимых ошибок и увеличить незавершённую работу. Ретрай может улучшить долю успешных ответов для клиента и одновременно удвоить нагрузку на зависимость. Любое действие проверяйте повторным прогоном с тем же профилем.</p>\n<h2>Проверяемый критерий готовности</h2>\n<p>Тест готов к инженерному решению, если другой человек может по его записи восстановить сценарий, среду, данные, модель входа, пороги и остановку; фактический вход отделён от возможностей генератора; результаты привязаны к версиям и времени; а каждый вывод отвечает на вопрос, который был сформулирован до запуска. Для изменения индекса этого достаточно, чтобы сравнить два запуска на одинаковом контракте. Для заявления о production capacity потребуются отдельные условия, масштаб и согласование владельцев системы.</p>\n<p>Практический финал должен быть коротким: «при такой модели входа и таких данных p95 пересёк порог на такой ступени; одновременно вырос такой ресурс; повтор с теми же условиями подтвердил или опроверг результат». Если в эту фразу нельзя вставить источник наблюдения и границу применимости, тест ещё не закончен.</p>\n<h2>Проверяемые источники</h2>\n<ul><li><a href=\"https://grafana.com/docs/k6/latest/using-k6/scenarios/\" target=\"_blank\" rel=\"noopener noreferrer\">Документация k6: сценарии и модели нагрузки</a> — описание executor и различий между VU-моделью и arrival rate.</li><li><a href=\"https://grafana.com/docs/k6/latest/using-k6/thresholds/\" target=\"_blank\" rel=\"noopener noreferrer\">Документация k6: thresholds</a> — синтаксис проверяемых порогов для ошибок, процентов и перцентилей.</li><li><a href=\"https://www.rfc-editor.org/info/rfc2330\" target=\"_blank\" rel=\"noopener noreferrer\">IETF RFC 2330: Framework for IP Performance Metrics</a> — первичное описание связи метрики и методики измерения.</li></ul>"
|
||
}
|