DEV Community

Aaroophan Varatharajan
Aaroophan Varatharajan

Posted on Originally published at aaroophan.Medium on

The Problem With “Just Parallelize It”: Rethinking Distributed Computing #11

The computation was easy to split. Moving the data, regrouping the results, recovering failed workers, and deciding who should do what was the part hiding behind “just.”

MapReduce looks simple: split the work, process it in parallel, and combine the results. But distributed computing gets complicated when data must move, shuffle, recover from failures, and stay balanced.
"MapReduce looks simple: split the work, process it in parallel, and combine the results. But distributed computing gets complicated when data must move, shuffle, recover from failures, and stay balanced."

Suppose we have a large collection of documents and want to count how many times each word appears.

On one machine, the idea is almost insultingly ✨simple✨

Read a document.

For every word, increment a counter.

hello → 1
world → 1
hello → 2
Enter fullscreen mode Exit fullscreen mode

Now imagine the collection is enormous.

No problem.

We have many machines.

Split the documents across them.

Machine A counts one subset.

Machine B counts another.

Machine C counts another.

They can work mostly independently.

This is what gets called an embarrassingly parallel problem or if we are feeling kinder to the problem, a delightfully parallel one.

There is little communication required while each worker processes its own input.

So the solution feels obvious:

Just parallelize it.

That sentence is doing an impressive amount of unpaid labour.

Because splitting the computation was never the hardest part.

The difficult part begins when all those independently computed answers have to become one answer.


First, Let Every Worker Count Locally

Give Worker A:

hello world
hello map
Enter fullscreen mode Exit fullscreen mode

Give Worker B:

world reduce
hello
Enter fullscreen mode Exit fullscreen mode

Each worker can process its input independently.

Worker A produces:

hello → 2
world → 1
map → 1
Enter fullscreen mode Exit fullscreen mode

Worker B produces:

world → 1
reduce → 1
hello → 1
Enter fullscreen mode Exit fullscreen mode

So far, the parallelism is lovely.

Nobody needs to coordinate while reading the documents.

Nobody needs a shared counter.

Nobody waits for somebody else’s loop to finish.

We took one big input and turned it into several smaller independent inputs.

Exactly what we wanted.

Then someone asks:

How many times did hello appear overall ?

Worker A knows 2.

Worker B knows 1.

Neither worker knows the global answer.

The work was parallel.

The result is not finished until related pieces meet again.

That is the first hidden constraint:

Independent computation eventually creates dependent results.

We split the data apart.

Now some of it has to come back together.


Fine. Send Everything to One Machine

The simplest answer is obvious.

Let every worker send its counters to one coordinator.

The coordinator combines them.

For two workers, this is wonderfully reasonable.

For thousands of workers processing enormous datasets, it starts feeling less charming.

Now we need to worry about:

  • How workers communicate,
  • Where intermediate results are stored,
  • How the coordinator finds them,
  • What happens when a worker fails,
  • How workers are initialized,
  • How work is distributed,
  • Whether some workers finish much earlier than others,
  • How a single reducer avoids becoming the next bottleneck.

The computation: count(word)

was never complicated.

The distributed execution around it is.

This is exactly the gap MapReduce was designed to hide. Dean and Ghemawat describe the runtime as taking responsibility for partitioning input, scheduling work, handling machine failures, and managing inter-machine communication while the programmer supplies the application-specific computation.

The seductive wrong path was not foolish.

“Split the data and combine the answers” is correct.

It just leaves out nearly everything required to make that sentence work across a cluster.


So Describe the Computation, Not the Machinery

MapReduce makes a surprisingly aggressive proposal.

Instead of asking the programmer to orchestrate all those workers, ask for two functions.

A Map function: Y = map(x)

and a Reduce function: Z = reduce(List)

The idea comes from higher-order functional programming, where functions themselves can be treated as inputs to a larger computation.

MapReduce takes that style and turns it into a distributed execution model.

The programmer describes:

What should happen to each independent input?

and:

How should related outputs be combined?

The framework worries about making those functions run across the machines.

Google’s original description is deliberately narrow: the user supplies a map function that produces intermediate key/value pairs and a reduce function that merges values associated with the same key; the runtime handles the distributed execution around them.

This is the crucial abstraction shift.

We stop writing:

Send partition 17 to machine 6, wait for it, retry if necessary, transfer its output to machine 23…

and start writing:

For every input, do this.

For every group of matching outputs, do this.

That sounds almost suspiciously easier.

The trick is in how the framework knows which outputs belong together.


The Key Is the Rendezvous Point

For word counting, the Map function does not produce one final counter.

It emits key/value pairs.

Map(docId, text):
  for each word w in text:
        emit(w, 1)
Enter fullscreen mode Exit fullscreen mode

For: hello world hello

The mapper emits:

(hello, 1)
(world, 1)
(hello, 1)
Enter fullscreen mode Exit fullscreen mode

Another mapper somewhere else may emit:

(world, 1)
(reduce, 1)
(hello, 1)
Enter fullscreen mode Exit fullscreen mode

