The MapReduce Paradigm | Oh My Kode

The MapReduce Paradigm

20 Nov 2021

21 minutes read

Suppose you must count how many times each word appears across a library of a billion books. One librarian would take a lifetime. But hand each of a thousand librarians a shelf, ask each to tally the words on their shelf, then have a second team add up the tallies word by word, and the job finishes in an afternoon. That, in one sentence, is MapReduce : split the work, do the same simple thing to every piece in parallel, then combine the pieces back together. Introduced by Dean and Ghemawat at Google in 2004 (Dean & Ghemawat, 2004), it became the paradigm that made “big data” tractable on clusters of ordinary machines. This post builds it from that librarian intuition all the way to its algebraic foundations (why it works comes down to a structure called a monoid) and its cost model (why the middle step, the shuffle, is the part that really hurts). We keep it readable for newcomers and rigorous for the initiated.

1. The problem : one machine is not enough

Some datasets are simply too large to fit on, or be processed by, a single computer in reasonable time. The instinctive fix is to use many machines, but distributed programming is notoriously hard : you must partition the data, ship it around, coordinate the workers, handle machines that crash mid-job, and reassemble the answer. MapReduce’s contribution was not a new algorithm ; it was a restriction. It asks you to express your computation using just two pure functions, map and reduce, and in exchange the framework handles all the distributed plumbing, partitioning, scheduling, data movement, fault tolerance, for free.

A MapReduce program is defined by two user functions. The map function is applied independently to every input record and emits intermediate key-value pairs. The reduce function is applied to each intermediate key together with the list of all values that were emitted for it, and produces the final output. Everything in between is the framework's job.

2. The two functions, and the hidden third one

Formally, if we write $[X]$ for “a list of $X$”, the two functions have these types :

\[\begin{equation} \texttt{map} : (K_1 \times V_1) \longrightarrow [\,K_2 \times V_2\,], \qquad \texttt{reduce} : (K_2 \times [\,V_2\,]) \longrightarrow [\,K_3 \times V_3\,]. \label{eq:types} \end{equation}\]

Read them slowly. map takes one input record, a key–value pair $(k_1, v_1)$, and emits a list of intermediate pairs (zero, one, or many). reduce takes one intermediate key $k_2$ and the whole list of values that were emitted under that key, and boils them down to the output.

Between the two sits a step you never write but that does the heavy lifting : the shuffle (or group-by-key) 1. It collects every intermediate pair produced by every mapper and regroups them so that all values sharing a key land together, ready for a single reducer.

\[\begin{equation} \text{shuffle}\Big(\bigcup_{(k_1,v_1)\in D}\texttt{map}(k_1,v_1)\Big) = \Big\{\, \big(k,\; [\,v : (k,v) \text{ was emitted}\,]\big) \,\Big\}. \label{eq:shuffle} \end{equation}\]

In plain words : mappers each shout out little (key, value) notes ; the shuffle sorts all the notes into bins, one bin per key ; each reducer empties one bin. The entire computation is the composition

\[\begin{equation} \texttt{MapReduce}(D) = \bigcup_{k \in K_2} \texttt{reduce}\big(k,\ \text{shuffle}(\dots)[k]\big). \label{eq:whole} \end{equation}\]

3. The canonical example : counting words

The “hello world” of MapReduce is word counting. The map function turns each word into the pair $(\text{word}, 1)$ ; the shuffle groups identical words ; the reduce sums the ones.

def map(doc_id, text):
    for word in text.split():
        emit(word, 1)                 # (k2, v2) = (word, 1)

def reduce(word, counts):             # counts = [1, 1, 1, ...]
    emit(word, sum(counts))           # (k3, v3) = (word, total)

The animation traces three tiny documents through the three stages : map emits ones, the shuffle gathers each word’s ones into a bin, and reduce adds them up.

