TomskSoft's open source NATter service

Введение

В связи с массовым переходом систем, разрабатываемых и поддерживаемых ТомскСофтом, как, например Camfrog, на микросервисную архитектуру необходим инструмент, который позволит сделать этот переход плавным и безболезненным, не прерывая текущую их работу. Кроме того, так как интеграция микросервисов, как правило, осуществляется с помощью вспомогательных систем, таких как, например, брокер сообщений, а изначально монолитная система функционирует, скажем, на HTTP, то этот инструмент должен обеспечить надёжную и стабильную коммуникацию между сервисами, работающими на разных протоколах, будь то сеансовых или с динамической маршрутизацией.

Что такое NATter и зачем он нужен?

NATter – это система с открытым кодом, разрабатываемая ТомскСофтом и написанная на языке Golang, задача которой – проксировать сообщения, между брокером и веб-сайтом. Смысл в том, что новоиспечённый микросервис декомпозируемой монолитной системы использует, например, NATS для обмена сообщениями, а сама система, делегировавшая какую-то часть функционала этому сервису, использует HTTP, что затрудняет коммуникацию между ними. Задача NATter’а как раз и состоит в том, чтобы возобновить общение между монолитом и отколовшимся от микросервисом.

Пример использования

Допустим, клиентам какой-либо системы нужно проверять валидность email’а при регистрации или его изменении. Один из способов реализации – это валидация непосредственно на веб-сервере.

Другой способ основан на микросерверном подоходе к архитектуре: выделяется отдельный сервис, занимающийся валидацией email’а и работающий, например, с NATS. Так как запросы продолжают приходить на веб, то возникает проблема его коммуникации с сервисом.

NATter в данном случае с одной стороны предоставляет HTTP-интерфейс для веба, с другой – имеет доступ к очереди сообщений, по ту сторону которой находится сервис, производящий валидацию email’а. Кто именно в конечном итоге обработает запрос и вернёт ответ, для NATter’а и, следовательно, для веба не имеет значения.

Таким образом можно задавать NATter'у неограниченное количество маршрутов, являющихся связующим звеном между вебом и микросервисами.

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

Батчинг

NATter поддерживает отправку сообщений пакетами, то есть каждый отдельный маршрут возможно настроить таким образом, чтобы каждое новое сообщение, пришедшее от отправителя, отдавалось получателю не сразу, а при условиях, определяемых параметрами, такими как таймаут и размер пакета, измеряемый в количестве сообщений. Для сериализации используется Protocol Buffers. Общий алгоритм работы батчинга:

  1. заводится таймер, обнуляется счётчик сообщений, определяющий размер пакета, и ожидается приход очередного сообщения;
  2. от отправителя приходит сообщение, которое удерживается в памяти до тех пор пока не выполнится одно из следующих условий:
    • таймер истёк;
    • размер пакета достиг определённого максимума;
    • сервис завершает работу;
  3. после выполнения одного из условий выше каждое удерживаемое сообщение конвертируется в google.protobuf.Any, после чего массив полученных структур сериализуется в бинарник, отправляемый получателю;
  4. если сервис не завершает работу, возврат к шагу 1.

Архитектура

Основой NATter’а является пакет driver, предоставляющий интерфейсы для работы с протоколом:

type Conn interface {
        Serve(context.Context) error
        Close() error
        Receiver(*entity.Route) Receiver
        Sender(*entity.Route) Sender
}

type Receiver interface {
        Listen(Sender) error
        ListenRequest(Sender) error
}

type Sender interface {
        Send(payload []byte) error
        Request(payload []byte) ([]byte, error)
}

Эти интерфейсы в совокупности реализуют шаблон проектирования "Абстрактная фабрика". Conn представляет собой соединение по протоколу, которое можно контролировать с помощью основных методов Serve() и Close(). Также есть два вспомогательных фабричных метода: Receiver() и Sender() – создающих и возвращающих сущности соответственно для приёма и отправки сообщений по протоколу, определяемому сущностью Conn. Настройка получателя и отправителя осуществляется на основе экземпляра структуры entity.Route, передаваемой в фабричный метод:

type Route struct {
        Mode     RouteMode
        Async    bool
        Topic    string
        Endpoint string
        URI      string
}

Здесь Mode – это режим маршрута вида “отправитель-получатель-направленность”, Async – флаг асинхронности маршрута, Topic – топик-источник или топик-назначение сообщения, Endpoint – адрес-назначение сообщения, URI – путь-источник сообщения.

