Batch processing
Unix tools: one pipeline, six small programs
unix-philosophyuniform-interfacesort-vs-in-memory-aggregationThe pipeline
click a stage to inspect · tick to enableawk '{print $7}' — Split each line on whitespace and print only the 7th field: the requested path.
stdin · 30 lines
stdout · 30 lines
Try: untick sort. uniq -c only merges adjacent duplicates, so one URL comes out as several partial counts and the top 5 is wrong. Each program does one thing and knows nothing about the others; stdin/stdout lines are the only interface they share.
The input file in HDFS is cut into M blocks. The scheduler starts one map task per block, ideally on a machine that already stores that block.
Map tasks
Shuffle (network)
Nothing on the wire yet: reducers start fetching once map output is sorted and written.
Every mapper sends to every reducer: M × R = 6 transfers. Numbers = records per transfer.
Reduce tasks
Try: step to “Partition & sort” and change R; every key is always routed to the same reducer, because its partition is a pure function of the key. Then turn the combiner on and watch the shuffle shrink: mappers pre-sum their own counts before sending.
Joins in batch: bring related records together
sort-merge-joinsecondary-sortjoin-skewbroadcast-hash-joinpartitioned-hash-joinhigh-level-batch-apisReduce-side sort-merge join. Two sets of mappers read profiles and events and both emit user_id as the key. The shuffle brings every record for a user to the same reducer; a secondary sort puts the profile first, then the events in time order. The reducer keeps just one profile in memory and streams the events past it.
reducer 1
4 joined rowsreducer 2
7 joined rowsreducer 3
7 joined rowsTry: make u104 a celebrity under sort-merge; one reducer gets all its events and the whole job waits for it. Switch on the skewed join (as in Pig or Hive): the hot key's events are scattered across reducers and its profile is copied to each. Then try broadcast: no shuffle and no skew, but only because every mapper can hold the whole small table.
The output of a batch workflow
batch-output-immutabilityOutput directories (immutable)
The job writes a complete new file set (like building an SSTable or Lucene index) next to the old one. Readers switch only when it is done.
What the application reads right now
Try: with the buggy code, run the job in both modes. Building new files leaves production untouched until the switch, and Roll back repoints readers to yesterday's output: the inputs are immutable, so a bad deploy costs a re-run, not lost data. Writing straight into the database exposes half-done runs and leaves nothing to roll back to.
Beyond MapReduce: materialize or pipeline?
workflow-schedulerintermediate-state-materializationdataflow-enginelineage-recomputationMapReduce workflow: materialize every stage to HDFS
Dataflow engine (Spark, Flink, Tez): pipeline operators
Try: kill a task in the last stage. MapReduce reruns just that task, because its input sits durably in HDFS; the dataflow engine has kept nothing, so it recomputes the lost partition's whole lineage from the original input. Add a checkpoint after stage 2 and the recompute chain gets shorter, but the checkpoint write itself sits on the critical path: a trade-off that pays off for long or expensive lineages. Either way, skipping materialization and the barrier between stages wins by a wide margin. (A toy cost model: 4 tasks per stage, one straggler each.)
Graphs and iteration: Pregel supersteps
pregel-bspSuperstep 0
Try: step through shortest paths from A. In each superstep a vertex reads the messages sent to it in the previous one, keeps the best distance, and sends candidate distances along its edges; vertices with nothing new vote to halt and sleep until a message arrives. The job ends when every vertex is halted and no message is in flight. Vertices live on different workers, so most messages cross the network, which is why Pregel jobs are often slower than a single machine for small graphs.
- takeawayBatch processing is Unix pipes at scale. Small programs share one interface (files of records), sorting does the grouping, and MapReduce adds partitioning by key and the shuffle that brings every key to one reducer.
- takeawayMost of the cost is moving data. Sort-merge joins work on anything but shuffle everything. Broadcast and partitioned hash joins skip the shuffle when the inputs allow it. Hot keys make the slowest worker set the job time.
- takeawayImmutable input plus fresh output makes jobs safe to retry, rerun and roll back. Dataflow engines drop the materialization between stages for speed and recover by recomputing from lineage instead.
Key concepts · 18
book chapter 10- Unix philosophy
Small programs that each do one thing well, composed with pipes into larger jobs.
Because every tool reads and writes the same thing, tools written decades apart compose freely, and you can inspect any intermediate stage by cutting the pipe. MapReduce is the same idea spread over many machines.
awksortuniqxargs↑ see it in the visualization - Uniform interface
Every program takes stdin and writes stdout as a stream of newline-separated records; MapReduce uses files in a distributed filesystem the same way.
A shared interface is what makes composition possible. It also separates logic from wiring: a program does not care where its input came from or where its output goes.
↑ see it in the visualization - Sorting vs in-memory aggregation
Counting by sorting so equal keys are adjacent, as opposed to holding a hash table of counts in memory.
A hash table is fast while the distinct keys fit in RAM. Sorting can spill to disk and merge sorted runs, so it scales past memory, which is why MapReduce is built around sorting.
↑ see it in the visualization - Distributed filesystem (HDFS)
A shared-nothing filesystem: a daemon on each machine stores blocks, a central NameNode tracks where they are, and blocks are replicated.
It is the stdin/stdout of MapReduce. Jobs read input and write output as files in it, and the scheduler runs each map task on a machine that already holds that block (data locality).
HDFSGFSAmazon S3↑ see it in the visualization - MapReduce
A programming model and framework: a user-defined mapper extracts key-value pairs from each record, the framework sorts and groups them by key, and a reducer processes all the values for each key.
You write two callbacks that each see a small piece of the data, and the framework handles parallelism, moving data between machines and retrying failures. Each job is a single map-sort-reduce, so real work is chained into workflows.
Hadoop MapReduceGoogle MapReduceCouchDB views↑ see it in the visualization - Shuffle
Partitioning mapper output by a hash of the key, sorting it, and copying each partition from every mapper to its reducer.
It guarantees every value for a key reaches the same reducer, and it is the most expensive part of most jobs: an all-to-all copy over the network. Combiners shrink it by pre-aggregating on the mapper.
↑ see it in the visualization - Reduce-side sort-merge join
Mappers emit the join key from both inputs, the shuffle brings all records with that key to one reducer, and the reducer joins them.
It needs no assumptions about the inputs, and each reducer only holds one key's records at a time. The price is sorting, copying and merging every input record.
HivePig↑ see it in the visualization - Secondary sort
Arranging reducer input so that, within one key, records arrive in a chosen order, e.g. the profile record first and then events by timestamp.
With the record you join against arriving first, the reducer can stream the rest past it with constant memory instead of buffering them.
↑ see it in the visualization - Skew and hot keys in joins
A few keys (a celebrity's user id) have far more records than the rest, so one reducer gets much more work.
The job finishes only when the slowest reducer does. Skewed joins sample for hot keys, spread their records across several reducers, and copy the other side of the join to each of them.
Pig skewed joinHive skew joinCrunch sharded join↑ see it in the visualization - Broadcast hash join
A map-side join: the small input is loaded entirely into an in-memory hash table in every mapper, which then streams its block of the large input and looks keys up.
No reducers, no sort and no shuffle, so it is much faster. It only works when one side is small enough to fit in each mapper's memory.
Pig replicated joinHive MapJoinSpark broadcast join↑ see it in the visualization - Partitioned hash join
A map-side join for two inputs already partitioned the same way on the join key: mapper i joins partition i of each input.
Each mapper only needs one partition of the smaller side in memory. It relies on how the inputs were written earlier, so it needs the partitioning recorded as metadata (Hive bucketed map joins).
↑ see it in the visualization - Immutable batch output
Batch jobs write fresh files (search indexes, key-value database files) rather than mutating a live database; readers switch to the new output atomically.
Bad code costs a re-run or a rollback, not corrupted data (human fault tolerance). Building files in bulk is also far faster than one network write per record, and it doesn't overload production databases.
Lucene index segmentsVoldemortHBase bulk loadElephantDB↑ see it in the visualization - Workflow
A chain of jobs where each job's output directory is the next one's input, managed by a scheduler that runs them in dependency order.
Most real analyses take dozens of MapReduce jobs. Since each output is a file, any stage can be rerun or inspected on its own.
OozieAzkabanLuigiAirflow↑ see it in the visualization - Materialization of intermediate state
Writing each job's full output to the distributed filesystem, replicated, before the next job may start.
It makes fault tolerance simple (just rerun the failed task), but every intermediate dataset is written and replicated, and a stage cannot start until the slowest task of the previous one is done.
↑ see it in the visualization - Dataflow engine
An engine that runs a whole workflow as one job: a graph of operators that hand data to each other directly, sorting only where needed.
Operators can start as soon as input arrives, intermediate data stays in memory or on local disk, and there is no repeated task start-up. That makes it much faster than a chain of MapReduce jobs.
SparkFlinkTez↑ see it in the visualization - Recomputation from lineage
Fault tolerance without materialization: lost data is rebuilt by rerunning the operators that produced it, starting from input that is still available.
Avoids writing intermediate state at all, but a failure late in a long pipeline can mean a lot of recomputation, and operators must be deterministic so the rebuilt data matches what was lost. Checkpoints bound the cost.
Spark RDD lineageFlink checkpoints↑ see it in the visualization - Pregel / bulk synchronous parallel
A graph processing model: in each superstep every vertex processes messages sent to it, updates its state, and sends messages along its edges.
Iterative graph algorithms like PageRank don't fit one MapReduce pass. Pregel keeps vertex state in memory between iterations, and a checkpoint after each superstep gives fault tolerance.
PregelApache GiraphSpark GraphXFlink Gelly↑ see it in the visualization - High-level APIs and declarative queries
Relational-style operators (join, filter, group by) and SQL on top of the engines, instead of hand-written map and reduce functions.
Less code, and a query optimizer can choose the join algorithm (broadcast vs sort-merge) and push filters down. Unlike an MPP database, it still lets you run arbitrary code over arbitrary file formats.
HivePigSpark SQLFlink Table APICascading↑ see it in the visualization