Более глубокое погружение в gRPC и создание собственной микросервисной архитектуры в Node.js
Соавтор: Алексей Коржиков
Что такое ГРПК?
Удаленные вызовы процедур gRPC, конечно же!
gRPC — это современная среда с открытым исходным кодом удаленного вызова процедур (RPC), которая может работать где угодно. Это позволяет клиентским и серверным приложениям прозрачно взаимодействовать и упрощает создание подключенных систем.
История
- Март 2015 🗓
- Google ➡️ с открытым исходным кодом
- Стандартизируйте архитектуру, структуру и инфраструктуру микросервисов
SPDY (HTTP/2)
QUIC (HTTP/3)
Stubby
- Часть Фонда облачных вычислений
- Мотивация, FAQ, Туториалы
RPC
метод распределенных вычислений, когда
- Клиентская программа отправляет запрос на известный удаленный сервер для выполнения указанной процедуры.
- Удаленный сервер выполняет локальную процедуру и отправляет ответ клиенту.
- Клиент продолжает работать
В процессе задействовано больше шагов:
- локальная клиентская заглушка
- маршалинг параметров в сообщение
- вызов удаленного сервера
- заглушка локального сервера
- распаковка или десортировка параметров сообщения
Вопросы
- Как клиентская служба вызывает удаленную службу?
- Как выставить удаленный сервис?
- Как сериализуются данные для сети?
- Как происходит общение?
- Аутентификация?
Клиент 😀 ⬅️ ➡️ 💻 Связь с сервером
Веб-протоколы
- МЫЛО
- ОТДЫХ
- ГрафQL
Или даже более общий
- HTTP/1.1
- HTTP/2
- TCP
- UDP
- Веб-сокеты
- Отправленные сервером события
Нам всегда нужна клиентская библиотека для связи с сервером!
Функции
- Определения службы (протокол)
- Строгая типизация
- Буферы протокола ⏭
Клиент и сервер
- Сгенерированный код (заглушки)
- 10+ языков
- Платформы и среды — Android, Web, Flutter
- Основная версия 1.43.0
- Точки расширения
Общение
- 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;
}
Кто-нибудь знает, что означают цифры справа?
История
- Буферы протокола — Google Developers
- Июль 2008 🗓
- Google ➡️ с открытым исходным кодом
Функции
- Набранный формат
.proto
- Генерация кода
protoc
- компилятор буферов протокола- 10+ языков
- Сторонние дополнения для протокольных буферов
- Описание услуг
Дополнительно
// 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
, и в результате мы хотим узнать окончательную сумму в канадских долларах и коэффициент конверсии. Мы также предполагаем, что это может быть более одного поставщика валюты, например.
- Ставки Центрального банка Европы
- Ставки Банка Англии
- Курсы криптовалют
Вот как это работает:
- Конвертер валют выбирает каждого провайдера, накапливает и использует для конвертации курсы, полученные от провайдеров.
- Валютный провайдер — это прокси для получения единого источника курсов, он также конвертирует курсы в общий формат, определенный в прототипах.
Более глубокий взгляд на файлы *.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 для шаблонов наших новых сервисов и общих библиотек.
- Например, мы хотим создать новую библиотеку
logger
. - В корневом каталоге запустите команду
yarn bootstrap:common
и следуйте за starter. - Перейдите в новую папку в терминале
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
Ура! 🎉 Это работает!
Как создать новую услугу
- Например, мы хотим создать новый сервис
crypto-compare-provider
, который является еще одним поставщиком курсов валют, возвращающим криптовалюты. - Создайте папку по пути
./packages/services/grpc/crypto-compare-provider
. Для простоты просто скопируйте существующийecb-provider
и переименуйте его. - Заходим в папку в терминале
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 в качестве тестового фреймворка и решили написать интеграционные тесты для наших сервисов. Это лучший способ понять различные ситуации, которые могут произойти с сервисами на конкретном входе/выходе.
- Начнем с создания тестового файла
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 и конфигурацию инструментов для проекта монорепозитория, которые повышают скорость и качество при разработке микросервисной архитектуры с нуля.
Ссылки
- Буферы протокола — Разработчики Google
- gRPC
- Масштабируемые микросервисы с gRPC, Kubernetes и Docker, Сандип Динеш, Google
- Микросервисы NestJS — 4 — Использование gRPC
- Начало работы с gRPC и JavaScript — Колин Айриг, Joyent
- Ускоренный курс протокольных буферов
- Ускоренный курс по gRPC — режимы, примеры, плюсы и минусы и многое другое
- Основное введение в gRPC в Node
- Выбор технологии API: GRPC, REST, GraphQL — Юра Гороховский
- GraphQL, gRPC или REST? Решение дилеммы разработчика API — Роб Кроули
- История о том, почему мы переходим на gRPC и как мы это делаем — Маттиас Грютер, Spotify
- Введение в gRPC: общая структура RPC, которая ставит мобильные устройства и HTTP/2 на первое место от Мете Атамель