Jon Avezbaki
AvenueJ

Designing Data-Intensive Applications

System Design Fundamentals & Notes

Chapter 0: What Is This? How Was This Written?

This page is meant to act as a summary of notes I took while reading Designing Data-Intensive Applications by Martin Kleppmann. Specifically, the first edition.

None of the text on this page was AI generated.

I read the book, annotated as I went along, and wrote down notes in a Google Doc. This page is the web-friendly version of that Google Doc, created through much formatting and effort.

Much of the text here is either directly copied from Kleppmann’s own writing, my own summary/rewording, or some combination of the two. I also pull in information from other sources occasionally, but I try not to stray too far from Kleppmann as my source of truth. In the cases where I directly pilfer Kleppmann’s own writing, my aim is to provide a substantially different reading experience here. Similar ideas and concepts are usually formatted for easier comparison through tables or other visualizations. This formatting is directly from my own notes. No AI told me what to put in a table and where.

You will ocassionally find empty table cells in this guide. This is deliberate. If I am not 110% sure as to the answer the cell should describe, or if Kleppmann’s own writing on the subject seems ambigous, I defer to leaving that area empty.

This page is not meant to act as a replacement for reading the book itself, especially since a second edition is out now. Think of this more like a review/cheat sheet. Cheers!

Chapter 1: Foundations of Systems

Fan-outThe number of requests to other services that we need to make in order to serve one incoming request. Think of delivering a single tweet to 30M followers.
ThroughputThe number of records we can process per second, or the total time it takes to run a job on a dataset of a certain size.
Vertical scaling /Scaling upMoving to a more powerful machine
Horizontal scaling /scaling out /Shared-nothing architectureDistributing load across multiple machines
ElasticSystems that can automatically add computing resources when they detect a load increase

Response Time

Response timeThe time between a client sending a request and receiving a response. This is important in online systems. This should be thought of not as a single number, but as a distribution of values.

Measuring Response Time

The distribution of values should be thought of in percentiles. For example, if the 95th percentile response time is 1.5 seconds, that means 95 out of 100 requests take less than 1.5 seconds. High percentiles of response times, also known as tail latencies, are important because they directly affect a user’s experience of the service. For example, Amazon describes response time requirements for internal services in terms of the 99.9th percentile, even though it only affects 1 in 1,000 requests.

If an end-user request requires multiple back-end calls, a higher proportion of end-user requests end up being slow (an effect known as tail latency amplification).

On the other hand, optimizing the 99.99th percentile (the slowest 1 in 10,000 requests) was deemed too expensive and to not yield enough benefit for Amazon’s purposes. Reducing response times at very high percentiles is difficult because they are easily affected by random events outside of your control, and the benefits are diminishing.

Testing Response Time

It is important to measure response times on the client side. When testing, the load generating client needs to keep sending requests independently of the response time. If the client waits for the previous request to complete before sending the next one, that behavior has the effect of artificially keeping the queues shorter in the test than they would be in reality, which skews the measurements.

Chapter 2: Data Models and Query Languages

SQL Vs NoSQL

Declarative vs Imperative

Declarative Imperative
function getSharks() {
    var sharks = [];
    for (var i = 0; i < animals.length; i++) {
        if (animals[i].family === "Sharks")
            sharks.push(animals[i]);
    }
    return sharks;
}
SELECT *
FROM animals
WHERE family = 'Sharks'
Many commonly used programming languages are declarative SQL and CSS are examples of imperative languages
Declarative code is very hard to parallelize across multiple cores and multiple machines, because it specifies instructions that must be performed in a particular order. Specifies only the pattern of results, not the algorithm that is used to determine the results, so the database is free to use a parallel implementation of the query language. Performance improvements can also be introduced without requiring any changes to queries.

Document-Oriented Databases

Impedance mismatchDescribes the awkwardness of needing a translation layer between data that is stored in relational tables into objects used in application code

The document in “document-oriented” can refer to JSON, XML, YAML, and BSON. Databases include MongoDB, RethinkDB, CouchDB, and Espresso.

A foreign key in a relational model is analogous to a document reference in a document model.

Document-oriented databases are good when you need large parts of the document at the same time. This avoids needing multiple lookups across different parts of the database. They target use cases where data comes in self-contained documents and relationships between one document and another are rare.

Graph-Like Data Models

Graphs are good for evolvability: as you add features to your application, a graph can easily be extended to accommodate changes in your application’s data structures. Queries to graph databases are partially characterized by having mechanisms for an unknown number of joins, since you don’t know how many nodes and vertices you need to traverse to retrieve your data. No schema is technically enforced.

Chapter 3: Storage and Retrieval

Indexes

In order to efficiently find the value for a particular key in the database, we need to use an index. The general idea behind them is to keep some additional metadata on the side, which acts as a signpost and helps you locate the data you want. Any kind of index usually slows down writes, because the index also needs to be updated every time the data is written. However, well-chosen indexes speed up reads.

Hash Index

One kind of index is a hash index. Throw the key into a hash function and that’s your index!

  • The hash table must fit in memory. You could theoretically maintain a hashmap on disk, but it’s very difficult for it to perform well due to random access I/O. This is bad for durability.
  • Range queries are not efficient. You have to look up each key individually.
  • O(1) reads and writes in memory

B-Tree Index

A self-balancing tree on disk.

31 11 19 38 45 1 2 8 10 11 13 15 19 26 31 35 38 42 45 51 60 61
Write-ahead-log (WAL)Append-only data file. Before any update is applied to the database, it is added to this file. Used by many databases internally. Allows for undo/redo functionality, as well as crash recovery.
  • No biggie if the hardware dies while we’re updating the B-Tree, that’s why we typically use a WAL with B-Trees
  • Since it’s stored on disk, it’s not as fast as say, a hash index that is stored on memory
  • Relatively good for reads since you only need O(logn) for key retrieval, since that’s the height of the tree
  • Writes can be slower since it’s on disk
  • Big datasets are way more feasible since data is stored on disk, and has more space
  • Range queries are easy to do, since keys are sorted and co-located

Log-Structured Merge-Tree (LSM Tree)

An LSM Tree is a 2-part mechanism for storing indexes: the memtable and the SSTables.

Read Request Memory Memtable Disk SSTable SSTable SSTable SSTable

Memtable

The first part is the tree, which is a balanced tree (red-black tree, AVL tree, many options). This tree is sometimes known as a memtable. As the name implies, it is stored in memory.

When the memtable gets bigger than some threshold (typically a few MB), it’s written out to disk as a Sorted String Table.

Sorted String Table (SSTable)

A sorted list of key-value pairs flushed out from the memtable. This is easy to put in sorted order because the source memtable is a balanced tree.

Merging and Compaction

SSTables are append only. You will eventually get to a state where you have multiple SSTables, with duplicate values in different (or even the same) data segments. A background process will merge and compact these SSTables into a single one at regular intervals. This process throws away duplicate keys, keeping only the most recent version of each key. If a key is deleted, a special value known as a tombstone is inserted to represent its deletion. If the last known value for a key is a tombstone, all of its older values are purged in the merge and compaction process.

Key Lookup

