Привет, Хабр! Меня зовут Дарья Борисова, я системный аналитик в ПСБ.
Однажды я попробовала интеграции... и теперь они преследуют меня везде, как навязчивый мотив из песни.
Пришлось изучать и внедрять разные подходы, а заодно накопить вагон и маленькую тележку лайфхаков. Сегодня я работаю с Системой быстрых платежей в ПСБ — и готова поделиться тем, что спасло нас в критичных ситуациях.
Почти наверняка вы бывали в ситуациях, когда всё выпустили в прод, а сервер нагрузку не тянет. Или бизнес давит сроками, а времени на идеальные решения нет. Приходится подставлять костыли и ставить быстрые заплатки. Вопрос в том, могут ли они стать надежным решением? И какие компромиссы придется принять — об этом и поговорим.
А точнее: об оптимизации REST API в бою: как снизить количество запросов без потери данных, где проводить расчеты (и чем это грозит), зачем стандартизировать ответы, как кешировать с умом и почему health-check — это не просто «жив/мертв».
Оптимизация количества запросов
Все говорят: «Запросы дорогие». А что это значит на самом деле?

Почему запросы нужно экономить/оптимизировать? Как это вообще сделать?
Для начала вспомним, что скрывается за каждым запросом:
Обмен несколькими сообщениями на транспортном уровне при установке соединения;
Шифрование/дешифровка данных (безопасность никто не отменял).
И для всего этого нужно время.
Что даст оптимизация
1. Снижение нагрузки на сервер и уменьшение риска отказов.
Чем больше запросов приходит на сервер, тем выше нагрузка на него. Это может привести к замедлению работы сервера, увеличению времени отклика и даже к отказам системы при пиковых нагрузках. Оптимизация запросов позволяет снизить нагрузку, что особенно важно для высоконагруженных систем.
2. Повышение производительности клиентского приложения и улучшение UX.
Чем меньше запросов - тем меньше объём данных, передаваемых между клиентом и сервером, а также сокращает время ожидания ответа. Это улучшает пользовательский опыт, так как интерфейс становится более отзывчивым.
3. Экономия ресурсов сети и повышение её пропускной способности.
Каждый запрос требует передачи данных через сеть, что занимает ресурсы канала связи. Если запросы выполняются часто и без необходимости, то растёт потребление трафика и снижается общая пропускная способность сети. Это особенно актуально для мобильных приложений, где трафик может быть ограничен.
Как оптимизировать?
У нас есть два выхода: оптимизировать частоту передачи или уменьшить объём передаваемых данных. Рассмотрим оба варианта с их плюсами, минусами, подводными граблями.
Вариант 1. Оптимизируем частоту передачи
Пример из практики:
Бизнес предложил валидировать поле "Сумма списания" в реальном времени — подсвечивать клиенту, если денег на счёте недостаточно. Такая проверка требовала запроса к бекенду, реализовать её исключительно силами фронта мы не могли по ряду причин.
Начали искать компромисс, ведь слать запрос при каждом изменении суммы (например, пока пользователь вводит "1000₽") нерационально. Это создает лишнюю нагрузку. Вместо этого мы перенесли валидацию на этап, когда клиент закончил ввод и подтвердил операцию (например, нажал "Далее").
Событийные модели: Отправляем данные только при значимом событии, а не постоянно. Например, обновление данных о состоянии устройства только при изменении этого состояния.

