Реализация пакетной обработки файлов (Batch Processing) на сервере

Ручная загрузка больших файлов на сервер часто приводит к зависаниям, ошибкам памяти и потере данных. Мы разрабатываем пакетную обработку файлов (batch processing), которая автоматизирует импорт CSV и параллельную обработку данных. Наша команда реализует проект под ключ — от аудита до внедрения, обеспечивая стабильную работу и последующую поддержку.

Разработка и обслуживание любых видов сайтов:

Информационные сайты или веб-приложения
Сайты визитки, landing page, корпоративные сайты, онлайн каталоги, квиз, промо-сайты, блоги, новостные ресурсы, информационные порталы, форумы, агрегаторы
Сайты или веб-приложения электронной коммерции
Интернет-магазины, B2B-порталы, маркетплейсы, онлайн-обменники, кэшбэк-сайты, биржи, дропшиппинг-платформы, парсеры товаров
Веб-приложения для управления бизнес-процессами
CRM-системы, ERP-системы, корпоративные порталы, системы управления производством, парсеры информации
Сайты или веб-приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, конструкторы сайтов, порталы предоставления электронных услуг, видеохостинги, тематические порталы

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Услуги, которые мы предлагаем
Показано 1 из 1Все 2062 услуг
Реализация пакетной обработки файлов (Batch Processing) на сервере
Сложный
~3-5 дней
Часто задаваемые вопросы

Мы регулярно сталкиваемся с ситуацией, когда клиенту нужно импортировать 50 000 строк из CSV за разумное время, не обрушив сервер. Без чёткой стратегии batch-обработки такие задачи приводят к OOM и бесконечным таймаутам. Наш опыт показывает: правильная архитектура сокращает время обработки в 10 раз и исключает потерю данных. При этом не имеет значения, работаете ли вы с десятками или сотнями тысяч записей — паттерны остаются теми же.

Ключевые проблемы batch-обработки

Память. Загружать весь CSV в массив — верный путь к исчерпанию памяти. Правильный паттерн — потоковое чтение чанками. Мы используем LazyCollection Laravel, который читает файл построчно без загрузки в память.

Частичные ошибки. Если из 10 000 строк 50 невалидны — останавливать весь процесс неправильно. Наша логика: пропускаем плохие строки, пишем в лог c контекстом (например, email ошибочной записи), и продолжаем.

Воспроизводимость. Если процесс упал на 7000-й строке — мы не начинаем заново. Laravel Batch позволяет продолжить с места остановки, сохраняя уже обработанные чанки.

Параллелизм. Последовательная обработка 50 000 записей по 100 мс каждая займёт почти 1,5 часа. Разбивка на параллельные Job'ы (оптимальный чанк — 500 записей) сокращает это до 5–10 минут на 4 воркерах.

Типичная проблема Решение в нашей практике
OOM при загрузке Потоковое чтение LazyCollection + чанки
Остановка при первой ошибке allowFailures() + логирование контекста
Невозможность возобновления Сохранение состояния в job_batches
Медленная последовательная обработка Параллельные Job'ы с чанками по 500

Как избежать утечки памяти при импорте больших CSV?

Мы применяем паттерн «Batch → Chunks → Jobs». После загрузки файла мастер-задача разбивает данные на чанки, каждый чанк обрабатывается отдельным Job'ом параллельно. После завершения всех Job'ов запускается задача агрегации результатов.

namespace App\Services;

use Illuminate\Bus\Batch;
use Illuminate\Support\Facades\Bus;
use Illuminate\Support\LazyCollection;

class CsvImportService
{
    private const CHUNK_SIZE = 500;

    public function startImport(string $filePath, int $importId): string
    {
        $jobs = [];

        LazyCollection::make(function () use ($filePath) {
            $handle = fopen($filePath, 'r');
            $header = fgetcsv($handle);
            while (($row = fgetcsv($handle)) !== false) {
                yield array_combine($header, $row);
            }
            fclose($handle);
        })
        ->chunk(self::CHUNK_SIZE)
        ->each(function ($chunk, $index) use (&$jobs, $importId) {
            $jobs[] = new ProcessCsvChunkJob(
                importId: $importId,
                chunkIndex: $index,
                rows: $chunk->values()->toArray()
            );
        });

        $batch = Bus::batch($jobs)
            ->name("csv-import-{$importId}")
            ->allowFailures()
            ->then(function (Batch $batch) use ($importId) {
                Import::find($importId)?->update(['status' => 'completed']);
                ImportCompletedEvent::dispatch($importId);
            })
            ->catch(function (Batch $batch, \Throwable $e) use ($importId) {
                Import::find($importId)?->update([
                    'status' => 'partially_failed',
                    'error_message' => $e->getMessage(),
                ]);
            })
            ->finally(function (Batch $batch) use ($importId) {
                $import = Import::find($importId);
                $import?->update([
                    'total_jobs' => $batch->totalJobs,
                    'failed_jobs' => $batch->failedJobs,
                    'finished_at' => now(),
                ]);
            })
            ->onQueue('batch-processing')
            ->dispatch();

        Import::find($importId)?->update(['batch_id' => $batch->id]);

        return $batch->id;
    }
}

Job обработки чанка

class ProcessCsvChunkJob implements ShouldQueue
{
    use Batchable, Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public int $tries = 3;
    public int $timeout = 120;
    public int $backoff = 10;

    public function __construct(
        private int $importId,
        private int $chunkIndex,
        private array $rows
    ) {}