To look up a key, you check the memtable first, then all the SSTables. This is costly, especially if the key does not exist. For this reason bloom filters are commonly used to speed up this process. Through a combination of hash functions and manipulation of a bit string, bloom filters can tell you if a key does not exist in a database, and thus saves many unnecessary disk reads for nonexistent keys.

  • Supports range queries, but slow compared to B-Trees, since all SSTables need to be checked
  • Extra CPU usage for compaction
  • Number of keys not limited to memory

B-Trees vs LSM Trees

B-TreeLSM Tree
Breaks the database down intofixed sized blocks or pages, typically 4KB or more in sizevariable sized segments, typically several megabytes or more in size
The write operation is performed asan overwrite on a page on disk with new dataan append operation to files, but never modify files in place
Faster forreadswrites
Can be used as secondary indexes?YesYes
Can be compressed better?

Multi-Column Indexes

The most common type of multi-column index is called a concatenated index, which simply combines several fields into one key by appending one column to another. This is like an old-fashioned paper phone book, which provides an index from (lastname, firstname). Last names are sorted first, then first names.

Found The Key, But Where Is The Value?

The key in an index is the thing that queries search for, but the value can be one of three things:

  1. The actual row in question (clustered index)
  2. A reference to the row stored elsewhere (nonclustered index)
  3. A reference to the row stored elsewhere, along with some data from the actual row (covering index)

In nonclustered and covering indexes, the location where the rest of the row data lives is known as a heap file. There is a performance penalty for navigating to a heap file. Clustered and covering indexes can speed up reads, but they require additional storage and can add overhead on writes.

In-Memory Databases

In-memory key-value stores. Some are intended for caching use only (Memcached). Others aim for durability, achievable by:

  • Battery-powered RAM
  • Writing a log of changes to disk
  • Writing periodic snapshots to disk
  • Replicating the in-memory state to other machines

Examples include VoltDB, MemSQL, OracleTimesTen, RAMCloud, and Redis.

Counterintuitively, the performance advantage of in-memory databases is NOT due to the fact that they don’t need to read from disk. Even disk-based storage engines may never need to read from disk if you have enough memory. Rather, they can be faster because they can avoid the overheads of encoding in-memory data structures in a form that can be written to disk.

Transaction Processing & Analytics

OLTP vs OLAP

OLTPOLAP
Main read patternSmall number of records per query, fetched by keyAggregate over large number of records
Main write patternRandom-access, low-latency writes from user inputBulk import (ETL) or event stream
Primarily used byEnd userInternal analyst
What data representsLatest state of data (current point in time)History of events that happened over time
Dataset sizeGigabytes to terabytesTerabytes to petabytes
Storage orientationrow-orientedUsually column-oriented

OLAP (Online Analytics Processing) is done on a data warehouse, which is a separate database that analysts can query without affecting OLTP (Online Transaction Processing) operations. The process of getting data into the warehouse is known as Extract-Transform-Load (ETL).

Many data warehouses are used in a fairly formulaic style, known as a star schema (or dimensional modeling). At the center of the schema is a so-called fact table. Each row represents an event that occurred at a particular time. Some of the columns will contain attributes of the event, while others will contain foreign key references to dimension tables. The dimensions represent the who, what, where, when, how, and why of the event.

A variation of this template is known as the snowflake schema, where dimensions are further broken down into subdimensions.

Fact Table Dimension Table Sub- Dimensional Table Sub- Dimensional Table Dimension Table Sub- Dimensional Table Sub- Dimensional Table Dimension Table Sub- Dimensional Table Sub- Dimensional Table Dimension Table Sub- Dimensional Table Sub- Dimensional Table Dimension Table Sub- Dimensional Table Sub- Dimensional Table

Column-Oriented Storage

If you have trillions of rows and petabytes of data in your fact tables, storing and querying them efficiently becomes a challenging problem. Although fact tables are often over 100 columns wide, a typical data warehouse query only accesses 4 or 5 of them at one time. Loading all the other column data for the rows you’re querying from disk into memory, parsing them, and then filtering them, is expensive. That is why data warehouses frequently rely on column oriented storage.

Compression

Depending on the data in the column, different compression techniques can be used. One technique that is particularly effective in data warehouses is bitmap encoding. Often, the number of distinct values is small compared to the number of rows, as there are usually repeat values. We can take n distinct values and turn them into n separate bitmaps. Each bit represents one row. The bit is 1 if the row has that value, and 0 if it doesn’t.

We can further compress these bitmaps with run-length encoding, which works like so:

10000001100 → 1, 6, 2, 2 (1 one, 6 zeros, 2 ones, 2 zeros)

Bitwise AND and OR operations can be used to operate on such chunks of compressed column data directly. This technique is known as vectorized processing.

Sort order can also help with column compression. If the primary sort column does not have many distinct values, then after sorting, it will have long sequences where the same value is repeated many times in a row. Run-length encoding can be used to compress that column.

Caching

A materialized view is a database object that represents the result of some query. They are not often used in OLTP databases because they are written to disk, and need to be updated when the underlying data changes. This makes writes expensive. However, in a read-heavy database, like OLAP, they make much more sense. They typically use a materialized view known as a data cube or OLAP cube. The advantage of this is that certain queries become very fast because they’ve effectively been precomputed.

$5000 $4000 $4320 $5000 $5680 $6000 $10000 Los Angeles Chicago New York Phoenix Mango Cherry Strawberry Grapes Q4 Q3 Q2 Q1 Category Dimension Time Dimension

Chapter 4: Encoding and Evolution

Encoding Formats

Dataflow

Microservices architecture /Service-oriented architecture (SOA)Approach that decomposes a large application into smaller services by area of functionality, such that one service makes a request to another when it requires some functionality or data from that other service. A key design goal of this approach is to make the application easier to change and maintain by making services independently deployable and evolvable.
MiddlewareSoftware that supports one service making requests to another service owned by the same organization within a microservices/service-oriented architecture

RPC

A remote procedure call (RPC) is when a program makes a request to a remote network service look the same as calling a function in your programming language. This is not a great paradigm because it conflates local and remote execution, which are fundamentally different:

Network Request Local Function Call
The request or response may be lost due to a network problem, or the remote machine may be slow or unavailable. These problems are outside of your control. Either succeeds or fails, depending only on parameters that are under your control
Along with the 3 outcomes listed in the other column, network requests may timeout. Either
  1. Returns a result
  2. Throws an exception
  3. Never returns (infinite loop or crash)
If you retry a failed network request, it could be that the requests are actually getting through, and only the responses are getting lost. Retrying will cause the action to be performed multiple times, unless you build a mechanism for deduplication (idempotence). Don’t have this problem
Much slower than a function call, and latency is wildly variable Normally takes the same amount of time on each execution
Parameters must be encoded, problematic for larger objects Parameters are references to objects in local memory
The client and service may be implemented in different programming languages, so the RPC framework must translate data types from one language into another Don’t have this problem

REST vs SOAP

