Из этой статьи вы узнаете об основных наиболее часто используемых типах интеграции приложений и распределенных систем, таких как REST, gRPC, Kafka, RabbitMQ, WebSocket.

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

Взаимодействие между приложениями. Интеграции.

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

Интеграция — процесс взаимодействия независимых приложений. Основная цель интеграции — обеспечить обмен информацией между приложениями.

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

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

  • Асинхронное взаимодействие — неблокирующее взаимодействие, при котором клиент может продолжать работу после отправки запроса.

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

Интеграция с помощью передачи файлов и интеграция через общую базу данных
Интеграция с помощью передачи файлов и интеграция через общую базу данных
Интеграция с помощью RPC, Messaging и API
Интеграция с помощью RPC, Messaging и API
  • Передача файлов (File Transfer) — приложение обменивается файлами с данными с другим приложением.

  • Общая база данных (Shared Database) — приложения обмениваются данными через общую базу данных.

    Удалённый вызов процедур (Remote Procedure Call — RPC) — каждое приложение описывает для других приложений все процедуры, которые можно вызвать удалённо, чтобы обменяться данными или выполнить запрос. Процедурой называют часть программного кода, которая выполняет некоторую функцию. Под вызовом процедуры понимают выполнение функции.

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

  • API — приложения обмениваются друг с другом посредством программных интерфейсов.

REST API, json-schema, OpenAPI

REST API

REST (Representational State Transfer) — архитектурный стиль, описывающий ограничения для проектирования приложений, использующих протокол HTTP (HTTPS) для передачи данных.

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

REST API
REST API

RESTful описывает принципы проектирования приложений, множество из которых — ограничения протокола HTTP, с которыми вы уже знакомы:

  • клиент‑серверная архитектура;

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

  • по возможности использовать кэширование данных — кэширование может выполняться как на стороне клиента, так и на стороне сервера. Кэширование может увеличить производительность за счёт использования сохранённых ранее данных, однако стоит учитывать возможность их устаревания;

Кеширование на стороне сервера
Кеширование на стороне сервера
  • ресурс должен иметь уникальный постоянный идентификатор (URL), который не может измениться за время обмена данными или в результате изменения его состояния. Если ресурсу присваивается другой идентификатор, сервер обязан сообщить клиенту, что запрос был неудачным и дать ссылку на новый адрес;

  • использовать унифицированный интерфейс (Hypermedia as the Engine of Application State — HATEOAS) — унифицированный HTTP‑интерфейс определяет CRUD (Create, Read, Update, Delete) — операции для создания, чтения, обновления и удаления ресурсов в виде HTTP‑методов POST, GET, PUT, DELETE;

  • использовать коды состояния (status code) — HTTP‑ответы должны включать коды состояний, например, 200 OK, 400 Bad Request, 502 Bad Gateway.

При проектировании API важно не забывать про идемпотентность методов HTTP.

Идемпотентные методы — это методы, которые либо не изменяют состояние в базе данных, либо изменяют состояние только при первом запросе. В случае повторной отправки идентичного запроса, состояние в базе данных не изменяется. Идемпотентными методами являются: GET, PUT, DELETE, HEAD и OPTIONS.

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

Json-schema

JSON Schema — один из языков описания структуры JSON‑объекта, использующий синтаксис JSON. Он используется для определения и проверки структуры данных, а также для автоматической генерации кода и документации.

Пример описания JSON (json-schema)
Пример описания JSON (json-schema)

JSON Schema состоит из нескольких ключевых элементов:

  • Тип данных: Определяет тип данных, который может быть представлен в JSON Schema. Например, string, number, boolean, object и array.

  • Обязательность значения: Указывает, является ли значение обязательным или необязательным. Обязательные значения обозначаются символом required, а необязательные — отсутствием этого символа.

  • Минимальное и максимальное значения: Определяют минимальное и максимальное допустимые значения для числовых типов данных.

  • Минимальная и максимальная длины: Определяют минимальную и максимальную допустимые длины для строк.

  • Допустимые значения: Определяют список допустимых значений для данного поля.

  • Дополнительные свойства: Определяют, могут ли быть добавлены дополнительные свойства в объект или массив.

  • Дополнительные ограничения: Определяют дополнительные ограничения на структуру данных, такие как уникальность ключей или порядок элементов в массиве.

Подробнее узнать о json‑schema можно здесь, а провалидировать ее корректность можно, например, тут.

OpenAPI

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

В качестве формата используется XML и JSON, но в общем случае может быть выбран и другой удобный язык разметки — YAML.

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

Официальное описание OpenAPI доступно на github или тут.