Lazy Loading: Загружаем данные по мере их использования, вместо того, чтобы грузить всё и сразу. При помощи этого метода часто реализуют пагинацию на тех же маркетплейсах, подгружая следующую страницу автоматически, когда вы пролистали до конца текущей.
Вариант 2. Уменьшаем объём данных:
-
Дельта-кодирование: Отправляем не весь объект, а только измененные поля (PATCH вместо PUT).
Пример: Настройка SMS-оповещений (20+ полей) по разным событиям. Как правило, всё уже преднастроено и пользователь (клиент банка) меняет 1-2 пункта — зачем же отправлять все 20 после каждого изменения? API Composition: Объединяем несколько запросов в один.
Пример: Менеджер рассматривает ипотечную заявку. Кроме получения данных от клиента, нужно выполнить ещё несколько проверок. Итого, за один запрос получаем: оценку недвижимости + данные клиента + кредитный рейтинг клиента + расчёт страховки.
Пока что звучит хорошо, но…
У композитных запросов есть минусы:
Мы получаем огромное, порой избыточное, полотно данных, хотя мы тут как бы боремся с объёмом передаваемых данных… Могут быть нужны только некоторые части набора данных. Например, у нас уже есть данные о клиенте, но получать приходится весь комплект. Объём данных растёт почти на пустом месте, а производительность может упасть. А оно нам надо? Оно нам не надо.
Что делать? Возможно, надо попытаться указывать в запросе, какие запросы в композите вам нужны (если нужны только данные клиента, а не оценка недвижимости) или настроить сервер так, чтобы клиент отправлял минимальный набор данных, тем самым делегируя серверу не только обработку данных, но и сбор данных для него.
Сложные эндпоинты труднее разрабатывать, тестировать и поддерживать, требуется больше внимания. А любое изменение в структуре данных или логике может потребовать пересмотра всего эндпоинта.
Проблемы с кешированием.
В ячейку кеша вы кладёте обычно весь ответ. И если изменяется часть данных, то из-за этой части данных придется инвалидировать всю ячейку кеша, тем самым снижая эффективность его использования.
Потеря гибкости
В композитных API добавление нового поля может повлечь создание отдельного endpoint или модификацию существующего. И тогда появляется риск сломать существующую архитектуру, потому что композитные API более громоздкие и менее гибкие для изменений.
Указанные недостатки вовсе не означают, что способ плохой. Вы же не отказываетесь от картошки только потому что она сырая, вы просто учитесь ее готовить. Также и здесь. Дам пару простых советов по композитным API из своего опыта:
Ограничивайте размер пакета.
Убедитесь, что клиенту нужны ВСЕ данные из пакета, иначе дайте клиенту возможность выбора состава запроса.
Продумайте обработку ошибок (сервер должен явно указывать, какие запросы были выполнены, а какие - нет).
Оптимизируйте логику обработки пакета на сервере.
Где производить расчёты: на клиенте или на сервере?