RESTSOAP
Is a…design philosophyXML-based protocol for making network API requests
Association with HTTP:Builds upon the principles of HTTP. It emphasizes simple data formats, using URLs for identifying resources, and using HTTP features for cache control, authentication, and content type negotiation.Although it is most commonly used over HTTP, it aims to be independent from HTTP and avoids using most HTTP features. Instead, it comes with a sprawling and complex multitude of related standards (the web service framework) that adds various features.

The API of a SOAP web service is described using an XML-based language called the Web Services Client Description (WSDL). Features of WSDL:

  • Enabled code generation, so that the client can access a remote service using local classes and method calls. This is useful in statically typed languages, and less so in dynamic ones.
  • Not human-readable, need tools like IDEs to use

Async Messaging Passing Systems

One main difference is that message passing is usually one-way, unlike RPC.

Examples of modern message brokers include RabbitMQ, ActiveMQ, HornetQ, NATS, and Apache Kafka. They generally work as follows: One process sends a message to a named queue or topic, and the broker ensures that the message is delivered to one or more consumers or subscribers of that queue or topic.

Distributed Actor Framework

Actor modelProgramming model for concurrency in a single process. Rather than dealing with threads, logic is encapsulated in actors. Each actor typically represents one client or entity, and manages its own state (which is not shared with other actors). Actors communicate with other actors by sending and receiving asynchronous messages. Message delivery is not guaranteed.
Distributed actor frameworkFramework in which the actor model is used to scale an application across multiple nodes. A message broker and an actor model are integrated together into a single framework.

Chapter 5: Replication

Replication means keeping a copy of the same data on multiple machines that are connected via a network. Each node that stores a copy of the database is called a replica.

Leader-Based Replication

  1. One of the replicas is designated as the leader. When clients want to write to the database, they must send their requests to the leader, which first writes the new data to its local storage.
  2. The other replicas are known as followers. Whenever the leader writes new data to its local storage, it also sends the data change to all of its followers as part of a replication log or change stream. Each follower takes the log from the leader and updates its local copy of the database accordingly.

Writes are only accepted by the leader (followers are read-only). Reads are accepted by both the leader and followers.

Sync vs Async Replication

Sync / Semi-Sync

In synchronous replication, the leader waits until the follower sends a success response back before informing the user/client of a success message.

Pro Con
Follower is guaranteed to have an up-to-date copy of the data that is consistent with the leader. If the leader suddenly fails, we can be sure that the data is still available on the follower. If the follower does not respond (crash, network fault, etc.), the write cannot be processed. The leader must block all writes and wait until the synchronous replica is available again.

Following from the “Con” column above, it is impractical for all followers to be synchronous: any one node outage would cause the whole system to grind to a halt. In practice, if you enable synchronous replication on a database, it usually means one of the followers is synchronous, and the others are asynchronous. If the synchronous follower becomes unavailable or slow, one of the asynchronous followers is made synchronous. This guarantees that you have an up-to-date copy of the data on at least two nodes. This is also known as semi-synchronous (since not all of the followers are synchronous).

Async

In asynchronous replication, the leader does NOT wait for a response from the follower. This is the most common configuration for leader-based replication.

Pro Con
The leader can continue processing writes, even if all of its followers have fallen behind. Not durable. If the leader fails and is not recoverable, any writes that have not yet been replicated to followers are lost.

Adding a New Follower

  1. Take a consistent snapshot of the leader’s database at some point in time. Most databases have this feature anyways, as it’s required for backups.
  2. Copy the snapshot to the new follower node.
  3. The follower connects to the leader and requests all the data changes that have happened since the snapshot was taken.

Node Outage

Follower

On its local disk, each follower keeps a log of the data changes it has received from the leader. If a follower crashes, it can recover quite easily from its log. It can then connect to the leader and request all the data changes that occurred during the time when the follower was disconnected, and then continue as normal.

Leader

When a leader fails, a (usually automatic) process happens known as failover, which consists of the following steps:

  1. Determining that the leader has failed (usually via timeout)
  2. Choosing a new leader (can be done via controller node or election process)
  3. Reconfiguring the system to use the new leader
    3.1. Clients need to send their write requests to the new leader
    3.2. Old leader becomes a follower and recognizes the new leader

Replication Methods

Statement-based replication

The leader logs every write request (statement) that it executes and sends that statement log to its followers.

Write-ahead log (WAL) shipping

Use a write-ahead log to build a replica on another node. The downside is that a WAL describes the data on a very low level, such as which bytes were changed in which disk blocks. This makes it closely coupled to the storage engine. This can mean that upgrading to a new version of the database software requires downtime.

Logical (row-based) log replication

To decouple the log from the storage engine, you can use a logical log. Rather than byte-level granularity, they use row-level granularity.

Trigger-based replication

This is replication that happens at the application layer. This is useful for when you need to replicate data to different database systems or custom conflict resolution logic. This approach is prone to bugs and incurs greater overheads. It works via a trigger, which lets you register custom application code that is automatically executed when a data change occurs in a database system.

Replication Lag

Replication lag is the delay between a write happening on a leader and when it is reflected onto a follower.

Read-after-write consistency

Read-after-write consistency is a guarantee that if a user reloads a page, they will always see any updates they submitted themselves. It is not guaranteed by default in a leader-based replication scheme. A write can be sent to a leader, who then forwards it along to a follower. However, a read request may have been sent to a follower before that could happen, to which they’ll retrieve old data (the newer data has only made its way to the leader). Methods for achieving read-after-write consistency with leader-based replication:

  • When reading something that the user modified, read it from the leader; otherwise, read it from a follower. This only works if most things in the application are not editable by the user. If that is not the case…
  • Use other criteria to determine whether to read from the leader. You could track the time of the last update, and for 1 minute after that time, make all reads from the leader.
  • The client can remember the timestamp of its most recent write. Then the system can ensure that the replica serving any reads for that user reflects updates at least until that timestamp.

If your replicas are distributed to multiple data centers, you have additional constraints. Any requests that need to be served by the leader must be routed to the datacenter that contains the leader. This is especially applicable when implementing cross-device read-after-write consistency. This is the idea that if the user enters some information on one device and then views it on another device, they should see the information they just entered. A computer on home WiFi and a mobile phone on a cell signal will not necessarily route to the same datacenter. If your approach requires reading from the leader, you may first need to route requests from all of a user’s devices to the same datacenter.

Monotonic Reads

Monotonic reads means if a user makes several reads in sequence, they will not see time go backwards, i.e., they will not read older data after having previously read newer data.

One way of achieving monotonic reads is to make sure that each user always makes their reads from the same replica. This replica can be chosen based on a hash of their user ID perhaps.

Consistent Prefix Reads

Consistent Prefix Reads guarantees that if a sequence of writes happens in a certain order, then anyone reading those writes will see them appear in the same order. This is a particular problem in partitioned (sharded) databases. Different partitions usually operate independently, so there is no global ordering of writes. One solution is to make sure that any writes that are causally (not casually) related to each other are written to the same partition.

Leaderless Replication

Clients send writes to several nodes. This kind of database is known as Dynamo-style, based off of Amazon’s in-house Dynamo system. It made leaderless replication fashionable after a long period of obsolescence.