Пример спецификации с пояснениями приведен на изображении ниже.

Пример описания OpenAPI
Пример описания OpenAPI

Спецификация OpenAPI состоит из следующих базовых объектов:

  • /pay — имя конечной точки (эндпоинта, «ручки») — url, на который отправляются запросы. Для каждого эндопинта можно прописать набор HTTP‑методов (GET, POST, PUT, PATCH, DELETE);

  • post — HTTP‑метод (в данном случае POST);

  • parameters — список параметров, передаваемых в запросе;

    • name — имя параметра

    • in — где параметр передается (path, header, query, cookie)

  • requestBody — тело запроса;

  • responses — список возможных ответов на запрос;

  • 200 — HTTP код статуса

  • components — секция, содержащая компоненты (объекты) для удобного переиспользования в YAML‑коде.

  • $ref является ссылкой на компонент (в components), где определяется объект ответа/запроса. Это сделано для облегчения чтения YAML‑кода и его переиспользования.

Также присутствует:

  • tags— теги, используемые для маркировки группы эндпоинтов;

  • summary — короткое описание;

  • description — описание объекта;

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

Удобный инструмент для описания спецификации в формате OpenAPI — swagger.io (из РФ доступен с VPN).

Плюсы использования OpenAPI:

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

  1. OpenAPI позволяет ускорить описание большого количества методов, особенно с повторяющимися объектами.

  2. Из спецификации OpenAPI можно сгенерировать код сервера и клиента, а также объекты данных.

  3. Swagger можно использовать при тестировании.

  4. Cпецификация выглядит очень наглядно, информативно и интерактивно, по сравнению с ее описанием в любом текстовом редакторе.

gRPC, HTTP/2, Protobuf.

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

В качестве формата обмена данными gRPC использует Protobuf (Protocol Buffers), в качестве транспортного протокола использует HTTP 2.0.

HTTP/2 — вторая крупная версия сетевого протокола HTTP. Поддерживает двунаправленную потоковую передачу в одном TCP‑соединении, позволяет группировать запросы во фреймы с возможность приоритезации, а затем объединять (мультиплексировать) и эти фреймы.

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

gRPC и Protobuf
gRPC и Protobuf

gRPC предоставляет четыре типа потоковой передачи:

  • Унарный (Unary) — позволяет клиенту отправить один запрос и получить один ответ от сервера. Подходит для ситуаций, когда клиенту нужно выполнить операцию и получить результат.

  • На стороне сервера (Server Streaming) — клиент отправляет запрос на сервер, а сервер возвращает поток ответов клиенту. Например, когда запрошена лента новостей и сервер отправляет новости по мере их появления.

  • На стороне клиента (Client Streaming): клиент отправляет поток сообщений запросов на сервер. Сервер возвращает один ответ клиенту. Полезно при передаче большого объема данных или логировании событий.

  • Двунаправленный (Bidirectional Streaming) — клиент и сервер передают потоки данныъ друг другу. Клиент это тот, кто инициирует такой вид двунаправленной потоковой передачи. Клиент также завершает соединение. Этот тип передачи подходит, например, для чата или передачи видеопотока.

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

Генерируемый код действует как прослойка между логикой приложения и библиотекой gRPC. Этот код, используемый клиентом, называется стабом (stub, заглушка).

Критерий выбора gRPC

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

Так как gRPC:

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

  2. Самый распространённый формат данных, используемый в gRPC‑интеграциях — Protobuf является бинарным и более легковесным, а следовательно также снижает время и затраты на его сериализацию/десереализацию и отправку по сети, в то время как, например, JSON в случае с REST API является текстовым форматом, более «тяжёлым» для указанных операций.

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

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

Есть два сервиса, один — отправляющий какие‑либо метрики, второй — принимающий и обрабатывающий их.

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

Что еще почитать: отличные статьи от Яндекса и от OTUS на Хабре.

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

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

Для целей тестирования API существует несколько удобных инструментов типа Postman, Insomnia и другие.

Неплохая обзорная статья на Хабре об основах работы с Postman.

В общем виде задача тестирования API — убедиться, что оно работает правильно (возвращает ожидаемый ответ на запрос пользователя или соответствующую ошибку при некоректном запросе).

Брокеры сообщений. Kafka, RabbitMQ.

Apache Kafka и RabbitMQ это распределённые программные брокеры сообщений. Их использование лежит в основе событийно‑ориентированной (event‑driven) архитектуры взаимодействия сервисов.

Такие системы обычно состоят из трёх базовых компонентов:

  • сервер (брокер), обрабатывающий сообщения;

  • продюсеры, которые отправляют сообщения в некую именованную очередь, заранее сконфигурированную администратором на сервере;

  • консюмеры, которые считывают те же самые сообщения по мере их появления.