Поле Mode имеет вид “отправитель-получатель-направленность”, где отправитель – это имя протокола-источника сообщения, получатель – имя протокола-назначения сообщения, а направленность – одно из двух значений: oneway (запрос) и twoway (запрос-ответ). Например, http-broker-oneway означает одностороннее проксирование сообщения от HTTP-клиента к брокеру сообщений, а broker-http-twoway – проксирование запроса от брокера к HTTP-серверу и обратное проксирование ответа.

Сами интерфейсы Receiver и Sender предоставляют методы как для односторонней обработки сообщений (Listen() и Send()), так и для обработки запросов-ответов(ListenRequest() и Request()). При этом Receiver требует передачи Sender в свои методы, так как после приёма сообщения Receiver’ом оно сразу направляется Sender’у.

Простой пример использования интерфейсов для создания маршрута:

var (
        httpConn driver.Conn = // ...
        natsConn driver.Conn = // ...
)

route := &entity.Route{
        Topic:    "hello",
        Endpoint: "https://www.world.com/",
}

natsReceiver := natsConn.Receiver(route)
httpSender := httpConn.Sender(route)

natsReceiver.Listen(httpSender)

ctx, _ := context.WithTimeout(context.Background(), time.Minute)

go func() {
        httpConn.Serve(ctx)
        httpConn.Close()
}

go func() {
        natsConn.Serve(ctx)
        natsConn.Close()
}

<-ctx.Done()

В данном примере создаются сущности Conn, относящиеся к протоколам “HTTP” (httpConn) и “NATS” (natsConn). После этого создаются natsReceiver (с топиком hello) и httpSender (с адресом https://www.world.com/). Строка

natsReceiver.Listen(httpSender)

буквально означает следующее: “отправить сообщение, принятое natsReceiver’ом (т.е. из топика hello), httpSender’у (т.е. по адресу https://www.world.com/)”. Далее httpConn и natsConn приводятся в готовность вызовом блокирующего метода Serve() и закрываются по завершении контекста ctx вызовом Close().

Регистрация маршрутов, описанная выше в общих чертах, выполняется сущностью service.Router. Конструктор Router’а принимает массив маршрутов и карту <string, driver.Conn>, где string – это имя соединения; эта карта при обработке режима маршрута (entity.Route.Mode), указываемого в конфиге, позволяет определить, по каким именно протоколам требуются соединения для регистрации маршрута. Например, если имеется следующая карта:

map[string]driver.Conn{
        "nats":      natsConn,
        "kafka":     kafkaConn,
        "websocket": websocketConn,
}

то поле Route.Mode может быть nats-websocket-oneway, kafka-nats-oneway, websocket-kafka-twoway и т.д.

Инициализация соединений и создание на их основе карты осуществляется в методе (*cmd.NATter) setupConns() error.

Выпуск в open source

Изначально проект разрабатывался только для внутреннего использования и назывался (и, по крайней мере, пока продолжает называться внутри ТомскСофта) “NATter v2.0”. Название было унаследовано от менее универсальной системы “NATter”, ныне уже не используемой, на которой и основана данная разработка. Решение о выпуске проекта в open source поставило перед нами задачу разработать такую архитектуру, которая позволит дополнять функционал NATter'а собственными реализациями работы с протоколами помимо тех, что уже “вшиты” в базовую реализацию сервиса (HTTP, NATS, Kafka), не изменяя заложенную концепцию, а также “вытянуть” все зависимости из нашей общей библиотеки, написанной специально для сервисов Camshare, непосредственно в NATter.

Используемые библиотеки

Реализация необходимых модулей внутренней библиотеки для сервисов Camshare была вытянута в почти неизменном виде, а значит, что и внешние Go-библиотеки для реализации и модульного тестирования были взяты те же, что и в нашем самописном фреймворке. Упоминания стоят следующие библиотеки, фундаментально повлиявшие на ход разработки как Camshare-сервисов в целом, так и NATter’а в частности:

  • nats-io/nats.go – клиент, используемый для реализации работы с NATS;
  • Shopify/sarama – клиент, используемый для реализации работы с Apache Kafka;
  • go-chi/chi – HTTP-роутер, используемый для реализации REST API;
  • golang/protobuf – библиотека для работы с Protocol Buffers, используемая для сериализации сообщений в пакеты;
  • stretchr/testify – библиотека, предоставляющая инструменты для тестирования кода: assert’ы, mock’и, suite’ы.

Несмотря на то что модули внутренней библиотеки уже были готовы к использованию, их перенос непосредственно в исходный код NATter’а требовал склеивания логики пакетов из библиотеки, реализующих работу с протоколами, с логикой соответствующих пакетов в самом сервисе во избежание наличия лишних уровней абстракции.

Заключение

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

Исходный код NATter’а уже загружен в публичный репозиторий, и в скором времени ожидается первый релиз.