What happens when a node goes offline for a while, and then comes back online? It missed a considerable amount of data. There are two mechanisms that are often used to handle this:

Read repairA read request is made from several nodes in parallel. This way, stale responses can be detected by comparing the values to one another.
Anti-entropy processSome datastores have a background process that constantly looks for differences in the data between replicas and copies any missing data from one replica to another.

Quorum

Separately, you can use a quorum: the minimum number of nodes that need to vote on an operation before it can be considered successful. This is determined via the following equation: w + r > n, where

  • w is the number of nodes written to successfully
  • r is the number of nodes read from successfully
  • n is the total number of replicas

If this equation is obeyed, the reads and writes are called quorum reads and quorum writes. With quorum reads, one of the r replicas you read from must have seen the most recent successful write. Keep in mind, however, that quorums are not strongly consistent (reads made at the same time will not necessarily return the same value).

What do you do if, because of a network interruption, r or w nodes cannot be reached? Then a quorum cannot be determined. In this case, you can use a sloppy quorum. Writes and reads still require w and r successful responses, but those may include nodes that are not among the designated n “home” nodes for a value.

Once the network interruption is fixed, any writes that one node temporarily accepted on behalf of another node are sent to the appropriate “home” nodes. This is known as hinted handoff.

Version Vector

Version vectorSometimes incorrectly equated with version clocks, which are a separate thing. A version vector is a collection of all version numbers from all replicas. Version vectors are sent from the database replicas to clients when values are read, and need to be sent back to the database when a value is subsequently written. The version vector allows the database to distinguish between overwrites and concurrent writes.

Replication Topology

Circular topology Star topology All-to-all topology
  • In circular and star topology, if just one node fails, it can interrupt the flow of replication messages between other nodes.
  • All-to-all topology is the most general. Avoids a single point of failure so has good fault tolerance. However, some network links may be faster than others, with the result that some replication messages may overtake others.

Comparing Replication Approaches

Single Leader Replication Multi-Leader Replication Leaderless Replication
How it works Every write must go to the datacenter with the leader. You can have a leader in each data center, so every write can be processed in the local datacenter and is replicated asynchronously to the other data centers. Clients send writes to several nodes.
When to use When you only have one data center (not exclusive to this scenario though). When you have multiple data centers and/or your application needs to continue to work while it is disconnected from the internet. In this case, every device has a local database that acts as a leader (and datacenter). When you have multiple data centers.
Tolerance of data center outages If the datacenter with the leader fails, failover can prompt a follower in another datacenter to be leader. Each datacenter can continue operating independently of the others, and replication catches up when the failed datacenter comes back online.
Pros Perceived performance may be better, since users don’t see latency between the local data center and data that is replicated to others. A temporary network interruption usually does not prevent writes from being processed. Designed to tolerate conflicting concurrent writes, network interruptions, and latency spikes.
Cons Sending every write to the leader’s datacenter can add latency to writes and might defeat the purpose of having multiple datacenters. Sensitive to problems over the local network in the datacenter. Write conflicts (which many implementations handle poorly) when data is concurrently modified in two different data centers. Avoid multi-leader replication if possible.
Linearizability Maybe. No, because of write conflicts. Usually not.
Uses Consensus Requires consensus to maintain its leadership and for leadership changes. Typically not. The write conflicts that occur here are typically because of the lack of consensus between leaders. Typically not. The write conflicts that occur here are typically because of the lack of consensus between leaders.

Chapter 6: Partitioning

The main reason for wanting to partition data is scalability. Different partitions can be placed on different nodes in a shared-nothing cluster. Our goal with partitioning is to spread the data and the query load evenly across nodes. If every node takes a fair share, then, in theory, 10 nodes should be able to handle 10 times as much data and 10 times the read and write throughput of a single node. If some partitions have more data or queries than others, we call it skewed. A partition with a disproportionately high load is called a hot spot.

Partitioning Of Key-Value Data

In this table, we only consider the case where we access these values by their primary index (the key). Accessing them with any other criteria would require a full scan. The following table compares various partitioning methods.

Random Key Range Partitioning Hash Partitioning
How it Works Assign records to nodes randomly. Think of how an encyclopedia set is organized (sorted keys). A-B is one book, C-D-E-F is another, until X-Y-Z. You can use a hash function to determine the partition of a given key.
Pros Distributes load very evenly. Great for range queries. Distributes load very evenly.
Cons When you’re trying to read a particular item, you have no way of knowing which node it is on, so you have to query all nodes in parallel. This can still lead to hot spots. This is dependent on how you choose the keys and key ranges you partition by, and will vary on application. Destroys the ordering of keys, so not efficient for range queries.
Most Common Rebalancing Method Rebalanced dynamically by splitting the range into two subranges when a partition gets too big. Fixed number of partitions is most common, but dynamic partitioning can also be used.

Partitioning and Secondary Indexes

Secondary Indexes do not identify a record uniquely. Rather, they are a way of searching for occurrences of a particular value.

Local (Document-Partitioned) Index Global (Term-Partitioned) Index
How it Works Different partitions maintain their own secondary indexes completely independently. Querying for these is known as scatter/gather since all partitions must be queried and the result must be combined. One separate partition holds all the secondary indexes, which in turn holds all the primary indexes of records from all partitions.
Pros Writes are faster, since only one partition needs to be updated. Reads are more efficient. Only one partition needs to be queried.
Cons Prone to tail latency amplification. Scatter/gather must query all partitions. Writes are slower and more complicated, since a write to a single document may now affect multiple partitions (assuming range partitioning is applied to the global index, which it usually is).

This article is helpful for diving deeper into this topic with visual aids.

Rebalancing Partitions

The process of moving load from one node in the cluster to another is called rebalancing. Rebalancing works differently depending on how you are partitioning nodes.

Fixed Number of Partitions Dynamic Partitioning Partitioning Proportionally to Nodes
How it Works Create many more partitions than there are nodes, and assign several partitions to each node. For example, a database running on a cluster of 10 nodes may be split into 1,000 partitions from the outset so that approximately 100 partitions are assigned to each node. When a node is added to the cluster, the new node can steal a few partitions from every existing node until partitions are fairly distributed once again. If a node is removed from the cluster, the same happens in reverse. When a partition grows to exceed a configured size, it is split into two partitions. If a lot of data is deleted and a partition shrinks below some threshold, it can be merged with an adjacent partition. After a large partition has been split, one of its two halves can be transferred to another node in order to balance the load. There are a fixed number of partitions per node. When the number of nodes increases, the partitions become smaller. When a new node joins the cluster, it randomly chooses a fixed number of existing partitions to split, and then takes ownership of half of each of those split partitions while leaving the others in place.
Optimal for Key-Range Partitioning
How the number of partitions adapt to the total data volume The number of partitions you select is the maximum number of nodes you can have. The number of partitions you select also affect their size. If they’re too big, rebalancing and recovering from node failures becomes expensive. If they’re too small, they incur excessive overhead. You’re looking for a number that’s just right. The number of partitions adapt to the total data volume. If there is only a small amount of data, a small number of partitions is sufficient, so overheads are small. If there is a large amount of data, the size of each individual partition is limited to a configurable maximum. The size (not number) of each partition grows proportionally to the dataset size (as long as the number of nodes remains unchanged).

