Более глубокое погружение в gRPC и создание собственной микросервисной архитектуры в Node.js

Соавтор: Алексей Коржиков

Что такое ГРПК?

Удаленные вызовы процедур gRPC, конечно же!

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

История

  • Март 2015 🗓
  • Google ➡️ с открытым исходным кодом
  • Стандартизируйте архитектуру, структуру и инфраструктуру микросервисов
  • SPDY (HTTP/2)
  • QUIC (HTTP/3)
  • Stubby
  • Часть Фонда облачных вычислений
  • Мотивация, FAQ, Туториалы

RPC

метод распределенных вычислений, когда

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

В процессе задействовано больше шагов:

  • локальная клиентская заглушка
  • маршалинг параметров в сообщение
  • вызов удаленного сервера
  • заглушка локального сервера
  • распаковка или десортировка параметров сообщения

Вопросы

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

Клиент 😀 ⬅️ ➡️ 💻 Связь с сервером

Веб-протоколы

  • МЫЛО
  • ОТДЫХ
  • ГрафQL

Или даже более общий

Нам всегда нужна клиентская библиотека для связи с сервером!

Функции

  • Определения службы (протокол)
  • Строгая типизация
  • Буферы протокола ⏭

Клиент и сервер

Общение

  • HTTP/2
  • Сериализация
  • Порядок сообщений
  • Потоки
  • Синхронный/асинхронный

Аутентификация

// http://protobuf-compiler.herokuapp.com/
syntax = "proto3";

package hello;

service HelloService {
  rpc JustHello (HelloRequest) returns (HelloResponse);

  rpc ServerStream(HelloRequest) returns (stream HelloResponse);

  rpc ClientStream(stream HelloRequest) returns (HelloResponse);

  rpc BothStreams(stream HelloRequest) returns (stream HelloResponse);
}

message HelloRequest {
  string greeting = 1;
}

message HelloResponse {
  string reply = 1;
}

Что такое буферы протокола?

Эффективная технология сериализации структурированных данных

message Person {
  string name = 1;
  int32 id = 2;
  bool has_ponycopter = 3;
}

Кто-нибудь знает, что означают цифры справа?

История

Функции

Дополнительно

// rule type name tag
repeated uint64 vals = 1;
  • Поля и типы
  • Сообщение
  • Скаляр
  • перечисления
  • Повторный
  • Нет типа void, нет скалярных типов в аргументах
  • Пакеты
  • Услуги — не используются protobuf напрямую
syntax = "proto3";
package hello;
service HelloService {
  rpc SayHello (HelloRequest) returns (HelloResponse);
}
message HelloRequest {
  string greeting = 1;
}
message HelloResponse {
  string reply = 1;
}
  • Плагины
  • Параметры компилятора
  • Вложенные типы
  • Обязательное, Дополнительное
  • Без версионности
  • Не меняйте теги для существующих полей
  • package mypackage.v1, package mypackage.v2beta1
  • Карты, oneOf, все

Криптовалюта 🦄 Конвертер валют

Предпосылки

1. Оформить демо-проект

Начнем с клонирования демонстрационного монорепозитория.

git clone [email protected]:x-technology/mono-repo-nodejs-svc-sample.git

2. Установите протокол

Для эффективной работы с форматом .proto и возможности генерировать представление буферов протокола на основе TypeScript нам необходимо установить библиотеку protoc.

Если вы пользователь MacOS и у вас есть менеджер пакетов brew, следующая команда — самый простой способ установки:

brew install protobuf
# Ensure it's installed and the compiler version at least 3+
protoc --version

Для пользователей Linux выполните следующие команды:

PROTOC_ZIP=protoc-3.14.0-linux-x86_64.zip
curl -OL https://github.com/protocolbuffers/protobuf/releases/download/v3.14.0/$PROTOC_ZIP
sudo unzip -o $PROTOC_ZIP -d /usr/local bin/protoc
sudo unzip -o $PROTOC_ZIP -d /usr/local 'include/*'
rm -f $PROTOC_ZIP

В качестве альтернативы вручную загрузите и установите протокол отсюда.

3. Подготовьте среду

Убедитесь, что у нас установлен Node.js v14+. Если нет, nvm — очень хороший инструмент для локальной установки нескольких версий узлов и легкого переключения между ними.

Затем нам нужно установить зависимости и загрузить lerna в монорепозиторий.

yarn install
yarn lerna bootstrap

Ура! 🎉 Теперь мы готовы приступить к проекту.

Структура монорепозитория

Для лучшего управления проектом монорепо мы использовали Lerna и Yarn Workspaces.