input map → (word, 1) shuffle (group by key) reduce (sum) "the cat" "the dog" "cat dog" (the,1) (cat,1) (the,1) (dog,1) (cat,1) (dog,1) the → [1,1] cat → [1,1] dog → [1,1] the → 2 cat → 2 dog → 2
Figure 1 - The word-count dataflow. Each document is mapped to $(\text{word},1)$ pairs (blue) ; the shuffle regroups them into one bin per word (green) ; each reducer sums its bin into a final count (red). The mappers and reducers all run in parallel ; only the shuffle in the middle requires the machines to talk to each other.

4. The algebra underneath : why reduce needs a monoid

Here is the deep reason MapReduce parallelises so cleanly. A reducer folds a list of values into one result with some binary operation $\oplus$ (for word count, $\oplus$ is addition). For the framework to be free to split that list across machines, aggregate in any grouping, and merge partial results, the operation cannot be arbitrary, it must be associative. And because the shuffle delivers a key’s values in no guaranteed order, it should also be commutative. An operation that is associative with an identity element is exactly a monoid.

A monoid is a set $M$ with a binary operation $\oplus : M \times M \to M$ that is associative, $(a \oplus b) \oplus c = a \oplus (b \oplus c)$, and has an identity $e$ with $e \oplus a = a \oplus e = a$. If moreover $a \oplus b = b \oplus a$, it is a commutative monoid.
If $\oplus$ is associative, the fold $v_1 \oplus v_2 \oplus \cdots \oplus v_n$ has the same value under any parenthesisation. Hence the reducer may combine the values in any grouping, in particular as a balanced binary tree, without changing the result. If $\oplus$ is also commutative, the value is independent of the order of the $v_i$ as well.

This is not a technicality, it is the whole game. Associativity is what lets a billion additions be reorganised into a tree of depth $\log_2 n$ and evaluated in parallel : the work stays $O(n)$ but the span (critical-path length) collapses to $O(\log n)$.

Sequential fold — each ⊕ must wait for the previous result ⊕⊕⊕⊕ ⊕⊕⊕ v₁ v₂ v₃ v₄ v₅ v₆ v₇ v₈ span (critical path) = n − 1 = 7 steps, strictly one after another Associative regrouping — re-parenthesise into a balanced tree; each level runs at once v₁ v₂ v₃ v₄ v₅ v₆ v₇ v₈ ⊕ ⊕ ⊕ ⊕ ⊕ ⊕ Σ round 1 round 2 round 3 span = ⌈log₂ n⌉ = 3 rounds — the work (7 operations) is the same, the depth collapses
Figure 2 - Why associativity buys parallelism. Top : a naive fold is a chain, one accumulator crawling through all $n-1$ additions in sequence. Bottom : because $\oplus$ is associative we may re-parenthesise the very same $n-1$ operations into a balanced tree and run each level at once, finishing in $\lceil \log_2 n \rceil$ rounds instead of $n-1$. Same work, far shorter critical path, and this is exactly what makes combiners correct.
A combiner is a "mini-reduce" run on each mapper before the shuffle, to pre-aggregate its own output and cut the amount of data sent over the network. It is correct exactly when the reduction is an associative-commutative aggregation : summing $1{+}1{+}1{+}1$ on the map side to send a single "$4$" is valid because $+$ is a commutative monoid. But an operation like computing an average is not a monoid (averaging averages is wrong), which is why you must instead carry the monoidal pair $(\text{sum}, \text{count})$ and divide only at the very end.

5. The execution as a dataflow graph

Concretely, the input is chopped into $M$ splits, one per map task ; the intermediate space of keys is partitioned into $R$ pieces (usually by $\text{hash}(k) \bmod R$), one per reduce task. The physical dataflow is a graph : $M$ map nodes on the left, $R$ reduce nodes on the right, and, crucially, an all-to-all connection in the middle, every mapper may have data destined for every reducer.

M mappers shuffle : all-to-all R reducers M₁ M₂ M₃ R₁ R₂ R₃ hash≡0 hash≡1 hash≡2 M₁ → every reducer up to M × R data streams cross the network — this is the scalability bottleneck
Figure 3 - The execution graph. Map and reduce tasks are embarrassingly parallel, but the shuffle is an all-to-all exchange of up to $M \times R$ streams. Moving intermediate data across the network is the dominant cost of most real MapReduce jobs.

