Pull to refresh
1051.99
OTUS
Цифровые навыки от ведущих экспертов

Создаем высокопроизводительные микросервисы с помощью gRPC, Ballerina и Go

Reading time8 min
Views11K
Original author: Lakmal Warusawithana

Привет, Хабр. Будущих студентов курса "Software Architect" приглашаем принять участие в открытом вебинаре на тему "Идемпотентность и коммутативность API в очередях и HTTP".

Также делимся переводом полезного материала.


В рамках современной микросервисной архитектуры мы можем разделить микросервисы на две основные группы в зависимости от их взаимосвязи и взаимодействия. Первая группа представляет из себя внешние микросервисы, которые напрямую доступны пользователям. В основном это API на основе HTTP, которые используют обычные текстовые сообщения (JSON, XML и т. д.), оптимизированные для использования сторонними разработчиками, использующие передачу репрезентативного состояния (REST) ​​в качестве коммуникационной технологии. 

Распространенность и хорошая поддержка REST играют решающую роль в успехе внешних микросервисов. OpenAPI предоставляет четко определенные спецификации для описания, создания, использования и визуализации REST API. Для таких API существуют системы управления API, которые обеспечивают безопасность, ограничение скорости, кеширование и монетизацию наряду с реализацией бизнес-требований. В качестве альтернативы REST API на основе HTTP можно использовать GraphQL, но это уже тема для отдельной статьи.

Вторая группа — внутренние микросервисы, которые не предназначены для взаимодействия с внешними системами или сторонними разработчиками. Эти микросервисы взаимодействуют друг с другом для выполнения определенного набора задач. Внутренние микросервисы используют либо синхронный, либо асинхронный режим связи. Во многих реализациях синхронного режима мы наблюдаем использование REST API по HTTP, но это не самая подходящая технология для таких целей. В этой статье мы подробно рассмотрим, как мы можем использовать бинарный протокол, такой как gRPC, в качестве оптимизированного протокола межсервисного взаимодействия.

Что такое gRPC?

gRPC — это относительно новая парадигма API удаленного вызова процедур (Remote procedure call — RPC) для связи между сервисами. Как и все другие RPC, он позволяет напрямую вызывать методы в серверном приложении на другом компьютере, как если бы это был локальный объект. Как и другие бинарные протоколы, такие как Thrift и Avro, для определения контракта на обслуживание gRPC использует язык описания интерфейсов (interface description language — IDL). gRPC по умолчанию использует HTTP/2 — новейший сетевой транспортный протокол, что делает gRPC намного более быстрым и надежным по сравнению с REST на HTTP/1.1. 

Вы можете определить контракт gRPC с помощью Protocol Buffers, где каждое определение сервиса указывает количество методов с ожидаемыми входными и выходными сообщениями, структурой данных параметров и типов возврата. Используя инструменты, предоставляемые основными языками программирования, можно сгенерировать скелет на стороне сервера и код (заглушку) на стороне клиента с помощью того же файла Protocol Buffers, который определяет контракт.

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

Рисунок 1. Сегмент архитектуры микросервисов интернет-магазина розничной торговли
Рисунок 1. Сегмент архитектуры микросервисов интернет-магазина розничной торговли

Одним из основных преимуществ микросервисной архитектуры является возможность создания различных сервисов, использующих наиболее подходящий для каждого них язык программирования, освобождающая от необходимости написания всей структуры на одном языке. На Рисунке 1 показан сегмент микросервисной архитектуры интернет-магазина розничной торговли, где четыре микросервиса реализованы на Ballerina и Golang, которые работают вместе, чтобы обеспечить базовый функционал розничного интернет-магазина. Поскольку gRPC поддерживается многими основными языками программирования, когда мы определяем сервисные контракты, реализация может выполняться с помощью подходящего под конкретный сервис языка программирования. 

Определим контракты для каждого сервиса. 

syntax="proto3";
 
package retail_shop;
 
