Хабрахабр

Тестирование производительности веб-сервиса в рамках Continuous Integration. Опыт Яндекса

Тысячи хостов с сотнями тысяч запросов в секунду. Почти всех новых сотрудников Яндекса поражают масштабы нагрузок, которые испытывают наши продукты. При этом отвечать на запросы мы должны за доли секунды. И это только один из сервисов. Даже незначительное изменение в продукте может оказать существенное влияние на производительность, поэтому важно тестировать и оценивать влияние своего кода на сервис.

Вы узнаете, как доверить анализ машине, а не следить за ними на графиках. В нашем сервисе рекламных технологий тестирование работает в рамках методологии Continuous integration, более подробно об организации которой мы расскажем 25 октября на мероприятии Яндекс изнутри, а сегодня мы поделимся с читателями Хабра опытом автоматизации оценки важных продуктовых метрик, связанных с производительностью сервиса. Поехали!

Для этого есть немало онлайн-инструментов. Речь не пойдет о том, как протестировать сайт. В нашем случае – для страниц поисковой выдачи и сайтов партнеров. Сегодня мы поговорим о высоконагруженном внутреннем бэкенд-сервисе, который является частью большой системы и готовит информацию для внешнего сервиса. А значит, компания потеряет деньги. Если наш компонент не успевает отвечать, то информацию от него просто не отдадут пользователю. Поэтому очень важно отвечать вовремя.

Какие важные показатели сервера можно выделить?

  • Request per second (RPS). Счастье одного пользователя нам, конечно же, важно. Но что, если к вам пришел не один, а тысячи пользователей. Сколько запросов в секунду может выдержать ваш сервер и не упасть?
  • Time per request. Контент сайта должен отрисовываться как можно быстрее, чтобы пользователю не надоело ждать, и он не ушел в магазин за попкорном. В нашем случае он не увидит важную часть информации на странице.
  • Resident set size (RSS). Обязательно следим за тем, сколько ваша программа потребляет памяти. Если сервис съест всю память, вряд ли можно будет говорить об отказоустойчивости.
  • HTTP-ошибки.

Итак, давайте по порядку.

Request per second

Давайте разберемся, что это такое. Наш разработчик, который долгое время занимался вопросами нагрузочного тестирования, любит говорить про критический ресурс системы.

Например, длина очереди, время ожидания ответа, threads-worker pool и т.д. У каждой системы есть свои конфигурационные характеристики, которые определяют работу. Можно провести эксперимент. И вот может так случиться, что ёмкость вашего сервиса упирается в какой-то из этих ресурсов. Ресурс, увеличение которого повысит ёмкость вашего сервиса, и будет для вас критическим. Увеличивать по очереди каждый ресурс. Но такой всё равно можно "нащупать". В хорошо сконфигурированной системе, чтобы поднять ёмкость, придется увеличить не один ресурс, а несколько. Будет отлично, если вы сможете настроить свою систему так, чтобы все ресурсы работали в полную силу, а сервис укладывался в заданные ему временные рамки.

Так как у нас этот процесс встроен в CI-систему, мы используем очень простую "пушку", с ограниченной функциональностью. Чтобы оценить, сколько запросов в секунду выдержит ваш сервер, нужно направить в него поток запросов. У него есть подробная документация. Но из открытого ПО для этой задачи отлично подойдет Яндекс.Танк. В подарок к Танку идёт сервис для просмотра результатов.

Он также поможет собрать метрики вашего сервиса, построить графики и прикрутить модуль с нужной вам логикой. Небольшой офтоп. У Яндекс.Танка достаточно богатая функциональность, не ограниченная автоматизацией обстрела запросами. В общем, очень рекомендуем познакомиться с ним.

Запросы, которыми вы будете обстреливать сервер, могут быть однотипные, искусственно созданные и размноженные. Теперь в Танк нужно скормить запросы, чтобы ими обстрелять наш сервис. Однако куда точнее будут измерения, если вы сможете собрать реальные пул запросов от пользователей за некоторый интервал времени.

Ёмкость можно измерять двумя способами.

Открытая модель нагрузки (стресс-тестирование)

Нагрузку мы будем давать не постоянную, а наращивать или даже подавать её волнами. Сделать "пользователей", то есть несколько потоков, которые будут отправлять запрос в вашу систему. Наращиваем RPS и ловим точку, в которой обстреливаемый сервис “пробьёт” SLA. Тогда это приблизит нас к реальной жизни. Таким образом можно найти пределы работы системы.

