company_banner

Тестирование производительности веб-сервиса в рамках 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 запросов в секунду – такое значение выдаст однопоточный генератор.


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


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


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


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


    Time per request


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


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


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


    Resident set size


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


    В Linux пишется файл /proc/PID/smaps – это расширение, основанное на картах, показывающее потребление памяти для каждого из отображений процесса. Если вы ваш процесс использует tmpfs, то в 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 нас не интересует.


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


    HTTP-ошибки


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


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


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


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


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


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


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


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


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


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


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


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


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


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

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


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


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


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




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


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

    Яндекс

    480,00

    Как мы делаем Яндекс

    Поделиться публикацией
    Комментарии 6
      –2
      «Continuous Intergation»
      А специально в названии в слове Intergation буквы поменяли местами с gr на rg? Или это новый термин в компании?
        –2
        Уже поправили. Спасибо.
        0
        Круто. Но возникает несколько вопросов:
        • Я правильно понимаю, что вся эта машина заводится на каждый коммит?
        • А сколько стоит такое удовольствие по времени и железу? В длительности одного цикла CI — интуитивно кажется, что 95% времени занимает именно тестирование производительности. И в железе — если обычный цикл CI обходится билд-агентом, то здесь уже нужно целое тестовое окружение, которое при этом утилизирует ресурсы виртуалки под 100%
        • Я правильно понимаю, что конфигурация железа не обязана походить на боевую, так как нам важно лишь заметить проседание производительности относительно предыдущих коммитов?
        • Если тестируемому сервису нужно сходить в другие сервисы, то вы их мокаете, или реально воспроизводите весь контур?
        • Есть ли у менеджера проекта опция «записать просадку производительности в техдолг, но выпустить суперважную фичу прямо сейчас»?

          0
          1. Не совсем на каждый, через небольшой интервал. Всё таки коммитов много, а весь граф тестов не легкий. Но при этом система CI всегда находит коммит, который внес значительное изменение.
          2. У нас большую часть времени в цикле занимает подготовка индексов, на которых работает сервис. Но это наша специфика. Они занимают терабайты суммарно и строятся из нескольких источников. Если говорить про тесты, которые измеряют RPS, то они занимают час каждая итерация, которые проводятся параллельно. Если про железо. Нужна возможность запустить эту часть CI на отдельной «чистой» машине, чтобы измерения были правильными. В нашей системе запуска такая возможность есть, мы указываем необходимую нам конфигурацию хоста, выбирается подходящий и тест на хосте работает один. Мы не делаем отдельное тестовое окружение, но да, на час машина занята только 1 тестом.
          3. Правильно, у нас разные машины на проде и на тестах.
          4. Мокаем. Поднять все сервисы, значит по сути провести интеграционное тестирование. Это не то, что мы тут хотим. Данные для мока мы готовим заранее из логов в продакшене.
          5. В основном нет. Если это не предусмотрено изначально, мы такие коммиты в прод не пускаем. А ещё мы все фичи выпускаем с возможностью их быстрого выключения, так что, если в продакшене какое-то включение неожиданно дает дополнительную нагрузку, то мы быстро фичу вырубаем.
            0
            А моки отвечают мгновенно или с рандомной задержкой?
            Просто иногда бывают сервисы, которым нужно много ходить в другие сервисы и когда мы меряем время ответа, то наверное хочется учесть и штрафы за обращение в другой сервис. Как решается этот вопрос?

            По железу немного переформулирую вопрос:
            1. Сколько вам нужно билд агентов для того чтобы проводить стандартный флоу CI и сколько нужно докинуть сверху инстансов, чтобы начать мерить производительность, так чтобы очередь в CI конвейере не разрасталась.
            2. Могут ли эти самые инстансы сосуществовать в рамках одной физической машины с другими виртуалками или они полностью утилизируют ресурсы физической машины, на которой запущены?
            3. По железу эти инстансы отличаются от обычных билд агентов?
              0
              Мгновенно. Для нас походы в сервисы по времени это «о» малое по времени. Поэтому его учет не влияет на результат. Но если нужно чистое время, то сервис пишет логи и мы умеем из них собирать реальное время работы.

              1. Я боюсь, что эти цифры частные, критический путь у всех систем разных. Опять же, ничем концептуальным хосты, на которых проверяется нагрузка, не отличаются от хостов для других тестов. У нас облако общее, мы можем одновременно запускать разное число агентов, от десятков до сотен. Тесты нагрузки не большая часть из них. Это десятки агентов. Но опять же, для каких-то программ условно достаточно просто сделать билд + запуск и запустить 15 агентов на отстрел. Тогда эти десятки агентов под нагрузку будут основным куском. А нам нужно провести много подготовительных действий. Чтобы не разрасталась очередь надо иметь минимум x3 запас.
              2. Нет, не могут. И даже если не утилизируют всё, им нельзя ни с кем соседствовать, чтобы обеспечить точность измерений.
              3. Не отличаются

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

        Самое читаемое