Продюсеры ничего не знают о консюмерах при записи данных в брокер, а консюмеры ничего не знают о продюсерах данных.

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

Существует две модели работы брокеров:

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

  • push‑модель — продюсеры самостоятельно посылает потребителю новую порцию данных. По такой модели, например, работает RabbitMQ. Она снижает задержку обработки сообщений и позволяет балансировать распределение сообщений по консюмерам.

Существует несколько моделей доставки сообщений:

  • At least once (хотя бы один раз — допускаются дубликаты сообщений): продюсер отправляет сообщение в очередь и ждет подтверждения от брокера. Консюмер извлекает сообщение из очереди и отправляет подтверждение о его обработке. Если продюсер не получает подтверждения от брокера, он повторно отправляет сообщение. Сообщение будет обработано хотя бы один раз, но при этом возможны ситуации, когда одно и то же сообщение будет обработано несколько раз. Для предотвращения дублирования сообщений необходимо, чтобы компоненты были идемпотентными, то есть повторная обработка одного и того же сообщения не приводила бы к изменению состояния системы или ее данных. Также можно использовать уникальные идентификаторы для сообщений и хранить список уже обработанных сообщений.

  • At most once (не более чем один раз, но может не быть доставлено вообще — допускаются потери сообщений): продюсер отправляет сообщение в очередь и не ждёт подтверждения от брокера. Консюмер извлекает сообщение из очереди и не отправляет подтверждение о его обработке. Исключается возможность дублирования сообщений, но при этом сообщение может быть потеряно.

  • Exactly once (строго только один раз — строго одно сообщение): брокер гарантирует, что каждое сообщение будет доставлено и обработано ровно один раз. Продюсер отправляет сообщение в очередь и ждет подтверждения от брокера о его приеме. Консюмер извлекает сообщение из очереди и отправляет подтверждение об обработке.

Kafka

Apache Kafka — распределённый программный брокер сообщений используемый с целью обработки потоковых данных в реальном времени с высокой пропускной способностью и низкой задержкой.

Интеграция с помощью Kafka
Интеграция с помощью Kafka

Кластер Kafka состоит из брокеров.

Kafka хранит сообщения, которые поступают от других процессов, называемых «продюсерами» (producers), в формате «ключ — значение».
Ключ (Key) может быть любой: числовой, строковый, объект или вовсе пустой.
Значение (Value) тоже может быть любым — числом, строкой или объектом в своей предметной области, который можно сериализовать (JSON, Protobuf и т. д.) и хранить.

В сообщении продюсер может указать время (timestamp), либо за него это сделает брокер в момент приёма сообщения.

Заголовки (Headers) выглядят как в HTTP‑протоколе — строковые пары ключ‑значение, и могут использоваться для передачи метаданных.

Данные в Kafka могут быть разбиты партиции (partitions) в рамках разных топиков (topics). Увеличение партиций увеличивает параллелизм чтения и записи. Партиции находятся на одном или нескольких брокерах, что позволяет кластеру масштабироваться.
Внутри партиции сообщения строго упорядочены по своим порядковым номерам (offset), а также индексируются и сохраняются вместе с временем создания.

О репликации данных между партициями

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

Записанные в лидера данные автоматически реплицируются фолловерами внутри кластера Kafka. Они подключаются к лидеру, читают данные и асинхронно сохраняют к себе в локальное хранилище.

Консюмеры со своей стороны также читают из лидерской партиции — это позволяет достичь консистентности при работе с данными.

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

У каждого из брокеров есть локальное хранилище, данные в котором могут устаревать по времени или по размеру (настроить это можно глобально или индивидуально в каждом топике).

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

Kafka поддерживает все три формата доставки сообщений: At most once, at least once, exactly once.

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

  • описании форматов данных — чаще всего это JSON;

  • определении количества топиков исходя из бизнес‑требований и архитектуры системы. Например, для разных типов данных (логирование, транзакции, события) могут требоваться отдельные топики. Разделение данных по топикам помогает изолировать различные потоки данных, улучшая управляемость и безопасность;

  • определении параметров топика: названия топика, количества партиций (разделов), времени хранения данных или ограничения на объем хранимых данных.

  • определение стратегии партиционирования:

    • хэширование (стратегия по умолчанию, когда для распределения между партициями используется хэш от ключа сообщения), или

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

RabbitMQ

RabbitMQ — программный брокер сообщений на основе стандарта AMQP.

