Почему джоба очистки файлов съедала всю память раз в сутки

Ночная джоба собирала список файлов старше 30 дней для удаления из S3-совместимого хранилища. Хранилище росло, список файлов — тоже, и в какой-то момент джоба стала падать с OutOfMemoryError прямо посередине выполнения.

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

List toDelete = new ArrayList<>(); for (FileMetadata f : storage.listAll()) { if (f.olderThan30Days()) { toDelete.add(f); } } for (FileMetadata f : toDelete) { storage.delete(f.getKey()); }

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

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

Любой список, собираемый перед обработкой, стоит мысленно умножать на продакшен-объём, а не на тестовый.

senior·base — что спрашивают на самом деле


В этом посте были ссылки, но мы их удалили по правилам Сетки