Русский
Русский
English
Статистика
Реклама

Microservices

Из песочницы Создание микросервисной архитектуры с использованием single-spa (миграция существующего проекта)

28.10.2020 18:06:09 | Автор: admin
image

Это первая статья по в данной теме, всего их планируется 3:

  1. * Создание root application из вашего существующего проекта, добавление в него 3 микро-приложения (vue, react, angular)
  2. Общение между микро-приложениями
  3. Работа с git (deploy, обновления)

Оглавление


  1. Общая часть
  2. Зачем это нужно
  3. Создание root контейнера (определение см. ниже) из вашего монолита
  4. Создаеммикро-приложение VUE (vue-app)
  5. Создаем микро-приложение REACT (react-app)
  6. Создаем микро-приложение ANGULAR (angular-app)

1. Общая часть


Задача этой статьи: добавить возможность использовать существующий монолитный проект как root контейнер для микросервисной архитектуры.

Существующий проект выполнен на angular 9.

Для микросервисной архитектуры используем библиотеку single-spa.

В root проект необходимо добавить 3 проекта, используем разные технологии: vue-app, angular-app, react-app (см. п. 4, 5, 6).

Параллельно с созданием этой статьи, я стараюсь внедрить эту архитектуру в боевой проект, над которым в данный момент работаю. Следовательно буду стараться описывать все ошибки, которые у меня возникнут в процессе разработки и их решения.

Root application (далее root) корень (контейнер) нашего приложения. В него мы будем класть (регистрировать) все наши микросервисы. Если вы уже имеете какой либо проект и хотите реализовать в нем эту архитектуру, то root application будет именно ваш существующий проект, откуда со временем вы будете стараться выгрызать куски вашего приложения, создавать отдельные микросервисы и регистрировать его в этом контейнере.

Такой подход создания root контейнера даст отличную возможность перехода на другой технологию без особой боли.

К примеру мы решили переехать с angular на vue полностью, но проект жирный, и в данный момент приносит много денег бизнесу.

Без микросервисной архитектуры, в мыслях бы этого не могло появиться, только у отчаянных людей, которые верят в единорогов и что мы все голограмма.
Для того, чтобы перейти на новую технологию в реальности необходимо переписать весь проект, и только тогда мы смогли бы кайфануть от его появления на бою.

Другой вариант, это микросервисная архитектура. Вы можете создать root проект из своего монолита, добавить туда новый проект на том же vue, настроить роуминг в root, готово. Можно лить в бой, постепенно выпиливать с root проекта небольшие кусочки и переносить их в ваш vue микро-проект. В результате в вашем root контейнере останется только те файлы, которые необходимы для импорта вашего нового проекта.

Это можно сделать прям здесь и сейчас, без потерь, крови и главное реально.
В качестве root я буду использовать angular, так как существующий проект был написан именно на нем.

Общий интерфейс, в который будет заворачиваться single page application:

bootstrap(mounter, bus) вызывается после загрузки сервиса, скажет в какой элемент дома нужно монтироваться, даст ему шину сообщений на которую микросервис у себя подпишется и сможет слушай и посылать запросы и команду

mount() монтировать приложение из дома

unmount() демонтаж приложения

unload() выгрузка приложения

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

2. Зачем это нужно


Начнем в этом пункте строго по порядку.

Существует 2 типа архитектуры:

  1. Монолит
  2. Микросервисная архитектура

image

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

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

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

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

В придачу ко всему этому срабатывает закон Иглсона, который говорит, чтоВаш код, который вы не просматривали 6 или более месяцев, выглядит так, будто его написал кто-то другой.

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

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

Всегда ли так происходит с монолитом?

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

В первую очередь нам необходимо обратить внимание на параметры нашего проекта.

Попробую вынести пункты, по которым можно понять, так ли нам необходима микросервисная архитектура:

  • Над проектом работает 2 и более команды, количество фронтенд разработчиков 10+;
  • Ваш проект состоит из 2 и более бизнес модели, например у вас интернет магазин с огромным количеством товаров, фильтров, нотификации, и функционал курьерского распределения доставок (2 отдельные не маленькие бизнес модели, которые будут друг другу мешать). Это все может жить отдельно и не зависеть друг от друга.
  • Набор возможностей UI растёт ежедневно или еженедельно, не оказывая влияния на остальную часть системы.

Микрофронтенды применяются для того, чтобы:

  • Отдельные части фронтенда могли разрабатываться, тестироваться и развёртыватьсянезависимо;
  • Отдельные части фронтенда могли быть добавлены, удалены или заменены без повторной сборки;
  • Разные части фронтенда могли быть созданы с помощьюразныхтехнологий.
  • В идеале, связи между этими микро-приложениями не должно быть от слова совсем, поэтому этот пункт должен тоже влиять на принятии решения о выпиливании бизнес-кейсов (или кейсы для расширенного пользовательского опыта) в отдельный микро-сервис.
  • Скорость разработки должна быть постоянной, несмотря на рост приложения
  • Разные команды должны иметь возможность использовать собственные инструменты.

Какие бонусы мы еще можем получить от single-spa библиотеки?

  • Вы можете управлять большими общими зависимостями (например, библиотеками React, Vue или Angular) проще с помощью карты импорта, как вы увидите позже в этой должности.
  • Single-spa имеет ленивую загрузку включен для модулей в браузере, так что ваше приложение будет загружать модули только тогда, когда это необходимо.
  • Разделение переднего конца на несколько модулей в браузере позволяет разрабатывать и развертывать приложение независимо друг от друга.

Микросервис в моем понимании самостоятельный single page application, который будет решать только одну задачу пользователя. Это приложение так же не должно решать задачу команды целиком.

SystemJS это библиотека JS с открытым исходным кодом, которая обычно используется в качестве полифилла для браузеров.

Полифилл является частью кода JS, используемого для обеспечения современной функциональности для старых браузеров, которые не поддерживают его.

Одной из особенностей SystemJSявляется карта импорта, которая позволяет импортировать модуль по сети и сопоставить его с переменным именем.

Например, можно использовать карту импорта для библиотеки React, которая загружается через CDN:

НО!

Если вы создаете проект с нуля, даже с учетом того, что вы определили все параметры вашего проекта, решили что у вас будет огромный Мега супер проект с командой 30+ человек, постойте!

Мне очень нравится мысль небезызвестного основоположника идеи микросервисов Мартин Фаулер.

Он предложил объединить монолитный подход и микросервисы в один (MonolithFirst). Основная его идея звучит так:
не следует начинать новый проект с микросервисов даже при полной уверенности, что будущее приложение будет достаточно большим, чтобы оправдать такой подход

Так же здесь опишу минусы использования такой архитектуры:

  • Взаимодействие между фрагментами невозможно обеспечить стандартными ламповыми методами (DI, например).
  • Как быть с общими зависимостями? Ведь размер приложения будет расти как на дрожжах, если их не выносить из фрагментов.
  • За роутинг в конечном приложении все равно должен отвечать кто-то один.
  • Неясно, что делать с тем, что разные микросервисы могут находиться на разных доменах
  • Что делать, если один из фрагментов недоступен / не может отрисоваться.

3. Создание root контейнера


И так, хватит теории, пора начинать.

Заходим в консоль

ng add single-spa-angularnpm i systemjs@6.1.4,npm i -d @types/systemjs@6.1.0,npm import-map-overrides@1.8.0

В ts.config.app.json глобально импортируем декларации (типы)

//ts.config.app.json"compilerOptions": {"outDir": "./out-tsc/app","types": [(+)"systemjs"]},


Добавляем вapp-routing.module.ts все микроприложения, которые мы добавим в root

//app-routing.module.ts{path: 'vue-app',children: [{path: '**',loadChildren: ( ) => import('./spa-host/spa-host.module').then(m => m.SpaHostModule),data: { app: '@somename/vue-app' }}]},{path: 'angular-app',children: [{path: '**',loadChildren: ( ) => import('./spa-host/spa-host.module').then(m => m.SpaHostModule),data: { app: '@somename/angular-app' }}]},{path: 'react-app',children: [{path: '**',loadChildren: ( ) => import('./spa-host/spa-host.module').then(m => m.SpaHostModule),data: { app: '@somename/react-app' }}]},

Так же нужно добавить config

// extra-webpack.config.jsonmodule.exports = (angularWebpackConfig, options) => {return {...angularWebpackConfig,module: {...angularWebpackConfig.module,rules: [...angularWebpackConfig.module.rules,{parser: {system: false}}]}};}

Изменим файл package.json, добавим в него все необходимые для работы либы

// package.json"dependencies": {...,(+) "single-spa": "^5.4.2",(+) "single-spa-angular": "^4.2.0",(+) "import-map-overrides": "^1.8.0",(+) "systemjs": "^6.1.4",}"devDependencies": {...,(+)"@angular-builders/custom-webpack": "^9",(+)"@types/systemjs": "^6.1.0",}

Добавляем необходимые библиотеки в angular.json

// angular.json{  ...,"architect": {"build": {...,"scripts": [...,(+)"node_modules/systemjs/dist/system.min.js",(+)"node_modules/systemjs/dist/extras/amd.min.js",(+)"node_modules/systemjs/dist/extras/named-exports.min.js",(+)"node_modules/systemjs/dist/extras/named-register.min.js",(+)"node_modules/import-map-overrides/dist/import-map-overrides.js"]}}},

В корне проекта создаем папку single-spa. В него добавим 2 файла.

1. route-reuse-strategy.ts файл маршрутизации наших микросервисов.
Если дочернее приложение выполняет маршрутизацию внутри себя, это приложение интерпретирует это как изменение маршрута.

По умолчанию это приведет к уничтожению текущего компонента и замене его новым экземпляром того же компонента spa-host.

Эта стратегия повторного использования маршрута смотрит на routeData.app, чтобы определить, должен ли новый маршрут быть обрабатывается как тот же маршрут, что и предыдущий, гарантируя, что мы не перемонтируем дочернее приложение, когда указанное дочернее приложение маршруты внутри себя.

// route-reuse-strategy.tsimport { RouteReuseStrategy, ActivatedRouteSnapshot, DetachedRouteHandle } from '@angular/router';import { Injectable } from '@angular/core';@Injectable()export class MicroFrontendRouteReuseStrategy extends RouteReuseStrategy {shouldDetach(): boolean {// маршрут не сохраняетсяreturn false;}store(): void { }shouldAttach(): boolean {return false;}// время присоединения маршрутаretrieve(): DetachedRouteHandle {return null;}shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {return future.routeConfig === curr.routeConfig || (future.data.app && (future.data.app === curr.data.app));}}

2. Сервис single-spa.service.ts

В сервисе будет храниться метод монтирования (mount) и демонтирования (unmount) микро-фронтенд приложений.

mount функция жизненного цикла, которая будет вызываться всякий раз, когда зарегистрированное приложение не смонтировано, но его функция активности возвращает true. При вызове эта функция должна просмотреть URL-адрес, чтобы определить активный маршрут, а затем создать элементы DOM, события DOM и т.п.

unmount функция жизненного цикла, которая будет вызываться всякий раз, когда монтируется зарегистрированное приложение, но ее функция активности возвращает false. При вызове эта функция должна очищать все элементы DOM.

//single-spa.service.tsimport { Injectable } from '@angular/core';import { mountRootParcel, Parcel, ParcelConfig } from 'single-spa';import { Observable, from, of } from 'rxjs';import { catchError, tap } from 'rxjs/operators';@Injectable({providedIn: 'root',})export class SingleSpaService {private loadedParcels: {[appName: string]: Parcel;} = {};mount(appName: string, domElement: HTMLElement): Observable<unknown> {return from(System.import<ParcelConfig>(appName)).pipe(tap((app: ParcelConfig) => {this.loadedParcels[appName] = mountRootParcel(app, {domElement});}));}unmount(appName: string): Observable<unknown> {return from(this.loadedParcels[appName].unmount()).pipe(tap(( ) => delete this.loadedParcels[appName]));}}

Далее создаем директорию container/app/spa-host.

Этот модуль будет реализовывать регистрации и отображение наших микро-фронтенд приложений в root.

Добавим в модуль 3 файла.

1. Сам модуль spa-host.module.ts

//spa-host.module.tsimport { RouterModule, Routes } from '@angular/router';import { CommonModule } from '@angular/common';import { NgModule } from '@angular/core';import { SpaUnmountGuard } from './spa-unmount.guard';import { SpaHostComponent } from './spa-host.component';const routes: Routes = [{path: '',canDeactivate: [SpaUnmountGuard],component: SpaHostComponent,},];@NgModule({declarations: [SpaHostComponent],imports: [CommonModule, RouterModule.forChild(routes)]})export class SpaHostModule {}

2. Компонент spa-host.component.ts координирует монтаж и демонтаж микро-фронтенд приложений

// spa-host.component.ts import { Component, OnInit, ViewChild, ElementRef, OnDestroy, ChangeDetectionStrategy } from '@angular/core';import { ActivatedRoute } from '@angular/router';import { Observable } from 'rxjs';import {SingleSpaService} from '../../single-spa/single-spa.service';@Component({selector: 'app-spa-host',template: '<div #appContainer></div>',changeDetection: ChangeDetectionStrategy.OnPush})export class SpaHostComponent implements OnInit {@ViewChild('appContainer', { static: true })appContainerRef: ElementRef;appName: string;constructor(private singleSpaService: SingleSpaService, private route: ActivatedRoute) { }ngOnInit() {// тащим название подгружаемой картыthis.appName = this.route.snapshot.data.app;this.mount().subscribe();}// собираем наш подгруженный проект по выбранному роутуmount(): Observable<unknown> {return this.singleSpaService.mount(this.appName, this.appContainerRef.nativeElement);}// разбираемunmount(): Observable<unknown> {return this.singleSpaService.unmount(this.appName);}}

3. spa-unmount.guard.ts проверяет, если имяприложения вроуте другое,разбираемпредыдущий сервис, если тоже, просто переходим на него.

// spa-unmount.guard.tsimport { Injectable } from '@angular/core';import { CanDeactivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';import { Observable } from 'rxjs';import { map } from 'rxjs/operators';import { SpaHostComponent } from './spa-host.component';@Injectable({ providedIn: 'root' })export class SpaUnmountGuard implements CanDeactivate<SpaHostComponent> {canDeactivate(component: SpaHostComponent,currentRoute: ActivatedRouteSnapshot,currentState: RouterStateSnapshot,nextState: RouterStateSnapshot): boolean | Observable<boolean> {const currentApp = component.appName;const nextApp = this.extractAppDataFromRouteTree(nextState.root);if (currentApp === nextApp) {return true;}return component.unmount().pipe(map(_ => true));}private extractAppDataFromRouteTree(routeFragment: ActivatedRouteSnapshot): string {if (routeFragment.data && routeFragment.data.app) {return routeFragment.data.app;}if (!routeFragment.children.length) {return null;}return routeFragment.children.map(r => this.extractAppDataFromRouteTree(r)).find(r => r !== null);}}

Регистрируем все что добавили в в app.module

//app.module.tsproviders: [...,{(+)provide: RouteReuseStrategy,(+)useClass: MicroFrontendRouteReuseStrategy}]

Изменим main.js.

// main.tsimport { enableProdMode, NgZone } from '@angular/core';import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';import { start as singleSpaStart } from 'single-spa';import { getSingleSpaExtraProviders } from 'single-spa-angular';import { AppModule } from './app/app.module';import { PlatformLocation } from '@angular/common';if (environment.production) {enableProdMode();}singleSpaStart();// название проектаconst appId = 'container-app';// Так как наше приложение использует маршрутизацию, мне необходимо импортировать функцию getSingleSpaExtraProviders.platformBrowserDynamic(getSingleSpaExtraProviders()).bootstrapModule(AppModule).then(module => {NgZone.isInAngularZone = () => {// @ts-ignorereturn window.Zone.current._properties[appId] === true;};const rootPlatformLocation = module.injector.get(PlatformLocation) as any;const rootZone = module.injector.get(NgZone);// tslint:disable-next-line:no-string-literalrootZone['_inner']._properties[appId] = true;rootPlatformLocation.setNgZone(rootZone);}).catch(err => {});

Далее создаем файл import-map.json в папке share. Файл нужен для добавления карт импорта.
В данный момент он будет у нас пустой и наполняться по мере добавления в root приложений.

<head><!doctype html><html lang="en"><head><meta charset="utf-8"><title>My first microfrontend root project</title><base href="http://personeltest.ru/aways/habr.com/">...(+)<meta name="importmap-type" content="systemjs-importmap" /><script type="systemjs-importmap" src="http://personeltest.ru/aways/habr.com/assets/import-map.json"></script></head><body><app-root></app-root><import-map-overrides-full></import-map-overrides-full><noscript>Please enable JavaScript to continue using this application.</noscript></body></html>

4. Создаем микро-приложение VUE (vue-app)


Теперь, когда мы добавили в свой монолитный проект возможность стать root приложением, пора создать свое первое внешнее микро-приложение с single-spa.

Во-первых, нам нужно установить глобально create-single-spa, интерфейс командной строки, который поможет нам создавать новые проекты single-spa с помощью простых команд.

Заходим в консоль

npm install --global create-single-spa

Создаем простое приложение vue с помощью команды в консоле

create-single-spa

Интерфей командной строки предложит выбрать директорию, название проекта, организации и тип приложения для создания

image

? Directory for new project vue-app ? Select type to generate single-spa application / parcel ? Which framework do you want to use? vue ? Which package manager do you want to use? npm ? Organization name (use lowercase and dashes) somename 

Запускаем наше микро-приложение

npm i npmrun serve--port 8000

Когда мы введем путь в браузере localhost:8080/, в случае с vue мы увидим пустой экран. Что же произошло?
Так как в созданном микро-приложении нет файла index.js.

Single-spa предоставляет игровую площадку, с которой можно загружать приложение через интернет, поэтому давайте сначала воспользуемся ей.

Добавим в index.js
single-spa-playground.org/playground/instant-test?name=@some-name/vue-app&url=8000
При создании root приложения, мы заранее добавили карту для загрузки нашего vue проекта.

{"imports": { ... , "vue": "https://unpkg.com/vue",  "vue-router": "https://cdn.jsdelivr.net/npm/vue-router@3.0.7/dist/vue-router.min.js", "@somename/vue-app": "//localhost:8080/js/app.js"}}

Готова! Теперь с нашего angular root проекта мы можем загружать микро-приложения, написанное на vue.

5. Создаем микро-приложение REACT (react-app)


Создаем так же простое приложение react с помощью команды в консоле

create-single-spa

Название организации:somename

Название проекта:react-app

? Directory for new project react-app ? Select type to generate single-spa application / parcel ? Which framework do you want to use? react ? Which package manager do you want to use? npm ? Organization name (use lowercase and dashes) somename 

Проверим, добавили ли мы карту импорта в нашем root приложении

{"imports": { ... ,    "react": "https://cdn.jsdelivr.net/npm/react@16.13.1/umd/react.development.js",    "react-dom": "https://cdn.jsdelivr.net/npm/react-dom@16.13.1/umd/react-dom.development.js",    "@somename/react-app": "//localhost:8081/somename-projname.js",}}

Готово! Теперь по нашем роуту react-app у нас загружается react микро-проект.

6. Создаем микро-приложение ANGULAR (angular-app)


Angular микро-приложение создаем абсолютно так же, как и 2 предыдущих

create-single-spa

Название организации:somename

Название проекта:angular-app

? Directory for new project angular-app ? Select type to generate single-spa application / parcel ? Which framework do you want to use? angular ? Which package manager do you want to use? npm ? Organization name (use lowercase and dashes) somename 

Проверим, добавили ли мы карту импорта в нашем root приложении

{    "imports": {     ... ,    "@somename/angular-app": "//localhost:8082/main.js",     }}

Запускаем, проверяем, все должно работать.

Это мой первый пост на Хабре, буду очень благодарен за ваши комментарии.
Подробнее..

Перевод GraphQL на Rust

09.03.2021 18:08:33 | Автор: admin

В этой статье я покажу как создать GraphQL сервер, используя Rust и его экосистему; будут приведены примеры реализации наиболее часто встречающихся задач при разработке GraphQL API. В итоге API трёх микросервисов будут объединены в единую точку доступа с помощью Apollo Server и Apollo Federation. Это позволит клиентам запрашивать данные одновременно из нескольких источников без необходимости знать какие данные приходят из какого сервиса.

Введение

Обзор

С точки зрения функциональности описываемый проект довольно похож на представленный в моей предыдущей статье, но в этот раз с использованием стэка Rust. Архитектурно проекта выглядит так:

Каждый компонент архитектуры освещает несколько вопросов, которые могут возникнуть при реализации GraphQL API. Доменная модель включает данные о планетах Солнечной системы и их спутниках. Проект имеет многомодульную структуру (или монорепозиторий) и состоит из следующих модулей:

Существуют две основных библиотеки для разработки GraphQL сервера на Rust: Juniper и Async-graphql, но только последняя поддерживает Apollo Federation, поэтому она была выбрана для реализации проекта (есть также открытый запрос на реализацию поддержки Federation в Juniper). Обе библиотеки предлагают code-first подход.

Помимо этого использованы PostgreSQL для реализации слоя данных, JWT для аутентификации и Kafka для асинхронного обмена сообщениями.

Стэк технологий

В следующей таблице показан стэк основных технологий, использованных в проекте:

Тип

Название

Сайт

GitHub

Язык программирования

Rust

link

link

GraphQL библиотека

Async-graphql

link

link

Единая GraphQL точка доступа

Apollo Server

link

link

Web фреймворк

actix-web

link

link

База даных

PostgreSQL

link

link

Брокер сообщений

Apache Kafka

link

link

Оркестрация контейнеров

Docker Compose

link

link

Также некоторые использованные Rust библиотеки:

Тип

Название

Сайт

GitHub

ORM

Diesel

link

link

Kafka клиент

rust-rdkafka

link

link

Хэширование паролей

argonautica

link

link

JWT библиотека

jsonwebtoken

link

link

Бибилиотека для тестирования

Testcontainers-rs

link

link

Необходимое ПО

Чтобы запустить проект локально, вам нужен только Docker Compose. В противном случае вам может понадобиться следующее:

  • Rust

  • Diesel CLI (для установки выполните cargo install diesel_cli --no-default-features --features postgres)

  • LLVM (это нужно для работы крэйта argonautica)

  • CMake (это нужно для работы крэйта rust-rdkafka)

  • PostgreSQL

  • Apache Kafka

  • npm

Реализация

В Cargo.toml в корне проекта указаны три приложения и одна библиотека:

Root Cargo.toml

[workspace]members = [    "auth-service",    "planets-service",    "satellites-service",    "common-utils",]

Начнём с planets-service.

Зависимости

Cargo.toml выглядит так:

Cargo.toml

[package]name = "planets-service"version = "0.1.0"edition = "2018"[dependencies]common-utils = { path = "../common-utils" }async-graphql = "2.4.3"async-graphql-actix-web = "2.4.3"actix-web = "3.3.2"actix-rt = "1.1.1"actix-web-actors = "3.0.0"futures = "0.3.8"async-trait = "0.1.42"bigdecimal = { version = "0.1.2", features = ["serde"] }serde = { version = "1.0.118", features = ["derive"] }serde_json = "1.0.60"diesel = { version = "1.4.5", features = ["postgres", "r2d2", "numeric"] }diesel_migrations = "1.4.0"dotenv = "0.15.0"strum = "0.20.0"strum_macros = "0.20.1"rdkafka = { version = "0.24.0", features = ["cmake-build"] }async-stream = "0.3.0"lazy_static = "1.4.0"[dev-dependencies]jsonpath_lib = "0.2.6"testcontainers = "0.9.1"

async-graphql это GraphQL библиотека, actix-web web фреймворк, а async-graphql-actix-web обеспечивает интеграцию между ними.

Ключевые функции

Начнём с main.rs:

main.rs

#[actix_rt::main]async fn main() -> std::io::Result<()> {    dotenv().ok();    let pool = create_connection_pool();    run_migrations(&pool);    let schema = create_schema_with_context(pool);    HttpServer::new(move || App::new()        .configure(configure_service)        .data(schema.clone())    )        .bind("0.0.0.0:8001")?        .run()        .await}

Здесь окружение и HTTP сервер конфигурируются с помощью функций, определённых в lib.rs:

lib.rs

pub fn configure_service(cfg: &mut web::ServiceConfig) {    cfg        .service(web::resource("/")            .route(web::post().to(index))            .route(web::get().guard(guard::Header("upgrade", "websocket")).to(index_ws))            .route(web::get().to(index_playground))        );}async fn index(schema: web::Data, http_req: HttpRequest, req: Request) -> Response {    let mut query = req.into_inner();    let maybe_role = common_utils::get_role(http_req);    if let Some(role) = maybe_role {        query = query.data(role);    }    schema.execute(query).await.into()}async fn index_ws(schema: web::Data, req: HttpRequest, payload: web::Payload) -> Result {    WSSubscription::start(Schema::clone(&*schema), &req, payload)}async fn index_playground() -> HttpResponse {    HttpResponse::Ok()        .content_type("text/html; charset=utf-8")        .body(playground_source(GraphQLPlaygroundConfig::new("/").subscription_endpoint("/")))}pub fn create_schema_with_context(pool: PgPool) -> Schema {    let arc_pool = Arc::new(pool);    let cloned_pool = Arc::clone(&arc_pool);    let details_batch_loader = Loader::new(DetailsBatchLoader {        pool: cloned_pool    }).with_max_batch_size(10);    let kafka_consumer_counter = Mutex::new(0);    Schema::build(Query, Mutation, Subscription)        .data(arc_pool)        .data(details_batch_loader)        .data(kafka::create_producer())        .data(kafka_consumer_counter)        .finish()}

Эти функции делают следующее:

  • index обрабатывает GraphQL запросы (query) и мутации

  • index_ws обрабатывает GraphQL подписки

  • index_playground предоставляет Playground GraphQL IDE

  • create_schema_with_context создаёт GraphQL схему с глобальным контекстом доступным в рантайме, например, пул соединений с БД

Определение GraphQL запроса и типа

Рассмотрим как определить запрос:

Определение запроса

#[Object]impl Query {    async fn get_planets(&self, ctx: &Context<'_>) -> Vec {        repository::get_all(&get_conn_from_ctx(ctx)).expect("Can't get planets")            .iter()            .map(|p| { Planet::from(p) })            .collect()    }    async fn get_planet(&self, ctx: &Context<'_>, id: ID) -> Option {        find_planet_by_id_internal(ctx, id)    }    #[graphql(entity)]    async fn find_planet_by_id(&self, ctx: &Context<'_>, id: ID) -> Option {        find_planet_by_id_internal(ctx, id)    }}fn find_planet_by_id_internal(ctx: &Context<'_>, id: ID) -> Option {    let id = id.to_string().parse::().expect("Can't get id from String");    repository::get(id, &get_conn_from_ctx(ctx)).ok()        .map(|p| { Planet::from(&p) })}

Эти запросы получают данные из БД используя слой репозитория. Полученные сущности конвертируются в GraphQL DTO (это позволяет соблюсти принцип единственной ответственности для каждой структуры). Запросы get_planets и get_planet могут быть выполнены из любой GraphQL IDE например так:

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

{  getPlanets {    name    type  }}

Структура Planet определена так:

Определение GraphQL типа

#[derive(Serialize, Deserialize)]struct Planet {    id: ID,    name: String,    planet_type: PlanetType,}#[Object]impl Planet {    async fn id(&self) -> &ID {        &self.id    }    async fn name(&self) -> &String {        &self.name    }    /// From an astronomical point of view    #[graphql(name = "type")]    async fn planet_type(&self) -> &PlanetType {        &self.planet_type    }    #[graphql(deprecation = "Now it is not in doubt. Do not use this field")]    async fn is_rotating_around_sun(&self) -> bool {        true    }    async fn details(&self, ctx: &Context<'_>) -> Details {        let loader = ctx.data::>().expect("Can't get loader");        let planet_id = self.id.to_string().parse::().expect("Can't convert id");        loader.load(planet_id).await    }}

В impl определяется резолвер для каждого поля. Также для некоторых полей определены описание (в виде Rust комментария) и deprecation reason. Это будет отображено в GraphQL IDE.

Проблема N+1

В случае наивной реализации функции Planet.details выше возникла бы проблема N+1, то есть, при выполнении такого запроса:

Пример возможного ресурсоёмкого GraphQL запроса

{  getPlanets {    name    details {      meanRadius    }  }}

для поля details каждой из планет был бы сделан отдельный SQL запрос, т. к. Details отдельная от Planet сущность и хранится в собственной таблице.

Но с помощью DataLoader, реализованного в Async-graphql, резолвер details может быть определён так:

Определение резолвера

