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: учебник