Привет! Меня зовут Максим Соколов, я — аналитик в команде “Управление доступностью товаров и категорий”. В нашей команде была выделена отдельная подгруппа, которая создавалась специально под новый продукт-фичу для селлеров. Сразу стало понятно, что для реализации нового функционала требуется разработка нового микросервиса. Командой разработки было принято решение интегрироваться по gRPC, но мне до конца не было понятно, почему выбор именно такой. И тут я решил разобраться подробнее!
До этого я занимался анализом доработок по уже существующему функционалу, поэтому не задавался вопросом, почему выбран тот или иной путь реализации API. Но здесь-то всё с нуля, а специалисту, который дебютирует в этом выборе, становится ещё трудней.
Могу сказать, что выбор технологии для проекта — задача непростая, требующая совместного участия разных участников технической команды: аналитик, разработчик, тимлид, архитектор.
На свете много статей про проектирование API, в которых описаны архитектурные стили, протоколы, технологии. Информации — огромное количество, иногда она даже противоречивая, поэтому новичку тяжело подступиться к теме.
В этой статье я хочу дать точку входа для джун/мидл системных аналитиков, которые хотят разобраться в межсервисной интеграции. Мы пройдёмся по HTTP, REST, RPC и gRPC, разберёмся в их значениях. Выясним, почему эти аббревиатуры появляются, когда происходит проектирование API, и попробуем понять, когда и что следует применять.
Также по ходу статьи буду оставлять ссылки на хорошие (по моему мнению) статьи для более глубокого погружения в поднимаемые темы.
API. Каким он должен быть?
Начнём с того, что API (Application Programming Interface) — это программный интерфейс, который позволяет различным программным компонентам взаимодействовать друг с другом. Всё довольно просто: API представляет собой набор методов, которые на вход получают какие-то параметры и выполняют какое-то действие с ними, при этом для реализации такого интерфейса могут быть использованы различные технологии.
В этой статье также мы постараемся разобраться с возможными вариантами реализации API.
1 RPC
Идея RPC (Remote Procedure Call) понятна после расшифровки аббревиатуры. RPC — это концепция, в рамках которой программа на одном сервере (компьютере) может вызвать функцию на другом сервере (компьютере). В каком-то смысле можно сказать, что API и есть RPC, а REST API или gRPC — это как варианты реализации. Поэтому предлагаю здесь не останавливаться и перейти к ещё одной известной реализации удалённого вызова процедур.
2 API на основе HTTP
В начале нужно вспомнить, что протокол — это свод некоторых договорённостей, которые должны соблюдаться для какого-либо взаимодействия.
А теперь к делу!
HTTP — это протокол прикладного уровня (самого высокого), который использует протокол TCP (протокол на уровень ниже) как транспорт.
Представьте, что компьютеры — это узлы графа, которые напрямую или косвенно связаны. TCP — это протокол, который обеспечивает надёжную доставку данных от одного компьютера (узла графа) к другому в рамках нашей сети (нашего графа) без потерь. Данные отправляются по кусочкам (пакетам), а отправитель получает сообщение о том, что пакет доставлен успешно или неуспешно и нужна ещё одна попытка. А HTTP даёт нам некоторую структуру сообщений, понятную как для клиента, так и для сервера.
Для того чтобы та или иная функциональность на стороне клиента работала, необходимо, чтобы на стороне клиента (самое частое в браузере) запрос формировался и отправлялся, а сервер его понимал и формировал обратное сообщение с какой-то информацией.
Так, в начале взаимодействия формируется TCP-сессия в качестве транспорта, а потом клиентское приложение отправляет HTTP-запрос на сервер, который обрабатывает его, формирует ответ и передаёт этот ответ обратно клиенту.
Давайте разберём, из чего состоит HTTP-запрос и ответ:
Начнём с HTTP-запроса на сервер. Он состоит из трёх компонентов.
Стартовая строка (Start line)
Обычно выглядит стартовая строка именно так: Метод + URL + HTTP/Версии. Действие, необходимое для выполнения, определяется методом, их существует множество, но реально используются несколько: GET, POST, DELETE, PATCH.
URL — это уникальный адрес какого-либо объекта, над которым мы хотим совершить ту или иную операцию.
Заголовки (Headers)
Это «поля», которые переносят вспомогательную/уточняющую информацию. Например, авторизационные токены, данные браузера, тип устройства. Технически заголовков может не быть вовсе, но почти всегда через них передаются метаданные запроса.
Тело запроса (Body)
Также необязательная часть запроса (например, в ручках с глаголом GET тело не используется). В этой части мы передаём какую-то дополнительную информацию, которую сервер должен обработать при вызове определённого метода, в виде текста при помощи определённого формата. Форматы могут использоваться любые (JSON, BSON, XML, HTML и даже Protocol Buffers).
HTTP-ответ имеет похожую структуру.
Статус ответа (Status)
Это стартовая строка HTTP-ответа. Обычно выглядит следующим образом: Версия протокола + код ответа + пояснение.
Заголовки (Headers)
См. описание компонента «Заголовки» в описании HTTP-запроса.
Тело ответа (Body)
См. описание компонента «Тело запроса» в описании HTTP-запроса.
О форматах
Выше я уже говорил, что при использовании протокола HTTP может быть выбран любой формат представления текста, но самым популярным для API в силу своей читаемости и удобства стал, конечно же, JSON. Сначала он выступил альтернативой, а позже почти полностью вытеснил SOAP с XML-форматом. То есть, когда мы говорим про API, использующий HTTP, обычно подразумевается JSON-over-HTTP API, что в прямом смысле означает, что для API-сервиса мы используем протокол HTTP и JSON как формат для передачи информации в теле запроса. Также я на просторах интернета встречал название HTTP API.
Плюсы и минусы API на основе HTTP
+ Быстро и легко разрабатываются.
- Нужно делать свою спецификацию, чтобы наши методы могли использовать другие программисты, что делает процесс интеграции сложным/неудобным/долгим.
- Тяжёлые сообщения из-за текстового формата.
3 REST API
Со временем один из создателей HTTP-протокола Рей Фидинг сформулировал архитектурные принципы, соблюдая которые при проектировании архитектуры можно избежать ряда проблем. Давайте взглянем на эти принципы.
Клиент-серверная архитектура. Это правило говорит о том, что клиентская часть ПО не является самодостаточной и должна обращаться к серверу для решения той или иной задачи.
Хранение сессии на клиенте Stateless. Сервер не должен хранить состояние сессии клиента и с каждым новым запросом должен получать всю необходимую информацию для его выполнения.
Кэширование. Для оптимизации количества запросов какие-то не часто изменяемые данные нужно временно хранить локально у клиента/на сервере.
Единообразие интерфейса. API должен быть универсальными интерфейсом, определяющимся 4 HTTP-глаголами: GET (получение ресурса), POST (создание нового ресурса), PUT (обновление ресурса) и DELETE.
Многослойная архитектура. Зачастую схема взаимодействия компонентов между собой включает в себя множество компонентов (прокси, гейтвеи, микросервисы с разными функциональностями и т. д.). Данная концепция предполагает, что ни клиент, ни сервер не должны быть осведомлены о том, как осуществляется цепочка вызовов за пределами их непосредственных соседей.
Код по требованию. Последнее требование является, скорее, идеей и, вообще, опционально для соблюдения. Но тем не менее эта концепция гласит, что не все запросы приводят к вычислениям на стороне сервера, а результатом запроса может быть какой-то код-программа, которую клиент сам и исполнит.
Формально можно сказать, что, соблюдая все эти ограничения, у вас REST API. Но мы видим, что здесь нет привязки к протоколу HTTP и JSON.
Мысленно вернёмся к факту, что автор этих принципов — один из создателей HTTP, поэтому напрашивается вывод, что протокол создавался, чтобы по максимуму их учитывать. Поэтому неудивительно, что этот архитектурный стиль реализуется именно на HTTP.
По моему мнению, самым важным является принцип единообразия интерфейса, поскольку именно он и говорит нам, что API должен строиться по определённой спецификации, которая будет понятна для всех потенциальных клиентов.
Это главное отличие REST API от просто API поверх протокола HTTP.
Ещё одной важной ремаркой является то, что почти все существующие сервисы удовлетворяют этим принципам сегодня, поскольку REST может быть очень широко интерпретируем.
В связи с этим сегодня довольно популярными суждениями являются следующие.
REST API — это синоним API на основе HTTP. В целом для упрощения коммуникации IT-специалистов можно принять это определение, хотя фактически один API построен согласно архитектурному стилю, а другой — это API, использующий определённый протокол.
Для REST API используется JSON. Выше я уже раскрыл, что JSON — это лишь один из форматов (хоть и самый популярный), которые могут использоваться для представления и передачи текстовой информации по HTTP.
Таким образом, для упрощения предлагается называть «JSON-over-HTTP API, спроектированный с соблюдением архитектурного стиля REST» просто «REST API».
Также API, удовлетворяющие ограничениям REST, называют RESTful API.
Для лёгкости поддержки микросервисов API должны быть понятно и полно задокументированы. Здесь на помощь и приходит Swagger. Этот инструмент позволяет легко генерировать документацию и примеры кода, определяя структуру, конечные точки.
Плюсы и минусы REST API
+ Прост в реализации несмотря на то, что здесь будет чуть сложнее, чем просто с HTTP (придётся соблюдать спецификацию).
+ Легко интегрироваться (спасибо спецификации Open API).
+ Легко читаемый формат JSON.
- Тяжёлые сообщения из-за текстового формата.
Подробнее про REST можно прочитать в замечательной статье.
4 gRPC
Испытав потребность в высокоэффективных системах, компания Google выпустила свой фреймворк (технологию) gRPC, а также под него наши американские коллеги выпустили новую версию транспортного протокола HTTP/2.0. Давайте пройдёмся по особенностям, отличающим этот подход от REST.
Особенность 1. Protocol Buffers
В качестве формата данных вместо JSON используется Protocol Buffers (в народе протобаф). Protobuf — это бинарный формат данных, который подразумевает, что в сообщении вместо массива пар ключ-значение (как это устроено в JSON, где пары ключ-значение можно писать в любом порядке) мы просто отправляем значения в установленном порядке. При этом оба участника сообщения имеют «под рукой» протоконтракт, который и позволяет определить, под каким порядковым номером идёт значение по тому или иному ключу. Другими словами, и клиент, и сервер знают, в каком строгом порядке должны идти значения и, так как это условие реализуется, то и потребности в лишней прогонке ключей по сети нет. А представьте, какая это крутая оптимизация на масштабе в тысячи запросов в секунду. Это плюс!
Но у бинарного формата есть и обратная сторона: формат менее человеко-читаемый, чем текстовый JSON.
Пример протоконтракта:
message User {
int32 id = 1;
string name = 2;
string email = 3;
}
Ещё одним плюсом формата является совместимость почти со всеми актуальными языками программирования, что особенно актуально для больших компаний с огромным количеством сервисов и легаси.
Бывает, что API gRPC-сервиса представлено в виде набора методов в Swagger. Такое представление делают при помощи gRPC-gateway, конвертируя (проксируя) gRPC в REST для повышения читабельности и упрощения аналитики. Получается, что по факту аналитик может вызывать REST-методы через Swagger с удобным JSON, но сам сервис при этом остается gRPC. Для интересующихся есть пример в статье.
Особенность 2. HTTP/2.0
В предыдущей актуальной версии протокола — HTTP/1.1 — необходимо было устанавливать несколько TCP-соединений (клиент — сервер), если нужно было сделать несколько запросов. Есть хороший бытовой пример, иллюстрирующий неудобство такого подхода. Если вы хотите задать другу несколько вопросов по телефону, то, получив ответ на вопрос, вы сбрасываете звонок, звоните снова и задаёте новый вопрос, и так по кругу.
Принцип мультиплексирования запроса, реализованный в HTTP/2.0, решает эту проблему. HTTP-запрос делится на два «фрейма». Один (Headers frame) содержит в себе заголовки, а второй (Data frame) — полезные данные. Это позволяет прогонять полезные данные из нескольких запросов с одними и теми же заголовками, причём система будет воспринимать их как один запрос.
В HTTP/2.0 также предусмотрено сжатие заголовков запроса, что еще сильнее облегчает запрос, но в этой статье нам не так важно, как именно это реализуется. Главное — понять, что gRPC эффективнее и быстрее, чем REST.
Благодаря все тому же принципу мультиплексирования существует 4 способа использования gRPC.
Унарный (Unary)
Это самый простой и распространённый паттерн, при котором клиент отправляет один запрос и получает один ответ от сервера. Это похоже на обычный HTTP-запрос и ответ.
Серверный стриминг (Server streaming)
В этом способе клиент отправляет один запрос и получает несколько ответов от сервера. Сервер может передавать ответы по мере их готовности, не дожидаясь запроса от клиента. Это бывает полезно, например, в приложениях для такси, где сервер может отправлять клиенту (водителю или пассажиру) обновления о геолокации.
Клиентский стриминг (Client streaming)
В этом способе клиент отправляет несколько запросов и получает один ответ от сервера. Клиент может передавать запросы по мере их готовности, не дожидаясь подтверждения от сервера. Это полезно в сценариях, например, когда датчик транслирует температурные показатели.
Двунаправленный стриминг (Bidirectional streaming)
При такой реализации клиент и сервер могут отправлять и получать несколько сообщений в обоих направлениях. Сообщения могут отправляться и приниматься независимо, без строгого порядка. Это полезно в ситуациях, когда клиенту и серверу нужно вести непрерывный и динамический диалог или обмениваться данными в режиме реального времени, к примеру, чат в реальном времени или финансовые транзакции с изменением котировок.
Плюсы и минусы gRPC
+ Быстрый. То, что нужно для высоконагруженных систем.
+ Уже написанная спецификация.
+ Есть возможность сделать стриминг информации.
- Не так прост в реализации.
- Менее читаемый Protobuff (в отличие от JSON).
5 Сравнение gRPC, REST API и API на основе HTTP
| API на основе HTTP | REST API | gRPC |
Спецификация | Отсутствует | Есть | Есть |
Объём передаваемых данных | Большой, потому что передается текст | Большой, потому что передается текст | Маленький благодаря бинарному формату (не передаются ключи и спецсимволы как в JSON) |
Возможность стриминга | Отсутствует | Отсутствует | Есть |
Читабельность контрактов/сообщений | Низкая, потому что нет определённого формата | При использовании JSON высокая | Низкая из-за особенностей Protocol Buffers |
Когда используется? | Не используется | Подходит для проектов с небольшой нагрузкой, а также используется при написании API для веб-браузера | Для построения инфраструктуры, закрытой для внешних пользователей. Идеально для интеграции высоконагруженных микросервисов |
Итого
Хочется сказать, что REST API и gRPC — это просто архитектурные стили, которые регламентируют использование протокола HTTP и форматов сообщений (JSON и Protobuf). У gRPC есть ряд возможностей, которые подходят больше для реализации внутренних высоконагруженных систем, а REST чаще используется для интеграций с фронтендом и сторонними решениями из-за своего подхода к функциям как к операциям над ресурсами и более человеко-читаемого формата.
Да, по факту можно пойти против системы и сделать свой вид API с использованием HTTP-протокола и, допустим, XML. Но зачем, если за нас уже придумали эффективные способы реализации сервисных интерфейсов и расписали всё в спецификациях, описали и накопили большой опыт использования, к которому можно обратиться.
Понятно, что аналитики не взаимодействуют с вышеописанным ручками, в отличие от разработчиков, и при проектировании нового сервиса финальное слово за тимлидом/техлидом/старшим разработчиком, но аналитикам тоже хочется понимать теорию и иметь представление, почему тот или иной паттерн был применён.
Дополнения и комментарии только приветствуются!
Отдельное спасибо хочется передать моему другу и коллеге — Артёму Заборскому, без которого эта статья не получилась бы такой, какая она вышла. А также благодарность коллегам Владимиру Шаплину и Илье Борисюку за ревью.