Статьи

(Серебряная) пуля для задачи N + 1

Rails — это один из самых популярных вариантов построения минимально жизнеспособных продуктов (MVP). Вы можете загружать, создавать прототипы и выпускать приложения с такой легкостью, что Rails является выбором многих разработчиков по умолчанию для MVP.

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

Одна потенциальная область улучшения — это запросы, которые ваше приложение отправляет в базу данных. Сократите количество запросов, увеличьте производительность вашего приложения.

Большинство приложений Rails имеют данные, распределенные по многим моделям с ассоциациями между ними, используя ORM для доступа. ORM могут помочь вам устранить несоответствие импедансов между реляционными базами данных и объектно-ориентированными моделями, что, надеюсь, сделает вашу жизнь проще. Но незнание некоторых из их подводных камней может значительно снизить производительность вашего приложения. Одна из таких ловушек — проблема выбора N + 1.

Что такое проблема с N + 1?

Эта проблема возникает, когда код должен загрузить дочерние элементы отношения родитель-потомок («многие» в «один ко многим»). В большинстве ORM по умолчанию включена отложенная загрузка, поэтому выдаются запросы для родительской записи, а затем один запрос для КАЖДОЙ дочерней записи. Как и следовало ожидать, выполнение N + 1 запросов вместо одного запроса приведет к заполнению базы данных запросами, чего мы можем и должны избегать.

Рассмотрим простое приложение для блога, в котором есть много статей, опубликованных разными авторами:

#Articles model
class Article < ActiveRecord::Base
  belongs_to :author
end

#Authors model
class Author < ActiveRecord::Base
  has_many :posts
end

Мы хотим перечислить 5 последних статей на боковой панели статьи вместе с их заголовком и именем автора.

Это может быть достигнуто с помощью следующего

 #In our controller
@recent_articles = Article.order(published_at: :desc).limit(5)

#in our view file
@recent_articles.each do |article|
    Title: <%= article.title %> 
    Author:<%= article.author.name %>
end

Приведенный выше код отправит 6 (5 + 1) запросов в базу данных, 1 для извлечения 5 последних статей и затем 5 для их соответствующих авторов. В приведенном выше случае, поскольку мы ограничиваем количество запросов до 5, мы не увидим, чтобы эта проблема сильно влияла на производительность нашего приложения. Однако для запросов с большим пределом это может быть фатальным.

Каждый запрос имеет немного накладных расходов. Гораздо быстрее выдать 1 запрос, который возвращает 100 результатов, чем выдать 100 запросов, каждый из которых возвращает 1 результат. Это особенно верно, если ваша база данных находится на другом компьютере, который, скажем, находится в сети на расстоянии 1-2 мс. Здесь, выдача 100 запросов последовательно имеет минимальную стоимость 100-200 мс, даже если они могут быть мгновенно удовлетворены MySQL.

Решение — стремительная загрузка

Стремительная загрузка — это механизм загрузки связанных записей объектов, возвращаемых Model.find В приведенном выше примере, если мы используем загрузку с нетерпением для получения данных, сведения об авторе будут загружены вместе со статьями.

В Rails ActiveRecord имеет метод под названием includes

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

 #In our controller
#Using includes(:authors) will include authors model.
@recent_articles = Article.order(published_at: :desc).includes(:authors).limit(5)

#in our view file
@recent_articles.each do |article|
    Title: <%= article.title %> 
    Author:<%= article.author.name %>
end

Как быстрая загрузка может предотвратить проблему с N + 1?

Стремительная загрузка — это решение проблемы с запросом N + 1, позволяющее избежать выполнения ненужных запросов во время циклического прохождения объекта.

Запросы в нашем примере идут от

 Article Load (0.9ms) SELECT 'articles'.* FROM 'articles'