А теперь перейдем к одной из моих любимых микросервисных тем: выбор стороны для проведения расчетов. Почему любимой? Потому что я долго разбиралась в этой теме. Итак, история.
Мы делаем платеж, с платежа берётся комиссия. Для этого есть специальный сервис комиссий: он агрегирует в себе всю логику и условия для этого, в том числе размер процента, который является изменяемой величиной. Обычно у комиссий есть разные условия для расчёта: наличие бесплатного лимита, тип карты списания и прочее.
В нашем типе платежа логика комиссии не зависит ни от каких внешних факторов, и логика самая примитивная - обычный процент от суммы. То есть для нас сервис комиссий делает простую арифметическую операцию. Мне показалось неэффективным запрашивать эту операцию в сервисе комиссий при каждом платеже. И я стала искать варианты. Начала с критериев, по которым можно принять решение о стороне проведения расчётов
Проводим вычисления на сервере, если:
Ресурсоемкие вычисления
Если вычисления требуют значительного объёма памяти или времени, лучше делегировать эту задачу тому микросервису, который специально предназначен для таких задач. Обычно такой микросервис оптимизирован под работу с большими объемами данных и имеет соответствующие ресурсы.
Пример:
Микросервис аналитики собирает данные из разных источников и обрабатывает их для создания отчётов. Вызывающий сервис отправляет запросы на обработку данных, а аналитический микросервис берет на себя все тяжелые вычисления.
Конфиденциальные данные
Если расчёты связаны с конфиденциальными данными, такими как личные данные пользователей, финансовая информация или другая защищенная информация, вычисления следует выполнять на стороне сервера, который отвечает за безопасность и защиту данных.
Пример:
Микросервис аутентификации и авторизации занимается шифрованием и дешифровкой токенов, а также проверкой прав доступа. Вызывающие сервисы передают ему данные для проверки, а сам микросервис проводит все необходимые криптографические операции.
Вычисления, требующие синхронизации и согласованности данных
Если нужно поддерживать консистентность данных между несколькими сервисами, вычисления лучше проводить на той стороне, которая управляет этими данными. Это помогает избежать проблем с синхронизацией и консистентностью.
Пример:
Микросервис управления заказами ведет учет всех заказов и связанных с ними данных. Вызывающий сервис передает информацию о новом заказе, а микросервис управления заказами обновляет свои данные и возвращает подтверждение об успешном создании заказа.
Проводим вычисления на клиенте, если:
Простые и быстрые вычисления
Когда речь идет о простых операциях, таких как арифметика, проверка валидности данных или форматирование, вычисления чаще всего выполняются на стороне вызывающего сервиса. Это уменьшает количество сетевых вызовов и снижает нагрузку на сеть.
Кеширование и повторное использование данных
Если одни и те же вычисления могут потребоваться многократно, имеет смысл кэшировать результаты на стороне клиента. Это уменьшит нагрузку на вызывающий сервис и ускорит последующие запросы.
Пример:
Микросервис поиска продуктов предоставляет результаты поиска на основе пользовательских запросов. Результаты поиска кешируются, чтобы минимизировать количество обращений к базе данных и ускорить ответы на повторяющиеся запросы.
Обратимость операций
Если операция обратима (то есть ее можно отменить или откатить назад), такие вычисления лучше проводить на стороне вызываемого микросервиса. Это упрощает ведение журнала операций и возможность восстановления состояния системы.
Пример:
Микросервис обработки платежей поддерживает функции отмены транзакций. Вызывающий сервис инициирует отмену платежа, а платежный микросервис выполняет всю необходимую логику отмены и возвращает статус операции.
Вернемся к истории про расчёт комиссии. Перечисленные критерии указывают на то, что можно реализовать это на клиенте. Что нам мешает?
История (продолжение)
Итак, вводные:
Параметры расчёта хранятся на сервере;
В любой момент может измениться не только само число, но и логика вычислений: могут добавиться какие-то условия, бесплатные лимиты и прочее.Но любой новый способ расчёта будет применяться с началом нового операционного дня. То есть, мы не можем оказаться в ситуации, что в обед вдруг поменялся способ расчёта.
Решение
Мы решили, что сделаем кеш на стороне клиента, и каждый день во время первого платежа будем запрашивать 2 параметра: разрешение на самостоятельное вычисление (читай, логика расчета не изменилась, новых факторов, влияющих на сумму комиссии, не появилось) и сам процент. И тогда вычисляем это у себя в вызывающем сервисе.

А вот если сервис комиссий не разрешил нам вычислять самостоятельно, то мы ходим за комиссией при каждом платеже.