Request Routing

When a client wants to make a request, how does it know which node to connect to? This problem is known as service discovery. There are different approaches:

  • Allow clients to contact any node and make them handle the request directly, or forward the request to the appropriate node.
  • Send all requests from clients to a routing tier first that acts as a partition-aware load balancer.
  • Make clients aware of the partitioning and the assignment of partitions to nodes.

Many distributed data systems rely on a separate coordination service such as ZooKeeper to keep track of this cluster metadata. Each node registers itself in ZooKeeper, and ZooKeeper maintains the authoritative mapping of partitions to nodes. The routing tier or the partitioning-aware client can subscribe to this information in ZooKeeper.

Chapter 7: Transactions

A transaction is a way for an application to group several reads and writes together into a logical unit. Conceptually, all the reads and writes in a transaction are executed as one operation: either the entire transaction succeeds (commit) or it fails (abort, rollback). They are ultimately an abstraction layer that allow an application to pretend that certain concurrency problems and certain kinds of hardware and software faults don’t exist.

ACID

The safety guarantees provided by transactions are often described by the well known acronym ACID, which stands for Atomicity, Consistency, Isolation, and Durability.

AtomicityThe ability to abort a transaction on error and have all writes from that transaction discarded
ConsistencyCertain statements about your data (invariants) that are always true (for example, in an accounting system, credits and debits across all accounts must always be balanced). However, this idea of consistency depends on the application’s notion of invariants. It’s the application’s responsibility to define its transactions correctly so that they preserve consistency. This is not something that the database can guarantee. Thus, the letter C does not really belong in the ACID acronym.
IsolationConcurrently executing transactions shouldn’t interfere with each other
DurabilityA promise that once a transaction has been committed successfully, any data it has written will not be forgotten, even if there is a hardware fault or the database crashes. However, there is no such thing as perfect durability. For example, if all the hardware is destroyed, there’s nothing you can do.

Isolation

Isolation tries to hide concurrency issues from application developers. However, it does not operate as a binary (on/off). There are many different levels of isolation, and they all protect against different race conditions and concurrency issues, with varying tradeoffs. Before we talk about isolation levels, let’s understand what concurrency issues can arise.

General Concurrency Problems

Dirty readsOne client reads another client’s writes before they have been committed
Dirty writesOne client overwrites data that another client has written, but not yet committed
Read skew /Non-Repeatable ReadsA client sees different parts of the database at different points in time
Lost updatesTwo clients concurrently perform a read-modify-write cycle. One overwrites the other’s write without incorporating its changes, so its data is lost. Can be prevented with implicit or explicit locks, or by not doing anything, and just aborting a transaction when the transaction manager has detected a lost update has happened.

Phantom Concurrency Problems

When a write in one transaction changes the result of a search query in another transaction, it is called a phantom. The problem with phantoms is that there is no object to which we can attach locks. To solve this, we can employ an approach called materializing conflicts. However, doing this is ugly, and should be considered a last resort.

Write skewA transaction reads something, makes a decision based on the value it saw, and writes the decision to the database. However, by the time the write is made, the premise of the decision is no longer true.
Phantom readsA transaction reads objects that match some search condition. Another client makes a write that affects the results of that search.

Pessimistic & Optimistic Concurrency Control

ContentionWhen there are many transactions trying to access the same objects.
Pessimistic concurrency controlBased on the principle that if anything might possibly go wrong, it is better to wait until the situation is safe again before doing anything (like blocking with a mutex).
Optimistic concurrency controlTransactions continue, even if something dangerous is happening, in the hope that everything will be alright. If something bad happened (isolation was violated), the transaction is aborted and has to be retried. If contention is low, optimistic concurrency control performs better than pessimistic ones. If there is high contention, performance is poor.

Database Locking Mechanisms

Shared modeFor reading
Exclusive modeFor writing
Predicate lockOperate by locking all objects that match some search condition (predicate). Applies even to objects that do not yet exist in the database.
Index-range lock /Next-key lockSuperset of predicate locks. They simplify a predicate by making it match a greater set of objects. This is an obvious disadvantage, but ultimately means fewer overall locks, so less locks to check.

Isolation Levels

Description Implementation Concurrency Problems Prevented
Read Committed Isolation Prevents dirty reads and dirty writes. Dirty reads can be prevented by the database juggling multiple values. The database gives the reader the old value. When the old value is overwritten in a commit by whoever had the write lock, the reader is given the new value. Dirty writes can be prevented using row-level locks.
  • Dirty Reads
  • Dirty Writes
Snapshot Isolation Each transaction reads from a consistent snapshot of the database. In other words, the transaction sees all the data that was committed in the database at the start of the transaction. Multi-version concurrency control (MVCC) which maintains several different versions of an object side by side. MVCC is sometimes also used for dirty read prevention.
  • Dirty Reads
  • Dirty Writes
  • Read Skew
Serializable Isolation Strongest isolation level. It guarantees that even though transactions may execute in parallel, the end result is the same as if they had executed one at a time, serially, without concurrency.

Many options:

  • Execute transactions in serial order
  • Two-Phase Locking
  • Serializable Snapshot Isolation

All concurrency issues.
Serializable Isolation Implementations
Description Concurrency Control Type Pros & Cons Linearizable
Execute Transactions in Serial Order If you can make each transaction very fast to execute, and the transaction throughput is low enough to process on a single CPU core, this is a simple and effective option. Pessimistic Poor scalability. Typically linearizable
Two-Phase Locking (2PL) The two phases in the name are referencing acquiring locks and releasing locks. The locks used can either be in shared mode or exclusive mode. In the case of phantom prevention, they can also either be a predicate lock or index-range lock. Pessimistic Poor performance. Typically linearizable
Serializable Snapshot Isolation (SSI) SSI, as the name implies, is based on Snapshot Isolation. All reads within a transaction are made from a consistent snapshot of the database. On top of snapshot isolation, SSI adds an algorithm for detecting serialization conflicts among writes and determining which transactions to abort. Optimistic Relatively new, but seems to be better than all alternatives. No. The whole point of a consistent snapshot is that it does not include writes that are more recent that the snapshot, and thus, reads from the snapshot are not linearizable.

Chapter 8: Problems in Distributed Systems

Partial failureSome parts of the system are broken in some unpredictable way, even though other parts of the system are working fine
Noisy neighborA node near you that is using a lot of resources

Async vs Sync Network

Asynchronous Network Synchronous Network
Examples Datacenter networks & the internet Fixed-line telephone network (non-cellular, non VoIP)
Suffers from this delay type: Unbounded delay, meaning they try to deliver packets as quickly as possible, but it could take forever. Bounded delay, meaning the maximum end-to-end latency of the network is fixed.
Switching Type Ethernet and IP are packet-switched protocols Circuit Switching. When you make a call over a telephone network, it establishes a circuit: a fixed amount of bandwidth for the call.
Resource Partitioning Style Dynamic resource partitioning. Bandwidth allocation is constantly changing depending on the amount of data being handled. Static resource partitioning. Has a particular bandwidth requirement, and can only allocate exactly that much space.
Optimized for: Bursty traffic, which is characterized by having unexpected or sudden network traffic peaks and troughs (highly variable). Requesting a web page, sending an email, or transferring a file doesn’t have any particular bandwidth requirement. We just want it to complete as quickly as possible. Relatively constant amount of traffic. Makes sense for a telephone network, because it transfers a fairly constant number of bits per second for the duration of the call.
Pros Maximizes utilization of the wire. Does not suffer from queueing.
Cons Variable delays and …

