Перейти к содержимому
TelegramGitHub

Designing Data-Intensive Applications, глава 10: Пакетная обработка

Главная мысль: batch job читает ограниченный набор входных данных и создаёт новый результат. Вход не меняется. Поэтому задачу можно безопасно повторить, а сбои легко скрыть от кода.

Вступление Part III: системы и производные данные

Заголовок раздела «Вступление Part III: системы и производные данные»

Часть III книги про интеграцию нескольких систем в одно приложение. Здесь важно различие:

ТипЧто это
System of recordИсточник истины. Новые данные попадают сюда первыми. Каждый факт хранится один раз
Derived data systemРезультат обработки других данных: кэш, индекс, materialized view, denormalized копия. Её можно пересоздать из источника

Тип зависит не от СУБД, а от того, как вы её используете. Batch-обработка в этой главе создаёт именно derived data.

ТипКак работаетГлавная метрика
Service (online)Ждёт запрос и быстро отвечаетResponse time, availability
Batch (offline)Берёт большой вход, запускает job, пишет выход. Часто по расписаниюThroughput
Stream (near-real-time)Обрабатывает события вскоре после появленияLatency (глава 11)

Анализ лога цепочкой awk | sort | uniq -c | sort | head показывает идеи, которые перешли в Hadoop.

  • Каждая программа делает одно дело хорошо.
  • Вывод одной программы становится входом другой, ещё неизвестной.
  • Единый интерфейс: файл как последовательность байтов. Всё подключается ко всему.
  • Логика отделена от wiring: программа читает stdin и пишет stdout. Куда идут данные, решает shell.
  • Вход неизменяем. Можно запускать много раз и пробовать разные варианты.
  • Результат любого шага можно посмотреть или сохранить в файл и продолжить с этого места.

Главный предел: Unix-инструменты работают на одной машине.

MapReduce похож на Unix-процесс, распределённый на тысячи машин. Вместо файлов и pipes он использует HDFS.

HDFS строится по принципу shared-nothing. Каждая машина хранит свои блоки. NameNode знает, где лежит какой блок. Блоки реплицируются (или используют erasure coding). Object storage (S3 и др.) похож, но хранение и вычисления там обычно разделены.

MapReduce: input в HDFS, map, shuffle, reduce и запись результата в HDFS

Как выполняется job:

  1. Входной файл делится на записи. Каждый блок файла обрабатывает свой mapper.
  2. map вызывается для каждой записи и выдаёт пары key-value. Ноль или больше пар на запись.
  3. Framework делит вывод mapper по reducer через hash от ключа. Каждая партиция сортируется. Этот шаг называют shuffle.
  4. Reducer забирает свои партиции со всех mapper и сливает их, сохраняя порядок.
  5. reduce вызывается один раз на ключ с итератором по значениям. Результат пишется в HDFS.

Важные детали:

  • Сортировка встроена. Это самая важная часть модели.
  • Код mapper запускают рядом с данными (data locality), чтобы не гонять файлы по сети.
  • Число reducer задаёт автор job. Число mapper зависит от числа блоков входа.
  • Выход job считается валидным только после успешного завершения. Частичный результат сбойной job отбрасывается.

Одна job часто не хватает. Например, посчитать просмотры на URL можно одной job, а топ URL требует второй.

  • Hadoop не умеет связывать job. Связь идёт через имя каталога в HDFS: выход первой job равен входу второй.
  • Вторая job стартует только после успеха первой.
  • Для зависимостей используют workflow-планировщики: Oozie, Azkaban, Luigi, Airflow.
  • Workflow из 50–100 job обычны для рекомендательных систем.
  • Pig, Hive, Cascading, Crunch строят такие цепочки автоматически.

MapReduce не имеет индексов. Job читает весь вход (full scan). Для аналитики это нормально.

Не ходите из job в удалённую БД за каждой записью. Это медленно, перегружает БД и делает результат недетерминированным. Лучше скопировать таблицу в HDFS (ETL) и делать join локально.

  • Sort-merge join. Mapper для каждого входа извлекает join key. Shuffle собирает записи с одним ключом в одном reduce. Secondary sort ставит запись из справочника первой. Reducer хранит одну запись в памяти.
  • GROUP BY делается так же: ключ группировки становится ключом mapper.
  • Sessionization. Группировка событий по пользователю или сессии, например для A/B-тестов.

Идея: mapper «отправляет сообщения» reducer по ключу как по адресу. Сеть и сбои узлов спрятаны от кода.

