Статьи

Взгляд на Nanomsg и протоколы масштабируемости (почему ZeroMQ не должен быть вашим первым выбором)

Ранее в этом месяце я исследовал ZeroMQ и его перспективное решение для создания быстрых, высокопроизводительных и масштабируемых распределенных систем. Несмотря на то, что ZeroMQ хорошо подходит для решения подобных проблем, у него есть свои недостатки. Его создатели пытались исправить многие из этих недостатков через духовных наследников Crossroads I / O и Nanomsg .

Ныне существующий ныне ввод / вывод Crossroads — это правильный форк ZeroMQ с истинным намерением построить вокруг него жизнеспособную коммерческую экосистему. Nanomsg, однако, является переосмыслением ZeroMQ — полное переписывание в C 1 . Он основан на безупречных эксплуатационных характеристиках ZeroMQ, одновременно обеспечивая несколько существенных улучшений, как внутренних, так и внешних. Он также пытается решить многие из странных ситуаций, которые ZeroMQ часто демонстрирует. Сегодня я расскажу о том, что отличает nanomsg от его предшественника, и реализую для него вариант использования в форме обнаружения службы.

Наномсг против ZeroMQ

ZeroMQ часто сталкивается с тем, что он не предоставляет API для новых транспортных протоколов, что по существу ограничивает вас TCP, PGM, IPC и ITC. Nanomsg решает эту проблему, предоставляя подключаемый интерфейс для транспортных протоколов и протоколов обмена сообщениями. Это означает поддержку новых транспортов (например, WebSockets) и новых шаблонов обмена сообщениями за пределами стандартного набора PUB / SUB, REQ / REP и т. Д.

Nanomsg также полностью POSIX-совместимый, что дает ему более чистый API и лучшую совместимость. Сокеты больше не представляются как пустые указатели и не привязаны к контексту — просто инициализируйте новый сокет и начните использовать его за один шаг. С ZeroMQ контекст внутренне действует как механизм хранения для глобального состояния и, для пользователя, как пул потоков ввода-вывода. Эта концепция была полностью удалена из Nanomsg.

В дополнение к соответствию POSIX, Nanomsg надеется на совместимость на уровнях API и протоколов, что позволит ему быть заменой или иным образом взаимодействовать с ZeroMQ и другими библиотеками, которые реализуют ZMTP / 1.0 и ZMTP / 2.0. , Однако, он еще не достиг полного паритета.

ZeroMQ имеет фундаментальный недостаток в своей архитектуре. Его гнезда не являются поточно-ориентированными. Само по себе это не является проблематичным и, в действительности, полезно в некоторых случаях. Изолируя каждый объект в его собственном потоке, устраняется необходимость в семафорах и мьютексах. Потоки не соприкасаются друг с другом, и вместо этого достигается параллелизм при передаче сообщений. Этот шаблон хорошо работает для объектов, управляемых рабочими потоками, но не работает, когда объекты управляются в пользовательских потоках. Если поток выполняет другую задачу, объект блокируется. Nanomsg устраняет отношения один-к-одному между объектами и потоками. Вместо того чтобы полагаться на передачу сообщений, взаимодействия моделируются как наборы конечных автоматов. Следовательно, гнезда nanomsg являются поточно-ориентированными.

Nanomsg имеет ряд других внутренних оптимизаций, направленных на повышение эффективности памяти и процессора. ZeroMQ использует простую трехуровневую структуру для хранения и сопоставления подписок PUB / SUB, которая хорошо работает для подписок менее 10000, но быстро становится необоснованной для чего-либо, выходящего за рамки этого числа. Nanomsg использует оптимизированное для пространства дерево, называемое радикальным деревом,  для хранения подписок. В отличие от своего предшественника, библиотека также предлагает настоящий API нулевого копирования, который значительно повышает производительность, позволяя копировать память с машины на машину, полностью обходя ЦП.

ZeroMQ реализует балансировку нагрузки с использованием алгоритма циклического перебора. Хотя он обеспечивает равномерное распределение работы, он имеет свои ограничения. Предположим, у вас есть два центра обработки данных, один в Нью-Йорке и один в Лондоне, и на каждом сайте размещены экземпляры служб «foo». В идеале, запрос на foo из Нью-Йорка не должен направляться в лондонский центр обработки данных и наоборот. Благодаря циклической балансировке ZeroMQ, это, к сожалению, вполне возможно. Одна из новых функций для пользователей, которую предлагает nanomsg, — это приоритетная маршрутизация для исходящего трафика. Мы избегаем этой проблемы с задержкой, назначая приоритет первым службам foo, размещенным в Нью-Йорке, для приложений, также размещенных там. Приоритет два затем присваивается службам foo, размещенным в Лондоне, что дает нам отказоустойчивость в случае, если foos в Нью-Йорке недоступны.

