Началось все с того, что наши базы данных стали все чаще кидать таймауты. Быстрый анализ "самых жручих запросов" показал, что пара десятков клиентов насели на API, раз в час вытягивая "вообще все". Видимо, чтобы пихнуть данные в какой-то свой дашборд...
Знаете, сейчас появилось куча этих долбаных "Business Intelligence" сервисов, которым даешь url/пароль/swagger, а они рисуют красивые графики и циферки для менеджеров, до капли высасывая ваш бедный endpoint.
Раз, сука, в 10 секунд.
(пользуясь случаем отмечу богическое удобство SQL Server в решении задач типа "ааа, все тормозит, ааа, что же делать". Там есть охреническая тулза Query Store, которая появилась в SQL 2016 и собирает статистику запросов. Очень жаль, что для Postgres ниче такого нет... кажется @antonrevyako пилит что-то такое и как раз ищет бета-тестеров)
Ну, в общем, да, все тупит и надо что-то делать.
Вопрос абьюза API обычно решается четырьмя способами:
1. "Софт" рейт-лимит, после превышения разрешенных запросов/сек начинаем мягко троттлить клиента (добавлять задержку, для особо наглых - прогрессирующую)
2. Кеширование (опционально - на CDN/лоадбалансере, тогда вообще красота)
3. "Хард" рейт-лимит, после превышения возвращаем http-статус "а рожа не треснет?" более известный как 429.
4. Лимит на кол-во данных - заставляем клиента запрашивать длинные списки "постранично"
Алгоритмов для рейт-лимитов (пункты 1 и 3) много - Leaky bucket, Fixed window, Sliding window - за этим пжлста в Гугл. Лучше обсудим недостатки всех 4-х способов...
Я - дибил, поэтому выбрал 4ый способ - "постраничные запросы". Это было много лет назад, еще на стадии MVP. Мы задумчиво поплевали в потолок, махнули рукой и решили "сделаем как у Джиры". Пусть запрашивают постранично, а там посмотрим.
Дело, конечно, кончилось тем, что клиенты начали дергать АПИшку 1000 раз в минуту, вытягивая страницы друг за другом. По пути выяснилось, что в реляционных базах "постраничный" запрос вида "
SELECT x OFFSET m" по производительности НИЧЕМ не лучше "обычного". Execution plan будет такой же, все индексы, сканы, сортировки и хеш-джойны отработают так же. Разве что i/o будет получше - с диска придется прочитать чуть меньше данных.Кажется, пейджинг - это просто хитрый способ "раздеть" клиента на расходование лимитов.
Минусы 3-го способа (хард-лимит) очевидны. Лимиты всех бесят. Но они хотя бы прозрачны и понятны. Главное все четко описать в доках, а в теле http-response возвращать объяснение "а что, собственно, случилось"
Минусы 2-го способа (кеширование) - не везде применим и надо следить за целостностью. Например, если через АПИ создана новая запись - ее как правило надо сразу показать в "списках", без всяких кешей.
Хорошо, когда есть способы "инвалидировать" кеш, но их чаще всего нет (а пытаясь их сделать легко свалиться в спагетти-код).
Ах да, и кешировать наверняка придется "per-user" (такие факапы у нас тоже были - людям отдавались данные, на которые у них нет прав). Короче, сложно и гемор.
Минусы 1-го способа (троттлинг) - это удивительно, но их почти нет. Подумаешь, в мобильном приложении экран 3 секунды потормозит на десятый раз... Зато и бэк разгрузим, и целостность данных не поедет. Да и накодить такое легко.
P.S. Бонус-тип. Что делать, если вы взяли пример с нас - идиотов - и ввели постраничное ограничение и получили миллионы запросов подряд? Поднять ограничение можно, но придется уведомлять клиентов - и не факт, что они почешутся переписывать код. Мы в итоге сделали комбинацию троттлинга и long tail caching - это когда кешируется только "хвост списка", начиная с N-ой страницы. Т.к. данные обычно отсортированы по "дате последнего изменения", ясен пень что после N-ой страницы люди вытягивают годами не менявшееся старье... В общем врубили лонг-тейл-кеш на 12 часов.