Тонкости Rails 4 — Thread-Safety

от автора

В Rails 4.0 по умолчанию будет включена опция config.threadsafe! и в данном уроке вы узнаете о том, что же она все-таки делает, как влияет на production и как вообще стоит вести себя с потоками.


Cодержание цикла «Тонкости Rails 4»


Какое-то время назад Aaron Patterson (Tenderlove) опубликовал запись в своем блоге. В ней он рассказал об опции threadsafe в рельсах и упомянул о том, что скорее всего она будет по умолчанию включена в релизе Rails 4. Поэтому в этом эпизоде я расскажу именно о ней.

Для начала создадим простое приложение на Rails 3 под названием thready:

terminal

$ rails new thready $ cd thready        

Открыв и оглядев конфигурационный файл приложения для продакшена видно, что опция закомментирована:

/config/environments/production.rb

# Enable threaded mode # config.threadsafe! 

Необходимо понимать, что ее включение не означает волшебного превращения в мультипоточное приложение. Что же в таком случае она делает? При просмотре исходного кода метода threadsafe! видно, что происходит установка набора опций:

def threadsafe!                 @preload_frameworks = true    @cache_classes      = true    @dependency_loading = false   @allow_concurrency  = true    self                        end                           

Первые 3 опции отвечают за так называемую нетерпеливую загрузку приложения. Таким образом при запуске приложения оно загружается сразу же целиком, вместо загрузки по частям, как это происходит при отключенном threadsafe. 


Четвертая опция, allow_concurrency отрубает использование промежуточного слоя Rack::Lock. Запустив в терминале, в режиме разработчика команду rake middleware видно, что Rack::Lock — это один из первых слоев, регистрирующий активность:

terminal

$ rake middleware          use ActionDispatch::Static use Rack::Lock             # Остальная часть стека опущена. 

Включив опцию threadsafe в продакшене и запустив:

$ rake middleware RAILS_ENV=production  

Этого слоя больше не будет видно. Для ответа на вопрос «А что же делает Rack::Lock» стоит взглянуть в исходный код (внимание, файл успел претерпеть изменения):

/lib/rack/lock.rb

require 'thread'          require 'rack/body_proxy'                           module Rack                 class Lock                  FLAG = 'rack.multithread'.freeze                                                      def initialize(app, mutex = Mutex.new)       @app, @mutex = app, mutex                end                                                                                   def call(env)                                old, env[FLAG] = env[FLAG], false          @mutex.lock                                response = @app.call(env)                  response[2] = BodyProxy.new(response[2]) { @mutex.unlock }                                response                rescue Exception            @mutex.unlock             raise                   ensure                      env[FLAG] = old         end                     end                     end                       

Когда в метод call приходит очередной запрос, то внутри него на объект mutex накладывается блокировка и только после этого происходит обработка запроса, после чего блок снимается. Такой подход гарантирует, что в каждый очередной момент времени будет обработан только один запрос. Для лучшего понимания процесса я покажу это наглядно. Для этого создадим новый контроллер с одним методом:


terminal

$ rails g controller foo bar 

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

/app/controllers/foo_controller.rb

class FooController < ApplicationController   def bar     sleep 1     render text: "foobar\n"   end end 

Теперь запустим сервер в режиме development. Заметьте, что используется WEBrick (по умолчанию это как раз он и есть). Теперь, в отдельной вкладке консоли сделаем запрос к сайту с помощью curl:



terminal

$ curl http://localhost:3000/foo/bar foobar 

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

