Статьи

Отслеживание проблем производительности Common Rails с помощью Skylight

свет неба

Сегодня я собираюсь представить вам Skylight — умный и простой в использовании онлайн-инструмент для профилирования Rails, Sinatra и Grape. Открывая его основные функции, мы обсудим общие проблемы с производительностью Rails, а также способы их устранения.

Skylight чем-то похож на популярный сервис New Relic , но он был разработан специально для приложений Rails с целью облегчить взаимодействие с панелью мониторинга. Команда Skylight считает, что в New Relic слишком много функций, и не все из них действительно полезны. Поэтому команда Skylight сосредоточена на основных компонентах, которые обеспечивают немедленную ценность. Skylight бесплатен для 100 000 запросов в месяц, и вы также получаете бесплатную 30-дневную пробную версию для проверки его функциональности. Неплохо, а?

Исходный код примера приложения можно найти на GitHub .

Препараты

Чтобы увидеть Skylight в действии, нам нужен пример приложения на Rails. Это будет очень просто, но этого будет достаточно для этой вводной статьи:

$ rails new SkylightDiag -T

Я использую Rails 5.0.0.1, но Skylight работает и с версиями 3 и 4.

Предположим, в этом приложении мы хотим перечислить музыкальные альбомы и их треки. Все данные будут полностью поддельными, но это не имеет большого значения. Создать AlbumSong

 $ rails g model Album title:string musician:string
$ rails g model Song title:string duration:integer album:belongs_to
$ rails db:migrate

Убедитесь, что ассоциации установлены правильно:

модели / album.rb

 [...]
has_many :songs, dependent: :delete_all
[...]

модели / song.rb

 [...]
belongs_to :album
[...]

Добавьте контроллер и корневой маршрут (представление будет добавлено позже):

albums_controller.rb

 class AlbumsController < ApplicationController
  def index
    @albums = Album.all
  end
end

конфиг / routes.rb

 [...]
root 'albums#index'
[...]

Вместо того, чтобы заполнять образцы данных вручную, давайте полагаться на db / seed.rb и гем Faker, который возвращает случайные данные различного вида:

Gemfile

 [...]
gem 'faker'
[...]

Не забудь бежать

 $ bundle install

Этот драгоценный камень был недавно обновлен и теперь включает еще больше типов образцов данных (включая имена героев пива и звездных войн). Подготовьте файл seed.rb :

дБ / seeds.rb

 50.times do
  album = Album.create({title: Faker::Book.title, musician: Faker::StarWars.character})
  30.times do
    album.songs.create({title: Faker::Book.title, duration: rand(500)})
  end
end

Что ж, если вы фанат «Игры престолов», вы можете взять имена ее персонажей

Наконец, беги

 $ rails db:seed

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

Интеграция светового проема

Прежде чем углубиться в код, давайте быстро обсудим, какие среды поддерживает Skylight . Он работает с Ruby 1.9.2 и выше, но рекомендуется использовать 2.1+, чтобы в полной мере воспользоваться инструментами сервисов. Как я уже говорил ранее, он работает с Rails 3+, но также поддерживается Sinatra 1.2+ и даже Grape 0.10+ (но у вас будет меньше подробной информации). Наконец, Skylight должен работать с любым * nix сервером, никакой специальной настройки даже для Heroku не требуется. Клиент отслеживания написан на Rust и C, поэтому он не должен занимать много памяти.

Чтобы начать, получите 30-дневную пробную версию здесь (вы также получите кредит в размере 50 долларов в качестве небольшого бонуса от меня :)). Заполните основную информацию о себе и своем приложении, затем добавьте новый драгоценный камень в Gemfile :

Gemfile

 [...]
gem "skylight"
[...]

Бегать

 $ bundle install

Также обратите внимание, что мастер установки Skylight просит вас выполнить команду, похожую на:

 $ bundle exec skylight setup SOME_KEY

Эта команда завершает интеграцию, создав файл config / skylight.yml . После этого вы готовы идти. Обратите внимание, что вам был предоставлен личный токен, используемый для обмена данными приложения. Его можно восстановить на странице настроек вашего аккаунта. Также не забывайте, что Skylight не будет отправлять данные в среду разработки, поэтому ваше приложение должно быть развернуто где-то, чтобы начать отслеживать его производительность. Например, при развертывании на Heroku вы можете установить токен Skylight с помощью следующей команды:

 $ heroku config:add SKYLIGHT_AUTHENTICATION='123abc'

На этой странице представлен полный обзор процесса интеграции Skylight с другими платформами и объясняется, как отслеживать сервисы вне Rails, такие как Net::HTTP

N + 1 проблема запроса

Как только вы закончите настройку, давайте эмулируем первую и, возможно, наиболее известную проблему, называемую «запрос N + 1». Чтобы увидеть его в действии, перечислите все альбомы и их треки на главной странице приложения:

просмотров / альбомы / index.html.erb

 <h1>Albums</h1>
<ul>
  <%= render @albums %>
</ul>

просмотров / альбомы / _album.html.erb

 <li>
  <strong><%= album.title %></strong> by <%= album.musician %>
  <ul>
    <% album.songs.each do |song| %>
      <li><%= song.title %> (<%= song.duration %>s)</li>
    <% end %>
  </ul>
</li>

Загрузите сервер и перейдите по http://localhost:3000 Внутри терминала вы увидите вывод, похожий на этот:

 Album Load (1.0ms)  SELECT "albums".* FROM "albums" ORDER BY published DESC
Song Load (0.0ms)  SELECT "songs".* FROM "songs" WHERE "songs"."album_id" = ?  [["album_id", 301]]
Song Load (1.0ms)  SELECT "songs".* FROM "songs" WHERE "songs"."album_id" = ?  [["album_id", 300]]
Song Load (1.0ms)  SELECT "songs".* FROM "songs" WHERE "songs"."album_id" = ?  [["album_id", 299]]
Song Load (0.0ms)  SELECT "songs".* FROM "songs" WHERE "songs"."album_id" = ?  [["album_id", 298]]
....
.... many similar stuff goes here

По сути, для каждой родительской записи (альбома) мы отправляем отдельный запрос, чтобы извлечь его дочерние элементы (песни) и эффективно спамить базу данных. Разверните свое приложение в производственной среде, откройте его главную страницу, а затем через пару минут обновите панель управления Skylight.

Сама приборная панель действительно хороша и аккуратна, и вы можете прочитать этот документ, чтобы лучше узнать его и узнать об основных концепциях Skylight. Он представляет информацию о запросах в минуту, типичном и неполадках времени ответа, а также общую информацию о конечных точках. В нашем случае конечной точкой будет метод AlbumsController#create В нижней части страницы вы можете выбрать диапазон времени для отображения — вот и все.

nplusone-1

Красный значок базы данных рядом с именем конечной точки означает, что у нее есть несколько повторяющихся SQL-запросов, тогда как круговая диаграмма указывает на высокий уровень выделения объектов. Также обратите внимание на столбец «Агония», который рассчитывается по специальному алгоритму Skylight, чтобы определить, какая конечная точка оказывает наиболее неблагоприятное влияние на пользователей приложения. Предположим, у вас есть две конечные точки: одна имеет время отклика 600 мс, но получает множество запросов, тогда как вторая имеет время отклика 3 с, но почти не имеет запросов. Очевидно, что 600 мс намного лучше, чем 3 с, но пока первая конечная точка получает гораздо больше запросов, это должно быть главным приоритетом для дальнейшего сокращения времени отклика.

Теперь, если вы нажмете на конечную точку AlbumsController#create

nplusone-2

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

Таким образом, решение этой проблемы очень просто. Все, что вам нужно сделать, это реализовать так называемую «готовую загрузку»
это значительно уменьшает количество запросов. Это делается с помощью метода include:

albums_controller.rb

 [...]
def index
  @albums = Album.includes(:songs)
end
[...]

Теперь в терминале вы увидите

 Album Load (1.0ms)  SELECT "albums".* FROM "albums"
Song Load (13.0ms)  SELECT "songs".* FROM "songs" WHERE "songs"."album_id" IN (202, 203, 204, 205, 206, ...)

что, если, конечно, намного лучше. После развертывания обновленной версии приложения на сервере Skylight
больше не сообщает об этой проблеме:

nplusone ушедшей

Большой!

Включение кеширования фрагментов

Другой очень распространенный метод для ускорения работы вашего приложения — включение кеширования . В этой демонстрации мы будем использовать метод кэширования вложенных фрагментов . Сначала включите его для родительской коллекции:

просмотров / альбомы / index.html.erb

 <%= render @albums, cached: true %>

Далее для детей записи:

просмотров / альбомы / _album.html.erb

 <li>
  <strong><%= album.title %></strong> by <%= album.musician %>
  <ul>
    <% album.songs.each do |song| %>
      <% cache song do %>
        <li><%= song.title %> (<%= song.duration %>s)</li>
      <% end %>
    <% end %>
  </ul>
</li>

Не забывайте, что для правильной работы необходимо настроить кэширование на своем сервере. Что касается Heroku, настройка очень проста. Включите дополнение Memcachier (бесплатная версия):

 $ heroku addons:create memcachier:dev

Загляните в жемчужину Далли

Gemfile

 [...]
gem 'dalli'
[...]

установить его

 $ bundle install

и настроить конфигурацию для производственной среды:

конфиг / окружающая среда / production.rb

 [...]