Notice what the programmer did not specify.

We did not say which reducer should receive hello.

We did not manually send all hello values to one machine.

We simply declared:

These values belong to the key hello .

The key becomes the meeting point.

Hadoop’s Mapper behaves in this style: each input pair can produce zero or more intermediate key/value pairs, after which all intermediate values associated with the same key are grouped for a reducer.

This is elegant because the mapper can stay embarrassingly parallel.

Each one emits local facts.

The framework later decides how those facts find their relatives.

And that “later” is where most of the distributed communication lives.


The Shuffle Is Where Parallelism Has to Talk

At first, all map tasks can work independently.

Then the framework gathers their intermediate results.

It sorts them by key.

It groups values belonging to the same key.

So scattered outputs like:

Mapper A: (hello, 1)
Mapper B: (world, 1)
Mapper C: (hello, 1)
Mapper D: (hello, 1)
Enter fullscreen mode Exit fullscreen mode

Have to become:

hello → [1, 1, 1]
world → [1]
Enter fullscreen mode Exit fullscreen mode

This movement and regrouping is the shuffle.

And this is where the phrase “embarrassingly parallel” develops conditions.

The map phase was happy because each input could be processed independently.

The reduce phase cannot begin meaningfully until values sharing a key have been brought together.

Apache Hadoop describes the reducer’s input as the grouped output of the mappers; during shuffle, each reducer fetches its relevant partitions from mapper outputs, while sorting and grouping arrange matching keys together.

The data we worked so hard to split apart now has to cross the cluster according to a new relationship.

Not:

Which input file did this come from?

but:

Which key does this result belong to?

That is the hidden cost of decomposition.

Parallelism gave us independence.

The shuffle has to restore the dependencies that matter.


Reduce Finally Produces the Answer We Actually Wanted

Once all values for a key are together, the Reduce function can be beautifully boring.

Reduce(word, values):
    sum = 0
    for each value in values:
        sum += value
    emit(word, sum)
Enter fullscreen mode Exit fullscreen mode

For: hello --> [1, 1, 1]

We get: hello --> 3

Now the architecture makes sense as a progression:

Input
  ↓
Map
  ↓
Intermediate (key, value) pairs
  ↓
Group by key
  ↓
Reduce
  ↓
Final (key, value) pairs
Enter fullscreen mode Exit fullscreen mode

Hadoop follows essentially this data model: map tasks transform input records into intermediate key/value pairs, the framework groups values with the same key, and the reducer receives each key with its associated collection of values.

The interesting part is not that map and reduce are clever functions.

They are often extremely simple.

The cleverness is that the framework knows how to take those simple functions and turn them into distributed computation.


Hadoop Takes the Annoying Questions Personally

Now return to our manual implementation.

Before MapReduce, we had questions.

  • Who divides the input?
  • Who starts the workers?
  • Which worker gets which piece?
  • How does intermediate data move?
  • Who notices that Worker 37 died?
  • Who gives its work to somebody else?
  • Who knows when all the map tasks are finished?
  • Who groups the keys?
  • Who starts the reducers?
  • Who writes the final result?

MapReduce’s answer is:

The framework.

Apache Hadoop describes MapReduce as a framework for processing large datasets in parallel across clusters while scheduling tasks, monitoring them, and re-executing failed work when necessary. Applications primarily provide the map and reduce logic plus job configuration.

That is why the abstraction matters.

It does not somehow eliminate communication.

It owns the communication.

It does not eliminate failures.

It handles failed tasks.

It does not make scheduling unnecessary.

It moves scheduling out of application code.

The complexity is still present.

It has changed owners.

So The Origami Software Engineer moved the complexity into the framework, where a good abstraction belongs.


Failure Gets Easier When Work Has Clear Boundaries

Suppose one mapper fails halfway through its input.

Without a framework, the application needs to decide:

  • What work that machine had,
  • What completed,
  • What output survived,
  • Where to restart,
  • How to avoid corrupting the final result.

MapReduce already thinks in tasks.

A map task operates on a portion of the input.

A reduce task operates on grouped intermediate data.

Those boundaries make recovery more manageable.

If a worker fails, the framework can arrange to execute unfinished work elsewhere rather than expecting the application author to implement a custom recovery protocol for every data-processing job.

The original MapReduce runtime was explicitly designed to mask machine failures by monitoring workers and re-executing work when required.

This is one of those places where the programming model and the failure model fit each other unusually well.

The framework knows what the unit of work is.

Therefore it knows what can be tried again.

Failure handling was not bolted on afterward.

The task structure gives failure recovery something concrete to operate on.


More Tasks Give Us More Parallelism and More Overhead

Now suppose the dataset is large.

How many map tasks should we create?

  • Ten?
  • A thousand?
  • A million?

Smaller tasks give the framework more pieces to distribute across available workers.

That can improve parallelism and load balancing.

If one machine is slower, there are more opportunities for other machines to consume remaining work.

But finer granularity also means:

  • More task initialization,
  • More scheduling,
  • More metadata,
  • More intermediate outputs,
  • More communication. (Potentially)

