Недавно вышла книга The dRuby book — distributed and parallel computing with Ruby (перевод японской книги, написанной автором самой библиотеки). В этой статье я попытаюсь дать обзор глав книги, касающихся библиотеки DRb. Если вам захочется ознакомиться с темой более подробно, книгу можно купить или скачать. Сразу скажу, что я не буду говорить в этом посте ни о синхронизации потоков, ни о библиотеке Rinda.
Предположим, что вы пишите систему, которая работает с более чем одним процессом. Например, у вас есть веб-сервер, который в фоновом режиме запускает задачи, работающие долгое время. Или вам просто нужно обеспечить пересылку данных из одного процесса в другой и координировать их. Для таких ситуаций и нужна библиотека DRb. Она написана целиком на Ruby и включена в стандартную библиотеку, поэтому начать работать с ней можно моментально. Для её подключения достаточно написать
Достоинства библиотеки DRb большей частью проистекают из динамичности самого языка Ruby.
Во-первых, при затрате минимальных усилий на подготовительном этапе, дальше вы работаете с объектами не задумываясь, где они расположены: в одном процессе или в другом. Библиотека полностью маскирует от вас все технические детали.
Во-вторых, вы не обязаны жестко прописывать интерфейс. Любой руби-объект может выставить свой интерфейс наружу — таким образом вы можете как воспользоваться функциональностью одного из стандартных классов типа
И наконец, клиент даже не обязан знать классы объектов, которые ему возвращает сервер, он может их использовать и без этого. Таким образом сервер волен скрыть столько много деталей, сколько ему угодно.
Но, конечно, есть и подводные камни, и их предостаточно. К счастью, dRuby несложен в понимании, ну а понимание его устройства позволяет большей части проблем просто не допускать. Документация к этой библиотеке, к сожалению, не проясняет множества моментов, поэтому статья будет интересна и новичкам, и людям уже поработавшим с библиотекой.
Для проверки того, что всё работает — откроем два irb терминала. Признаюсь, что не знаю, насколько велики отличия в ruby 1.8, так что давайте договоримся, что мы обсуждаем версию 1.9 (тем более, что 1.8 — скоро перестанет поддерживаться, ура!)
Условно эти два терминала — это сервер и клиент. Сервер должен предоставить front-объект, который будет принимать запросы. Этот объект может быть любым объектом: хоть объектом встроенного типа, хоть модулем со специально созданным интерфейсом. В свою очередь клиент, подключается к серверу и взаимодействует с этим объектом.
Давайте для примера в первом терминале запустим сервер и выставим наружу обыкновенный массив.
Теперь подключим клиент. Узнаем первый элемент массива и запишем в массив ещё один элемент
Теперь можно из первого терминала вызвать
Как вы уже заметили, сервер запускается командой
Когда сервер запускается в отдельном скрипте необходимо написать
Для того, чтобы подключиться к серверу необходимо вызвать метод
О смысле команды
Давайте всё-таки разберемся, каким образом выполняется выполнение метода удаленного объекта. Для этого вызов метода прокси-объекта выполняет сериализацию(маршализацию) имени метода и список аргументов, получившуюся строку по TCP-протоколу передаем серверу, который десериализует аргументы вызова, выполняет метод на фронтальном объекте, сериализует результат и передает его обратно клиенту. Всё выглядит просто. Фактически вы работаете с удаленным объектом так же как с обычным, а множество действий для удаленного выполнения метода прокси-объект и сервер от вас скрывают.
Но не всё так просто. Удаленный вызов метода — удовольствие дорогое. Представьте, что метод на сервере «дергает» множество методов аргумента. Это обернулось бы в то, что сервер и клиент вместо того, чтобы производить вычисления, львиную долю времени обращались бы друг к другу по сравнительно медленному протоколу (и хорошо ещё, если два процесса расположены на одной машине). Чтобы не допустить этого, аргументы и результат между процессами передаются по значению, а не по ссылке (маршализация объекта хранит в себе лишь внутреннее состояние объекта, и не знает его
Впрочем, всегда можно определить поведение dRuby так, чтобы передавать аргументы и результат по ссылке, но об этом позже.
Теперь настало время поговорить о первой проблеме. Не все объекты поддаются маршализации. Например, Proc- и IO-объекты, а также потоки (Thread-объекты) не могут быть маршализованы и переданы по копии. dRuby в этом случае поступает следующим образом: если маршализация не сработала, то объект передается по ссылке.
Итак, как же объект передается по ссылке? Вспомним Си. Там для этой цели используются указатели. В Руби роль указателя выполняет
Сделаем нашему серверу метод
И с клиента запустим код.
Новый метод
Но каждый раз писать
Теперь объект
Приведу пример, который встречался мне на практике. Сервер выставляет фронтальным объектом хэш, в котором значениями являются тикеты (изнутри тикет представляет из себя машину состояний). Тогда
А теперь пора вспомнить про обещание и рассказать, для чего нужно вызывать
А теперь пусть клиент вызовет метод
Фактически на фронтальном объекте вызывается метод map c аргументом-блоком. А его, как мы помним, маршализовать не получается. Значит этот блочный аргумент передается по ссылке. Метод
Здесь есть риск наткнуться на новую неприятность. Допустим, метод вернул вам ссылку (не копию) на объект, сгенерированный прямо в методе. Если сервер этот объект нигде отдельно не сохранил (например, не поместил в специальный хэш), то сервер на него не имеет ссылки. Клиент на удаленной машине имеет, а сервер — нет! Поэтому поздно или ранотролль придет к тебе за почтой за этим объектом придет GC — сборщик мусора. Это означает, что через некоторое время ссылка типа
Поэтому надо заботиться о том, чтобы сервер хранил ссылки на возвращаемые объекты по крайней мере до тех пор, пока они будут использоваться сервером. Для этого есть несколько решений:
1) сохранять все возвращаемые объекты, передаваемые по ссылке, в массив — тогда сборщик мусора их не будет собирать, ведь ссылка используются;
2) передавать клиенту ссылку в блок. Например:
Вместо такого кода:
Следует писать так:
Действующая локальная переменная на сервере не может быть собрана сборщиком мусора. Значит, внутри блока ссылка гарантировано будет работать.
3) В книге описан ещё один способ — надо вклиниться в процесс создания ссылки на этапе получения
Последний способ можно реализовать, выполнив
перед запуском сервера. За более подробной информацией обращайтесь к главе 11 книги — Handling Garbage Collection. Мне она кажется интересной и возможно у вас после её прочтения появятся новые способы использования манипулирования процессом сборки мусора. Но всё-таки я думаю, что на практике лучше использовать второй способ — и выдавать ссылки в блок. Надежнее и понятнее.
Осталось осветить, наверное, последний момент. Предположим, что вы передаете объект
А теперь представьте, что вы передаете не ссылку, а копию. Сериализация на сервере сохранила состояние объекта и имя его класса. Клиент получил строку и пытается десериализовать её. У него это, конечно, не получается, ведь клиент не может создать объект несуществующего класса
Нет, всё же это не последний момент. Я обещал не писать про синхронизацию, но всё же пару слов скажу.
Для распределенного программирования синхронизация действий и атомарность операций — критически важные понятия. Сервер запускается в отдельном потоке. И на каждый запрос к серверу автоматически создается отдельный поток, в котором этот запрос обрабатывается. Так что просто необходимо запретить разным потокам обращаться к одной и той же информации одновременно. Так что, программируя распределенные и параллельные системы используйте:
1)конструкцию
2)модуль стандартной библиотеки
3)классы стандартной библиотеки
Удачи в использовании DRb! Надеюсь, я предотвратил кому-нибудь долгие часы в попытках понять, почему объект не изменяется, хотя вы и применяете деструктивный метод, как заставить работать на клиенте метод, принимающий блок, и почему полученная ссылка работала-работала — и вдруг перестала.
Впрочем, в книге вы найдете намного больше, особенно про библиотеку Rinda и её собратьев.
Предположим, что вы пишите систему, которая работает с более чем одним процессом. Например, у вас есть веб-сервер, который в фоновом режиме запускает задачи, работающие долгое время. Или вам просто нужно обеспечить пересылку данных из одного процесса в другой и координировать их. Для таких ситуаций и нужна библиотека DRb. Она написана целиком на Ruby и включена в стандартную библиотеку, поэтому начать работать с ней можно моментально. Для её подключения достаточно написать
require 'drb'
Достоинства библиотеки DRb большей частью проистекают из динамичности самого языка Ruby.
Во-первых, при затрате минимальных усилий на подготовительном этапе, дальше вы работаете с объектами не задумываясь, где они расположены: в одном процессе или в другом. Библиотека полностью маскирует от вас все технические детали.
Во-вторых, вы не обязаны жестко прописывать интерфейс. Любой руби-объект может выставить свой интерфейс наружу — таким образом вы можете как воспользоваться функциональностью одного из стандартных классов типа
Hash
или Queue
, а можете сделать свой класс с любым интерфейсом. Кроме того вам ничто не мешает менять интерфейс прямо в процессе исполнения, и даже использовать method_missing
для обработки любых запросов. И уж разумеется, обновление интерфейса сервера вообще никак не влияет на клиента, если тот не вызывает методы, которые изменили сигнатуру или поведение. Таким образом сервер и клиент максимально независимы.И наконец, клиент даже не обязан знать классы объектов, которые ему возвращает сервер, он может их использовать и без этого. Таким образом сервер волен скрыть столько много деталей, сколько ему угодно.
Но, конечно, есть и подводные камни, и их предостаточно. К счастью, dRuby несложен в понимании, ну а понимание его устройства позволяет большей части проблем просто не допускать. Документация к этой библиотеке, к сожалению, не проясняет множества моментов, поэтому статья будет интересна и новичкам, и людям уже поработавшим с библиотекой.
Для проверки того, что всё работает — откроем два irb терминала. Признаюсь, что не знаю, насколько велики отличия в ruby 1.8, так что давайте договоримся, что мы обсуждаем версию 1.9 (тем более, что 1.8 — скоро перестанет поддерживаться, ура!)
Условно эти два терминала — это сервер и клиент. Сервер должен предоставить front-объект, который будет принимать запросы. Этот объект может быть любым объектом: хоть объектом встроенного типа, хоть модулем со специально созданным интерфейсом. В свою очередь клиент, подключается к серверу и взаимодействует с этим объектом.
Давайте для примера в первом терминале запустим сервер и выставим наружу обыкновенный массив.
require 'drb'
front = []
DRb.start_service('druby://localhost:1234', front)
front << 'first'
# если вы запускаете сервер не в консоли, а отдельным скриптом - обязательно добавьте строку DRb.thread.join
Теперь подключим клиент. Узнаем первый элемент массива и запишем в массив ещё один элемент
require 'drb'
DRb.start_service
remote_obj = DRbObject.new_with_uri('druby://localhost:1234')
p remote_obj
p remote_obj[0]
remote_obj << 'second'
Теперь можно из первого терминала вызвать
front[1]
и увидеть, что там находится строка 'second'
. А можно подключить ещё один клиент, и из него тоже оперировать тем же самым фронтальным объектом.Как вы уже заметили, сервер запускается командой
DRb.start_service
(следите за регистром!). Метод принимает в качестве аргумента строку с адресом вида 'druby://hostname:port'
и фронтальный объект. Фронтальный объект — это объект, который будет принимать запросы.Когда сервер запускается в отдельном скрипте необходимо написать
DRb.thread.join
в конце скрипта. Дело в том, что DRb-сервер запускается отдельным потоком, а руби завершает работу программы, как только завершился главный поток. Поэтому если главный поток не подождет закрытия потока DRb-сервера, то досрочно завершатся оба потока, и сервер мгновенно станет недоступен. Будьте готовы к тому, что выполнение метода DRb.Thread.join
блокирует текущий поток до момента, пока сервер не будет выключен.Для того, чтобы подключиться к серверу необходимо вызвать метод
DRbObject.new_with_uri
и передать ему в качестве аргумента адрес, по которому запускается сервер. Этот метод вернет прокси-объект remote_obj
. Запросы (вызовы методов) к прокси-объекту автоматически передаются объекту на удаленном сервере, там вызванный метод выполняется, а затем результат возвращается вызвавшему метод клиенту. (Впрочем, не все методы вызываются на сервере. Например, судя по поведению, метод #class
выполняется локально)О смысле команды
DRb.start_service
у клиента речь пойдет чуть позднее.Давайте всё-таки разберемся, каким образом выполняется выполнение метода удаленного объекта. Для этого вызов метода прокси-объекта выполняет сериализацию(маршализацию) имени метода и список аргументов, получившуюся строку по TCP-протоколу передаем серверу, который десериализует аргументы вызова, выполняет метод на фронтальном объекте, сериализует результат и передает его обратно клиенту. Всё выглядит просто. Фактически вы работаете с удаленным объектом так же как с обычным, а множество действий для удаленного выполнения метода прокси-объект и сервер от вас скрывают.
Но не всё так просто. Удаленный вызов метода — удовольствие дорогое. Представьте, что метод на сервере «дергает» множество методов аргумента. Это обернулось бы в то, что сервер и клиент вместо того, чтобы производить вычисления, львиную долю времени обращались бы друг к другу по сравнительно медленному протоколу (и хорошо ещё, если два процесса расположены на одной машине). Чтобы не допустить этого, аргументы и результат между процессами передаются по значению, а не по ссылке (маршализация объекта хранит в себе лишь внутреннее состояние объекта, и не знает его
object_id
— стало быть объект сначала сериализованный, а затем десериализованный будет лишь копией исходного объекта, но никак не тем же самым объектом, так что передача автоматически производится по копии). В Ruby обычно всё передается по ссылке, а в dRuby обычно — по значению. Таким образом, если вы выполните front[0].upcase!
на сервере, то значение front[0]
изменится, а если вы выполните remote_obj[0].upcase!
, то вы получите первый элемент в верхнем регистре, но значение на сервере не поменяется, так как remote_obj.[](0)
— это копия первого элемента. Можно считать этот вызов аналогичным методу front[0].dup.upcase!
Впрочем, всегда можно определить поведение dRuby так, чтобы передавать аргументы и результат по ссылке, но об этом позже.
Теперь настало время поговорить о первой проблеме. Не все объекты поддаются маршализации. Например, Proc- и IO-объекты, а также потоки (Thread-объекты) не могут быть маршализованы и переданы по копии. dRuby в этом случае поступает следующим образом: если маршализация не сработала, то объект передается по ссылке.
Итак, как же объект передается по ссылке? Вспомним Си. Там для этой цели используются указатели. В Руби роль указателя выполняет
object_id
. Для передачи объекта по ссылке используется объект класса DRbObject
.DRbObject
— это, фактически, прокси-объект для передачи по ссылке. Экземпляр этого класса DRbObject.new(my_obj)
содержит object_id
объекта my_obj
и URI-адрес сервера, откуда объект пришел. Это позволяет перехватить вызов метода и передать его именно тому объекту на удаленной машине (или в другом терминале), которому метод предназначался.Сделаем нашему серверу метод
def front.[](ind)
DRbObject.new(super)
end
И с клиента запустим код.
remote_obj.[0].upcase!
Новый метод
#[]
вернул не копию первого элемента, а ссылку, так что после выполнения метода upcase!
фронтальный объект изменился, это легко проверить, выполнив, например команду puts remote_obj
или puts front
— с клиента и сервера соответственно.Но каждый раз писать
DRbObject.new
— лень. К счастью, есть и другой способ передавать объект по ссылке, а не по значению. Для этого достаточно сделать объект немаршализуемым. Сделать это легко, достаточно включить в объект модуль DRbUndumped
.my_obj.extend DRbUndumped
class Foo; include DRbUndumped; end
Теперь объект
my_obj
и все объекты класса Foo
будут автоматически передаваться по ссылке (а Marshal.dump(my_obj)
будет выдавать TypeError 'can\'t dump'
).Приведу пример, который встречался мне на практике. Сервер выставляет фронтальным объектом хэш, в котором значениями являются тикеты (изнутри тикет представляет из себя машину состояний). Тогда
remote_obj[ticket_id]
выдает копию тикета. Но это не позволяет нам изменить состояние тикета на сервере, только локально. Давайте заинклюдим DRbUndumped
в класс Ticket
. Теперь мы из хэша получаем не копию тикета, а ссылку на него — и любые действия с ним случаются теперь не на клиенте, а прямо на сервере.А теперь пора вспомнить про обещание и рассказать, для чего нужно вызывать
DRb.start_service
у клиента. Представьте, что у вас на сервере указан фронтальным объектом массив, как в первом примере.А теперь пусть клиент вызовет метод
remote_obj.map{|x| x.upcase}
Фактически на фронтальном объекте вызывается метод map c аргументом-блоком. А его, как мы помним, маршализовать не получается. Значит этот блочный аргумент передается по ссылке. Метод
map
на сервере будет обращаться к нему инструкцией yield
, а значит клиент является сервером! Но раз клиент должен время от времени быть сервером — значит он тоже должен запустить DRb-сервер методом start_service
. При этом не обязательно указывать URI этого сервера. Как это работает изнутри я не знаю, но работает. И как вы уже заметили, отличия между клиентом и сервером меньше, чем может показаться.Здесь есть риск наткнуться на новую неприятность. Допустим, метод вернул вам ссылку (не копию) на объект, сгенерированный прямо в методе. Если сервер этот объект нигде отдельно не сохранил (например, не поместил в специальный хэш), то сервер на него не имеет ссылки. Клиент на удаленной машине имеет, а сервер — нет! Поэтому поздно или рано
DRbObject
у клиента «протухнет» и станет указывать в никуда. Попытка обратиться к методам этого объекта вызовет ошибку.Поэтому надо заботиться о том, чтобы сервер хранил ссылки на возвращаемые объекты по крайней мере до тех пор, пока они будут использоваться сервером. Для этого есть несколько решений:
1) сохранять все возвращаемые объекты, передаваемые по ссылке, в массив — тогда сборщик мусора их не будет собирать, ведь ссылка используются;
2) передавать клиенту ссылку в блок. Например:
Вместо такого кода:
Ticket.send :include, DRbUndumped
def front.get_ticket
Ticket.new
end
foo = remote_obj.get_ticket
foo.start
foo.closed? # Здесь ссылка foo может уже не иметь оригинала на сервере. Особенно, если метод start длится достаточно долго.
Следует писать так:
Ticket.send :include, DRbUndumped
def front.get_ticket
object_to_reference = Ticket.new
yield object_to_reference
end
remote_obj.get_ticket do |foo|
foo.start
foo.closed?
end
Действующая локальная переменная на сервере не может быть собрана сборщиком мусора. Значит, внутри блока ссылка гарантировано будет работать.
3) В книге описан ещё один способ — надо вклиниться в процесс создания ссылки на этапе получения
object_id
объекта и попробовать в этот момент так или иначе оттянуть процесс сборки мусора. Можно автоматически добавлять элемент в хэш и хранить объект вечно (как вы можете догадаться, память рано или поздно кончится), можно хранить ссылку на объект и очищать её вручную, можно очищать этот хэш раз в несколько минут.Последний способ можно реализовать, выполнив
require 'drb/timeridconv'
DRb.install_id_conv(DRb::TimerIdConv.new)
перед запуском сервера. За более подробной информацией обращайтесь к главе 11 книги — Handling Garbage Collection. Мне она кажется интересной и возможно у вас после её прочтения появятся новые способы использования манипулирования процессом сборки мусора. Но всё-таки я думаю, что на практике лучше использовать второй способ — и выдавать ссылки в блок. Надежнее и понятнее.
Осталось осветить, наверное, последний момент. Предположим, что вы передаете объект
Foo
как ссылку. Клиент знать не знает ни про какой класс Foo
и тем не менее это не мешает ему работать с объектом. По сути клиент оперирует с объектом класса DRbObject
. Всё, как обычно.А теперь представьте, что вы передаете не ссылку, а копию. Сериализация на сервере сохранила состояние объекта и имя его класса. Клиент получил строку и пытается десериализовать её. У него это, конечно, не получается, ведь клиент не может создать объект несуществующего класса
Foo
. Тогда десериализация вернет объект типа DRb::DRbUnknown
, который будет хранить буфер с маршализованным объектом. Этот объект можно передать дальше (например, в очередь задач). Также можно узнать имя класса, подгрузить соответствующую библиотеку с классом и вызвать метод reload
— тогда будет предпринята ещё одна попытка произвести десериализацию. Это Нет, всё же это не последний момент. Я обещал не писать про синхронизацию, но всё же пару слов скажу.
Для распределенного программирования синхронизация действий и атомарность операций — критически важные понятия. Сервер запускается в отдельном потоке. И на каждый запрос к серверу автоматически создается отдельный поток, в котором этот запрос обрабатывается. Так что просто необходимо запретить разным потокам обращаться к одной и той же информации одновременно. Так что, программируя распределенные и параллельные системы используйте:
1)конструкцию
lock = Mutex.new; lock.synchronize{ do_smth }
2)модуль стандартной библиотеки
MonitorMixin
3)классы стандартной библиотеки
Queue
, SizedQueue
Удачи в использовании DRb! Надеюсь, я предотвратил кому-нибудь долгие часы в попытках понять, почему объект не изменяется, хотя вы и применяете деструктивный метод, как заставить работать на клиенте метод, принимающий блок, и почему полученная ссылка работала-работала — и вдруг перестала.
Впрочем, в книге вы найдете намного больше, особенно про библиотеку Rinda и её собратьев.