Queueing

  1. The network switch must queue the packets into the destination network link
  2. If all CPUs are busy, the OS will queue the request until the application is ready
  3. Could also be queued by the virtual machine monitor
Reduced utilization of the wire. The circuit allocates the same fixed amount of bandwidth regardless of whether the wire is fully utilized or only 1% utilized.

TCP vs UDP

TCPUDP
Stands for:(T)ransmission (C)ontrol (P)rotocol(U)ser (D)atagram (P)rotocol
Connection TypeRequires an established connection before transmitting dataNo connection is needed to start and end a data transfer
Data sequencingCan sequence data (send in a specific order)Cannot sequence or arrange data
Data retransmissionCan retransmit data if packets fail to arriveNo data retransmitting. Lost data can’t be retrieved.
DeliveryDelivery is guaranteedDelivery is not guaranteed
Check for errorsThorough error-checking guarantees data arrives in its intended stateMinimal error-checking covers the basics but may not prevent all errors
BroadcastingNot supportedSupported
SpeedSlowFast
Good use casesEmail, texting, file transfer, web browsingLive streaming, online gaming, video chat

Detecting Faults

A timeout is the only way of detecting a fault. But then how long should the timeout be?

  • A long timeout means a long wait until a node is declared dead
  • A short timeout detects faults faster, but carries a higher risk of incorrectly declaring a node dead during a temporary slowdown

A good timeout value can only be determined experimentally.

Clocks

It doesn’t make sense to think of a clock reading as a point in time. It is more like a range of times, within a confidence interval. Unfortunately, most systems don’t expose this uncertainty.

Network Time Protocol (NTP)Allows the computer’s clock to be adjusted according to the time reported by a group of servers. The servers in turn get their time from a more accurate time source such as a GPS receiver.
Logical clocksBased on incrementing counters. They do not measure time of day or amount of time elapsed, only the relative ordering of events (which they does better than physical clocks).
Physical Clocks
Based on oscillating quartz crystal.
Time-Of-Day Clock Monotonic Clock
Used for getting current date and time Used for measuring elapsed duration
May jump backwards in time Guaranteed to always move forward
Usually synchronized with NTP NTP may adjust the frequency at which the monotonic clock moves forward if the quartz crystal drifts (moves faster or slower than it should). The adjustment process is known as slewing.

Timing

Unexpected Pauses

A node in a distributed system must assume that its execution can be paused for a significant length of time at any point. A thread or process can pause on a node for a number of reasons:

  • The JVM’s garbage collector stops all running threads. Can be mitigated by treating this like a planned outage.
  • In virtualized environments, a virtual machine can be suspended, which pauses the execution of all processes
  • A user can close their laptop
  • OS can context-switch to another thread
  • Thread may be paused waiting on disk I/O
  • A UNIX process receives a SIGSTOP signal

During the pause, the rest of the nodes keep operating and may even declare the node dead. Eventually, the paused node may continue running without even noticing that it was asleep until it checks its clock sometime later.

For some systems, this is unacceptable. These are known as real-time systems. In these systems, there is a specified deadline by which the software must respond. They are most commonly used in safety-critical embedded devices (i.e. airbags). They are very difficult and expensive to develop. For most server-side data processing systems, real-time guarantees are simply not economical or appropriate.

Lease

You can think of a lease as a lock with a timeout. It’s useful in a distributed system in lots of cases, such as determining who is the leader. Only one node can hold the lease at any one time. Thus, when a node obtains a lease, it knows it’s the leader until the lease expires. In order to remain a leader, the node must periodically renew the lease before it expires. If the node fails, it stops renewing the lease, so another node can take over when it expires.

However, there’s an issue: what if…

  1. Node obtains the lease
  2. Node pauses
  3. Lease expires
  4. Node unpauses

In this case, the node will believe it is still the leader. This can be resolved using fencing.

When a lease or lock is administered, a fencing token is also given. This number increases every time a lock or lease is granted. A server will only process requests from the fencing token with the highest number. So if the server processed a fencing token of 34, and then someone comes along with 33, it’ll register that fencing token as outdated, and not process the request. This, of course, opens you up to the vulnerability of dishonest nodes.

Byzantine

Byzantine faultFault where nodes are malfunctioning, or if malicious attackers are interfering with the network.
Byzantine fault-tolerantCharacteristic of a system that continues to operate correctly despite byzantine faults. In most server-side data systems, the cost of deploying byzantine fault-tolerant solutions makes them impractical. They are a necessity in certain use cases though:
  • In aerospace environments, the data in a computer’s memory or CPU register could become corrupted by radiation, leading it to respond to other nodes in arbitrarily unpredictable ways.
  • In a system with multiple participating organizations, some participants may attempt to cheat. Since peer-to-peer networks have no central authority, byzantine fault-tolerance is more relevant.
Two Generals ProblemScenario where two army generals need to agree on a battle plan. They have set up camp far away from each other. They can only communicate by messenger. The messengers sometimes get delayed or lost (like packets in a network).
Byzantine Generals ProblemSame as the Two Generals Problem, except there are n generals who need to agree, and some messengers are traitors.

Distributed Algorithm Properties

Safety Liveness
Short Definition Nothing bad happens Something good eventually happens
Long Definition If a safety property is violated, you can point to a particular point in time that it happened, and it can’t be undone. Imagine giving out a duplicate fencing token. A liveness property may not hold at some point in time, but there is always hope that it will be satisfied in the future. Imagine waiting for a response to your request.
Examples (Using Fencing Tokens) Uniqueness
  • No two requests for a fencing token return the same value
Monotonic Sequence
  • Token values will always increase in the order they’re given out
Availability
  • A node that requests a fencing token and does not crash eventually receives a response
Can they be violated? It is common to require that safety properties always hold, in all possible situations. We are allowed to make caveats. For example, we could say that a request needs to receive a response only if a majority of nodes have not crashed.

Chapter 9: Consistency and Consensus

Consistency Models

Causal Consistency (Causality)Linearizability
DefinitionCausality imposes an ordering of events: cause comes before effect; a message is sent before that message is received; the question comes before the answer.Linearizability is the database giving the illusion of their being only one copy of the data. As soon as one client successfully completes a write, all clients reading from the database must be able to see the value just written. In other words, linearizability is a recency guarantee.
ConcurrencyIf A and B are concurrent, there is no causal link between them.Linearizability implies causality: any system that is linearizable will preserve causality. You can think of causality as a subset of linearizability. Because there is no causal link between two concurrent operations, there are no concurrent operations in a linearizable datastore.
OrderCausality defines a partial order: some operations are ordered with respect to each other (like one set that is a subset of another), but others are incomparable (neither set overlaps). An example would be git branches.Linearizability implies a total order, which allows any two elements to be compared. Think of real numbers. You always know which one is bigger and which one is smaller.
Misc. InfoRead skew means reading data in a state that violates causality.Linearizability comes with a performance penalty proportional to the uncertainty of delays in the network. It does not prevent write skew.

