(Начало - здесь)
Шаблон "Мажордом". Топология.
Прогресс ускоряется, когда в нем не участвуют юристы и разные комитеты. Одно-страничная спецификация MDP сразу превращает PPP в нечто более солидное. Именно так и должна происходить разработка сложных архитектур: начинать следует с написания соглашений, а только потом писать софт, их реализующий.
Протокол "Мажордом" (The Majordomo Protocol - MDP) расширяет и углубляет PPP в одном интересном направлении: он добавляет "имя сервиса" к запросу, который отправляет клиент, и требует от рабочих регистрироваться для оказания конкретных услуг. Добавление имен сервиса превращает брокер из шаблона "Пират-параноик" в сервис - ориентированный брокер.
Для MDP хорошо то, что он сформировался на основе работающего кода, наследуется от более простого протокола (PPP), и представляет собой набор улучшений, каждое из которых решает отдельную проблему. Все это упрощает процесс разработки.
Для реализации шаблона "Мажордом" нужно написать фреймворк для клиентов и рабочих. Действительно, не очень умно добиваться от каждого разработчика приложений, чтобы он читал спецификации и добивался, чтобы они заработали, тогда они могли бы просто использовать более простой API, который за них делает работу.
И так, пока наш первое соглашение (сам MDP) определяет, как части нашей распределенной архитектуры общаются с другими частями, второе соглашение определяет, как приложения пользователя общается с технологическим фреймворком, который мы собираемся создать.
Шаблон "Мажордом" состоит из двух частей: клиент и рабочий. Так как мы собираемся писать и клиента, и рабочего, нам нужно два API.
Ниже приведен эскиз клиентского приложения, использующего API.
Примечание: используем псевдо-объектный стиль кодирования, просто потому, что в он же используется руководстве.
Код приложения "клиент":
***
Мы открываем сессию связи с брокером, в цикле шлем запрос, получаем ответ, and в конце закрываем сессию.
А вот код приложения "рабочий":
Замечания по поводу кода API рабочего:
Теперь разработаем брокер нашего шаблона "Мажордом". В основе его структуры будет набор очередей, по одной на сервис. Мы будем создавать эти очереди при появлении рабочих (при пропадании рабочих, мы могли бы удалять их, но пока не станем усложнять себе задачу). Дополнительно, мы мы будем хранить очередь рабочих для каждого сервиса.
Код брокера:
Да, код брокера чуть посложней. Чуть.
В цикле из сокета читаются сообщения. Отбрасываются два служебных фрейма, извлекается заголовок (третий фрейм):
В зависимости от заголовка, сообщение обрабатывается как от клиента либо как от рабочего.
Затем, если пришло время, всем живым рабочим отправляется хартбит:
Вот и все.
Несколько замечаний по коду брокера:
ZeroMQ: надежные схемы "Запрос/Ответ". Сервис - ориентированная надежная очередь. Шаблон "Мажордом".
Шаблон "Мажордом". Топология.
Прогресс ускоряется, когда в нем не участвуют юристы и разные комитеты. Одно-страничная спецификация MDP сразу превращает PPP в нечто более солидное. Именно так и должна происходить разработка сложных архитектур: начинать следует с написания соглашений, а только потом писать софт, их реализующий.
Протокол "Мажордом" (The Majordomo Protocol - MDP) расширяет и углубляет PPP в одном интересном направлении: он добавляет "имя сервиса" к запросу, который отправляет клиент, и требует от рабочих регистрироваться для оказания конкретных услуг. Добавление имен сервиса превращает брокер из шаблона "Пират-параноик" в сервис - ориентированный брокер.
Для MDP хорошо то, что он сформировался на основе работающего кода, наследуется от более простого протокола (PPP), и представляет собой набор улучшений, каждое из которых решает отдельную проблему. Все это упрощает процесс разработки.
Для реализации шаблона "Мажордом" нужно написать фреймворк для клиентов и рабочих. Действительно, не очень умно добиваться от каждого разработчика приложений, чтобы он читал спецификации и добивался, чтобы они заработали, тогда они могли бы просто использовать более простой API, который за них делает работу.
И так, пока наш первое соглашение (сам MDP) определяет, как части нашей распределенной архитектуры общаются с другими частями, второе соглашение определяет, как приложения пользователя общается с технологическим фреймворком, который мы собираемся создать.
Шаблон "Мажордом" состоит из двух частей: клиент и рабочий. Так как мы собираемся писать и клиента, и рабочего, нам нужно два API.
Ниже приведен эскиз клиентского приложения, использующего API.
Примечание: используем псевдо-объектный стиль кодирования, просто потому, что в он же используется руководстве.
Код приложения "клиент":
***
program mdclient; {$APPTYPE CONSOLE} uses SysUtils, zmq_h, czmq_h, mdcliapi in 'mdcliapi.pas', mdp in 'mdp.pas'; procedure DoIt; var i: Integer; fSession: p_mdcli_t; fVerbose: Boolean; reply: p_zmsg_t; request: p_zmsg_t; begin fVerbose := (ParamCount > 0) and (ParamStr(1) = '-v'); fSession := mdcli_new('tcp://localhost:5555', fVerbose); i := 0; for i := 0 to 99999 do begin request := zmsg_new(); zmsg_pushstr(request, 'Hello world'); reply := mdcli_send(fSession, 'echo', request); if reply <> nil then zmsg_destroy(reply) else break; // Прерван или отказ end; zclock_log('%d requests / replies processed'#10, i); mdcli_destroy(fSession); end; begin DoIt; Readln; end.Всё. :)
Мы открываем сессию связи с брокером, в цикле шлем запрос, получаем ответ, and в конце закрываем сессию.
А вот код приложения "рабочий":
program mdworker; {$APPTYPE CONSOLE} uses SysUtils, zmq_h, czmq_h, mdwrkapi in 'mdwrkapi.pas', mdp in 'mdp.pas'; procedure DoIt; var fVerbose: Boolean; reply: p_zmsg_t; request: p_zmsg_t; session: p_mdwrk_t; begin fVerbose := (ParamCount > 0) and (ParamStr(1) = '-v'); session := mdwrk_new('tcp://localhost:5555', 'echo', fVerbose); reply := nil; while (true) do begin request := mdwrk_recv(session, reply); if (request = nil) then break; // Рабочий был прерван reply := request; // Эхо ... :-) end; mdwrk_destroy(session); Exit; end; begin doIt; Readln; end.В принципе, то же самое, но с небольшими отличиями. Сначала рабочий выполняет recv(), который передает брокеру пустой ответ. Затем рабочий в цикле возвращает текущий ответ и получает новый запрос. API клиента и рабочего создать очень просто, так как они основаны на коде шаблона "Пират-Параноик", который был отлажен ранее.
Константы протокола Majordomo определены в модуле mdp.pas: :
А вот так реализован сам модуль клиентского API: :
Код реализации API рабочего: :
Замечания по поводу кода API рабочего:
- API - однонитевое. Это означает, что, например, рабочий не может послать хартбит в фоне. К счастью, это как раз то, что нам нужно: если рабочий зависает, харбитинг останавливается и брокер перестает перестает отправлять запросы рабочему.
- API рабочего не выполняет экпоненциальный back-off, это усложнит реализацию.
- API не формирует отчеты об ошибках. Если чего-то не хватает, формируется исключение. Это идеально для эталонной реализации, любые ошибки протокола становятся сразу видны. Для реальных же приложений, API должен быть устойчивым к появлению неправильных сообщений.
Теперь разработаем брокер нашего шаблона "Мажордом". В основе его структуры будет набор очередей, по одной на сервис. Мы будем создавать эти очереди при появлении рабочих (при пропадании рабочих, мы могли бы удалять их, но пока не станем усложнять себе задачу). Дополнительно, мы мы будем хранить очередь рабочих для каждого сервиса.
Код брокера:
program mdbrocker; {$APPTYPE CONSOLE} uses SysUtils, mdbrkapi in 'mdbrkapi.pas', zmq_h, czmq_h, ZMQ_Utils, mdp in 'mdp.pas'; procedure doIt; // Задача брокера. СОздаем экземпляр брокера и // обрабатываем сообщения, поступающие в сокет брокера var empty: p_zframe_t; fVerbose: Boolean; header: p_zframe_t; item: zmq_pollitem_t; msg: p_zmsg_t; rc: Integer; self: p_broker_t; sender: p_zframe_t; worker: p_worker_t; begin fVerbose := (ParamCount > 0) and (ParamStr(1) = '-v'); self := s_broker_new(fVerbose); s_broker_bind(self, 'tcp://*:5555'); // Получение и обработка сообщений, пока не случится прерывание while true do begin zPollItemInit(item, self.socket, 0, ZMQ_POLLIN, 0); rc := zmq_poll(@item, 1, cHEARTBEAT_INTERVAL * ZMQ_POLL_MSEC); if rc = -1 then break; // Прерван // Если есть входное сообщение, обрабатываем if (item.revents and ZMQ_POLLIN) <> 0 then begin msg := zmsg_recv(self.socket); if msg = nil then break; // Прерван if self.verbose then begin zclock_log('I: received message:'); zmsg_print(msg); end; sender := zmsg_pop(msg); empty := zmsg_pop(msg); header := zmsg_pop(msg); if (zframe_streq(header, cMDPC_CLIENT)) then s_broker_client_msg(self, sender, msg) else if (zframe_streq(header, cMDPW_WORKER)) then s_broker_worker_msg(self, sender, msg) else begin zclock_log('E: invalid message: '); zmsg_print(msg); zmsg_destroy(msg); end; zframe_destroy(sender); zframe_destroy(empty); zframe_destroy(header); end; // Дисконнект и удаление всех умерших рабочих // Если нужно - отправка хартбита ожидающим рабочим. if (zclock_time() > self.heartbeat_at) then begin s_broker_purge(self); worker := zlist_first(self.waiting); while worker <> nil do begin s_worker_send(worker, cMDPW_HEARTBEAT, nil, nil); worker := zlist_next(self.waiting); end; self.heartbeat_at := zclock_time() + cHEARTBEAT_INTERVAL; end; end; if (zctx_interrupted <> 0) then zclock_log('W: interrupt received, shutting down...'); s_broker_destroy(self); end; begin doIt; Readln; end.
Да, код брокера чуть посложней. Чуть.
В цикле из сокета читаются сообщения. Отбрасываются два служебных фрейма, извлекается заголовок (третий фрейм):
sender := zmsg_pop(msg); empty := zmsg_pop(msg); header := zmsg_pop(msg);
В зависимости от заголовка, сообщение обрабатывается как от клиента либо как от рабочего.
if (zframe_streq(header, cMDPC_CLIENT)) then s_broker_client_msg(self, sender, msg) else if (zframe_streq(header, cMDPW_WORKER)) then s_broker_worker_msg(self, sender, msg)
Затем, если пришло время, всем живым рабочим отправляется хартбит:
if (zclock_time() > self.heartbeat_at) then begin s_broker_purge(self); worker := zlist_first(self.waiting); while worker <> nil do begin s_worker_send(worker, cMDPW_HEARTBEAT, nil, nil); worker := zlist_next(self.waiting); end; self.heartbeat_at := zclock_time() + cHEARTBEAT_INTERVAL; end;
Вот и все.
Код реализации API брокера: :
Несколько замечаний по коду брокера:
- Протокол "Мажордом" позволяет обслужить клиентов и рабочих с помощью одного сокета. Это упрощает развертывание и обслуживание брокера. Обычно другие прокси требуют два сокета.
- Брокер реализует все требования протокола MDP/0.1, включая отключение, когда брокер посылает неправильные команды, хартбиты и прочее.
- Брокер может быть легко расширен до многонитевого, каждая нить могла бы обслуживать один сокет и один набор клиентов и рабочих. Такой подход был бы интересен для сегментирования объемных сетевых структур. Псевдоклассовая организация кода позволяет довольно легко сделать это.
- Модель надежности типа "основной/резервный" или "живой/живой" для брокера легко реализуется, так как брокер фактически не хранит состояние корреспондентов за исключением наличия сервиса. Это позволяет клиентам и рабочим переключиться на другого брокера, когда первый недоступен.
- В примере используется пятисекундный хартбитинг, в основном для того, чтобы уменьшить время вывода при отладке. Реальные значения могут быть ниже для большинства сетевых приложений. Тем не менее, любой перезапрос должен быть достаточно медленным, чтобы позволить сервису перезапуститься. Например, 10 секунд (или дольше).
Здравствуйте, s_Dec (retries_left) компилятор не проходит, определения функций удаления s_dec
ОтветитьУдалить[dcc32 Error] mdcliapi.pas(115): E2003 Undeclared identifier: 's_Dec'
ОтветитьУдалить[dcc32 Error] mdcliapi.pas(45): E2003 Undeclared identifier: 's_strdup'
Да, был обновлвен zmq_utils, а я забыл выложить обновление, можно скачать:
ОтветитьУдалитьhttps://drive.google.com/file/d/0B2vGKHGkken9Nlo1ZVRwOG5mSXc/view
Функция z_Dec() выполняет декремент и возвращает результат.
Функция zstrdup() копирует строки, используя менеджер памяти .dll библиотеки ZeroMQ.