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