AsyncAPI для розподілених систем
Всім привіт. Я Сергій Моренець, розробник, викладач, спікер та технічний письменник, хочу поділитися з вами своїм досвідом роботи з такою цікавою темою, як написання документації для розподілених систем, і все, що з цим пов’язане.
Коли ви розробляєте власний API, то один з найважливіших його атрибутів — це грамотна документація (специфікація) з прикладами використання для споживачів. Якщо ви розробляли REST API для ваших проєктів, то швидше за все використовували для документування специфікацію OpenAPI та її реалізацію Swagger. Але такої документації не було для розподіленої архітектури, побудованої на Apache Kafka, включаючи опис типів, що використовуються. Більше того, спочатку взагалі не було універсального способу для документування таких розподілених систем.
Деякі кроки в цьому напрямку зробила компанія Confluent, яка розробила компонент Schema Registry для того, щоб споживачі могли там зберігати та валідувати схеми для повідомлень в Apache Kafka. Але це може використовуватися тільки в рамках Kafka і лише для опису схем.

Ну, і за останні роки у нас накопичилося достатньо досвіду роботи з таким підходом, і ми розглядаємо ці технології на тренінгах з Apache Kafka та мікросервісів. Сподіваюся, що ця стаття буде корисною для всіх, хто займається проєктуванням та розробкою таких систем.
Можливості AsyncAPI
У 2017 році іспанський розробник Фран Мендеc працював у компанії Hitch над мікросервісним застосунком. Коли Мендесу потрібно було написати специфікацію для своєї системи, він виявив, що OpenAPI не підходить для його випадку, а якихось готових рішень для асинхронного API (messaging) просто немає.
Крім того, OpenAPI було заточено під один протокол — HTTP та опис функціональності з точки зору сервера. А архітектура обміну асинхронними повідомленнями має на увазі два типи ролей застосунків (споживачі та відправники) і, до того ж, може використовувати різні протоколи.
Більше того, той самий застосунок може бути і відправником, і одержувачем. Тому Мендес вирішив створити власну специфікацію на базі OpenAPI і назвав її AsyncAPI. Незабаром він перейшов працювати до компанії New Relic, де продовжував роботу над AsyncAPI. Його роботою зацікавилися великі ІТ-компанії, і в 2019 році Мендес пішов у одиночне плавання, щоб повністю присвятити себе AsyncAPI.
Зараз AsyncAPI — це частина Linux Foundation, яку підтримують такі компанії, як IBM, Solace, Postman та Red Hat. Зараз поточна версія 2.6, але активно ведуться роботи над 3.0. Ця фундаментальна версія мала вийти ще у вересні 2022 року, але через велику кількість змін все ще не закінчена.
Щоправда, починаючи з 2017 року, загальна ситуація дещо змінилася. У 2019 році вийшла специфікація CloudEvents, яка дозволяє описувати формат повідомлень, підтримує практично ті ж протоколи, що і AsyncAPI, містить спеціальний SDK для найпопулярніших мов програмування, але в цілому є полегшеною (lite) версією того ж AsyncAPI.
Ось порівняльна картинка, яка показує різницю між OpenAPI та AsyncAPI:

Що нам, розробникам, дає AsyncAPI та які проблеми вона вирішує?
- Написання специфікації для messaging systems (у форматі JSON або YAML).
- Web IDE під назвою AsyncAPI Studio для розробки специфікацій.
- Генератори коду (за допомогою AsyncAPI Generator) для різних платформ.
- Генератори документації.
- Можливість валідації повідомлень.
- Власний CLI.
- Підтримка більшої кількості сучасних протоколів і технологій (AMQP, HTTP, Kafka, STOMP, Mercure, WebSocker, Pulsar, Google Pub/Sub).

Але це ще не все. Зараз розробляється система під назвою AsyncAPI Event Gateway, яка дозволить виконати ті завдання, для яких зазвичай використовують патерн API Gateway:
- валідація, фільтрація та агрегація повідомлень;
- аутентифікація;
- моніторинг.
Вона ще на стадії бета-тестування і поки не готова для виробництва.

AsyncAPI та документи
Ключовим компонентом AsyncAPI є документ (специфікація), який може містити наступні елементи:
- server — по суті це message broker, який отримує дані від producers та надсилає їх одержувачам;
- producer — застосунок, який надсилає повідомлення серверу;
- consumer — застосунок, який підписується на отримання повідомлень на сервері;
- channel — внутрішнє сховище для повідомлень на сервері. Залежно від протоколу це може бути топік, черга, subject або routing key;
- application — producer чи consumer;
- protocol — набір правил, за яким сервер обмінюється даними або відправляє команди до клієнтських застосунків;
- message — об’єкт, який відправляється producers, щоб одержали споживачі. Містить payload і (опціонально) заголовки. Може бути подією, запитом чи командою.

Таким чином, першим кроком у роботі з AsyncAPI є створення такого документа. Його можна написати в будь-якому редакторі, але зручніше це зробити за допомогою AsyncAPI Studio. Це досить сучасний вебредактор, що включає:
- дерево навігації;
- HTML попереднього перегляду;
- візуалізатор message-flow вашого проєкту.

Якщо ви пишете такий документ вперше, не біда, у цій студії є підтримка готових шаблонів:

Створимо документацію для типового платіжного сервісу, який здійснює оплату і повідомляє про це інші послуги за допомогою нотифікацій.
asyncapi: '2.6.0'
info:
title: Payment service
version: '1.0.0'
description: |
Payment service is responsible for create payment requests using
supported payment providers (gateways).
servers:
dev:
url: kafka:9092
protocol: kafka
description: Single-node Kafka cluster using KRaft protocol (without Zookeeper)
defaultContentType: application/json
channels:
payments:
description: This topic contains events about payment processing updates
subscribe:
summary: Inform about payment status changes (success or failure).
operationId: sendPaymentStatus
message:
$ref: '#/components/messages/paymentSuccessEvent'
components:
messages:
paymentSuccessEvent:
name: paymentSuccessEvent
title: Event successfully payed
summary: Inform about success of the payment operation in the payment service.
traits:
- $ref: '#/components/messageTraits/commonHeaders'
payload:
$ref: "#/components/schemas/paymentSuccessPayload"
schemas:
paymentSuccessPayload:
type: object
required:
- id
- entityId
- type
- source
- payload
- createdAt
properties:
id:
type: string
description: Globally unique event identifier. Should be generated by producer
entityId:
type: string
description: Identifier of the entity that is connected by this event
type:
type: string
description: Event discriminator which is unique for each event type
source:
type: string
description: Identifier of the producer of this event (for example, service name)
payload:
type: object
description: Custom optional payload with additional event data
createdAt:
$ref: "#/components/schemas/createdAt"
createdAt:
type: string
format: date-time
description: Date and time when the message was created.
messageTraits:
commonHeaders:
headers:
type: object
properties:
__TypeId__:
type: string
Відразу скажу, що хоча такий документ спирається на певну схему, проте можна розширити його за допомогою Specification Extensions. Тобто ви можете додати свою властивість, але тільки якщо його назва починається з префікса x-, наприклад,
AsyncAPI підтримує два типи визначень у документах (Reference Object та інші). Коли ви використовуєте Reference Object, то просто вказуєте посилання на інший об’єкт (на його ім’я). Наприклад, якщо ви визначили тип createdAt десь у документі, то далі можете посилатися на нього як:
#/components/schemas/createdAt
Або ж, якщо його визначено у зовнішньому файлі definitions.yml, то посилання буде:
definitions.ym#/components/schemas/createdAt
Reference Objects дозволяють перевикористовувати готові визначення (за допомогою елемента $ref) і уникати copy-paste. Всі інші визначення, наприклад, Message Object або Server Object просто містять опис структури вашого об’єкта.
Деякі пояснення щодо написаного документа та його вмісту:
- channels — перерахування Kafka topics, використаних застосунком;
- operationId — унікальна назва (ідентифікатор) операції (яка призводить до надсилання або отримання повідомлення);
- publish — означає, що поточна програма отримує (а не публікує) повідомлення з даного топіка;
- subscribe — означає, що поточний застосунок публікує повідомлення в топіку, які інші застосунки (сервіси) повинні отримати;
- required — перерахування обов’язкових полів для того об’єкта, який ми прикріплюємо до повідомлення;
- traits — додаткова інформація про об’єкт (наприклад, ми вказали заголовки повідомлення за допомогою елемента messageTraits).
Зверніть увагу на незвичне використання термінів publish/subscribe. Щоб це не привело вас до плутанини, раджу прочитати дискусію на цю тему.
Для payload ми використовували тільки одну подію — PaymentSuccessEvent (конструктори видалені з лістингу так як вони несуттєві для даного прикладу):
@NoArgsConstructor
public class PaymentSuccessEvent extends BaseEvent<PaymentDTO> {}
@Getter
@NoArgsConstructor
public abstract class BaseEvent<T> {private String id;
private String entityId;
private String type;
private String source;
private LocalDateTime createdAt;
private T payload;
}
Редактор в Async Studio досить просунутий, тут є і підказки, і можливість вибору за допомогою code completion:

Після цього в панелі HTML Preview можна побачити документацію у тому вигляді, як її можна згенерувати:

А ось як виглядає візуалізація data-flow для нашого сервісу:

Висновки
Таким чином, за допомогою AsyncAPI ми описали у специфікації:
- Повідомлення та їхній вміст, що використовуються (payload, заголовки).
- Бізнес-операції з надсилання/ приймання повідомлень.
- Використані протоколи та Kafka topics.
- Сервери, що використовуються (Kafka brokers).
Безкоштовний вебредактор AsyncAPI Studio дозволяє не лише створювати документи-специфікації, а й переглядати HTML-документацію та потоки даних у рамках поточного проєкту.
Поки що це текстовий файл, який можна переглядати в AsyncAPI Studio. У наступній частині ми поговоримо про інструментарій, який дозволить генерувати код, документацію та автоматизувати роботу з AsyncAPI.
2 коментарі
Додати коментар Підписатись на коментаріВідписатись від коментарівдосить цікаво, дякую за аналіз
Чи підтримується інші формати серіалізації повідомлень, окрім json. Скажімо protobuf, msgpack, etc... Чи можливо якось розширити опис, якщо використовується якийсь кастомний формат?