Среда и конфигурирование Rails часть 3

Режим разработки.

Режим разработки принимается в Rails по умолчанию, именно в нем вы будете проводить большую часть времени:

# Определенные здесь параметры имеют больший приоритет, чем параметры
# в файле config/environment.rb
# В режиме разработки код приложения перезагружается при каждом запросе.
# Это увеличивает время реакции, но идеально подходит для разработки,
# так как вам не приходится перезагружать веб-сервер после внесения каждого
# изменения в код.
config.cache_classes = false
# Записывать в протокол сообщения об ошибках при случайном вызове метода
# для объекта nil.
config.whiny_nils = true
# Активировать сервер точек останова, с которыми соединяется
# script/breakpointer
config.breakpoint_server = true
# Показывать полные отчеты об ошибках и запретить кэширование
config.action_controller.consider_all_requests_local = true
config.action_controller.perform_caching = false
config.action_view.cache_template_extensions = false
config.action_view.debug_rjs = true
# Не обращать внимание, если почтовый клиент не может отправить сообщение
config.action_mailer.raise_delivery_errors = false

Динамическая перезагрузка классов

Одна из отличительных особенностей Rails – скорость цикла внесения изменений в режиме разработки. Правите код, щелкаете по кнопке Обновить в броузере – и все! Изменения волшебным образом отражаются на приложении. Такое поведение управляется параметром config.cache_classes, который, как видите, установлен в false в самом начале
сценария config/environments/development.rb.

Не вдаваясь в технические детали, скажу, что если параметр config.cache_classes равен true, то Rails загружает классы с помощью предложения require, а если false – то с помощью предложения load.

Когда вы затребуете файл с кодом на Ruby с помощью require, интерпретатор исполнит и кэширует его. Если файл затребуется снова (припоследующих запросах), интерпретатор пропустит предложение require и пойдет дальше. Если же файл загрузится предложением load, то интерпретатор считает и разберет его снова вне зависимости от того,
сколько раз файл загружался раньше.

Теперь рассмотрим загрузку классов в Rails более пристально, поскольку иногда вам не удается заставить код перезагружаться автоматически, и это может довести до белого каления, если не понимаешь, как на самом деле работает механизм загрузки классов!

Загрузчик классов в Rails

В старом добром Ruby нет никаких соглашений об именовании файла в соответствии с содержимым. Однако в Rails, как легко заметить, почти всегда имеется прямая связь между именем Ruby-файла и содержащимся в нем классом. В Rails используется тот факт, что Ruby предоставляет механизм обратных вызовов для отсутствующих констант.
Когда Rails встречает в коде неопределенную константу, он вызывает подпрограмму загрузки класса, которая, полагаясь на соглашения об именовании файлов, пытается найти и затребовать нужный сценарий.

Откуда загрузчик классов знает, где искать файл? Мы уже затрагивали этот вопрос выше при обсуждении роли сценария initializer.rb в процессе запуска Rails. В Rails имеется концепция путей загрузки, и по умолчанию множество путей включает практически все каталоги, куда вам может прийти в голову поместить код приложения.

Метод default_load_paths определяет, в каком порядке Rails просматривает каталоги в пути загрузки. Мы разберем код этого метода и объясним назначение каждой части пути загрузки.

Каталог test/mocks (подробно рассматривается в главе 17 «Тестирование») дает возможность переопределить поведение стандартных классов Rails:

paths = ["#{root_path}/test/mocks/#{environment}"]
# Добавить каталог контроллера приложения.
paths.concat(Dir["#{root_path}/app/controllers/"])
# Затем подкаталоги компонентов.
paths.concat(Dir["#{root_path}/components/[_a-z]*"])
# Затем стандартные каталоги для включаемых файлов.
paths.concat %w(
app
app/models
app/controllers
app/helpers
app/services
app/apis
components
config
lib
vendor
).map { |dir| "#{root_path}/#{dir}" }.select { |dir|
File.directory?(dir) }
paths.concat Dir["#{root_path}/vendor/plugins/*/lib/"]
paths.concat builtin_directories
end

Хотите посмотреть содержимое пути загрузки для своего проекта? Запустите консоль и распечатайте переменную $:. Вот так:

$ console
Loading development environment.
>> $:
=> ["/usr/local/lib/ruby/gems/1.8/gems/ ... # выводятся примерно 20 строк

Режим тестирования

Если Rails запускается в режиме тестирования (то есть значение переменной окружения RAILS_ENV равно test), то действуют следующие параметры:

# Определенные здесь параметры имеют больший приоритет, чем параметры
# в файле config/environment.rb
# Среда тестирования служит исключительно для прогона набора тестов
# вашего приложения. Ни для чего другого она не предназначена. Помните,
# что тестовая база данных – это "рабочая область", она уничтожается
# и заново создается при каждом прогоне. Не полагайтесь на хранящиеся
# в ней данные!
config.cache_classes = true
# Записывать в протокол сообщение об ошибке при случайном вызове метода
# для объекта nil.
config.whiny_nils = true
# Показывать полные отчеты об ошибках и запретить кэширование
config.action_controller.consider_all_requests_local = true
config.action_controller.perform_caching = false
# Не разрешать объекту ActionMailer отправлять почтовые сообщения
# реальным адресатам. Метод доставки :test сохраняет отправленные
# сообщения в массиве ActionMailer::Base.deliveries.
config.action_mailer.delivery_method = :test

Как правило, вообще не возникает необходимости модифицировать параметры среды тестирования.

Режим эксплуатации

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

# Определенные здесь параметры имеют больший приоритет, чем параметры
# в файле config/environment.rb
# Среда эксплуатации предназначена для готовых, "живых" приложений.
# Код не перезагружается при каждом запросе.
config.cache_classes = true
# Для распределенной среды использовать другой протокол.
# config.logger = SyslogLogger.new
# Полные отчеты об ошибках запрещены, кэширование включено.
config.action_controller.consider_all_requests_local = false
config.action_controller.perform_caching = true
# Разрешить отправку изображений, таблиц стилей и javascript-сценариев
# с сервера статического контента.
# config.action_controller.asset_host = "http://assets.example.com"
# Отключить ошибки доставки почты, недопустимые электронные адреса
# игнорируются.
# config.action_mailer.raise_delivery_errors = false

Нестандартные среды

При необходимости для приложения Rails можно создать нестандартную среду исполнения, скопировав и изменив один из существующих файлов в каталоге config/environments. Чаще всего это делают ради формирования дополнительных вариантов среды эксплуатации, например, для постадийной работы (staging) или контроля качества.

У вас есть доступ к промышленной базе данных с рабочей станции, на которой ведется разработка? Тогда имеет смысл организовать смешанную (triage) среду. Для этого клонируйте обычные параметры режима разработки, но в описании соединения с базой данных укажите на промышленный сервер. Такая комбинация может оказаться полезной для быстрой диагностики ошибок в процессе промышленной эксплуатации.

Протоколирование

В большинстве программных контекстов в Rails (моделях, контроллерах, шаблонах представлениях) присутствует атрибут logger, в котором хранится ссылка на объект протоколирования, согласованный с интерфейсом Log4r или с применяемым по умолчанию в Ruby 1.8+ классом Logger. Чтобы получить ссылку на объект logger из любого места программы, воспользуйтесь константой RAILS_DEFAULT_LOGGER. Для нее даже есть специальная комбинация клавиш в редакторе TextMate (rdb →).

В Ruby совсем нетрудно создать новый объект Logger:

$ irb
> require 'logger'
=> true
irb(main):002:0> logger = Logger.new STDOUT
=> #<Logger:0x32db4c @level=0, @progname=nil, @logdev=
#<Logger::LogDevice:0x32d9bc ... >
> logger.warn "do not want!!!"
W, [2007-06-06T17:25:35.666927 #7303] WARN -- : do not want!!!
=> true

> logger.info “in your logger, giving info”
I, [2007-06-06T17:25:50.787598 #7303] INFO — : in your logger, giving
your info
=> true

Обычно сообщение в протокол добавляется путем вызова того или иного метода объекта logger, в зависимости от серьезности ситуации. Определены следующие стандартные уровни серьезности (в порядке возрастания):

  • debug – указывайте этот уровень для вывода данных, полезных в будущем для отладки. В режиме эксплуатации сообщения такого уровня обычно не пишутся;
  • info – этот уровень служит для вывода информационных сообщений. Я обычно использую его, чтобы запротоколировать, снабдив временными штампами, необычные события, которые все же укладываются в рамки корректного поведения приложения;
  • warn – данный уровень служит для вывода информации о необычных ситуациях, которые имеет смысл расследовать подробнее.Иногда я вывожу в протокол предупреждающие сообщения, когда в программе срабатывает сторожевой код, препятствующий клиенту выполнить недопустимое действие. Цель при этом – уведомить лицо, ответственное за сопровождение, о злонамеренном пользователе или об ошибке в пользовательском интерфейсе, например:

def create
begin
@group.add_member(current_user)
flash[:notice] = "Вы успешно присоединились к #{@scene.display_name}"
rescue ActiveRecord::RecordInvalid
flash[:error] = "Вы уже входите в группу #{@group.name}"
logger.warn "Пользователь пытался дважды присоединиться к одной и
той же группе. Пользовательский интерфейс не должен
это разрешать."
end
redirect_to :back
end

  • error – этот уровень служит для вывода информации об ошибках, не требующих перезагрузки сервера;
  • fatal – самое страшное, что может случиться. Ваше приложение теперь неработоспособно, для его перезапуска необходимо ручное вмешательство.

Протоколы Rails

В папке log приложения Rails хранится три файла-протокола, соответствующих трем стандартным средам, а также протокол и pid-файл сервера Mongrel. Файлы-протоколы могут расти очень быстро. Чтобы упростить очистку протоколов, предусмотрено задание rake:

rake log:clear # Усекает все файлы *.log files в log/ до нулевой длины

На этапе разработке очень полезным может оказаться содержимое файла log/development.log. Разработчики часто открывают окно терминала, в котором запущена команда tail -f, чтобы следить, что пишется в этот файл:

$ tail -f log/development.log
User Load (0.000522) SELECT * FROM users WHERE (users.'id' = 1)
CACHE (0.000000) SELECT * FROM users WHERE (users.'id' = 1)

В протокол разработки выводится много интересной информации, например при каждом запросе в протокол – полезные сведения о нем. Ниже приведен пример, взятый из одного моего проекта, и описание выводимой информации:

Processing UserPhotosController#show (for 127.0.0.1 at 2007-06-06
17:43:13) [GET]
Session ID: b362cf038810bb8dec076fcdaec3c009
Parameters: {"/users/8-Obie-Fernandez/photos/406"=>nil,
"action"=>"show", "id"=>"406", "controller"=>"user_photos",
"user_id"=>"8-Obie-Fernandez"}
User Load (0.000477) SELECT * FROM users WHERE (users.'id' = 8)
Photo Columns (0.003182) SHOW FIELDS FROM photos
Photo Load (0.000949) SELECT * FROM photos WHERE (photos.'id' = 406
AND (photos.resource_id = 8 AND photos.resource_type = 'User'))
Rendering template within layouts/application
Rendering photos/show
CACHE (0.000000) SELECT * FROM users WHERE (users.'id' = 8)
Rendered adsense/_medium_rectangle (0.00155)
User Load (0.000541) SELECT * FROM users WHERE (users.'id' = 8)
LIMIT 1
Message Columns (0.002225) SHOW FIELDS FROM messages
SQL (0.000439) SELECT count(*) AS count_all FROM messages WHERE
(messages.receiver_id = 8 AND (messages.'read' = 0))
Rendered layouts/_header (0.02535)
Rendered adsense/_leaderboard (0.00043)
Rendered layouts/_footer (0.00085)
Completed in 0.09895 (10 reqs/sec) | Rendering: 0.03740 (37%) | DB:
0.01233 (12%) | 200 OK [http://localhost/users/8-Obie-
Fernandez/photos/406]
User Columns (0.004578) SHOW FIELDS FROM users

 

  • контроллер и вызванное действие;
  • IP-адрес компьютера, отправившего запрос;
  • временной штамп, показывающий, когда поступил запрос;
  • идентификатор сеанса, ассоциированного с этим запросом;
  • хеш параметров запроса;
  • информация о запросе к базе данных, включая время и текст предложения SQL;
  • информация о попадании в кэш, включая время и текст SQL-запроса, ответ на который был получен из кэша, а не путем обращения к базе данных;
  • информация о рендеринге каждого шаблона, использованного при выводе представления, и время, затраченное на обработку шаблона;
  • общее время выполнения запроса и вычисленное по нему число запросов в секунду;
  • сравнение времени, потраченного на операции с базой данных и на рендеринг;
  • код состояния HTTP и URL для ответа, отправленного клиенту.

Анализ протоколов

Используя протокол разработки и толику здравого смысла, можно легко выполнить различные виды неформального анализа.
Производительность. Изучение производительности приложения один из напрашивающихся видов анализа. Чем быстрее выполняется запрос, тем больше запросов сможет обслужить данный процесс Rails.Поэтому производительность часто выражается в запросах в секунду. Найдите, для каких запросов обращение к базе данных и рендеринг выполняются долго, и разберитесь в причинах.

Важно понимать, что время, сохраняемое в протоколе, не отличается повышенной точностью. Оно, скорее, даже неправильно просто потому, что очень трудно замерить временные характеристики процесса, находясь внутри него. Сложив процентные доли времени, затраченного на обращение к базе данных и на рендеринг, вы далеко не всегда получите величину, близкую к 100%.

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

SQL-запросы. ActiveRecord ведет себя не так, как вы ожидали? Протоколирование текста SQL-запроса, сгенерированного ActiveRecord, часто помогает отладить ошибки, связанные со сложными запросами.

Выявление ошибок вида N+1 select. При отображении некоторой записи вместе с ассоциированным с ней набором записей есть шанс допустить так называемую ошибку вида N+1 select. Ее признак – наличие серии из многих предложений SELECT, отличающихся только значением первичного ключа.
Вот, например, фрагмент протокола реального приложения Rails, демонстрирующий ошибку N+1 select в том, как загружаются экземпляры класса FlickrPhoto:

FlickrPhoto Load (0.001395) SELECT * FROM flickr_photos WHERE
(flickr_photos.resource_id = 15749 AND flickr_photos.resource_type =
'Place' AND (flickr_photos.'profile' = 1)) ORDER BY updated_at desc
LIMIT 1
FlickrPhoto Load (0.001734) SELECT * FROM flickr_photos WHERE
(flickr_photos.resource_id = 15785 AND flickr_photos.resource_type =
'Place' AND (flickr_photos.'profile' = 1)) ORDER BY updated_at desc
LIMIT 1
FlickrPhoto Load (0.001440) SELECT * FROM flickr_photos WHERE
(flickr_photos.resource_id = 15831 AND flickr_photos.resource_type =
'Place' AND (flickr_photos.'profile' = 1)) ORDER BY updated_at desc
LIMIT 1

… и так далее, и так далее на протяжении многих и многих страниц протокола. Знакомо?

К счастью, на каждый из этих запросов к базе уходит очень небольшое время – примерно 0,0015 с. Это объясняется тем, что:
1) MySQL исключительно быстро выполняет простые предложения SELECT;
2) мой процесс Rails работал на машине, где находилась база данных.

