Снижение затрат на API с помощью условных вызовов

Снижение затрат на API с помощью условных вызовов

1. Обзор

В предыдущей статье я описал, как решил проблему задержек при ответах главного экрана, возникавших при работе с сервисом MyData, используя стратегию рендеринга с приоритетом кэша и фонового обновления — иными словами, подход, соответствующий современному шаблону Stale-While-Revalidate (SWR). В этой статье рассматривается следующая проблема, с которой я столкнулся при эксплуатации того же сервиса. На этот раз проблема заключалась не в производительности, а в стоимости.

После изменения структуры тарификации API для поиска данных во внешних организациях само количество вызовов стало операционными расходами, из-за чего стало трудно продолжать использовать прежний подход — «получать всё при необходимости». Чтобы решить эту проблему, я применил условные вызовы на основе временных меток, используя время предыдущего поиска, и перераспределил моменты выполнения вызовов (пакетное обновление + Lazy Loading). Оглядываясь назад, можно сказать, что этот подход, названия которого я тогда не знал, основывался на той же идее, что и условные запросы — механизм, уже давно предоставляемый протоколом HTTP в качестве стандарта. В этой статье последовательно рассматриваются проблема, процесс её решения и переосмысление с точки зрения стандартных механизмов.

2. Проблема: количество вызовов становится расходом

Сервис MyData устроен так, чтобы собирать сведения об активах и операциях пользователей, вызывая стандартные API множества внешних финансовых организаций. Даже в рамках одного процесса обновления последовательно вызываются подчинённые API, включая поиск списка организаций, поиск списка счетов, получение подробных данных по конкретным счетам и поиск истории операций. В результате даже при однократном обновлении информации одного пользователя генерируется значительное количество вызовов.

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

Если задержка ответа, рассмотренная в предыдущей статье, была проблемой, которую пользователи непосредственно ощущали, то эта проблема оставалась для них незаметной, но напрямую влияла на устойчивость эксплуатации. Её можно сформулировать так: «Действительно ли нужно каждый раз заново получать всё, лишь чтобы проверить данные, которые всё равно не изменились?» В конечном счёте решение заключалось не в полном отказе от вызовов, а в выявлении и пропуске ненужных вызовов.

3. Стратегия решения 1: условные вызовы на основе временных меток

Спецификация стандартного API MyData включает понятие Timestamp, предназначенное для указания момента времени, относительно которого выполняется поиск. Благодаря этому сначала можно проверить, изменилось ли что-нибудь с момента предыдущего поиска. Если изменений нет, ранее сохранённые данные используются без изменений, а вызовы низкоуровневых API пропускаются. Подробный поиск выполняется только при обнаружении изменений.

Процесс обработки можно обобщить следующим псевдокодом.

function 자산정보갱신(사용자ID, 기관ID):
  이전조회시점 = 조회이력.최근조회시점(사용자ID, 기관ID)
  변경여부 = 외부기관API.변경확인(사용자ID, 이전조회시점)
 
  if 변경여부 == 없음:
     return 저장소.기존데이터(사용자ID, 기관ID)
 
  최신데이터 = 외부기관API.상세조회(사용자ID)  // 과금 대상 호출
  저장소.갱신(사용자ID, 기관ID, 최신데이터)
  조회이력.기록(사용자ID, 기관ID, 현재시각)
  return 최신데이터

Ключ к этой структуре — использование асимметрии: «стоимость запроса низкая, а стоимость получения данных высокая». Сначала мы выполняем лёгкий вызов, чтобы проверить наличие изменений, и ограничиваем более тяжёлый вызов, возвращающий полный набор данных, только случаями, когда изменение подтверждено. Такая структура также обеспечивает большую экономию для пользователей, чьи данные меняются нечасто.

4. Стратегия решения 2: перераспределение времени выполнения вызовов

Если условные вызовы были механизмом фильтрации по принципу «выполнять ли вызов», то вторая стратегия заключалась в пересмотре вопроса «когда выполнять вызов». Мы объединили два подхода.

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

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

Иными словами, подход заключается в том, чтобы выполнять вызовы «в необходимое время и только в необходимом объёме». Часто просматриваемая сводная информация подготавливалась заранее посредством периодических пакетных обновлений, редко просматриваемая подробная информация получалась по запросу с помощью Lazy Loading, а перед каждым вызовом выполнялась условная проверка. Так сформировалась структура из трёх частей.

4-1. Структурное сравнение

[Таблица 1] Сравнение структуры вызовов до и после улучшения

Категория

Существующая структура (As-Is)

Улучшенная структура (To-Be)

Решение о вызове

Получать всё пакетно при каждом обновлении

Получать только необходимое после проверки наличия изменений

Момент вызова

Сразу при открытии экрана или запросе обновления

Периодические пакетные обновления + Lazy Loading при открытии экранов подробностей

Характеристики объёма вызовов

Растёт пропорционально активности пользователей

Сходится к частоте изменения данных

Подробные данные

Получаются заранее в рамках основного процесса

Получаются только на экранах, которые пользователь действительно открывает

5. Ретроспектива: связь с условными запросами HTTP

5-1. Механизм, который уже был в HTTP

В HTTP существует стандартный механизм, называемый условным запросом. Когда клиент включает время изменения (Last-Modified) или идентификатор версии (ETag), полученные в предыдущем ответе, в заголовок If-Modified-Since или If-None-Match следующего запроса, сервер, если с тех пор ничего не изменилось, возвращает только ответ 304 Not Modified без тела. Идея заключается в том, чтобы «сначала спросить, изменилось ли что-нибудь, и не передавать тело, если изменений нет».

Условные вызовы на основе Timestamp в данном случае основаны на той же идее, что и этот механизм. Предварительная проверка изменений по времени предыдущего поиска выполняет роль, аналогичную If-Modified-Since, а пропуск вызовов низкоуровневых API и повторное использование сохранённых данных при отсутствии изменений оказывает эффект, аналогичный ответу 304. Однако, как и в случае с различием, рассмотренным в предыдущей статье, уровни здесь отличаются: обработка не выполнялась автоматически на уровне протокола с помощью заголовков HTTP, а была реализована непосредственно в коде приложения с использованием спецификации Timestamp стандартного API. Речь шла не о заимствовании механизма, предоставляемого протоколом, а о самостоятельной реализации той же идеи на уровне приложения.

6. Связь с предыдущей статьёй: две оси одной и той же модели кэширования

Интересно, что стратегия SWR из предыдущей статьи и условные вызовы в этой статье представляют собой две сосуществующие оси модели кэширования HTTP. Их направления противоположны. SWR отдаёт приоритет скорости ответа, следуя стратегии «сначала показать устаревшие данные, а затем выполнить проверку в фоновом режиме». Условные запросы отдают приоритет экономии на передаче данных и стоимости вызовов: «сначала выполнить проверку и вообще ничего не передавать, если изменений нет».

[Таблица 2] Сравнение двух стратегий кэширования

Параметр

Stale-While-Revalidate (предыдущая статья)

Условные запросы (эта статья)

Основной принцип работы

Сразу ответить устаревшими данными, а затем выполнить проверку в фоновом режиме

Проверить наличие изменений и получить данные только при обнаружении изменений

Приоритет

Скорость ответа и воспринимаемая производительность

Снижение затрат на вызовы и передачу данных

Что сохраняется

Время ожидания пользователя

Ненужные вызовы и передача данных

Точки применения

Время отклика при переходе на главный экран

Вызовы для получения данных из внешних организаций

В реальном сервисе обе стратегии сосуществовали в разных точках. Для ответов экранов, с которыми сталкивались пользователи, мы применяли стратегию, ориентированную на SWR, а для исходящих запросов — условные вызовы. В рамках единой теории кэширования в соответствующих местах использовались стратегии, движущиеся в противоположных направлениях, в зависимости от того, «что следует сохранять».

7. Ограничения и компромиссы

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

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

8. Установление эксплуатационных рекомендаций

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

[Таблица 3] Критерии выбора стратегии вызовов по типам данных (пример)

Тип данных

Когда выполнять вызов

Условная проверка

Сводка по активам (отображается на главном экране)

Периодическое пакетное обновление

Применяется (пропускать, если данные не изменились)

Данные счёта · История транзакций

Отложенный вызов при переходе на экран подробной информации

Применяется (пропускать, если данные не изменились)

Данные от недавно интегрированной организации

Немедленное полное получение данных сразу после интеграции

Не применяется (нет истории для сравнения)

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

9. Заключение

Задержка ответа, обсуждавшаяся в предыдущей статье, и затраты на вызовы, рассмотренные в этой статье, казались разными проблемами, но после их решения мы обнаружили, что в конечном счёте они сводятся к одному и тому же вопросу: «Действительно ли этот вызов необходим прямо сейчас?» На стороне экрана ответом был рендеринг по принципу «сначала кэш», а на стороне внешних вызовов — условные вызовы и перенос времени выполнения вызовов.

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

cryss

Site footer