service OrderService {
   rpc UpdateOrder(Item) returns (Order);
}  
message Item {
   string itemNumber = 1;
   int32 quantity = 2;
}
message Order {
   string itemNumber = 1;
   int32 totalQuantity = 2;
   float subTotal = 3;

Листинг 1. Контракт для микросервиса Order (order.proto)

Order получает товары и количество покупок и возвращает промежуточный итог. Здесь я использую инструмент gRPC Ballerina для создания шаблонного кода службы gRPC и заглушки/клиента соответственно. 

$ ballerina grpc --mode service --input proto/order.proto --output gen_code

Определим контракты для каждого сервиса. 

import ballerina/grpc;
listener grpc:Listener ep = new (9090);
 
service OrderService on ep {
   resource function UpdateOrder(grpc:Caller caller, Item value) {
       // Implementation goes here.
 
       // You should return an Order
   }
}
public type Order record {|
   string itemNumber = "";
   int totalQuantity = 0;
   float subTotal = 0.0;
  
|};
public type Item record {|
   string itemNumber = "";
   int quantity = 0;
  
|}; 

Листинг 2. Фрагмент сгенерированного шаблонного кода (OrderServicesampleservice.bal).

Сервис gRPC идеально соотносится ​​с типом service Ballerina, gRPC rpc соотносится ​​с resource function Ballerina, а сообщения gRPC — с типом record

Для микросервиса Order я создал отдельный проект Ballerina и использовал сгенерированный шаблонный код OrderService для реализации унарного сервиса gRPC

Унарная блокировка

OrderService вызывается в микросервисе Cart. Для создания клиентской заглушки и клиентского кода мы можем использовать следующую команду Ballerina.

$ ballerina grpc --mode client --input proto/order.proto --output gen_code

Сгенерированная клиентская заглушка имеет как блокирующие, так и неблокирующие удаленные методы. Этот пример кода демонстрирует, как унарный сервис gRPC взаимодействует с блокирующим клиентом gRPC.

public remote function UpdateOrder(Item req, grpc:Headers? headers = ()) returns ([Order, grpc:Headers]|grpc:Error) {
      
       var payload = check self.grpcClient->blockingExecute("retail_shop.OrderService/UpdateOrder", req, headers);
       grpc:Headers resHeaders = new;
       anydata result = ();
       [result, resHeaders] = payload;
       return [<Order>result, resHeaders];
   }
};

Листинг 3. Фрагмент сгенерированного кода удаленного объекта для режима блокировки

Абстракция удаленного метода Ballerina представляет собой хорошо подогнанную заглушку клиента gRPC, и вы можете сами отметить, насколько код вызова UpdateOrder чист и аккуратен.

Микросервис Checkout выставляет окончательный счет, объединяя все промежуточные заказы, полученные от микросервиса Cart. В нашем случае мы собираемся отправлять все промежуточные заказы в виде stream Order сообщений.

syntax="proto3";
package retail_shop;
 
service CheckoutService {
   rpc Checkout(stream Order) returns (FinalBill) {}
}
message Order {
   string itemNumber = 1;
   int32 totalQuantity = 2;
   float subTotal = 3;
}
message FinalBill {
   float total = 1;
}

Листинг 4. Сервисный контракт для микросервиса Checkout (checkout.proto)

Чтобы сгенерировать шаблонный код для checkout.proto, вы можете использовать команду ballerina grpc.

$ ballerina grpc --mode service --input proto/checkout.proto --output gencode

Потоковая передача клиента gRPC

Потоковые сообщения микросервисов Cart (клиент) доступны в качестве аргумента объекта потока, который вы может перебирать с помощью цикла, обрабатывая каждое отдельное сообщение, отправленное клиентом. Вот пример реализации:

service CheckoutService on ep {
   resource function Checkout(grpc:Caller caller, stream<Order,error> clientStream) {
       float totalBill = 0;
 
       //Iterating through streamed messages here
       error? e = clientStream.forEach(function(Order order) {
           totalBill += order.subTotal;           
       });
       //Once the client completes stream, a grpc:EOS error is returned to indicate it
       if (e is grpc:EOS) {
           FinalBill finalBill = {
               total:totalBill
           };
           //Sending the total bill to the client
           grpc:Error? result = caller->send(finalBill);
           if (result is grpc:Error) {
               log:printError("Error occurred when sending the Finalbill: " + 
result.message() + " - " + <string>result.detail()["message"]);
           } else {
               log:printInfo ("Sending Final Bill Total: " + 
finalBill.total.toString());
           }
           result = caller->complete();
           if (result is grpc:Error) {
               log:printError("Error occurred when closing the connection: " + 
result.message() +" - " + <string>result.detail()["message"]);
           }
       }
       //If the client sends an error instead it can be handled here
       else if (e is grpc:Error) {
           log:printError("An unexpected error occured: " + e.message() + " - " +
                                                   <string>e.detail()["message"]);
       }   
   }
}

Листинг 5. службы Фрагмент кода Service для примера реализации CheckoutService(CheckoutServicesampleservice.bal)

По завершению клиентского потока возвращается ошибка grpc:EOS, которую можно использовать для определения того, когда следует отправить окончательное ответное сообщение (агрегированный итог) клиенту с помощью caller object.

Клиентского код и клиентскую заглушку для CheckoutService можно сгенерировать с помощью следующей команды:

$ ballerina grpc --mode client --input proto/checkout.proto --output gencode

