Сегодня я собираюсь представить вам 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.
Предположим, в этом приложении мы хотим перечислить музыкальные альбомы и их треки. Все данные будут полностью поддельными, но это не имеет большого значения. Создать Album
Song
$ 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
В нижней части страницы вы можете выбрать диапазон времени для отображения — вот и все.
Красный значок базы данных рядом с именем конечной точки означает, что у нее есть несколько повторяющихся SQL-запросов, тогда как круговая диаграмма указывает на высокий уровень выделения объектов. Также обратите внимание на столбец «Агония», который рассчитывается по специальному алгоритму Skylight, чтобы определить, какая конечная точка оказывает наиболее неблагоприятное влияние на пользователей приложения. Предположим, у вас есть две конечные точки: одна имеет время отклика 600 мс, но получает множество запросов, тогда как вторая имеет время отклика 3 с, но почти не имеет запросов. Очевидно, что 600 мс намного лучше, чем 3 с, но пока первая конечная точка получает гораздо больше запросов, это должно быть главным приоритетом для дальнейшего сокращения времени отклика.
Теперь, если вы нажмете на конечную точку AlbumsController#create
Эти две циклические красные стрелки рядом с зеленой строкой 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
больше не сообщает об этой проблеме:
Большой!
Включение кеширования фрагментов
Другой очень распространенный метод для ускорения работы вашего приложения — включение кеширования . В этой демонстрации мы будем использовать метод кэширования вложенных фрагментов . Сначала включите его для родительской коллекции:
просмотров / альбомы / 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:
Вместо этого лучше сгруппировать все эти инструкции 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
Это также лучше, если вы хотите прервать процесс импорта, если возникла ошибка, так как транзакция не может
быть частично примененным.
Вот графическое представление:
Эти красные циклические стрелки все еще там, потому что у нас действительно есть запросы, выполняемые в цикле.
Добавление индекса
Другой очень простой, но иногда упускаемый из виду метод оптимизации производительности — это добавление индекса базы данных. Абсолютно необходимо сделать все ваши столбцы с индексированными внешними и первичными ключами; столбцы с логическими значениями (например, admin
banned
published
Драгоценный камень 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
$ 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:
Повышение производительности может быть не таким высоким, но при выполнении многих операций, связанных со столбцом, но наличие индекса может иметь большое значение.
Вывод
В этой статье мы обсудили Skylight — умный профилировщик для приложений Rails, Sinatra и Grape. Изучая его основные функции, мы имели возможность взглянуть на некоторые наиболее распространенные проблемы производительности в приложениях Rails. Конечно, наше примерное приложение не очень репрезентативно, но у вас есть хотя бы базовое представление о том, на что способен Skylight, поэтому обязательно попробуйте его для реального проекта.
Надеемся, что вам понравилось читать эту статью, и теперь вас больше волнует производительность ваших приложений. Я благодарю вас за то, что вы остались со мной. До скорого!