Использование Scientist для рефакторинга критических участков Ruby on Rails приложения
Перевод статьи “Using Scientist to Refactor Critical Ruby on Rails Code”
Darren Broemmer от 18 мая 2022 года.
Попросите любого программиста просмотреть ключевые участки продуктового кода, и он обязательно укажет на несколько моментов, требующих рефакторинга. Почему же так много плохого, ненадежного или непонятного кода продолжает работать в продуктовом окружении?
Ответ прост: инженеры боятся его трогать. Задачи для рефакторинга обнаруживаются и добавляются в бэклог, но редко попадают в текущий спринт.
Для этого есть множество причин. Код мог быть написан программистом, давно покинувшим команду и теперь в нём никто не разбирается. Бывает что этот код имеет решающее значение для бизнеса и из-за этого никто не хочет нести ответственность за потенциальный сбой или потерю дохода.
В данном посте мы рассмотрим, как можно использовать гем Scientist для уверенной миграции, рефакторинга и изменения критического продуктового Ruby-кода.
Но сначала вы спросите — а нельзя ли использовать тесты для поиска ошибок?
Это ведь то самое для чего нужны тесты в Rails, верно?
И да и нет. Часто бывает трудно получить полную уверенность в новых изменениях до развертывания. Допустим модульные (Unit) и системные тесты проходят. Этого достаточно?
Реальность такова, что ничто не может заменить реальный мир, то есть продуктовую эксплуатацию. А если качество тестовых данных плохое или тесты отсутствуют? А будет ли новое ПО работать достаточно хорошо, чтобы справляться с нагрузкой?
Команды с публичными сервисами иногда обнаруживают, что им нужно решать проблемы «совместимости с ошибками» (bugwards compatibility). Когда ошибка существует в рабочей среде некоторое время, клиенты могут обходить её таким способом, который зависит от привычного неправильного поведения. Клиенты часто используют ваш софт неожиданным образом.
С Scientist можно следить за правками в Ruby и Rails сразу в боевом окружении
Если продуктовое окружение это лучшее место, где можно обрести уверенность в своих правках, можно подумать о том, чтобы понаблюдать за поведением кода именно там. Поначалу это может показаться пугающим, ведь идея «тестирования в бою» противоречит классическим методам разработки программного обеспечения.
Однако хорошая новость заключается в том, что это можно легко и безопасно сделать в Ruby и Rails с помощью гема Scientist. Название утилиты основано на научном методе проведения экспериментов для проверки гипотез. В данном случае наша гипотеза состоит в том, что новый код работает, а его использование это “эксперимент”.
Причина, по которой мы можем безопасно использовать этот подход, заключается в том, что в экспериментах по-прежнему используется результат существующего кода. Новый код оценивается только в целях наблюдения и сравнения, как на точность, так и на производительность. Мы уменьшаем проблемы с тестовым покрытием, которые обсуждались ранее, оценивая производительность с использованием реальных данных и параметров в боевом окружении. Эксперименты обычно проводят с определённой частотой, чтобы свести к минимуму влияние на производительность. Однако при желании можно оценить и каждый вызов.
Давайте теперь кратко рассмотрим, как Scientist работает с техникой “Branch by Abstraction”.
Паттерн “Branch by Abstraction” в геме Scientist
Работа Scientist начинается с паттерна Branch by Abstraction, описанного Мартином Фаулером как «постепенное крупномасштабное изменение программной системы».
Мы вводим уровень абстракции, чтобы изолировать обновляемый код. Этот уровень решает, какую реализацию использовать, чтобы эксперимент был прозрачен для остальной части системы. Данный метод связан с использованием флага (feature flag), который определяет какая из ветвей кода будет исполнена.
Гем Scientist, созданный в Github, реализует этот паттерн с помощью “эксперимента”. Существующий код называется “контрольным”, а новая реализация — “кандидатом”. Обе части кода выполняются в случайном порядке, но клиенту возвращается только результат ”контрольной” части.
Использование Scientist для рефакторинга сервиса в Ruby
Рассмотрим Ruby-сервис, который возвращает наибольший простой множитель для заданного числа. Предположим, мы определили способы оптимизации для сокращения необходимого набора кандидатов, что ускорит работу сервиса.
Однако владельцы сервиса хотят быть уверены, что при оптимизации не вкралась ошибка. Они также хотят увидеть все улучшения производительности. Давайте напишем следующий код, для вызова этого метода:
require 'scientist' def largest_prime_factor(number) science "prime-factors" do |experiment| experiment.use { find_largest_prime_factor(number) } # old way experiment.try { improved_largest_prime_factor(number) } # new way end # returns the control value end
В этот момент вызывается только выражение use
(контрольное). Но чтобы сделать эксперимент полезным, надо определить пользовательский класс Experiment
, чтобы включить его и опубликовать результаты (в данном случае просто логирование). Scientist генерирует очень полезные данные, но по умолчанию ничего с ними не делает. Эта часть остается на ваше усмотрение.
require 'scientist/experiment' require 'pp' class MyExperiment include Scientist::Experiment attr_accessor :name def initialize(name) @name = name end def enabled? true end def raised(operation, error) p "Operation '#{operation}' failed with error '#{error.inspect}'" super # will re-raise end def publish(result) pp result end end
Результаты эксперимента будут зарегистрированы, и мы сможем со временем вносить улучшения на основе анализа. Как только новый код будет соответствовать требованиям и доверие к нему станет высоким, осуществим переход на новую реализацию, просто заменив код делегированием в новую реализацию.
LabTech для упрощения использования Scientist в Ruby on Rails
Есть гем LabTech, который может помочь настроить Scientist в Rails приложении и удобнее обрабатывать результаты.
Приложения, использующие AppSignal
, могут использовать вспомогательный инструментарий Appsignal.instrument
, чтобы отслеживать, сколько времени требуется для выполнения событий Scientist. Обернув в него код эксперимента, можно увидеть, как события появляются в AppSIgnal
.
Теперь вернемся к LabTech — пример ниже просто принимает число для разложения.
Приступить к работе легко, если у вас есть доступ к консоли. Сначала надо добавить гем LabTech в Gemfile и запустить bundle install
.
gem 'lab_tech'
Результаты и конфигурация эксперимента хранятся в БД, поэтому требуется миграция.
rails lab_tech:install:migrations db:migrate
Уровень абстракции тот же, за исключением того, что используется модуль LabTech. Полный код доступен на GitHub.
def largest_prime_factor(number) LabTech.science "prime-factors" do |experiment| ... end end
На данный момент эксперимент отключен, поэтому воспользуемся консолью, чтобы включить его во всех случаях или настроить процент исполнения.
bin/rails console LabTech.enable "prime-factors" LabTech.enable "prime-factors", percent: 5
Теперь можно запустить тесты, и эксперимент будет выполнен. Для текстового представления результатов есть следующие команды в консоли Rails.
LabTech.summarize_results "prime-factors" LabTech.summarize_errors "prime-factors"
После нескольких успешных прогонов и одной синтетической ошибки получим пример того, как выглядит сводка результатов. Существует обзор успехов и ошибок, а также диаграмма ASCII, показывающая различия в производительности.
---------------------------------------------------------------------------- Experiment: prime-factors ---------------------------------------------------------------------------- Earliest results: 2022-04-27T02:42:45Z Latest result: 2022-05-01T17:27:39Z (5 days) 3 of 4 (75.00%) correct 1 of 4 (25.00%) mismatched Median time delta: +0.000s (90% of observations between +0.000s and +0.000s) Speedups (by percentiles): 0% [ · █ ] +2.4x faster 5% [ · █ ] +2.4x faster 10% [ · █ ] +2.4x faster 15% [ · █ ] +2.4x faster 20% [ · █ ] +2.4x faster 25% [ · █ ] +2.4x faster 30% [ · █ ] +2.4x faster 35% [ · █ ] +2.4x faster 40% [ · █ ] +2.4x faster 45% [ · █ ] +2.4x faster 50% [ · · · · · · ·· · · · · · · · · █ ·· · · · · · ] +2.4x faster 55% [ · █ ] +2.4x faster 60% [ · █ ] +2.4x faster 65% [ · █ ] +2.4x faster 70% [ · █] +6.9x faster 75% [ · █] +6.9x faster 80% [ · █] +6.9x faster 85% [ · █] +6.9x faster 90% [ · █] +6.9x faster 95% [ · █] +6.9x faster 100% [ · █] +6.9x faster ----------------------------------------------------------------------------
Для лёгкого и удобного анализа результатов есть гем Blazer. Он прост в установке и позволяет выполнять SQL-запросы. Запрос здесь показывает, что реализация-кандидат значительно быстрее оригинала.
В примере нашего сервиса, возвращающего наибольший простой делитель, ускорение в улучшенной реализации происходит за счет эвристики, исключающей некоторые возможные факторы, которые необходимо учитывать. Поскольку мы рассматриваем большие числа и находим простой множитель, мы можем прекратить поиск после того, как доберемся до целевого числа, делённого на этот множитель. Новый код добавляет только одну инструкцию для достижения этой цели.
Мы также можем увидеть сокращение времени выполнения используя Blazer.
Варианты использования и ограничения Scientist
Оптимальное использование Scientist включает задачи поиска, вычисления и код, который не имеет побочных эффектов. Код, включающий транзакционные обновления или внешние интеграции, такие как электронная почта, не совсем вписывается в модель, поскольку изменяемое действие выполняется дважды (как в старой, так и в новой реализации).
Это не тривиальное ограничение. Оно блокирует несколько вариантов использования. Однако есть обходные пути, если эксперимент очень важен. Стоит подумать имеют ли значение побочные эффекты или дублирование или они не являются проблемой. Например, в некоторых случаях может не иметь значения, будут ли отправлены два электронных письма во время оценки. Другой вариант — сделать так, чтобы новый код определял результат, но не сохранял его. Это не позволит сравнить производительность, но позволит проверить точность.
Другие ограничения проистекают из того, что Scientist фокусируется на возвращаемых значениях. В некоторых случаях действительные результаты могут отличаться с течением времени, независимо от того, включают ли они просто временные метки в ответ или изменяются сторонние факторы. Во многих случаях мы можем написать пользовательскую логику сравнения в эксперименте, чтобы проверить точность, выходящую за рамки обычного сравнения строк.
Наконец, ограничение LabTech заключается в том, что он не был портирован на Rails 7 на момент написания.
Лучшие практики для эффективных Scientist-экспериментов в Rails
При проведении экспериментов стоит учитывать следующие моменты:
- В Rails-проектах Scientist может быть настроен либо в инициализаторе, либо в оболочке, такой как гем Rails LabTech. Большинство приложений Rails уже имеют базу данных, поэтому LabTech использует ActiveRecord для хранения результатов.
- Чтобы не замедлять разработку и тестирование, включайте эксперимент только в промежуточной (staging) и продуктовой средах.
- Чтобы свести к минимуму любое потенциальное влияние на продуктовую среду, запускайте эксперимент только для некоторого процента запросов. LabTech поддерживает это из коробки как необязательный параметр при включении эксперимента (изначально он отключен по умолчанию). Используя чистый Scientist, эту логику легко реализовать через
enabled?
метод. - Некоторая логика требует больших ресурсов, поэтому хорошей отправной точкой может быть низкая частота дискретизации. По мере того, как вы обретете уверенность в результатах, увеличивайте процент оцениваемых запросов.
- Вы можете добавить атрибуты контекста, чтобы получить максимальную отдачу от результатов. В качестве контекста эксперимента может быть задан хэш с параметрами, который затем становится доступным в опубликованных результатах, например:
experiment.context :user => user
Подводя итоги: наблюдайте и контролируйте свое приложение Ruby с помощью Scientist
В этом посте мы рассмотрели, как использовать гем Scientist для изменения, переноса и рефакторинга кода Ruby в продуктовой среде.
Мы рассмотрели место Scientist в паттерне Branch by Abstraction, и погрузились в рефакторинг. Затем увидели, как LabTech помогает со сбором результатов и конфигурацией Scientist.
Мы коснулись некоторых ограничений Scientist, прежде чем, наконец, изложить несколько лучших практик.
Вам необходимо наблюдать и контролировать то, что происходит в системе. Интегрируйте Scientist в процесс разработки, чтобы с большей уверенностью вносить критические изменения в код Ruby.
Перевод статьи “Using Scientist to Refactor Critical Ruby on Rails Code”
Darren Broemmer от 18 мая 2022 года.