Other consistency models include:

Eventual consistencyIf you stop writing to the database and wait for some unspecified length of time, then eventually, all read requests will return the same value
Sequential conistency /Timeline consistencySimilar to linearizability, but does not guarantee linearizable reads
S3 consistencyThe consistency model used for Amazon S3
Global consistencySome history of events that is totally ordered

The relationship between all these consistency models is visualized as follows:

Global consistency Linearizability Sequential Consistency S3 Consistency Causal Consistency Eventual Consistency

When Do You Want Linearizability?

  • Uniqueness constraints as data is written, such as usernames, emails, file names, etc.
  • Bank account balance never goes negative
  • You don’t sell more items than you have in stock
  • Don’t double-book same seat on flight

Serializability

SerializabilityAn isolation property of transactions, where every transaction may read and write multiple objects. It guarantees that transactions behave the same as if they had been executed in some serial order.
Strict serializability /Strong one-copy serializabilityWhen a database provides both serializability and linearizability

CAP Theorem

Partition Tolerance Partition in this case means “network partition”. This vertex represents tolerance specifically for network partition faults. Consistency (Linearizability) Availability

CAP theorem is similar to ACID, in that it is vague and unhelpful. It is sometimes presented as: Consistency, Availability, Partition Tolerance: pick 2 out of the 3. However, this is misleading because network faults always happen eventually. So, the real question is, when a network fault does inevitably happen, you can either choose to be consistent or available.

How Do You Achieve Linearizability?

Lamport Timestamps

Lamport timestamps are sequence numbers that are consistent with causality and provide a total order. They take on the form of (counter, nodeID). If you are given two lamport timestamps, the one with a greater counter value is greater. If the counter values are equal, the one with the greater node ID is greater.

Every node and every client keeps track of the maximum counter value it has seen so far, and includes that maximum on every request. When a node receives a request or response with a maximum counter value greater than its own counter value, it immediately increases its own counter to that maximum.

Lamport timestamps are not enough to achieve linearizability though. Total order of operation only emerges after you have collected all of the operations. If one operation needs to decide right now whether a decision should be made (e.g. 2 concurrent writes for a uniqueness constraint), it might need to check with every other node that there’s no concurrently executing operation that could affect its decision.

Total Order Broadcast

In order to use total ordering between multiple nodes, we should use total order broadcast / atomic broadcast, which is a message exchange protocol that guarantees that no messages are lost, and every message is delivered to every node in the same order and exactly once. Total order broadcast is used in database replication, serializable transactions, creating messages log, and lock for fencing tokens.

Total order broadcast is based on a principle known as state machine replication: If every message represents a write to the database, and every replica processes the same writes in the same order, then the replicas will remain consistent with each other.

Consensus

The goal of consensus is to get several nodes to agree on something. This is formalized as: one or more nodes may propose values, and the consensus algorithm decides on one of those values. A consensus algorithm must satisfy the following properties:

Definition Information Safety or Liveness Property
Uniform Agreement No two nodes decide differently Core idea of consensus. Everyone decides on the same outcome, and once you have decided, you cannot change your mind. Safety
Integrity No node decides twice Safety
Validity If a node decides value v, then v was proposed by some node Here to rule out trivial solutions. For example, the consensus algorithm could always decide null, even if no one proposed it. Safety
Termination Every node that does not crash eventually decides some value Formalizes the idea of fault tolerance. A consensus algorithm cannot sit around and do nothing forever - it must make progress. Liveness

Most consensus algorithms (Paxos, Raft, Zab, etc.) are actually total order broadcast algorithms, because the nodes decide on a sequence of values, rather than a single value. Total order broadcast itself is equivalent to repeated rounds of consensus.

Most consensus algorithms also use a leader, but don’t guarantee that the leader is unique. They instead define epoch numbers, and guarantee that for each epoch, the leader is unique. Every time the current leader is thought to have died, an election begins with an incremented epoch number. If two different nodes think they’re both the leader, they compare epoch numbers. They know the one with the higher epoch number is the true leader.

Limitations of Consensus

Problems Consensus Solves

Two-Phase Commit (2PC)

2PC is a consensus algorithm (except that it doesn’t meet the termination property) that aims to achieve atomic transaction commits across multiple nodes. This is easy enough to do on a single node, and is usually handled by the storage engine in that case.

So how does 2PC address this?

  1. The application reads and writes data on multiple database nodes as normal
  2. When it’s ready to commit, the coordinator sends a “prepare” request to each of the nodes, asking them if they’re able to commit
  3. The point at which the coordinator has received all responses and makes a decision is known as the commit point. At this point, the coordinator writes its commit or abort decision down into a transaction log.
  4. If any of the nodes respond “no”, an “abort” request is sent to all nodes
  5. Otherwise, the coordinator sends a “commit” request and the commit actually takes place
Potential IssueSolution
The commit or abort request fails for any nodeThe coordinator must retry until it succeeds
Node crashes before it successfully commits or aborts the requestThe transaction will be committed when it recovers
The coordinator crashesWhen the coordinator recovers, it determines the status of all in doubt transactions by reading its transaction log. However, it is not uncommon for in-doubt transactions to become orphaned (potentially because the transaction log became corrupted).

The entirety of 2PC hinges on the success of the transaction coordinator. If the coordinator has crashed and takes 20 minutes to start up again, locks will also be held by pending commits for 20 minutes. If the coordinator’s log is entirely lost for some reason, those locks will be held forever (or at least until the situation is manually resolved).

The transaction coordinator is itself a kind of database (in which transaction outcomes are stored), so it needs to be approached like one. That is why it’s typically replicated. We don’t want it to be a single point of failure.

Distributed Transactions

(Y’know, as opposed to single node transactions)

Database-internal distributed transactionsHeterogeneous distributed transactions
Distributed transactions among a single database. Some distributed databases (i.e. databases that use replication and partitioning in their standard configuration) support internal transactions among the nodes of that database.A distributed transaction between two different technologies (i.e., different databases, message brokers, etc.)

X/Open XA (short for eXtended architecture) is a standard for implementing 2PC across heterogeneous technologies. It is a C API for interfacing with a transaction coordinator. XA is supported by many traditional relational databases, as well as message brokers. It allows you to atomically commit transactions between different technologies.

However, it cannot detect deadlocks across different systems, and it does not work with Serializable Snapshot Isolation.

Membership and Coordination Services

ZooKeeper is the most common membership and coordination service. Others include etcd and Consul. It is essentially a key-value store that is designed to hold small amounts of data that can fit entirely in memory. This data is replicated across all nodes in a system using a fault-tolerant total order broadcast algorithm (consensus). Lots of tools rely on ZooKeeper specifically (HBase, Hadoop, Kafka, etc.). Along with consensus, it comes with the following features:

  • Linearizable atomic operations
  • Total ordering of operations
    • Provides a fencing token via a transaction ID and a version number
  • Failure detection
    • Uses heartbeats
  • Change notifications
    • A client can find out when another client joins the cluster, or fails
  • Service discovery
    • Find out which IP addresses you need to connect to in order to reach a particular service

If you need a feature that is reducible to consensus, it is best to use ZooKeeper.

Chapter 10: Batch Processing

Batching processing systems take a large amount of input data, run a job to process it, and produce some output data. They are often scheduled periodically.

MapReduce

MapReduce is a batch processing algorithm. It lets you write code to process large datasets in a distributed file system, and thus, works in parallel across multiple machines easily.

It is implemented in various open source data systems, such as Hadoop, CouchDB, and MongoDB. Other frameworks have functionality built atop Hadoop’s MapReduce API, which is somewhat cumbersome. Some of these frameworks are: Pig, Hive, Cascading, and Crunch. Hadoop has the most commonly referenced implementation of MapReduce, so we’ll focus on that.

MapReduce treats inputs as immutable, and avoids side effects (such as writing to external databases). This allows for good performance and maintainability. It is designed to tolerate frequent unexpected task termination.

MapReduce is based on two eponymous functions: the mapper function and the reducer function. They work in the following order:

  1. A set of input files is read
  2. The mapper function extracts a (key, value) pair from each input record. This makes it suitable for sorting.
  3. Behind the scenes, the shuffle happens. This means that the data is partitioned by reducer, sorted by key, and data partitions are copied from mappers to reducers.
  4. The reducer function iterates over the sorted (key, value) pairs, and derives some information from them

Joins

How are joins performed in MapReduce? There are no indexes in MapReduce, and thus, no foreign keys. It must read the entire content of a file. There are two kinds of joins: map-side joins and reduce-side joins. They are categorized based on where the join logic happens. The variations are outlined below.

Map-Side Joins
Join logic happens in the mappers. There are no reducers and no sorting.
Reduce-Side Joins
Join logic happens in the reducers. The output of the join is partitioned and sorted by the join key.
Broadcast Hash Joins Partitioned Hash Joins / Bucketed Map Joins Sort-Merge Joins
One input is small and held in an in-memory hash map, the other input is large. Small input is sent to all nodes where the larger input lives. Iterate through the large input and check if the key exists in the small input (as a hash map). Used when both datasets are too big to fit in memory. The two join inputs must be partitioned in the same way. Then the hash map approach can be used independently for each partition. Each of the inputs being joined goes through a mapper that extracts the join key. By partitioning, sorting, and merging, all the records with the same key end up going to the same call of the reducer. This function can then output the joined records.

Workflows

MapReduce jobs can be chained together into workflows. The output of one job becomes the input to the next job. This works by having the first job’s output in HDFS be the second job’s input. If you have multiple jobs chained together, you might not care about the output of a single job. In that case, it’s just a means to an end, or intermediate state. The process of writing out this intermediate state is known as materialization, and ensures durability. While this means that if a workflow is terminated when it’s only 50% done and can then pick right back up where it left off, it has downsides:

Dataflow Engines

As an answer to the problems with MapReduce, other dataflow engines for distributed batch computations were developed, such as Spark, Tez, and Flink.

Rather than explicit map and reduce functions, these dataflow engines employ operators. The advantages they have over mapper and reducer functions are numerous:

However, it’s easy for MapReduce to recover from faults since it materializes intermediate state. Other dataflow engines take a different approach to tolerating faults. They try and recompute from other data that is still available.

Graphs

Let’s examine graph algorithms in the context of batch processing algorithms. These graph algorithms are often iterative, traversing one edge at a time and joining together vertices as they go. This is done until there are no more edges to traverse, or some other condition is met.

Recall that MapReduce must read the entire content of a file. Thus, performing this kind of graph algorithm with MapReduce is extremely inefficient. It will read the entire input dataset and produce a complete new output dataset, even if only a small part of the graph has changed compared to the last iteration.

An optimization for batch processing graphs was developed, known as the Bulk Synchronous Parallel (BSP) model of computation, or the Pregel model. In each iteration, a function is called for each vertex, passing it all the messages that were sent to it, much like a call to the reducer. Unlike MapReduce, the Pregel model remembers its state in memory from one iterate to the next. Various dataflow engines implement the Pregel Model, such as Spark and Flink.

Hadoop vs MPP Databases

Massively Parallel Processing (MPP) databases are a type of data warehouse where multiple nodes process analytic SQL queries in parallel. They are monolithic, tightly integrated pieces of software that take care of storage layout on disk, query planning, scheduling, and execution.

Hadoop / MapReduce MPP
Dump data into HDFS, figure out how to process it later. This approach is known as the sushi principle, since “raw data is better” Requires careful up-front modeling of the data before it can be imported
Eager to write data to disk, partly for fault tolerance, partly because it assumes the dataset will be too big to fit in memory anyway Prefer to keep as much data as possible in memory to avoid the cost of reading to disk
Queries can take an indefinite amount of time Queries take a few seconds or minutes
MapReduce only reruns the individual task that failed. It would be very wasteful to rerun a time-consuming job just because one task failed. If a node crashes while a query is executing, most MPP databases abort the entire query, and either let the user resubmit the query, or automatically run it again
Can run arbitrary code Run SQL queries
Raw data is dumped into HDFS via an ETL process, then MapReduce jobs are written to clean up the data and transform it into a relational form Imported into an MPP data warehouse for analytic purposes

Chapter 11: Stream Processing

Stream processing software includes RabbitMQ, ActiveMQ, HornetQ, Qpid, TIBCO Enterprise Message Service, IBM MQ, Azure Service Bus, and Google Cloud Pub / Sub.

An event is a small, self-contained, immutable object containing the details of something that happened at some point in time. It is the data unit that comprises a stream, which is just any sequence of data that is made available over time. Related events are usually grouped together into a topic or stream. Events are created by producers / publishers / senders and processed by potentially multiple consumers / subscribers / recipients.

Brokers

A message broker / message queue acts as the middle man between these two groups. It is essentially a kind of database that is optimized for handling message streams.

AMQP/JMS-style message broker Log-based message broker
  1. Broker assigns individual messages to consumers
  2. Consumers acknowledge individual messages when they have been successfully processed
  3. Messages are deleted from the broker once they have been acknowledged
The broker assigns all messages in a partition to the same consumer node, and always delivers messages in the same order
Can’t go back and read old messages Can read old messages if needed

Brokerless

There exists brokerless stream processing software, such as ZeroMQ and nanomsg. Alternatively, you can avoid using any of these kinds of software entirely and rely on UDP multicast. You can also use webhooks, which are a pattern in which a callback URL of one services is registered with another service, and it makes a request to that URL whenever an event occurs.

Methods For Turning Changes To A Database Into A Stream

Change Data Capture (CDC)Event Sourcing
DefinitionProcess of observing all data changes written to a database and extracting them in a form in which they can be replicated to other systemsStoring all changes to the application state as a log of change events
MutabilityMutableImmutable