Опуская теорию, формула выглядит так: Для расчета количества пользователей можно воспользоваться формулой Литтла (о ней можно почитать тут).

RPS = 1000 / T * workers, где

• T – среднее время обработки запроса (в миллисекундах);
• workers – количество потоков;
• 1000 / T запросов в секунду – такое значение выдаст однопоточный генератор.

Закрытая модель нагрузки (нагрузочное тестирование)

Нужно настроить так, чтобы входная очередь, соответствующая конфигурации вашего сервиса, была всегда забита. Берём фиксированное количество “пользователей”. Cмотрим, сколько запросов в секунду конструкция сможет выдать. При этом делать число потоков больше, чем лимит очереди, бессмысленно, так как мы будем упираться в это число, а остальные запросы будут отбрасываться сервером с 5xx ошибкой. Такая схема в общем случае не похожа на реальный поток запросов, но она поможет показать поведение системы при максимальной нагрузке и оценить её пропускную способность на текущий момент.

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

После отстрела пушка выдает нам, сколько запросов в секунду наш сервис смог выдать. Мы при тестировании нашего сервиса используем закрытую модель. Яндекс.Танк этот показатель тоже легко подскажет.

Time per request

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

Только теперь зададим RPS, соответствующий среднему показателю вашей системы в продакшене. Для получения среднего времени ответа воспользуемся тем же Яндекс.Танком. По собранным данным можно посчитать процентили времен ответов. После обстрела получим времена ответов каждого запроса.

Например, мы отталкиваемся от продакшена. Дальше нужно понять, какой процентиль мы считаем важным. Поэтому мы считаем значимым время ответа, в которое вмещается 99% запросов. Мы можем оставить 1% запросов на ошибки, неответы, дебажные запросы, которые отрабатывают долго, проблемы с сетью и т.п.

Resident set size

Измеряя показатель RSS, мы хотим знать, сколько памяти программа забрала у операционной системы за время работы. Непосредственно наш сервер работает с файлами через mmap.

Если вы ваш процесс использует tmpfs, то в smaps попадёт как анонимная, так и неанонимная память. В Linux пишется файл /proc/PID/smaps – это расширение, основанное на картах, показывающее потребление памяти для каждого из отображений процесса. Вот пример записи в smaps. В неанонимную память входят, например, файлы, подгруженные в память. Указан конкретный файл, а его параметр Anonymous = 0kB.

7fea65a60000-7fea65a61000 r--s 00000000 09:03 79169191 /place/home/.../some.yabs
Size: 4 kB
Rss: 4 kB
Pss: 4 kB
Shared_Clean: 0 kB
Shared_Dirty: 0 kB
Private_Clean: 4 kB
Private_Dirty: 0 kB
Referenced: 4 kB
Anonymous: 0 kB
AnonHugePages: 0 kB
Swap: 0 kB
KernelPageSize: 4 kB
MMUPageSize: 4 kB
Locked: 0 kB
VmFlags: rd mr me ms

Когда процесс (тот же mmap) делает запрос в операционную систему на выделение определенного размера памяти, ему выделяется адрес. А это пример анонимного выделения памяти. В этот момент мы ещё не знаем какой физический кусок памяти выделится. Пока процесс занимает только виртуальную память. Это пример выделения анонимной памяти. Мы видим безымянную запись. У системы запросили размер 24572 kB, но им не воспользовались и фактически заняли только RSS = 4 kB.

7fea67264000-7fea68a63000 rw-p 00000000 00:00 0
Size: 24572 kB
Rss: 4 kB
Pss: 4 kB
Shared_Clean: 0 kB
Shared_Dirty: 0 kB
Private_Clean: 0 kB
Private_Dirty: 4 kB
Referenced: 4 kB
Anonymous: 4 kB
AnonHugePages: 0 kB
Swap: 0 kB
KernelPageSize: 4 kB
MMUPageSize: 4 kB
Locked: 0 kB
VmFlags: rd wr mr mw me ac

Так как выделенная неанонимная память никуда не денется после остановки процесса, файл не удалится, то такой RSS нас не интересует.

Проводим обстрел, аналогичный тестированию time per request. Перед тем, как начать отстрел по серверу, мы суммируем RSS из /proc/PID/smaps, выделенный под анонимную память, и запоминаем его. Разница между начальным и конечным состоянием и будет количество памяти, которое использовал ваш процесс во время работы. После окончания считаем RSS ещё раз.

HTTP-ошибки

