Фоновая задача молча останавливается на полпути. Логи обрываются, ошибок нет, процесс просто убит. Классика. У меня так случилось с ботом для алгоритмической торговли: он должен был регулярно забирать данные по 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 секунды. Это не в десять раз быстрее — это на порядки быстрее.
Практические шаги
- Посчитай суммарное время на полном объёме данных, а не на тестовой выборке. Это займёт две минуты, но спасёт от ночного даунтайма.
- Если суммарное время близко к лимиту окружения — закладывай чекпоинты с самого начала.
- Сохраняй прогресс на диск через равные интервалы, а не только в конце.
- При старте загружай сохранённое и пропускай уже обработанные элементы.
- Не забывай про TTL кэша, чтобы данные не протухли.
Подход работает, когда задача идемпотентна: повторная обработка тикера даёт тот же результат, и её можно пропустить. Для загрузки котировок, выгрузки отчётов, синхронизации справочников — отлично. Не подойдёт для потоковых операций, где частичный результат нельзя фиксировать или данные должны быть строго свежими.
Если долгая задача падает по таймауту, смотри не на сеть, а на дизайн. Заложи в неё возможность продолжиться с места обрыва. Это проще, чем кажется, а экономит часы отладки.
Комментарии (0)
Войдите, чтобы комментировать.
Пока нет комментариев. Будьте первым.