You've already forked The-API-Book
mirror of
https://github.com/twirl/The-API-Book.git
synced 2025-08-10 21:51:42 +02:00
push and poll models
This commit is contained in:
@@ -12,7 +12,7 @@ const pendingOrders = await api.
|
||||
}]}
|
||||
```
|
||||
|
||||
Внимательный читатель может подметить, что этот интерфейс нарушает нашу же рекомендацию, данную в главе «Описание конечных интерфейсов»: количество данных в ответе ничем не ограничено. Эта проблема присутствовала и в предыдущих версиях этого эндпойнта, но отказ от синхронного создания заказа её усугубляет: так как почти все проверки лимитов желательно делать асинхронно, количество заданий на создание заказа почти ничем не ограничено, и их легко может быть создано очень большое количество.
|
||||
Внимательный читатель может подметить, что этот интерфейс нарушает нашу же рекомендацию, данную в главе «Описание конечных интерфейсов»: количество возвращаемых данных в любом ответе должно быть ограничено, но в нашем интерфейсе отсутствуют какие-либо лимиты. Эта проблема существовала и в предыдущих версиях этого эндпойнта, но отказ от синхронного создания заказа её усугубил: операция создания задания должна работать максимально быстро, и, следовательно, почти все проверки лимитов мы должны проводить асинхронно — а значит, клиент потенциально может создать очень много заданий.
|
||||
|
||||
Исправить эту проблему достаточно просто — можно ввести лимит записей и параметры фильтрации и сортировки, например так:
|
||||
|
||||
@@ -30,7 +30,7 @@ api.getOngoingOrders({
|
||||
})
|
||||
```
|
||||
|
||||
Однако введение лимита ставит другой вопрос: если всё же количество записей, которые нужно выбрать, превышает лимит, каким образом клиент должен их выбрать?
|
||||
Однако введение лимита ставит другой вопрос: если всё же количество записей, которые нужно выбрать, превышает лимит, каким образом клиент должен получить к ним доступ?
|
||||
|
||||
Стандартный подход к этой проблеме — введение параметра `offset` или номера страницы данных:
|
||||
|
||||
@@ -63,7 +63,7 @@ api.getOngoingOrders({
|
||||
}]
|
||||
```
|
||||
|
||||
Приложение партнёра запросило первую страницу заказов:
|
||||
Приложение партнёра запросило первую страницу списка заказов:
|
||||
|
||||
```
|
||||
api.getOrders({
|
||||
@@ -107,7 +107,7 @@ api.getOrders({
|
||||
}]
|
||||
```
|
||||
|
||||
Тогда, запросив вторую страницу заказов, вместо одного заказа `"id": 1`, приложение партнёра получит повторно ещё и заказ `"id": 2`:
|
||||
Тогда, запросив вторую страницу заказов, вместо одного заказа `"id": 1`, приложение партнёра получит повторно заказ `"id": 2`:
|
||||
|
||||
```
|
||||
api.getOrders({
|
||||
@@ -125,9 +125,9 @@ api.getOrders({
|
||||
}
|
||||
```
|
||||
|
||||
Следует отметить, что такие перестановки крайне неудобны даже для пользовательского интерфейса — если,допустим, предположить, что заказы запрашивает бухгалтер партнёра, чтобы рассчитать выплаты, то и он легко может просто не заметить, что какой-то заказ посчитан дважды. Однако в случае *программной* интеграции ситуация становится намного сложнее: у разработчика приложения *нет никакой возможности* понять, что происходит с его перебором, и переберёт ли он *все* заказы вообще хоть когда-нибудь.
|
||||
Такие перестановки крайне неудобны и для пользовательских интерфейсов — если, допустим, предположить, что заказы запрашивает бухгалтер партнёра, чтобы рассчитать выплаты, то и он легко может просто не заметить, что какой-то заказ посчитан дважды. Однако в случае *программной* интеграции ситуация становится намного сложнее: разработчику приложения нужно написать достаточно неочевидный код (сохраняющий состояние уже полученных страниц данных), чтобы провести такой перебор корректно.
|
||||
|
||||
Отметим теперь, что ситуацию легко можно сделать гораздо более запутанной. Например, если заказы мы отсортируем заказы по статусу:
|
||||
Отметим теперь, что ситуацию легко можно сделать гораздо более запутанной. Например, если мы добавим сортировку не только по дате создания, но и по статусу заказа:
|
||||
|
||||
```
|
||||
api.getOrders({
|
||||
@@ -332,7 +332,7 @@ GET /v1/orders/created-history⮠
|
||||
"occured_at",
|
||||
// Идентификатор заказа
|
||||
"order_id"
|
||||
}]
|
||||
}, …]
|
||||
}
|
||||
```
|
||||
|
||||
|
@@ -0,0 +1,99 @@
|
||||
### Двунаправленные потоки данных. Push и poll-модели
|
||||
|
||||
В предыдущей главе мы рассмотрели следующий кейс: партнёр получает информацию о новых событиях, произошедших в системе, периодически опрашивая эндпойнт, поддерживающий отдачу упорядоченных списков:
|
||||
|
||||
```
|
||||
GET /v1/orders/created-history⮠
|
||||
older_than=<item_id>&limit=<limit>
|
||||
→
|
||||
{
|
||||
"orders_created_events": [{
|
||||
"id",
|
||||
"occured_at",
|
||||
"order_id"
|
||||
}, …]
|
||||
}
|
||||
```
|
||||
|
||||
Подобный паттерн (известный как [*поллинг*](https://en.wikipedia.org/wiki/Polling_(computer_science))) — наиболее часто встречающийся способ организации двунаправленной связи в API, когда партнёру требуется не только отправлять какие-то данные на сервер, но и получать оповещения от сервера об изменении какого-то состояния.
|
||||
|
||||
При всей простоте, поллинг всегда заставляет искать компромисс между отзывчивостью, производительностью и пропускной способностью системы:
|
||||
* чем длиннее интервал между последовательными запросами, тем больше будет задержка между изменением состояния на сервере и получением информации об этом на клиенте, и тем потенциально большим будет объём данных, которые необходимо будет передать за одну итерацию;
|
||||
* с другой стороны, чем этот интервал короче, чем большее количество запросов будет совершаться зря, т.к. никаких изменений в системе за прошедшее время не произошло.
|
||||
|
||||
Иными словами, поллинг всегда создаёт какой-то фоновый трафик в системе, но никогда не гарантирует максимальной отзывчивости. Иногда эту проблему решают с помощью «долгого поллинга» ([long polling](https://en.wikipedia.org/wiki/Push_technology#Long_polling)) — т.е. целенаправленно замедляют отдачу сервером ответа на длительное (секунды, десятки секунд) время — однако мы не рекомендуем использовать этот подход в современных системах из-за связанных технических проблем (в частности, в условиях ненадёжной сети у клиента нет способа понять, что соединение на самом деле потеряно, и нужно отправить новый запрос, а не ожидать ответа на текущий).
|
||||
|
||||
Если оказывается, что обычного поллинга для решения пользовательских задач недостаточно, то можно перейти к обратной модели (*push*): сервер *сам* сообщает клиенту, что в системе произошли изменения.
|
||||
|
||||
Хотя и проблема, и способы её решения выглядят похоже, в настоящий момент применяются совершенно разные технологии для доставки сообщений от бэкенда к бэкенду, и от бэкенда к клиентскому устройству.
|
||||
|
||||
#### Доставка сообщений на клиентское устройство
|
||||
|
||||
Поскольку разнообразные мобильные платформы сейчас составляют значительную долю всех клиентских устройств, на технологии взаимного обмена данных между сервером и конечным пользователем накладываются значительные ограничения с точки зрения экономии заряда батареи (и отчасти трафика). Многие производители платформ и устройств следят за потребляемыми приложением ресурсами, и могут отправлять приложение в фон или вовсе закрывать открытые соединения. В такой ситуации частый поллинг стоит применять только в активных фазах работы приложения (т.е. когда пользователь непосредственно взаимодействует с UI) либо если приложение работает в контролируемой среде (например, используется сотрудниками компании-партнера непосредственно в работе, и может быть добавлено в системные исключения).
|
||||
|
||||
Альтернатив поллингу на данный момент можно предложить две.
|
||||
|
||||
##### Дуплексные соединения
|
||||
|
||||
Самый очевидный вариант — использование технологий, позволяющих передавать по одному соединению сообщения в обе стороны. Наиболее известной из таких технологий является [WebSockets](https://websockets.spec.whatwg.org/). Иногда для организации полнодуплексного соединения применяется [Server Push, предусмотренный протоколом HTTP/2](https://datatracker.ietf.org/doc/html/rfc7540#section-8.2), однако надо отметить, что формально спецификация не предусматривает такого использования.
|
||||
|
||||
Несмотря на то, что идея в целом выглядит достаточно простой и привлекательной, в реальности её использование довольно ограничено. Поддержки инициирования *сервером* отправки сообщения обратно на клиент практически нет в популярных протоколах и фреймворках (gRPC поддерживает потоки сообщений с сервера, но их всё равно должен инициировать клиент; использование потоков для пересылки сообщений по мере их возникновения — фактически то же самое использование HTTP/2 Server Push в обход спецификации), и существующие стандарты спецификаций API также не поддерживают такой обмен данными. WebSockets является низкоуровневым протоколом, и формат взаимодействия придётся разработать самостоятельно.
|
||||
|
||||
К тому же дуплексные соединения по-прежнему страдают от ненадёжной сети и требуют дополнительных ухищрений для того, чтобы отличить сетевую проблему от отсутствия новых сообщений. Всё это приводит к тому, что данная технология используется в основном веб-приложениями.
|
||||
|
||||
##### Сторонние сервисы отправки push-уведомлений
|
||||
|
||||
Одна из неприятных особенностей технологии типа long polling / WebSockets — необходимость поддерживать открытое соединение между клиентом и сервером, что для мобильных приложений может быть проблемой с точки зрения производительности и энергопотребления. Один из вариантов решения этой проблемы — делегирование отправки уведомлений стороннему сервису (самым популярным выбором на сегодня является Firebase Cloud Messaging от Google), который в свою очередь доставит уведомление через встроенные механизмы платформы. Использование встроенных в платформу механизмов получения уведомлений снимает с разработчика головную боль по написанию кода, поддерживающего открытое соединение, и снижает риски неполучения сообщения. Недостатками third-party серверов сообщений является необходимость платить за них и ограничения на размер сообщения.
|
||||
|
||||
Независимо от выбранной технологии, отправка сообщений с сервера на устройство конечного пользователя страдает от одной большой проблемы: в результате всех описанных ограничений, процент успешной доставки уведомлений никогда не равен 100; потери сообщений могут достигать десятков процентов. С учётом ограничений на размер контента, **скорее правильно говорить не о push-модели, а о комбинированной**: приложение продолжает периодически опрашивать сервер, а пуши являются триггером для внеочередного запроса.
|
||||
|
||||
#### Доставка сообщений backend-to-backend
|
||||
|
||||
В отличие от клиентских приложений, серверные API практически безрезультативно используют единственный подход — отдельный канал связи для обратных вызовов. При интеграции партнёр указывает URL своего собственного сервера обработки сообщений, и сервер API вызывает этот эндпойнт (также называемый ‘webhook’) для оповещения о произошедшем событии. Хотя long polling, Web Sockets и HTTP/2 Push тоже вполне применимы для backend-2-backend взаимодействия, мы сходу затрудняемся назвать примеры популярных API, которые использовали бы эти технологии. Главными причинами такого положения дел нам видятся:
|
||||
* бо́льшая критичность корректной и своевременной обработки событий, и отсюда повышенные требования к гарантиям их доставки;
|
||||
* возможность покрыть такое взаимодействие строгой спецификацией.
|
||||
|
||||
Предположим, что в нашем кофейном примере партнёр располагает некоторым бэкендом, готовым принимать оповещения о новых заказах, поступивших в его кофейни. Решение этой задачи декомпозируется на несколько шагов:
|
||||
|
||||
##### Договоренность о контракте
|
||||
|
||||
В зависимости от важности партнёра для вашего бизнеса здесь возможны разные варианты:
|
||||
* производитель API может реализовать возможность вызова webhook-а в формате, предложенном партнёром;
|
||||
* наоборот, партнёр должен разработать эндпойнт в стандартном формате, предлагаемом производителем API;
|
||||
* любой промежуточный вариант.
|
||||
|
||||
Важно, что в любом случае должен существовать формальный контракт (очень желательно — в виде спецификации) на формат запроса к эндпойнту-webhook-у и формат его ответа, а также, что важно, возникающие ошибки.
|
||||
|
||||
##### Договорённость о методах авторизации
|
||||
|
||||
Так как webhook-и представляют собой отдельный канал взаимодействия, для него придётся разработать отдельный способ авторизации. Мы повторяем здесь настоятельную рекомендацию не изобретать безопасность и использовать существующие стандартные механизмы, например, [mTLS](https://en.wikipedia.org/wiki/Mutual_authentication#mTLS).
|
||||
|
||||
##### API для задания адреса webhook-а
|
||||
|
||||
Так как callback-эндпойнт контролируется партнёром, жёстко зафиксировать его обычно не представляется возможным — должен существовать API (возможно, в виде кабинета партнёра) для задания URL webhook-а (и публичных ключей авторизации).
|
||||
|
||||
**Важно**. К операции задания адреса callback-а нужно подходить с максимально возможной серьёзностью, поскольку, получив доступ к такой функциональности, злоумышленник может совершить множество весьма неприятных атак:
|
||||
* если указать в качестве приёмника сторонний URL, можно получить доступ к потоку всех заказов партнёра и при этом вызвать перебои в его работе; очень желательно требовать второй фактор авторизации для подтверждения этой операции;
|
||||
* если указать в качестве webhook-а URL интранет-сервисов компании-провайдера API, можно осуществить [SSRF-атаку](https://en.wikipedia.org/wiki/SSRF) на инфраструктуру самой компании.
|
||||
|
||||
#### Типичные проблемы интеграций с обратными вызовами
|
||||
|
||||
Двунаправленные интеграции (и клиентские, и серверные — хотя последние в большей степени) несут в себе очень неприятные риски для разработчика API. Если в случае прямых вызовов корректность работы системы зависит от того, как хороша она написана, то в случае обратных вызовов всё в точности наоборот: качество работы интеграции напрямую зависит от того, как код webhook-эндпойнта написан партнёром. Мы можем столкнуться здесь с самыми различными видами проблем в партнёрском коде:
|
||||
* false-positive ответы, когда сообщение не было обработано, но сервер партнёра тем не менее ошибочно вернул код успеха;
|
||||
* и наоборот, возможны false-negative ответы, когда сообщение было обработано, но эндпойнт почему-то вернул ошибку;
|
||||
* длительное время обработки запросов — возможно, настолько длительное, что сервер сообщений просто не будет успевать их отправить;
|
||||
* ошибки в реализации идемпотентости, т.е. повторная обработка одного и того же сообщения партнёром;
|
||||
* просто недоступность эндпойнта.
|
||||
|
||||
Очевидно, вы никак не можете гарантировать, что партнёр не совершил какую-то из перечисленных ошибок. Но вы можете попытаться минимизировать возможный ущерб.
|
||||
|
||||
1. Состояние системы должно быть восстановимо. Даже если партнёр неправильно обработал сообщения, всегда должна быть возможность реабилитироваться и получить список последних событий (либо полное состояние) системы, чтобы исправить случившиеся ошибки.
|
||||
2. Должны быть зафиксированы технические параметры:
|
||||
* ключи идемпотентности;
|
||||
* допустимое количество параллельных запросов к эндпойнту;
|
||||
* политика перезапросов при получении ошибки;
|
||||
* гарантии доставки (exactly once, at least once).
|
||||
3. Должна быть реализована система мониторинга состояния партнёрских эндпойнтов:
|
||||
* при появлении большого числа ошибок (таймаутов) должно срабатывать оповещение (в т.ч. оповещение партнёра о проблеме), возможно, с несколькими уровнями эскалации;
|
||||
* если в очереди скапливается большое количество необработанных событий, должен существовать механизм аварийного отключения партнёра.
|
||||
|
Reference in New Issue
Block a user