6. Fault tolerance, almost for free

On a cluster of thousands of commodity machines, failures are the norm, not the exception. MapReduce survives them with a strikingly simple idea that is only possible because map and reduce are pure, deterministic functions of their inputs : if a task fails, just run it again. A map task’s output depends only on its input split, so re-executing it on another machine reproduces exactly the same intermediate data. The master node monitors tasks and re-schedules any that die.

The same determinism defeats stragglers, the occasional slow machine that would otherwise hold up the whole job. Near the end, the framework launches speculative backup copies of the still-running tasks ; whichever finishes first wins, and the duplicate is killed. Purity is what makes this safe : running a task twice can never corrupt the result.

7. The cost model : replication versus reducer size

For the advanced reader, the interesting theory is quantitative : what does a MapReduce computation actually cost, and what are its limits ? Two parameters, due to Afrati and Ullman (Afrati et al., 2013), capture the fundamental tension.

The replication rate $r$ is the average number of intermediate $(k,v)$ pairs emitted per input record, that is, the total map-output size divided by the input size. It measures communication. The reducer size $q$ is the largest number of values sent to any single reducer. It measures per-reducer memory / load.

These pull in opposite directions. Make each reducer handle a bigger slice of the problem (large $q$) and you need to replicate the input less (small $r$), cheaper communication, but coarser parallelism and fatter reducers. Shrink the reducers (small $q$) for more parallelism and you must broadcast inputs to more of them (large $r$), more network traffic. For many problems one can prove a lower-bound tradeoff curve of the shape

\[\begin{equation} r \;\geq\; \frac{c\,|\text{output-dependency}|}{q} \qquad\text{(schematically)}, \label{eq:tradeoff} \end{equation}\]

so that $r$ and $q$ cannot both be small : you pay in communication for what you save in reducer load, and vice versa. This curve, not raw CPU, is what governs the practical scalability of a MapReduce algorithm.

Complementarily, Karloff, Suri and Vassilvitskii (Karloff et al., 2010) gave a clean complexity model (called $\mathsf{MRC}$) that treats a MapReduce job as a sequence of rounds, subject to sublinear constraints : each machine has memory $O(n^{1-\varepsilon})$, there are $O(n^{1-\varepsilon})$ machines, and a good algorithm finishes in $O(\log n)$ or even $O(1)$ rounds. In this model the scarce resource is the number of rounds, since each round pays a full, expensive shuffle. Minimising rounds is the central algorithmic challenge, and it is why iterative algorithms (graph traversal, machine learning) fit MapReduce so awkwardly, each iteration is another round, another shuffle to disk.

8. Life after MapReduce

That last observation, that every round writes intermediate data to disk and reads it back, is precisely why the classic MapReduce engine was eventually superseded for many workloads. Apache Spark (Zaharia et al., 2012) kept the paradigm, transformations expressed as map/reduce-style operators over partitioned data, but kept intermediate results in memory and modelled the whole job as a lazy DAG of operations, so a ten-round iterative algorithm no longer pays ten trips to disk. The programming model you write still looks like map and reduce ; what changed is the execution engine beneath it.

So MapReduce, the specific Google system, has faded, but MapReduce, the idea, is everywhere : in Spark, in Flink, in the map/reduce/groupByKey of every data framework, and in the humble parallel reduce of your favourite language, all of them resting on the same algebraic bedrock, an associative operation you are allowed to reparenthesise at will.

9. Conclusion

MapReduce earns its fame by trading generality for tractability : accept the discipline of expressing your job as a map and a reduce, and a cluster’s worth of hard problems, partitioning, scheduling, data shuffling, machine failures, stragglers, dissolve into the framework. Underneath the engineering sits a small, beautiful piece of algebra, the reducer is a fold over a commutative monoid, and it is associativity that licenses every parallel regrouping, every combiner, every re-execution. The costs, too, are governed by clean mathematics : the replication-rate / reducer-size tradeoff and the number of shuffle rounds, not CPU cycles, are what bound what a cluster can do. Learn to see your computation as “map each piece, then combine associatively”, and a surprising fraction of large-scale data processing suddenly has a shape.