async fn details(&self, ctx: &Context<'_>) -> Result {    let data_loader = ctx.data::>().expect("Can't get data loader");    let planet_id = self.id.to_string().parse::().expect("Can't convert id");    let details = data_loader.load_one(planet_id).await?;    details.ok_or_else(|| "Not found".into())}

data_loader это объект в контектсе приложения, определённый так:

Определение DataLoader'а

let details_data_loader = DataLoader::new(DetailsLoader {    pool: cloned_pool}).max_batch_size(10);

DetailsLoader реализован следующим образом:

DetailsLoader definition

pub struct DetailsLoader {    pub pool: Arc}#[async_trait::async_trait]impl Loader for DetailsLoader {    type Value = Details;    type Error = Error;    async fn load(&self, keys: &[i32]) -> Result, Self::Error> {        let conn = self.pool.get().expect("Can't get DB connection");        let details = repository::get_details(keys, &conn).expect("Can't get planets' details");        Ok(details.iter()            .map(|details_entity| (details_entity.planet_id, Details::from(details_entity)))            .collect::>())    }}

Такой подход позволяет предотвратить проблему N+1, т. к. каждый вызов DetailsLoader.load выполняет только один SQL запрос, возвращающий пачку DetailsEntity.

Определение интерфейса

GraphQL интерфейс и его реализации могут быть определены следующим образом:

Определение GraphQL интерфейса

#[derive(Interface, Clone)]#[graphql(    field(name = "mean_radius", type = "&CustomBigDecimal"),    field(name = "mass", type = "&CustomBigInt"),)]pub enum Details {    InhabitedPlanetDetails(InhabitedPlanetDetails),    UninhabitedPlanetDetails(UninhabitedPlanetDetails),}#[derive(SimpleObject, Clone)]pub struct InhabitedPlanetDetails {    mean_radius: CustomBigDecimal,    mass: CustomBigInt,    /// In billions    population: CustomBigDecimal,}#[derive(SimpleObject, Clone)]pub struct UninhabitedPlanetDetails {    mean_radius: CustomBigDecimal,    mass: CustomBigInt,}

Здесь вы также можете видеть, что если в структуре нет ни одного поля со "сложным" резолвером, то она может быть реализована с использованием атрибута SimpleObject.

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

Кастомные скаляры позволяют определить как представлять и как парсить значения определённого типа. Проект содержит два примера определения кастомных скаляров; оба являются обёртками для числовых структур (т. к. невозможно определить внешний трейт на внешней структуре из-за orphan rule). Эти обёртки определены так:

Кастомный скаляр: обёртка для BigInt

#[derive(Clone)]pub struct CustomBigInt(BigDecimal);#[Scalar(name = "BigInt")]impl ScalarType for CustomBigInt {    fn parse(value: Value) -> InputValueResult {        match value {            Value::String(s) => {                let parsed_value = BigDecimal::from_str(&s)?;                Ok(CustomBigInt(parsed_value))            }            _ => Err(InputValueError::expected_type(value)),        }    }    fn to_value(&self) -> Value {        Value::String(format!("{:e}", &self))    }}impl LowerExp for CustomBigInt {    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {        let val = &self.0.to_f64().expect("Can't convert BigDecimal");        LowerExp::fmt(val, f)    }}

Кастомный скаляр: обёртка для BigDecimal

#[derive(Clone)]pub struct CustomBigDecimal(BigDecimal);#[Scalar(name = "BigDecimal")]impl ScalarType for CustomBigDecimal {    fn parse(value: Value) -> InputValueResult {        match value {            Value::String(s) => {                let parsed_value = BigDecimal::from_str(&s)?;                Ok(CustomBigDecimal(parsed_value))            }            _ => Err(InputValueError::expected_type(value)),        }    }    fn to_value(&self) -> Value {        Value::String(self.0.to_string())    }}

В первом примере также показано, как представить гигантское число в виде экспоненциальной записи.

Определение мутации

Мутация может быть определена следующим образом:

Определение мутации

pub struct Mutation;#[Object]impl Mutation {    #[graphql(guard(RoleGuard(role = "Role::Admin")))]    async fn create_planet(&self, ctx: &Context<'_>, planet: PlanetInput) -> Result {        let new_planet = NewPlanetEntity {            name: planet.name,            planet_type: planet.planet_type.to_string(),        };        let details = planet.details;        let new_planet_details = NewDetailsEntity {            mean_radius: details.mean_radius.0,            mass: BigDecimal::from_str(&details.mass.0.to_string()).expect("Can't get BigDecimal from string"),            population: details.population.map(|wrapper| { wrapper.0 }),            planet_id: 0,        };        let created_planet_entity = repository::create(new_planet, new_planet_details, &get_conn_from_ctx(ctx))?;        let producer = ctx.data::().expect("Can't get Kafka producer");        let message = serde_json::to_string(&Planet::from(&created_planet_entity)).expect("Can't serialize a planet");        kafka::send_message(producer, message).await;        Ok(Planet::from(&created_planet_entity))    }}

Чтобы использовать объект как входной параметр мутации, надо определить структуру следующим образом:

Определение input type

#[derive(InputObject)]struct PlanetInput {    name: String,    #[graphql(name = "type")]    planet_type: PlanetType,    details: DetailsInput,}

Мутация защищена RoleGuard'ом, который гарантирует что только пользователи с ролбю Admin могут выполнить её. Таким образом, для выполнения, например, следующей мутации:

Пример использования мутации

mutation {  createPlanet(    planet: {      name: "test_planet"      type: TERRESTRIAL_PLANET      details: { meanRadius: "10.5", mass: "8.8e24", population: "0.5" }    }  ) {    id  }}

вам нужно указать заголовок Authorization с JWT, полученным из auth-service (это будет описано далее).

Определение подписки

В определении мутации выше вы могли видеть что при добавлении новой планеты отправляется сообщение:

Отправка сообщения в Kafka

let producer = ctx.data::().expect("Can't get Kafka producer");let message = serde_json::to_string(&Planet::from(&created_planet_entity)).expect("Can't serialize a planet");kafka::send_message(producer, message).await;

Клиент API может быть уведомлен об этом событии с помощью подписки, слушающей Kafka consumer:

Определение подписки

pub struct Subscription;#[Subscription]impl Subscription {    async fn latest_planet<'ctx>(&self, ctx: &'ctx Context<'_>) -> impl Stream + 'ctx {        let kafka_consumer_counter = ctx.data::>().expect("Can't get Kafka consumer counter");        let consumer_group_id = kafka::get_kafka_consumer_group_id(kafka_consumer_counter);        let consumer = kafka::create_consumer(consumer_group_id);        async_stream::stream! {            let mut stream = consumer.start();            while let Some(value) = stream.next().await {                yield match value {                    Ok(message) => {                        let payload = message.payload().expect("Kafka message should contain payload");                        let message = String::from_utf8_lossy(payload).to_string();                        serde_json::from_str(&message).expect("Can't deserialize a planet")                    }                    Err(e) => panic!("Error while Kafka message processing: {}", e)                };            }        }    }}

Подписка может быть использована так же, как запросы и мутации:

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

subscription {  latestPlanet {    id    name    type    details {      meanRadius    }  }}

Подписки должны отправляться на ws://localhost:8001.

Интеграционные тесты

Тесты запросов и мутаций можно написать так:

Тест запроса

#[actix_rt::test]async fn test_get_planets() {    let docker = Cli::default();    let (_pg_container, pool) = common::setup(&docker);    let mut service = test::init_service(App::new()        .configure(configure_service)        .data(create_schema_with_context(pool))    ).await;    let query = "        {            getPlanets {                id                name                type                details {                    meanRadius                    mass                    ... on InhabitedPlanetDetails {                        population                    }                }            }        }        ".to_string();    let request_body = GraphQLCustomRequest {        query,        variables: Map::new(),    };    let request = test::TestRequest::post().uri("/").set_json(&request_body).to_request();    let response: GraphQLCustomResponse = test::read_response_json(&mut service, request).await;    fn get_planet_as_json(all_planets: &serde_json::Value, index: i32) -> &serde_json::Value {        jsonpath::select(all_planets, &format!("$.getPlanets[{}]", index)).expect("Can't get planet by JSON path")[0]    }    let mercury_json = get_planet_as_json(&response.data, 0);    common::check_planet(mercury_json, 1, "Mercury", "TERRESTRIAL_PLANET", "2439.7");    let earth_json = get_planet_as_json(&response.data, 2);    common::check_planet(earth_json, 3, "Earth", "TERRESTRIAL_PLANET", "6371.0");    let neptune_json = get_planet_as_json(&response.data, 7);    common::check_planet(neptune_json, 8, "Neptune", "ICE_GIANT", "24622.0");}

Если часть запроса может быть переиспользована в другом запросе, вы можете использовать фрагменты:

Тест запроса с использованием фрагмента

const PLANET_FRAGMENT: &str = "    fragment planetFragment on Planet {        id        name        type        details {            meanRadius            mass            ... on InhabitedPlanetDetails {                population            }        }    }";#[actix_rt::test]async fn test_get_planet_by_id() {    ...    let query = "        {            getPlanet(id: 3) {                ... planetFragment            }        }        ".to_string() + PLANET_FRAGMENT;    let request_body = GraphQLCustomRequest {        query,        variables: Map::new(),    };    ...}

Чтобы использовать переменные, запишите тест так:

Тест запроса с использованием фрагмента и переменной

#[actix_rt::test]async fn test_get_planet_by_id_with_variable() {    ...    let query = "        query testPlanetById($planetId: String!) {            getPlanet(id: $planetId) {                ... planetFragment            }        }".to_string() + PLANET_FRAGMENT;    let jupiter_id = 5;    let mut variables = Map::new();    variables.insert("planetId".to_string(), jupiter_id.into());    let request_body = GraphQLCustomRequest {        query,        variables,    };    ...}

В этом проекте используется библиотека Testcontainers-rs, что позволяет подготовить тестовое окружение, то есть, создать временную БД PostgreSQL.

Клиент к GraphQL API

Вы можете использовать код из предыдущего раздела для создания клиента к внешнему GraphQL API. Также для этого существуют специальные библиотеки, например, graphql-client, но я их не использовал.

Безопасность API

Существуют различные угрозы безопасности GraphQL API (см. список); рассмотрим некоторые из них.

Ограничения глубины и сложности запроса

Если бы структура Satellite содержала поле planet, был бы возможен такой запрос:

Пример тяжёлого запроса

{  getPlanet(id: "1") {    satellites {      planet {        satellites {          planet {            satellites {              ... # more deep nesting!            }          }        }      }    }  }}

Сделать такой запрос невалидным можно так:

Пример ограничения глубины и сложности запроса

pub fn create_schema_with_context(pool: PgPool) -> Schema {    ...    Schema::build(Query, Mutation, Subscription)        .limit_depth(3)        .limit_complexity(15)    ...}

Стоит отметить, что при указании ограничений выше может перестать отображаться документация сервиса в GraphQL IDE. Это происходит потому, что IDE пытается выполнить introspection query, который имеет заметные глубину и сложность.

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

Эта функциональность реализована в auth-service с использованием крэйтов argonautica и jsonwebtoken. Первый отвечает за хэширование паролей пользователей с использованием алгоритма Argon2. Аутентификация и авторизация показаны исключительно в демонстрационных целях; пожалуйста, изучите вопрос более тщательно перед использованием в продакшене.

Рассмотрим как реализован вход в систему:

Реализация входа в систему

pub struct Mutation;#[Object]impl Mutation {    async fn sign_in(&self, ctx: &Context<'_>, input: SignInInput) -> Result {        let maybe_user = repository::get_user(&input.username, &get_conn_from_ctx(ctx)).ok();        if let Some(user) = maybe_user {            if let Ok(matching) = verify_password(&user.hash, &input.password) {                if matching {                    let role = AuthRole::from_str(user.role.as_str()).expect("Can't convert &str to AuthRole");                    return Ok(common_utils::create_token(user.username, role));                }            }        }        Err(Error::new("Can't authenticate a user"))    }}#[derive(InputObject)]struct SignInInput {    username: String,    password: String,}

Посмотреть реализацию функции verify_password можно в модуле utils, create_token в модуле common_utils. Как вы могли бы ожидать, функция sign_in возвращает JWT, который в дальнейшем может быть использован для авторизации в других сервисах.

Для получения JWT выполните следующую мутацию:

Получение JWT

mutation {  signIn(input: { username: "john_doe", password: "password" })}

Используйте параметры john_doe/password. Включение полученного JWT в последующие запросы позволит получить доступ к защищённым ресурсам (см. следующий раздел).

Авторизация

Чтобы запросить защищённые данные, добавьте заголовок в HTTP запрос в формате Authorization: Bearer $JWT. Функция index извлечёт роль пользователя из HTTP запроса и добавит её в параметры GraphQL запроса/мутации:

Получение роли

async fn index(schema: web::Data, http_req: HttpRequest, req: Request) -> Response {    let mut query = req.into_inner();    let maybe_role = common_utils::get_role(http_req);    if let Some(role) = maybe_role {        query = query.data(role);    }    schema.execute(query).await.into()}

К ранее показанной мутации create_planet применён следующий атрибут:

Использование гарда

#[graphql(guard(RoleGuard(role = "Role::Admin")))]

Сам гард реализован так:

Реализация гарда

struct RoleGuard {    role: Role,}#[async_trait::async_trait]impl Guard for RoleGuard {    async fn check(&self, ctx: &Context<'_>) -> Result<()> {        if ctx.data_opt::() == Some(&self.role) {            Ok(())        } else {            Err("Forbidden".into())        }    }}

Таким образом, если вы не укажете токен, сервер ответит сообщением "Forbidden".

Определение перечисления

GraphQL перечисление может быть определено так:

Определение перечисления

#[derive(SimpleObject)]struct Satellite {    ...    life_exists: LifeExists,}#[derive(Copy, Clone, Eq, PartialEq, Debug, Enum, EnumString)]#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]pub enum LifeExists {    Yes,    OpenQuestion,    NoData,}

Работа с датами

Async-graphql поддерживает типы даты/времени из библиотеки chrono, поэтому вы можете определить такие поля как обычно:

Определение поля с датой

#[derive(SimpleObject)]struct Satellite {    ...    first_spacecraft_landing_date: Option,}

Поддержка Apollo Federation

Одна из целей satellites-service продемонстрировать как распределённая GraphQL сущность (Planet) может резолвиться в двух (или более) сервисах и затем запрашиваться через Apollo Server.

Тип Planet был ранее определён в planets-service так:

Определение типа Planet в planets-service

#[derive(Serialize, Deserialize)]struct Planet {    id: ID,    name: String,    planet_type: PlanetType,}

Также в planets-service тип Planet является сущностью:

Определение сущности Planet

#[Object]impl Query {    #[graphql(entity)]    async fn find_planet_by_id(&self, ctx: &Context<'_>, id: ID) -> Option {        find_planet_by_id_internal(ctx, id)    }}

satellites-service расширяет сущность Planet путём добавления поля satellites:

Расширение типа Planet в satellites-service

struct Planet {    id: ID}#[Object(extends)]impl Planet {    #[graphql(external)]    async fn id(&self) -> &ID {        &self.id    }    async fn satellites(&self, ctx: &Context<'_>) -> Vec {        let id = self.id.to_string().parse::().expect("Can't get id from String");        repository::get_by_planet_id(id, &get_conn_from_ctx(ctx)).expect("Can't get satellites of planet")            .iter()            .map(|e| { Satellite::from(e) })            .collect()    }}

Также вам нужно реализовать функцию поиска для расширяемого типа. В примере ниже функция просто создаёт новый инстанс Planet:

Функция поиска для типа Planet

#[Object]impl Query {    #[graphql(entity)]    async fn get_planet_by_id(&self, id: ID) -> Planet {        Planet { id }    }}

Async-graphql генерирует два дополнительных запроса (_service and _entities), которые будут использованы Apollo Server'ом. Эти запросы внутренние, то есть они не будут отображены в API Apollo Server'а. Конечно, сервис с поддержкой Apollo Federation по-прежнему может работать автономно.

Apollo Server

Apollo Server и Apollo Federation позволяют достичь две основные цели:

  • создать единую точку доступа к нескольким GraphQL API

  • создать единый граф данных из распределённых сущностей

Таким образом, даже если вы не используете распределённые сущности, для frontend разработчиков удобнее использовать одну точку доступа, чем несколько.

Существует и другой способ создания единой GraphQL схемы, schema stitching, но пока что я его не использовал.

Модуль включает следующий исходный код:

Мета-информация и зависимости

{  "name": "api-gateway",  "main": "gateway.js",  "scripts": {    "start-gateway": "nodemon gateway.js"  },  "devDependencies": {    "concurrently": "5.3.0",    "nodemon": "2.0.6"  },  "dependencies": {    "@apollo/gateway": "0.21.3",    "apollo-server": "2.19.0",    "graphql": "15.4.0"  }}

Определение Apollo Server

const {ApolloServer} = require("apollo-server");const {ApolloGateway, RemoteGraphQLDataSource} = require("@apollo/gateway");class AuthenticatedDataSource extends RemoteGraphQLDataSource {    willSendRequest({request, context}) {        if (context.authHeaderValue) {            request.http.headers.set('Authorization', context.authHeaderValue);        }    }}let node_env = process.env.NODE_ENV;function get_service_url(service_name, port) {    let host;    switch (node_env) {        case 'docker':            host = service_name;            break;        case 'local': {            host = 'localhost';            break        }    }    return "http://" + host + ":" + port;}const gateway = new ApolloGateway({    serviceList: [        {name: "planets-service", url: get_service_url("planets-service", 8001)},        {name: "satellites-service", url: get_service_url("satellites-service", 8002)},        {name: "auth-service", url: get_service_url("auth-service", 8003)},    ],    buildService({name, url}) {        return new AuthenticatedDataSource({url});    },});const server = new ApolloServer({    gateway, subscriptions: false, context: ({req}) => ({        authHeaderValue: req.headers.authorization    })});server.listen({host: "0.0.0.0", port: 4000}).then(({url}) => {    console.log(`? Server ready at ${url}`);});

Если код выше может быть упрощён, не стесняйтесь поправить.

Авторизация в apollo-service работает так же, как было показано ранее для Rust сервисов (вам надо указать заголовок Authorization и его значение).

Приложение, написанное на любом языке или фреймворке, может быть добавлено в качестве нижележащего сервиса под Apollo Server, если оно реализует спецификацию Federation; список библиотек, добавляющих поддержку этой спецификации доступен в документации.

При реализации модуля я столкнулся со следующими ограничениями:

Взаимодействие с БД

Уровень хранения реализован с помощью PostgreSQL and Diesel. Если вы не используете Docker при локальном запуске, то нужно выполнить diesel setup, находясь в директории каждого из сервисов. Это создаст пустую БД, к которой далее будут применены миграции, создающие таблицы и инициализирующие данные.

Запуск проекта и тестирование API

Как было отмечено ранее, проект можно запустить двумя способами:

  • с использованием Docker Compose (docker-compose.yml)

    Здесь, в свою очередь, также возможны два варианта:

    • режим разработки (используя локально собранные образы)

      docker-compose up

    • production mode (используя релизные образы)

      docker-compose -f docker-compose.yml up

  • без Docker

    Запустите каждый Rust сервис с помощью cargo run, потом запустите Apollo Server:

    • cd в папку apollo-server

    • определите переменную среды NODE_ENV, например, set NODE_ENV=local (для Windows)

    • npm install

    • npm run start-gateway

Успешный запуск apollo-server должен выглядеть так:

Лог запуска Apollo Server

[nodemon] 2.0.6[nodemon] to restart at any time, enter `rs`[nodemon] watching path(s): *.*[nodemon] watching extensions: js,mjs,json[nodemon] starting `node gateway.js`Server ready at http://0.0.0.0:4000/

Вы можете перейти на http://localhost:4000 в браузере и использовать встроенную Playground IDE:

Здесь возможно выполнять запросы, мутации и подписки, определённые в нижележащих сервисах. Кроме того, каждый из этих сервисов имеет собственную Playground IDE.

Тест подписки

Чтобы убедиться в том, что подписка работает, откройте две вкладки любой GraphQL IDE; в первой подпишитесь таким образом:

Пример подписки

subscription {  latestPlanet {    name    type  }}

Во второй укажите заголовок Authorization как было описано ранее и выполните мутацию:

Пример мутации

mutation {  createPlanet(    planet: {      name: "Pluto"      type: DWARF_PLANET      details: { meanRadius: "1188", mass: "1.303e22" }    }  ) {    id  }}

Подписанный клиент будет уведомлен о событии:

CI/CD

CI/CD сконфигурирован с помощью GitHub Actions (workflow), который запускает тесты приложений, собирает их Docker образы и разворачивает их на Google Cloud Platform.

Вы можете посмотреть на описанные API здесь.

Замечание: На "продакшн" среде пароль отличается от указанного ранее, чтобы предотвратить изменение данных.

Заключение

В этой статье я рассмотрел как решать наиболее частые вопросы, которые могут возникнуть при разработке GraphQL API на Rust. Также было показано как объединить API Rust GraphQL микросервисов для получения единого GraphQL интерфейса; в подобной архитектуре сущность может быть распределена среди нескольких микросервисов. Это достигается за счёт использования Apollo Server, Apollo Federation и библиотеки Async-graphql. Исходный код рассмотренного проекта доступен на GitHub. Не стесняйтесь написать мне, если найдёте ошибки в статье или исходном коде. Благодарю за внимание!

Полезные ссылки

Подробнее..

Как готовить микрофронтенды в Webpack 5

27.04.2021 16:10:26 | Автор: admin

Всем привет, меня зовут Иван и я фронтенд-разработчик.

На моём комментарии про микрофронтенды набралось целых три лайка, поэтому я решил написать статью с описанием всех шишек, что наш стрим набил и набивает в результате внедрения микрофронтендов.

Начнём с того, что ребята с Хабра (@artemu78, @dfuse, @Katsuba) уже писали про Module Federation, так что, моя статья - это не что-то уникальное и прорывное. Скорее, это шишки, костыли и велосипеды, которые полезно знать тем, кто собирается использовать данную технологию.

Причина

Причина, по которой решено было внедрять микросервисный подход на фронте, довольно простая - много команд, а проект один, нужно было как-то разделить зоны ответственности и распараллелить разработку. Как раз в тот момент, мне на глаза попался доклад Павла Черторогова про Webpack 5 Module Federation. Честно, это перевернуло моё видение современных веб-приложений. Я очень вдохновился и начал изучать и крутить эту технологию, чтобы понять, можно ли применить это в нашем проекте. Оказалось, всё что нужно, это дописать несколько строк в конфиг Webpack, создать пару компонентов-хелперов, и... всё завелось.

Настройка

Итак, что же нужно сделать, чтобы запустить микрофронтенды на базе сборки Webpack 5?

Для начала, убедитесь, что используете Webpack пятой версии, потому что Module Federation там поддерживается из коробки.

Настройка shell-приложения

Так как, до внедрения микрофронтендов, у нас уже было действующее приложение, решено было использовать его в качестве точки входа и оболочки для подключения других микрофронтендов. Для сборки использовался Webpack версии 4.4 и при обновлении до 5 версии возникли небольшие проблемы с некоторыми плагинами. К счастью, это решилось простым поднятием версий плагинов.

Чтобы создать контейнер на базе сборки Webpack и при помощи этого контейнера иметь возможность импортировать ресурсы с удаленных хостов добавляем в Webpack-конфиг следующий код:

const webpack = require('webpack');// ...const { ModuleFederationPlugin } = webpack.container;const deps = require('./package.json').dependencies;module.exports = {  // ...  output: {    // ...    publicPath: 'auto', // ВАЖНО! Указывайте либо реальный publicPath, либо auto  },  module: {    // ...  },  plugins: [    // ...    new ModuleFederationPlugin({      name: 'shell',      filename: 'shell.js',      shared: {        react: { requiredVersion: deps.react },        'react-dom': { requiredVersion: deps['react-dom'] },        'react-query': {          requiredVersion: deps['react-query'],        },      },      remotes: {        widgets: `widgets@http://localhost:3002/widgets.js`,      },    }),  ],  devServer: {    // ...  },};

Теперь нам нужно забутстрапить точку входа в наше приложение, чтобы оно запускалось асинхронно, для этого создаем файл bootstrap.tsx и кладем туда содержимое файла index.tsx

// bootstrap.tsximport React from 'react';import { render } from 'react-dom';import { App } from './App';import { config } from './config';import './index.scss';config.init().then(() => {  render(<App />, document.getElementById('root'));});

А в index.tsx вызываем этот самый bootstrap

import('./bootstrap');

В общем то всё, в таком виде уже можно импортировать ваши микрофронтенды - они указываются в объекте remotes в формате <name>@<адрес хоста>/<filename>. Но нам такая конфигурация не подходит, ведь на момент сборки приложения мы ещё не знаем откуда будем брать микрофронтенд, к счастью, есть готовое решение, поэтому возьмем код из примера для динамических хостов, так как наше приложение написано на React, то оформим хэлпер в виде React-компонента LazyService:

// LazyService.tsximport React, { lazy, ReactNode, Suspense } from 'react';import { useDynamicScript } from './useDynamicScript';import { loadComponent } from './loadComponent';import { Microservice } from './types';import { ErrorBoundary } from '../ErrorBoundary/ErrorBoundary';interface ILazyServiceProps<T = Record<string, unknown>> {  microservice: Microservice<T>;  loadingMessage?: ReactNode;  errorMessage?: ReactNode;}export function LazyService<T = Record<string, unknown>>({  microservice,  loadingMessage,  errorMessage,}: ILazyServiceProps<T>): JSX.Element {  const { ready, failed } = useDynamicScript(microservice.url);  const errorNode = errorMessage || <span>Failed to load dynamic script: {microservice.url}</span>;  if (failed) {    return <>{errorNode}</>;  }  const loadingNode = loadingMessage || <span>Loading dynamic script: {microservice.url}</span>;  if (!ready) {    return <>{loadingNode}</>;  }  const Component = lazy(loadComponent(microservice.scope, microservice.module));  return (    <ErrorBoundary>      <Suspense fallback={loadingNode}>        <Component {...(microservice.props || {})} />      </Suspense>    </ErrorBoundary>  );}

Хук useDynamicScript нужен нам, чтобы в рантайме прикреплять загруженный скрипт к нашему html-документу.

// useDynamicScript.ts  import { useEffect, useState } from 'react';export const useDynamicScript = (url?: string): { ready: boolean; failed: boolean } => {  const [ready, setReady] = useState(false);  const [failed, setFailed] = useState(false);  useEffect(() => {    if (!url) {      return;    }    const script = document.createElement('script');    script.src = url;    script.type = 'text/javascript';    script.async = true;    setReady(false);    setFailed(false);    script.onload = (): void => {      console.log(`Dynamic Script Loaded: ${url}`);      setReady(true);    };    script.onerror = (): void => {      console.error(`Dynamic Script Error: ${url}`);      setReady(false);      setFailed(true);    };    document.head.appendChild(script);    return (): void => {      console.log(`Dynamic Script Removed: ${url}`);      document.head.removeChild(script);    };  }, [url]);  return {    ready,    failed,  };};

loadComponent это обращение к Webpack-контейнеру, по сути - обычный динамический импорт.

// loadComponent.tsexport function loadComponent(scope, module) {  return async () => {    // Initializes the share scope. This fills it with known provided modules from this build and all remotes    await __webpack_init_sharing__('default');    const container = window[scope]; // or get the container somewhere else    // Initialize the container, it may provide shared modules    await container.init(__webpack_share_scopes__.default);    const factory = await window[scope].get(module);    const Module = factory();    return Module;  };}

Ну и напоследок опишем тип для нашего микросервиса, дженерик нужен для того, чтобы правильно работала типизация пропсов.

// types.tsexport type Microservice<T = Record<string, unknown>> = {  url: string;  scope: string;  module: string;  props?: T;};
  • url - имя хоста + имя контейнера (например, http://localhost:3002/widgets.js), с которого мы хотим подтянуть модуль

  • scope - параметр name, который мы укажем в удаленном конфиге ModuleFederationPlugin

  • module - имя модуля, который мы хотим подтянуть

  • props - опциональный параметр, если вдруг наш микросервис требует пропсы, нужно их типизировать

Вызов компонента LazyService происходит следующим образом:

import React, { FC, useState } from 'react';import { LazyService } from '../../components/LazyService';import { Microservice } from '../../components/LazyService/types';import { Loader } from '../../components/Loader';import { Toggle } from '../../components/Toggle';import { config } from '../../config';import styles from './styles.module.scss';export const Video: FC = () => {  const [microservice, setMicroservice] = useState<Microservice>({    url: config.microservices.widgets.url,    scope: 'widgets',    module: './Zack',  });  const toggleMicroservice = () => {    if (microservice.module === './Zack') {      setMicroservice({ ...microservice, module: './Jack' });    }    if (microservice.module === './Jack') {      setMicroservice({ ...microservice, module: './Zack' });    }  };  return (    <>      <div className={styles.ToggleContainer}>        <Toggle onClick={toggleMicroservice} />      </div>      <LazyService microservice={microservice} loadingMessage={<Loader />} />    </>  );};

В общем то, по коду видно, что мы можем динамически переключать наши модули, а основной url хранить, например, в конфиге.

Так, с shell-приложением вроде разобрались, теперь нужно откуда-то брать наши модули.

Настройка микрофронтенда

Для начала проделываем все те же манипуляции что и в shell-приложении и убеждаемся, что версия Webpack => 5

Настраиваем ModuleFederationPlugin, но уже со своими параметрами, эти параметры указываем при подключении модуля в основное приложение.

// ...new ModuleFederationPlugin({      name: 'widgets',      filename: 'widgets.js',      shared: {        react: { requiredVersion: deps.react },        'react-dom': { requiredVersion: deps['react-dom'] },        'react-query': {          requiredVersion: deps['react-query'],        },      },      exposes: {        './Todo': './src/App',        './Gallery': './src/pages/Gallery/Gallery',        './Zack': './src/pages/Zack/Zack',        './Jack': './src/pages/Jack/Jack',      },    }),// ...

В объекте exposes указываем те модули, которые мы ходим отдать наружу, точку входа в приложение так же нужно забутстрапить. Если в микрофронтенде нам не нужны модули с других хостов, то компонент LazyService тут не нужен.

Вот и всё, получен работающий прототип микрофронтенда.

Выглядит круто, работает тоже круто. Общие зависимости не грузятся повторно, версии библиотек рулятся плагином, можно динамически переключать модули, в общем, сказка. Если копать глубже, то это очень гибкая технология, можно использовать её не только с React и JavaScript, но и со всем, что переваривает Webpack, то есть теоретически можно подружить части приложения написанные на разных фреймворках, это конечно не очень хорошо, но сделать так можно. Можно собрать модули и положить на CDN, можно использовать контейнер как общую библиотеку компонентов для нескольких приложений. Возможностей реально много.

Проблемы

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

Потеря контекстов в React-компонентах

Как только понадобилось работать с контекстом библиотеки react-router, то возникли проблемы, при попытке использовать в микрофронтенде хук useLocation, например, приложение вылетало с ошибкой.

Ошибка при попытке обращения к контексту shell-приложения из микрофронтендаОшибка при попытке обращения к контексту shell-приложения из микрофронтенда

Для взаимодействия с бэкендом мы используем Apollo, и хотелось, чтобы ApolloClient объявлялся только единожды в shell-приложении. Но при попытке из микрофронтенда просто использовать хук useQuery, в рантайме приложение вылетало с такой же ошибкой как и для useLocation.

Экспериментальным путём было выяснено, для того чтобы контексты правильно работали, нужно в микрофронтендах использовать версию npm-пакета не выше, чем в shell-приложение, так что за этим нужно внимательно следить.

Дублирование UI-компонентов в shell-приложении и микрофронтенде

Так как разработка ведётся разными командами, есть шанс, что разработчики напишут компоненты с одинаковым функционалом и в shell-приложении и в микрофронтенде. Чтобы этого избежать, есть несколько решений:

  1. Выносить UI-компоненты в отдельный npm-пакет и использовать его как shared-модуль

  2. "Делиться" компонентами через ModuleFederationPlugin

В принципе, у обоих подходов есть свои плюсы, но мы выбрали первый, потому что так удобнее и прозрачнее управлять библиотекой компонентов. Да и саму технологию Module Federation хотелось использовать как механизм для построения микрофронтендов, а не аналог npm.

Заключение

Пока что выглядит так, что переход на Webpack 5 Module Federation решает проблему, которая стояла перед нашим стримом, а именно - разделение зоны ответственности и распараллеливание разработки. При этом, нет больших накладных расходов при разработке, а настройка довольно проста даже для тех, кто не знаком с этой технологией.

Минусы у этого подхода конечно же есть, накладные расходы для развертывания зоопарка микрофронтендов будут значительно выше, чем для монолита. Если над вашим приложением работает одна-две команды и оно не такое большое, то наверное не стоит делить его на микросервисы.

Но для нашей конкретной проблемы, это решение подошло хорошо, посмотрим, как оно покажет себя в будущем, технология развивается и уже появляются фреймворки и библиотеки, которые под капотом используют Module Federation.

Полезные ссылки

Репозиторий из примера

Документация Module Federation в доках Webpack 5

Примеры использования Module Federation

Плейлист по Module Federation на YouTube

Подробнее..

От одного приложения к сотне. Путь микрофронтенда в Тинькофф Бизнес

16.06.2021 12:16:05 | Автор: admin

Привет, меня зовут Ваня, недавно я выступил на CodeFest 11, где рассказал про путь Тинькофф Бизнеса на фронтенде от одного приложения к сотне. Но так как в ИT очень быстро все меняется, а ждать запись еще долго, сейчас я тезисно расскажу о нашем шестилетнем путешествии в дивный мир микрофронтенда!

Мою статью можно разбить на две части. В первой части вас ждет история развития через призму бизнеса. А во второй рассказ о том, как мы адаптировались к новым вызовам.

Этапы развития

  1. Одно приложение на AngularJS в 20142015 годах.

  2. Миграция на Angular2.

  3. Утяжеление десяти приложений новой функциональностью.

  4. Переход к микросервисам и разбиение на 100 приложений.

На дворе начало 2015 года. К нам приходит бизнес и говорит: Мы хотим сделать зарплатный проект! Посмотрите, что есть сейчас на рынке по технологиям, и сделайте. Выбираем AngularJS, быстро создаем приложение. Спустя некоторое время аппетиты вырастают, мы создаем еще два сервиса. На этот момент фронтенд-приложения никак не взаимодействуют друг с другом.

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

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

В одном из проектов у нас было пять фронтенд-команд, в каждой по 35 человек, то есть в самый лучший момент в одном проекте работали 25 фронтендеров! Иногда было действительно больно: ты вот-вот замержишь свою задачку, но нет! Перед тобой кто-то успевает и все твои пайплайны начинают проходить заново! До сих пор мне не по себе от этих воспоминаний.

В какой-то момент мы осознали, что сборка приложения занимает уже полчаса и будет только расти, и решили распиливать наши монолиты на микросервисы.

Вот так быстро и пролетели шесть лет эволюции наших подходов. Теперь взглянем на наш путь через призму своих решений для микрофронтенда.

Сайдбар

Первые три приложения мы подружили между собой с помощью сайдбара.

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

По технической части это iframe со всеми плюсами и минусами, о которых довольно подробно расписано в докладе Яндекса. Но больше всего нам не нравилась перезагрузка страницы на каждую смену продукта. Это мешало переиспользованию состояния клиента, требовало каждый раз обращаться к бекенду и добавляло некрасивые мерцания страницы при переходах.

Подсвеченная область отдельное приложение СайдбарПодсвеченная область отдельное приложение Сайдбар

Frame Manager

Именно рваные переходы мы убрали с появлением Frame Manager'а (далее буду называть его ФМ).

Подсвеченная область отдельное приложение Frame ManagerПодсвеченная область отдельное приложение Frame Manager

В отличие от сайдбара, который встраивался в приложение с помощью iframe, ФМ находился на странице всегда и сам встраивал в себя приложения.

Слева концепция сайдбара (было), справа Frame Manager'а (стало)Слева концепция сайдбара (было), справа Frame Manager'а (стало)

Для клиента ФМ так же не заметен, как и сайдбар, но для нас он выполняет уже больше функций. Кроме рейтинга, списка продуктов и авторизации он может передавать состояние между приложениями, например через глобальный window. Пользователь больше не замечает, что ходит между несколькими отдельными приложениями, так как верхняя часть у него никуда не пропадает и визуально весь процесс выглядит как подгрузка контента на одну страницу.

В плане интеграции приложения тоже все поменялось:

  • Раньше приложению-клиенту достаточно было подключить необходимый скрипт к себе в index.html.

  • Теперь все приложения ФМа хранятся в отдельной конфигурации и используются как единый источник правды.

Минус этого подхода мы все равно остались с iframe, который нам не особо нравился из-за особенностей работы с ним.

Однажды через поддержку к нам обратились пользователи с ситуацией: Раньше у меня работал плагин для Google Chrome, а с недавнего времени именно на вашем сайте перестал. Почините, пожалуйста! Обычно на такие просьбы не реагируют: пользователь что-то себе установил пусть сам и разбирается. Но только не в нашей компании. Команда долго изучала вопрос, смотрела, какое окружение у клиента, версия браузера и все-все, но ответа так и не было. В итоге мы полностью повторили окружение, загрузили себе плагины и путем дебагинга установили, что данный плагин не работает, если у iframe динамически менять атрибут src или пересоздавать фрейм. К сожалению, мы так и не смогли исправить такое поведение, поскольку на этой концепции построено все взаимодействие ФМ и дочерних приложений.

Бесфрейм-менеджер

Однажды мы собрались и подумали: Несколько лет страдаем от iframe. Как перестать страдать? Давайте просто уберем его! Сказано сделано. Так и появился бесфрейм-менеджер с фантазией у нас, конечно, не фонтан ;-)

Ключевые отличия от предыдущей версии самописная изоляция и работа с микрофронтендом вместо фрейма. На мой взгляд, лучше всего мотивацию создания отражает слайд из моей презентации:

В решении три составляющие:

  1. Webpack-плагин основа нашего решения, подробнее о которой можно прочитать в статье Игоря.

  2. Angular builder обвязка для настройки и запуска плагина.

  3. Angular schematics скрипт для упрощения работы с файловой структурой с помощью AST.

В 2021 году плагин становится менее актуальным, потому что вышел Webpack 5 с Module Federation, но напомню, что мы вели разработку в 2018 году, а Angular стал поддерживать последнюю версию вебпака лишь с двенадцатой версии, которая вышла 12 мая 2021 года. Мы пока не уверены, сможет ли MF заменить наше решение, и изучаем комбинацию подходов.

Что же касается других решений, на которые можно было перейти для отказа от iframe, то это Single SPA. Он всем хорош и очень популярен, но в плане Angular есть небольшой дисклеймер.

http://personeltest.ru/aways/single-spa.js.org/docs/ecosystem-angular/https://single-spa.js.org/docs/ecosystem-angular/

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

Что же касается Angular builder и schematics, то они нужны, чтобы разработчики, которые будут интегрировать наше решение к себе, не выполняли километровую инструкцию, а просто написали в консоли:

ng update @scripts/deframing

И вся магия подключения произойдет у них на глазах, но не потребует никакого ручного вмешательства. Подробнее с работой билдеров и схематиков можно ознакомиться в классной статье.

Тестирование

Для нас важным моментом является автоматическое тестирование, тем более в проекте, от которого зависят все остальные команды. Путем проб и ошибок мы пришли к такому разделению тестового покрытия:

  1. Можно прогонять тесты совместно с локальным ФМом. Так разработчики всегда могут быть уверены, что на текущей продовой сборке все работает и выглядит так, как и задумывалось.

  2. Сам ФМ определяет несколько жизненно важных процессов, работоспособность которых гарантирует при любых условиях: это авторизация, роутинг, работа с данными приложений. Для этого создаются приложения-стабы (stub), суть которых подключиться к ФМу и выполнить одну из вышеперечисленных функций. То есть на каждое изменение кодовой базы ФМа будет гарантированно работать эта функция.

Ну и как же обойтись без описания багов, которые мы встретили на своем пути. Их тоже можно поделить глобально на две группы: накопление стилей и сторонние библиотеки.

Накопление стилей пользователь гуляет между приложениями, и у него накапливается состояние. Допустим, один из разработчиков написал:

.my-pretty-header {    display: none;}

Если у кого-то из следующих приложений есть такое же название класса, этот стиль применится так же!

Пример: диалог решил спрятаться под меню, чтобы пользователь не догадался, что от него требуется:

Этот тип багов мы решили путем префикса по id приложения для всех стилей, чтобы они не имели глобальную видимость.

Сторонние библиотеки если на одной странице два и более приложения используют библиотеку, которая на старте создает новый инстанс, то получается такая картина:

То есть библиотека пытается дважды запуститься и второй сервис затирает первый, что ломает поведение обоих. Это мы исправили путем небольших доработок библиотек.

Microzord

Вот мы и прошли шесть лет технического развития нашего решения. И что может быть лучше, чем поделиться этим опытом с сообществом? Все наработки будут публиковаться под npm scope @microzord с открытым кодом на Гитхабе.

В планах предоставить клиент не только для Angular, но и для работы с другими фреймворками. Сейчас лишь небольшая часть вынесена на Гитхаб, но будьте уверены, ребята не заставят себя ждать и в одной из следующих статей расскажут о гитхабе поподробнее.

Подробнее..

Перевод Построение инфраструктуры распределенной трассировки Netflix

15.12.2020 18:16:33 | Автор: admin

Для будущих учащихся на курсе "Highload Architect" и всех желающих подготовили перевод интересной статьи.

Также приглашаем посмотреть открытый урок
"Паттерны горизонтального масштабирования хранилищ".


Наша группа Кевин Лью (Kevin Lew),Нараянан Аруначалам (Narayanan Arunachalam),Элизабет Карретто (Elizabeth Carretto),Дастин Хаффнер (Dustin Haffner), Андрей Ушаков,Сет Кац (Seth Katz),Грег Баррелл (Greg Burrell),Рам Вайтхилингам (Ram Vaithilingam),Майк Смит (Mike Smith)иМаулик Пандей (Maulik Pandey)

@NetflixhelpsПочему "Король тигров" не идет на моем телефоне? подписчик Netflix спрашивает через Twitter

Это пример вопроса, на который инженерам нашей службы поддержки приходится отвечать в попытке помочь подписчику решить его проблему, а это очень сложно, когда вы имеете дело с распределенной системой. Для анализа неполадок со стримингом видео требуется проверка всех аспектов работы учетной записи подписчика. В предыдущей статье нашего блога мы рассказали об Edgar, нашей системе поиска и устранения неисправностей в сеансах стриминга. Теперь давайте поговорим о том, как мы спроектировали инфраструктуру трассировки, на основе которой работает Edgar.

Распределенная трассировка: отсутствие контекста при поиске и устранении неисправностей крупномасштабных сервисов

До появления Edgar нашим инженерам приходилось просматривать горы метаданных и журналов, полученных от различных микросервисов Netflix, чтобы понять причинуконкретногосбоя стриминга, который возник у кого-то из наших подписчиков. Восстановление сеанса стриминга было утомительным и занимающим много времени процессом, в котором требовалось проследить все взаимодействия (запросы) между приложением Netflix, нашей сетью доставки контента (CDN) и серверными микросервисами. В начале процесса инженер вручную получал информацию об учетной записи подписчика, который участвовал в сеансе. Далее необходимо было сложить вместе все детали пазла в надежде, что получившаяся картина позволит решить проблему подписчика. Нам нужно было повысить продуктивность работы специалистов за счет распределенной трассировки запросов.

Если бы у нас был идентификатор каждого сеанса стриминга, то путем распределенной трассировки можно было бы легко воспроизвести сбой сеанса и увидеть при этом топологию сервисов, теги повторных попыток и ошибок, а также значения задержек для всех вызовов сервисов. Мы также могли бы получать контекстную информацию о сеансе стриминга, объединяя соответствующие трассировки с метаданными учетной записи и журналами сервисов. Эта идея привела нас к созданию Edgar инфраструктуры распределенной трассировки, ориентированной на удобство пользователей.

Рисунок 1. Поиск и устранение сбоя сеанса в EdgarРисунок 1. Поиск и устранение сбоя сеанса в Edgar

Когда четыре года назад мы начинали создавать Edgar, существовало очень мало систем распределенной трассировки с открытым исходным кодом, которые отвечали нашим потребностям. Мы решили подождать, пока эти системы достигнут зрелого состояния, и первое время собирали трассировки от Java-сервисов стриминга с помощью собственных библиотек. К 2017 году такие открытые проекты, как Open-Tracing и Open-Zipkin, достигли достаточного уровня зрелости, чтобы их можно было использовать в многоязычных средах выполнения, применяющихся в Netflix.

Наш выбор пал на Open-Zipkin, поскольку эта система лучше интегрировалась с нашей средой выполнения Java на основе Spring Boot. Мы используемMantisдля обработки потока собранных трассировок, а для хранения трассировок мы используем Cassandra. Наша инфраструктура распределенной трассировки состоит из трех компонентов: инструментарий библиотек трассировки, обработка потоков и хранилище. Трассировки, получаемые от различных микросервисов, обрабатываются как поток и перемещаются в хранилище данных. В следующих разделах описан наш путь по созданию этих компонентов.

Инструментарий трассировки: как он повлияет на наш уровень обслуживания?

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

В основе распределенной трассировки лежит распространение контекста для локальных межпроцессных вызовов и клиентских вызовов к удаленным микросервисам для любого произвольного запроса. При передаче контекста запроса фиксируются причинно-следственные связи между микросервисами во время выполнения. Мы использовали механизм распространения контекста на основезаголовков HTTP B3из Open-Zipkin. Мы следим за тем, чтобы заголовки, используемые для распространения контекста, правильно передавались между микросервисами в разнообразных средах выполнения Java и Node, которые интегрированы в нашу систему разработки ПО (внутри компании она называется paved road). В эту систему входят как базы legacy-кода, так и новые среды, например Spring Boot. Следуя принципу нашей культуры Свобода и ответственность, мы поддерживаем библиотеки трассировки и в других средах (Python, NodeJS, Ruby on Rails и др.), которые не входят в систему paved road. Нашисвободные, но высокоскоординированныеинженерные группы могут по своему усмотрению выбирать подходящую библиотеку трассировки для своей среды выполнения и отвечают за обеспечение правильного распространения контекста и интеграцию перехватчиков сетевых вызовов.

Средства интеграции со средами выполнения создают теги инфраструктуры, такие как имя сервиса, группа автоматического масштабирования и идентификаторы контейнерных инстансов. При поиске и устранении сбоев в сеансах стриминга Edgar использует эту схему тегов инфраструктуры для запроса и объединения трассировок с данными журналов. Кроме того, благодаря использованию согласованных тегов в Edgar оказалось легко устанавливать глубокие связи с разными системами мониторинга и развертывания. Обеспечив интеграцию со средами выполнения, мы должны были задать надлежащую политику выборки данных трассировки, чтобы обеспечить удобство поиска и устранения сбоев.

Обработка потока: выполнять или нет выборку данных трассировки?

Это был самый важный вопрос, который мы рассматривали при создании нашей инфраструктуры, поскольку политика выборки данных определяет количество трассировок, которые записываются, передаются и хранятся. При использовании мягкой политики выборки данных трассировки формируется большое количество трассировок в каждом сервисном контейнере, а это может привести к снижению производительности сервисов стриминга, поскольку библиотека трассировки будет потреблять больше ресурсов ЦП, памяти и сети. Еще одной проблемой, которую несет с собой мягкая политика выборки, является необходимость в масштабируемой обработке потоков и инфраструктуре хранения, позволяющей размещать крупные объемы данных.

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

В большинстве систем распределенной трассировки политика выборки применяется в точке приема запросов (если представить себе диаграмму вызовов микросервисов). Мы выбралигибридный подход к выборке данных на основе заголовков, который позволяет записывать 100% трассировок для определенного, настраиваемого набора запросов. В остальном же трафике выборка производится случайным образом в соответствии с политикой, заданной в точке приема. Такой гибкий подход позволяет библиотекам трассировки записывать все трассировки наших важнейших микросервисов стриминга, собирая при этом минимальное количество трассировок от таких вспомогательных систем, как система отложенной пакетной обработки данных. Наши инженерные группы настроили свои сервисы на максимальную производительность с учетом возросшей из-за трассировки потребности в ресурсах. Следующей проблемой была потоковая передача больших объемов трассировок через масштабируемую платформу обработки данных.

Mantis это основная платформа обработки операционных данных в Netflix. Мы выбрали платформу Mantis в качестве магистрали для передачи и обработки больших объемов данных трассировки, поскольку нам требовалась масштабируемая система потоковой обработки, способная справляться с эффектами backpressure. Наш агент сбора данных трассировки перемещает эти данные в кластер заданий Mantis с помощьюбиблиотеки Mantis Publish. Мы помещаем диапазоны в буфер на определенный период времени, чтобы собрать все диапазоны трассировки в первом задании. Второе задание забирает поток данных из первого задания, выполняет хвостовую выборку данных и записывает трассировки в систему хранения. Такая цепочка заданий Mantis позволяет нам масштабировать все компоненты обработки данных независимо друг от друга. Дополнительное преимущество использования Mantis заключается в возможности выполнять произвольный просмотр данных в режиме реального времени вRavenс помощьюязыка запросов Mantis (MQL). Однако наличие масштабируемой платформы потоковой обработки не особо помогает, если невозможно обеспечить экономичное хранение данных.

Хранилище без переплат

Сначала в качестве хранилища данных мы использовали Elasticsearch: нас привлекли гибкая модель данных и возможности обработки запросов, которые есть в этом продукте. Мы продолжали добавлять в систему все больше сервисов стриминга, и объем данных трассировки начал экспоненциально расти. Из-за высокой скорости записи данных приходилось постоянно масштабировать кластеры Elasticsearch, вследствие чего возрастала операционная нагрузка. Запросы на чтение данных выполнялись все дольше, поскольку кластеры Elasticsearch использовали значительные вычислительные ресурсы для индексирования вносимых в них трассировок. Из-за больших объемов получаемых данных со временем упала производительность и операций чтения, и операций записи. Выйти из этой ситуации нам удалось путем перехода наCassandraв качестве хранилища данных: этот продукт позволил нам справиться с большим объемом получаемых данных. Использование простых поисковых индексов в Cassandra дает нам возможность поддерживать приемлемые задержки чтения, выполняя при этом большие объемы операций записи.

В теории горизонтальное масштабирование позволило бы нам поддерживать высокую скорость записи и сохранять большие объемы данных в кластерах Cassandra. Это означает, что затраты на хранение трассировок растут линейно в зависимости от объема хранимых данных. Нам нужно было сделать так, чтобы рост затрат на хранение былсублинейнымпо отношению к объему хранимых данных. Стремясь достичь этой цели, мы сформировали следующие стратегии оптимизации хранилища:

  1. Использовать более дешевые томаElastic Block Store(EBS) вместо инстансов EC2 с SSD.

  2. Задействовать улучшенные методы сжатия, чтобы уменьшить размер данных трассировки.

  3. Хранить только важные и интересные трассировки, используя для этого простые фильтры на основе правил.

Мы добавляли новые узлы Cassandra, когда на существующих узлах переполнялись хранилища инстансов EC2 с SSD. Использование более дешевых томов EBS Elastic вместо хранилищ на базе инстансов с SSD было привлекательным вариантом, поскольку AWS позволяет динамически увеличивать размер томов EBS без повторного выделения узла EC2. Это позволило нам увеличивать общую емкость хранилища, не добавляя новые узлы Cassandra в существующий кластер. В 2019 году наши замечательные коллеги из группы Cloud Database Engineering (CDE) протестировали производительность EBS для нашего сценария и перенесли существующие кластеры на тома EBS Elastic.

За счет оптимизации параметров Time Window Compaction Strategy (TWCS, стратегия уплотнения временных интервалов) они сократили количество операций записи на диск и объединения для файлов Cassandra SSTable, сократив тем самым нагрузку ввода-вывода на EBS. Эта оптимизация помогла нам сократить объем сетевого трафика, связанного с репликацией данных между узлами кластера, поскольку файлы SSTable создавались реже, чем в нашей предыдущей конфигурации. Кроме того, обеспечение возможности сжатия блоков Zstd в файлах данных Cassandra позволило наполовину уменьшить размер наших файлов данных трассировки. Благодаря этим оптимизированным кластерам Cassandra мы теперь тратим на 71% меньше средств на обеспечение работы кластеров и можем хранить в 35 раз больше данных, чем при использовании предыдущей конфигурации.

Мы заметили, что пользователи Edgar просматривали менее чем 1% собранных трассировок. Зная это, мы полагаем, что можем понизить нагрузку операций записи и помещать больше данных в систему хранения, если будем удалять трассировки, которые не нужны пользователям. В настоящее время мы используем простой фильтр на основе правил в задании Mantis по сохранению данных. Этот фильтр сохраняет интересные трассировки для путей вызовов сервисов, которые очень редко используются в Edgar. Чтобы определить, является ли трассировка интересной единицей данных, фильтр проверяет все диапазоны трассировки, помещенные в буфер, на предмет тегов предупреждений, ошибок и повторных попыток. Хвостовая выборка позволила сократить объем данных трассировки на 20% без влияния на работу пользователей. Существует возможность использовать методы классификации на основе машинного обучения, чтобы еще больше сократить объемы данных трассировки.

Несмотря на то что мы добились значительного прогресса, сейчас мы достигли очередной поворотной точки на пути построения нашей системы хранения данных трассировки. Реализация новых возможностей для пользователей Edgar может потребовать от нас хранить в 10 раз больше данных по сравнению с текущими объемами. С учетом этого сейчас мы экспериментируем с вариантом многоуровневого хранения для нового шлюза данных. Этот шлюз данных имеет интерфейс запросов, который позволяет абстрагироваться от сложностей, связанных с чтением и записью данных в многоуровневые хранилища. Кроме того, шлюз данных направляет получаемые данные в кластер Cassandra и перемещает сжатые файлы данных из кластера Cassandra в S3.Мы планируем сохранять данные за последние несколько часов в кластерах Cassandra, а остальные трассировки, хранящиеся в течение длительного времени, будут находиться в корзинах S3.

Таблица 1. Временная шкала оптимизации хранилищаТаблица 1. Временная шкала оптимизации хранилища

Дополнительные преимущества

Помимо обеспечения работы Edgar, данные трассировки используются в следующих случаях.

Мониторинг состояния приложений

Данные трассировки являются основным сигналом, используемымTelltaleпри мониторинге состояния приложений на макроуровне в Netflix. Telltale использует причинно-следственную информацию из трассировок для определения топологии микросервисов и соотнесения трассировок с данными временных рядов изAtlas. Такой подход позволяет получить более детальную картину состояния приложений.

Проектирование устойчивости

Наша группа по хаос-инжинирингу использует трассировки для проверки правильности внесения сбоев, когда наши инженеры выполняют стресс-тесты своих микросервисов с помощью платформытестирования методом внедрения отказов(FIT).

Эвакуация из облачных регионов

Инженерная группа, ответственная за мониторинг спроса, применяет трассировку дляповышения точности предварительного масштабированияво время эвакуации из облачного региона. Трассировки позволяют определять типы устройств, взаимодействующих с микросервисами, чтобы изменения в спросе для этих сервисов можно было лучше учитывать при эвакуации из региона AWS.

Оценка затрат на инфраструктуру при выполнении A-/B-тестирования

Группа по data science и исследованию продуктов определяет затраты на выполнениеA-/B-тестированияна микросервисах путем анализа трассировок, в которых есть соответствующие имена и теги тестов A/B.

Что дальше?

По мере роста Netflix объем и сложность наших программных систем продолжают повышаться. При расширении Edgar мы будем уделять основное внимание следующим областям:

  • Обеспечение удобства работы разработчиков при сборе трассировок из всех сред выполнения. Мы надеемся, что при наличии простого способа попробовать в деле распределенную трассировку больше инженеров будут оснащать свои сервисы трассировками и предоставлять дополнительный контекст для каждого запроса, снабжая тегами соответствующие метаданные.

  • Расширение возможностей наших аналитиков по запросу данных трассировки, чтобы компетентные пользователи в Netflix могли создавать собственные узконаправленные панели мониторинга и системы.

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

Пока мы развиваем инфраструктуру распределенной трассировки, наши инженеры продолжают использовать Edgar для устранения таких проблем со стримингом, как Почему "Король тигров" не идет на моем телефоне?. Наша инфраструктура распределенной трассировки способствует тому, что подписчики Netflix могут в любое время смотреть свои любимые сериалы, в том числе и Король тигров!

Мы ищем талантливых коллег, которые хотели бы присоединиться к нам в нашей работе по созданию инфраструктуры распределенной трассировки. Если вы активно интересуетесь темой мониторинга систем, обязательносвяжитесь с нами.


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

Посмотреть открытый урок "Паттерны горизонтального масштабирования хранилищ".

Подробнее..

Асинхронное взаимодействие. Брокеры сообщений. Apache Kafka

24.12.2020 18:19:54 | Автор: admin
Данная публикация предназначена для тех, кто интересуется устройством распределенных систем, брокерами сообщений и Apache Kafka.
Здесь вы не найдете эксклюзивного материала или лайфхаков, задача этой статьи заложить фундамент и рассказать о внутреннем устройстве упомянутого брокера. Таким образом, в следующих публикациях мы сможем делать ссылки на данную статью, рассказывая о более узкоспециализированных темах.


Привет! Меня зовут Дмитрий Шеламов и я работаю в Vivid.Money на должности backend-разработчика в отделе Customer Care. Наша компания европейский стартап, который создает и развивает сервис интернет-банкинга для стран Европы. Это амбициозная задача, а значит и ее техническая реализация требует продуманной инфраструктуры, способной выдерживать высокие нагрузки и масштабироваться согласно требованиям бизнеса.

В основе проекта лежит микросервисная архитектура, которая включает в себя десятки сервисов на разных языках. В их числе Scala, Java, Kotlin, Python и Go. На последнем я пишу код, поэтому практические примеры, приведенные в этой серии статей, будут задействовать по большей части Go (и немного docker-compose).

Работа с микросервисами имеет свои особенности, одна из которых организация коммуникаций между сервисами. Модель взаимодействия в этих коммуникациях бывает синхронной или асинхронной и может оказать существенное влияние на производительность и отказоустойчивость системы в целом.

Асинхронное взаимодействие


Итак, представим что у нас есть два микросервиса (А и Б). Будем считать, что коммуникация между ними осуществляется через API и они ничего не знают о внутренней реализации друг друга, как и предписывает микросервисный подход. Формат передаваемых между ними данных заранее оговорен.

image

Задача перед нами стоит следующая: нам нужно организовать передачу данных от одного приложения к другому и, желательно, с минимальными задержками.
В самом простом случае поставленная задача достигается синхронным взаимодействием, когда А отправляет приложению Б запрос, после чего сервис Б его обрабатывает и, в зависимости от того, успешно или не успешно был обработан запрос, отправляет некоторый ответ сервису А, который этот ответ ожидает.
Если же ответ на запрос так и не был получен (например, Б рвет соединение до отправки ответа или А отваливается по таймауту), сервис А может повторить свой запрос к Б.

С одной стороны, такая модель взаимодействия дает определенность статуса доставки данных для каждого запроса, когда отправитель точно знает, были ли получены данные получателем и какие дальнейшие действия ему необходимо делать в зависимости от ответа.
С другой стороны, плата за это ожидание. После отправки запроса сервис А (или поток, в котором выполняется запрос) блокируется до того момента, пока не получит ответ или не сочтет запрос неудавшимся согласно своей внутренней логике, после чего примет дальнейшие действия.

Проблема не только в том, что ожидание и простой имеют место быть, задержки в сетевом взаимодействии неизбежны. Основная проблема заключается в непредсказуемости этой задержки. Участники коммуникации в микросервисном подходе не знают подробностей реализации друг друга, поэтому для запрашивающей стороны не всегда очевидно, обрабатывается ли ее запрос штатно или нужно переотправить данные.

Все, что остается А при такой модели взаимодействия это просто ждать. Может быть наносекунду, а может быть час. И эта цифра вполне реальна в том случае, если Б в процессе обработки данных выполняет какие-либо тяжеловесные операции, вроде обработки видео.

Возможно, вам проблема не показалась существенной одна железка ждет пока другая ответит, велика ли потеря?
Чтобы сделать эту проблему более личной, представим, что сервис А это приложение, запущенное на вашем телефоне, и пока оно ожидает ответ от Б, вы видите на экране анимацию загрузки. Вы не можете продолжить пользоваться приложением до тех пор, пока сервис Б не ответит, и вынуждены ждать. Неизвестное количество времени. При том, что ваше время гораздо ценнее, чем время работы куска кода.

Подобные шероховатости решаются следующим образом вы разделяете участников взаимодействия на два лагеря: одни не могут работать быстрее, как бы вы их ни оптимизировали (обработка видео), а другие не могут ждать дольше определенного времени (интерфейс приложения на вашем телефоне).
Затем вы заменяете cинхронное взаимодействие между ними (когда одна часть вынуждена ждать другую, чтобы удостовериться, что данные были доставлены и обработаны сервисом-получателем) на асинхронное, то есть модель работы по принципу отправил и забыл в этом случае сервис А продолжит свою работу, не дожидаясь ответа от Б.

Но как в этом случае гарантировать то, что передача прошла успешно? Вы же не можете, допустим, загрузив видео на видеохостинг, вывести пользователю сообщение: ваше видео может быть обрабатывается, а может быть и нет, потому что сервис, занимающийся загрузкой видео, не получил от сервиса-обработчика подтверждение, что видео дошло до него без происшествий.

В качестве одного из решений данной проблемы мы можем добавить между сервисами А и Б прослойку, которая будет выступать временным хранилищем и гарантом доставки данных в удобном для отправителя и получателя темпе. Таким образом мы сможем расцепить сервисы, синхронное взаимодействие которых потенциально может быть проблемным:

  • Данные, которые теряются при аварийном завершении сервиса-получателя теперь могут быть снова получены из промежуточного хранилища, в то время как сервис-отправитель продолжает выполнять свою работу. Таким образом мы получаем механизм гарантии доставки;
  • Эта прослойка также защищает получателей от скачков нагрузки, ведь получателю выдаются данные по мере их обработки, а не по мере их поступления;
  • Запросы на выполнение тяжеловесных операций (таких как рендеринг видео) теперь могут быть переданы через эту прослойку, обеспечивая меньшую связность между быстрыми и медленными частями приложения.


Под вышеобозначенные требования вполне подходит и обычная СУБД. Данные в ней можно хранить в течении продолжительного времени, не беспокоясь о потере информации. Также исключена и перегрузка получателей, ведь они вольны сами выбрать темп и объемы чтения предназначенных для них записей. Подтверждение же обработки можно реализовать, помечая прочитанные записи в соответствующих таблицах.

Однако выбор СУБД в качестве инструмента для обмена данными может привести к проблемам с производительностью с ростом нагрузки. Причина в том, что большинство баз данных не предназначены для такого сценария использования. Также во многих СУБД отсутствует возможность разделения подключенных клиентов на получателей и отправителей (Pub/Sub) в этом случае, логика доставки данных должна быть реализована на клиентской стороне.
Вероятно, нам нужно нечто более узкоспециализированное, чем база данных.

Брокеры сообщений


Брокер сообщений (очередь сообщений) это отдельный сервис, который отвечает за хранение и доставку данных от сервисов-отправителей к сервисам-получателям с помощью модели Pub/Sub.
Эта модель предполагает, что асинхронное взаимодействие осуществляется согласно следующей логике двух ролей:

  • Publishers публикуют новую информацию в виде сгруппированных по некоторому атрибуту сообщений;
  • Subscribers подписываются на потоки сообщений с определенными атрибутами и обрабатывают их.

Группирующим сообщения аттрибутом выступает очередь, которая нужна, чтобы разделять потоки данных, таким образом, получатели могут подписываться только на те группы сообщений, которые их интересуют.
По аналогии с подписками на различных контент-платформах подписавшись на определенного автора, вы можете фильтровать контент, выбирая смотреть только тот, который вам интересен.

image

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

В общем случае, тело представляет из себя набор байт определенного формата.
Получатель обязательно должен знать этот формат, чтобы после получения сообщения иметь возможность десериализовать его тело для дальнейшей обработки.
Использовать можно любой удобный формат, однако, важно помнить об обратной совместимости, которую поддерживают, например, бинарный Protobuf и фреймворк Apache Avro.

По такому принципу работает большинство брокеров сообщений, построенных на AMQP (Advanced Message Queuing Protocol) протоколе, который описывает стандарт отказоустойчивого обмена сообщениями посредством очередей.
Данный подход обеспечивает нам несколько важных преимуществ:

  • Слабая связанность. Она достигается за счет асинхронной передачи сообщений: то есть, отправитель скидывает данные и продолжает работать, не дожидаясь ответа от получателя, а получатель вычитывает и обрабатывает сообщения, когда удобно ему, а не когда они были отправлены. В данном случае очередь можно сравнить с почтовым ящиком, в который почтальон кладет ваши письма, а вы их забираете, когда удобно вам.
  • Масштабируемость. Если сообщения появляются в очереди быстрее, чем консьюмер успевает их обрабатывать (речь идет не о пиковых нагрузках, а о стабильном разрыве между скоростью записи и обработки), мы можем запустить несколько экземпляров приложения-консьюмера и подписать их на одну очередь.
    Этот подход называется горизонтальным масштабированием, а экземпляры одного сервиса принято называть репликами. Реплики сервиса-консьюмера будут читать сообщения из одной очереди и обрабатывать их независимо друг от друга.
  • Эластичность. Наличие между приложениями такой прослойки, как очередь, помогает справляться с пиковыми нагрузками: в этом случае очередь будет выступать буфером, в котором сообщения будут копиться и по мере возможности считываться консьюмером, вместо того, чтобы ронять приложение-получатель, отправляя данные ему напрямую.
  • Гарантии доставки. Большинство брокеров предоставляют гарантии at least once и at most once.


At most once исключает повторную обработку сообщений, однако допускает их потерю. В этом случае брокер будет доставлять сообщения получателям по принципу отправил и забыл. Если получатель не смог по какой-то причине обработать сообщение с первой попытки, брокер не будет осуществлять переотправку.

At least once, напротив, гарантирует получение сообщения получателем, однако при этом есть вероятность повторной обработки одних и тех же сообщений.
Зачастую эта гарантия достигается с помощью механизма Ack/Nack (acknowledgement/negative acknowledgement), который предписывает совершать переотправку сообщения, если получатель по какой-то причине не смог его обработать.
Таким образом, для каждого отправленного брокером (но еще не обработанного) сообщения существует три итоговых состояния получатель вернул Ack (успешная обработка), вернул Nack (неуспешная обработка) или разорвал соединение.
Последние два сценария приводят в переотправке сообщения и повторной обработке.

Однако брокер может произвести повторную отправку и при успешной обработке сообщения получателем. Например, если получатель обработал сообщение, но завершил свою работу, не отправив сигнал Ack брокеру.
В этом случае брокер снова положит сообщение в очередь, после чего оно будет обработано повторно, что может привести к ошибкам и порче данных, если разработчик не предусмотрел механизм устранения дублей на стороне получателя.

Стоит отметить, что существует еще одна гарантия доставки, которая называется exactly once. Ее трудно достичь в распределенных системах, но при этом она же является наиболее желаемой.
В этом плане, Apache Kafka, о которой мы будем говорить далее, выгодно выделяется на фоне многих доступных на рынке решений. Начиная с версии 0.11, Kafka предоставляет гарантию доставки exactly once в пределах кластера и транзакций, в то время как AMQP-брокеры таких гарантий предоставить не могут.
Транзакции в Кафке тема для отдельной публикации, сегодня же мы начнем со знакомства с Apache Kafka.

Apache Kafka


Мне кажется, что будет полезно для понимания начать рассказ о Кафке со схематичного изображения устройства кластера.

image

Отдельный сервер Кафки именуется брокером. Брокеры образуют собой кластер, в котором один из этих брокеров выступает контроллером, берущим на себя некоторые административные операции (помечен красным).

За выбор брокера-контроллера, в свою очередь, отвечает отдельный сервис ZooKeeper, который также осуществляет service discovery брокеров, хранит конфигурации и принимает участие в распределении новых читателей по брокерам и в большинстве случаев хранит информацию о последнем прочитанном сообщении для каждого из читателей.
Это важный момент, изучение которого требует опуститься на уровень ниже и рассмотреть, как отдельный брокер устроен внутри.

Commit log


Структура данных, лежащая в основе Kafka, называется commit log или журнал фиксации изменений.

image

Новые элементы, добавляемые в commit log, помещаются строго в конец, и их порядок после этого не меняется, благодаря чему в каждом отдельном журнале элементы всегда расположены в порядке их добавления.

Свойство упорядоченности журнала фиксаций позволяет использовать его, например, для репликации по принципу eventual consistency между репликами БД: в них хранят журнал изменений, производимых над данными в мастер-ноде, последовательное применение которых на слейв-нодах позволяет привести данные в них к согласованному с мастером виду.
В Кафке эти журналы называются партициями, а данные, хранимые в них, называются сообщениями.
Что такое сообщение? Это основная единица данных в Kafka, представляющая из себя просто набор байт, в котором вы можете передавать произвольную информацию ее содержимое и структура не имеют значения для Kafka.
Сообщение может содержать в себе ключ, так же представляющий из себя набор байт. Ключ позволяет получить больше контроля над механизмом распределения сообщений по партициям.

Партиции и топики


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

image

Так вот в Кафке функцию очереди выполняет не партиция, а topic. Он нужен для объединения нескольких партиций в общий поток. Сами же партиции, как мы сказали ранее, хранят сообщения в упорядоченном виде согласно структуре данных commit log.
Таким образом, сообщение, относящееся к одному топику, может хранится в двух разных партициях, из которых читатели могут вытаскивать их по запросу.

Следовательно, единицей параллелизма в Кафке выступает не топик (или очередь в AMQP брокерах), а партиция. За счет этого Кафка может обрабатывать разные сообщения, относящиеся к одному топику, на нескольких брокерах одновременно, а также реплицировать не весь топик целиком, а только отдельные партиции, предоставляя дополнительную гибкость и возможности для масштабирования в сравнении с AMQP брокерами.

Pull и Push


Обратите внимание, что я не случайно использовал слово вытаскивает по отношению к читателю.
В описанных ранее брокерах доставка сообщений осуществляется путем их проталкивания (push) получателям через условную трубу в виде очереди.
В Кафке процесса доставки как такового нет: каждый читатель сам ответственен за вытягивание (pull) сообщений из партиций, которые он читает.

image

Производители, формируя сообщения, прикрепляют к нему ключ и номер партиции. Номер партиции может быть выбран рандомно (round-robin), если у сообщения отсутствует ключ.

Если вам нужен больший контроль, к сообщению можно прикрепить ключ, а затем использовать hash-функцию или написать свой алгоритм, по которому будет выбираться партиция для сообщения. После формирования, производитель отправляет сообщение в Кафку, которая сохраняет его на диск, помечая, к какой партиции оно относится.

Каждый получатель закреплен за определенной партицией (или за несколькими партициями) в интересующем его топике, и при появлении нового сообщения получает сигнал на вычитывание следующего элемента в commit log, при этом отмечая, какое последнее сообщение он прочитал. Таким образом при переподключении он будет знать, какое сообщение ему вычитать следующим.

Какие преимущества имеет данный подход?


  • Персистентность. В классических брокерах сообщение хранится в памяти брокера ровно до того момента, как брокер получит сигнал об успешной обработке сообщения читателем, который это сообщение вытащил из очереди. В Кафке же сообщения хранятся столько, сколько нужно (в зависимости от Retention Policy, об этом позднее), а значит из одной партиции одновременно могут читать сообщения несколько получателей.
  • Message Replay. Читатели могут перечитывать сообщения сколько угодно раз, начиная с произвольного места в партиции. Это может быть полезно, например, для восстановления данных на стороне читателя при потере части изменений в БД.
  • Упорядоченность. Она гарантируется в том числе потому, что нет механизма переотправки (в силу ненадобности) в обычных брокерах в процессе доставки переотправлямые сообщения постоянно перетасовываются в очереди, так как они закидываются в нее снова после каждой неудачной попытки их обработать.
  • Чтение и запись пачками. Читатель может читать сообщения пачками (batch) из одной партиции, а не по отдельности, как это происходит с обычными брокерами. Это бывает полезно для уменьшения сетевой задержки: при передаче большого количества сообщений (1кк и выше), гонять по сети каждое сообщение отдельно становится дорого.


Недостатки


К недостаткам данного подхода можно отнести работу с проблемными сообщениями. В отличие от классических брокеров, битые сообщения (которые не удается обработать с учетом существующей логики получателя или из-за проблем с десериализацей) нельзя бесконечно перезакидывать в очередь, пока получатель не научится их корректно обрабатывать.
В Кафке по умолчанию вычитывание сообщений из партиции останавливается, когда получатель доходит до битого сообщения, и до тех пор, пока оно не будет пропущено и закинуто в карантинную очередь (также именуемой dead letter queue) для последующей обработки, чтение партиции продолжить не получится.

Также в Кафке сложнее (в сравнении с AMQP-брокерами) реализовать приоритет сообщений. Это напрямую вытекает из того факта, что сообщения в партициях хранятся и читаются строго в порядке их добавления. Один из способов обойти данное ограничение в Кафке создать нескольких топиков под сообщения с разным приоритетом (отличаться топики будут только названием), например, events_low, events_medium, events_high, а затем реализовать логику приоритетного чтения перечисленных топиков на стороне приложения-консьюмера.

Еще один недостаток данного подхода связан тем, что необходимо вести учет последнего прочитанного сообщения в партиции каждым из читателей.
В силу простоты структуры партиций, эта информация представлена в виде целочисленного значения, именуемого offset (смещение). Оффсет позволяет определить, какое сообщение в данный момент читает каждый из читателей. Ближайшая аналогия оффсета это индекс элемента в массиве, а процесс чтения похож на проход по массиву в цикле с использованием итератора в качестве индекса элемента.
Однако этот недостаток нивелируется за счет того, что Kafka, начиная с версии 0.9, хранит оффсеты по каждому пользователю в специальном топике __consumer_offsets (до версии 0.9 оффсеты хранились в ZooKeeper).
К тому же, вести учет оффсетов можно непосредственно на стороне получателей.

image

Также усложняется и масштабирование: напомню, что в AMQP брокерах для того, чтобы ускорить обработку потока сообщений, нужно просто добавить несколько экземпляров сервиса-читателя и подписать их на одну очередь, при этом не требуется вносить никаких изменений в конфигурации самого брокера.

Однако в Кафке масштабирование происходит несколько сложнее, чем в AMQP брокерах.
Например, если вы добавите еще один экземпляр читателя и натравите его на ту же партицию, вы получите нулевой КПД, так как в этом случае оба экземпляра будут читать один и тот же набор данных.
Поэтому базовое правило масштабирования Кафки количество конкурентных читателей (то бишь группа сервисов, реализующих одинаковую логику обработки (реплик)) топика не должно превышать количество партиций в этом топике, иначе какая-то пара читателей будут обрабатывать одинаковый набор данных.

Consumer Group


Чтобы избежать ситуации с чтением одной партиции конкурентными читателями, в Кафке принято объединять несколько реплик одного сервиса в consumer Group, в рамках которого Zookeeper будет назначать одной партиции не более одного читателя.

Так как читатели привязываются непосредственно к партиции (при этом читатель обычно ничего не знает о количестве партиций в топике), ZooKeeper при подключении нового читателя производит перераспределение участников в Consumer Group таким образом, чтобы каждая партиция имела одного и только одного читателя.
Читатель обозначает свою Consumer Group при подключении к Kafka.

image

В то же время ничего не мешает вам повесить на одну партицию несколько читателей с разной логикой обработки. Например вы храните в топике список событий по действиям пользователей и хотите использовать эти события для формирования нескольких представлений одних и тех же данных (например для бизнес-аналитиков, продуктовых-аналитиков, системных-аналитиков и пакета Яровой) и последующей отправкой их в соответствующие хранилища.

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

image

Чтобы решить эту проблему, мы можем пойти от обратного: если все события, относящиеся к одной сущности (например, все действия относящиеся к одному user_id), будут всегда добавляться в одну и ту же партицию, они будут упорядочены в рамках топика просто потому, что находятся в одной партиции, порядок внутри которой гарантирован Кафкой.
Для этого нам и нужен ключ у сообщений: например, если мы будем использовать для выбора партиции, в которую будет добавлено сообщение, алгоритм, вычисляющий хэш от ключа, то сообщения с одинаковым ключом будут гарантированно попадать в одну партицию, а значит и вытаскивать получатель сообщения с одинаковым ключом в порядке их добавления в топик.
В кейсе с потоком событий о действиях пользователей ключом партицирования может выступать user_id.

Retention Policy


Теперь пришло время поговорить о Retention Policy.
Это настройка, которая отвечает за удаление сообщений с диска при превышении пороговых значений даты добавления (Time Based Retention Policy) или занимаемого на диске пространства (Size Based Retention Policy).

  • Если вы настроите TBRP на 7 суток, то все сообщения старше 7 суток будут помечаться для последующего удаления. Иными словами, эта настройка гарантирует, что в каждый момент времени будут доступны для чтения сообщения младше порогового возраста. Можно задавать в часах, минутах и милисекундах.
  • SBRP работает аналогичным образом: при превышении порога занимаемого дискового пространства, сообщения будут помечаться для удаления с конца (более старые). Нужно иметь в виду: так как удаление сообщений происходит не мгновенно, занимаемый объем диска всегда будет чуть больше указанного в настройке. Задается в байтах.


Retention Policy можно настроить как для всего кластера, так и для отдельных топиков: например, сообщения в топике для отслеживания деиствии пользователеи можно хранить несколько днеи, в то время как пуши в течении нескольких часов. Удаляя данные согласно их актуальности, мы экономим место не диске, что может быть важно при выборе SSD в качестве основного дискового хранилища.

Compaction Policy


Еще одним способом оптимизации объема, занимаемого на диске, может быть использование Compaction Policy эта настройка позволяет хранить только последнее сообщение по каждому ключу, удаляя все предыдущие сообщения. Это может быть полезно, когда нас интересует только последнее изменение.

Сценарии использования Kafka


  • Отслеживание действий пользователей на клиентской части. При этом логгируемая информация может быть самой разной: от списка просмотренных страниц до щелчков мыши. Сообщения о действиях публикуются в один или несколько топиков, где потребителем может выступать, например, хранилище аналитических данных (Clickhouse можно подписать непосредственно на топик Кафки!) для дальнейшего построения отчетов или рекомендательных систем.
    В Customer Care отделе Vivid.Money мы используем топик Кафки для доставки в аналитическое хранилище логов о действиях операторов в нашей CRM.
  • Обмен сообщениями. Кафка может выступать этаким единым интерфейсом для отправки различных уведомлений, пушей или электронных писем во всем проекте. Любой сервис может подключиться к Кафке и отправить сообщение в определенный топик для уведомлений, из которого на той стороне сервис-консьюмер (имеющий доступ к контактной информации клиентов) его считает, преобразует в формат пригодный для отправки нотификации непосредственно клиенту, и осуществит фактическую отправку.
    Благодаря этому мы можем отправить пуш буквально из любой части нашей инфраструктуры, без необходимости получения контактных данных пользователя (и его предпочтений по способу связи) в инициирующем отправку нотификации сервисе. В свою свою очередь, успешно получив сообщение, Кафка гарантирует то, что оно будет доставлено клиенту, даже если на стороне сервиса нотификаций возникли неполадки.
  • Мониторинг. Через топики кафки можно организовать сбор и агрегацию логов и метрик для их централизованной обработки, используя ее как транспорт.
  • Журнал фиксации (commit log). Можно дублировать в топик транзакции БД, чтобы сервисы-потребители синхронизировали состояние связанных данных уже в своих базах/сторонних системах.
    Опять же, долгосрочное хранение сообщений позволяет выступать Кафке этаким буфером для изменений, который позволяет переиграть изменения из топика Кафки для приведения данных на стороне получателя к согласованному виду в случае сбоев приложений получателей или повреждению данных в их БД.
    По такому принципу у нас в Customer Care организована синхронизация данных профиля клиента в используемых нами CRM-системах с изменениями данных пользователей в наших внутренних базах.


Подытожим основные преимущества Kafka


  • В один топик может писать один или несколько производителей идеально для агрегирования данных из большого количества источников, что становится особенно полезно при использовании Кафки в качестве системы доставки сообщений в микросервисной архитектуре;
  • Несколько потребителей с учетом особенностей механизма получения сообщений (pull) один и тот же поток сообщений может читать несколько потребителей, не мешая при этом друг другу.
    При этом конкурентных читателей (например, реплики одного сервиса) можно объединить в Consumer Group, а ZooKeeper, в свою очередь, будет следить, чтобы каждая партиция одновременно читалась не более, чем одним участником каждой группы;
  • Хранение данных на диске в течение длительного времени позволяет не беспокоится о потере данных при резком росте нагрузки. Кафка, будучи своего рода буфером, компенсирует отставание потребителей, позволяя накапливать в себе сообщения до нормализации нагрузки или масштабирования консьюмеров. Также обеспечивается гибкая конфигурация, где отдельные потоки данных (топики) хранятся на диске с разным сроком;
  • Хорошо масштабируется, засчет меньшей, в сравнении с AMQP брокерами, единицей параллелизма партицией. Разные партиции могут храниться в разных брокерах, обеспечивая дополнительную гибкость при горизонтальном масштабировании;
  • Быстродействие. В силу простоты механизма, при которой процесса доставки нет как такового, а процесс передачи данных представляет из себя запись-хранение-выдача, Кафка обладает очень большой пропускной способностью она исчисляется миллионами сообщений в секунду.
Подробнее..

Перевод Spring Cloud и Spring Boot. Часть 1 использование Eureka Server

26.01.2021 20:09:19 | Автор: admin

Будущих студентов курса Разработчик на Spring Framework и всех желающих приглашаем на открытый онлайн-урок по теме Введение в облака, создание кластера в Mongo DB Atlas018. Участники вместе с преподавателем-экспертом поговорят о видах облаков и настроят бесплатный Mongo DB кластер для своих проектов.

А сейчас делимся с вами традиционным переводом статьи.


В этой статье мы поговорим о том, как установить и настроить службу обнаружения (service discovery) для Java-микросервисов.

Что такое Eureka Server?

Eureka Server это service discovery (обнаружение сервисов) для ваших микросервисов. Клиентские приложения могут самостоятельно регистрироваться в нем, а другие микросервисы могут обращаться к Eureka Server для поиска необходимых им микросервисов.

Eureka Server также известен как Discovery Server и содержит такую информацию как IP-адрес и порт микросервиса.

Для создания приложения с Eureka Server необходимо в pom.xml добавить указанную ниже зависимость.

<dependency>  <groupId>org.springframework.cloud</groupId>  <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId></dependency>

Запускаем Eureka Server

Перейдите на https://start.spring.io и создайте шаблон проекта. Укажите метаданные, такие как Group, Artifact, и добавьте указанные ниже зависимости / модули. Нажмите "Generate Project" и загрузите проект в zip-файле. Далее разархивируйте его и импортируйте в IDE как Maven-проект.

  • Eureka Server

  • Web

  • Actuator

Проверьте, что pom.xml выглядит следующим образом:

<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://personeltest.ru/away/maven.apache.org/POM/4.0.0" xmlns:xsi="http://personeltest.ru/away/www.w3.org/2001/XMLSchema-instance"        xsi:schemaLocation="http://personeltest.ru/away/maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">   <modelVersion>4.0.0</modelVersion>   <parent>       <groupId>org.springframework.boot</groupId>       <artifactId>spring-boot-starter-parent</artifactId>       <version>2.0.7.RELEASE</version>       <relativePath/> <!-- lookup parent from repository -->   </parent>   <groupId>com.example.eureka.server</groupId>   <artifactId>eureka-server</artifactId>   <version>0.0.1-SNAPSHOT</version>   <name>eureka-server</name>   <description>Demo project for Spring Boot</description>   <properties>       <java.version>1.8</java.version>       <spring-cloud.version>Finchley.SR2</spring-cloud.version>   </properties>   <dependencies>       <dependency>           <groupId>org.springframework.boot</groupId>           <artifactId>spring-boot-starter-actuator</artifactId>       </dependency>       <dependency>           <groupId>org.springframework.boot</groupId>           <artifactId>spring-boot-starter-web</artifactId>       </dependency>       <dependency>           <groupId>org.springframework.cloud</groupId>           <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>       </dependency>       <dependency>           <groupId>org.springframework.boot</groupId>           <artifactId>spring-boot-starter-test</artifactId>           <scope>test</scope>       </dependency>   </dependencies>   <dependencyManagement>       <dependencies>           <dependency>               <groupId>org.springframework.cloud</groupId>               <artifactId>spring-cloud-dependencies</artifactId>               <version>${spring-cloud.version}</version>               <type>pom</type>               <scope>import</scope>           </dependency>       </dependencies>   </dependencyManagement>   <build>       <plugins>           <plugin>               <groupId>org.springframework.boot</groupId>               <artifactId>spring-boot-maven-plugin</artifactId>           </plugin>       </plugins>   </build></project>

Теперь откройте файл EurekaServerApplication.java и добавьте для класса аннотацию @EnableEurekaServer, как показано ниже.

package com.example.eureka.server.eurekaserver;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;@SpringBootApplication@EnableEurekaServerpublic class EurekaServerApplication {  public static void main(String[] args) {     SpringApplication.run(EurekaServerApplication.class, args);  }}

Добавьте в application.properties, расположенный в src/main/resources, следующие параметры.

spring.application.name=eureka-serverserver.port=8761eureka.client.register-with-eureka=falseeureka.client.fetch-registry=false
  • spring.application.name уникальное имя для вашего приложения.

  • server.port порт, на котором будет запущено ваше приложение, мы будем использовать порт по умолчанию (8761).

  • eureka.client.register-with-eureka определяет, регистрируется ли сервис как клиент на Eureka Server.

  • eureka.client.fetch-registry получать или нет информацию о зарегистрированных клиентах.

Запустите сервер Eureka как Java-приложение и перейдите по адресу http://localhost:8761/

Вы увидите, что Eureka Server запущен и работает, но в нем еще нет зарегистрированных приложений.

Регистрация клиентского приложения в Eureka Server

Перейдите на https://start.spring.io и создайте шаблон проекта. Укажите метаданные, такие как Group, Artifact, и добавьте указанные ниже зависимости / модули. Нажмите "Generate Project" и загрузите проект в zip-файле. Далее разархивируйте его и импортируйте в IDE как Maven-проект.

  • DevTools

  • Actuator

  • Discovery Client

Проверьте, что ваш pom.xml выглядит следующим образом:

<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://personeltest.ru/away/maven.apache.org/POM/4.0.0" xmlns:xsi="http://personeltest.ru/away/www.w3.org/2001/XMLSchema-instance"      xsi:schemaLocation="http://personeltest.ru/away/maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">  <modelVersion>4.0.0</modelVersion>  <parent>     <groupId>org.springframework.boot</groupId>     <artifactId>spring-boot-starter-parent</artifactId>     <version>2.0.7.RELEASE</version>     <relativePath/> <!-- lookup parent from repository -->  </parent>  <groupId>com.example.eureka.client</groupId>  <artifactId>EurekaClientApplication</artifactId>  <version>0.0.1-SNAPSHOT</version>  <name>EurekaClientApplication</name>  <description>Demo project for Spring Boot</description>   <properties>     <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>     <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>     <java.version>1.8</java.version>     <spring-cloud.version>Finchley.SR2</spring-cloud.version>  </properties>   <dependencies>     <dependency>        <groupId>org.springframework.boot</groupId>        <artifactId>spring-boot-starter-web</artifactId>     </dependency>     <dependency>        <groupId>org.springframework.boot</groupId>        <artifactId>spring-boot-starter-actuator</artifactId>     </dependency>     <dependency>        <groupId>org.springframework.boot</groupId>        <artifactId>spring-boot-devtools</artifactId>        <scope>runtime</scope>     </dependency>     <dependency>        <groupId>org.springframework.boot</groupId>        <artifactId>spring-boot-starter-test</artifactId>        <scope>test</scope>     </dependency>     <dependency>        <groupId>org.springframework.cloud</groupId>        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>     </dependency>  </dependencies>   <dependencyManagement>     <dependencies>        <dependency>           <groupId>org.springframework.cloud</groupId>           <artifactId>spring-cloud-dependencies</artifactId>           <version>${spring-cloud.version}</version>           <type>pom</type>           <scope>import</scope>        </dependency>     </dependencies>  </dependencyManagement>  <build>     <plugins>        <plugin>           <groupId>org.springframework.boot</groupId>           <artifactId>spring-boot-maven-plugin</artifactId>        </plugin>     </plugins>  </build></project>

Откройте файл EurekaClientApplication.java и добавьте для класса аннотацию @EnableDiscoveryClient, как показано ниже.

package com.example.eureka.client.application;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.cloud.client.discovery.EnableDiscoveryClient;@SpringBootApplication@EnableDiscoveryClientpublic class EurekaClientApplication {  public static void main(String[] args) {     SpringApplication.run(EurekaClientApplication.class, args);  }}

Добавление REST-контроллера

Создайте класс HelloWorldController в пакете com.example.eureka.client.application и добавьте GET-метод в этом классе.

package com.example.eureka.client.application;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.PathVariable;import org.springframework.web.bind.annotation.RestController;@RestControllerpublic class HelloWorldController {   @GetMapping("/hello-worlds/{name}")   public String getHelloWorld(@PathVariable String name) {       return "Hello World " + name;   }}

В application.properties, расположенный в src/main/resources, добавьте следующие параметры.

spring.application.name=eureka-client-serviceserver.port=8081eureka.client.service-url.defaultZone=http://localhost:8761/eureka/

Параметр eureka.client.service-url.defaultZone определяет адрес Eureka Server, чтобы клиентское приложение могло там зарегистрироваться.

Запуск клиентского приложения

Перед запуском приложения необходимо убедиться, что Eureka Server запущен и работает. Запустите нашего клиента как Java-приложение и перейдите в Eureka Server по адресу http://localhost:8761/. На этот раз вы должны увидеть, что наше клиентское приложение зарегистрировалось на Eureka Server.

Теперь вы знаете как использовать Eureka Server для своих микросервисов. В следующей статье посмотрим на распределенную трассировку (Distributed Tracing) для Spring Boot-микросервисов.


Узнать подробнее о курсе Разработчик на Spring Framework.

Записаться на открытый онлайн-урок по теме
Введение в облака, создание кластера в Mongo DB Atlas018.

Подробнее..

О мифологии миграции монолита в облака

23.04.2021 12:19:27 | Автор: admin

Около десяти лет назад микросервисы получили первое признание. (Однако есть альтернативное мнение, что микросервисам уже пятнадцать лет). С тех пор масса фирм воспользовалась услугами облачных провайдеров и перенесла свои сервисы к ним. А некоторые из них даже успели разочароваться в облачных технологиях и вернулись к традиционной схеме монолита (или почти к ней, см. например [TB]).

Эта статья - не попытка уговорить вас на перенос вашего монолита в облако или отговорить от этого. Это попытка развеять возможно сидящие глубоко в вашем сознании необоснованные опасения или ожидания (другими словами - мифы и заблуждения) по поводу такого переноса.

Но вначале нам необходимо договориться о том, что же это такое - монолит.

Развивая идею из публикации [SN], я предлагаю ввести пять частичных метрик для измерения монолитности той или иной программной системы:

  1. Монолитность Deploy-процесса. Она измеряется количеством процессов, которые приводят к созданию исполняемых артефактов приложения. При этом мы считаем только терминальные, т.е. порождающие конечные артефакты процессы. Подготовительные процессы, генерирующие компоненты или вспомогательные элементы, в нашей метрике не учитываются. Таким образом, эта целочисленная величина имеет минимальное значение 1.

  2. Монолитность исполняемых артефактов. Это количество исполняемых артефактов. Например, это может быть количество ваших JAR-файлов. При этом иногда, в зависимости от особенностей вашей системы, вместо собственно JAR-файлов следует считать папку с основным JAR-файлом и вспомогательными файлами используемых библиотек и конфигураций.

  3. Монолитность времени исполнения (run time). Один исполняемый артефакт может использоваться у вас многократно (параллельно), с балансировкой каким-либо балансёром или диспетчером.

  4. Монолитность хранения данных. Эта величина измеряется количеством разных систем хранения ваших данных. Хранит ваше приложение данные в одной базе данных или в нескольких? Если система использует файловую систему в качестве хранилища данных - в скольки файлах и папках она это делает?

  5. Функциональная монолитность. В отличии от предыдущих величин, которые вычисляются более или менее однозначно, вычислить эту величину часто бывает очень трудно. Более того, разные специалисты при её вычислении могут прийти к разным результатам. Для её вычисления необходимо проанализировать входные и выходные интерфейсы приложения, его функциональность, и решить, на сколько независимых процессов можно было бы (не вдаваясь в детали текущей реализации) разбить основную функциональность приложения.

Таким образом, монолитность вашего приложения можно грубо оценить с помощью описанных пяти положительных целочисленных величин с минимальным значением 1.

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

Ну а теперь, вооруженные этим определением монолитности, перейдём к описанию типичных мифов по поводу переноса монолита в облако.

Миф 1: Всё или ничего

Перенос программной системы в облако у многих интуитивно ассоциируется с переездом на новое место жительства. И если недолго пожить на новой квартире без мебели как-то можно, то для продолжения бизнеса необходимо иметь постоянно функционирующую систему. Другими словами, миф состоит в том, что сначала надо перенести весь монолит в облако, хорошенько протестировать и только потом переводить туда реальный бизнес.

На самом деле это не так. Архитектурный паттерн, известный под именами Призма( см. [PR]), схожий с Canary deployments[CD] и A/B Testing, позволяет разрабатывать новые микросервисы постепенно, независимо от основного приложения и сбоку от него. Идея паттерна показана на рисунке внизу.

Паттерн "Призма"Паттерн "Призма"

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

Если ваша система общается с внешним миром с помощью HTTP-протокола, оба треугольника могут быть реализованы в виде одного простого proxy-сервера. Различные Service Mesh инструменты типа Istio предоставляю такую возможность на уровне конфигурации.

Первая (красная) призма направляет все входные данные на монолит, дублируя при этом данные интересные заново имплементированному сервису и посылая дублированные данные на него. Вторая призма перехватывает выходные данные вашего монолита, дублирует результаты запланированного к замещению сервиса и посылает дубликаты на сравнение в Comparator. Ну а Comparator сравнивает результаты нового варианта с результатами монолита.

Таким образом, монолит работает как и раньше (синие элементы на рисунке).

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

Миф 2: Просто расщепим на части и докенизируем

Если первое заблуждение характерно для слишком осторожных команд, то это - для слишком оптимистичных.

Нередко миграция монолита в облако представляется как простое расщепление монолита на несколько частей и перенос каждой части сначала в Docker/Kubernetes, а возможно потом - в одну из облачных сред типа AWS или Azure.

Увы, в большинстве случае так просто решить проблему не получается.

Если ваш монолит используется давно и интенсивно, он наверняка оброс вторичными автоматическими и ручными процессами, которые иногда для бизнеса могут быть ценнее процессов самого монолита. Как правило, эти процессы пользуются конечными или промежуточными данными, созданными монолитом либо наоборот, в определённых ситуациях процессы вашего монолита требуют ручного вмешательства или помощи внешних процессов.

Поэтому, прежде чем приступать к миграции монолита, надо тщательно инвентаризировать его окружение. Типичными неочевидными stockholderes являются сотрудники или системы следующих отделов или направлений:

  1. Финансы (например, Data Warehouse)

  2. Reports

  3. Рекламации

  4. Ручные обрывы процессов в определённых ситуациях

  5. Коррекция неверных данных

  6. Evaluation (Периодическая оценка ситуации)

  7. Настройка системы

  8. Audit

  9. Workarounds

Способы удовлетворения потребностей этих групп должны быть учтены при подготовке плана миграции.

С другой стороны, вы сможете воспользоваться некоторыми стандартными компонентами, предлагаемыми облачными провайдерами и не перетаскивать туда собственные решения стандартных проблем типа авторизации пользователей или гибкого поиска данных одновременно в базе данных и других хранилищах. Ознакомьтесь с предложениями marketplace вашего будущего облачного провайдера перед началом работ по миграции.

Расщепление монолита на части означает, как правило, не только замену API общения компонент монолита между собой, но и смену парадигмы их коммуникации. На практике это оборачивается переходом на event driven механизм коммуникации.

Не вдаваясь в детали, отметим пунктиром, над чем в этом случае следует подумать и что постараться сделать:

  1. Использование паттерна SAGA (см. [SA])при реализации распределённых транзакций.

  2. Использование паттерна QCRS (см. [QC])при работе с распределёнными данными.

  3. Использование идемпотентных функций (см. [IF]), позволяющих при возникновении проблем просто вызывать их заново без опасения за состояние сохранённых данных).

Миф 3: Переносить надо только функциональность, всё остальное предоставит провайдер

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

Реальность облачной жизни иная. По крайней мере, следующие проблемы вам наверняка придётся решать самостоятельно:

  1. Мониторинг

  2. Аудит

  3. Desaster Recovery

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


Миф 4: Облачные сервисы очень дёшевы

Практически все облачные провайдеры предлагают разного рода калькуляторы, позволяющие оценить стоимость предоставляемых услуг и сравнить их с вашими текущими затратами. Во всех моих расчётах калькуляторы всегда обещали большую выгоду от переноса систем в облако.

Экономия ожидается прежде всего по заработной плате, т.к. калькуляторы практически не предполагают расходов на зарплату системным администраторам. Реальность выглядит по-другому. Вряд ли в настоящее время возможно поддерживать реальный бизнес на облачном сервисе без наличия компетентных администраторов в штате, либо консультантов. Просто потому, что облачные сервисы всё еще сложны и бурно развиваются, а сервис и поддержка облачных провайдеров недостаточны для ведения более-менее серьёзного облачного бизнеса без технической компетенции в собственной фирме.

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

Практика показывает, что со временем центр тяжести перемещается на оплату именно этих, а не базовых сервисов.

Ну и последнее: облачный провайдер обещает фиксированные цены на ограниченный период, например - на год. Никто не знает, как он может после этого изменить свою ценовую политику.

Поэтому, мигрируя свои сервисы в облако, надо постоянно помнить о vendor lock-in и проектировать облачную архитектуру так, чтобы при необходимости иметь возможность сменить провайдера.

Миф 5: Микросервисы должны быть маленькими

То, что микросервисы должны быть маленькими, явствует из самого их названия, не правда ли? Поэтому при миграции в облака монолит надо разбить на большое число маленьких частей, разве не так? Примеры ландшафтов со многими сотнями и даже тысячами микросервисов, как например в банке Monzo ( см. [MB]), вроде бы также говорят в пользу такого подхода.

И опять, - в жизни всё сложнее. Излишнее измельчение сервисов может привести к слишком большому количеству интерфейсов между ними, сложности их обновления, проблемам при обновлении базовых библиотек и т.д. и т.п.

К счастью, отрасль уже накопила достаточное понимание проблемы и выработала конструктивные рекомендации по вопросу расщепления монолита на микросервисы. Детали вы найдёте в работах [HS], [ST], [HS1]. Здесь же короткий перечень основных критериев, когда такое расщепление возможно стоит делать:

  1. Выделять в отдельный сервис функциональность с объективно повышенной вероятностью ошибок в run time.

  2. Реализовывать компоненты с заведомо разным жизненным циклом в разных сервисах.

  3. Разделять на сервисы компоненты с заведомо разными ожидаемыми частотами внесения изменений и обновлений.

  4. Выделять в отдельный сервис функциональность, требующую повышенной скалируемости.

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

А иногда различные микросервисы появляются на свет просто из-за организационных причин (необходимость реализовать их разными командами).

Миф 6: Затраты на миграцию монолита непредсказуемы

Хорошо это или плохо, но последние годы многие маленькие команды и большие коллективы разрабатывают программные системы в рамках агильного подхода. Часто, особенно у относительно молодых специалистов, это приводит к формированию убеждения, что среднесрочное и долгосрочное планирование в рамках агильного подхода невозможно. Следовательно, затраты на миграцию монолита в облако достоверно оценить (предсказать) нельзя.

Это заблуждение. Применение соответствующих аналитических методов позволяет быстро перечислить внешние интерфейсы вашего монолита и выявить неочевидных stockholderes. Использование грамотных архитектурных методов (учитывая наличие функционирующей системы) даёт возможность относительно быстрого построения компонентной модели. Эта модель, в свою очередь, позволит принять обоснованные решения о дальнейшем использование своих или аренде готовых компонент провайдера. Так же на этом этапе вы можете определиться с отображением компонентной архитектуры в физические микросервисы.

Если вы владеете исходным кодом монолита, вы можете за фиксированное время провести анализ пригодности его компонент к прямому переносу и оценить объём необходимого рефакторинга.

Миф 7: Раз все так делают, значит так можно

Сервис мега-провайдера типа AWS или Azure отличается низкой латентностью, высокой надёжностью и доступностью по всему миру за счёт их собственных Data Centers. Неудивительно, что многие фирмы и государственные службы пользуются их услугами.

При этом использующие эти сервисы фирмы на вопрос о соблюдении правил защиты данных потребителей нередко отвечают: Ну если все так делают, значит это легально.

Мега-провайдеры усиливают эту аргументацию обещанием, что вы можете для хранения ваших данных выбрать географически подходящий Data Center , например для европейцев - во Франкфурте.

Увы, это не соответствует действительности.Все мега-провайдеры соблюдают законы США, в том числе - CLOUD Act [CA], позволяющий американским службам без судебного решения потребовать от провайдера любые данные. А это противоречит многим национальным и европейским законам о защите данных их граждан.

Но это в теории, на практике-то ничего неприятного не происходит?, возможно спросите вы. Нет, происходит. Например, в Германии уже рассматривались случаи наложения штрафов на образовательные учреждения и отдельных преподавателей за использование облачных продуктов типа Microsoft Office 365, Zoom и т.п.( см. [ MO]). В апреле 2021 года немецкая служба защиты приватных данных (Datenschutz-Aufsichtsbehrde) потребовала от многочисленных немецких фирм обоснования хранения данных у американских облачных провайдеров в связи с решением Европейского Суда по вопросу соглашения Privacy Shield (см. [DF]).

Живущий в некоторых из нас конформист предъявит на это, возможно, следующий аргумент - то что немцы защищают так права своих детей - это их дело. И что что американские спецслужбы ловят террористов и злостных неплательщиков налогов через облачные сервисы - дело тоже неплохое. Но наш сервис предназначен не для детей, да и террористы им вряд ли пользуются.

Увы - эта аргументация не действует. Во первых - если вы своими действиями нарушаете закон страны, где живут ваши пользователи - вы должны понимать, чем вам это грозит.

Что же касается американских спецслужб - я очень советую вспомнить про Сноудена, а ещё лучше прочитать (если не читали) его книгу [ES]. Лично меня в ней поразили две вещи.

Во-первых то, что Сноуден, не будучи штатным сотрудником американских спецслужб, а простым фрилансером, устроенным через цепочку фирм, получил доступ к информации высших уровней секретности. Применив не очень сложные хакерские инструменты и незамысловатые трюки, он считал эту информацию с глубоко секретных серверов и передал её другим людям. Сноудом двигали идеалистические побуждения и информацию он передал журналистам. Но на его месте мог оказаться человек с другими идеалами, который мог скачать с серверов другую информацию и передать её в другие руки, например - в руки мафии. А может, такое уже происходило или происходит прямо сейчас?

Вторым поразившим меня в этой книге был эпизод с вербовкой абсолютно невиновного молодого человека, который в будущем мог занять в иерархии своей страны интересующую спецслужбы позицию. Сноуден в деталях рассказывает о попытках подставить этого человека под судебную ответственность. Знание некоторых деталей его биографии, его привязанностей и слабостей им в этом очень помогло. Собственно, в этом нет ничего нового. Мы все много раз видели это в кино про КГБ, ЦРУ и т.д. Но воспоминания Сноудена как участника операции показывают, что это всё не выдумки. Как мы видим, спецслужбы иногда охотятся и за абсолютно невиновными людьми. Хотите вы с вашим сервисом в этом участвовать (осознанно или неосознанно), особенно, если этого можно не делать?

Наверное нет. Так что же делать?

Самое главное - будьте честны перед своими пользователями. Перед тем как доверить вам свои данные, человек должен осознать, чем он рискуют. Расскажите ему (ей) об этом, перед тем как начать сотрудничество.

Постарайтесь сами обойтись без излишних приватных данных. Возможно, ваш бизнес может обойтись без обратной связи с некоторыми категориями пользователей через Email или телефон. Если ваш бизнес ориентирован на профессионалов, вы можете поддерживать в учётных записях (accounts) только логин, пароль и серию контрольных вопросов.

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

Ну и как самый крайний вариант - не используйте специализированные фичи мега-провайдеров, а используйте решения на базе открытых решений типа Docker/Kubernetes/Kafka, что позволит вам при необходимости развернуть бизнес в облаках локальных провайдеров интересующей вас страны.

Разумеется, хорошим решением является хранение и обработка персональных данных локально с использованием облачных сервисов для решения абстрактных алгоритмических задач. Увы, такое разделение возможно далеко не всегда.

Вместо заключения

Изложенное в этой статье не претендует на роль истины в последней инстанции. Процесс миграции монолитов в облака находится в полном разгаре. Накопленный опыт миграции неплохо обобщён и осмыслен в книге [MM].

Автор признателен дочитавшим эту статью до конца и с интересом ожидает ваших суждений, возражений и мнений.А с какими мифами сталкивались вы?

Ссылки и литература

CA

CLOUD Act

https://en.wikipedia.org/wiki/CLOUD_Act

CD

Canary deployments

https://octopus.com/docs/deployments/patterns/canary-deployments

DF

Deutsche Firmen in der Datenschutzfalle Behrden intensivieren Ermittlungen wegen US-Cloud-Nutzung

https://www.xing-news.com/reader/news/articles/3938440?cce=em5e0cbb4d.%3AtgdwSkqSkgtEqcgH83anAB&link_position=digest&newsletter_id=74341&toolbar=true&xng_share_origin=email

DH

Datenschutzverste im Homeschooling und Bugelder

https://digitalcourage.de/blog/2020/datenschutzverstoesse-im-homeschooling-und-bussgelder

EK

EuGH kippt Rechtsgrundlage fr Datentransfers in die USA

https://www.handelsblatt.com/politik/international/privacy-shield-abkommen-eugh-kippt-rechtsgrundlage-fuer-datentransfers-in-die-usa/26009730.html?ticket=ST-1251974-6dlleMe6cOkufQQeue3s-ap1

ES

Edward Snowden. Permanent Record

Macmillan; Airport- edition (17 Sept. 2019). ISBN-13: 978-1529035667

HS

How small should Microservices be

https://medium.com/@ggonchar/deciding-on-size-of-microservices-dbb2a8d8f7e5

HS1

How Small Should Microservices Be?

https://stackoverflow.com/questions/56509701/how-small-a-micro-service-should-be

IF

Pattern: Idempotent Consumer

https://microservices.io/patterns/communication-style/idempotent-consumer.html

MB

Modern Banking in 1500 Microservices

https://www.infoq.com/presentations/monzo-microservices/?utm_source=email&utm_medium=architecture-design&utm_campaign=newsletter&utm_content=09222020

MM

Sam Newman. Monolith to Microservices: Evolutionary Patterns to Transform Your Monolith (English Edition) 1

ORelly. ISBN 13: 978-1492047841

MO

Microsoft Office 365: Die Grnde fr das Nein der Datenschtzer

https://www.heise.de/news/Microsoft-Office-365-Die-Gruende-fuer-das-Nein-der-Datenschuetzer-4919847.html

NI

The NIST Definition of Cloud Computing

https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-145.pdf

PR

Prizma switch technology

https://www.semanticscholar.org/paper/Prizma-switch-technology-Engbersen/f49764456f1668ab082d17c1f0c7f8b104835ebb

QC

Pattern: Command Query Responsibility Segregation (CQRS)

https://microservices.io/patterns/data/cqrs.html

SA

Pattern: Saga

https://microservices.io/patterns/data/saga.html

SM

6 Strategies for Migrating Applications to the Cloud

https://aws.amazon.com/de/blogs/enterprise-strategy/6-strategies-for-migrating-applications-to-the-cloud/

SN

Sam Newman: Migrating Monoliths to Microservices With Decomposition and Incremental Changes.

The InfoQ eMag/ Issue #91 /February 2021. Re-examining Microservices After the First Decade

ST

Should that be a Microservice? Keep These Six Factors in Mind

https://tanzu.vmware.com/content/blog/should-that-be-a-microservice-keep-these-six-factors-in-mind

TB

Thomas Betts. To Microservices and Back Again - Why Segment Went Back to a Monolith.

The InfoQ eMag/ Issue #91 /February 2021. Re-examining Microservices After the First Decade

Иллюстрации автора.

Подробнее..

Документирование микросервисов в LeanIX (EAM)

08.07.2020 12:13:32 | Автор: admin


Расскажу о нашем опыте автоматического документирования 150+ микросервисов в системе LeanIX Enterprise Architecture Managment. Многое получилось, как мы и хотели, для чего-то пришлось делать специальные доработки, часть вопросов не смогли решить. Но в любом случае мы получили опыт и готовы им поделиться.


Начну статью с оговорки, что это результат прототипа или исследования. Попытка ответить на вопрос: "Возможно ли использовать системы описания архитектуры предприятия для описания системы построенной на микросервисном подходе?"


Для чего вообще нужно описывать ландшафт предприятия?


Короткий ответ для поддержания процессов управления архитектурой предприятия (Enterprise Architecture Management / EAM).


Более полный ответ уже заключен в самой дисциплине EAM это системная концепция для разработки и развития IT-ландшафта в соответствии с бизнес-возможностями и процессами, а также оргструктурой организации. Бизнес-возможности (business capabilities) в свою очередь являются основой, на которой предприятие строит свои текущие и будущие бизнес-процессы.


EAM включает в себя набор сценариев использования каталога IT-систем, вот основные:



Cценарии выше релевантны не только для архитекторов предприятия (Enterprise Architect), но и для системных архитекторов (System Architect) и архитекторов решений (Solution Architects). Сам по себе каталог IT-систем особенно полезен на этапе интеграции приложений и отслеживания зависимостей. Тем более, что в большинстве организаций такой каталог является "Single Point of Truth".


Актуальность такого каталога необходимо постоянно поддерживать, либо тратя на это много времени сил, либо автоматизируя наполнение.
Эта проблема особенно ощутима, если хотя бы часть ландшафта предприятия построена на микросервисной архитектуре. В этом случае каталог будет содержать уже не 100-500 систем, а 500-1000 (и даже большое) компонентов.



Вот и у нас есть устоявшийся ландшафт на 500+ IT-систем (на диаграмме разбивка на отделы), и уже сейчас поддержка единой базы IT-приложений требует существенных трудозатрат (каталог самих приложений, их связей с бизне- функциями, связи между собой, roadmap каждого приложения и т.п.).


Ситуация становится более критичной с каждым новым релизом, так как большую часть функций старых систем мы сейчас активно переносим на микросервисный подход, и количество "компонентов" IT-ландшафта растет.


Соответственно, возникает вопрос как с этим всем жить?


  • Самое простое решение не документировать в центральном каталоге каждый микросервис. Такой вариант не является совсем уж неверным: сами микросервисы не настолько интересны для бизнес-анализа ландшафта, а группы микросервисов можно объединить в "приложения"/"системы"/"домены" и документировать уже приложения. У этого решения, естественно, есть очень приятный плюс "сокращение трудозатрат" поддержки EAM каталога. Но если каждая система в каталоге будет представлять собой "черный ящик", то теряется прозрачность работы и взаимодействия внутренних компонентов. Также каждая IT-система в рамках своих проектов должна изобретать способ документирования своего каталога микросервисных компонентов, потому что при разработке все равно необходим общей список всех микросервисов приложения.


  • Второе решение автоматизировать документирование микросервисов в каталоге IT-ландшафта. Вот с этим мы и поэкспериментируем.



Текущий ландшафт у нас описан в системе, построенной на платформе Alfabet. Сейчас мы активно анализируем EAM решение от компании LeanIX как потенциального преемника.


LeanIX относительно молодой продукт, поэтому с моей точки зрения, в нем пока мало "унаследовательности", и он достаточно гибок и обладает современными возможностями для интеграции, и что немаловажно, опять же по моему мнению, у него довольно удобный и приятный пользовательский интерфейс.
Даже Gartner в 2019 году назвал LeanIX одним из Visionaries.



Можно долго перечислять причины выбора именно LeanIX (мы действительно проводили RFI/RFP процесс), но не надо забывать что одним из основных инвесторов компании LeanIX является Deutsche Telekom Capital Partners, то есть мы сами заинтересованы в развитии продуктов LeanIX.


Разработчики системы LeanIX EAM изначально заложили в продукт возможность документирования микросервисов. Попытались они это сделать на основе подхода, предложенного проектом Pivio ( http://pivio.io/)


Сам по себе "Pivio-подход" работает следующим образом: рядом с исходным кодом компонента (микросервиса) размещается yaml файл с метаинформацией о компоненте, который
посредством Pivio Client загружается в Pivio Server.



Cервер хранит информацию в ElasticSearch и предоставляет API для ваших инструментов.
Также существует Pivio Webview c очень ограниченными возможностями поиска и визуализации каталога (видимо, это как раз та часть проекта Pivio, которую вы должны были сами расширять для своих потребностей).


Вызов Pivio Client может быть частью процесса сборки или деплоймента компонента.


Естественно, для загрузки документов в Pivio Client они должны быть определенного формата. (см. http://pivio.io/docs/)


Под спойлером пример такого документа:


Скрытый текст
id: next-generation-print-2342-2413-9189-1990name: Next Generation Print Serviceshort_name: NGPStype: serviceowner: Team Goldfingerdescription: Prints all kinds of things. Now with 3D printing support.vcsroot: git://git.vcs.local/UBPcontact: Auric Goldfingerlifecycle: productiontags:- Old- Demolinks:homepage: http://wiki.local/ubpbuildchain: http://ci.local/ubpapi_docs: http://docs.local/ubp-apiservice:provides:- description: REST APIservice_name: uber-bill-print-serviceprotocol: httpsport: 8443transport_protocol: tcppublic_dns:- api.demo-company.com- soap.demo-company.io- description: SOAP API (legacy)service_name: print-serviceprotocol: httpsport: 80transport_protocol: tcpdepends_on:internal:- service_name: print-servicewhy: need to print- service_name: gateway-service- short_name: NGPSport: 8719external:- target: https://api.superdealz.me:443transport_protocol: tcpvia: proxy-servicewhy: Need to sync data with it.- target: mqtt://192.xxx.xxx.xxx:5028transport_protocol: tcpwhy: Get the latest Dealz.

От общей архитектуры Pivio, система LeanIX переиспользовала только PivioClient и формат файла (LeanIX Metadata как раз и есть файл в формате Pivio). В качестве каталога микросервисов и визуализации (поиск, отчеты, диаграммы) используются возможности самого LeanIX EAM.



Практика


Вот эту схему интеграции мы и стали проверять на своем проекте. Проект достаточно крупный: 15+ команд разработки, 150+ микросервисов уже в эксплуатации. Нам повезло, метаинформацию о своих микросервисах мы уже хранили вместе с кодом (у нас multirepo схема git-репозиториев, и в каждом репозитории есть метаинформация о компоненте).


Формат нашей метаинформации (https://github.com/AOEpeople/vistecture) отличается от Pivio, но это все еще yaml файл и преобразовать его "на лету" в pivio формат не составило труда.
Информация об интерфейсах и зависимостях между компонентами у нас также была уже задокументирована в машиночитаемом формате (мы используем её для настройки API Gateway).
Для начала мы решили не копировать всю информацию из swagger в систему EAM (структуру данных, методы и т.п.), это осталось пока за рамками, но будет задачей на будущее.


В планах было все просто: встраиваем в наш CI/CD pipeline дополнительный шаг по загрузке метаинформации в LeanIX (конвертация, вызов Pivio Client, который доступен как в качестве jar файла, так и в виде docker контейнера). Выглядело в планах все отлично: декларативное описание компонента, полная автоматизация обновления информации в EAM.



Документация LeanIX по использованию Pivo Client тут: https://dev.leanix.net/docs/microservices-based-on-yaml-files


Но к середине первого дня работы к нам пришло осознание, что все будет не так просто:


  • Оказалось, что LeanIX поддерживает только часть возможностей Pivio-формата (не все атрибуты).
  • Что еще страшнее, оказалось, что реализация Pivio в LeanIX не поддерживает описание интерфейсов между микросервисами (мы не можем документировать зависимости между компонентами).
  • Естественно, наш способ документирования не включал в себя случай, когда мы выводим сервис из эксплуатации и его надо удалить (нет у нас пока pipeline на удаление сервиса). Да и Pivio Client не умеет удалять описание микросервисов из LeanIX.

Пункт 1. решился дополнительным вызовом REST интерфейса самого LeanIX. (см. https://eu.leanix.net/services/pathfinder/v1/docs/#/)


Пункт 2. решился использованием GraphQL API LeanIX.


  • позволяет создать сущность "Интерфейса" и связать его c провайдером и потребителем.

Скрытый текст

Creating Interface


mutation {   createFactSheet(validateOnly: false,     input: {name: service["name"], type:Interface},     patches: [{op: replace, path:"/description",value: service_url },         {op: replace, path:"/externalId",value:"externalId:service["name"]"}] ) {                 factSheet {                     id                     name                     displayName}        }}

Attach interface to provider


mutation{  upsertRelation(from:      {externalId: {type: externalId, id: service["name"] }},           to: {externalId: {type: externalId, id: service["provider"]}},            type: relInterfaceToProviderApplication) {                relation {                id            }      }}

  • Но graphQL API LeanIX не декларатильвен, значит, надо выяснять разницу между тем что есть в LeanIX и тем, что мы хотим создать/обновить.

Скрытый текст

Searching if Interface/Microservice already exist:


{allFactSheets(     filter: {externalIds: ["externalId/" + externalId]}        ) {        edges {            node {                id                name                }            }      }}

  • Также мы столкнулись с тем, что нельзя привязать интерфейс к микросервису, если сам микросервис еще не документирован, следовательно, это требует последовательного создания документации на провайдера, потребителя и интерфейсов между ними. Решили мы пока эту задачу "топорно": создаем документацию на все компоненты и только потом документируем интерфейсы и зависимости.

Пункт 3. пока решили оставить как ручной шаг.


В итоге получилась такая рабочая схема:


Тут крупнее


После этого можно считать, что процесс автоматизации у нас заработал, a значит, мы можем позволить себе поддерживать документацию о наших микросервисах в системе LeanIX без дополнительных трудозатрат со стороны команд разработки.


Что это нам дало:


  • полнотекстовый поиск по описаниям микросервисов и интерфейсов (в том числе по endpoints name).
  • информацию о версии компонента на продакшене, а также название команды, которая за компонент отвечает сейчас.
  • список потребителей каждого конкретного интерфейса.
  • автоматическая визуализация распределения микросвервисов по их областям (доменам) и командам разработчикам.

Тут крупнее


На схеме уже видно, что баланс разделения ответственности между командами в области домена order куда лучше, чем, например, в presales. В order большая часть работы выполняется всего двумя командами и затраты на коммуникации и интеграции значительно ниже, чем между 4-мя командами в presales.


  • Возможность полуавтоматически визуализировать потоки коммуникации.

    Тут крупнее
    На схеме процесс сбора бизнес метрик, который позволяет оценить прогресс наших бизнес-процессов.


  • Возможность визуализировать зависимости между сервисами.

    Тут крупнее
    Тут отражен "крайний" случай, когда на диаграмме видны все-все сервисы из нашей системы. Естественно, для анализа мы используем всегда "подмножество" сервисов или кросс-доменные взаимодействия.



Итоги и открытые вопросы:


  • Вопрос о том, нужно ли вообще документировать микросервисы в общем IT-ландшафте все еще остается открытым, так как пользы от такой детальной документации для главного пользователя EAM (Enterprise Architect / Business Architect) пока немного. Но мы показали, что такая подробная документация нам почти "ничего не стоит".
  • Автоматизация документирования отлично вписывается в pipeline CI/CD
  • Сама система LeanIX гибка и предоставляет широкие возможности для интеграции. Но это и ее минус, так как не все способы одинаково функциональны и требуется разобраться во всех них.
    • Интеграция с Pivio Client декларативная, выглядит как то, что надо, но не поддерживает описание интерфейсов и у нее нет логики по "удалению" документации из LeanIX.
    • В REST API легко делать простые операции, но любой поиск по критериями превращается в "танцы с бубном" на стороне клиента.
    • GraphQL API не всегда логичный (к этому, наверное, можно привыкнуть).
    • Integration API до него пока не добрались, потому что требуется разбираться с проприетарном форматом обмена данными (LDIF)
  • LeanIX молодая система, и в процессе проверки нашего подхода было найдено несколько дефектов. Надо отдать должное команде LeanIX: они исправляли проблемы быстро.
  • Нам имеет смысл пересмотреть какие API мы действительно хотим видеть в общем каталоге. Вероятно, что у нас будет три типа API:
    • internal в рамках подсистемы/домена;
    • system для поддержки процессов через несколько доменов;
    • enterprise уровень для интеграции внешних систем.

Cледующие шаги:


  • Хочется задокументировать swagger описания интерфейсов в той же системе (пока они у нас отдельно все собраны) или хотя бы просто проставить ссылки на наш API Developer Portal / Swagger UI.
  • Вероятно, придется настроить интеграцию через "Integration API", так как по описанию он наиболее полный с точки зрения автоматизации документирования.
  • После демонстрации нашего решения и результата компании LeanIX нам было предложено попробовать новый продукт "LeanIX Cloud Native Suite: Microservices Intelligence", который имеет метамодель более ориентированную на описание микросервисной архитектуры. Кроме вышеописанных интеграционных подходов также обещают прямую интеграцию с kubernets, что позволит снимать часть актуальной информации прямо с боевого окружения. Возможно, это станет темой следующей статьи.

Немного и полезных ссылок на прощание:


Подробнее..

Вначале был монолит как мы меняем нашу архитектуру, не мешая бизнесу

18.09.2020 16:04:09 | Автор: admin


Всем привет! Меня зовут Игорь Наразин, я тим-лид команды в направлении логистики Delivery Club. Хочу рассказать, как мы строим и трансформируем нашу архитектуру и как это влияет на наши процессы в разработке.

Сейчас Delivery Club (как и весь рынок фудтеха) растёт очень быстро, что порождает огромное количество вызовов для технической команды, которые можно обобщить двумя самыми важными критериями:

  • Нужно обеспечивать высокую стабильность и доступность всех частей платформы.
  • Одновременно с этим держать высокий темп разработки новых фич.

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

Но нам удаётся (пока) и то, и другое. О том, как мы это делаем, и пойдет речь далее.

Во-первых, я расскажу про нашу платформу: как мы её трансформируем с учетом постоянно растущих объемов данных, какие критерии предъявляем к нашим сервисами и с какими проблемами сталкиваемся на этом пути.

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

Начнём с платформы.

Вначале был монолит


Первые строчки кода Delivery Club были написаны 11 лет назад, и в лучших традициях жанра архитектура представляла собой монолит на PHP. Он в течение 7 лет всё больше и больше наполнялся функциональностью, пока не столкнулся с классическими проблемами монолитной архитектуры.

Поначалу он нас полностью устраивал: его было просто поддерживать, тестировать и деплоить. И с начальными нагрузками он справлялся без проблем. Но, как это обычно бывает, в какой-то момент мы достигли таких темпов роста, что наш монолит стал очень опасным узким местом:

  • любой отказ или проблема в монолите отразится на абсолютно всех наших процессах;
  • монолит жёстко завязан на определенный стек, который нельзя менять;
  • с учетом роста команды разработки вносить изменения становится сложно: высокая связанность компонентов не даёт быстро доставлять фичи;
  • монолит невозможно гибко масштабировать.

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

Наша основная проблема заключалась в размере кодовой базы монолита и слабой экспертизе команды в ней (платформу мы так и называем old). Конечно, сначала мы хотели просто взять и распилить монолит, чтобы полностью решить вопрос. Но очень быстро поняли, что на это уйдёт не один год, причем количество изменений, которые туда вносятся, не позволят этому закончиться никогда.

Поэтому мы пошли другим путём: оставили его как есть, а остальные сервисы решили строить вокруг монолита. Он продолжает быть основной точкой логики обработки заказов и мастером данных, но начинает стримить данные для других сервисов.

Экосистема


Как рассказывал Андрей Евсюков в статье про наши команды, у нас выделены главные направления по доменным областям: R&D, Logistics, Consumer, Vendor, Internal, Platform. В рамках этих направлений уже сосредоточены основные доменные области, с которыми работают сервисы: например, для Logistics это курьеры и заказы, а для Vendor рестораны и позиции.

Дальше нам нужно подняться на уровень выше и выстроить экосистему наших сервисов вокруг платформы: процессинг заказов находится в центре и является мастером данных, остальные сервисы строятся вокруг него. При этом нам важно сделать наши направления автономными: при отказе одной части, остальные продолжают функционировать.

При низких нагрузках выстроить нужную экосистему достаточно просто: наш процессинг обрабатывает и хранит данные, а сервисы направлений обращаются за ними по мере необходимости.

Низкие нагрузки, синхронные запросы, всё работает круто.

На первых этапах мы так и делали: большинство сервисов общались между собой синхронными HTTP-запросами. При определенной нагрузке это было позволительно, но чем больше рос проект и число сервисов, тем большей проблемой это становилось.


Высокие нагрузки, синхронные запросы: страдают все, даже пользователи абсолютно других доменов курьеры.

Сделать сервисы внутри направлений автономными ещё сложнее: например, возрастание нагрузки на логистику не должно влиять на остальные части системы. С любым количеством синхронных запросов это нерешаемая задача. Очевидно, что нужно было отказываться от синхронных запросов и переходить к асинхронному взаимодействию.

Шина данных


Таким образом, у нас получилась масса узких мест, где за данными мы обращались в синхронном режиме. Эти места были очень опасными с точки зрения роста нагрузки.

Вот пример. Кто хоть раз делал заказ через Delivery Club, знает, что после того, как курьер забрал заказ, становится видна карта. На ней можно отслеживать передвижение курьера в реальном времени. Для этой фичи задействовано несколько микросервисов, основные из них:

  • mobile-gateway, который является backend for frontend для мобильного приложения;
  • courier-tracker, который хранит логику получения и отдачи координат;
  • logistics-couriers, который хранит эти координаты. Они присылаются из мобильных приложений курьеров.



В первоначальной схеме это всё работало синхронно: запросы из мобильного приложения раз в минуту шли через mobile-gateway к сервису courier-tracker, который обращался к logistics-couriers и получал координаты. Конечно, в этой схеме было не всё так просто, но в итоге всё сводилось к простому выводу: чем больше у нас активных заказов, тем больше запросов на получение координат приходило в logistics-couriers.

Рост у нас порой непредсказуемый и, что главное, быстрый вопрос времени, когда такая схема откажет. Значит, нужно переделать процесс на асинхронное взаимодействие: сделать так, чтобы запрос координат был максимально дешевым. Для этого необходимо преобразовать наши потоки данных.

Транспорт


Мы уже использовали RabbitMQ, в том числе для общения между сервисами. Но в качестве основного вида транспорта мы остановились на уже хорошо зарекомендовавшем себя инструменте Apache Kafka. Про него мы напишем отдельную подробную статью, а сейчас я бы хотел кратко рассказать о том, как мы его используем.

Когда мы только начали внедрять Kafka в качестве транспорта, то использовали его в сыром виде, подключаясь напрямую к брокерам и отправляя в них сообщения. Этот подход позволил нам быстро проверить Kafka в боевых условиях и решить, использовать ли далее как основной вид транспорта.

Но такой подход имеет существенный недостаток: у сообщений нет никакой типизации и валидации мы не знаем наверняка, какой формат сообщений читаем из топика.

Из-за этого увеличивается риск ошибок и несогласованностей между сервисами, которые являются поставщиками данных, и теми, кто их потребляет.

Для решения этой проблемы мы написали обёртку микросервис на Go, который скрыл Kafka за своим API. Это добавило два преимущества:

  • валидация данных в момент отправки и приёма. По сути, это одни и те же DTO, поэтому мы всегда уверены в формате ожидаемых данных.
  • быстрая интеграция наших сервисов с этим транспортом.

Таким образом, работа с Kafka стала максимально абстрагированной для наших сервисов: они лишь работают с верхнеуровневым API этой обёртки.

Вернёмся к примеру


Переводя синхронное взаимодействие на шину событий, нам необходимо инвертировать поток данных: то, за чем мы обращались, должно теперь само попадать к нам через Kafka. В примере речь идёт о координатах курьера, для которых теперь мы заведём специальный топик и будем продюсить их по мере получения от курьеров сервисом logistics-couriers.

Сервису courier-tracker остаётся аккумулировать координаты в нужном объёме и на нужный срок. В итоге наш эндпоинт становится максимально простым: взять данные из базы сервиса и отдать их мобильному приложению. Рост нагрузки на неё теперь для нас безопасен.



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

Eventually consistency


В этом примере всё работает круто, за исключением того, что координаты курьеров будут не всегда актуальными по сравнению с синхронным вариантом: в архитектуре, построенной на асинхронном взаимодействии, встаёт вопрос об актуальности данных в каждый момент времени. Но у нас не так много критичных данных, которые нужны держать всегда свежими, поэтому нам эта схема идеально подходит: мы жертвуем актуальностью какой-то информации ради увеличения уровня доступности системы. Но мы гарантируем, что в конечном счёте во всех частях системы все данные будут актуальны и консистентны (eventually consistency).

Такая денормализация данных необходима, когда речь идёт о высоконагруженной системе и микросервисной архитектуре: каждый сервис сам обеспечивает сохранение тех данных, которые нужны ему для работы. Например, одна из главных сущностей нашего домена это курьер. Ею оперирует множество сервисов, но всем им нужен разный набор данных: кому-то нужны личные данные, а кому-то только информация о типе передвижения. Мастер данных этого домена продюсит всю сущность в стрим, а сервисы аккумулируют нужные части:



Таким образом, мы четко разделяем наши сервисы на те, что являются мастерами данных и те, кто использует эти данные. По сути, это headless commerce из evolutionary archicture у нас четко отделены все витрины (сайт, мобильные приложения) от производителей этих данных.

Денормализация


Ещё пример: у нас есть механизм таргетированных уведомлений курьерам это сообщения, которые придут им в приложение. На стороне бэкенда есть мощное API для отправки таких уведомлений. В нём можно настраивать фильтры рассылки: от конкретного курьера до групп курьеров по определённым признакам.

За эти уведомления отвечает сервис logistics-courier-notifications. После того, как он получил запрос на отправку, его задача сгенерировать сообщения для тех курьеров, которые попали в таргетинг. Для этого ему необходимо знать нужную информацию по всем курьерам Delivery Club. И у нас есть два варианта для решения этой задачи:

  • сделать эндпоинт на стороне сервиса мастера данных по курьерам (logistics-couriers), который по переданным полям сможет отфильтровать и вернуть нужных курьеров;
  • хранить всю нужную информацию прямо в сервисе, потребляя её из соответствующего топика и сохраняя те данные, по которым нам в дальнейшем нужно будет фильтровать.

Часть логики генерации сообщений и фильтрования курьеров не является нагруженной, она выполняется в фоне, поэтому вопроса о нагрузках на сервис logistics-couriers не стоит. Но если выбрать первый вариант, мы столкнёмся с набором проблем:

  • придётся поддерживать узкоспециализированный эндпоинт в стороннем сервисе, который, скорее всего, понадобится только нам;
  • если выбрать слишком широкий фильтр, то в выборку попадут вообще все курьеры, которые просто не поместятся в HTTP-ответ, и придётся реализовывать пагинацию (и итерировать по ней при опросе сервиса).

Очевидно, что мы остановились на хранении данных в самом сервисе. Он автономно и изолированно выполняет всю работу, никуда не обращаясь, а только аккумулируя все нужные данные у себя из топика Kafka. Есть риск, что мы получим сообщение о создании нового курьера позднее, и он не попадёт в какую-то выборку. Но этот недостаток асинхронной архитектуры неизбежен.

В итоге у нас сформулированы несколько важных принципов к проектированию сервисов:

  • У сервиса должна быть конкретная ответственность. Если для его полноценного функционирования нужен ещё сервис, то это ошибка проектирования, их нужно либо объединять, либо пересматривать архитектуру.
  • Критично смотрим на любые синхронные обращения. Для сервисов в одном направлении это допустимо, но для общения между сервисами разных направлений нет
  • Share nothing. Мы не ходим в БД сервисов в обход них самих. Все запросы только через API.
  • Specification First. Сначала описываем и утверждаем протоколы.

Таким образом, итеративно трансформируя нашу систему согласно принятым принципам и подходам, мы пришли к такой архитектуре:



У нас уже есть шина данных в виде Kafka, которая уже имеет существенное количество потоков данных, но всё ещё остаются синхронные запросы между направлениями.

Как мы планируем развивать нашу архитектуру


Delivery club, как я говорил вначале, быстро растёт, мы релизим в прод огромное количество новых фич. А ещё больше экспериментируем (подробно об этом рассказал Николай Архипов) и тестируем гипотезы. Это всё порождает огромное количество источников данных и ещё больше вариантов их использования. А правильное управление потоками данных, которые очень важно грамотно выстроить это и есть наша задача.

Дальше мы будем продолжать внедрять выработанные подходы во все сервисы Delivery Club: строить экосистемы сервисов вокруг платформы с транспортом в виде шины данных.

Первоочередная задача добиться того, чтобы информация по всем доменам системы поставлялась в шину данных. Для новых сервисов с новыми данными это не проблема: на этапе подготовки сервиса он будет обязан стримить данные своего домена в Kafka.

Но кроме новых у нас есть большие legacy-сервисы с данными по основным нашим доменам: заказам и курьерам. Реализовать стриминг этих данных как есть проблематично, так как они хранятся размазанными по десяткам таблиц, и каждый раз строить конечную сущность для продюсинга всех изменений будет очень накладно.

Поэтому для старых сервисов мы решили использовать Debezium, который позволяет стримить информацию напрямую из таблиц на основе bin-log: в итоге получается готовый топик с сырыми данными из таблицы. Но они непригодны для использования в исходном виде, поэтому через трансформеры на уровне Kafka они будут преобразованы в понятный для потребителей формат и запушены в новый топик. Таким образом, у нас будет набор приватных топиков с сырыми данными из таблиц, который будет трансформироваться в удобный формат и транслироваться в публичный топик для использования потребителями.



Получится несколько точек входа на запись в Kafka и разные типы топиков, поэтому дальше мы реализуем на стороне хранилища права доступа по ролям и добавим валидацию схем на стороне шины данных через Confluent.

Дальше из шины данных сервисы будут потреблять данные из нужных топиков. И мы сами будем использовать эти данные для своих систем: например, стримить через Kafka Connect в ElasticSearch или в DWH. С последним процесс будет сложнее: чтобы информация в нём была доступна для всех, её необходимо очистить от любых персональных данных.

Также нужно окончательно решить вопрос с монолитом: там ещё остаются критичные процессы, которые мы в ближайшее время будем выносить. Совсем недавно мы уже выкатили отдельный сервис, который занимается первой стадией создания заказа: формированием корзины, чека и оплаты. Дальше он отправляет эти данные в монолит для дальнейшей обработки. Ну а все остальные операции уже не требуют синхронности.

Как заниматься таким рефакторингом прозрачно для клиентов


Расскажу на ещё одном примере: каталоге ресторанов. Очевидно, что это очень нагруженное место, и мы решили выносить его в отдельный сервис на Go. Для ускорения разработки мы поделили вынос на два этапа:

  1. Сначала внутри сервиса ходим напрямую в реплику базы нашего монолита и получаем оттуда данные.
  2. Затем начинаем стримить нужные нам данные через Debezium и аккумулировать в базе самого сервиса.



Когда сервис готов, встаёт вопрос о том, как его прозрачно встроить в текущий рабочий процесс. Мы использовали схему со сплитом трафика: с клиентов весь трафик шёл в сервис mobile-gateway, а дальше делились между монолитом и новым сервисом. Изначально мы продолжали весь трафик обрабатывать через монолит, но часть дублировали в новый сервис, сравнивали их ответы и записывали логи о расхождениях в наши метрики. Этим мы обеспечили прозрачность тестирования сервиса в боевых условиях. После этого оставалось только постепенно переключать и наращивать на нём трафик, пока новый сервис полностью не вытеснит монолит.

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

Вместе со всеми этими концептуальными изменениями у нас продолжается активная разработка и поставка фич в прод, на которую уходит основное время. Тут мы подходим ко второй проблеме, про которую я говорил вначале: с учетом численности разработчиков (180 человек) остро встает вопрос валидации архитектуры и качества новых сервисов. Новое не должно деградировать систему, оно должно изначально встраиваться правильно. Но как при промышленных масштабах это контролировать?

Архитектурный комитет


Необходимость в нём возникла не сразу. Когда команда разработки была небольшая, любые изменения в системе было легко контролировать. Но чем больше людей, тем сложнее это делать.

Это порождает как реальные проблемы (сервис не выдерживал нагрузки из-за неправильного проектирования), так и концептуальные (а давайте здесь ходить синхронно, нагрузка же маленькая).

Понятно, что большинство вопросов решается на уровне команды. Но если речь идёт о какой-то сложной интеграции в текущую систему, то тогда у команды может просто не хватить экспертизы. Поэтому хотелось создать какое-то объединение людей со всех направлений, к которому можно было бы прийти с любым вопросом про архитектуру и получить исчерпывающий ответ.

Так мы пришли к созданию архитектурного комитета, куда входят тимлиды, руководители направлений и CTO. Мы собираемся раз в две недели и обсуждаем планирующиеся крупные изменения в системе или просто решаем конкретные вопросы.

В итоге, проблему с контролем крупных изменений мы закрыли, остаётся вопрос общего подхода к качеству кода в Delivery Club: конкретные проблемы кода или фреймворка в разных командах могут решаться по-разному. Мы пришли к гильдиям по модели Spotify: это объединения неравнодушных к какой-то технологии людей. Например, есть гильдии Go, PHP и Frontend.

Они вырабатывают единые стили программирования, подходы к проектированию и архитектуре, помогают формировать и поддерживать инженерную культуру на высшем уровне. Также у них есть свой собственный бэклог, в рамках которого они улучшают внутренние инструменты, например, наш Go-шаблон для микросервисов.

Код на прод


Кроме того, что крупные изменения проходят через архитектурный комитет, а за культурой кода в целом следят гильдии, у нас ещё есть важный этап подготовки сервиса к продакшену: составление чек-листа в Confluence. Во-первых, составляя чек-лист, разработчик ещё раз оценивает свое решение; во-вторых, это требование эксплуатации, так как им нужно понимать, что за новый сервис появляется в продакшене.

В чек-листе обычно указывается:

  • ответственный за сервис (это обычно тех-лид сервиса);
  • ссылки на дашборд с настроенными алертами;
  • описание сервиса и ссылка на Swagger;
  • описание сервисов, с которым будет взаимодействовать;
  • предполагаемая нагрузка на сервис;
  • ссылка на health-check. Это URL, по которому служба эксплуатации настраивает свои мониторинги. Health-check раз в какой-то период дёргается: если вдруг он не ответил с кодом 200, значит, с сервисом что-то не так и к нам прилетает алерт. В свою очередь, health check может дёргать такие же URLы критичных для него сервисов, а также обязательно включать проверку всех компонентов сервиса, например, PostgreSQL или Redis.

Алерты сервиса проектируются ещё на этапе архитектурного согласования. Важно, чтобы разработчик понимал, что сервис живой, и учитывал не только технические метрики, но и продуктовые. Здесь имеются в виду не какие-то бизнесовые конверсии, а метрики, которые показывают, что сервис работает как надо.

Для примера можно взять уже рассмотренный выше сервис courier-tracker, отслеживающий курьеров на карте. Одна из основных метрик в нём это количество курьеров, у которых обновляются координаты. Если вдруг какие-то маршруты долго не обновляются, приходит алерт что-то пошло не так. Может быть, где-то за данными не сходили, или неправильно зашли в базу, или отвалился какой-нибудь другой сервис. Это не техническая метрика и не продуктовая, но она показывает жизнеспособность сервиса.

Для метрик мы используем Graylog и Prometheus, строим дашборды и настраиваем алерты в Grafana.

Несмотря на объём подготовки, доставка сервисов в прод достаточно быстрая: все сервисы изначально упакованы в Docker, в stage выкатываются автоматически после формирования типизированного чарта для Kubernetes, а дальше всё решается кнопкой в Jenkins.

Выкатка нового сервиса в прод заключается в назначении задачи на админов в Jira, в которой предоставляется вся информация, которую мы подготовили ранее.

Под капотом


Сейчас у нас 162 микросервиса, написанные на PHP и Go. Они распределились между сервисами примерно 50% на 50%. Изначально мы переписали на Go некоторые высоконагруженные сервисы. Дальше стало ясно, что Go проще в поддержке и мониторинге в продакшене, у него низкий порог входа, поэтому в последнее время мы пишем сервисы только на нём. Цели переписать на Go оставшиеся PHP-сервисы нет: он вполне успешно справляется со своими функциями.

В PHP-сервисах у нас Symfony, поверх которого мы используем свой небольшой фреймворк. Он навязывает сервисам общую архитектуру, благодаря которой мы снижаем порог входа в исходный код сервисов: какой бы сервис вы ни открыли, всегда будет понятно, что и где в нём лежит. А также фреймворк инкапсулирует слой транспорта общения между сервисами, для разработчика запрос в сторонний сервис выглядит на высоком уровне абстракции:
$courierResponse = $this->courierProtocol->get($courierRequest);
Здесь мы формируем DTO запроса ($courierRequest), вызываем метод объекта протокола конкретного сервиса, который является обёрткой над конкретным эндпоинтом. Под капотом наш объект $courierRequest преобразуется в объект запроса, который заполняется полями из DTO. Это всё гибко настраивается: поля могут подставляться как в заголовки, так и в сам URL запроса. Далее запрос посылается через cURL, получаем объект Response и обратно его трансформируем в ожидаемый нами объект $courierResponse.

Благодаря этому разработчики сосредоточены на бизнес-логике, без подробностей взаимодействия на низком уровне. Объекты протоколов, запросов и ответов сервисов лежат в отдельном репозитории SDK этого сервиса. Благодаря этому, любой сервис, который захочет использовать его протоколы, получит весь типизированный пакет протоколов после импорта SDK.

Но у этого процесса большой недостаток: репозитории с SDK сложно поддерживать, потому что все DTO пишутся вручную, а удобную кодогенерацию сделать непросто: попытки были, но в конце-концов, с учётом перехода на Go, в это не стали вкладывать время.

В итоге, изменения в протоколе сервиса могут превратиться в несколько пулл-реквестов: в сам сервис, в его SDK, и в сервис, которому нужен этот протокол. В последнем нам нужно поднять версию импортированного SDK, чтобы туда попали изменения. Это часто вызывает вопросы у новых разработчиков: Я ведь только изменил параметр, почему мне нужно делать три реквеста в три разных репозитория?!

В Go всё сильно проще: у нас есть отличный генератор кода (Сергей Попов написал об этом подробную статью), благодаря которому весь протокол типизирован, и сейчас даже обсуждается вариант с хранением всех спецификаций в отдельном репозитории. Таким образом, если кто-то меняет спеку, все зависящие от неё сервисы сразу начнут использовать обновлённый вариант.

Технический радар




Кроме уже упомянутых Go и PHP мы используем огромное количество других технологий. Они варьируются от направления к направлению и зависят от конкретных задач. В основном, на бэкенде у нас используются:

  • Python, на котором пишет команда Data Science.
  • Kotlin и Swift для разработки мобильных приложений.
  • PostgreSQL в качестве базы данных, но на некоторых старых сервисах всё ещё крутится MySQL. В микросервисах используем несколько подходов: для каждого сервиса своя БД и share nothing мы не ходим в базы данных в обход сервисов, только через их API.
  • ClickHouse для узкоспециализированных сервисов, связанных с аналитикой.
  • Redis и Memcached в качестве in-memory хранилищ.


При выборе технологии мы руководствуемся специальными принципами. Одним из основных требований является Ease of use: используем максимально простую и понятную технологию для разработчика, по возможности придерживаясь принятого стека. Для тех, кто хочет узнать весь стек конкретных технологий, у нас составлен очень подробный техрадар.

Long story short


В итоге от монолитной архитектуры мы перешли к микросервисной, и сейчас уже имеем группы сервисов, объединенных по направлениям (доменным областям) вокруг платформы, которая является ядром и мастером данных.

У нас есть видение, как реорганизовать наши потоки данных и как это делать без влияния на скорость разработки новых фич. В будущем мы обязательно расскажем о том, куда нас это привело.

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

На этом у меня всё, спасибо, что дочитали!
Подробнее..

Open Architecture Meetup 311

28.10.2020 14:12:50 | Автор: admin
Приглашаем вас обсуждать актуальное микросервисы. Встречаемся на онлайн-митапе 3 ноября, где вместе со спикерами ответим на вопросы: как вынести части, которые можно переиспользовать, и отдать другим командам, и как микросервисная архитектура может помочь развитию сотрудников внутри компании?

Присоединяйтесь к нам!



О чём поговорим


Микрофронтенд или Как всё разбить на маленькие кусочки и собрать вместе


Дмитрий Григоров, Газпромбанк

О спикере: Тимлид команды интернет-банка, девелопер, спикер и просто хороший специалист по реакту. Более 5 лет в разработке и продвижении продуктов.

О докладе: Микросервисы и архитектура самые популярные направления, куда копают компании в последнее время. Когда проект только зарождается, никто не задумывается о том, что он может вырасти в один огромный монолит, который будет трудно поддерживать, и в нём невозможно разделить разработку на несколько команд.

И остаётся один вопрос: Как можно вынести части, которые можно переиспользовать и отдать другим командам? Об этом мы и поговорим.

Как микросервисная архитектура помогает развитию сотрудников внутри компании


Сергей Огородников, Райффайзенбанк

О спикере: Профессионально разрабатывает на C# с 2005 года, пришёл в.Net за пару месяцев до того, как подвезли generics. Сейчас работает старшим разработчиком в Райффайзенбанке в команде, занимающейся разработкой продуктов для HR. Интересуется DDD, software architecture, разработкой анализаторов кода, немного ФП.

О докладе: В докладе рассматривается архитектура внутреннего портала для сотрудников и менеджеров, в котором можно найти различную информацию и услуги HR. Исторически сложилось, что на портале реализованы довольно разные бизнес-процессы, поддерживаемые разными командами, использующими разные технологии. Расскажем, как микросервисная архитектура на бэке и фронте позволяет всем вместе работать на общий результат.

Начнем митап в 19:00 (МСК)

Регистрируйтесь, чтобы получить ссылку на трансляцию: письмо придет вам на почту.

До встречи онлайн!
Подробнее..

Перевод Наш путь к нулевым простоям при непрерывном обновлении с помощью Ambassador

24.10.2020 08:05:17 | Автор: admin


Мы в Lifion строим распределенную платформу и портфель продуктов для клиентов по всему миру. С учетом этого важно, чтобы мы могли выпускать обновления нашей платформы непрерывно прямо во время ее работы, прозрачно для наших пользователей, которым важна доступность системы, при этом они находятся в разных регионах и часовых поясах. В этой статье мы поделимся с вами путем, которым мы шли, чтобы получить нулевые простои при непрерывном обновлении с помощью Kubernetes. Мы запускаем наши нагрузки с помощью управляемого сервиса Kubernetes AWS EKS. В качестве шлюза API мы применяем Ambassador, сборку Envoy с открытым исходным кодом, специально разработанную для Kubernetes. Наша платформа состоит из более чем 150 микросервисов, большинство из них написаны на Node.js, запускаются в многих подах поверх многочисленных рабочих узлов.


Состояние проблемы


Поскольку мы разворачиваем наше решение NextGen HGM для все большего числа клиентов, и требования к платформе растут, мы хотим в дальнейшем обеспечивать надежность нашей нижележащей инфраструктуры. Однако проблема, которую мы обнаружили при проведении непрерывного обновления некоторых наших сервисов в кластере Kubernetes, заключается в том, что поды отключаются сразу же, как только сервисы переходят в состояние Terminating, вместо мягкой остановки, как мы предполагали. Это привело к сбоям некоторых запросов во время таких обновлений, что нежелательно, а также потенциально может повлиять на работу пользователей.


Главная причина (Нет обработки SIGTERM)


Среда запуска контейнеров в некоторой части жизненного цикла пода Kubernetes отправляет сигнал SIGTERM в старый под во время обновления, к примеру, когда новый под уже запущен и может принимать запросы для обслуживания. Проведя исследование, мы поняли, что часть наших команд инженеров запускали сервисы на Node.js в подах под PID1, так что SIGTERM явно не обрабатывался на уровне кода, несмотря на рекомендации:


Node.js не разработан для запуска под PID1, это может привести к неопределенному поведению при запуске внутри контейнера Docker. Например, процесс Node.js, запущенный под PID1 не будет отвечать на SIGINT (CTRL-C) и другие подобные сигналы.

Это значит, что во время обновления, когда поды получают сигнал SIGTERM, они ждут 30 секунд (промежуток времени для мягкого завершения), а затем им присылается SIGKILL, так что процессы с PID1 резко выключаются с кодом возврата 137. Это приводит к сбору существующих обрабатываемых в этот момент запросов к поду, на которые не будет дан ответ.



Решение (первая попытка)


В нашу библиотеку Node.js, поверх которой строятся наши микросервисы, мы добавили типовую обработку сигнала SIGTERM, который останавливает активное обслуживание запросов после некоторой задержки. Это значит, что оставшимся запросам дается некоторое время для завершения, прежде чем они будут закрыты и их соединения TCP с все еще активными keep-alive также будут завершены. Ну и наконец сервис останавливается.


...process.on('SIGINT', () => {    logger.warn('SIGINT received. Shutting down');    process.exit(0);  });  process.on('SIGTERM', () => {    logger.warn('SIGTERM received. Initiating graceful shutdown');    shutdown()      .catch((err) => {        logger.error('Ignoring error during graceful shutdown:', err);      })      .finally(() => {        logger.warn('Graceful shutdown completed.');        process.exit(0);      });?  });....module.exports = Object.assign(exposed, {  _listen: exposed.listen,  config,  listen: start,  listenAsync: startAsync,  reset,  setAfterShutdown: (fn) => {    hooks.afterShutdown = fn;  },  setBeforeShutdown: (fn) => {    hooks.beforeShutdown = fn;  },  start,  startAsync,  stop,  stopAsync: promisify(stop)});

Еще один барьер (получение запросов после SIGTERM)


После подключения обработки SIGTERM в нашей основной библиотеке мы получили еще одну проблему: один из инженеров сказал нам, что по факту его сервис все еще получал некоторые запросы после получения SIGTERM во время обновления. А если верить соглашению жизненного цикла пода в Kubernetes, как только под получил SIGTERM, он удаляется из endpoints сервиса, а значит, и из балансировщика нагрузки. Однако если под удаляется из кластера через API, то он только помечается для удаления на сервере метаданных. Сервер уже в свою очередь отправляет уведомление об удалении пода всем связанным подсистемам, обрабатывающим запрос на удаление:


  • kubelet, запускающий последовательности запуска и остановки пода
  • сервис kube-proxy, удаляющий на всех узлах ip-адрес пода в iptables
  • контроллер endpoints удаляет под из списка корректных endpoints, что приводит к удалению пода из Service

Мы можем воспроизвести эту проблему следующим кусочком кода:


function readyCheck() {    let ready = true;process.on('SIGTERM', () => {      logger.info('SIGTERM received, making service as no longer ready');      ready = false;    });return (req, res) => {      if (ready) {        logger.info('Successful ready check');        res.sendStatus(200);      } else {        res.sendStatus(503);        logger.info('Signaling ready check failure');      }    };  }....router.get('/ready', readyCheck());

Во время исследований мы нашли ключевую причину этой проблемы, она была связана с нашим шлюзом API Ambassador/Envoy. Ambassador оборачивает для Envoy, создавая ему настройки. Команда Ambassador помогла нам в наших исследованиях, их официальный ответ от техподдержки был таким:


Envoy старается быть максимально эффективным при поддержке открытых соединений к вышестоящим сервисам. Поскольку фактические endpoints меняются, эти долгоживущие соединения все еще могут быть связаны с старым подом, который будет удален через некоторое короткое время. Проблема в том, что существует некая задержка между Kubernetes, отправляющим SIGTERM для отключения пода, и Ambassador, удаляющим под из списка endpoints. Стоит отметить, что проблема существует и в kube-proxy, если вы с ним работаете в больших масштабах.

Проще говоря, из-за оптимизации Envoy некоторые сервисы будут получать запросы после того, как они получили SIGTERM, что приведет к потере текущих запросов (запрос пришел до DIGTERM, но еще не обработан) во время обновления этого сервиса.


Решение (вторая попытка)


Envoy нужно некоторое время для остановки существующих соединений на время остановки пода до получения подом SIGTERM. Так что в качестве решения мы добавили поддержку preStopHook в Helm Chart для нашего сервиса:


lifecycle:  preStop:    exec:      command:      - sleep      - "10"

Во время обновления процесс остановки старого пода ждет 10 секунд, чтобы дать возможность Envoy отключить все существующие соединения к поду, а также удостовериться, что под убран из endpoints балансировщика нагрузки. Сразу же после этого Kubernetes, как обычно, отправляет сигнал SIGTERM процессу с PID1.


Для гибкости мы расширили это изменение, так что команды инженеров могут также переопределять такое поведение по умолчанию, если это им понадобится. Например, у нас есть несколько сервисов, использующих NGINX для раздачи статического контента из пода, для них мы решили проблему следующими изменениями в helm chart:


lifecycle:  preStop:    exec:      command: [        "sh", "-c",        # Introduce a delay to the shutdown sequence to wait for the        # pod eviction event to propagate. Then, gracefully shutdown        # nginx.        "sleep 5 && /usr/sbin/nginx -s quit",      ]

Выводы


Если сложить все вместе, то эта статья покрывает:


  • Принципы обработки жизненного цикла Kubernetes, используемые для мягкого и безопасного выключения приложений, при котором они не будут терять текущие запросы.
  • Нюансы, с которыми поды удаляются из системы. Важность понимания и особенности обработки соответствующей процедуры выключения, особенно в крупных масштабах с применением Ambassador.

Если все эти функции применять вместе, мы можем достичь нашей цели: нулевого простоя при непрерывном обновлении сервисов.


N.B. Для тех, кому нужна практика построения систем с надежной архитектурой, готовых к перегрузкам, Слёрм проводит онлайн-интенсив по SRE. Интенсив пройдет 1113 декабря 2020, до начала декабря можно купить билет со скидкой.

Подробнее..

Отложенные задачи в рамках микро-сервисной архитектуры

13.02.2021 20:21:59 | Автор: admin

Часто в проектах возникает необходимость выполнения отложенных задач, таких как отправка email, push и других специфических задач, свойственных доменной области вашего приложения. Сложности начинаются, когда обычного crontab уже недостаточно, когда пакетная обработка не подходит и когда у каждой единицы задачи свое время выполнения или оно назначается динамически.

Для решения такой задачи было создано очередное решение под названием Trigger Hook. Принципиальная схема работы показана на рисунке 1. На схеме показано, что происходит с заданиями в течения всего их жизненного цикла. Смена цвета означает смену статуса задачи.

Рисунок 1 - Принципиальная схема работы Trigger HookРисунок 1 - Принципиальная схема работы Trigger Hook

задача, время запуска которой наступит не скоро

задача, время запуска которой скоро наступит

задача, время запуска которой наступило

обработанное задание

неподтвержденный статус задания в базе данных

команда на удаление

Жизненный цикл задачи:

  • При создании задачи она попадает в базу данных (квадратный блок) (красные и желтые).

  • В оперативную память подгружаются задачи (треугольный блок), если их время запуска скоро наступит (переход красный->желтый). Данная структура реализована в виде приоритезированной очереди (кучи).

  • При наступлении времени выполнения задачи, она посылается на выполнение (переход желтый->зеленый). Используется промежуточный буфер перед обработкой для компенсации пиковых нагрузок.

  • В случае успешной отправки задачи, она удаляется из базы данных (переход зеленый->голубой->удаление). Используется промежуточный буфер перед удалением, также для компенсации пиковых нагрузок.

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

Простота API

Id принимается в формате UUIDv4. Если не передать, то будет сгенерирован самостоятельно. Возможность передачи id задачи со стороны внешнего сервиса будет полезна при использовании асинхронного канала. Время запуска указывается в формате UNIX.

Создание:

task := &domain.Task{Id:     id,ExecTime: time,}triggerHook.Create(task)

Удаление:

triggerHook.Delete(task.Id)

Получение событий наступления времени запуска:

for {result := triggerHook.Consume()if err != send(result.Task()) {result.Rollback()}result.Confirm()}

Стойкость к сбоям

При обработке задач может произойти сбой, например, если потеряно соединение с брокером сообщений. В таком случае выполнение задачи подтверждено не будет, а будет проведена повторная попытка отправки. Задача отметится как обработанная только при вызове метода подтверждения. Внезапная остановка приложения не приведет к несогласованности в базе данных.

Кроме того, с учетом общей тенденции рынка к переносу приложений в облако в виде микро-сервисов формируются новые требования к приложению. По крайней мере то, что выходило на задний план ранее, сейчас становится более важным. При этом подходе контейнеризированные приложения имеют временную природу. Механизм Trigger Hook делает возможным сворачивание микро-сервиса на одном сервере и разворачивание на другом в производственной среде без мягкой остановки.

В случае аварийного завершения приложения, есть вероятность, что выполнение некоторых задач может быть не подтверждено в базе данных. При повторном запуске приложения эти задачи будут отправлены на выполнение повторно. Такое поведение является компромиссом в пользу обеспечения стойкости к сбоям. Получив сообщение от Trigger Hook, Ваше приложение должно выполнять задачу только один раз, а при повторном получении - игнорировать. Такие ситуации обычное явление в событийно ориентированных архитектурах и они не должны нарушать внутреннее состояние и генерировать большое количество ошибок.

Точность и производительность

Для избежания высокой частоты запросов в базу данных предусмотрен механизм периодической предзагрузки наборов задач, время выполнения которых находится в заданном диапазоне. Другими словами, делаются редкие запросы наборов задач вместо частых запросов задач по одной. Такая схема хорошо подходит, если например, на одно время назначено выполнение нескольких сотен тысяч задач. После загрузки задач они сортируются в порядке приоритета. Когда время таймера для самой приоритетной задачи истекает, то она сразу же поступает на обработку клиентскому коду. Это позволяет добиться большой пиковой производительности и обработки задач с секундной точностью.

Также, большая производительность отправки задач на выполнение достигается за счет простой схемы хранения задач, индексирования и много-поточного доступа к базе данных.

Были измерены основные показатели скорости обработки задач.

Сервер приложения:

  • AWS EC2 Ubuntu 20

  • t2.micro

  • 1 vCPUs 2.5 GHz

  • 1 GiB RAM

Сервер базы данных:

  • AWS RDS MySQL 8.0

  • db.t3.micro

  • 2 vCPUs

  • 1 GiB RAM

  • Network: 2085 Mbps

Тест

Длительность теста

Средняя скорость (задач/сек)

Количество задач

Создание задач

1 минута 11 сек

1396

100000

Удаление задач

52 сек

1920

100000

Отправка задач (состояние задачи от красной до голубой)

498 милисекунд

200668

100000

Подтверждение задач (состояние задачи от голубой до удаления)

2 сек

49905

100000

Мониторинг

Для быстрой проверки корректной работы Trigger Hook предоставляет возможность подключить time-series базу данных. На этапе инициализации есть возможность определить периодичность измерений и выбрать интересующие метрики. Полный список доступных метрик есть тут.

Также есть возможность подключить систему логирования через адаптер. Доступны:

  • фатальные ошибки - приводящие к полной остановке приложения

  • ошибки на которые стоит обратить внимание, но которые не приводят к остановке

  • дебаг сообщения

Далее в примере Вы можете увидеть пример подключения к InfluxDB+Grafana

Trigger Hook в составе микро-сервисной архитектуры

Асинхронное взаимодействие

При использовании микро-сервисной архитектуры, обычно, предпочтение отдают асинхронному взаимодействию. Trigger Hook хорошо вписывается в микро-сервисную, событийно ориентированную архитектуру. Но в любом случае, входящие (создание, удаление) и исходящее (событие наступления времени запуска задачи) каналы могут быть как асинхронными, так и синхронными в зависимости от требований.

Ниже, на рисунке 2 приведен один из возможных вариантов схемы коммуникации через асинхронный канал. В качестве брокера сообщений может выступать какая-нибудь очередь, например, RabbitMQ. Эта схема исключает блокировку вызываемого микро-сервиса вызывающим, как при синхронном запросе посредством, например HTTP. Брокер принимает неограниченное количество задач (условно неограниченное), а обработчик этих задач берется за них по мере освобождения. Как только команда на создание будет обработана, отправляется событие об успешном создании задачи. Так же через брокер, клиентский сервис получает это событие и реагирует на него соответствующим образом - меняет статус сущности, использующей отложенное задание. В качестве сущности может выступать, например Push уведомление на мобильные устройства с рекламой.

Существенным недостатком данной схемы является усложнение инфраструктуры обслуживающей подобное взаимодействие. По сути, введение статусов ожидания ответа от других микро-сервисов это есть распределенные транзакции.

Рисунок 2 - Схема коммуникации через асинхронный каналРисунок 2 - Схема коммуникации через асинхронный канал

На рисунке 3 показаны процессы создания сущности имеющий отложенное выполнение и на рисунке4 выполнение при наступлении времени.

Рисунок 3 - Процесс создания сущности с отложенным выполнениемРисунок 3 - Процесс создания сущности с отложенным выполнениемРисунок 4 - Выполнение задания сущностиРисунок 4 - Выполнение задания сущности

Совместный доступ

Отсутствие возможности передачи некоторой полезной нагрузки при создании задачи может некоторых разочаровать. Но уверяю, в этом нет необходимости. Trigger Hook содержит достаточный функционал для построения менеджера задач. Относитесь к Trigger Hook как к слою абстракции, находящегося на инфраструктурном уровне. Полная информация о задаче, например, тип, статус, время исполнения, количество попыток выполнения, полезная нагрузка и тп, будут содержаться в слое абстракции выше Trigger Hook.

Верхний слой будет обладать доменным знанием. Другими словами, менеджер задач будет иметь определенный набор типов задач, определенный набор событий, относящихся к тем или иным типам задач. Например, обращение к интерфейсу будет звучать как создай отложенную задачу на отправку email сообщения или создай отложенную задачу на списание платы по подписке на YouTube, а уже сам менеджер задач будет обращаться к Trigger Hook с запросом создай отложенную задачу. Когда придет время запустить задачу, Trigger Hook создаст событие время выполнения задания наступило. Это событие перехватит менеджер задач, обработает его, выдав, например, событие время списания платы по подписке наступило. На рисунках 5 и 6 показан этот процесс.

Рисунок 5 - Создание задания с использованием промежуточного слояРисунок 5 - Создание задания с использованием промежуточного слояРисунок 6 - Обработка события с использованием промежуточного слояРисунок 6 - Обработка события с использованием промежуточного слоя

Связь между компонентами приложения должна быть очень слабой. Это касается и микро-сервисов в целом. На практике, одной из причин усиления связи, является перенос части ответственности одного сервиса в другой. Поэтому, одной из самых сложных задач, является поиск границы раздела (монолитного, например) приложения на микро-сервисы. Что бы это сделать удачно, нужно учитывать специфику доменной области знаний и текущей реализации приложения. Теперь вопрос - в какой микро-сервис поместить слой менеджер задач?

Рисунок 7 - Менеджер задач в одном м/с с Trigger HookРисунок 7 - Менеджер задач в одном м/с с Trigger Hook

На рисунке 7 показана схема, где менеджер задач является отдельным, микро-сервисом, содержащий доменное знание о типах задач, событиях относящихся к этим задачам. Как видно из схемы, предполагается совместное использование одного микро-сервиса менеджера заданий для разных клиентских микро-сервисов. У каждого микро-сервиса свой канал для получения событий. В RabbitMq такой канал событий легко реализовать в виде схемы direct.

Рисунок 8 - Менеджер задач как часть клиентского м/сРисунок 8 - Менеджер задач как часть клиентского м/с

На рисунке 8 показана иная схема, где менеджер задач является частью клиентского микро-сервиса и используется только для своих внутренних нужд. Такая схема подойдет если нет других микро-сервисов использующих отложенные задания или же каждый микро-сервис имеет свой менеджер задач с Trigger Hook микро-сервисом.

Масштабирование

Некоторые приложения сложнее масштабировать, чем другие. Все намного проще, если состояние приложения хранится только во внешнем хранилище с поддержкой конкурентного доступа, например, классическая связка PHP + MySQL. В этом случае несколько экземпляров приложения PHP разворачиваются на разных серверах, а Nginx балансирует нагрузку между ними, при этом, MySQL ресурс остается один на все экземпляры PHP приложений. Если MySQL не справляется, то уже независимо от PHP приложения, могут быть добавлены реплики.

Все несколько сложнее, когда приложение хранит собственное состояние. Его сложнее масштабировать горизонтально. Trigger Hook хранит свое состояние в оперативной памяти. Оно подгружает задачи, время запуска которых скоро наступит. Допустим, Вы создали задачу, время выполнения которой наступит примерно через 5 секунд. Это означает, что Trigger Hook уже погрузил ее для выполнения. Но Вы захотели отменить эту задачу. Для этого нужно вызвать метод API delete. Важно вызвать этот метод у того экземпляра приложения, который взял задачу на обработку. Это первая сложность.

Вторая сложность заключается в том, что каждый экземпляр Trigger Hook должен иметь собственную схему в БД. Это связано с обеспечением согласованности базы данных при сбоях. В общем, с точки зрения производительности нет смысла использовать экземпляры Trigger Hook для одной базы данных, во первых, потому что Trigger Hook работает в много-поточном режиме, а во вторых, при прочих равных БД является узким горлышком.

На рисунке 9 показан пример масштабирования нагрузки. У каждого экземпляра Trigger Hook своя БД, на разных серверах (иначе особого смысла нет). Перед экземплярами Trigger Hook имеется балансировщик нагрузки. Кроме балансировки, он пишет в какую-нибудь hash map базу данных, например, Redis, пару ключ-значение:

task_id:instance_host
Рисунок 9 - Схема горизонтального масштабированияРисунок 9 - Схема горизонтального масштабирования

Это нужно для обеспечения функции удаления задачи. Если в Вашем приложении не предусмотрено удаление, то достаточно балансера без базы данных. События, генерируемые экземплярами Trigger Hook можно пересылать по одному каналу через брокер. Генерирование id будет происходить на стороне клиентского сервиса (при асинхронном взаимодействии) или на стороне trigger hook (при асинхронном или синхронном взаимодействии). Для клиентских сервисов интерфейс не изменится.

Приложение для демонстрации Trigger Hook

Приложение состоит из пяти микро-сервисов. Каждый использует Docker контейнер. Все работает на Kubernetes. Приложение легко можно развернуть в minikube. Тут описана подробная инструкция.

Рисунок 10 - Упрощенная схема взаимодействия микро-сервисовРисунок 10 - Упрощенная схема взаимодействия микро-сервисов

Message service - сервис (рисунок 11), который предоставляет API для создания email сообщений и назначения отправки на определенное время или отмены. Также позволяет просмотреть полный список сообщений и их статусы.

Некоторые особенности:

  • Находится на уровне домена.

  • Состоит из менеджера сообщений и менеджера заданий.

  • Написан на PHP, фреймворк Symfony 5.

  • Работает в двух экземплярах. Первый обслуживает API запросы при помощи Nginx. Второй - запускает демон через supervisor для прослушивания события из очереди RabbitMQ. Имеет вспомогательные экземпляры для запуска миграций.

  • Использует схему с рисунка 8 для управления заданиями.

Рисунок 11 - Message serviceРисунок 11 - Message service

Message Dashboard - интерфейс для Message service (рисунок 12).

Рисунок 12 - Интерфейс демо-приложенияРисунок 12 - Интерфейс демо-приложения

Сервис Mailer находится на уровне инфраструктуры. Должен непосредственно делать рассылку. Не реализован, так как не важен в рамках демо.

Trigger service - сервис уровня инфраструктуры. Использует GRPC канал для получения команд на создание и удаление заданий, AMQP для рассылки события наступления времени выполнения задания (триггер).

Рисунок 13 - Trigger serviceРисунок 13 - Trigger service

Monitoring - также находится на инфраструктурном уровне, так как показывает технические метрики без привязки к бизнес событиям. На рисунке 14 показано как выглядит панель. Используется Grafana и InfluxDB. Полное описание метрик есть тут.

Рисунок 14 - Технические метрики Trigger HookРисунок 14 - Технические метрики Trigger Hook

Надеюсь, приложение и статья будут Вам полезны! Следите за моим github, следите за проектом, ставьте звездочки). Спасибо!

Подробнее..

Fintech на практике как Quadcode технологии для трейдинга и банкинга разрабатывает

01.06.2021 12:20:22 | Автор: admin

Привет, самое хардовое IT комьюнити Рунета! Я Саша, главный архитектор в компании Quadcode. Мы пришли на Хабр для того, чтобы показать кухню Fintech варимся мы во всем этом 8 лет, поэтому уже можем поделиться опытом. В своем блоге будем рассказывать об архитектурах, технологиях, инструментах и лайфхаках.

Этот пост первый в списке, его можно считать знакомством. Под катом я расскажу про структуру нашей команды, про продукты Quadcode это платформа для трейдинга, банкинг и внутренние разработки, а также про наши первые шаги на пути к IPO.

Наша команда

Команда Quadcode уже 8 лет работает в финтехе. Цель компании создавать удобные финтех-инструменты для B2B клиентов со всего мира.

В разработке мы руководствуемся Agile принципами, да и в принципе склонны к гибким методологиям. Именно они позволяют достигать баланса в скорости и качестве разработки продуктов, поэтому разработка представляет из себя набор Scrum команд.

Во главе каждой команды стоит Team Lead. Сами команды сгруппированы в отделы, работающие над определенными предметными областями. Например, есть отдел Finance Development, в котором команды разрабатывают финансовые сервисы для платформы. Есть ветка, где располагаются владельцы продукта (product owners), задача которых развивать и улучшать наши продукты. Сейчас у нас в разработке 230+ опытных (реально опытных, у каждого много лет практики) специалистов. Это порядка 24 команд и 6 Product Owners. Джуниоров мы берем редко. Но с каждым годом искать опытных специалистов становится все сложнее, так что все больше в эту сторону смотрим.

Задачи по разработке выстраиваются на основе продуктовых Roadmap. Это план развития продукта с целью получения определенных бизнес-показателей. Роадмап выстраивается для каждого продукта и может быть составлен на разные временные промежутки: полгода, год, три года и т.д. Из готовых продуктовых роадмапов выстраивается общий план: когда, какие фичи и для каких продуктов должны быть сделаны.

Роадмап в нашем понимании это связующее звено между бизнесом, продуктом и разработкой.

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

Работаем удаленно или в офисе в Санкт-Петербурге каждый выбирает самостоятельно. Я для себя выбрал работу из дома, но по офису тоже скучаю, он у нас классный с тренажерным залом, массажным кабинетом и прочими плюшками.

Технологический стек

Наши основные языки для разработки Golang и C++. Из дополнительных технологий на бэкенде PHP, Python, NodeJS, на фронте JavaScript (ReactJS), в аналитике Python, Scala, а в автотестах Java.

Инфраструктура в компании гибридная. Мы арендуем собственные сервера в датацентрах. Все stateless приложения стараемся эксплуатировать в Kubernetes, если для этого нет ограничений, хотя бывает и такое. Kubernetes-кластера также преимущественно работают на наших серверах. То, что требует гарантированных ресурсов, например нагруженные базы данных, мы эксплуатируем на железе. Конечно, используем и облака там, где это приносит пользу. Например в задачах, где требуется обработать большое количество данных, чтобы предоставить отчет заказчику. Для таких задач нужно временно получить ресурсы для анализа, но после получения результата они не нужны.

Для точечных целей применяем технологии, которые позволяют решить специфические задачи. Например, наше Desktop приложение под Windows, Mac и Web написано на С++ и имеет единую кодовую базу. В данном случае С++ дает нам кроссплатформенность и отличную производительность при рендере графики. Однако мы практически не используем С++ для Backend разработки, потому что это дорого. Основной язык разработки для Backend у нас Go. В то же время мы не используем его как инструмент для тестирования. Для этих целей применяем Java, так как это намного удобнее и является уже практически промышленным стандартом в индустрии.

Какие продукты создает команда Quadcode

Наш флагманский продукт платформа для трейдинга. За 7 лет развития количество пользователей платформы выросло с 950 тысяч до 88 миллионов в 170+ странах.

Начиная с 2020 года развиваем трейдинговую платформу как SaaS решение, на базе которого любой желающий может организовать собственного брокера. И у нас уже есть первые клиенты в этой области.

Кроме того планируем расширять географический охват, выходить на новые рынки. Для этого обновляем платформу с учетом всех технических и юридических требований конкретных регионов. Все это поможет нам выйти на IPO.

А теперь кратко о наших продуктах:

SaaS Trading Platform

Команда с нуля разработала платформу с аптаймом 99.5%, на базе которой более 7 лет успешно функционирует брокер.

Платформа предоставляет клиенты под Windows, MacOS, Anrdoid, iOS, а также WEB трейдрум.

На платформе можно торговать следующими инструментами:

  • Digital опционы

  • FX опционы

  • CFD

  • Forex

  • Crypto и др.

Основной язык для разработки платформы Golang. Платформа начала свое существование с монолитной архитектуры классического для своего времени стека: PHP+PostgreSQL+Redis+JS.

Через 3 года эксплуатации было решено перейти на микросервисную архитектуру, так как монолит уже не давал гибкости и не мог обеспечить необходимые темпы разработки. С миграцией на микросервисную архитектуру мы также ушли с PHP в сторону Go, о чем не жалеем.

Go отличный язык, обеспечивающий хорошую производительность с довольно небольшим порогом входа. Для разработки микросервисов просто идеальный кандидат.

С прошлого года наша платформа развивается как SaaS решение. На базе решения любой желающий может без больших усилий организовать своего собственного брокера, все есть в коробке под ключ: трейдинговый сервис, процедуры KYC, биллинг, support, crm. Словом, все, чтобы быстро стартануть бизнес. Любого нового брокера можно поднять за месяц. Чтобы обеспечить вариативность в функционале, мы разрабатываем гибкую систему модулей для SaaS-решения.

* Для того, чтобы наглядно объяснить, что такое SaaS, и показать, куда мы в итоге хотим прийти, приведем пример с пиццей. Это так называемая модель Pizza-as-a-service, вкусно и полезно.* Для того, чтобы наглядно объяснить, что такое SaaS, и показать, куда мы в итоге хотим прийти, приведем пример с пиццей. Это так называемая модель Pizza-as-a-service, вкусно и полезно.

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

Сейчас добиваемся того, чтобы в экосистеме платформы был максимально широкий спектр инструментов: Forex, СFD и инвестиционные продукты в удобной для пользователя форме. Идеальный вариант сделать платформу подходящей как для банков, так и для их клиентов. Мы собираем паззл продукта из мельчайших деталей. Процесс этот не такой быстрый, но пока все получается. Быстро и не получится ни в правовом плане, ни в плане технологий.

Примеры задач, которые стоят перед командой в этом году:

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

  2. Также один из крупных проектов это разработка собственного движка Margin Forex & MCFD.

  3. Проработка Prediction Churn. Фича основана на анализе данных и предсказывает момент, когда пользователь решит уйти. Сейчас результат Prediction Churn достоверен с вероятностью 82%. Когда система предсказывает, что пользователь готов уйти с платформы,в работу включаются менеджеры, чтобы создать удобные для трейдера условия работы на платформе. Это позволяет продлить срок работы с трейдером. Чем дальше, тем точнее будет работать Prediction Churn, и тем лучше мы сможем держать контакт с пользователем.

Banking

Это второй наш продукт. В основе направления находится собственный лицензированный провайдер финансовых услуг, который зарегистрирован в Великобритании. Продукт предоставляет следующие функции B2B и B2C клиентам:

  • Дистанционный онбординг для физических и юридических лиц.

  • Доступ к счету через мобильное приложение и онлайн-банкинг.

  • Мультивалютные счета в формате IBAN.

  • SEPA, TARGET2 и SWIFT переводы.

  • Выпуск пластиковых и виртуальных карт.

Технологический стек классический: ядро системы работает под управлением JAVA. А также применяется PHP+JS для реализации административных интерфейсов управления и web приложений.

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

Внутренние разработки

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

Из наиболее интересных можно выделить следующие:

  1. Шина данных IQ Bus. Мы исповедуем микросервсиную архитектуру. В самом начале, когда возник вопрос, а что выбрать для обеспечения коммуникации между микросервисами, мы решили создать свое решение IQ Bus. Это шина, которая абстрагирует сервисы от транспортного уровня и предоставляет им простой унифицированный протокол для общения.

  2. Sandbox. В сложных многокомпонентных, а в нашем случае системах с большим количеством сервисов, всегда возникает проблема с тестированием. Важно иметь возможность получать воспроизводимое окружение для тестирования, так называемые тестовые стенды. Еще в самом начале пути мы создали Sandbox систему, с помощью которой можно собирать копии платформы с различными конфигурациями. Это своего рода конструктор, куда можно зайти, выбрать какая функциональность нужна - будет создана сборка, запущены необходимые микросервисы и можно тестировать. Все это работает на базе Docker + Kubernetes.

  3. Central Information System. Всегда возникает необходимость в инструменте, который может объединить в себе все системы компании. Речь не только про разработку, но и про КДП, HR, Финансовый отдел. Такая система должна помогать находить ответы на различные вопросы. Например, что за команда такая A, какие у нее сотрудники, кто руководитель, какой у нее ФОТ, что она сделала за прошедший квартал. И плюс еще много всяких индивидуальных хотелок. Найти такой продукт, имеющий в себе все, достаточно проблематично, да и выглядят такие системы довольно монструозно. Хороший пример SAP. Мы же вкладываемся в собственную разработку такой системы, которая реализует все потребности различных отделов и интегрируется с другими системами: Gitlab, таск трекер, финансовые системы (1C).

Вместо заключения

За 2020 мы проделали большой путь по разработке SAAS решения и внедрения нового банкинг продукта, сейчас появилось еще несколько важных целей. Компания использует стратегическое планирование, мы движемся в сторону присутствия на рынках всех стран, удвоения показателя EBITDA и выхода на IPO.

В будущих статьях на Хабре мы расскажем более подробно о нашем подходе к разработке, планированию и работе с командами. Вместо рекламной паузы ссылка на наши вакансии. Если остались вопросы, то пишите в ТГ @wolverinoid.

Важный момент этот пост является знакомством, о чем и говорилось выше, поэтому здесь так много мы, нам и т.п. Все будущие статьи, которые станем публиковать, готовятся с прицелом на то, чтобы поделиться опытом и знаниями, которые могут оказаться полезными всему сообществу. Ведь Хабр для этого и создан, верно?

Подробнее..

Перевод Использование микросервисов в работе с Kubernetes и GitOps

10.06.2021 18:12:02 | Автор: admin

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

К счастью, существуют стратегии решения этих проблем. Сначала мы рассмотрим рефакторинг сервиса на основе Kafka Streams с использованием Microservices Framework, который обеспечивает стандарты для тестирования, конфигурации и интеграции. Затем мы используем существующий проект streaming-ops для создания, проверки и продвижения нового сервиса из среды разработки в рабочую среду. Хотя это и не обязательно, но вы если хотите выполнить шаги, описанные в этой заметке, то вам понадобится собственная версия проекта streaming-ops, как описано в документации.

Проблемы микросервисной архитектуры

По мере того как инженерные группы внедряют архитектуры микросервисов, отдельные команды могут начать расходиться в своих технических решениях. Это может привести к различным проблемам:

  • Множественные решения общих потребностей в рамках всей организации нарушают принцип "Не повторяйся".

  • Разработчики, желающие сменить команду или перейти в другую, сталкиваются с необходимостью изучения нескольких технологических стеков и архитектурных решений.

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

Spring Boot

Чтобы снизить эти риски, разработчики обращаются к микросервисным фреймворкам для стандартизации общих задач разработки, и Spring Boot (расширение фреймворка Spring) является популярным примером одного из таких фреймворков.

Spring Boot предоставляет согласованные решения для общих проблем разработки программного обеспечения, например, конфигурация, управление зависимостями, тестирование, веб-сервисы и другие внешние системные интеграции, такие как Apache Kafka. Давайте рассмотрим пример использования Spring Boot для переписывания существующего микросервиса на основе Kafka Streams.

Сервис заказов

Проект streaming-ops - это среда, похожая на рабочую, в которой работают микросервисы, основанные на существующих примерах Kafka Streams. Мы рефакторизовали один из этих сервисов для использования Spring Boot, а полный исходный код проекта можно найти в репозитории GitHub. Давайте рассмотрим некоторые основные моменты.

Интеграция Kafka

Библиотека Spring for Apache Kafka обеспечивает интеграцию Spring для стандартных клиентов Kafka, Kafka Streams DSL и приложений Processor API. Использование этих библиотек позволяет сосредоточиться на написании логики обработки потоков и оставить конфигурацию и построение зависимых объектов на усмотрение Spring dependency injection (DI) framework. Здесь представлен компонент сервиса заказов Kafka Streams, который агрегирует заказы и хранит их по ключу в хранилище состояний:

@Autowiredpublic void orderTable(final StreamsBuilder builder) {  logger.info("Building orderTable");  builder    .table(this.topic,    Consumed.with(Serdes.String(), orderValueSerde()),    Materialized.as(STATE_STORE))    .toStream()    .peek((k,v) -> logger.info("Table Peek: {}", v));}

Аннотация @Autowired выше предписывает фреймворку Spring DI вызывать эту функцию при запуске, предоставляя инстанс StreamsBuilder, который мы используем для построения нашего DSL-приложения Kafka Streams. Этот метод позволяет нам написать класс с узкой направленностью на бизнес-логику, оставляя детали построения и конфигурирования объектов поддержки Kafka Streams фреймворку.

Конфигурация

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

В примере с сервисом заказов мы решили использовать файлы свойств Spring для конфигурации, связанной с Apache Kafka. Значения конфигурации по умолчанию предоставляются во встроенном ресурсе application.properties, и мы переопределяем их во время выполнения с помощью внешних файлов и функции Profiles в Spring. Здесь вы можете увидеть сниппет ресурсного файла application.properties по умолчанию:

# ################################################ For Kafka, the following values can be# overridden by a 'traditional' Kafka# properties filebootstrap.servers=localhost:9092...# Spring Kafkaspring.kafka.properties.bootstrap.servers=${bootstrap.servers}...

Например, значение spring.kafka.properties.bootstrap.servers обеспечивается значением в bootstrap.servers с использованием синтаксиса плейсхолдер ${var.name} .

Во время выполнения Spring ищет папку config в текущем рабочем каталоге запущенного процесса. Файлы, найденные в этой папке, которые соответствуют шаблону application-<profile-name>.properties, будут оценены как активная конфигурация. Активными профилями можно управлять, устанавливая свойство spring.profiles.active в файле, в командной строке или в переменной окружения. В проекте streaming-ops мы разворачиваем набор файлов свойств, соответствующих этому шаблону, и устанавливаем соответствующие активные профили с помощью переменной окружения SPRING_PROFILES_ACTIVE.

Управление зависимостями

В приложении сервиса заказов мы решили использовать Spring Gradle и плагин управления зависимостями Spring. dependency-management plugin впоследствии будет управлять оставшимися прямыми и переходными зависимостями за нас, как показано в файле build.gradle:

plugins {  id 'org.springframework.boot' version '2.3.4.RELEASE'  id 'io.spring.dependency-management' version '1.0.10.RELEASE'  id 'java'}

Следующие библиотеки Spring могут быть объявлены без конкретных номеров версий, поскольку плагин предоставит совместимые версии от нашего имени:

dependencies {  implementation 'org.springframework.boot:spring-boot-starter-web'  implementation 'org.springframework.boot:spring-boot-starter-actuator'  implementation 'org.springframework.boot:spring-boot-starter-webflux'  implementation 'org.apache.kafka:kafka-streams'  implementation 'org.springframework.kafka:spring-kafka'  ...

REST-сервисы

Spring предоставляет REST-сервисы с декларативными аннотациями Java для определения конечных точек HTTP. В сервисе заказов мы используем это для того, чтобы использовать фронтенд API для выполнения запросов в хранилище данных Kafka Streams. Мы также используем асинхронные библиотеки, предоставляемые Spring, например, для неблокирующей обработки HTTP-запросов:

@GetMapping(value = "/orders/{id}", produces = "application/json")public DeferredResult<ResponseEntity> getOrder(  @PathVariable String id,  @RequestParam Optional timeout) {     final DeferredResult<ResponseEntity> httpResult =     new DeferredResult<>(timeout.orElse(5000L));...

Смотрите полный код в файле OrdersServiceController.java.

Тестирование

Блог Confluent содержит много полезных статей, подробно описывающих тестирование Spring для Apache Kafka (например, смотрите Advanced Testing Techniques for Spring for Apache Kafka). Здесь мы кратко покажем, как легко можно настроить тест с помощью Java-аннотаций, которые будут загружать Spring DI, а также встроенный Kafka для тестирования клиентов Kafka, включая Kafka Streams и использование AdminClient:

@RunWith(SpringRunner.class)@SpringBootTest@EmbeddedKafka@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)public class OrderProducerTests {...

С помощью этих полезных аннотаций и фреймворка Spring DI создание тестового класса, использующего Kafka, может быть очень простым:

@Autowiredprivate OrderProducer producer;...@Testpublic void testSend() throws Exception {  ...  List producedOrders = List.of(o1, o2);  producedOrders.forEach(producer::produceOrder);  ...

Смотрите полный файл OrderProducerTests.java для наглядного примера.

Проверка в dev

Код сервиса заказов содержит набор интеграционных тестов, которые мы используем для проверки поведения программы; репозиторий содержит задания CI, которые вызываются при появлении PR или переносе в основную ветвь. Убедившись, что приложение ведет себя так, как ожидается, мы развернем его в среде dev для сборки, тестирования и дальнейшего подтверждения поведения кода.

Проект streaming-ops запускает свои рабочие нагрузки микросервисов на Kubernetes и использует подход GitOps для управления операционными проблемами. Чтобы установить наш новый сервис в среде dev, мы изменим развернутую версию в dev, добавив переопределение Kustomize в сервис заказов Deployment, и отправим PR на проверку.

Когда этот PR будет объединен, запустится процесс GitOps, модифицируя объявленную версию контейнера службы заказов. После этого контроллеры Kubernetes развертывают новую версию, создавая заменяющие Поды и завершая работу предыдущих версий.

После завершения развертывания мы можем провести валидацию новой службы заказов, проверив, правильно ли она принимает REST-звонки, и изучив ее журналы. Чтобы проверить конечную точку REST, мы можем открыть приглашение внутри кластера Kubernetes с помощью хелпер-команды в предоставленном Makefile, а затем использовать curl для проверки конечной точки HTTP:

$ make promptbash-5.0# curl -XGET http://orders-servicecurl: (7) Failed to connect to orders-service port 80: Connection refused

Наша конечная точка HTTP недостижима, поэтому давайте проверим журналы:

kubectl logs deployments/orders-service | grep ERROR2020-11-22 20:56:30.243 ERROR 21 --- [-StreamThread-1] o.a.k.s.p.internals.StreamThread     : stream-thread [order-table-4cca220a-53cb-4bd5-8c34-d00a5aa77e63-StreamThread-1] Encountered the following unexpected Kafka exception during processing, this usually indicate Streams internal errors:           org.apache.kafka.common.errors.GroupAuthorizationException: Not authorized to access group: order-table

Эти ошибки, скорее всего, ортогональны и поэтому потребуют независимых исправлений. Не имеет значения, как они будут устранены, необходимо быстро вернуть нашу систему в работоспособное состояние. GitOps предоставляет хороший путь для ускорения этого процесса путем отмены предыдущего коммита. Мы используем функцию возврата GitHub PR, чтобы организовать последующий PR, который отменяет изменения.

Как только PR будет объединен, процесс GitOps применит отмененные изменения, возвращая систему в предыдущее функциональное состояние. Для лучшей поддержки этой возможности целесообразно сохранять изменения небольшими и инкрементными. Среда dev полезна для отработки процедур отката.

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

  1. HTTP-порт по умолчанию был другим, из-за чего служба Kubernetes не могла правильно направить трафик сервису заказов.

  2. Идентификатор приложения Kafka Streams по умолчанию отличался от настроенного списка контроля доступа (ACL) в Confluent Cloud, что лишало наш новый сервис заказов доступа к кластеру Kafka.

Мы решили отправить новый PR, исправляющий значения по умолчанию в приложении. Изменения содержатся в конфигурационных файлах, расположенных в развернутых ресурсах Java Archive (JAR).

В файле application.yaml мы изменяем порт HTTP-сервиса по умолчанию:

Server:  Port: 18894

А в файле application.properties (который содержит соответствующие конфигурации Spring для Apache Kafka) мы модифицируем ID приложения Kafka Streams на значение, заданное декларациями Confluent Cloud ACL:

spring.kafka.streams.application-id=OrdersService

Когда новый PR будет отправлен, процесс CI/CD на основе GitHub Actions запустит тесты. После слияния PR другой Action опубликует новую версию Docker-образа службы заказов.

Еще один PR с новой версией службы заказов позволит нам развернуть новый образ с правильными настройками по умолчанию обратно в среду dev и повторно провести валидацию. На этот раз после развертывания мы сможем взаимодействовать с новым сервисом заказов, как и ожидалось.

$ make promptbash-5.0# curl http://orders-service/actuator/health{"status":"UP","groups":["liveness","readiness"]}bash-5.0# curl -XGET http://orders-service/v1/orders/284298{"id":"284298","customerId":0,"state":"FAILED","product":"JUMPERS","quantity":1,"price":1.0}

Наконец, с нашего устройства разработки мы можем использовать Confluent Cloud CLI для потоковой передачи заказов из темы orders в формате Avro (см. документацию Confluent Cloud CLI для инструкций по настройке и использованию CLI).

 ccloud kafka topic consume orders --value-format avroStarting Kafka Consumer. ^C or ^D to exit{"quantity":1,"price":1,"id":"284320","customerId":5,"state":"CREATED","product":"UNDERPANTS"}{"id":"284320","customerId":1,"state":"FAILED","product":"STOCKINGS","quantity":1,"price":1}{"id":"284320","customerId":1,"state":"FAILED","product":"STOCKINGS","quantity":1,"price":1}^CStopping Consumer.

Продвижение в prd

Имея на руках наш новый отрефакторенный и валидированный сервис заказов, мы хотим завершить работу, продвинув его в продакшн. С нашим инструментарием GitOps это простой процесс. Давайте посмотрим, как это сделать.

Сначала оценим хелпер-команду, которую можно запустить для проверки разницы в объявленных версиях сервиса заказов в каждой среде. С устройства разработчика в репозитории проекта мы можем использовать Kustomize для сборки и оценки окончательно материализованных манифестов Kubernetes, а затем поиска в них визуальной информации о сервисе заказов. Наш проект streaming-ops предоставляет полезные команды Makefile для облегчения этой задачи:

 make test-prd test-dev >/dev/null; diff .test/dev.yaml .test/prd.yaml | grep "orders-service"< image: cnfldemos/orders-service:sha-82165db > image: cnfldemos/orders-service:sha-93c0516

Здесь мы видим, что версии тегов образов Docker отличаются в средах dev и prd. Мы сохраним финальный PR, который приведет среду prd в соответствие с текущей версией dev. Для этого мы модифицируем тег изображения, объявленный в базовом определении для службы заказов, и оставим на месте переопределение dev. В данном случае оставление dev-переопределения не оказывает существенного влияния на развернутую версию службы заказов, но облегчит будущие развертывания на dev. Этот PR развернет новую версию на prd:

Перед слиянием мы можем повторно выполнить наши тестовые команды, чтобы убедиться, что в развернутых версиях службы заказов не будет различий, о чем свидетельствует отсутствие вывода команд diff и grep:

 make test-prd test-dev >/dev/null; diff .test/dev.yaml .test/prd.yaml | grep "orders-service"

Этот PR был объединен, и контроллер FluxCD в среде prd развернул нужную версию. Используя jq и kubectl с флагом --context, мы можем легко сравнить развертывание сервиса заказов на кластерах dev и prd:

 kubectl --context= get deployments/orders-service -o json | jq -r '.spec.template.spec.containers | .[].image'cnfldemos/orders-service:sha-82165db kubectl --context= get deployments/orders-service -o json | jq -r '.spec.template.spec.containers | .[].image'cnfldemos/orders-service:sha-82165db

Мы можем использовать curl внутри кластера, чтобы проверить, что развертывание работает правильно. Сначала установите контекст kubectl на ваш рабочий кластер:

 kubectl config use-context <your-prd-k8s-context>Switched to context "kafka-devops-prd".

Хелпер-команда подсказки в репозитории кода помогает нам создать терминал в кластере prd, который мы можем использовать для взаимодействия с REST-сервисом службы заказов:

 make promptLaunching-util-pod-------------------------------- kubectl run --tty -i --rm util --image=cnfldemos/util:0.0.5 --restart=Never --serviceaccount=in-cluster-sa --namespace=defaultIf you don't see a command prompt, try pressing enter.bash-5.0#

Внутри кластера мы можем проверить работоспособность (здоровье - health) службы заказов:

bash-5.0# curl -XGET http://orders-service/actuator/health{"status":"UP","groups":["liveness","readiness"]}bash-5.0# exit

Наконец, мы можем убедиться, что заказы обрабатываются правильно, оценив журналы из orders-and-payments-simulator:

 kubectl logs deployments/orders-and-payments-simulator | tail -n 5Getting order from: http://orders-service/v1/orders/376087   .... Posted order 376087 equals returned order: OrderBean{id='376087', customerId=2, state=CREATED, product=STOCKINGS, quantity=1, price=1.0}Posting order to: http://orders-service/v1/orders/   .... Response: 201Getting order from: http://orders-service/v1/orders/376088   .... Posted order 376088 equals returned order: OrderBean{id='376088', customerId=5, state=CREATED, product=STOCKINGS, quantity=1, price=1.0}Posting order to: http://orders-service/v1/orders/   .... Response: 201Getting order from: http://orders-service/v1/orders/376089   .... Posted order 376089 equals returned order: OrderBean{id='376089', customerId=1, state=CREATED, product=JUMPERS, quantity=1, price=1.0}

Симулятор заказов и платежей взаимодействует с конечной точкой REST сервиса заказов, публикуя новые заказы и получая их обратно от конечной точки /v1/validated. Здесь мы видим код 201 ответа в журнале, означающий, что симулятор и сервис заказов взаимодействуют правильно, и сервис заказов правильно считывает заказы из хранилища состояния Kafka Streams.

Резюме

Успешное внедрение микросервисов требует тщательной координации в вашей инженерной организации. В этом посте вы увидели, как микросервисные фреймворки полезны для стандартизации практики разработки в ваших проектах. С помощью GitOps вы можете уменьшить сложность развертывания и расширить возможности таких важных функций, как откат. Если у вас есть идеи относительно областей, связанных с DevOps, о которых вы хотите узнать от нас, пожалуйста, не стесняйтесь задать вопрос в проекте, или, что еще лучше - PRs открыты для этого!

Все коды на изображениях для копирования доступны здесь.


Перевод материала подготовлен в рамках курса Microservice Architecture. Всех желающих приглашаем на открытый урок Атрибуты качества, тактики и паттерны. На этом вебинаре рассмотрим, что такое качественная архитектура, основные атрибуты качества и тактики работы с ними.

Подробнее..

Go-swagger как основа взаимодействия микросервисов

04.08.2020 14:20:22 | Автор: admin


Здравствуй, NickName! Если ты программист и работаешь с микросервисной архитектурой, то представь, что тебе нужно настроить взаимодействие твоего сервиса А с каким-то новым и ещё неизвестным тебе сервисом Б. Что ты будешь делать в первую очередь?

Если задать такой вопрос 100 программистам из разных компаний, скорее всего, мы получим 100 разных ответов. Кто-то описывает контракты в swagger, кто-то в gRPC просто делает клиенты к своим сервисам без описания контракта. А кто-то и вовсе хранит JSON в гуглодоке :D. В большинстве компаний складывается свой подход к межсервисному взаимодействию на основании каких-либо исторических факторов, компетенций, стека технологий и прочего. Я хочу рассказать, как сервисы в Delivery Club общаются друг с другом и почему мы сделали именно такой выбор. И главное как мы обеспечиваем актуальность документации с течением времени. Будет много кода!

Ещё раз привет! Меня зовут Сергей Попов, я тим-лид команды, отвечающей за поисковую выдачу ресторанов в приложениях и на сайте Delivery Club, а также активный участник нашей внутренней гильдии разработки на Go (возможно, мы об этом ещё расскажем, но не сейчас).

Сразу оговорюсь, речь пойдет, в основном, про сервисы, написанные на Go. Генерирование кода для PHP-сервисов мы ещё не реализовали, хотя достигаем там единообразия в подходах другим способом.

К чему, в итоге, мы хотели прийти:

  1. Обеспечить актуальность контрактов сервисов. Это должно ускорить внедрение новых сервисов и упростить коммуникацию между командами.
  2. Прийти к единому способу взаимодействия по HTTP между сервисами (пока не будем рассматривать взаимодействия через очереди и event streaming).
  3. Стандартизировать подход к работе с контрактами сервисов.
  4. Использовать единое хранилище контрактов, чтобы не искать доки по всяким конфлюенсам.
  5. В идеале, генерировать клиенты под разные платформы.

Из всего перечисленного на ум приходит Protobuf как единый способ описания контрактов. Он имеет хороший инструментарий и может генерировать клиенты под разные платформы (наш п.5). Но есть и явные недостатки: для многих gRPC остается чем-то новым и неизведанным, а это сильно усложнило бы его внедрение. Ещё одним важным фактором было то, что в компании давно принят подход specification first, и документация уже существовала на все сервисы в виде swagger или RAML-описания.

Go-swagger


Так совпало, что в то же время мы начали адаптацию Go в компании. Поэтому следующим нашим кандидатом на рассмотрение оказался go-swagger инструмент, который позволяет генерировать клиентов и серверный код из swagger-спецификации. Из очевидных недостатков он генерирует код только для Go. На самом деле, там используется гошное кодогенерирование, и go-swagger позволяет гибко работать с шаблонам, так что, теоретически, его можно использовать для генерирования кода на PHP, но мы ещё не пробовали.

Go-swagger это не только про генерирование транспортного слоя. Фактически он генерирует каркас приложения, и тут я бы хотел немного упомянуть о культуре разработки в DC. У нас есть Inner Source, а это значит, что любой разработчик из любой команды может создать pull request в любой сервис, который у нас есть. Чтобы такая схема работала, мы стараемся стандартизировать подходы в разработке: используем общую терминологию, единый подход к логированию, метрикам, работе с зависимостями и, конечно же, к структуре проекта.

Таким образом, внедряя go-swagger, мы вводим стандарт разработки наших сервисов на Go. Это еще один шаг навстречу нашим целям, на который мы изначально не рассчитывали, но который важен для разработки в целом.

Первые шаги


Итак, go-swagger оказался интересным кандидатом, который, кажется, может покрыть большинство наших хотелок требований.
Примечание: весь дальнейший код актуален для версии 0.24.0, инструкцию по установке можно посмотреть в нашем репозитории с примерами, а на официальном сайте есть инструкция по установке актуальной версии.
Давайте посмотрим, что он умеет. Возьмём swagger-спеку и сгенерируем сервис:

> goswagger generate server \    --with-context -f ./swagger-api/swagger.yml \    --name example1

Получилось у нас следующее:



Makefile и go.mod я уже сделал сам.

Фактически у нас получился сервис, который обрабатывает запросы, описанные в swagger.

> go run cmd/example1-server/main.go2020/02/17 11:04:24 Serving example service at http://127.0.0.1:54586   > curl http://localhost:54586/hello -iHTTP/1.1 501 Not ImplementedContent-Type: application/jsonDate: Sat, 15 Feb 2020 18:14:59 GMTContent-Length: 58Connection: close "operation hello HelloWorld has not yet been implemented"

Второй шаг. Разбираемся с шаблонизацией


Очевидно, что сгенерированный нами код далёк от того, что мы хотим видеть в эксплуатации.

Что мы хотим от структуры нашего приложения:

  • Уметь конфигурировать приложение: передавать настройки подключения к БД, указывать порт HTTP-соединений и прочее.
  • Выделить объект приложения, который будет хранить состояние приложения, подключение к БД и прочее.
  • Сделать хэндлеры функциями нашего приложения, это должно упростить работу с кодом.
  • Инициализировать зависимости в main-файле (в нашем примере этого не будет, но мы всё равно этого хотим.

Для решения новых задач мы можем переопределить некоторые шаблоны. Для этого опишем следующие файлы, как это сделал я (Github):



Нам необходимо описать файлы шаблонов (`*.gotmpl`) и файл для конфигурации (`*.yml`) генерирования нашего сервиса.

Далее по порядку разберем те шаблоны, которые сделал я. Глубоко погружаться в работу с ними не буду, потому что документация go-swagger достаточно подробная, например, вот описание файла конфигурации. Отмечу только, что используется Go-шаблонизация, и если у вас уже есть в этом опыт или приходилось описывать HELM-конфигурации, то разобраться не составит труда.

Конфигурирование приложения


config.gotmpl содержит простую структуру с одним параметром портом, который будет слушать приложение для входящих HTTP-запросов. Также я сделал функцию InitConfig, которая будет считывать переменные окружения и заполнять эту структуру. Вызывать буду из main.go, поэтому InitConfig сделал публичной функцией.

package config import (    "github.com/pkg/errors"    "github.com/vrischmann/envconfig") // Config structtype Config struct {    HTTPBindPort int `envconfig:"default=8001"`} // InitConfig funcfunc InitConfig(prefix string) (*Config, error) {    config := &Config{}    if err := envconfig.InitWithPrefix(config, prefix); err != nil {        return nil, errors.Wrap(err, "init config failed")    }     return config, nil}

Чтобы этот шаблон использовался при генерировании кода, его нужно указать в YML-конфиге:

layout:  application:    - name: cfgPackage      source: serverConfig      target: "./internal/config/"      file_name: "config.go"      skip_exists: false

Немного расскажу про параметры:

  • name несёт чисто информативную функцию и на генерирование не влияет.
  • source фактически путь до файла шаблона в camelCase, т.е. serverConfig равносильно ./server/config.gotmpl.
  • target директория, куда будет сохранен сгенерированный код. Здесь можно использовать шаблонизацию для динамического формирования пути (пример).
  • file_name название сгенерированного файла, здесь также можно использовать шаблонизацию.
  • skip_exists признак того, что файл будет сгенерирован только один раз и не будет перезаписывать существующий. Для нас это важно, потому что файл конфига будет меняться по мере роста приложения и не должен зависеть от генерируемого кода.

В конфиге кодогенерирования нужно указывать все файлы, а не только те, которые мы хотим переопределить. Для файлов, которые мы не меняем, в значении source указываем asset:<путь до шаблона>, например, как здесь: asset:serverConfigureapi. Кстати, если интересно посмотреть оригинальные шаблоны, то они здесь.

Объект приложения и хэндлеры


Объект приложения для хранения состояния, подключений БД и прочего я описывать не буду, всё аналогично только что сделанному конфигу. А вот с хэндлерами всё немного интереснее. Наша ключевая цель состоит в том, чтобы при добавлении URL в спецификацию у нас в отдельном файле создалась функция с заглушкой, и самое главное, чтобы наш сервер вызывал эту функцию для обработки запроса.

Опишем шаблон функции и заглушки:

package app import (    api{{ pascalize .Package }} "{{.GenCommon.TargetImportPath}}/{{ .RootPackage }}/operations/{{ .Package }}"    "github.com/go-openapi/runtime/middleware") func (srv *Service){{ pascalize .Name }}Handler(params api{{ pascalize .Package }}.{{ pascalize .Name }}Params{{ if .Authorized }}, principal api{{ .Package }}.{{ if not ( eq .Principal "interface{}" ) }}*{{ end }}{{ .Principal }}{{ end }}) middleware.Responder {    return middleware.NotImplemented("operation {{ .Package }} {{ pascalize .Name }} has not yet been implemented")}

Немного разберём пример:

  • pascalize приводит строку с CamelCase (описание остальных функции здесь).
  • .RootPackage пакет сгенерированного веб-сервера.
  • .Package название пакета в сгенерированном коде, в котором описаны все необходимые структуры для HTTP-запросов и ответов, т.е. структуры. Например, структура для тела запроса или структура ответа.
  • .Name название хэндлера. Оно берётся из operationID в спецификации, если указано. Я рекомендую всегда указывать operationID для более очевидного результата.

Конфиг для хэндлера следующий:

layout:  operations:    - name: handlerFns      source: serverHandler      target: "./internal/app"      file_name: "{{ (snakize (pascalize .Name)) }}.go"      skip_exists: true

Как видите, код хэндлеров не будет перезаписываться (skip_exists: true), а название файла будет генерироваться из названия хэндлера.

Окей, функция с заглушкой есть, но веб-сервер ещё не знает, что эти функции нужно использовать для обработки запросов. Я исправил это в main.go (весь код приводить не буду, полную версию можно найти здесь):

package main {{ $name := .Name }}{{ $operations := .Operations }}import (    "fmt"    "log"     "github.com/delivery-club/go-swagger-example/{{ dasherize .Name }}/internal/generated/restapi"    "github.com/delivery-club/go-swagger-example/{{ dasherize .Name }}/internal/generated/restapi/operations"    {{range $index, $op := .Operations}}        {{ $found := false }}        {{ range $i, $sop := $operations }}            {{ if and (gt $i $index ) (eq $op.Package $sop.Package)}}                {{ $found = true }}            {{end}}        {{end}}        {{ if not $found }}        api{{ pascalize $op.Package }} "{{$op.GenCommon.TargetImportPath}}/{{ $op.RootPackage }}/operations/{{ $op.Package }}"        {{end}}    {{end}}     "github.com/go-openapi/loads"    "github.com/vrischmann/envconfig"     "github.com/delivery-club/go-swagger-example/{{ dasherize .Name }}/internal/app") func main() {    ...    api := operations.New{{ pascalize .Name }}API(swaggerSpec)     {{range .Operations}}    api.{{ pascalize .Package }}{{ pascalize .Name }}Handler = api{{ pascalize .Package }}.{{ pascalize .Name }}HandlerFunc(srv.{{ pascalize .Name }}Handler)    {{- end}}    ...}

Код в импорте выглядит сложным, хотя на самом деле здесь просто Go-шаблонизация и структуры из репозитория go-swagger. А в функции main мы просто присваиваем хэндлерам наши сгенерированные функции.

Осталось сгенерировать код с указанием нашей конфигурации:

> goswagger generate server \        -f ./swagger-api/swagger.yml \        -t ./internal/generated -C ./swagger-templates/default-server.yml \        --template-dir ./swagger-templates/templates \        --name example2

Финальный результат можно посмотреть в нашем репозитории.

Что мы получили:

  • Мы можем использовать свои структуры для приложения, конфигов и всего, что захотим. Самое главное это достаточно просто встраивается в генерируемый код.
  • Мы можем гибко управлять структурой проекта, вплоть до названий отдельных файлов.
  • Go-шаблонизация выглядит сложной и к ней нужно привыкнуть, но в целом это очень мощный инструмент.

Третий шаг. Генерирование клиентов


Go-swagger позволяет генерировать и пакет клиента для нашего сервиса, который могут использовать другие Go-сервисы. Здесь я не буду подробно останавливаться на генерировании кода, подход точно такой же, как и при генерировании серверного кода.

Для проектов на Go принято складывать публичные пакеты в ./pkg, мы сделаем так же: положим клиент для нашего сервиса в pkg, а сам код сгенерируем следующим образом:

> goswagger generate client -f ./swagger-api/swagger.yml -t ./pkg/example3

Пример сгенерированного кода здесь.

Теперь все потребители нашего сервиса могут импортировать себе этот клиент, например, по тэгу (для моего примера тэг будет example3/pkg/example3/v0.0.1).

Шаблоны клиентов можно настраивать, чтобы, например, прокидывать open tracing id из контекста в заголовок.

Выводы


Естественно, наша внутренняя реализация отличается от приведенного здесь кода, в основном, за счёт использования внутренних пакетов и подходов к CI (запуск различных тестов и линтеров). В сгенерированном коде из коробки настроен сбор технических метрик, работа с конфигами и логирование. Мы стандартизировали все общие инструменты. За счёт этого мы упростили разработку в целом и выпуск новых сервисов в частности, обеспечили более быстрое прохождение чек-листа сервиса перед деплоем на прод.

Давайте проверим, получилось ли достигнуть первоначальных целей:

  1. Обеспечить актуальность описанных для сервисов контрактов, это должно ускорить внедрение новых сервисов и упростить коммуникацию между командами Да.
  2. Прийти к единому способу взаимодействия по HTTP между сервисами (пока не будем рассматривать взаимодействия через очереди и event streaming) Да.
  3. Стандартизировать подход к работе с контрактами сервисов, т.к. мы давно пришли к подходу Inner Source в разработке сервисов Да.
  4. Использовать единое хранилище контрактов, чтобы не искать документацию по всяким конфлюенсам Да (фактически Bitbucket).
  5. В идеале, генерировать клиенты под разные платформы Нет (на самом деле, не пробовали, шаблонизация не ограничивает в этом плане).
  6. Внедрить стандартную структуру сервиса на Go Да (дополнительный результат).

Внимательный читатель, наверное, уже задался вопросом: как файлы шаблонов попадают в наш проект? Сейчас мы храним их в каждом нашем проекте. Это упрощает повседневную работу, позволяет что-то настраивать под конкретный проект. Но есть и другая сторона медали: отсутствует механизм централизованного обновления шаблонов и доставки новых фич, в основном, связанных с CI.

P.S. Если этот материал понравится, то в дальнейшем подготовим статью про стандартную архитектуру наших сервисов, расскажем, какими принципами мы пользуемся при разработке сервисов на Go.
Подробнее..

Сервисы с Apache Kafka и тестирование

09.01.2021 18:16:01 | Автор: admin

Когда сервисы интегрируются при помощи Kafka очень удобно использовать REST API, как универсальный и стандартный способ обмена сообщениями. При увеличении количества сервисов сложность коммуникаций увеличивается. Для контроля можно и нужно использовать интеграционное тестирование. Такие библиотеки как testcontainers или EmbeddedServer прекрасно помогают организовать такое тестирование. Существуют много примеров для micronaut, Spring Boot и т.д. Но в этих примерах опущены некоторые детали, которые не позволяют с первого раза запустить код. В статье приводятся примеры с подробным описанием и ссылками на код.


Пример


Для простоты можно принять такой REST API.


/runs POST-метод. Инициализирует запрос в канал связи. Принимает данные и возвращает ключ запроса.
/runs/{key}/status GET-метод. По ключу возвращает статус запроса. Может принимать следующие значения: UNKNOWN, RUNNING, DONE.
/runs /{key} GET-метод. По ключу возвращает результат запроса.


Подобный API реализован у livy, хотя и для других задач.


Реализация


Будут использоваться: micronaut, Spring Boot.


micronaut


Контроллер для API.


import io.micronaut.http.annotation.Body;import io.micronaut.http.annotation.Controller;import io.micronaut.http.annotation.Get;import io.micronaut.http.annotation.Post;import io.reactivex.Maybe;import io.reactivex.schedulers.Schedulers;import javax.inject.Inject;import java.util.UUID;@Controller("/runs")public class RunController {    @Inject    RunClient runClient;    @Inject    RunCache runCache;    @Post    public String runs(@Body String body) {        String key = UUID.randomUUID().toString();        runCache.statuses.put(key, RunStatus.RUNNING);        runCache.responses.put(key, "");        runClient.sendRun(key, new Run(key, RunType.REQUEST, "", body));        return key;    }    @Get("/{key}/status")    public Maybe<RunStatus> getRunStatus(String key) {        return Maybe.just(key)                .subscribeOn(Schedulers.io())                .map(it -> runCache.statuses.getOrDefault(it, RunStatus.UNKNOWN));    }    @Get("/{key}")    public Maybe<String> getRunResponse(String key) {        return Maybe.just(key)                .subscribeOn(Schedulers.io())                .map(it -> runCache.responses.getOrDefault(it, ""));    }}

Отправка сообщений в kafka.


import io.micronaut.configuration.kafka.annotation.*;import io.micronaut.messaging.annotation.Body;@KafkaClientpublic interface RunClient {    @Topic("runs")    void sendRun(@KafkaKey String key, @Body Run run);}

Получение сообщений из kafka.


import io.micronaut.configuration.kafka.annotation.*;import io.micronaut.messaging.annotation.Body;import javax.inject.Inject;@KafkaListener(offsetReset = OffsetReset.EARLIEST)public class RunListener {    @Inject    RunCalculator runCalculator;    @Topic("runs")    public void receive(@KafkaKey String key, @Body Run run) {        runCalculator.run(key, run);    }}

Обработка сообщений происходит в RunCalculator. Для тестов используется особая реализация, в которой происходит переброска сообщений.


import io.micronaut.context.annotation.Replaces;import javax.inject.Inject;import javax.inject.Singleton;import java.util.UUID;@Replaces(RunCalculatorImpl.class)@Singletonpublic class RunCalculatorWithWork implements RunCalculator {    @Inject    RunClient runClient;    @Inject    RunCache runCache;    @Override    public void run(String key, Run run) {        if (RunType.REQUEST.equals(run.getType())) {            String runKey = run.getKey();            String newKey = UUID.randomUUID().toString();            String runBody = run.getBody();            runClient.sendRun(newKey, new Run(newKey, RunType.RESPONSE, runKey, runBody + "_calculated"));        } else if (RunType.RESPONSE.equals(run.getType())) {            runCache.statuses.replace(run.getResponseKey(), RunStatus.DONE);            runCache.responses.replace(run.getResponseKey(), run.getBody());        }    }}

Тест.


import io.micronaut.http.HttpRequest;import io.micronaut.http.client.HttpClient;import static org.junit.jupiter.api.Assertions.assertEquals;public abstract class RunBase {    void run(HttpClient client) {        String key = client.toBlocking().retrieve(HttpRequest.POST("/runs", "body"));        RunStatus runStatus = RunStatus.UNKNOWN;        while (runStatus != RunStatus.DONE) {            runStatus = client.toBlocking().retrieve(HttpRequest.GET("/runs/" + key + "/status"), RunStatus.class);            try {                Thread.sleep(500);            } catch (InterruptedException e) {                e.printStackTrace();            }        }        String response = client.toBlocking().retrieve(HttpRequest.GET("/runs/" + key), String.class);        assertEquals("body_calculated", response);    }}

Для использования EmbeddedServer необходимо.


Подключить библиотеки:


testImplementation("org.apache.kafka:kafka-clients:2.6.0:test")testImplementation("org.apache.kafka:kafka_2.12:2.6.0")testImplementation("org.apache.kafka:kafka_2.12:2.6.0:test")

Тест может выглядеть так.


import io.micronaut.context.ApplicationContext;import io.micronaut.http.client.HttpClient;import io.micronaut.runtime.server.EmbeddedServer;import org.junit.jupiter.api.Test;import java.util.HashMap;import java.util.Map;public class RunKeTest extends RunBase {    @Test    void test() {        Map<String, Object> properties = new HashMap<>();        properties.put("kafka.bootstrap.servers", "localhost:9092");        properties.put("kafka.embedded.enabled", "true");        try (EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer.class, properties)) {            ApplicationContext applicationContext = embeddedServer.getApplicationContext();            HttpClient client = applicationContext.createBean(HttpClient.class, embeddedServer.getURI());            run(client);        }    }}

Для использования testcontainers необходимо.


Подключить библиотеки:


implementation("org.testcontainers:kafka:1.14.3")

Тест может выглядеть так.


import io.micronaut.context.ApplicationContext;import io.micronaut.http.client.HttpClient;import io.micronaut.runtime.server.EmbeddedServer;import org.junit.jupiter.api.Test;import org.testcontainers.containers.KafkaContainer;import org.testcontainers.utility.DockerImageName;import java.util.HashMap;import java.util.Map;public class RunTcTest extends RunBase {    @Test    public void test() {        try (KafkaContainer kafka = new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:5.5.3"))) {            kafka.start();            Map<String, Object> properties = new HashMap<>();            properties.put("kafka.bootstrap.servers", kafka.getBootstrapServers());            try (EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer.class, properties)) {                ApplicationContext applicationContext = embeddedServer.getApplicationContext();                HttpClient client = applicationContext.createBean(HttpClient.class, embeddedServer.getURI());                run(client);            }        }    }}

Spring Boot


Контроллер для API.


import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.*;import java.util.UUID;@RestController@RequestMapping("/runs")public class RunController {    @Autowired    private RunClient runClient;    @Autowired    private RunCache runCache;    @PostMapping()    public String runs(@RequestBody String body) {        String key = UUID.randomUUID().toString();        runCache.statuses.put(key, RunStatus.RUNNING);        runCache.responses.put(key, "");        runClient.sendRun(key, new Run(key, RunType.REQUEST, "", body));        return key;    }    @GetMapping("/{key}/status")    public RunStatus getRunStatus(@PathVariable String key) {        return runCache.statuses.getOrDefault(key, RunStatus.UNKNOWN);    }    @GetMapping("/{key}")    public String getRunResponse(@PathVariable String key) {        return runCache.responses.getOrDefault(key, "");    }}

Отправка сообщений в kafka.


import com.fasterxml.jackson.core.JsonProcessingException;import com.fasterxml.jackson.databind.ObjectMapper;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.kafka.core.KafkaTemplate;import org.springframework.stereotype.Component;@Componentpublic class RunClient {    @Autowired    private KafkaTemplate<String, String> kafkaTemplate;    @Autowired    private ObjectMapper objectMapper;    public void sendRun(String key, Run run) {        String data = "";        try {            data = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(run);        } catch (JsonProcessingException e) {            e.printStackTrace();        }        kafkaTemplate.send("runs", key, data);    }}

Получение сообщений из kafka.


import com.fasterxml.jackson.core.JsonProcessingException;import com.fasterxml.jackson.databind.ObjectMapper;import org.apache.kafka.clients.consumer.ConsumerRecord;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.kafka.annotation.KafkaListener;import org.springframework.stereotype.Component;@Componentpublic class RunListener {    @Autowired    private ObjectMapper objectMapper;    @Autowired    private RunCalculator runCalculator;    @KafkaListener(topics = "runs", groupId = "m-group")    public void receive(ConsumerRecord<?, ?> consumerRecord) {        String key = consumerRecord.key().toString();        Run run = null;        try {            run = objectMapper.readValue(consumerRecord.value().toString(), Run.class);        } catch (JsonProcessingException e) {            e.printStackTrace();        }        runCalculator.run(key, run);    }}

Обработка сообщений происходит в RunCalculator. Для тестов используется особая реализация, в которой происходит переброска сообщений.


import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Component;import java.util.UUID;@Componentpublic class RunCalculatorWithWork implements RunCalculator {    @Autowired    RunClient runClient;    @Autowired    RunCache runCache;    @Override    public void run(String key, Run run) {        if (RunType.REQUEST.equals(run.getType())) {            String runKey = run.getKey();            String newKey = UUID.randomUUID().toString();            String runBody = run.getBody();            runClient.sendRun(newKey, new Run(newKey, RunType.RESPONSE, runKey, runBody + "_calculated"));        } else if (RunType.RESPONSE.equals(run.getType())) {            runCache.statuses.replace(run.getResponseKey(), RunStatus.DONE);            runCache.responses.replace(run.getResponseKey(), run.getBody());        }    }}

Тест.


import com.fasterxml.jackson.databind.ObjectMapper;import org.springframework.http.MediaType;import org.springframework.test.web.servlet.MockMvc;import org.springframework.test.web.servlet.MvcResult;import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;import static org.junit.jupiter.api.Assertions.assertEquals;import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;public abstract class RunBase {    void run(MockMvc mockMvc, ObjectMapper objectMapper) throws Exception {        MvcResult keyResult = mockMvc.perform(MockMvcRequestBuilders.post("/runs")                .content("body")                .contentType(MediaType.APPLICATION_JSON)                .accept(MediaType.APPLICATION_JSON))                .andExpect(status().isOk())                .andReturn();        String key = keyResult.getResponse().getContentAsString();        RunStatus runStatus = RunStatus.UNKNOWN;        while (runStatus != RunStatus.DONE) {            MvcResult statusResult = mockMvc.perform(MockMvcRequestBuilders.get("/runs/" + key + "/status")                    .contentType(MediaType.APPLICATION_JSON)                    .accept(MediaType.APPLICATION_JSON))                    .andExpect(status().isOk())                    .andReturn();            runStatus = objectMapper.readValue(statusResult.getResponse().getContentAsString(), RunStatus.class);            try {                Thread.sleep(500);            } catch (InterruptedException e) {                e.printStackTrace();            }        }        String response = mockMvc.perform(MockMvcRequestBuilders.get("/runs/" + key)                .contentType(MediaType.APPLICATION_JSON)                .accept(MediaType.APPLICATION_JSON))                .andExpect(status().isOk())                .andReturn().getResponse().getContentAsString();        assertEquals("body_calculated", response);    }}

Для использования EmbeddedServer необходимо.


Подключить библиотеки:


<dependency>    <groupId>org.springframework.kafka</groupId>    <artifactId>spring-kafka</artifactId>    <version>2.5.10.RELEASE</version></dependency><dependency>    <groupId>org.springframework.kafka</groupId>    <artifactId>spring-kafka-test</artifactId>    <version>2.5.10.RELEASE</version>    <scope>test</scope></dependency>

Тест может выглядеть так.


import com.fasterxml.jackson.databind.ObjectMapper;import org.junit.jupiter.api.Test;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;import org.springframework.boot.test.context.SpringBootTest;import org.springframework.boot.test.context.TestConfiguration;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Import;import org.springframework.kafka.test.context.EmbeddedKafka;import org.springframework.test.web.servlet.MockMvc;@AutoConfigureMockMvc@SpringBootTest@EmbeddedKafka(partitions = 1, brokerProperties = {"listeners=PLAINTEXT://localhost:9092", "port=9092"})@Import(RunKeTest.RunKeTestConfiguration.class)public class RunKeTest extends RunBase {    @Autowired    private MockMvc mockMvc;    @Autowired    private ObjectMapper objectMapper;    @Test    void test() throws Exception {        run(mockMvc, objectMapper);    }    @TestConfiguration    static class RunKeTestConfiguration {        @Autowired        private RunCache runCache;        @Autowired        private RunClient runClient;        @Bean        public RunCalculator runCalculator() {            RunCalculatorWithWork runCalculatorWithWork = new RunCalculatorWithWork();            runCalculatorWithWork.runCache = runCache;            runCalculatorWithWork.runClient = runClient;            return runCalculatorWithWork;        }    }}

Для использования testcontainers необходимо.


Подключить библиотеки:


<dependency>    <groupId>org.testcontainers</groupId>    <artifactId>kafka</artifactId>    <version>1.14.3</version>    <scope>test</scope></dependency>

Тест может выглядеть так.


import com.fasterxml.jackson.databind.ObjectMapper;import org.apache.kafka.clients.consumer.ConsumerConfig;import org.apache.kafka.clients.producer.ProducerConfig;import org.apache.kafka.common.serialization.StringDeserializer;import org.apache.kafka.common.serialization.StringSerializer;import org.junit.ClassRule;import org.junit.jupiter.api.Test;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;import org.springframework.boot.test.context.SpringBootTest;import org.springframework.boot.test.context.TestConfiguration;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Import;import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory;import org.springframework.kafka.core.*;import org.springframework.test.web.servlet.MockMvc;import org.testcontainers.containers.KafkaContainer;import org.testcontainers.utility.DockerImageName;import java.util.HashMap;import java.util.Map;@AutoConfigureMockMvc@SpringBootTest@Import(RunTcTest.RunTcTestConfiguration.class)public class RunTcTest extends RunBase {    @ClassRule    public static KafkaContainer kafka = new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:5.5.3"));    static {        kafka.start();    }    @Autowired    private MockMvc mockMvc;    @Autowired    private ObjectMapper objectMapper;    @Test    void test() throws Exception {        run(mockMvc, objectMapper);    }    @TestConfiguration    static class RunTcTestConfiguration {        @Autowired        private RunCache runCache;        @Autowired        private RunClient runClient;        @Bean        ConcurrentKafkaListenerContainerFactory<Integer, String> kafkaListenerContainerFactory() {            ConcurrentKafkaListenerContainerFactory<Integer, String> factory = new ConcurrentKafkaListenerContainerFactory<>();            factory.setConsumerFactory(consumerFactory());            return factory;        }        @Bean        public ConsumerFactory<Integer, String> consumerFactory() {            return new DefaultKafkaConsumerFactory<>(consumerConfigs());        }        @Bean        public Map<String, Object> consumerConfigs() {            Map<String, Object> props = new HashMap<>();            props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, kafka.getBootstrapServers());            props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");            props.put(ConsumerConfig.GROUP_ID_CONFIG, "m-group");            props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);            props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);            return props;        }        @Bean        public ProducerFactory<String, String> producerFactory() {            Map<String, Object> configProps = new HashMap<>();            configProps.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, kafka.getBootstrapServers());            configProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);            configProps.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);            return new DefaultKafkaProducerFactory<>(configProps);        }        @Bean        public KafkaTemplate<String, String> kafkaTemplate() {            return new KafkaTemplate<>(producerFactory());        }        @Bean        public RunCalculator runCalculator() {            RunCalculatorWithWork runCalculatorWithWork = new RunCalculatorWithWork();            runCalculatorWithWork.runCache = runCache;            runCalculatorWithWork.runClient = runClient;            return runCalculatorWithWork;        }    }}

Перед всеми тестами необходимо стартовать kafka. Это делается вот таким вот образом:


kafka.start();

Дополнительные свойства для kafka в тестах можно задать в ресурсном файле.


application.yml

spring:  kafka:    consumer:      auto-offset-reset: earliest

Ресурсы и ссылки


Код для micronaut


Код для Spring Boot


PART 1: TESTING KAFKA MICROSERVICES WITH MICRONAUT


Testing Kafka and Spring Boot


Micronaut Kafka


Spring for Apache Kafka

Подробнее..

Envoy как универсальный сетевой примитив

05.02.2021 00:16:03 | Автор: admin

В октябре прошлого года мои коллеги представили на EnvoyCon доклад "Построение гибкой подсистемы компрессии в Envoy". Вот он ниже



Судя по статистике сегодняшней статьи от SergeAx, тема компрессии сетевого трафика оказалась интересной многим. В связи с чем я немедленно возжелал вселенской славы и решил кратко пересказать содержание доклада. Тем более, что он не только о компрессии, но и том, как можно упростить сопровождение сетевой подсистемы как backend'а, так и мобильного frontend'а.


Я не стал полностью "новелизировать" видео доклада, а только ту часть, которую озвучил Хосе Ниньо. Она заинтересует больше людей.


Для начала о том, что такое Envoy.


В описании на официальном сайте значится следующее. Envoy это высокопроизводительный распределённый прокси-сервер, спроектированный как для отдельных сервисов и приложений, так и для работы в качестве коммуникационной шины в микросервисной архитектуре, то есть в сервис-мэшах, с учётом уроков вынесенных из эксплуатации NGINX, HAProxy И так далее.



Принцип работы прокси-сервера очень прост: клиент устанавливает сетевое соединение не напрямую с сервисом, а через прокси-сервер, который в случае необходимости, например, сжимает проходящий через него сетевой трафик. Или оконечивает TLS. Или, не разрывая соединения с клиентом, пытается повторно соединиться с временно недоступным сервисом. Или, если совсем всё плохо, соединиться с резервным сервисом. И так далее.
Главное в том, что подобная схема заметно снижает сложность кода и клиента, и сервиса. Особенно сервиса.



Однако, если перед клиентом поставить ещё один прокси-сервер, как на картинке ниже, то и его код упростится так же сильно. Это основа работы сервис-мэшей вся сложность, необходимая для надёжной и эффективной коммуникации, включая аппаратную акселерацию, вынесена из сервисов и изолирована в прокси.



Таким образом общая архитектура в очень многих компаниях всё чаще начинает выглядеть примерно вот так.



Мобильный клиент общается с граничным прокси (Edge), который решает, куда отправлять клиентские запросы дальше, попутно балансируя нагрузку на сервера. Сервисы получают запросы от Edge не напрямую, а через вспомогательные прокси (Sidecar). Далее, сервисы формируют ответы, опционально пообщавшись друг с другом, и отсылают их к Edge.


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



Кроме того, таких мобильных клиентов может быть несколько, если хочется поддерживать не только Android. В общем, ребята в Lyft сообразили, что было бы неплохо превратить мобильные клиенты в обычные узлы сервис-мэша и унифицировать сетевой стек, используя Envoy как универсальный сетевой примитив. Тогда экспериментировать с алгоритмами компрессии, политиками реконнекта и т.д. нужно будет только в одном месте, а не в трёх. При этом
даже сетевой код трогать не придётся, достаточно доставить по месту употребления нужную конфигурацию, а код Envoy всё сделает сам не разрывая текущих соединений.



Так появился проект Envoy Mobile, который представляет собой байндинги на Java, Kotlin, Swift, Objective-C к Envoy. А тот уже линкуется к мобильному приложению как нативная библиотека.


Тогда задача уменьшения объёма трафика описанная в статье от FunCorp, могла бы быть решена примерно как на картинке ниже (если поменять местами компрессор и декомпрессор, и заменить response на request). То есть даже без необходимости установки обновлений на телефонах.



Можно пойти дальше, и ввести двустороннюю компрессию



В общем, поле для экспериментов в такой схеме сильно увеличивается в размере.

Подробнее..

Перевод Лучшие фреймворки для микросервисов

21.06.2021 16:11:25 | Автор: admin

Выберите правильный фреймворк для архитектуры микросервисов

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

Преимущества микросервисов

  • Внедрение новых технологий и процессов.

  • Независимое масштабирование приложений.

  • Готовность к облачным вычислениям.

  • Безупречная интеграция.

  • Эффективное использование аппаратного обеспечения.

  • Безопасность на уровне услуг.

  • Функции на базе API для эффективного повторного использования.

  • Независимая разработка и развертывание приложений.

Критерии выбора фреймворка

Ниже перечислены некоторые критические аспекты, которые необходимо учитывать при выборе подходящего фреймворка:

  • Популярность измеряется показателем признания фреймворка в отрасли, основанным на количестве клиентов, которые сделали его (фреймворк) стандартом предприятия. Другими важными аспектами являются наличие документации и количество квалифицированных специалистов, доступных на рынке.

  • Зрелость сообщества репутация поддерживающих фреймворк компаний, таких как Apache, Google или Spring. Зрелость фреймворка с точки зрения поддержки сообщества / коммерческой поддержки и частоты выпуска релизов для устранения проблем и добавления новых функций.

  • Простота разработки Фреймворки облегчают разработку приложений и повышают производительность разработчиков. IDE (Integrated Development Environment) и инструменты, поддерживающие фреймворки, также играют существенную роль в быстрой разработке приложений.

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

  • Поддержка архитектуры Фреймворки предоставляют модули кода и интерфейсы со встроенными шаблонами проектирования, которые облегчают разработчикам приложений сложность кодирования.

  • Поддержка автоматизации Фреймворк поддерживает автоматизацию задач, связанных со сборкой и развертыванием микросервисов.

  • Независимое развертывание Фреймворк должен поддерживать все аспекты независимого развертывания - прямую и обратную совместимость, многократное использование и переносимость.

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

Для разработки микросервисов доступны различные фреймворки в соответствии с требованиями проекта. Java, Python, C++, Node JS и .Net вот несколько языков для разработки микросервисов. Давайте подробно рассмотрим языки и связанные с ними фреймворки, которые поддерживают разработку микросервисов.

На приведенной ниже диаграмме показаны различные фреймворки, связанные с каждым языком, популярным в 2021 году и так далее.

Фреймворки для микросервисов (Microservices Frameworks)

1. Java

Существует несколько фреймворков для разработки архитектуры микросервисов с использованием языка программирования Java:

  • Spring Boot Spring Boot это популярный фреймворк микросервисов на Java. Позволяет создавать как небольшие, так и крупномасштабные приложения. Spring boot легко интегрируется с другими популярными фреймворками с помощью инверсии управления.

  • Dropwizard фреймворк Dropwizard используется для разработки удобных, высокопроизводительных и Restful веб-сервисов. Без дополнительных настроек поддерживает инструменты конфигурации, метрики приложения, протоколирования и работы.

  • Restlet фреймворк Restlet следует архитектурному стилю RST, который помогает Java-разработчикам создавать микросервисы. Принят и поддерживается Apache Software License.

  • Helidon Коллекция библиотек Java для написания микросервисов. Простой в использовании, с инструментальными возможностями, поддержкой микропрофилей, реактивным веб-сервером, наблюдаемый и отказоустойчивый.

  • AxonIQ Событийно-ориентированный фреймворк микросервисов с открытым исходным кодом, сфокусированный на Command Query Responsibility Segregation (CQRS), Domain-Driven Design (DDD) и скоринге событий.

  • Micronaut full-stack фреймворк на основе JVM для построения модульных, легко тестируемых микросервисных и бессерверных приложений. Создает полнофункциональные микросервисы, включая внедрение зависимостей, автоконфигурацию, обнаружение служб, маршрутизацию HTTP и клиент HTTP. Micronaut стремится избежать недостатков фреймворков Spring, Spring Boot, обеспечивая более быстрое время запуска, уменьшение объема памяти, минимальное использование рефлексии и спокойное юнит-тестирование.

  • Lagom Реактивный фреймворк микросервисов с открытым исходным кодом для Java или Scala. Lagom базируется на Akka и Play.

2. GoLang

Доступно несколько фреймворков для разработки архитектуры микросервисов с использованием языка программирования Go

  • GoMicro подключаемая библиотека RPC предоставляет фундаментальные строительные блоки для написания микросервисов на языке Go. Поддерживаются API-шлюз, интерактивный CLI, сервисный прокси, шаблоны и веб-панели.

3. Python

Доступно несколько фреймворков для разработки архитектуры микросервисов с использованием языка программирования Phyton:

  • Flask Web Server Gateway Interface (WSGI) Веб-ориентированный легкий фреймворк микросервисов на языке Phyton. Flask-RESTPlus - расширение для Flask, которое предоставляет поддержку для быстрого создания REST API.

  • Falcon веб-фреймворк API для построения надежных бэкендов приложений и микросервисов в Phyton. Фреймворк отлично работает как с асинхронным интерфейсом шлюза сервера (ASGI), так и с WSGI.

  • Bottle Быстрый, легкий и простой WSGI микросервисный веб-фреймворк на основе Phyton. Распространяется одним файловым модулем и не имеет зависимостей, кроме стандартной библиотеки Python.

  • Nameko Фреймворк Nameko для построения микросервисов на Phyton со встроенной поддержкой RPC через AMQP, асинхронных событий, HTTP GET и POST, а также WebSocket RPC.

  • CherryPy CherryPy позволяет разработчикам создавать веб-приложения, используя объектно-ориентированное программирование на Python.

4. NodeJS

Существует несколько фреймворков для разработки архитектуры микросервисов с использованием языков программирования NodeJS

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

5. .NET

ASP.Net, фреймворк, используемый для веб-разработки и делающий ее API. Микросервисы поддерживают встроенные функции, для их (микросервисов) построения и развертывания с помощью контейнеров Docker.

6. MultiLanguage

Существует несколько фреймворков для разработки архитектуры микросервисов с использованием нескольких языков

  • Spark создание веб-приложений микросервисов с использованием Kotlin и Java. Выразительный и простой веб-фреймворк DSL на Java/Kotlin, созданный для быстрой разработки.

Заключение

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

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


Перевод подготовлен в рамках курса "Microservice Architecture".

Всех желающих приглашаем на вебинар Атрибуты качества, тактики и паттерны. На этом открытом уроке рассмотрим, что такое качественная архитектура, основные атрибуты качества и тактики работы с ними.

Подробнее..

8 Kubernetes-инсайтов, шпаргалка по Curl и онлайн-курс Разработка облачных приложений с микросервисными архитектурами

28.01.2021 20:21:55 | Автор: admin

Мы приготовили для вас много новых инсайтов, записей важных вебинаров, книжек и шпаргалок. Прокачивайте скилы, читайте, смотрите, думайте, применяйте на практике! Станьте частью DevNation!

Начни новое:

Скачать:

  • Шпаргалка по команде Curl
    Примеры использования и синтаксис curl, включая ее использование для запроса API.

  • Шпаргалка по базовым вещам Podman

  • Debezium на OpenShift
    Debezium это распределенная опенсорсная платформа для отслеживания изменений в данных. Благодаря ее надежности и скорости ваши приложения смогут реагировать быстрее и никогда не пропустят события, даже если что-то пойдет на так. Наша шпаргалка поможет с развертыванием, созданием, запуском и обновление DebeziumConnector на OpenShift.
    Загрузить шпаргалку

Чем заняться на досуге:

Мероприятия:

  • 28 января, DevNation: The Show
    Еженедельный часовой чат в прямом эфире. Как обычно, в программе свежие новости и интерактивная игра для участников.

  • DevNation Deep Dive: Kubernetes
    Поспешите, поезд Kubernetes уже отправляется узнайте, как применять, развертывать и использовать Kubernetes для решения задач, с которые вы сталкиваетесь в облаке .

Смотри в записи:

  • Вебинар DevNation Tech Talk Сборка kubectl-плагина с помощью Quarkus
    Разбираем, как с нуля спроектировать kubectl-плагин и собрать его, используя Quarkus. Также рассмотрим удобную работу с Kubernetes-кластером с использованием нативной компиляции для получения сверхбыстрых бинарников и расширений для Kubernetes-клиента.

  • jconf.dev
    Бесплатная виртуальная Java-конференция прямо у вас на экране: четыре техно-трека с нашими комьюнити-экспертами по Java и облаку, 28 углубленных сессий и два потрясающих основных доклада.

  • AnsibleFest
    Два дня интереснейших докладов, демонстраций и практических занятий. Отличная возможность узнать, как разработчики, администраторы и ЛПР в сфере ИТ отвечают на вызовы перемен с помощью гибких технологий автоматизации с открытым кодом, которые позволяют перейти от того, что есть, к тому, что нужно.

  • J4K Conference
    Новая виртуальная конференция по Kubernetes, Java и облаку: 17 сессий с сотрудниками Red Hat, включая доклад Марка Литтла (Mark Little), главного человека в Red Hat по связующему ПО.

По-русски:

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru