Проблема задержки ответов, решенная с помощью стратегии кэширования

Проблема задержки ответов, решенная с помощью стратегии кэширования

1. Обзор

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

На тот момент сервис представлял собой WebApp, основанный на Vue 2, а кэш был реализован не на стороне клиента, а на стороне сервера. В этом отношении кэш находился на совершенно другом уровне по сравнению с кэшами в памяти клиента, которые сейчас предоставляются библиотеками для выборки данных. Однако сама стратегия «показать устаревшие данные сначала и обновить их в фоновом режиме» была в сущности той же самой, что и стандартная стратегия кэша Stale-While-Revalidate (SWR), предложенная в экосистеме Vue с TanStack Query (ранее известный как Vue Query) и в экосистеме React. В этой статье рассматриваются диагноз проблемы, проектирование решения, результаты применения, а также переосмысление с точки зрения SWR, при этом будет рассмотрена причина, по которой удалось достичь такого же вывода без учета трендов.

2. Ситуация с проблемой: задержка ответа главного экрана

2-1. Существующая структура

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

2-2. Появление проблемы

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

Особенно в случае, если некоторые внешние учреждения отвечали медленно, структурная уязвимость, при которой эта задержка влияло на общее время загрузки экрана, также стала очевидной. Структура, предназначенная для того, чтобы соблюдать принцип «всегда показывать свежие данные», на самом деле стала узким местом, от которого зависела воспринимаемая производительность всего сервиса по мере увеличения числа пользователей. На этом этапе стало ясно, что одновременно удовлетворить требования к структуре, гарантирующей 100% актуальные данные, и структуре, позволяющей пользователям мгновенно получать экран, было трудно. Необходимо было переосмыслить проектирование, сосредоточившись на одном из двух ценностей.

3. Стратегия решения: кэш с приоритетом на рендеринг + обновление в фоновом режиме

3-1. Принципы проектирования

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

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

3-2. Поток обработки

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

function 메인화면진입(사용자ID):
  캐시데이터 = 캐시저장소.조회(사용자ID)
 
  if 캐시데이터 존재:
    화면.즉시렌더링(캐시데이터)
  else:
    화면.로딩상태표시()
 
  비동기작업큐.등록(갱신작업, 사용자ID)
  return
 
function 갱신작업(사용자ID):
  최신데이터 = 외부기관조회.전체조회(사용자ID)
  캐시저장소.갱신(사용자ID, 최신데이터)
  if 화면.현재활성상태(사용자ID):
    화면.부분갱신(최신데이터)

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

3-3. Распределение обновлений через очередь сообщений

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

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

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

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

Категория

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

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

Момент обработки данных

Полный запрос в реальном времени при входе на экран

Мгновенный ответ кэша + фоновое обновление

Характеристика ответа

Зависит от скорости ответа внешних учреждений

Фиксированное время отклика по скорости запроса кэша

Шаблон возникновения нагрузки

Наплыв запросов на внешние проверки при одновременных подключениях

Сглаживание запросов через очередь сообщений

Пользовательский опыт

Ожидание завершения общего запроса

Немедленное отображение данных и естественное обновление

4. Результаты применения

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

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

5. Если посмотреть сейчас: связь с паттерном SWR

5-1. Что такое паттерн SWR

Stale-While-Revalidate - это стратегия кэширования, которая сначала возвращает кэшированные (устаревшие, Stale) данные, одновременно выполняя в фоновом режиме проверку (Revalidate), а по завершении проверка заменяет их на актуальные данные. В этой стратегии есть три механизма с различными подходами. Первый уровень - уровень протокола HTTP, где заголовок Cache-Control: stale-while-revalidate (RFC 5861) позволяет браузеру, CDN или обратному прокси сначала передать кэшированный ответ, а затем в фоновом режиме выполнить повторную проверку. Второй уровень - уровень приложения, при котором запросы к кэшу и логика обновления в фоновом режиме реализуются в коде бэкенда без зависимости от протоколов или библиотек. Третий уровень - уровень клиентских библиотек, как TanStack Query или SWR.js, которые предоставляют ту же стратегию декларативно в памяти браузера.

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

5-2. Сравнение реализации собственного метода и реализации клиентской библиотеки

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

[Таблица 2] Сравнение способов реализации

Элемент

Прямо реализованный способ на тот момент

TanStack Query / SWR.js и т.д.

Место хранения кэша

Отдельный кэш хранения (серверная сторона)

Клиентская память·хранилище

Триггер обновления

Регистрация асинхронных задач при входе на экран

Разнообразные триггеры, такие как вход на экран, возврат фокуса, переподключение и т.д.

Распределение обновляющих задач

Контроль пропускной способности на основе очереди сообщений

Устранение дублирования запросов (Dedupe), политика повторных попыток

Сложность реализации

Необходимо непосредственное проектирование и реализация

Немедленно применимо с декларативной настройкой

5-3. Причины, приведшие к тому же заключению

В то время я не знал ни о существовании протоколов или библиотек, которые бы предоставляли такую стратегию в стандартном виде, ни о самом названии Stale-While-Revalidate. Однако я осознал на практике, что «данные, которые нужно немедленно показать пользователю», и «данные, точность которых необходимо гарантировать», не обязательно обрабатывать одновременно и одним и тем же способом, и результатом этого осознания стало естественное окончание в тех же паттернах. Я считаю, что это пример демонстрирует, что хорошие архитектурные паттерны часто переоткрываются независимо в процессе тщательного рассмотрения реальных проблем, а не начинаются с определённых тенденций технологий.

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

Эта структура также имела четкие компромиссы. Данные, отображаемые при входе на экран, по определению немного устаревшие (Stale), поэтому их сложно применить к экранам или функциям, где свежесть данных имеет критическое значение. Кроме того, если неправильно спроектировать момент недействительности кеша, существует риск того, что пользователи будут видеть устаревшую информацию дольше, чем на самом деле.

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

7. Установление операционного руководства

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

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

Тип данных

Срок хранения кеша

Приоритет обновления

Общая стоимость активов

Долгий (низкая частота изменений)

Низкий

Список имеющихся товаров

Промежуточный (необходима адаптация при подписке и отмене)

Промежуточный

История сделок

Кратко (актуальность напрямую связана с доверием)

Высокий

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

8. Завершение

Оглядываясь на этот опыт, я пришёл к решению не через предварительное изучение трендов или установленных паттернов, а путём точного разложения проблемы, с которой я столкнулся в процессе эксплуатации. Сейчас можно применять ту же стратегию с помощью таких библиотек, как TanStack Query или SWR.js всего лишь с несколькими строками декларативного кода, но опыт того, “почему эта стратегия необходима”, дал мне понимание большее, чем простое применение библиотеки.

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

cryss

Site footer