Если на один ключ приходится очень много данных (linchpin object, например знаменитость в соцсети), один reducer становится медленным. Job ждёт самого медленного.

  • Skew join (Pig, Hive) и sharded join (Crunch). Записи горячего ключа идут на случайные reducer. Вторую сторону join копируют на все reducer.
  • Для GROUP BY: двухэтапная агрегация. Сначала частичная на случайных reducer, потом объединение.

Это урезанная job без reducer и без sort. Быстрее, но нужны допущения о данных.

ВариантУсловиеИдея
Broadcast hash joinОдна сторона мала и влезает в памятьКаждый mapper грузит маленький вход в hash table и сканирует большой
Partitioned hash join (bucketed map join в Hive)Оба входа партиционированы одинаково: тот же key, hash, число партицийHash table строится только для одной партиции
Map-side merge joinВходы партиционированы и отсортированы по ключуMapper сливает два отсортированных файла

Для оптимизации важно знать физическую раскладку данных: число партиций, ключ, сортировку. Эти метаданные хранят HCatalog и Hive metastore.

Выбор алгоритма join: broadcast, partitioned hash и sort-merge

Batch не OLTP и не чистая аналитика. Результат часто не отчёт, а новая структура данных.

  • Search index. Mapper делит документы, reducer строит индекс партиции. Файлы неизменяемы. При изменениях можно пересобрать всё или обновлять инкрементально.
  • ML и рекомендации. Результат часто key-value база для веб-приложения.
  • Как загружать результат. Не пишите в БД из mapper или reducer запись за записью. Это медленно, перегружает БД, а побочные эффекты видны при частичных и повторных попытках. Лучше собрать файлы базы в HDFS и загрузить их целиком (Voldemort, HBase bulk loading). Сервер атомарно переключается на новые файлы и может вернуться на старые.
  • Баг в коде: откатите код, перезапустите job, и результат снова верный (human fault tolerance). С БД, куда писали плохие данные, так не выйдет.
  • Можно держать старый результат в соседнем каталоге и переключаться.
  • Упавшую task безопасно перезапустить: вход неизменяем, вывод сбойной попытки отбрасывается.
  • Одни и те же файлы читают разные job, включая проверки качества выхода.
  • Логика отделена от wiring. Разные команды владеют разными job.

В Hadoop вместо текста часто используют Avro и Parquet: схема и эффективное кодирование.

MPP (massively parallel processing) базы, такие как Teradata, появились раньше и умеют те же алгоритмы join. Различия:

MPP-базаHadoop (HDFS + MapReduce)
ХранениеНужна модель заранее, свой форматЛюбые байты. Можно сначала сложить, потом разбираться (data lake, sushi principle)
ОбработкаSQL, один движокЛюбой код: ML, индексы, анализ изображений. SQL тоже (Hive)
СбоиОбычно abort и перезапуск всего запросаПовтор отдельной task
Память и дискДанные в памятиАктивно пишет на диск

Hadoop часто используют для ETL: сырые данные в HDFS, очистка и преобразование, загрузка в хранилище. Моделирование остаётся, но отделено от сбора. Разные модели обработки работают в одном кластере над одними файлами.

Почему MapReduce терпит частые сбои? Не из-за плохого железа. В дата-центрах Google batch-задачи имеют низкий приоритет, и их вытесняют (preemption). За час задача рискует примерно 5%. Это улучшает использование ресурсов. В YARN, Mesos и Kubernetes preemption применяют редко, и тогда эти решения менее оправданы.

У MapReduce есть слабое место: materialization промежуточного состояния. Каждая job пишет результат в HDFS с репликами.

  • Следующая job ждёт все task предыдущей, включая медленные (stragglers).
  • Mapper часто лишний: читает то, что только что записал reducer.
  • Промежуточные файлы реплицируются без нужды.

Dataflow engines (Spark, Tez, Flink) описывают весь workflow как один job. Функции называются operators. Они не обязаны чередоваться как map и reduce.

Сравнение: цепочка job MapReduce через HDFS и единый поток operators

Варианты связи операторов:

  • repartition и sort, как shuffle;
  • partition без sort, для partitioned hash join;
  • broadcast на все партиции.

Плюсы:

  • Сортировка только там, где нужна.
  • Нет лишних mapper.
  • Планировщик видит зависимости и учитывает locality.
  • Промежуточные данные в памяти или на local disk.
  • Operator стартует, как только готов вход.
  • JVM-процессы можно переиспользовать.