Проект имеет следующую структуру:

  • Папка ./packages/common содержит общие библиотеки, используемые в других сервисах проекта.
  • Папка ./packages/services/grpc содержит сервисы gRPC, которые мы создаем для совместного использования продукта.
  • Папка ./proto содержит файлы proto, описывающие протокол ввода/вывода и взаимодействие между сервисами.
  • ./node_modules — папка с зависимостями, общая для всех микросервисов.
  • ./lerna.json - файл конфигурации lerna, определяющий, как он должен работать с монорепозиторием.
  • ./package.json - описание нашего пакета, содержащего важную часть:
"workspaces": [
  "packages/common/*",
  "packages/services/grpc/*"
]

Идем дальше 🚚

Использование Лерны

Lerna предлагает несколько команд, которые можно легко выполнить для всех/или отфильтрованных пакетов.

Мы используем наши общие модули, скомпилированные в JavaScript, поэтому, прежде чем использовать их в сервисах, нам нужно сначала их собрать.

Следующая команда выполнила команду build для всех общих пакетов, отфильтрованных с флагом --scope=@common/*.

yarn lerna run build --scope=@common/*

Общие и услуги

Давайте посмотрим на ./packages/common. Он содержит общие библиотеки, используемые в других местах системы. Одной из таких библиотек является @common/grpc, она содержит прототипы, сгенерированные в форматах TypeScript/JavaScript, а также общий сервер gRPC.

Следующая команда требуется для запуска, если мы меняем прототипы:

cd ./packages/common/go-grpc && yarn build
# OR using lerna
yarn lerna run build --scope=@common/*

Для этой конкретной задачи мы реализуем только службы gRPC, которые хранятся в папке ./packages/services/grpc. Но если мы решим добавить rest, ./packages/common/rest будет хорошим местом для добавления.

Что мы строим

Мы создаем конвертер валют, который можно использовать при вызовах gRPC.

Мы намерены отправить запрос, аналогичный convert 0.345 ETH to CAD, и в результате мы хотим узнать окончательную сумму в канадских долларах и коэффициент конверсии. Мы также предполагаем, что это может быть более одного поставщика валюты, например.

  1. Ставки Центрального банка Европы
  2. Ставки Банка Англии
  3. Курсы криптовалют

Вот как это работает:

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

Более глубокий взгляд на файлы *.proto

В папке proto по нашей схеме мы создали следующие файлы:

  • currency-converter.proto - интерфейс преобразователя
  • currency-provider.proto - интерфейс провайдера
  • ecb-provider.proto и crypto-provider.proto - реализация двух конкретных провайдеров.

В поставщике реализации мы могли бы просто import создать существующий прото-файл и использовать его определения.

import "currency-provider.proto";
package ecbProvider;
service EcbProvider {
  rpc GetRates(currencyProvider.GetRatesRequest) returns (currencyProvider.GetRatesResponse) {}
}

Давайте подробнее рассмотрим, как прототипы генерируются из .proto в JavaScript.

Возвращаясь к модулю @common/go-grpc, мы можем найти ./bin/build.mjs.

Вот основная команда, которую мы смогли там найти:

protoc --plugin="protoc-gen-ts=`pwd`/node_modules/.bin/protoc-gen-ts" --ts_out="service=grpc-node:`pwd`/src/proto" --proto_path="`pwd`/../../../proto/" `pwd`/../../../proto/*.proto

Как создать новую общую библиотеку

Мы используем hygen для шаблонов наших новых сервисов и общих библиотек.

  1. Например, мы хотим создать новую библиотеку logger.
  2. В корневом каталоге запустите команду yarn bootstrap:common и следуйте за starter.
  3. Перейдите в новую папку в терминале
cd ./packages/common/logger

4. Установите зависимости

yarn install

5. Обязательно укажите соответствующее имя в файле package.json:

"name": "@common/logger",

Будем следовать правилу, что все распространенные библиотеки имеют префикс @common/

6. Создаем нашу библиотеку в src/index.js

export const debug = (message: string) => console.debug(message);
export const info = (message: string) => console.info(message);
export const error = (message: string) => console.error(message);
export default { debug, info, error };

7. Убедитесь, что сборка прошла успешно с помощью команды:

yarn build

8. Подключим нашу только что созданную библиотеку где-нибудь в существующем сервисе:

yarn lerna add @common/logger --scope=@grpc/ecb-provider

9. На последнем этапе нам нужно использовать библиотеку внутри сервиса ecb-provider. Изменим файл ./src/index.ts:

import logger from '@common/logger';

logger.debug('service has started');

10. Пересоберите ecb-провайдер, чтобы убедиться в отсутствии проблем

yarn build

Ура! 🎉 Это работает!

Как создать новую услугу

  1. Например, мы хотим создать новый сервис crypto-compare-provider, который является еще одним поставщиком курсов валют, возвращающим криптовалюты.
  2. Создайте папку по пути ./packages/services/grpc/crypto-compare-provider. Для простоты просто скопируйте существующий ecb-provider и переименуйте его.
  3. Заходим в папку в терминале
cd ./packages/services/grpc/crypto-compare-provider

4. Установите зависимости

yarn install

5. Обязательно задайте соответствующее имя в файле package.json:

"name": "@grpc/crypto-compare-provider",

Будем следовать правилу — все сервисы grpc имеют префикс @grpc/.
6. Создайте файл метода сервиса packages/services/grpc/crypto-provider/src/services/getRates.ts

import { currencyProvider } from '@common/go-grpc';

export default async (
  _: currencyProvider.GetRatesRequest,
): Promise<currencyProvider.GetRatesResponse> => {
  return new currencyProvider.GetRatesResponse({
    rates: [],
    baseCurrency: 'USD',
  });
};

8. Далее нам нужно использовать этот метод внутри server.ts

import { Server, LoadProtoOptions, currencyProvider } from '@common/go-grpc';
import getRates from './services/getRates';

const { PORT = 50051 } = process.env;
const protoOptions: LoadProtoOptions = {
  path: `${__dirname}/../../../../../proto/crypto-compare-provider.proto`,
  // this value should be equvalent to the one defined in *.proto file as "package cryptoCompareProvider;"
  package: 'cryptoCompareProvider',
  // this value should be equvalent to the one defined in *.proto file as "service CryptoCompareProvider"  
  service: 'CryptoCompareProvider',
};

const server = new Server(`0.0.0.0:${PORT}`, protoOptions);
server
  .addService<currencyProvider.GetRatesRequest,
    Promise<currencyProvider.GetRatesResponse>>('GetRates', getRates);
export default server;

9. Убедитесь, что сборка прошла успешно с помощью команды:

yarn build

10. Запускаем службу командой:

yarn start

Ура! 🎉 Это работает!

Как тестировать сервисы

Мы используем jest в качестве тестового фреймворка и решили написать интеграционные тесты для наших сервисов. Это лучший способ понять различные ситуации, которые могут произойти с сервисами на конкретном входе/выходе.

  1. Начнем с создания тестового файла test/services/index.spec.ts
mkdir -p test/services
touch test/services/index.spec.ts

2. Первым делом в тесте нам нужно определить сервер, который импортируется из папки src и запустить его в разделе beforeAll

import { ecbProvider, currencyProvider, createInsecure } from '@common/go-grpc';
import server from '../../src/server';

const testServerHost = 'localhost:50061';
beforeAll(async () => {
   await server.start(testServerHost);
});
afterAll(async () => {
   await server.stop();
});

3. Далее создадим клиент, который будет вызывать методы сервера по протоколу gRPC.

import { ecbProvider, createInsecure } from '@common/go-grpc';

const client = new ecbProvider.EcbProviderClient(
  testServerHost,
  createInsecure(),
);

4. Давайте добавим сюда первый набор тестов и будем ожидать определенного результата от метода сервиса.

describe('GetRates', () => {
 it('should return currency rates', async () => {
    const response = await client.GetRates(new currencyProvider.GetRatesRequest());

    expect(response.toObject()).toEqual({
      baseCurrency: 'EUR',
      rates: [
        { currency: 'USD', rate: 1.1348 },
      ],
    });
  });
});

5. Теперь пришло время попробовать это с помощью команды:

yarn test

Великолепно! 🎉 Это работает!

Как запустить это волшебство 🪄?

Как мы могли бы использовать эту магию, чтобы конвертировать для нас какую-то валюту?

export PORT=50052 && cd ./packages/services/grpc/ecb-provider/ && yarn start
export PORT=50051 && cd ./packages/services/grpc/currency-provider/ && yarn start

Вот инструмент grpcurl для отправки тестового запроса в сервис gRPC с терминала.

# list all services
grpcurl -import-path ./proto -proto ecb-provider.proto list

# list all methods of service
grpcurl -import-path ./proto -proto ecb-provider.proto list ecbProvider.EcbProvider

# call method GetRates
echo '{}' | grpcurl -plaintext -import-path ./proto -proto ecb-provider.proto -d @ 127.0.0.1:50052 ecbProvider.EcbProvider.GetRates

Ура! 🚀

Краткое содержание

Мы обнаружили gRPC, Lerna, Yarn Workspaces и конфигурацию инструментов для проекта монорепозитория, которые повышают скорость и качество при разработке микросервисной архитектуры с нуля.

Ссылки