Кроме того, nanomsg предлагает инструмент командной строки для взаимодействия с системой под названием nanocat . Этот инструмент позволяет отправлять и получать данные через сокеты nanomsg, что полезно для отладки и проверки работоспособности.

Протоколы масштабируемости

Возможно, наиболее интересным является философский уход Наномсга из ZeroMQ. Вместо того, чтобы действовать в качестве общей сетевой библиотеки, nanomsg намеревается предоставить «кирпичики Lego» для построения масштабируемых и производительных распределенных систем, реализуя то, что называется «протоколами масштабируемости». Эти протоколы масштабируемости являются шаблонами связи, которые являются абстракцией поверх транспортного уровня сетевого стека. Протоколы полностью отделены друг от друга, так что каждый может воплощать четко определенный распределенный алгоритм. Как заявил автор Nanomsg Мартин Сустрик, цель состоит в том, чтобы стандартизировать спецификации протокола через IETF .

В настоящее время Nanomsg определяет шесть различных протоколов масштабируемости: PAIR, REQREP, PIPELINE, BUS, PUBSUB и SURVEY.

ПАРА (Двунаправленная Связь)

PAIR реализует простую двустороннюю связь один к одному между двумя конечными точками. Два узла могут отправлять сообщения назад и вперед друг другу.

пара

REQREP (клиентские запросы, ответы сервера)

Протокол REQREP определяет шаблон для создания сервисов без сохранения состояния для обработки пользовательских запросов. Клиент отправляет запрос, сервер принимает запрос, выполняет некоторую обработку и возвращает ответ.

reqrep

ТРУБОПРОВОД (односторонний поток данных)

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

трубопровод

BUS (связь «многие ко многим»)

BUS позволяет отправлять сообщения, отправленные каждым узлом, каждому другому узлу в группе.

автобус

PUBSUB (тематическое вещание)

PUBSUB позволяет издателям многоадресно рассылать сообщения нескольким подписчикам. Подписчики, которые могут подключаться к нескольким издателям, могут подписываться на определенные темы, что позволяет им получать только те сообщения, которые имеют к ним отношение.

PubSub

ОБЗОР (Задать вопрос группе)

Последний протокол масштабируемости, и тот, который я буду далее исследовать путем реализации варианта использования с, — это SURVEY. Шаблон SURVEY аналогичен PUBSUB в том, что сообщение от одного узла передается всей группе, но отличается от него тем, что каждый узел в группе  отвечает на сообщение. Это открывает широкий спектр приложений, поскольку позволяет быстро и легко запрашивать состояние большого количества систем за один раз. Респонденты опроса должны ответить в пределах временного интервала, настроенного исследователем

опрос

Внедрение службы обнаружения

Как я уже говорил, протокол SURVEY имеет много интересных приложений. Например:

  • Какие данные у вас есть для этой записи?
  • Какую цену вы предложите за этот товар?
  • Кто может обработать этот запрос?

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

Это замечательно, потому что 1) системе обнаружения не нужно знать, какие существуют сервисы — она ​​просто слепо отправляет опрос — и 2) когда услуга раскручивается, она будет обнаружена и, если она умрет, она будет «неоткрытых».

Вот класс ServiceDiscovery:

from collections import defaultdict
import random

from nanomsg import NanoMsgAPIError
from nanomsg import Socket
from nanomsg import SURVEYOR
from nanomsg import SURVEYOR_DEADLINE

class ServiceDiscovery(object):

    def __init__(self, port, deadline=5000):
        self.socket = Socket(SURVEYOR)
        self.port = port
        self.deadline = deadline
        self.services = defaultdict(set)

    def bind(self):
        self.socket.bind('tcp://*:%s' % self.port)
        self.socket.set_int_option(SURVEYOR, SURVEYOR_DEADLINE, self.deadline)

    def discover(self):
        if not self.socket.is_open():
            return self.services

        self.services = defaultdict(set)
        self.socket.send('service query')

        while True:
            try:
                response = self.socket.recv()
            except NanoMsgAPIError:
                break

            service, address = response.split('|')
            self.services[service].add(address)

        return self.services

    def resolve(self, service):
        providers = self.services[service]

        if not providers:
            return None

        return random.choice(tuple(providers))

    def close(self):
        self.socket.close()

Метод обнаружения отправляет опрос, а затем собирает ответы. Обратите внимание, что мы создаем сокет SURVEYOR и устанавливаем для него опцию SURVEYOR_DEADLINE. Этот крайний срок — это количество миллисекунд, прошедших с момента отправки опроса до момента, когда должен быть получен ответ. Скорректируйте его в соответствии с топологией вашей сети. Как только крайний срок опроса будет достигнут, возникает ошибка NanoMsgAPIError, и мы нарушаем цикл. Метод разрешения берет имя службы и случайным образом выбирает доступного поставщика из наших обнаруженных служб.

Затем мы можем обернуть ServiceDiscovery демоном, который будет периодически запускать обнаружение.