Таким образом мы не делаем лишнего вызова и поддерживаем изменяемость логики.
Оптимизация, конечно, хороша для производительности, но часто ее нужно сочетать с другими нефункциональными требованиями: безопасность, надёжность и т. д.
И такие сочетания часто требуют поиска компромиссов. Рассмотрим основные, которые я вынесла для себя из разных кейсов и которые вам, я думаю, стоит знать, если вы принимаете решение о стороне вычислений.
Производительность vs Безопасность:
Если расчёты выполняются на стороне клиента (например, в браузере), то нагрузка на сервер уменьшается, что улучшает общую производительность системы. Однако при этом возникает риск того, что клиент может подделать результаты вычислений, особенно если данные чувствительны. Например, если речь идет о финансовых расчётах, злоумышленник может изменить итоговую сумму транзакции.
Компромисс: Критичные расчеты — только на сервере, даже если страдает производительность. Безопасность важнее.
В менее критичных случаях можно рассмотреть возможность выполнения части вычислений на клиентской стороне, но при этом необходимо тщательно проверять полученные результаты на серверной стороне.
Например, фронт может проверять права пользователя на совершение какой-либо операции, т. е. фронт будет выступать первым фильтром, а до сервера уже будут доходить отфильтрованные (читай, разрешённые) фронтом запросы, которые сервер будет, разумеется, перепроверять на своей стороне. Оптимизация в том, что сервер будет проверять уже не 100% поступающих запросов, а меньше.Производительность vs Надёжность:
Если сервер выполняет все расчёты, он может быстро исчерпать свои ресурсы, особенно при большом количестве пользователей. Это может снизить производительности всей системы.
С другой стороны, выполнение всех расчётов на сервере гарантирует точность и надёжность результатов, так как они защищены от вмешательства со стороны клиентов.
Компромисс: Можно использовать кеширование промежуточных результатов на стороне сервера или клиента, чтобы уменьшить нагрузку на сервер. Также можно внедрить механизмы ограничения запросов (rate limiting), чтобы предотвратить перегрузку сервера.Валидация vs Скорость:
Важно тщательно проверять данные перед их использованием в расчётах. Это помогает избежать ошибок и атак типа SQL-инъекций или XSS. Но валидация данных увеличивает время обработки запросов, что может негативно сказаться на времени отклика системы.
Компромисс: Разделение логики валидации на несколько этапов. Простую валидацию можно провести на клиентской стороне (например, проверка формата данных), а более сложную – на сервере. Это позволит сократить время отклика без ущерба для безопасности.
Унифицированный формат ответа
Зачем?
Предсказуемость и удобство! Клиенты и коллеги-разработчики не должны гадать, что получат в ответ при однородных запросах (например, при создании ресурса).
Стандарты:
JSON Schema — очевидный базис.
JSend — удобная спецификация для унифицированного представления json-ответов.
- success: Успешное выполнение операции.
- fail: Операция завершилась неудачно, но без исключений.
- error: Произошла ошибка, требующая внимания разработчика.HATEOAS — предполагает включение ссылок на другие доступные ресурсы прямо в ответе. Это позволяет клиентам перемещаться по системе, следуя ссылкам, предоставленным сервером. Это упрощает навигацию по API и уменьшает необходимость в документировании каждого возможного пути.
Это все стандартные приемы, а про конкретику давайте вместе порассуждаем в комментариях. Ведь многие наверняка с этим сталкивались.
Задачка со звёздочкой
Вы проектируете админскую панель. После создания/редактирования пользователя в админке — что возвращать: полную карточку или только ID?
Спойлер: всё не так однозначно. Обратимся к критериям и поищем компромисс.
Критерии решения:
Размер объекта: Большой сложный объект → возвращайте ID (чтобы не гнать лишние данные) или ссылку. Объект небольшой и в нём ограниченное количество полей – можно вернуть всю информацию и сделать жизнь клиента чуть-чуть проще.
Ожидания клиента:
Некоторые клиенты ожидают получить полную информацию о созданном ресурсе сразу, чтобы избежать дополнительного запроса. Это может быть удобно для фронтенда или других приложений, которые сразу отображают новую запись.
Другие клиенты могут предпочитать минималистичный ответ, чтобы затем самостоятельно запрашивать необходимую информацию по мере надобности.
Компромисс:
Используйте комбинационный подход, когда мы делегируем решение клиенту и предлагаем ему указать через параметр запроса (include_full_data=true), что он хочет получить полную информацию. А по умолчанию возвращаем только ID.
Кеширование
Цели: снизить нагрузку, ускорить ответы, сэкономить ресурсы, улучшить масштабируемость и пользовательский опыт.
Кешируемые ответы (на клиенте):
Несправедливо забытый аналитиками вид кеширования. Этот вид кеша хранится на стороне пользователя, чаще всего в браузере. Это может быть кэширование страниц, изображений и других статических ресурсов. Нам, аналитикам, проектирующим API, интересны кешируемые ответы, суть которых в том, что они сохранены для дальнейшего восстановления и использования позже, тем самым снижая число запросов к серверу. Не все HTTP-ответы могут быть закешированы.