Источником сообщений является внешнее приложение, которое создаёт соединение (connection) по протоколу AMQP и генерирующее сообщения в брокер RabbitMQ для дальнейшей обработки.

Интеграция с помощью RabbitMQ
Интеграция с помощью RabbitMQ

Формат сообщения:

  • headers — заголовки сообщения. Нужны для работы Exchange типа headers, а также для дополнительных возможностей Rabbit типа TTL;

  • routing key — ключ маршрутизации, может быть только один для одного сообщения;

  • delivery_mode — признак персистентности сообщения;

  • payload — полезная нагрузка, в любом сериализуемом формате.

Publisher всегда пишет в exchange с routing key, совпадающим с названием очереди.
Также publisher определяет delivery_mode для каждого сообщения — «признак персистентности». Это значит, что сообщение будет сохранено на диске и не исчезнет в случае перезагрузки брокера RabbitMQ.

Exchange является точкой входа и маршрутизатором всех сообщений (как входящих от Publisher, так и перемещающихся от внутренних процессов в Rabbit).

Выделяют 4 типа Exchange:

  • Direct Exchange: Сообщение отправляется в очередь, если routing key сообщения точно совпадает с именем очереди.

  • Topic Exchange: Этот тип обменника позволяет маршрутизировать сообщения на основе шаблонов ключей маршрутизации.

  • Fanout Exchange: Сообщение отправляется во все очереди, связанных с этим обменником, независимо от ключа маршрутизации.

  • Headers Exchange: Маршрутизация сообщений происходит на основе заголовков (headers) сообщений, а не ключей маршрутизации.

Queue (очередь), представляет из себя последовательное хранилище для необработанных сообщений. Хранение сообщений на диске (persistent) зависит от флага delivery_mode, назначаемым publisher для каждого сообщения.
Durable/Transient — признак персистентности очереди. Durable значит, что exchange сохранится после перезагрузки RabbitMQ. Это значение должно быть указано вместе с delivery_mode=2 (persistent).

Сообщения помещаются в очереди и извлекаются по принципу FIFO (first in, first out).

Есть три типа очередей:

  • Classic — обычная очередь, используется в большинстве случаев.

  • Quorum — аналог классической очереди, но с обеспечением гарантий консистентности, достигаемый кворумом в кластере.

  • Stream — новый вид очередей (начиная с версии RabbitMQ 3.9), аналог Apache Kafka.

Процесс работы RabbitMQ:

  1. Consumer создаёт соединение (connection) по протоколу AMQP и получает сообщения из очереди.

  2. RabbitMQ хранит сообщение до тех пор, пока принимающее приложение не подключится и не получит его из очереди. Потребитель может подтвердить получение сообщения сразу или после того, как полностью обработает его. Как только такое подтверждение получено, сообщение удаляется из очереди.

RabbitMQ поддерживает два формата доставки сообщений: at most once, at least once.

Как правило, необходимый объем работ аналитика при проектировании интеграции с помощью RabbitMQ заключается в:

  • описании форматов данных для Payload — чаще всего это JSON;

  • определении очередей (Queues) и признака их персистентности (вurable/еransient);

  • описании ключей сообщений (routing keys);

  • определении политики обмена (exchange types).

WebSocket

Сокет — название программного интерфейса для обеспечения обмена данными между процессами. Для взаимодействия с помощью стека протоколов TCP/IP используются адреса и порты.

Пара «адрес‑порт» определяет сокет («гнездо», соответствующее адресу и порту). В процессе обмена, как правило, используется два сокета — сокет отправителя и сокет получателя.

Каждый процесс может создать «слушающий» сокет (серверный сокет) и привязать его к какому‑нибудь порту операционной системы.

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

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

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

Если данные статичны или или медленно обновляемые, то использование веб‑сокетов не требуется.

WSS (WebSockets Secure) — это протокол для обмена данными между сервером и клиентом с использованием безопасного соединения, обычно через порт 443 (с использованием протокола HTTPs).

Интеграция с помощью WebSocket
Интеграция с помощью WebSocket

Для установления соединения WebSocket клиент и сервер используют протокол, похожий на HTTP. Запрос отправляется на ws: или wss: URI (аналог http или https).

Сервер отвечает на запрос рукопожатием (handshake) с HTTP‑кодом 101 Switching Protocols, переключается с HTTP на WebSocket и поддерживает двухстороннее соединение в реальном времени.

Сервер может открывать несколько WebSocket‑соединений с одним клиентом или с разными клиентами.

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

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


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

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

Эту и другие статьи по системному анализу и IT‑архитектуре, вы сможете найти в моем небольшом уютном Telegram‑канале: Записки системного аналитика

Комментарии (0)