config.cache_store = :dalli_store,
    (ENV["MEMCACHIER_SERVERS"] || "").split(","),
    {:username => ENV["MEMCACHIER_USERNAME"],
     :password => ENV["MEMCACHIER_PASSWORD"],
     :failover => true,
     :socket_timeout => 1.5,
     :socket_failure_delay => 0.2,
     :down_retry_delay => 60
    }
[...]

После развертывания в производство Skylight покажет дополнительную информацию о работе с хранилищем кэша, а время отклика заметно уменьшится:

кэш

Группировка транзакций

Другой, менее распространенный метод повышения производительности приложений — группирование нескольких транзакций в одну. Предположим, что пользователи могут добавлять дополнительные альбомы в приложение, загружая файл CSV. Конечно, мы не будем выполнять фактический анализ файла — давайте просто имитируем его, вводя простой цикл:

albums_controller.rb

   [...]
  def create
    50.times do
      Album.create({title: Faker::Book.title, musician: Faker::StarWars.character})
    end
    redirect_to root_path
  end
  [...]

Добавить маршрут

конфиг / routes.rb

 [...]
resources :albums, only: [:create]
[...]

и поместите кнопку на главной странице, чтобы запустить процесс импорта:

просмотров / альбомы / index.html.erb

 [...]
<%= link_to 'add more albums', albums_path, method: :post %>
[...]

При нажатии этой кнопки вы увидите, что каждая операция выполняется в своей собственной транзакции (да, Yoda теперь музыкант):

 (0.0ms)  begin transaction
SQL (4.0ms)  INSERT INTO "albums" ("title", "musician", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["title", "The Far-Distant Oxus"], ["musician", "Yoda"], ["created_at", 2016-09-13 13:09:29 UTC], ["updated_at", 2016-09-13 13:09:29 UTC]]
(81.0ms)  commit transaction

Мы тратим 4 мс на операцию INSERT, в то время как транзакция длится в 20 раз больше! Это визуализация Skylight:

no_transaction

Вместо этого лучше сгруппировать все эти инструкции INSERT

albums_controller.rb

   [...]
  def create
    Album.transaction do
      50.times do
        Album.create({title: Faker::Book.title, musician: Faker::StarWars.character})
      end
    end
    redirect_to root_path
  end
  [...]

Теперь вывод:

 (1.0ms)  begin transaction
SQL (1.0ms)  INSERT INTO "albums" ("title", "musician", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["title", "A Handful of Dust"], ["musician", "Yoda"], ["created_at", 2016-09-13 13:14:26 UTC], ["updated_at", 2016-09-13 13:14:26 UTC]]
...
many other INSERTs here
...
(133.0ms)  commit transaction

Это также лучше, если вы хотите прервать процесс импорта, если возникла ошибка, так как транзакция не может
быть частично примененным.

Вот графическое представление:

with_transaction

Эти красные циклические стрелки все еще там, потому что у нас действительно есть запросы, выполняемые в цикле.

Добавление индекса

Другой очень простой, но иногда упускаемый из виду метод оптимизации производительности — это добавление индекса базы данных. Абсолютно необходимо сделать все ваши столбцы с индексированными внешними и первичными ключами; столбцы с логическими значениями (например, adminbannedpublished Драгоценный камень lol_dba может сканировать ваши модели и представлять список столбцов, которые должны быть проиндексированы.

Давайте представим новую published

 $ rails g migration add_published_to_albums published:boolean

Немного измените миграцию

 def change
  add_column :albums, :published, :boolean, default: false
end

и применить его:

 $ rails db:migrate

«Опубликовать» 20 случайных альбомов:

дБ / seeds.rb

 Album.all.shuffle.take(20).each {|a| a.toggle!(:published)}

$ rails db:seed

Теперь давайте воспользуемся этим новым столбцом для отображения списка выпущенных альбомов или полного упорядоченного списка:

albums_controller.rb

 def index
  if params[:published] == 't'
    @albums = Album.includes(:songs).where(published: true)
  else
    @albums = Album.includes(:songs).order('published DESC')
  end
end

Вот результаты с панели инструментов Skylight:

published_no_index

Конечно, как вы уже догадались, published

 $ rails g migration add_missing_index_to_albums

Миграции / xyz_add_missing_index_to_albums.rb

 [...]
def change
  add_index :albums, :published
end
[...]

Применить миграцию:

 $ rails db:migrate

и наблюдать результаты на Skylight:

published_index

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

Вывод

В этой статье мы обсудили Skylight — умный профилировщик для приложений Rails, Sinatra и Grape. Изучая его основные функции, мы имели возможность взглянуть на некоторые наиболее распространенные проблемы производительности в приложениях Rails. Конечно, наше примерное приложение не очень репрезентативно, но у вас есть хотя бы базовое представление о том, на что способен Skylight, поэтому обязательно попробуйте его для реального проекта.

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