Давайте посмотрим на реализацию микросервиса Cart. У микросервиса Cart есть два REST API: один для добавления товаров в корзину, а другой — для окончательного расчета. При добавлении элементов в корзину он получит промежуточный заказ с промежуточным итогом для каждого элемента, выполнив вызов gRPC для микросервиса Order и сохранив его в памяти. Вызов микросервиса Checkout отправит все промежуточные заказы, хранящиеся в памяти, в микросервис Checkout в виде потока gRPC и вернет общую сумму для оплаты. Ballerina использует встроенный тип Stream и абстракции Client Object для реализации потоковой передачи клиента gRPC. На Рисунке 2 показано, как работает потоковая передача клиента Ballerina.

Рисунок 2. Потоковая передача клиента gRPC Ballerina 
Рисунок 2. Потоковая передача клиента gRPC Ballerina 

Полную реализацию потоковой передачи клиента CheckoutService можно найти в resource function checkout микросервиса Cart. Наконец, в процессе оформления заказа вам нужно совершить gRPC вызов микросервиса Stock, который реализован в Golang, и обновить инвентарь в магазине, вычтя проданные товары.

syntax="proto3";
package retail_shop;
option go_package = "../stock;gen";
import "google/api/annotations.proto";
 
service StockService {
   rpc UpdateStock(UpdateStockRequest) returns (Stock) {
       option (google.api.http) = {
           // Route to this method from POST requests to /api/v1/stock
           put: "/api/v1/stock"
           body: "*"
       };
   }
}
message UpdateStockRequest {
   string itemNumber = 1;
   int32 quantity = 2;
}
message Stock {
   string itemNumber = 1;
   int32 quantity = 2;

Листинг 6. Контракт для микросервиса Stock (stock.proto)

В этом сценарии та же служба UpdateStock будет вызываться с помощью вызова REST API для внешнего API, и с помощью gRPC вызова в качестве межсервисной связи. grpc-gateway — это плагин протокола protoc, который считывает определение сервиса gRPC и генерирует обратный прокси-сервер, который переводит RESTful JSON API в gRPC. 

 

Рисунок 3: grpc-gateway
Рисунок 3: grpc-gateway

grpc-gateway помогает вам предоставлять ваши API-интерфейсы одновременно в стиле gRPC и REST.

Следующая команда генерирует gRPC заглушки для Golang:

protoc -I/usr/local/include -I. \
-I$GOROOT/src \
-I$GOROOT/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \
--go_out=plugins=grpc:. \
stock.proto

Следующая команда генерирует код grpc-gateway для Golang:

protoc -I/usr/local/include -I. \
-I$GOROOT/src \
-I$GOROOT/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \
--grpc-gateway_out=logtostderr=true:. \
stock.proto

Следующая команда генерирует файл stock.swagger.json:

protoc -I/usr/local/include -I. \
-I$GOROOT/src \
-I$GOROOT/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \
-I$GOROOT/src \
--swagger_out=logtostderr=true:../stock/gen/. \
./stock.proto

Тестовый прогон

Клонируйте репозиторий git microservices-with-grpc и следуйте инструкциям README.md.

Заключение

gRPC является относительно новой технологией, но его быстрорастущая экосистема и сообщество определенно окажут влияние на развитие микросервисов. Поскольку gRPC является открытым стандартом, его поддерживают все основные языки программирования, что делает его идеальным для работы в среде многоязычных микросервисов. Как правило, мы можем использовать gRPC для всех синхронных коммуникаций между внутренними микросервисами, а также мы можем предоставлять его в виде REST API, используя новые технологии, такие как grpc-gateway. В дополнение к тому, что мы обсуждали в этой статье, такие фичи gRPC, как Deadlines, Cancellation, Channels и поддержка xDS, предоставят разработчикам еще больше возможностей и гибкости для создания высокоэффективных микросервисов.

Больше ссылок по теме

Чтобы узнать больше о поддержке Ballerina gRPC, читайте информацию по следующим ссылкам:

Golang уже получил всестороннюю поддержку gRPC, и мы можем расширить микросервисы, написанные на нем, среди прочего, с помощью Interceptor, Deadlines, Cancellation, и Channels от gRPC для повышения безопасности, надежности и отказоустойчивости. Загляните в git репозиторий grpc-go, в котором есть много рабочих примеров по этим концепциям.

Также посмотрите видео по теме:


Узнать подробнее о курсе "Software Architect".

Записаться на открытый урок "Идемпотентность и коммутативность API в очередях и HTTP".

Tags:
Hubs:
Total votes 8: ↑7 and ↓1+6
Comments0

Articles

Information

Website
otus.ru
Registered
Founded
Employees
101–200 employees
Location
Россия
Representative
OTUS