В наши дни архитектуры на базе микросервисов стали внедряться практически повсеместно. И нередки ситуации, когда какая-нибудь бизнес-функция может генерировать большое количество сетевого трафика в форме обмена сообщениями между несколькими микросервисами, которые она использует. Если мы сможем сделать способ передачи сообщений более эффективным за счет, например, уменьшения размера сообщений, то мы сможем использовать ту же инфраструктуру для работы с более высокими нагрузками.
Protobuf (сокращение от «protocol buffers») предоставляет независящие от языка и платформы механизмы сериализации структурированных данных для использования в коммуникационных протоколах, хранилищах данных и т. д. gRPC — это современный фреймворк удаленного вызова процедур («remote procedure call» — RPC) с открытым исходным кодом, который может работать где угодно. Их сочетание позволяет создать эффективный формат сообщений, который автоматически сжимается и обеспечивает первоклассную поддержку сложных структур данных, а также ряд других преимуществ (в отличие от JSON).
Микросервисные среды требуют большого количества коммуникаций между сервисами, и для этого между сервисами должен быть согласован ряд моментов. Они должны иметь согласованое API для обмена данными, например, POST (или PUT) и GET для отправки и получения сообщений. Они также должны договориться о формате данных (JSON). Клиентам, вызывающим сервис, также необходимо позаботиться о написании кода для удаленных вызовов (фреймворки!). Protobuf и gRPC предоставляют возможность определить схему сообщения (а JSON — нет) и сгенерировать скелет программы для использования gRPC‑сервиса (без фреймворков).
Хоть JSON — это человекочитаемый формат со вложенной структурой данных, у него есть несколько недостатков, например, отсутствие схемы, объекты могут быть довольно громоздкими, и кое‑где может не хватать комментариев.
В этой статье я покажу вам, как с помощью gRPC и Protobuf можно создать решения, которые помогут вам обойти эти ограничения.
Так что же такое gRPC и Protobuf?
gRPC — это современный фреймворк удаленного вызова процедур с открытым исходным кодом, который может работать где угодно. Он обеспечивает прозрачное взаимодействие клиентских и серверных приложений и упрощает создание связанных систем. gRPC в находится в стадии разработки в CNCF.
Я рекомендовал бы вам в качестве практики создать сервер потоковой передачи данных в формате JSON по HTTP. Тогда вы поймете, о чем я говорю. Потоковая передача данных встроена в gRPC. О концепциях gRPC можно почитать здесь. Лично мне gRPC чем-то напоминает CORBA.
Protobuf — это инструмент для сериализации данных. Protobuf предоставляет возможность определять полностью типизированные схемы для сообщений. Он также позволяет вставлять документацию прямо в само сообщение.
gRPC использует HTTP/2 с постоянным соединением и мультиплексированием для повышения производительности по сравнению с сервисами, работающими на REST по HTTP 1.1. Однако у постоянного соединения есть проблемы с прокси 4-го уровня. Нам нужен прокси, поддерживающий балансировку нагрузки на 7-м уровне. Envoy как раз может проксировать вызовы gRPC с поддержкой балансировки нагрузки на стороне сервере. Envoy также обеспечивает обнаружение сервисов на основе внешнего сервиса, известного как EDS, и я покажу вам, как использовать и эту функцию.
Что мы будем создавать
В этой статье я создаю gRPC-сервис на основе Kotlin. Я буду балансировать нагрузку между несколькими инстансами моего сервиса с помощью прокси Enovy. А также я настрою простой REST-сервис, который обеспечит обнаружение сервиса для Envoy. Базовая архитектура выглядит следующим образом.
Подготовка компонентов
Во-первых, нам нужно определить сообщение Protobuf, которое будет служить контрактом между клиентом и сервером (полный файл см. в event.proto):
syntax = "proto3";
import "google/protobuf/empty.proto";
package event;
option java_package = "com.proto.event";
option java_multiple_files = true;
message Event {
int32 event_id = 1;
string event_name = 2;
repeated string event_hosts = 3;
}
enum EVENT_TYPE {
UNDECLARED = 0;
BIRTHDAY = 1;
MARRIAGE = 2;
}
message CreateEventResponse{
string success = 1;
}
message AllEventsResponse{
Event event = 1;
}
service EventsService{
rpc CreateEvent(Event) returns (CreateEventResponse) {};
rpc AllEvents(google.protobuf.Empty) returns (stream AllEventsResponse) {};
}
Это сообщение будет использовано плагином Gradle gRPC для генерации заглушек. Эти заглушки будет использовать код клиента и сервера. Чтобы сгенерировать заглушки, вы можете запустить задачу Gradle generateProto.
Теперь пришло время написать сервер:
val eventServer = ServerBuilder.forPort(50051)
.addService(EventsServiceImpl()) //серверная реализация
.build()
eventServer.start()
println("Event Server is Running now!")
Runtime.getRuntime().addShutdownHook( Thread{
eventServer.shutdown()
} )
eventServer.awaitTermination()
После того как мы расправились с основным кодом сервера, мы напишем бизнес-логику, которая выводит захардкоженное сообщение и возвращает фиксированный ответ.
override fun createEvent(request: Event?, responseObserver: StreamObserver<CreateEventResponse>?) {
println("Event Created ")
responseObserver?.onNext(CreateEventResponse.newBuilder().setSuccess("true").build())
responseObserver?.onCompleted()
}
Далее давайте напишем клиент, который будет использовать наш сервис событий:
fun main(args: Array<String>) {
var eventsChannel = ManagedChannelBuilder.forAddress("10.0.0.112", 8080)
.usePlaintext()
.build()
var eventServiceStub = EventsServiceGrpc.newBlockingStub(eventsChannel)
for(i in 1..20) {
eventServiceStub.createEvent(Event.newBuilder().setEventId(i).setEventName("Event $i").build())
}
eventsChannel.shutdown()
}
Я скопировал код сервера в другой файл и изменил номер порта, чтобы имитировать несколько инстансов нашего сервиса.
Конфигурация прокси-сервера Envoy состоит из трех частей. Все эти настройки находятся в файле envoy.yaml. Убедитесь, что вы изменили IP-адрес EDS-сервиса в соответствии с вашими настройками. Обновить IP-адрес сервиса можно в файле EDSServer.kt.
Теперь определим фронтенд-сервис. Этот сервис будет принимать запросы от клиентов.
listeners:
- name: envoy_listener
address:
socket_address: { address: 0.0.0.0, port_value: 8080 }
filter_chains:
- filters:
- name: envoy.http_connection_manager
config:
stat_prefix: ingress_http
codec_type: AUTO
route_config:
name: local_route
virtual_hosts:
- name: local_service
domains: ["*"]
routes:
- match: { prefix: "/" }
route: { cluster: grpc_service }
http_filters:
- name: envoy.router
Определим бекенд-сервис (его имя grpc_service
в файле envoy.yaml
). Бекенд-сервис будет заниматься балансировкой нагрузки вызовов этой группы серверов. Обратите внимание, что мы не знаем фактическое местоположение бекенд-сервиса. Местоположение бекенд-сервиса (service discovery) предоставляется через EDS-сервис. Чуть-чуть дальше мы поговорим об определении конечной точки EDS.
- name: grpc_service
connect_timeout: 5s
lb_policy: ROUND_ROBIN
http2_protocol_options: {}
type: EDS
eds_cluster_config:
eds_config:
api_config_source:
api_type: REST
cluster_names: [eds_cluster]
refresh_delay: 5s
По желанию вы можете определить конечную точку EDS. (Вы также можете предоставить фиксированный список серверов). Это еще один сервис, который предоставляет список конечных точек бекенда. Таким образом, Envoy сможет динамически подстраиваться под доступные серверы. Я написал этот EDS-сервис в виде простого класса.
- name: eds_cluster
connect_timeout: 5s
type: STATIC
hosts: [{ socket_address: { address: 10.0.0.112, port_value: 7070 }}]
Запуск проекта
Скопируйте проект локально:
git clone https://github.com/masoodfaisal/grpc-example.git
Соберите проект с помощью Gradle:
cd grpc-example
./gradlew generateProto
./gradlew build
Поднимите EDS-сервер, чтобы обеспечить обнаружение сервисов для прокси-сервера Envoy:
cd grpc-example
./gradlew -PmainClass=com.faisal.eds.EDSServerKt execute
Инициализируйте несколько инстансов сервиса:
cd grpc-example
./gradlew -PmainClass=com.faisal.grpc.server.EventServerKt execute
./gradlew -PmainClass=com.faisal.grpc.server.EventServer2Kt execute
Запустите прокси Enovy:
cd envoy-docker
docker build -t envoy:grpclb .
docker run -p 9090:9090 -p 8080:8080 envoy:grpclb
Мой клиент выполняет вызов в цикле, который демонстрирует, как нагрузка распределяется по кругу.
./gradlew -PmainClass=com.faisal.grpc.client.EventClientKt execute
Заключение
gRPC обеспечивает более высокую производительность, меньше шаблонного кода для управления и сильно типизированную схему для ваших микросервисов. Среди других фич gRPC, полезных в мире микросервисов, можно выделить повторные попытки, таймауты и обработка ошибок. Так же напоследок хочу порекомендовать вам замечательную статья о gRPC, доступную на сайте CNCF.
И пусть ваш следующий сервис будет с gRPC!
На рынке труда не многие Kotlin-разработчики могут похвастаться навыкам работы с Kotlin DSL. Однако он по факту стал неотъемлемой частью экосистемы Kotlin. Владение этим инструментом является одним из показателей квалификации разработчика. Приглашаем всех желающих на открытый урок, на котором разложим Kotlin DSL по полочкам:
обсудим, что это такое и когда его уместно применять;
из каких элементов он состоит;
на практике напишем несложный пример.
Записаться на урок можно бесплатно на странице курса «Kotlin Backend Developer. Professional».
n0isy
Envoy всем хорош, кроме документации и сложности конфигаруционных файлов. Даже llm в него могут с трудом, что является показателем сложности.