PyRO: Python Remote Objects

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

В большинстве языков программирования предусмотрены стандартные средства для реализации параллельной работы. В python для этого используются две стандартные библиотеки: threading и multiprocessing. Рассмотрим подробнее особенности работы с ними.

Threading определяет высокоуровневые интерфейсы для работы нитями (потоками). На практике это означает, что в рамках одного процесса, с точки зрения ОС, создается несколько нитей (потоков), работающих далее параллельно. Неоспоримым достоинством такого подхода является общая память всех потоков, которая позволяет не задумываться о взаимодействии параллельных потоков друг с другом. Так, для передачи данных между потоками достаточно передать переменную, значение по ссылке будет доступно сразу. Но использование threading накладывает существенное ограничение в виде использования GIL (Global Interpreter Lock), который в большинстве случаев не даст нитям использовать несколько ядер процессора. Подробнее об устройстве и особенностях GIL можно прочесть в статье Андрея Светлова Загадочный GIL.

Multiprocessing позволяет работать с процессами на уровне операционной системы, предоставляя при этом интерфейс, очень похожий на threading. Но использование отдельных процессов влечет ограничение в виде раздельной памяти, поэтому возникают проблемы синхронизации и передачи данных, которые необходимо как-то решать. Для взаимодействия процессов в стандартной библиотеке предлагается использовать специальные объекты, обеспечивающие передачу данных, таких как Connection или Queue.

На практике использование threading позволяет почти без правок кода производить вызов функции в отдельном потоке, но взамен мы получаем ограничения, накладываемые GIL. Для использования multiprocessing необходимо вносить существенные правки в уже существующий код. Зачастую такие правки приходится делать на архитектурном уровне, поскольку изначально программа не предусматривает возможность использования очередей в качестве способа передачи данных.

Проиллюстрируем данную проблему на конкретном примере. Предположим, что у нас есть функция, обрабатывающая входные данные, не отпуская GIL, а затем сохраняет результат обработки данных в БД, которая работает некоторое продолжительное время:

Но нам нежелательно терять время, которое тратится на запись в базу, поэтому мы будем выносить работу этой функции в отдельный процесс. Так как в multiprocessing для связи процессов используются очереди, то логично использовать паттерн Producer-Consumer: продюсер будет добавлять задачи на запись в базу в общую очередь (Queue), а консьюмер будет по-очереди брать задачи из очереди и производить запись в базу.

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

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

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

Подводя итог, можно сказать, что стандартные средства в python достаточно трудоемки при внедрении в изначально однопоточную программу. Возникает закономерный вопрос: может быть существуют другие, универсальные и менее трудоемкие подходы? Один из таких подходов – это использование RPC (Remote Procedure Call).

Концепция RPC подразумевает возможность вызывать функции, которые будут выполняться в другом процессе/компьютере. Существует множество различных реализаций RPC, но большинство из них все равно достаточно громоздки в использовании. Так, например, стандарт CORBA разрабатывается около 20 лет, на данный момент версия 3.3 содержит более 1000 страниц описания стандарта, и не существует ни одной полной реализации стандарта. Есть готовые продукты, такие как ZeroC ICE – реализация RPC, использующая язык описания интерфейсов OMG IDL из CORBA, или Apache Thrift – фреймворк RPC, разработанный facebook. Каждый из этих продуктов является универсальным и поддерживает много языков программирования. Но для того, чтобы использовать удаленный вызов нужно проделать следующие операции:

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

В итоге получается не очень гибкий инструмент, к тому же не имеющий синтаксического сахара, присущего реализациям python.

Для python существует несколько популярных библиотек, мы рассмотрим одну из них. Библиотека PyRO (Python Remote Objects) реализует RPC к удаленным объектам. Для демонстрации простоты работы с ней рассмотрим простой пример использования. Возьмем нашу функцию example из предыдущего примера и поместим ее в класс

Теперь создадим экземпляр класса Consumer и на его основе запустим RPC сервер

После запуска программы нам на экран будет выведен uri-адрес:

Используя это адрес, мы можем создать proxy-объект, который подключится к удаленному объекту consumer. Далее у proxy-объекта можно производить вызовы функций как у обычного локального объекта.

