rails
September 30, 2022

Использование 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.

Happy coding!

Перевод статьи “Using Scientist to Refactor Critical Ruby on Rails Code”
Darren Broemmer от 18 мая 2022 года.