    public function handle(): void
    {
        if ($this->batch()?->cancelled()) {
            return;
        }

        $successCount = 0;
        $errors = [];

        foreach ($this->rows as $lineNum => $row) {
            try {
                $this->processRow($row);
                $successCount++;
            } catch (\Throwable $e) {
                $errors[] = [
                    'chunk' => $this->chunkIndex,
                    'line' => $lineNum,
                    'data' => array_slice($row, 0, 3),
                    'error' => $e->getMessage(),
                ];
            }
        }

        ImportChunkResult::create([
            'import_id' => $this->importId,
            'chunk_index' => $this->chunkIndex,
            'processed' => count($this->rows),
            'succeeded' => $successCount,
            'failed' => count($errors),
            'errors' => $errors,
        ]);

        Import::where('id', $this->importId)->increment('processed_rows', count($this->rows));
        Import::where('id', $this->importId)->increment('success_rows', $successCount);
    }

    private function processRow(array $row): void
    {
        $validated = validator($row, [
            'email' => 'required|email',
            'name' => 'required|string|max:255',
        ])->validate();

        User::updateOrCreate(
            ['email' => $validated['email']],
            ['name' => $validated['name']]
        );
    }
}

Что делать, если процесс прервался?

Laravel Batch сохраняет состояние в таблице job_batches. Завершённые чанки помечены как выполненные, незавершённые возобновляются автоматически после перезапуска воркера. Для принудительного рестарта можно запросить незавершённые индексы из ImportChunkResult и повторно диспатчить Job'ы.

Прогресс в реальном времени выводится через endpoint:

public function progress(int $importId): JsonResponse
{
    $import = Import::findOrFail($importId);
    $batch = $import->batch_id ? Bus::findBatch($import->batch_id) : null;

    return response()->json([
        'status' => $import->status,
        'processed_rows' => $import->processed_rows,
        'success_rows' => $import->success_rows,
        'total_rows' => $import->total_rows,
        'percentage' => $import->total_rows > 0 ? round($import->processed_rows / $import->total_rows * 100, 1) : 0,
        'batch' => $batch ? [
            'total_jobs' => $batch->totalJobs,
            'pending_jobs' => $batch->pendingJobs,
            'failed_jobs' => $batch->failedJobs,
            'progress' => $batch->progress(),
        ] : null,
    ]);
}

Ограничение нагрузки

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

[program:batch-worker]
command=php artisan queue:work --queue=batch-processing --max-jobs=50 --sleep=3 --timeout=120
numprocs=4
autostart=true
autorestart=true

numprocs=4 — четыре воркера, каждый обрабатывает чанки последовательно. --max-jobs=50 — после 50 задач воркер перезапускается, освобождая память.

Размер чанка Время обработки 50 000 записей Риск утечки памяти
100 ~20 минут Низкий
500 ~10 минут Низкий
1000 ~8 минут Средний
5000 ~6 минут Высокий

Выбор 500 как оптимального компромисса между скоростью и стабильностью.

Процесс работы

  1. Аналитика: изучаем формат файлов, объём данных, требования к скорости.
  2. Проектирование: выбираем размер чанка, конфигурируем очереди.
  3. Реализация: пишем код с чанками, обработкой ошибок, прогрессом и возобновлением.
  4. Тестирование: прогоняем на тестовых данных с эмуляцией сбоев.
  5. Деплой: настраиваем воркеры, мониторинг, передаём документацию.

Что входит в реализацию под ключ

  • Архитектура batch-обработки с чанками и параллельными Job'ами.
  • Endpoint прогресса и возобновления после сбоев.
  • Детальное логирование ошибок для анализа.
  • Документация по развёртыванию и эксплуатации.
  • Обучение команды работе с системой.
  • Поддержка в течение месяца после запуска.

Сроки и стоимость

Базовая реализация импорта CSV с чанками и прогрессом — от 1 рабочего дня. Добавление возобновления, детального лога и поддержки XLSX/JSON — ещё от 1–2 дней. Стоимость рассчитывается индивидуально — пишите, мы оценим ваш проект. Более 5 лет мы занимаемся веб-разработкой, реализовали свыше 50 проектов с batch-обработкой — гарантируем стабильность и масштабируемость решения.

Параметр Последовательная обработка Пакетная (наша реализация)
50 000 записей ~1,5 часа ~5–10 минут
Утечки памяти Вероятны при большом объёме Исключены (чанки по 500)
Обработка ошибок Остановка всего процесса Пропуск проблемных строк
Возобновление Только с начала С места остановки

Почему Bus::batch()?

Laravel Bus::batch() предоставляет встроенную поддержку групповых задач: отслеживание статуса, частичные ошибки, цепочки колбэков. Это избавляет от написания собственного планировщика и снижает риск ошибок.

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

Наши компетенции:

Последние работы

  • Разработка сайта компании B2B ADVANCE
    Разработка сайта компании B2B ADVANCE
    1542
  • Разработка веб-приложения для компании FEEDME
    Разработка веб-приложения для компании FEEDME
    1393
  • Разработка веб-сайта для компании БЕЛФИНГРУПП
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    1081
  • Разработка интернет магазина для компании FURNORO
    Разработка интернет магазина для компании FURNORO
    1368
  • Разработка веб-приложения для компании Enviok
    Разработка веб-приложения для компании Enviok
    1139
  • Разработка веб-сайта для компании ФИКСПЕР
    Разработка веб-сайта для компании ФИКСПЕР
    1060