У Netflix 222 миллиона пользователей, и он работает на более чем 1700 типах устройств — от самых современных смарт-телевизоров до недорогих мобильных устройств.
Мы в Netflix гордимся своей надежностью и хотим, чтобы она оставалась всегда на высоком уровне. Поэтому важно не допускать значительного снижения производительности в выпущенном приложении. Медленная прокрутка или рендеринг с задержкой раздражают и приводят к случайным переходам в UI. Прерывистое воспроизведение портит впечатление от просмотра. Регрессия производительности в релизах продукта приводит к ухудшению впечатления от использования, поэтому задача состоит в том, чтобы обнаружить и исправить снижение производительности до релиза продукта.
В этой статье описывается, как команда Netflix TVUI реализовала надежную стратегию для быстрого и легкого обнаружения аномалий производительности до релиза, а часто даже до того, как они будут засабмичены в кодовой базе.
Что мы имеем в виду под производительностью?
Технически, метрики производительности относятся к времени отклика и задержкам в работе приложения, включая время запуска.
Но телевизионные устройства, как правило, также более ограничены в памяти, чем другие устройства, и более подвержены сбою во время скачков в потреблении памяти — поэтому для Netflix TV мы заботимся о памяти так же, как о производительности, а может и больше.
В Netflix термин «производительность» обычно включает в себя как метрики производительности в прямом смысле, так и метрики памяти. В статье мы будем использовать этот термин именно в таком ключе.
Почему мы запускаем тесты производительности на коммитах?
Сложнее рассуждать о профиле производительности предрелизного кода, поскольку мы не можем собрать метрики в режиме реального времени для кода, который еще не выпущен. До поставки мы делаем «канареечный» релиз версии приложения, которая разрабатывается в соответствии с такими же метриками, что и производственная версия. Несмотря на пользу канареечных релизов, время от времени на них случаются пропуски регрессий, потому что канареечная пользовательская база составляет лишь часть производственного релиза. И в случае обнаружения регрессии в канареечном релизе возникает необходимость в зачастую трудоемком откате или корректировке.
Выполняя тесты производительности для каждого коммита (до и после слияния), мы можем раньше обнаружить коммиты, которые потенциально могут привести к снижению производительности. И чем раньше мы обнаружим такие коммиты, тем меньше будут затронуты последующие сборки и тем легче будет откатиться. В идеале мы ловим регрессии еще до того, как они попадут в основную ветку.
Что такое тестирование производительности?
Мы проводим тестирование производительности интерфейса TV с целью собрать метрики памяти и скорости отклика путем имитации всего спектра взаимодействий с Netflix TV.
Существует около 50 тестов производительности, каждый из которых предназначен для воспроизведения какого-либо аспекта вовлеченности участников. Мы следим за тем, чтобы каждый тест был кратким и сфокусированным на конкретной изолированной части функциональности (запуск, переключение профилей, прокрутка заголовков, выбор эпизода, воспроизведение и так далее), в то время как тестовый набор в целом должен покрывать весь опыт с минимальным дублированием. Таким образом, мы можем запускать несколько тестов параллельно, а отсутствие долгоиграющих тестов позволяет управлять общим временем тестирования и повторять тестовые прогоны. Каждый тест выполняется на комбинации устройств (физических и виртуальных) и версий платформы (SDK). Каждую уникальную комбинацию теста/устройства/SDK мы будем называть вариацией теста.
Мы запускаем полный набор тестов производительности дважды за один пул-реквест:
при первом создании пул-реквеста
когда пул-реквест мержится с нужной веткой
Измерение
Каждый тест производительности отслеживает либо память, либо скорость отклика. Обе эти метрики будут колебаться в ходе теста, поэтому мы записываем значения метрик через равные промежутки времени на протяжении всего теста. Чтобы сравнить тестовые прогоны, нам нужен метод для объединения диапазона наблюдаемых значений в одно значение.
Мы приняли следующие метрики:
Тесты памяти: максимальное значение памяти, наблюдаемое во время тестового запуска (поскольку именно это значение определяет, может ли устройство выйти из строя).
Тесты на скорость отклика: использование медианного значения, наблюдаемого во время тестового запуска (исходя из предположения, что выявленная медлительность зависит от всех ответов, а не только от худшего).
Какие есть трудности?
Когда Netflix работает в продакшене, мы собираем данные о производительности в режиме реального времени, что позволяет относительно легко делать выводы о производительности приложения. Гораздо сложнее оценить производительность пре-продакшн-кода (когда изменения смержены с основной веткой, но еще не попали в релиз и еще сложнее получить сигнал производительности для кода в пул-реквесте. Метрики тестов производительности уступают метрикам использования в реальном времени по нескольким причинам:
Объем данных: в приложении Netflix одни и те же шаги повторяются миллиарды раз, но скорость разработки и ограничения ресурсов диктуют, что тесты производительности могут выполняться только несколько раз для каждой сборки.
Моделирование: каким бы строгим или творческим ни был процесс тестирования, мы можем только приблизиться к опыту реальных пользователей, а не воспроизвести его. Пользователи регулярно используют Netflix часами, и у каждого из них разные предпочтения и привычки.
Шум: в идеале данная кодовая база, выполняющая любой заданный вариант теста, всегда будет возвращать идентичные результаты. В реальности же этого никогда не происходит: нет двух одинаковых компьютеров, сборка мусора не является полностью предсказуемой, объем API-запросов и активность серверной части являются переменными, как и загрузка серверов и пропускная способность сети. Для каждого теста будет фоновый шум, который нам нужно каким-то образом отфильтровать из нашего анализа.
Первоначальный подход: статические пороговые значения
Для нашей первой попытки проверки производительности мы установили максимально допустимые пороговые значения для метрик памяти. За этим подходом стояло веское обоснование — когда на телевизоре работает Netflix, существует жесткий предел объема памяти, при превышении которого Netflix может выйти из строя.
Было несколько проблем с подходом статических пороговых значений:
Индивидуальная подготовительная работа для каждого теста: поскольку каждый вариант теста имеет уникальный профиль памяти, соответствующий статический порог необходимо исследовать и назначать в каждом конкретном случае. Это было сложно и отнимало много времени, поэтому мы присвоили пороговые значения только для примерно 30% тестовых вариаций.
Отсутствие контекста: в качестве метода валидации статические пороговые значения оказались несколько необоснованными. Представьте коммит, который увеличивает использование памяти на 10%, но до уровня чуть ниже порогового значения. Следующий коммит может быть изменением README (что имеет нулевое воздействие на память), но из-за обычных изменений фонового шума устройства метрика может увеличиться ровно настолько, чтобы превысить пороговое значение.
Фоновая дисперсия не фильтруется: как только кодовая база сталкивается с порогом памяти, фоновый шум устройства становится основным фактором, определяющим, на какую сторону пороговой линии попадает результат теста.
Корректировка после оповещения: мы обнаружили, что мы неоднократно повышали пороговые значения, чтобы убрать фоновый шум
Переломный момент: обнаружение аномалий и точек изменения
Стало очевидным, что нам нужна техника валидации производительности, которая:
Устраняет предвзятость, связанную с неудачами, придавая равный вес всем тестовым запускам, независимо от результатов.
Не обрабатывает точечно данные о производительности в изоляции друг от друга, а вместо этого оценивает влияние сборки на производительность по сравнению с предыдущими сборками.
Может автоматически применяться к каждому тесту без необходимости предварительных исследований, ввода данных или постоянного ручного вмешательства.
Может в равной степени применяться к тестовым данным любого типа: память, время отклика или любые другие небулевые тестовые данные.
Сводит к минимуму влияние фонового шума, отдавая предпочтение дисперсии над абсолютными значениями.
Улучшает понимание, изучая точки данных как во время создания, так и за прошлое время.
Мы остановились на двояком подходе:
Обнаружение аномалий немедленно выявляет потенциальное снижение производительности путем сравнения с предыдущими данными.
Обнаруживая точки изменений, мы тем самым выявляем более тонкие изменения производительности, при помощи изучения прошлых и будущих кластеров данных.
Обнаружение аномалий
Мы называем аномалией любое значение метрики, которая более чем на n стандартных отклонений выше среднего значения, где среднее значение и стандартное отклонение получены из предыдущих m тестовых прогонов. Для тестов производительности Netflix TV мы в настоящее время установили n на 4 и m на 40, но эти значения можно скорректировать, чтобы максимизировать коэффициент соотношения сигнала к шуму. При обнаружении аномалии тесту присваивается статус «failed» и генерируется предупреждение.
Обнаружение аномалий работает, потому что пороговые значения являются динамическими и выводятся из существующих данных. Если данные демонстрируют большую фоновую дисперсию, порог аномалии будет увеличиваться, чтобы учесть дополнительный шум.
Точки изменения
Точки изменения — это точки данных на границе двух отдельных паттернов распределения данных. Мы используем метод под названием e-divisive для анализа 100 самых последних тестовых запусков, используя реализацию Python, основанную на этой реализации.
Поскольку нас интересуют только регрессии производительности, мы игнорируем точки изменения, которые имеют тенденцию к снижению. Когда для теста обнаруживается точка изменения, мы не завершаем тест ошибкой и не генерируем предупреждение (мы считаем точки изменения предупреждениями о необычных паттернах, а не полноценное сообщение об ошибке).
Как видите, точки изменения являются более тонким сигналом. Они не обязательно указывают на регрессию, но говорят о сборках, которые повлияли на последующее распределение данных.
Сборки, которые генерируют точки изменения в нескольких тестах, требуют дальнейшего изучения, прежде чем их можно будет включить в кандидаты на релиз.
Точки изменения дают нам больше уверенности в обнаружении регрессии, потому что они не принимают во внимание ложно-положительных сигналах, такие как однократные всплески данных. Поскольку для обнаружения точек изменений требуются данные постфактум, они лучше всего подходят для выявления потенциально кода с регрессией, который уже находится в основной ветке, но еще не попал в релиз.
Дополнительные настройки
Прогоны за тест
Чтобы разрешить вопрос, связанный с логической ошибкой предвзятости к неудачам, мы решили запустить все тесты 3 раза независимо от результата. Мы выбрали 3 итерации, чтобы получить достаточно данных для устранения большей части шума устройства (тесты распределяются по устройствам случайным образом) без создания «бутылочного горлышка» в производительности.
Подведение итогов по тестовым прогонам
Затем нам нужно было выбрать методологию для сжатия результатов каждой пачки из 3 прогонов в одно значение. Цель состояла в том, чтобы игнорировать результаты с отклоняющимися значениями, вызванные неустойчивым поведением устройства.
Изначально мы брали среднее из этих трех прогонов, но это привело к большому количеству ложных срабатываний, потому что самые нерегулярные прогоны теста влияли на результат слишком сильно. Переключение на медиану устранило некоторые из этих ложных срабатываний, но мы по-прежнему получали большое количество избыточных предупреждений (поскольку в периоды сильного шума устройства мы иногда видели аномальные значения в двух случаях из трех). Наконец, поскольку мы заметили, что результаты с выбросами, как правило, выше, чем обычно, а редко ниже, мы остановились на использовании минимального значения для трех прогонов, и это оказалось наиболее эффективным для устранения внешнего шума.
Какие же были результаты?
В проверке производительности мы перешли к обнаружению аномалий и точек, где заметили несколько улучшений.
а) Нас гораздо реже предупреждают о потенциальных регрессах производительности, а когда мы все-таки получаем их, это с гораздо большей вероятностью указывает на подлинный регресс. Наша рабочая нагрузка дополнительно снижается за счет того, что больше не нужно вручную увеличивать статические пороговые значения производительности после каждого ложного срабатывания.
В таблице ниже представлена сводка предупреждений за два разных месяца прошлого года. В марте 2021 года мы все еще использовали статические пороговые значения для оповещений о регрессии. К октябрю 2021 года мы перешли на обнаружение аномалий для оповещений о регрессии. Оповещения, которые были истинными регрессиями, — это количество предупрежденных коммитов, для которых предполагаемая регрессия оказалась как значимой, так и устойчивой.
Обратите внимание, что, поскольку мартовские тесты проверялись только тогда, когда порог был установлен вручную, общее количество тестовых прогонов в октябре было намного больше, и все же мы получили только 10% предупреждений.
б) Нас не предупреждают о последующих безобидных сборках, которые наследуют регрессивные коммиты от предыдущих сборок. (При использовании метода статического порога все последующие сборки были отмечены до тех пор, пока не откатили регрессивный билд.) Это связано с тем, что регрессивные сборки увеличивают как среднее значение, так и стандартное отклонение и, таким образом, помещают последующие нерегрессивные сборки ниже порога предупреждения.
c) Тесты производительности на пул-реквестах, которые почти всегда были красными (поскольку вероятность нарушения хотя бы одного статического порогового значения всегда была высокой), теперь в основном зеленые. Когда тесты производительности окрашены в красный цвет, у нас гораздо больше уверенности в том, что имеет место истинная регрессия производительности.
d) Визуальное отображение количества аномалий и точек изменения быстро помогает определить потенциально проблемные сборки.
Что дальше?
Дальнейшая работа
Есть еще несколько пунктов, которые мы хотели бы улучшить.
Облегчить определение того, были ли регрессии вызваны внешними агентами: часто оказывается, что обнаруженная регрессия, хотя и реальная, была не результатом коммита какого-то кода, а внешним фактором, таким как обновление до одной из зависимостей нашей платформы или включенного фиче флага. Было бы полезно резюмировать внешние изменения в сводках алертов.
Исключить/не учитывать исправленные регрессии при определении базовых показателей для проверки: При создании недавних значений среднего и стандартного отклонения мы могли бы улучшить обнаружение регрессии, отфильтровывая данные из бывших регрессий, которые с тех пор были исправлены.
Повышение скорости разработки: мы можем еще больше сократить общее время тестирования, убрав ненужные итерации в тестах и добавив больше устройств для обеспечения доступности и снизив акцент на тестировании тех частей приложения, производительность которых менее критична. Мы также можем предварительно собирать набор приложений (хотя бы частично), чтобы набор тестов не задерживался из-за ожидания свежих сборок.
Более точное отражение метрик, собранных в релизном приложении: в развертываемом приложении Netflix TV мы собираем дополнительные метрики, такие как TTR (время рендеринга) и коэффициент пустых ящиков (как часто заголовки в области просмотра пропускают изображения). Хотя тестовые метрики и метрики, собранные во время реального использования, не поддаются прямому сравнению, измерение относительного изменения метрик в предварительных сборках может помочь нам предвидеть регрессию в производственной среде.
Более широкое внедрение и новые варианты использования
На данный момент обнаружение аномалий и точек изменения применяется к каждому коммиту в репозитории TVUI и находится в процессе развертывания для коммитов в репозитории TV Player (уровень, управляющий операциями воспроизведения). Другие команды Netflix (за пределами TV-платформы) также проявили интерес к этим методам, и их конечной целью является стандартизация обнаружения регрессии в Netflix.
Обнаружение аномалий и точек изменения полностью независимы от фреймворка — единственные необходимые входные данные — это текущее значение и массив последних значений для сравнения. Таким образом, их полезность выходит далеко за рамки тестов производительности. Например, мы рассматриваем возможность использования этих методов для мониторинга надежности наборов тестов, не основанных на производительности — в этом случае в качестве метрики нас интересует процент успешных тестов.
В будущем мы планируем отделить логику аномалий и точек изменений от нашей тестовой инфраструктуры и предложить ее в виде автономной библиотеки с открытым исходным кодом.
В завершение
Используя методы, которые оценивают влияние сборки на производительность по отношению к характеристикам производительности (величина, дисперсия, тренд) соседних сборок, мы можем более уверенно отличать подлинные регрессии от метрик, повышенным по другим причинам — например, наследование проблемного кода, регрессии в предыдущих сборках или разовые всплески данных из-за ошибок в тестах. Мы также тратим меньше времени на поиск ложноотрицательных результатов, и нам больше не нужно вручную назначать пороговое значение для каждого результата — теперь сами данные устанавливают пороговые значения динамически.
Эта повышенная эффективность и более высокий уровень достоверности помогают нам быстро выявлять и исправлять регрессии до того, как они достигнут наших участников.
Обсуждаемые здесь методы аномалий и точек изменения можно использовать для выявления регрессий (или улучшений), неожиданных значений или точек изменений в любых хронологически упорядоченных количественных данных. Их полезность выходит далеко за рамки анализа производительности. Например, их можно использовать для определения точек перегиба в надежности системы, удовлетворенности клиентов, использовании продукта, объеме загрузок или доходах.
Мы рекомендуем вам попробовать эти методы на ваших собственных данных. Было бы интересно узнать больше об их удачном (или неудачном) применении в других контекстах.
Всех желающих приглашаем на открытое занятие «Краткое сравнение производительности инструментов НТ», на котором мы проведем серию тестов на разных инструментах НТ и сравним их производительность, а также процесс разработки скриптов и сценариев. Регистрация доступна по ссылке.