Слогер Создать блог
Разработка

Что делать, если задача не укладывается в таймаут: самовосстанавливающийся кэш

Разбор на реальном случае: 700 секунд работы в окружении с лимитом 600. Почему простой кэш не спасает и как докачивать данные по частям.

Фоновая задача молча останавливается на полпути. Логи обрываются, ошибок нет, процесс просто убит. Классика. У меня так случилось с ботом для алгоритмической торговли: он должен был регулярно забирать данные по 250 тикерам с биржевого API. Каждый запуск заканчивался на середине, без единой ошибки.

Первая мысль — временный сбой сети или API. Но повторные запуски умирали в одном и том же месте. Пришлось считать.

  • Один запрос к API — в среднем 2,8 секунды.
  • Тикеров — 250.
  • Итого: 2,8 × 250 = 700 секунд.
  • А окружение, где крутится бот, принудительно убивает процесс через 600 секунд.

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

Почему это произошло? На этапе разработки я тестировал на 10 тикерах — они отрабатывали за 28 секунд. «Работает, можно выкатывать», — подумал я. И не пересчитал время для полного списка. Элементарная ошибка масштабирования.

Почему обычный кэш не помог бы

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

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

Решение: кэш, который чинит сам себя

Идея в том, чтобы сохранять прогресс не в конце, а по ходу работы. Я выбрал контрольные точки: после каждых 50 успешно загруженных тикеров — записывать всё, что уже накопилось, в pickle-файл на диск.

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

Как это выглядит в коде

Алгоритм простой.

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

Ключевая строка — проверка fetched_count % 50 == 0. Это чекпоинт. Без неё прогресс не фиксируется, и всё возвращается к проблеме обычного кэша.

Сравнение подходов

КритерийОбычный кэшSelf-healing кэш
Первый запускДолжен отработать полностью, иначе кэш не создастсяМожет упасть — останется частичный прогресс
ПрерываниеДанные в памяти теряютсяНа диске сохраняются чекпоинты
Следующий запускНачинает с нуляДокачивает только недостающее
Время при заполненном кэшеБыстро, но до этого надо дожитьБыстро — и до этого доходит за два-три запуска

Результаты

Первый запуск с фиксом всё равно упёрся в таймаут. Но после него в кэше лежало около 200 тикеров из 250. Второй запуск докачал оставшиеся 50. А дальше началось интересное.

При 100% попадании в кэш внешних запросов нет вообще. Задача, которая раньше не успевала за 600 секунд, теперь отрабатывает в среднем за 2,8 секунды. Это не в десять раз быстрее — это на порядки быстрее.

Практические шаги

  1. Посчитай суммарное время на полном объёме данных, а не на тестовой выборке. Это займёт две минуты, но спасёт от ночного даунтайма.
  2. Если суммарное время близко к лимиту окружения — закладывай чекпоинты с самого начала.
  3. Сохраняй прогресс на диск через равные интервалы, а не только в конце.
  4. При старте загружай сохранённое и пропускай уже обработанные элементы.
  5. Не забывай про TTL кэша, чтобы данные не протухли.

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

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

По материалам: ai. Текст переработан редакцией Слогера.

← На главную

Рекламное место — Конец поста
Реклама · Слогер

Комментарии (0)

Войдите, чтобы комментировать.

Пока нет комментариев. Будьте первым.