ZeroMQ: сокети по-новому

У будь-якому середньому або великому додатку, будь воно desktop або web, для бізнесу або для особистого користування, програмісту необхідно вирішити важливу архітектурну завдання — як будуть спілкуватися між собою потоки, процеси, модулі, ноди, кластера, та інші частини еко-системи його програми.

Багато розробники вирішують йти по шляху найменшого опору, поклавши цю задачу, наприклад, на СУБД. Скажімо, один процес поклав дані в БД, другий прочитав, обробив — поклав ще й так далі.
Про обмін через файли в наші роки вже соромно говорити, але й таке трапляється.
Інші ж програмісти намагаються створити якесь своє, спеціалізоване рішення і, як правило, вибирають сокети.

Завдання проектування і розробки архітектури вкрай цікава, але це окрема тема. В даному пості хотів би поділитися своїм першим враженням від знайомства з бібліотекою ZeroMQ.

ZeroMQ пропонує розробнику якийсь високий рівень абстракції при роботі з «сокетами». Бібліотека бере на себе частину турбот по буферизації даних, обслуговування черг, встановлення та відновлення сполук, і інші речі. Замість того, щоб займатися такими дурницями, ви можете зосередитися на головному — архітектурі і логікою програми.

Однак, в цьому світі безкоштовний сир тільки в мишоловці. Тому я постарався по мірі сил і досвіду з'ясувати, чим доведеться поплатитися за зручність, які я знайшов плюси і мінуси при застосуванні даної бібліотеки.

Безпосередньо опис ZeroMQ, його API і купу іншої корисної інформації можна знайти на офіційному сайті ZeroMQ.

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

Ми ж займемося вирішенням типової задачі і порівняємо рішення на основі традиційних сокетів і «сокетів ZeroMQ».

Отже, завдання

Припустимо, ми маємо якийсь сервіс, який приймає по сокету з'єднання клієнта, отримує від нього запити та відправляє на них відповіді.
Для простоти, нехай це буде ехо-сервіс, тобто що отримав і відправив.

Далі потрібно визначитися з форматом обміну.
Традиційний сокет працює з послідовністю байтів, що для програми, яка обмінюється деякої структурованою інформацією, не є добре. Тому нам доведеться створити якийсь «пакет» з даними, для простоти в пакету буде один атрибут — довжина. Тобто спочатку передаємо довжину пакета, потім самі дані вказаної довжини. При прийомі відповідно буферизируем прийняту послідовність байт і розбираємо її на «пакети».
Всередину самого «пакету» ми можемо запхати що завгодно: бінарну структуру, текст, JSON, BSON, XML, і т.д.

Для простоти, сервер у нас буде приймати і передавати дані в одному потоці.
А от обробка даних на сервері повинна відбуватися в декілька потоків (будемо називати їх worker-ами).

Рішення

В якості рішення створив два джерела, один із звичайними сокетами, інший з ZeroMQ.
Не буду публікувати вихідний код в самому пості, для перегляду перейдіть за посиланням:
1) Традиційні сокети (19 Kb)
2) Сокети ZeroMQ (11,74 Kb)

Детальніше про тестиКожен файл із вихідним кодом — це готовий тест, при запуску якого стартує і сервер, і клієнти (в одному процесі, але в різних потоках).
Тест працює кілька секунд і видає результати роботи кожного клієнта: скільки пакетів і байт отримав, а також середню швидкість отримання пакетів.
При старті потоку клієнта відбувається передача одного або декількох пакетів з даними, а при отриманні кожного пакета — він передається назад.
Параметри тесту можна змінити, вони задані в #define-ах в кожному файлі.


Як видно, ZeroMQ скоротив обсяг коду приблизно в 2 рази, читабельність покращилася.
Тепер подивимося, скільки ми за це заплатили.

На моїй машині при початкових параметрах тест видав приблизно наступні результати:

1) 400 пакетів в секунду (традиційні сокети);
2) 500 пакетів в секунду (ZeroMQ).
* Примітка: за замовчуванням у тесті 10 клієнтських потоків і 2 worker-а, розмір пакета — 1Кб, час «обробки» (імітуємо usleep-ом) одного пакета сервером — 2мс.

Відразу обмовлюся, що якби обробка даних у нас йшла в один потік, разом з прийомом і передачею, то ZeroMQ програв би звичайним сокетам в 2-4 рази. Перевірено також на подібному тесті, проте публікувати його я поки не буду, тому однопотоковий сервер, який обробляє одночасно тільки один запит, а інші клієнти чекають — це не наш випадок.

Давайте розберемося, чому ZeroMQ показав кращі результати, ніж звичайні сокети, незважаючи на певний оверхед із-за рівня абстракції.

Основна причина, звичайно ж, криється у вихідному коді самого тесту. Обробка даних в декілька потоків на звичайних сокетах — завдання досить складне. В моєму тесті вона реалізована далеко не оптимальним способом:

1) немає ніякої черги завдань і прийнятих пакетів, ми банально не приймаємо дані, якщо не можемо їх обробити;
2) коли worker закінчив обробку запиту — він даремно спить, поки основний потік не запише йому в буфер наступну задачу;
3) основний потік у випадку зайнятості worker-ів вхолосту проходить основний цикл, поки worker не звільниться (або не з'являться події вводу-виводу);
4) при запису результату обробки запиту worker-му в буфер передачі клієнта, блокується основний потік (або worker чекає поки основний потік пройде основний цикл).

Усунення даних недоліків істотно збільшить обсяг коду і складність завдання, збільшиться вірогідність появи помилок.

Тепер давайте звернемося до варіанту з ZeroMQ.

Вихідний код більш читабельний, а головне — позбавлений будь-яких блокувань (mutex-ів, як у задачі з звичайними сокетами). Це основна перевага ZeroMQ.

У традиційному асинхронному програмуванні блокування неминучі, із збільшенням обсягу коду ви обов'язково десь поставите зайву блокування, а де-то забудете поставити потрібну. Потім з'являться вкладені блокування, які в підсумку призведуть до deadlock-ам і різних race condition. Якщо помилки будуть відбуватися в рідкісних випадках, на додатку в production ви замучитеся їх шукати. А ефект буде приголомшливий — ваш сервіс намертво зависає, незбережені дані будуть втрачені, а клієнти відключаться.