Что учесть при проектировании
Инвалидирование
Некоторые некешируемые запросы-ответы к определённым URI могут сделать недействительным предыдущие закешированные ответы на тех же URI.
Например, мы запросили баланс карты и закешировали его на клиенте.

А потом по тому же пути, но запросом типа PUT мы совершаем платеж. И тогда закешированные данные поэтому пути типа GET будут очищены.

PUT к объекту инвалидирует все закешированные ответы GET или HEAD запросов к этому ресурсу.
Для запросов типа POST и PATCH можно самостоятельно регулировать возможность кеширования. Поэтому приведу несколько полезных заголовков, если решите управлять этой стороной запроса.
Заголовки Cache-Control
Cache-Control: max-age=X: Указывает максимальное время хранения ответа в кеше до его устаревания (в секундах). Например, max-age=3600 означает, что ответ будет считаться актуальным в течение одного часа.
Cache-Control: no-cache: Указывает, что перед использованием ответа клиент должен проверить его свежесть на сервере (например, с помощью заголовка ETag).
Cache-Control: no-store: Ответ не должен сохраняться в кеше ни клиентом, ни промежуточными прокси-серверами.
Last-Modified
Дата последней модификации ресурса. Используется для проверки актуальности данных. Клиенты могут отправлять запросы с заголовком If-Modified-Since.
Так что не забывайте про кешируемые ответы (и про случаи, когда они становятся невалидными).
Наблюдаемость: Health Check
Что значит наблюдаемость? Это значит, что мы хотим быстро узнать о здоровье сервиса.
Пациент скорее жив?

У нас бывали случаи, когда сервис работает, (то есть мы видим, что он не упал), но запросы к нему валятся.
Один раз это было из-за перегруза, а второй – когда у него затянулся процесс инициализации после ребута, и он долго проводил проверки работоспособности. Разработчикам становится понятно, что посылать к нему запросы нельзя, пока он не будет готов их обработать, а вот что делать клиентам?
Разумеется, есть такая классная штука как кубер, но если по каким-то причинам она вам недоступна, то есть маленький и быстрый лайфхак.
API health check
Основная задача health check — быстро и надежно узнать, готов ли сервер обрабатывать запросы и выполнять свои функции. Как правило, такой чек нужен для 2 ситуаций: понять, что сервер готов к приёму запросов после перезагрузки, и понять, что он перегружен.
Health check обычно выполняется путем отправки простого запроса к специальному маршруту API, который возвращает информацию о состоянии системы. Он предусматривает проверку не только доступности API, но и готовности всех его ключевых компонентов: база, кеш, службы и так далее.
Как правило, такие чеки делаются автоматически через заданный интервал времени. При неудовлетворительном ответе мы обычно закрываем доступ к сервису до следующего чека.
«Но зачем это всё аналитику?» — спросит меня очень терпеливый читатель. А затем, что сейчас аналитик стоит в одном ряду с архитектором. И если он умеет не только собирать требования, но и оптимизировать, то такой сотрудник становится ценным игроком в компании.
Вместо заключения
Мы столько всего рассмотрели, что я в паре слов сделаю обзор всех выводов.
Не оптимизируйте«для галочки». Беритесь только за узкие места.
Готовые решения > кастомные костыли. Используйте Kubernetes, API-гейтвеи, если они доступны.
Сложная оптимизация = сложная поддержка. Не создавайте монстров.
Безопасность и надежность — прежде всего! Никаких рисков в угоду скорости.
Тестируйте последствия. Убедитесь, что оптимизация ничего не сломала.
Делитесь вашими кейсами оптимизации REST API в комментариях!
svetayet
Реально классная статья с кучей практических историй, но немного сумбурная (даже просто по оформлению заголовков, особенно в центральной части).
И зачем-то еще в конце вот это " Используйте Kubernetes, API-гейтвеи, если они доступны. " приписали, хотя об этом в статье вообще ни слова, и это крайне опосредовано относится к оптимизации rest.