Версионирование, пагинация и ответы N+1

Что это такое

Проблемы приходят вместе с пользователями и данными: опубликованное поле нельзя убрать, не сломав кого-нибудь, поэтому интерфейс должен уметь меняться, а коллекция вырастает за пределы одного ответа, поэтому её нужно разбивать на страницы. Отдельная беда — ответ, который вкладывает связанные ресурсы: сервер соблазняется выдавать по одному запросу на родителя, и стоимость растёт вместе с результатом. В первый день жизни API ничего из этого не видно, а заложить всё заранее гораздо дешевле.

Почему это важно

Эти три вещи дают большую долю жалоб на API, который был корректен на момент выпуска, и каждая из них — решение проектирования с окном, в котором оно дёшево. Ломающее изменение заставляет всех клиентов обновиться разом, что для публичного API практически невозможно; коллекция без пагинации в конце концов упирается в тайм-аут или исчерпывает память; а эндпоинт, число запросов у которого растёт с числом строк, работает на тестах и падает в продакшене на первом же крупном аккаунте.

Ключевые идеи

Добавление безопасно, вычитание — нет

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

добавить поле               -> безопасно
сделать поле обязательным   -> ломает
расширить тип               -> безопасно, если старые значения означают то же

Устаревание — это график, а не объявление

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

Пагинация должна быть и в ответе, и в запросе

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

Страница всегда должна быть ограничена

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

Запросы N+1 рождаются из формы ответа

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

Форма ответа — это контракт о стоимости

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

Практика

Спроектируйте контракт страниц

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

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

Найдите и исправьте запрос N+1

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

Готово, когда: число запросов больше не растёт с числом элементов на странице.

Частые ошибки

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

Что читать дальше