ZeroMQ вирішує цю проблему просто — процеси і потоки лише обмінюються повідомленнями. При цьому слід зробити застереження, що не рекомендується розшарювати ніякі загальні дані між потоками і використовувати блокування. ZeroMQ дозволяє не ділити між потоками дані про сокети і їх буфери, проте дані самого додатка залишаються головним болем розробника.
Всередині процесу між потоками також може відбуватися обмін повідомленнями, і не обов'язково через TCP. Достатньо передати функцій zmq_bind/zmq_connect замість «tcp://127.0.0.1:1010» щось на зразок «ipc://mysock» — і ваш обмін вже працює через UNIX сокети, а поставите «inproc://mysock» — і обмін піде через внутрішню пам'ять процесу. Це значно швидше і економічніше сокетів.
В якості прикладу візьміть ісходник тесту.
Потік, який здійснює обробку даних (worker) — це такий же клієнт, але тільки внутрішній. Він підключається до основного потоку через зазначений сокет (найефективніше inproc://) і отримує завдання, виконавши яке відправляє результат назад основному потоку. Останній вже переадресовує результат зовнішнього клієнта.
ZeroMQ дозволяє не піклуватися про розподіл завдань і пошуку вільного worker-а. В даному прикладі він автоматично ставить пакет в чергу на обробку (відправлення worker-у).

Безсумнівно, і у ZeroMQ є досить вагомі мінуси. Хоч ця бібліотека і бере на себе купу клопотів, вона не забезпечує гарантії доставки і збереження ваших повідомлень. Це віддається на відкуп розробника, що абсолютно правильно, на мій погляд.

Пройдемося по кількох, найбільш важливим аспектам роботи з ZeroMQ.

З'єднання

Плюси:
+ ZeroMQ автоматично відновлює вихідні з'єднання. У додатку ви можете і не помітити розриву з'єднання, якщо, звичайно, спеціально не будете відслідковувати цю подію (см.zmq_socket_monitor())

Мінуси:
— Я поки не здогадався, як дізнатися справжній IP-адресу, ім'я хоста або хоча б дескриптор клієнта, від якого надійшло повідомлення. Максимум що дає ZeroMQ — це якийсь ідентифікатор клієнта (для сокета типу ZMQ_ROUTER), який може бути призначений ZeroMQ автоматично, так і поставлене клієнтом самостійно перед встановленням з'єднання.
— Знову ж таки, я поки не здогадався як примусово відключити клієнта (припустимо, не авторизувався вчасно). А це загрожує нагромадження непотрібних з'єднань.

Черги

Плюси:
+ відправляються в ZeroMQ повідомлення потрапляють у внутрішню чергу, що дозволяє не чекати закінчення відправлення, а в разі з'єднання — не має значення, встановлене воно чи ні. Розмір черги може змінюватися.
+ існує також чергу на прийом, із-за чого реалізується стратегія т.зв. «справедливої черги». У разі вхідного з'єднання, ви отримуєте повідомлення з загальної черги для всіх клієнтів.

Мінуси:
— наскільки мені відомо, ви не можете керувати чергами — очищати, вважати фактичний розмір і т.д.
— у разі переповнення черги, нові повідомлення відкидаються

Повідомлення

Плюси:
+ В ZeroMQ ви працюєте не з потоком байт, а з окремими повідомленнями, довжина яких відома.
+ Повідомлення у ZeroMQ складається з одного або декількох т.зв. «фреймів», що досить зручно — можна по мірі проходження повідомлення з вузлів додавати/видаляти кадри з метаінформацією, не чіпаючи кадру з даними. Такий підхід, зокрема, використовується в сокеті типу ZMQ_ROUTER — ZeroMQ при прийомі повідомлення автоматично додає першим кадром ідентифікатор клієнта, від якого воно отримано.
+ Кожне повідомлення атомарно, тобто завжди буде отримано або передано повністю, включаючи всі фрейми.

Мінуси:
— Кожне повідомлення повинно уміщатися в пам'ять, тобто якщо потрібно передавати великі повідомлення — доведеться його розбивати на частини (на повідомлення, а не фрейми) самостійно. Максимальний розмір повідомлення, при цьому, можна налаштувати.

Ліричний відступ

У ZeroMQ, крім різних видів транспорту (tcp, ipc, inproc тощо), існує кілька типів сокетів: REQ, REP, ROUTER, DEALER, PUB, SUB, тощо
Раджу ознайомитися з ними документації уважно. Від типу сокету на обох кінцях залежить його поведінка. В деяких типах сокетів використовуються додаткові обов'язкові фрейми.
Згаданий вище Guide цілком непогано на прикладах ознайомить вас з основними типами сокетів.

Висновок

Якщо ви тільки починаєте проектувати свій додаток, або якісь його окремі прості частини, модулі і підзадачі, то дуже рекомендую придивитися до ZeroMQ.
В реальному додатку з асинхронною обробкою даних ZeroMQ забезпечить не тільки скорочення обсягу коду, але і деяке збільшення продуктивності.
Бінди даної бібліотеки є для багатьох мов програмування: C++, C#, CL, Delphi, Erlang, F#, Felix, Haskell, Java, Objective-C, Ruby, Ada, Basic, Clojure, Go, Haxe, Node.js, ooc, Perl, Scala.
Бібліотека крос-платформний, тобто можна використовувати як в Linux, так і під Windows. Правда, на жаль, поки що офіційної версії під MinGW не знайшов.
Але проект швидко розвивається, вже багато де використовується, будемо сподіватися і вірити.

Зауваження в коментарях вітаються!

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.