import os
import time

from service_discovery import ServiceDiscovery

DEFAULT_PORT = 5555
DEFAULT_DEADLINE = 5000
DEFAULT_INTERVAL = 2000

def start_discovery(port, deadline, interval):
    discovery = ServiceDiscovery(port, deadline=deadline)
    discovery.bind()

    print 'Starting service discovery [port: %s, deadline: %s, interval: %s]' \
        % (port, deadline, interval)

    while True:
        print discovery.discover()
        time.sleep(interval / 1000)

if __name__ == '__main__':
    port = int(os.environ.get('PORT', DEFAULT_PORT))
    deadline = int(os.environ.get('DEADLINE', DEFAULT_DEADLINE))
    interval = int(os.environ.get('INTERVAL', DEFAULT_INTERVAL))

    start_discovery(port, deadline, interval)

Параметры обнаружения настраиваются через переменные среды, которые я вставляю в контейнер Docker.

Службы должны подключаться к системе обнаружения при запуске. Когда они получают опрос, они должны ответить, указав, какую услугу они предоставляют и где она находится. Один из таких сервисов может выглядеть следующим образом:

import os
from threading import Thread

from nanomsg import REP
from nanomsg import RESPONDENT
from nanomsg import Socket

DEFAULT_DISCOVERY_HOST = 'localhost'
DEFAULT_DISCOVERY_PORT = 5555
DEFAULT_SERVICE_NAME = 'foo'
DEFAULT_SERVICE_PROTOCOL = 'tcp'
DEFAULT_SERVICE_HOST = 'localhost'
DEFAULT_SERVICE_PORT = 9000

def register_service(service_name, service_address, discovery_host,
                     discovery_port):
    socket = Socket(RESPONDENT)
    socket.connect('tcp://%s:%s' % (discovery_host, discovery_port))

    print 'Starting service registration [service: %s %s, discovery: %s:%s]' \
        % (service_name, service_address, discovery_host, discovery_port)

    while True:
        message = socket.recv()
        if message == 'service query':
            socket.send('%s|%s' % (service_name, service_address))

def start_service(service_name, service_protocol, service_port):
    socket = Socket(REP)
    socket.bind('%s://*:%s' % (service_protocol, service_port))

    print 'Starting service %s' % service_name

    while True:
        request = socket.recv()
        print 'Request: %s' % request
        socket.send('The answer is 42')

if __name__ == '__main__':
    discovery_host = os.environ.get('DISCOVERY_HOST', DEFAULT_DISCOVERY_HOST)
    discovery_port = os.environ.get('DISCOVERY_PORT', DEFAULT_DISCOVERY_PORT)
    service_name = os.environ.get('SERVICE_NAME', DEFAULT_SERVICE_NAME)
    service_host = os.environ.get('SERVICE_HOST', DEFAULT_SERVICE_HOST)
    service_port = os.environ.get('SERVICE_PORT', DEFAULT_SERVICE_PORT)
    service_protocol = os.environ.get('SERVICE_PROTOCOL',
                                      DEFAULT_SERVICE_PROTOCOL)

    service_address = '%s://%s:%s' % (service_protocol, service_host,
                                      service_port)

    Thread(target=register_service, args=(service_name, service_address,
                                          discovery_host,
                                          discovery_port)).start()

    start_service(service_name, service_protocol, service_port)

Еще раз, мы настраиваем параметры через переменные окружения, установленные для контейнера. Обратите внимание, что мы подключаемся к системе обнаружения с помощью сокета RESPONDENT, который затем отвечает на запросы службы с помощью имени и адреса службы. Сам сервис использует сокет REP, который просто отвечает на любые запросы с «ответом 42», но может принимать любое количество форм, таких как HTTP, необработанный сокет и т. Д.

Полный код для этого примера, включая Dockerfiles, можно найти на GitHub .

Наномсг или ZeroMQ?

Основываясь на всех улучшениях, которые nanomsg делает поверх ZeroMQ, вы можете удивиться, зачем вообще использовать последнее. Наномсг еще относительно молод. Несмотря на многочисленные языковые привязки , он еще не достиг зрелости ZeroMQ, в котором процветает сообщество разработчиков. ZeroMQ имеет обширную документацию и другие ресурсы, чтобы помочь разработчикам использовать библиотеку, в то время как nanomsg очень мало. Выполнение быстрого поиска в Google даст вам представление о разнице (от 500 000 результатов для ZeroMQ до 13 500 наномсг).

Тем не менее, улучшения Nanomsg и, в частности, его протоколы масштабируемости делают его очень привлекательным. Многие странные поведения, которые демонстрирует ZeroMQ, были решены полностью или, по крайней мере, смягчены. Он  активно развивается и быстро набирает обороты. Технически, nanomsg был в бета-версии с марта, но он начинает выглядеть готовым к производству, если он еще не там.