References

  1. Afrati, F. N., Das Sarma, A., Salihoglu, S., & Ullman, J. D. (2013). Upper and Lower Bounds on the Cost of a Map-Reduce Computation. Proceedings of the VLDB Endowment, 6(4), 277–288.
    @article{Afrati2013,
      author = {Afrati, Foto N. and Das Sarma, Anish and Salihoglu, Semih and Ullman, Jeffrey D.},
      title = {Upper and Lower Bounds on the Cost of a Map-Reduce Computation},
      journal = {Proceedings of the VLDB Endowment},
      volume = {6},
      number = {4},
      pages = {277--288},
      year = {2013}
    }
    
  2. Dean, J., & Ghemawat, S. (2004). MapReduce: Simplified Data Processing on Large Clusters. 6th Symposium on Operating Systems Design and Implementation (OSDI), 137–150.
    @inproceedings{Dean2004,
      author = {Dean, Jeffrey and Ghemawat, Sanjay},
      title = {MapReduce: Simplified Data Processing on Large Clusters},
      booktitle = {6th Symposium on Operating Systems Design and Implementation (OSDI)},
      pages = {137--150},
      year = {2004}
    }
    
  3. Karloff, H., Suri, S., & Vassilvitskii, S. (2010). A Model of Computation for MapReduce. Proceedings of the 21st Annual ACM-SIAM Symposium on Discrete Algorithms (SODA), 938–948.
    @inproceedings{Karloff2010,
      author = {Karloff, Howard and Suri, Siddharth and Vassilvitskii, Sergei},
      title = {A Model of Computation for MapReduce},
      booktitle = {Proceedings of the 21st Annual ACM-SIAM Symposium on Discrete Algorithms (SODA)},
      pages = {938--948},
      year = {2010}
    }
    
  4. Zaharia, M., Chowdhury, M., Das, T., Dave, A., Ma, J., McCauley, M., Franklin, M. J., Shenker, S., & Stoica, I. (2012). Resilient Distributed Datasets: A Fault-Tolerant Abstraction for In-Memory Cluster Computing. 9th USENIX Symposium on Networked Systems Design and Implementation (NSDI), 15–28.
    @inproceedings{Zaharia2012,
      author = {Zaharia, Matei and Chowdhury, Mosharaf and Das, Tathagata and Dave, Ankur and Ma, Justin and McCauley, Murphy and Franklin, Michael J. and Shenker, Scott and Stoica, Ion},
      title = {Resilient Distributed Datasets: A Fault-Tolerant Abstraction for In-Memory Cluster Computing},
      booktitle = {9th USENIX Symposium on Networked Systems Design and Implementation (NSDI)},
      pages = {15--28},
      year = {2012}
    }
    
  1. A subtle but important point : the shuffle both groups by key and, in most implementations, sorts the keys. The sort is not required by the abstract model $\eqref{eq:whole}$, but it makes the group-by streamable on machines whose intermediate data does not fit in memory, another place where an algebraic property (a total order on keys) is quietly doing systems work. ↩

who am i

Hi! I am a Data Scientist by profession, an Emacs devotee and an untalented bassist. I intend to use this space for writing about things that I think I have understood well in the hope that they may be helpful to others, including my future self.

what is this

OhMyKode is an opportunity to share knowledge about mathematics, computer science, machine learning and algorithmic beauty, which allows us to improve our skills and learn in depth. It is a sharing place to learn the how and the why.

© MMXVIII - MMXXVI by Maâmra Youcef - معامره يوسف
Content available under Creative Commons (BY-NC-SA) unless otherwise noted.
This site is hosted at Github Pages and powered by Jekyll & Papyrus.
“We can't skip Math forever !”