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-out | The 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. |
|---|---|
| Throughput | The 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 up | Moving to a more powerful machine |
| Horizontal scaling /scaling out /Shared-nothing architecture | Distributing load across multiple machines |
| Elastic | Systems that can automatically add computing resources when they detect a load increase |
Response Time
| Response time | The 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 |
|---|---|
| |
| 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 mismatch | Describes 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.
| 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.
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-Tree | LSM Tree | |
|---|---|---|
| Breaks the database down into | fixed sized blocks or pages, typically 4KB or more in size | variable sized segments, typically several megabytes or more in size |
| The write operation is performed as | an overwrite on a page on disk with new data | an append operation to files, but never modify files in place |
| Faster for | reads | writes |
| Can be used as secondary indexes? | Yes | Yes |
| 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:
- The actual row in question (clustered index)
- A reference to the row stored elsewhere (nonclustered index)
- 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
| OLTP | OLAP | |
|---|---|---|
| Main read pattern | Small number of records per query, fetched by key | Aggregate over large number of records |
| Main write pattern | Random-access, low-latency writes from user input | Bulk import (ETL) or event stream |
| Primarily used by | End user | Internal analyst |
| What data represents | Latest state of data (current point in time) | History of events that happened over time |
| Dataset size | Gigabytes to terabytes | Terabytes to petabytes |
| Storage orientation | row-oriented | Usually 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.
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.
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. |
|---|---|
| Middleware | Software 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
|
| 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
| REST | SOAP | |
|---|---|---|
| Is a… | design philosophy | XML-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 model | Programming 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 framework | Framework 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
- 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.
- 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
- 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.
- Copy the snapshot to the new follower node.
- 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:
- Determining that the leader has failed (usually via timeout)
- Choosing a new leader (can be done via controller node or election process)
- 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 repair | A 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 process | Some 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
wis the number of nodes written to successfullyris the number of nodes read from successfullynis 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 vector | Sometimes 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
- 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.
| Atomicity | The ability to abort a transaction on error and have all writes from that transaction discarded |
|---|---|
| Consistency | Certain 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. |
| Isolation | Concurrently executing transactions shouldn’t interfere with each other |
| Durability | A 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 reads | One client reads another client’s writes before they have been committed |
|---|---|
| Dirty writes | One client overwrites data that another client has written, but not yet committed |
| Read skew /Non-Repeatable Reads | A client sees different parts of the database at different points in time |
| Lost updates | Two 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 skew | A 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 reads | A transaction reads objects that match some search condition. Another client makes a write that affects the results of that search. |
Pessimistic & Optimistic Concurrency Control
| Contention | When there are many transactions trying to access the same objects. |
|---|---|
| Pessimistic concurrency control | Based 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 control | Transactions 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 mode | For reading |
|---|---|
| Exclusive mode | For writing |
| Predicate lock | Operate 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 lock | Superset 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. |
|
| 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. |
|
| 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:
| 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 failure | Some parts of the system are broken in some unpredictable way, even though other parts of the system are working fine |
|---|---|
| Noisy neighbor | A 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
| 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
| TCP | UDP | |
|---|---|---|
| Stands for: | (T)ransmission (C)ontrol (P)rotocol | (U)ser (D)atagram (P)rotocol |
| Connection Type | Requires an established connection before transmitting data | No connection is needed to start and end a data transfer |
| Data sequencing | Can sequence data (send in a specific order) | Cannot sequence or arrange data |
| Data retransmission | Can retransmit data if packets fail to arrive | No data retransmitting. Lost data can’t be retrieved. |
| Delivery | Delivery is guaranteed | Delivery is not guaranteed |
| Check for errors | Thorough error-checking guarantees data arrives in its intended state | Minimal error-checking covers the basics but may not prevent all errors |
| Broadcasting | Not supported | Supported |
| Speed | Slow | Fast |
| Good use cases | Email, texting, file transfer, web browsing | Live 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 clocks | Based 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…
- Node obtains the lease
- Node pauses
- Lease expires
- 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 fault | Fault where nodes are malfunctioning, or if malicious attackers are interfering with the network. |
|---|---|
| Byzantine fault-tolerant | Characteristic 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:
|
| Two Generals Problem | Scenario 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 Problem | Same 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
| Availability
|
| 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 | |
|---|---|---|
| Definition | Causality 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. |
| Concurrency | If 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. |
| Order | Causality 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. Info | Read 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 consistency | If 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 consistency | Similar to linearizability, but does not guarantee linearizable reads |
| S3 consistency | The consistency model used for Amazon S3 |
| Global consistency | Some history of events that is totally ordered |
The relationship between all these consistency models is visualized as follows:
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
| Serializability | An 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 serializability | When a database provides both serializability and linearizability |
CAP Theorem
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?
- The application reads and writes data on multiple database nodes as normal
- 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
- 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.
- If any of the nodes respond “no”, an “abort” request is sent to all nodes
- Otherwise, the coordinator sends a “commit” request and the commit actually takes place
| Potential Issue | Solution |
|---|---|
| The commit or abort request fails for any node | The coordinator must retry until it succeeds |
| Node crashes before it successfully commits or aborts the request | The transaction will be committed when it recovers |
| The coordinator crashes | When 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 transactions | Heterogeneous 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:
- A set of input files is read
- The mapper function extracts a
(key, value)pair from each input record. This makes it suitable for sorting. - 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.
- 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 |
|---|---|
| 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 | |
|---|---|---|
| Definition | Process of observing all data changes written to a database and extracting them in a form in which they can be replicated to other systems | Storing all changes to the application state as a log of change events |
| Mutability | Mutable | Immutable |