Если что-то в настройке теста или окружения пошло не так, и вам на все запросы сервер вернул 5хх и 4хх ошибки, то смысла в таком тесте было не много. Не забывайте следить за кодами ответов, которые во время тестирования возвращает сервис. Если ошибок много, то тест считается невалидным. Мы следим за долей плохих ответов.

Немного о точности измерений

Давайте вернёмся к предыдущим пунктам. А теперь самое важное. Нет, конечно же, вы можете добиваться стабильности показателей, учтя все факторы, погрешности и флуктуации. Абсолютные величины посчитанных нами метрик, оказывается, не так уж нам и важны. Но это не то, что нас интересует. Параллельно написать научную работу на эту тему (кстати, если кто искал себе такую, эта может быть неплохим вариантом).

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

Вот эта система и работает у нас в рамках Continuous integration, обеспечивая нас информацией о всевозможных изменениях, которые произошли в рамках каждого коммита. Окружение, запросы, данные, состояние сервиса – все доступные нам факторы должны быть зафиксированы. Уменьшить шум мы можем, очевидно, увеличив выборку, то есть произвести несколько итераций отстрела. Несмотря на это, всё зафиксировать не удастся, останется шум. Кроме того, необходимо найти баланс между шумом и длительностью отстрела. Далее, после отстрела, скажем, 15 итераций, можно посчитать медиану получившейся выборки. Если вы хотите выбрать более сложный и точный статистический метод в соответствии со своими требованиями, рекомендуем книгу, в которой перечислены варианты с описанием, когда и какой применяется. Мы, например, остановились на погрешности в 1%.

Что ещё можно сделать с шумом?

Тестовый стенд должен быть надежным, на нём не должны быть запущены другие программы, так как они могут приводить к деградации вашего сервиса. Отметим, что немаловажную роль в таком тестировании занимает среда, в которой вы проводите тесты. Кроме того, результаты могут и будут зависеть от профиля нагрузки, окружения, базы данных и от различных “магнитных бурь”.

Во-первых, если вы пользуетесь облаком, то там может происходить что угодно. Мы в рамках одного теста коммита проводим несколько итераций на разных хостах. Поэтому полагаться на результат от одного хоста нельзя. Даже если облако специализированное, как наше, всё равно там работают служебные процессы. И врать он будет вам всегда. А если хост у вас железный, где нет, как в облаке, стандартного механизма поднятия окружения, то его можно вообще один раз случайно сломать и так и оставить. Поэтому мы гоняем наши тесты в облаке.

Если ваши измерения производятся каждый раз на разных хостах, то результаты могут немного шуметь и из-за этого в том числе. Из этого, правда, вытекает другой вопрос. То есть по историческим данным собрать “коэффициент хоста” и учитывать его при анализе результатов. Тогда можно нормировать показания на хост.

В слово "железо" тут входят версия ядра и последствия uptime (по-видимому, не перемещаемые объекты ядра в памяти). Анализ исторических данных показывает, что "железо" у нас разное.

Таким образом, каждому "хосту" (при перезагрузке хост "умирает" и появляется "новый") ставим в соответствие поправку, на которую умножаем RPS перед агрегацией.

Поправки считаем и обновляем крайне топорным способом, подозрительно напоминающим некоторый вариант обучения с подкреплением.

Для заданного вектора похостовых поправок считаем целевую функцию:

  • в каждом тесте считаем стандартное отклонение "поправленных" полученных результатов RPS
  • берем от них среднее с весами равными $exp((testtimestamp - now) / tau) $,
  • у нас tau = 1 неделя.

0 и ищем значения всех остальных поправок, дающие минимум целевой функции. Дальше одну поправку (для хоста, у которого сумма этих весов наибольшая) фиксируем в 1.

Чтобы провалидировать результаты на исторических данных, считаем поправки на старых данных, считаем поправленный результат на свежих, сравниваем с неисправленным.

Перед запуском трестируемого сервиса запустить на хосте “синтетическую программу”, по работе которой можно оценить состояние хоста и рассчитать поправочный коэффициент. Ещё один вариант корректировки результатов и уменьшения шума – это нормировка на "синтетику". Возможно, кому-то из вас она приглянется. Но в нашем случае мы используем поправки по хостам, а эта идея так и осталась идеей.

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


Если вы дочитали статью, значит, вам точно будет интересно посмотреть доклад про Яндекс.Танк и анализ результатов нагрузочного тестирования.

Приходите в гости! Также напоминаем, что более подробно об организации Continuous integration мы расскажем 25 октября на мероприятии Яндекс изнутри.

Показать больше

Похожие публикации

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»