И тем не менее суммарно эти N запросов способны свести производительность на нет. Если бы не вышеупомянутые компенсирующие факторы, я столкнулся бы с серьезной проблемой, причем наиболее явственно она проявлялась бы при расположении базы данных на отдельной машине, поскольку ко времени выполнения каждого запроса добавлялись бы еще и сетевые задержки.
Проблема N+1 select – это еще не конец света. В большинстве случаев для ее решения достаточно правильно пользоваться параметром :include в конкретном вызове метода find.

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

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

Однако существует немало возможностей для ползучего проникновения в ваш код неявных операций доступа к базе данных на этапе рендеринга. Иногда эти операции инкапсулируются в модели, а иногда выполняются в ходе отложенной загрузки ассоциаций. Можем ли мы решительно осудить такую практику? Трудно дать определенный ответ. Бывают случаи (например, при кэшировании фрагментов), когда обращение к базе на этапе рендеринга имеет смысл.

Использование альтернативных схем протоколирования

Легко! Достаточно присвоить одной из переменных класса logger, например ActiveRecord::Base.logger, объект класса, совместимого с классом Logger из стандартного дистрибутива Ruby. Простой прием, основанный на возможности подмены объектов протоколирования, демонстрировался Дэвидом на различных встречах, в том числе в основном докладе на конференции Railsconf 2007. Открыв консоль, присвойте ActiveRecord::Base. logger новый экземпляр класса Logger, указывающий на STDOUT. Это позволит вам просматривать генерируемые SQL-запросы прямо на консоли.

Syslog

В различных вариантах ОС UNIX имеется системная служба syslog. Есть ряд причин, по которым она может оказаться более удобным средством протоколирования работы Rails-приложения в режиме эксплуатации:

  • более точный контроль над уровнями протоколирования и содержимым сообщений;
  • консолидация протоколов нескольких приложений Rails;
  • при использовании дистанционных средств syslog возможна консолидация протоколов приложений Rails, работающих на разных серверах. Конечно, это удобнее, чем обрабатывать разрозненные протоколы, хранящиеся на каждом сервере приложений в отдельности.

 

КОММЕНТАРИИ