So:

finer grain
    ↓
more parallelism
    ↓
more coordination and communication overhead
Enter fullscreen mode Exit fullscreen mode

Make tasks too large and we may leave parallel capacity unused.

Make them too small and the framework spends increasing effort managing the parallelism instead of doing useful work.

That trade-off is easy to miss when the advice is merely:

Use more machines.

Machines are not the abstraction boundary.

Tasks are.

The framework can balance those tasks, but the application still has to choose a granularity compatible with the work.

Parallelism is not free simply because it is available.


Then the Intermediate Data Starts Looking Ridiculous

Return to word count.

Suppose one mapper processes a huge document containing the word the ten thousand times.

Naively, it emits:

(the, 1)
(the, 1)
(the, 1)
...
Enter fullscreen mode Exit fullscreen mode

10,000 times.

All those values may eventually need to travel toward a reducer.

This works.

It is also a spectacular way to use the network to transport information we could have summarized locally.

The mapper already knows that, within its own output: the --> 10000

So MapReduce can introduce a combiner.

A combiner performs local aggregation before intermediate data is sent onward.

Instead of shipping ten thousand identical contributions: (the, 1) x 10000

we may be able to send something closer to: (the, 10000)

The final reducer still combines contributions from different map tasks.

But far less intermediate data may need to cross the network.

Hadoop supports combiners specifically as local aggregation over mapper output to reduce the amount of data transferred from mappers to reducers.

This optimization exists because the shuffle made another constraint visible:

Computation is often cheaper than communication.

Once the network becomes the expensive boundary, doing a little extra work locally can save a lot of distributed movement.


Word Count Was Only the Easy Example

The elegance of MapReduce is that the same pattern extends beyond counting words.

For a mean , mappers emit values associated with a key and reducers combine the values belonging to that key.

For an inverted index , mappers can emit: (word, documentsId) and reducers gather: word --> [document1, document7, document42] turning: document --> terms into: terms --> documents

For distributed grep , map tasks independently search separate pieces of a dataset and emit matches that can later be collected.

Sorting can also be expressed through the framework’s key-based partitioning and ordering.

The point is not that every algorithm on Earth should be forced through map and reduce.

The point is that a surprisingly large family of data-processing problems share the same structure:

  1. Do independent work over partitions;
  2. Emit intermediate relationships;
  3. Regroup related results;
  4. Combine them.

Once that common pattern is visible, building the distributed machinery once starts making much more sense than rebuilding it separately for every application.


The Framework Is Doing the Distributed Systems So You Don’t Have To

This is the part worth keeping.

MapReduce did not make distributed computing easy by discovering that distributed computing was secretly simple.

It made one useful class of distributed problems manageable by constraining the programming model.

You give it functions shaped like:

map

reduce

You express intermediate results as: (key, value)

You accept that grouping by key defines where distributed work must reconverge.

In exchange, the framework can understand the structure of your computation well enough to manage:

  • Input partitioning,
  • Parallel execution,
  • Communication,
  • Grouping,
  • Scheduling,
  • Load balancing,
  • Failed tasks,
  • Final output.

That bargain is why the abstraction works.

Google’s original MapReduce description emphasizes exactly this trade: programmers express computation through map and reduce while the runtime automatically parallelizes the job and handles partitioning, scheduling, machine failures, and inter-machine communication.

The framework can do so much because we agreed to describe the computation in a form it understands.

Abstraction came with constraints.

The constraints bought automation.


“Just Parallelize It” Was Never the Architecture

At the beginning, word count looked trivial.

Split the documents.

Count independently.

Add the results.

And technically, that was the whole algorithm.

But a distributed system still had to answer everything the algorithm ignored.

  • Where does each input go?
  • Who executes it?
  • How do intermediate outputs move?
  • How do all values for one key find each other?
  • What happens when one machine is slower?
  • What happens when one machine disappears?
  • How much work should each task contain?
  • When does additional parallelism become communication overhead?
  • Can intermediate data be reduced before it crosses the network?
  • Who knows when the job is finished?

None of those questions changes the mathematics of word count.

They change whether word count can run reliably across a large cluster.

That is why MapReduce’s most important contribution is not really map().

It is not really reduce() either.

It is the boundary around them.

The programmer owns the problem-specific transformation.

The framework owns much of the distributed execution required to make that transformation happen at scale.

We started with:

Just parallelize it.

Then we discovered that parallel computation was the easy part.

The hard part was everything required to make thousands of independent pieces of work behave like one computation.

So we stopped rewriting that machinery for every problem and built an abstraction that could own it.

That’s not failure.

That’s evolution.


The “I liked this” Starter Pack:

Don’t let your fingers get lazy now.

  • Like : It tells me this was worth writing.
  • A Comment: Tell me your thoughts, your favorite snack, or a better title for this blog.
  • Boost it: Especially with that one developer who definitely needs this.

Thanks for being here. It genuinely helps more than you know!

— Aaroophan Varatharajan

Find me elsewhere:

Top comments (0)