Код Pig, Hive и Cascading можно перевести с MapReduce на Tez сменой конфигурации.

Промежуточное состояние потеряно, значит его надо пересчитать.

  • Spark хранит lineage (какие партиции и operators дали данные) в абстракции RDD.
  • Flink делает checkpoint состояния operators.
  • Пересчёт требует детерминированных operators. Источники недетерминизма: порядок обхода hash table, случайные числа, системные часы, внешние источники. Для random используйте fixed seed.
  • Если промежуточные данные малы или расчёт тяжёлый, дешевле сохранить их в HDFS.

Flink строится вокруг pipelined execution. Но sort всё равно собирает весь вход. Входы и итоговые выходы обычно остаются в HDFS.

PageRank и подобные алгоритмы повторяют шаг, пока не наступит сходимость. В MapReduce это неэффективно: каждый раунд читает весь граф заново.

  • Pregel (BSP): Giraph, GraphX в Spark, Gelly во Flink. Вершина хранит состояние в памяти и получает сообщения предыдущей итерации. Работа идёт только там, где есть сообщения.
  • Отказоустойчивость: checkpoint состояния вершин после итерации.
  • Граф обычно делят по hash от ID вершины. Это даёт много сетевого трафика. Если граф помещается на одной машине, один поток часто быстрее кластера.
  • DAG в dataflow engines и граф как данные — разные вещи, хотя названия похожи.

Spark и Flink дают API на relational-операциях: join, group, filter, aggregate. Плюсы:

  • меньше кода, интерактивная работа в shell;
  • cost-based optimizer сам выбирает алгоритм join и порядок joins (Hive, Spark, Flink);
  • declarative фильтры позволяют читать только нужные колонки (column-oriented storage) и использовать vectorized execution.

Такие движки приближаются по скорости к MPP-базам, но сохраняют гибкость: произвольный код и любые форматы. Появляются готовые библиотеки: Mahout и MADlib для ML, k-nearest neighbors, genome analysis.

Термин (EN)Что значит
Batch processingОбработка ограниченного набора данных. Job завершается
Bounded inputВход с известным размером. В отличие от потока
HDFSРаспределённая файловая система Hadoop, shared-nothing
Map / ReduceФункции: извлечь key-value и обработать группу по ключу
ShuffleРазбиение вывода mapper по reducer, сортировка и копирование
Sort-merge joinReduce-side join через сортировку по join key
Broadcast hash joinМалый вход целиком в память каждого mapper
Partitioned hash joinHash join по одинаковым партициям обоих входов
SkewОдин ключ с огромным объёмом, одна reducer-партиция перегружена
MaterializationЗапись промежуточного результата на диск или в HDFS
Dataflow engineДвижок, исполняющий workflow как один job из operators (Spark, Flink, Tez)
RDDАбстракция Spark с отслеживанием происхождения данных
Deterministic operatorТе же входы дают тот же выход. Нужен для пересчёта
Pregel / BSPМодель итеративной обработки графов сообщениями между вершинами
Human fault toleranceВозможность исправить баг кода, перезапустив job на тех же данных
  • Делайте jobs идемпотентными: вход не меняется, результат пишется целиком и заменяет прежний. Тогда перезапуск безопасен.
  • Не пишите из задач в рабочую БД запись за записью. Готовьте файл или снимок и загружайте целиком с атомарным переключением.
  • Выбирайте join по данным: маленькая таблица измерений подходит для broadcast, а для двух больших ищите общий ключ партиционирования. Следите за skew: смотрите на самую медленную task.
  • Spark и Flink обычно быстрее MapReduce. Они пересчитывают потерянные данные, поэтому убирайте недетерминизм: время, random без seed, порядок hash table.
  • Храните сырые данные неизменяемыми (data lake). Моделирование делайте отдельным шагом. Тогда ошибку в трансформации можно исправить пересчётом.
  • Для зависимостей между jobs нужен workflow-планировщик (Airflow, Luigi). Метаданные о партиционировании и сортировке ведите в каталоге.
  1. Какие принципы Unix перешли в MapReduce? Какое главное ограничение Unix-инструментов?
  2. Чем sort-merge join отличается от broadcast hash join? Когда нужен каждый?
  3. Почему не стоит писать результат batch job в базу запись за записью?
  4. Что такое materialization и как dataflow engines избегают её недостатков? Чем платят за это при сбое?

Предыдущая: Согласованность и консенсус. Следующая: Потоковая обработка.