По данному простому примеру видно, что внедрить использование PyRO в уже существующем коде очень просто: достаточно подменить реальный объект на proxy, и весь остальной код останется практически без изменений. При этом не нужно описывать никакие интерфейсы (Thrift и ICE) или обрабатывать входящие данные (multiprocessing) – все подхватывается налету.

С точки зрения разработчика, PyRO обладает множеством достоинств:

  1. PyRO написана на чистом python, поэтому является легко переносимой. Поддерживаются следующие интерпретаторы Python 2.x, 3.x, PyPy, IronPython, Jython, а также все основные операционные системы.
  2. Pickle – в качестве аргументов функций можно использовать всё, что “пиклится”.
  3. tcp/ip – позволяет соединять различные ОС с различной архитектурой.
  4. Полное покрытие unittest и наличие множества простых примеров использования PyRO.
  5. Удаленные исключения: если на стороне удаленного объекта произойдет исключение, то само исключение и трейсбек вы будете видеть локально.

Для удобства работы с удаленными объектами в библиотеке реализован pyro name server (PNS) – утилита, которая агрегирует запущенные удаленные объекты в едином справочнике имен. По своей сути является аналогом dns:

  1. Сервер регистрирует uri-адрес удаленного объекта в PNS под некоторым алиасом
  2. Клиент обращается к PNS с запросом на получения uri адреса по алиасу.

Таким образом, вам не нужно думать как передавать uri адрес удаленного объекта на сторону клиента.

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

Один подход – это односторонний вызов (oneway call). В случае, если для некоторых ваших функций не нужен ответ сервера, вы добавляете имена функций в список _pyroOneway у proxy-объекта. Например:

Затем, при вызове функций из этого списка управление будет сразу возвращено в вашу программу, и вы сможете продолжить работу, а сам вызов будет произведен в фоновом режиме. Например:

Другой подход – асинхронный вызов. Так как концепция асинхронного вызова широко известна, проиллюстрируем его примером:

Описанного выше функционала достаточно для знакомства с библиотекой PyRO. C остальными ее возможностями вы можете ознакомиться в документации.

Возникает закономерный вопрос, для решения каких задач можно использовать PyRO? Рассмотрим несколько наглядных примеров, которые дадут представление о спектре возможного применения:

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

Пример 2. Прозрачное связывание различных по назначению программ. Вы можете связать свое серверное приложение с приложением, работающим локально у пользователя, и взаимодействовать между ними через обычные python-вызовы. Пример из жизни:
Есть некий кластер, на каждом узле которого запущено python-приложение, производящее много вычислений. С помощью ipython notebook + pyro мы можем из браузера зайти на любой узел и посмотреть состояние вычислений.

Иллюстрация к примеру 2

Пример 3. Экономия ресурсов. Вы можете поместить в отдельный процесс некий ресурс, которым могут пользоваться множество клиентов. Пример из жизни:
В проекте нам требовалось много работать с данными, находящимися в базе данных. Для ускорения работы с БД был реализован кэш, который при старте worker-а загружает в оперативную память часть базы данных, а затем выполняет необходимые запросы.

База была достаточно объемная, а на одном сервере использовалось 6 воркеров, каждый из которых держал свой экземпляр MemoryDB, что требовало большого количества оперативной памяти.
В целях оптимизации объект memdb был вытащен в отдельный процесс, а все worker подменяли у себя memdb на proxy-объект, через который проходили запросы. В итоге, вместо 6 копий базы данных использовалась одна, при этом общая для всех worker-ов, что в свою очередь позволило увеличить часть обычной БД, которая загружалась в оперативную память.

Читайте в следующей статье:

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

 
  • AndrewSvetlov

    Мне в своё время приглянулась RPyC (http://rpyc.readthedocs.org/en/latest/)
    Для мелочи оказалась удобней PyRO

    • http://twitter.com/gimlis gimlis

      Спасибо, обязательно посмотрю. В 4-ой версии PyRO хорошо переработали интерфейс использования по сравнению с 3-ей, сильно его упростив.



+7 (495) 646-87-45

info@chtd.ru

125252, г. Москва, пр. Берёзовой Рощи, д. 12, оф. 56

На карте