Как хранить промежуточные данные при распределенной обработке: сохранять или держать в памяти
Что проверяют
Умеете ли вы взвешивать скорость против отказоустойчивости, а не отвечать «в памяти быстрее». Хороший ответ начинается с вопроса: что будет, если узел упадёт посреди обработки. Следом обычно спрашивают про повторный запуск шага и дубли.
Как отвечать
Решение принимается по нескольким критериям:
- цена пересчёта: если шаг детерминированный, входные данные доступны повторно и пересчёт дешёвый, промежуточный результат держат в памяти и при сбое считают заново;
- одноразовый источник: сообщение уже подтверждено в очереди, поток событий ушёл. Без сохранения такие данные теряются, нужна контрольная точка;
- объём: всё, что не помещается в память узла с запасом, пишется на диск или в объектное хранилище. В .NET массивы от 85 000 байт попадают в Large Object Heap, и материализация
List<T>на миллионы элементов бьёт по GC; - кто читает дальше: если следующий шаг на другом узле, данные всё равно пересекают сеть, и надёжный промежуточный носитель вроде Kafka или blob-хранилища заодно решает передачу.
Инструменты в .NET: внутри процесса поток через IAsyncEnumerable<T>, System.Threading.Channels или TPL Dataflow с ограниченной ёмкостью, чтобы быстрый производитель не выедал память. Для общего кеша IDistributedCache поверх Redis. Для долгих процессов с состоянием Durable Functions или состояние грейнов в Orleans: прогресс сохраняется сам, и после рестарта работа продолжается с последнего шага.
Практичный ответ обычно гибридный: внутри шага данные в памяти, между шагами контрольная точка раз в N записей или секунд, а запись идемпотентна по ключу вида (jobId, stepId, chunk), чтобы повтор не создавал дублей.
Где спотыкаются
Называют Redis надёжным хранилищем. Это тоже память: с настройками по умолчанию после сбоя можно потерять последние записи, для контрольных точек нужна настроенная персистентность или другое хранилище.
Материализуют весь промежуточный результат через ToList() вместо потоковой обработки и получают OutOfMemoryException на первом большом прогоне.
Сохраняют прогресс, но не делают запись идемпотентной. После сбоя шаг выполняется повторно, и в итоговых данных появляются дубли.