Мелкие файлы в Spark: почему 10 ГБ читаются медленно

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

Такое часто появляется после потоковой записи маленькими micro-batch: каждые несколько секунд множество партиций пишет по одному файлу. За день накапливаются тысячи объектов, хотя полезных данных немного. Другая причина — чрезмерное партиционирование по дню, региону, продукту и клиенту сразу. Большинство каталогов оказываются почти пустыми; фильтр по дню не компенсирует огромный список мелких файлов внутри дня.

Сначала измерьте распределение

Посчитайте число файлов, медианный размер и суммарный размер по каждой логической партиции. Отдельно сравните время listing, планирования и scan в Spark UI. Если 90% файлов занимают по несколько килобайт, проблема не сводится к скорости чтения 10 ГБ. Если же файлы крупные, а одна задача долго распаковывает их, причина может быть в skew или сложной трансформации.

Для файловых источников Spark использует параметры spark.sql.files.maxPartitionBytes и spark.sql.files.openCostInBytes, чтобы группировать файлы в разделы чтения. Это может уменьшить число слишком мелких scan-задач, но не удаляет физические файлы и не отменяет стоимость обращения к ним. Не путайте число input-файлов и число задач: Spark способен сгруппировать несколько файлов в одну задачу.

Исправление начинается на записи

Уменьшите число выходных партиций перед записью, если это не создаст одну гигантскую задачу. Для регулярного потока увеличьте объём данных на одну запись или запланируйте compaction уже сохранённых файлов. Цель — разумный диапазон размеров для вашей системы, а не магическое одинаковое число мегабайт в любом кластере. Сравните время чтения и записи до и после на типичном запросе.

Операция compaction переписывает физические файлы, поэтому ей нужно безопасно публиковать новый набор и удалять старый с учётом читателей. В табличном формате с транзакционным журналом используйте предусмотренную процедуру обслуживания таблицы; простое ручное удаление объектов может нарушить ссылки активного снимка. Для обычного каталога файлов договоритесь, как переключается читаемый путь и когда старые файлы можно убрать.

Не ставьте coalesce(1) на весь дневной поток ради одного красивого файла. Один выходной раздел ограничит параллелизм и может перегрузить память или диск одного executor. Лучше выбрать размер выходных файлов исходя из объёма логической партиции: пустые и малые группы объединять, крупные оставлять параллельными.

После исправления проверьте эффект на том же временном фильтре и похожей нагрузке. Оцените не только общее время job, но и число файлов, число scan tasks, bytes read и длительность планирования. Если запись стала существенно дороже, а чтение ускорилось мало, компромисс для этой таблицы может быть невыгодным.

Разберём численно: если 10 ГБ распределены по 50 тысячам файлов, средний файл содержит около 0,2 МБ. Даже когда Spark упакует многие файлы в одну задачу, ему приходится обнаружить и открыть множество объектов. Если те же 10 ГБ лежат в 80 файлах, средний размер около 125 МБ. Эти числа не означают универсальную целевую норму; они показывают порядок различия между данными и служебными операциями.

Партиционирование по дню помогает запросу «вчерашние продажи» пропустить другие даты. Но дополнительная партиция по customer_id, если клиентов сотни тысяч, создаёт множество каталогов с крошечными файлами. Выбирайте физическую партицию по часто используемому фильтру с достаточно крупным объёмом данных на значение. Высококардинальные измерения обычно оставляют внутри файла и используют статистику формата, если она доступна.

Compaction планируйте по самым проблемным разделам, а не переписывайте всю таблицу каждый час. Например, вчерашний день после завершения потоковой загрузки можно собрать в более крупные файлы один раз, а текущий день оставить изменяемым. Если поздние события дописываются в прошлые даты, предусмотрите повторную оптимизацию этих разделов. Сам процесс обслуживания тоже потребляет вычисления и может конкурировать с аналитическими запросами.

На курсе Data Engineering Fast‑Track Spark и lakehouse рассматриваются как единый конвейер от записи до проверяемой витрины. Откройте программу, чтобы понимать стоимость структуры файлов на последующем чтении.

Если медленной оказывается одна задача соединения, механизм другой — сравните разделы программы по Spark и диагностику skew в этом материале.

Продолжить на практике

  • Data Engineering Fast‑Track: Kafka → Airflow → Spark ↗

    Практический fast-track по data engineering: за 4–6 недель собираем рабочий конвейер Kafka → Spark → Lakehouse под управлением Airflow. Разберём batch и streaming, окна и watermark, схемы и контракты. Мини‑проекты, Docker Compose, шаблоны DAG и пайплайнов. Курс подойдёт инженерам, аналитикам и тем, кто хочет освоить современные пайплайны данных с уверенным Python.