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. Общий алгоритм работы батчинга:
- заводится таймер, обнуляется счётчик сообщений, определяющий размер пакета, и ожидается приход очередного сообщения;
- от отправителя приходит сообщение, которое удерживается в памяти до тех пор пока не выполнится одно из следующих условий:
- таймер истёк;
- размер пакета достиг определённого максимума;
- сервис завершает работу;
- после выполнения одного из условий выше каждое удерживаемое сообщение конвертируется в google.protobuf.Any, после чего массив полученных структур сериализуется в бинарник, отправляемый получателю;
- если сервис не завершает работу, возврат к шагу 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’а уже загружен в публичный репозиторий, и в скором времени ожидается первый релиз.