Author Load (0.4ms) SELECT 'authors'.* FROM 'authors' WHERE 'authors'.'id' = ? ORDER BY 'authors'.'id' ASC LIMIT 1 [["id", 1]]
Author Load (0.3ms) SELECT 'authors'.* FROM 'authors' WHERE 'authors'.'id' = ? ORDER BY 'authors'.'id' ASC LIMIT 1 [["id", 2]]
Author Load (0.4ms) SELECT 'authors'.* FROM 'authors' WHERE 'authors'.'id' = ? ORDER BY 'authors'.'id' ASC LIMIT 1 [["id", 3]]
Author Load (0.3ms) SELECT 'authors'.* FROM 'authors' WHERE 'authors'.'id' = ? ORDER BY 'authors'.'id' ASC LIMIT 1 [["id", 4]]    
Author Load (0.4ms) SELECT 'authors'.* FROM 'authors' WHERE 'authors'.'id' = ? ORDER BY 'authors'.'id' ASC LIMIT 1 [["id", 5]]

в

 Article Load (0.4ms) SELECT 'articles'.* FROM 'articles'
Author Load (0.4ms) SELECT 'authors'.* FROM 'authors' WHERE 'authors'.'id' IN (1,2,3,4,5)

Меньше запросов для хорошего блага. 🙂

Жемчужина пули

Bullet — это драгоценный камень, написанный Ричардом Хуангом, который помогает сократить количество запросов, которые делает приложение. Впервые он был опубликован в 2009 году как плагин, и он по-прежнему довольно полезен для мониторинга вашего приложения на предмет повышения производительности. Bullet помогает отслеживать запросы вашего приложения к базе данных и уведомляет вас о любых сценариях N + 1. Интересно, что он также уведомляет вас о любых неиспользованных нагрузках.

Bullet имеет много способов уведомить вас о проблемах с запросами N + 1: уведомления Growl, предупреждения JavaScript по умолчанию и даже использование XMPP. Кроме того, он сохраняет в bullet.log точную строку и трассировку стека того, что вызвало предупреждение. Если вы хотите, он также может записать в журнал приложения.

Использование и настройка

Добавьте Bullet в ваш Gemfile и запустите пакетную bundle install

 group :development do
  gem 'bullet'
end

Этот драгоценный камень следует использовать только в среде разработки, так как вы не хотите, чтобы пользователи вашего приложения получали оповещения о проблемах с запросом N + 1.

Следующее, что нужно сделать, — это настроить способ уведомления Bullet.

Общая конфигурация

Пуля должна быть включена в приложении, просто добавление драгоценного камня не уведомит вас о неправильных запросах. Конфигурация выполняется в config / environment / development.rb .

 config.after_initialize do
  #Enable bullet in your application
  Bullet.enable = true
end

Уведомлять через оповещения Javascript

Пуля может быть настроена на уведомление разработчика с помощью простого предупреждения JavaScript. При загрузке страниц, которые выполняют запросы N + 1, появляется окно с предупреждением. Чтобы настроить предупреждения JavaScript, добавьте следующий код в блок конфигурации выше

 Bullet.alert = true

Уведомить через консоль браузера

Если вам не нравятся всплывающие окна предупреждений во всем приложении, вы можете использовать консоль браузера, чтобы уведомить вас о запросах N + 1, добавив

 Bullet.console = true

Уведомлять через Rails Logs

Bullet также может добавить в ваш журнал Rails уведомление о ваших плохих запросах. Таким образом, если вы используете некоторые инструменты анализатора для ваших журналов, вы можете добавить следующее

 Bullet.rails_logger = true

Вход в файл

Если вам нужно, чтобы запросы были записаны в файл, Bullet позволяет сделать это

 Bullet.bullet_logger = true

который создает файл журнала с именем bullet.log со всеми вашими ошибочными запросами.

Уведомлять через уведомления Growl

Если вы предпочитаете рычать уведомления, вы можете включить поддержку рычания, используя

 Bullet.growl = true

Вывод

Это завершает мой бурный тур по проблеме N + 1 и жемчужине пули. Если у вас есть ассоциация в приложении Rails, вы должны начать использовать эти приемы уже сегодня.

Ресурсы

Rubygems: пуля драгоценный камень
Github: исходный код
RailsCast: учебник