% repeat 5 (curl http://localhost:3000/foo/bar &) % foobar foobar foobar foobar foobar 

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

Теперь запустим сервер в режиме продакшена и снова сделаем к нему 5 параллельных запросов:



terminal

% rails s -e production % repeat 5 (curl http://localhost:3000/foo/bar &) 

Все 5 ответов от сервера придут одновременно, благодаря включенной опции threadsafe!, из-за которой слой Rack::Lock больше не используется. Так что теперь запросы обрабатываются асинхронно, ура!



Означает ли все это, что при включенной многопоточности теперь нужно начинать писать потоко-безопасный код? На самом деле, все зависит от настроек продакшена. Большинство из популярных rails-серверов, таких как Unicorn и Phusion Passenger в каждый момент времени пропускают только один запрос через одного worker’а. Другими словами, даже при включенной опции запросы будут обрабатываться по очереди.



Можно посмотреть как будет вести себя Unicorn. Для этого необходимо необходимо раскоментировать следующую строчку:



/Gemfile

# Use unicorn as the app server gem 'unicorn' 

После чего запустить bundle install. Теперь с помощью команды unicorn можно запустить сервер для Rails-приложение в режиме production:



terminal

$ unicorn -E production -p 3000 

Запустив теперь вновь curl будет видно, что нет никакой многопоточности. Именно так работает Unicorn c одним worker’ом. Для Unicorn’а не требуется дополнительного mutex’а и слоя Rack::Lock. Именно по этой причине опция threadsafe! будет включена по умолчанию в продакшене: логика для обработки запросов и потоков отдается на откуп окружению в продакшене. 

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



Еще одно небольшое замечание: опция threadsafe! может сменить свое название в релизе 4 рельс с целью лучше показать логику ее работы.

Итак, мы уже знаем, что Unicorn и Passenger не поддерживают многопоточность, а что же делать, если хочется сервер с ее поддержкой? На помощь приходит puma. Этот сервер основан на Mongrel, благодаря чему способен запускать любое Rack-приложение с использованием нескольких потоков. Puma поддерживает JRuby, Rubinius и даже MRI. Давайте попробуем:



/Gemfile

# Use unicorn as the app server # gem 'unicorn' gem 'puma' 

Теперь запустим сервер в режиме продакшена:


terminal

$ rails s puma -e production 

Запустив теперь curl будет видно, что ответы от сервера приходят почти мгновенно.



Puma не очень хорошо будет работать в MRI, из-за встроенного в эту версию руби механизма под названием Global Interpreter Lock. Но JRuby и Rubinius обладают более качественной поддержкой потоков, так что в них Пума будет работать лучше.




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



/app/controllers/foo_controller.rb

class FooController < ApplicationController   @@counter = 0   def bar     counter = @@counter     sleep 1     counter += 1     @@counter = counter     render text: "#{@@counter}\n"   end end 

В контроллере имеется переменная класса под названием @@counter, увеличивающуюся на 1 при каждом вызове метода bar. В самом методе мы сохраняем значение переменной класса, спим секунду, после чего возвращаем значение и отображаем его на экран. Посмотрим, как все будет работать в однопоточной среде:

terminal

$ rails s 

Запустим curl 4 раза, после чего появятся 4 цифры, каждая — с секундной задержкой:


terminal

% repeat 4 (curl http://localhost:3000/foo/bar &) % 1 2 3 4 

Теперь остановим сервер, и запустим Puma в режиме продакшена:

terminal

$ rails s puma -e production 

Ее вывод будет отличаться:

terminal

% repeat 4 (curl http://localhost:3000/foo/bar &) % 1 1 1 1 

Теперь запросы обрабатываются одновременно, в нескольких потоках. Таким образом последний запрос успел завершиться еще до того, как первый успел довести свою работу до конца. Поэтому следует быть очень аккуратным в работе с данными (@@counter, к примеру), к которым имеют доступ несколько потоков. Для разрешения возникшей проблемы необходимо воспользоваться объектом типа mutex:


/app/controllers/foo_controller.rb

class FooController < ApplicationController   @@counter = 0   @@mutex = Mutex.new    def bar     @@mutex.synchronize do       counter = @@counter       sleep 1       counter += 1       @@counter = counter     end     render text: "#{@@counter}"   end end 

Обрамленный код mutex’ом становится потоко-безопасным. После этого нужно перезапустить сервер приложения и вновь воспользоваться curl. Счетчик теперь работает корректно, поскольку код обрабатывает потоки последовательно друг за другом.

Такие вещи как переменные класса и объекта, глобальные переменные и константы необходимо использовать внутри mutex’а. Несмотря на предупреждения интерпетатора константы в руби можно изменить. Как и строки, впрочем. В случае со строками стоит воспользоваться методом freeze для запрета на их изменение. И не забудьте, пожалуйста, что код внутри класса является shared memory, поэтому не надо вставлять методы в сам класс динамически после загрузки приложения.

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

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

Есть еще одна проблема, с которой можно столкнуться в мультипоточном приложении и связана она с пулом соединений базы данных. Этот пул задает количество одновременных соединений к бд и по умолчанию равен 5. Для демонстрации я закомментил mutex.

/app/controllers/foo_controller.rb

def bar   #@@mutex.synchronize do     counter = @@counter     sleep 1     counter += 1     @@counter = counter   #end   render text: "#{@@counter}\n" end 

Попробовав теперь сделать 12 одновременных запросов к приложению станет видно, что только 4 из них прошли, из-за ограничения количества соединений. Несмотря на то, что в данном методе не происходит запроса к бд, rails все равно резервирует соединения на протяжении каждого запроса. И если во время запроса будет не хватать соединений, то приложение будет ждать, пока не получит своего. Таким образом, если запрос затянется по времени, то начнут сыпаться timeout ошибки и другие запросы тоже отвалятся по причине нехватки доступных соединений из пула бд.

Увеличив значение в пуле до 15, можно повторить предыдущий запрос curl и все запросы пройдут успешно.

Спасибо за внимание!

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


Приложение

ссылка на оригинал статьи http://habrahabr.ru/post/172109/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *