Проектирование API

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

Проектирование API для микрослужб.

Необходимо различать два типа API:

  • Общедоступные API, вызываемые клиентскими приложениями
  • Внутренние API для взаимодействия между службами

Эти два типа имеют разные требования. Общедоступный API должен быть совместим с клиентскими приложениями, такими как браузерные приложения или собственные мобильные приложения. Большинство общедоступных API используют REST по протоколу HTTP. Но внутренние API-интерфейсы должны учитывать производительность сети. В зависимости от детализации служб взаимодействие между службами может привести к слишком большому сетевому трафику. Службы могут быстро стать привязанными к ввода-выводам, поэтому рекомендации, такие как скорость сериализации и размер полезных данных, становятся более важными. Некоторые популярные варианты REST по протоколу HTTP включают удаленный вызов процедуры gRPC (gRPC), Apache Avro и Apache Thrift. Эти протоколы поддерживают двоичную сериализацию и повышают эффективность по сравнению с HTTP.

Соображения

При выборе способа реализации API следует учитывать следующие факторы:

  • REST и удаленный вызов процедуры (RPC): Рассмотрим компромиссы между интерфейсом в стиле REST и интерфейсом rPC.

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

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

    Для интерфейса RESTful большинство команд выбирают REST по протоколу HTTP через JSON. Для интерфейса в стиле RPC популярные платформы включают gRPC, Avro и Thrift.

  • Эффективность: Учитывайте эффективность с точки зрения скорости, памяти и размера загружаемых данных. Обычно интерфейс на основе gRPC быстрее, чем REST по протоколу HTTP.

  • Язык определения интерфейса (IDL): Используйте IDL для определения методов, параметров и возвращаемых значений API. IDL может создавать клиентский код, код сериализации и документацию по API. Средства тестирования API используют языки описания интерфейсов. Платформы, такие как gRPC, Avro и Thrift, определяют собственные спецификации IDL. REST по протоколу HTTP не имеет стандартного формата IDL, но распространенный вариант — OpenAPI (прежнее название — Swagger). Вы также можете создать REST API HTTP без использования языка формального определения, но вы теряете преимущества создания и тестирования кода.

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

  • Поддержка платформы и языка: Почти каждая платформа и язык поддерживают HTTP. Avro, gRPC и Thrift предоставляют библиотеки для C++, C#, Java и Python. Thrift и gRPC также поддерживают Go.

  • Совместимость и взаимодействие: При выборе протокола, например gRPC, может потребоваться уровень перевода протокола между общедоступным API и серверной частью. Шлюз может выполнять такую функцию. Если вы используете сетку службы, проверьте совместимость протокола с сеткой службы. Например, Linkerd имеет встроенную поддержку HTTP, Thrift и gRPC.

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

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

Проектирование API RESTful

Следующие ресурсы помогут вам разработать API RESTful:

Обратите внимание на следующие факторы:

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

  • Для различных типов клиентов, таких как мобильные приложения и классические веб-браузеры, могут потребоваться различные размеры полезных данных или шаблоны взаимодействия. Рекомендуется использовать паттерн Backends for Frontends для создания отдельных серверных частей для каждого клиента. Каждый серверная часть предоставляет оптимальный интерфейс для данного клиента.

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

  • Методы HTTP могут иметь асинхронную семантику, где метод немедленно возвращает ответ, но служба выполняет операцию асинхронно. В этом случае метод должен возвращать код ответа HTTP 202 . Этот код указывает, что запрос был принят для обработки, но еще не обработан. Для более подробной информации, см. статью о шаблоне "Асинхронный Запрос-Ответ".

API универсального доступа к данным: рекомендации по OData и GraphQL

ИНТЕРФЕЙСы REST API предоставляют структурированный подход для предоставления ресурсов, но для некоторых сценариев требуется более гибкие шаблоны доступа к данным. Интерфейсы API, ориентированные на запросы, такие как OData и GraphQL, предоставляют альтернативные варианты, позволяющие клиентам точно указывать необходимые данные. Этот подход может снизить избыточный запрос данных и улучшить производительность. Эти типы API определяют приоритет операций чтения. Операции изменения, такие как создание, обновление и удаление, могут быть более сложными для реализации, но различные платформы могут эффективно управлять этими операциями.

Когда следует учитывать универсальные API доступа к данным

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

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

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

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

Избегайте универсальных API доступа к данным в следующих ситуациях:

  • Архитектура микрослужб подчеркивает строгие границы служб и инкапсуляцию домена.

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

  • API в основном поддерживают простые операции создания, чтения, обновления и удаления (CRUD) или хорошо определенных бизнес-рабочих процессов.

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

  • Требования к безопасности требуют явных определений конечных точек для минимизации поверхностей атак.

  • Ваша команда не имеет опыта реализации и оптимизации языка запросов.

Сопоставление шаблонов REST с DDD

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

export class Location {
    readonly latitude: number;
    readonly longitude: number;

    constructor(latitude: number, longitude: number) {
        if (!Number.isFinite(latitude) || latitude < -90 || latitude > 90) {
            throw new RangeError('latitude must be between -90 and 90');
        }
        if (!Number.isFinite(latitude) || longitude < -180 || longitude > 180) {
            throw new RangeError('longitude must be between -180 and 180');
        }
        this.latitude = latitude;
        this.longitude = longitude;
    }
}

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

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

Схема репозитория дронов.

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

Этот пример применимо к авиационной и аэрокосмической промышленности.

Схема службы дронов.

На схеме показано взаимодействие между службой планировщика и службой дронов, которая взаимодействует с хранилищем данных. Слева сервис планировщика отправляет GET-запрос HTTP в сервис дрона с конечной точкой /api/drone. Стрелка указывает обратно в службу планировщика, помеченную ответом, который содержит JSON, за которым следует три периода. Служба дронов содержит обработчик HTTP-запросов и репозиторий. Обработчик HTTP-запросов получает запрос GET. Репозиторий подключается к хранилищу данных. Две стрелки указывают двунаправленную связь между репозиторием и хранилищем данных. Внутри службы дронов две стрелки указывают двунаправленное взаимодействие между обработчиком HTTP-запросов и репозиторием. Стрелка из репозитория в обработчик HTTP-запросов помечена дроном (сущностью), который представляет объект домена, возвращаемый репозиторием.

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

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

В следующих примерах показано, как основные понятия REST соответствуют общим конструкциям DDD:

  • Агрегаты сопоставляют ресурсы в REST естественным образом. Например, API для доставки может предоставлять ресурс в виде агрегата доставки.

  • Агрегаты определяют границы согласованности. Операции с агрегатами не должны оставлять агрегат в несогласованном состоянии. Избегайте создания API, которые позволяют клиенту управлять внутренним состоянием агрегата. Вместо этого следует отдавать предпочтение крупнозернистым API, которые предоставляют агрегаты как ресурсы.

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

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

  • Объекты значений неизменяемы. Чтобы выполнить обновления, замените весь объект значения. В REST реализуйте обновления через PUT или PATCH запросы.

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

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

Концепция DDDD Эквивалент REST Example
Агрегат Resource { "1":1234, "status":"pending"... }
Идентичность URL https://delivery-service/deliveries/1
Дочерние объекты Links { "href": "/deliveries/1/confirmation" }
Обновление объектов значений PUT или PATCH PUT https://delivery-service/deliveries/1/dropoff
Репозиторий Коллекция https://delivery-service/deliveries?status=pending

Управление версиями API

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

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

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

Схема с двумя вариантами поддержки управления версиями.

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

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

При изменении реализации службы помечайте изменение версией. Версия содержит важные сведения для устранения ошибок. Этот подход поддерживает анализ первопричин, так как вы знаете, какая версия службы вызывается. Рассмотрите возможность использования семантического управления версиями для версий служб. Семантическое версиирование использует формат МЕДЖОР.МИНОР. ПАТЧ. Но клиенты должны выбирать API только по номеру основной версии или дополнительной версии, если существуют значительные, но неразрывные изменения между дополнительными версиями. Например, клиенты могут выбирать между версией 1 и версией 2 API, но они не должны выбирать версию 2.1.3. Если вы разрешаете этот уровень детализации, вы рискуете поддерживать слишком много версий.

Дополнительные сведения см. в статье "Реализация управления версиями для веб-API RESTful".

Идемпотентные операции

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

Спецификация HTTP указывает, что методы GET, PUT, и DELETE должны быть идемпотентными. POST методы не гарантируется быть идемпотентными. POST Если метод создает новый ресурс, как правило, нет гарантии, что операция является идемпотентной. Спецификация определяет идемпотент следующим образом:

Метод запроса считается идемпотентным , если предполагаемое влияние на сервер нескольких идентичных запросов с этим методом совпадает с эффектом для одного такого запроса. (RFC 7231)

Понять разницу между PUT и POST семантикой при создании новой сущности. В обоих случаях клиент отправляет представление сущности в тексте запроса. Но значение универсального идентификатора ресурса (URI) отличается.

  • POST Для метода URI представляет родительский ресурс новой сущности, например коллекцию. Например, чтобы создать новую доставку, URI может быть /api/deliveries. Сервер создает сущность и назначает ей новый URI, например /api/deliveries/39660. Этот URI возвращается в Location заголовке ответа. Каждый раз, когда клиент отправляет запрос, сервер создает новую сущность с новым универсальным кодом ресурса (URI).

  • PUT Для метода URI определяет сущность. Если у существующей сущности есть этот универсальный код ресурса (URI), сервер заменяет существующую сущность версией в запросе. Если сущность не использует этот универсальный код ресурса (URI), сервер создает его. Например, предположим, что клиент отправляет запрос PUT в api/deliveries/39660. Если ресурс доставки не использует этот URI, сервер создает новый. Если клиент снова отправляет тот же запрос, сервер заменяет существующую сущность.

Служба доставки использует следующий код для реализации PUT метода:

[HttpPut("{id}")]
[ProducesResponseType<Delivery>(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<IActionResult> Put([FromBody]Delivery delivery, string id)
{
    logger.LogInformation("In Put action with delivery {Id}: {@DeliveryInfo}", id, delivery.ToLogInfo());
    try
    {
        var internalDelivery = delivery.ToInternal();

        // Create the new delivery entity.
        await deliveryRepository.CreateAsync(internalDelivery);

        // Create a delivery status event.
        var deliveryStatusEvent = new DeliveryStatusEvent { DeliveryId = delivery.Id, Stage = DeliveryEventType.Created };
        await deliveryStatusEventRepository.AddAsync(deliveryStatusEvent);

        // Return HTTP 201 (Created)
        return CreatedAtRoute("GetDelivery", new { id= delivery.Id }, delivery);
    }
    catch (DuplicateResourceException)
    {
        // This method mainly creates deliveries. If the delivery already exists, update it.
        logger.LogInformation("Updating resource with delivery id: {DeliveryId}", id);

        var internalDelivery = delivery.ToInternal();
        await deliveryRepository.UpdateAsync(id, internalDelivery);

        // Return HTTP 204 (No Content)
        return NoContent();
    }
}

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

Следующий шаг

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