Part II — Storage · Chapter 3

Distributed storage: HDFS, file formats and Parquet

~45 min read6 interactive widgets4 plates

In this chapter

  1. What HDFS is
  2. The three goals of a DFS
  3. Blocks, and why they are so big
  4. NameNode and DataNodes
  5. The single point of failure and its remedies
  6. Federation
  7. Replication and block placement
  8. Erasure Coding
  9. Where HDFS is not a good fit
  10. HDFS I/O: the read path
  11. Why big data file formats
  12. Row-oriented vs column-oriented
  13. Parquet: history and data model
  14. Unnesting: definition and repetition levels
  15. Two exercises on levels
  16. Encodings and the physical file layout
  17. Test your knowledge

1. What HDFS is

The definition the course uses is worth taking apart word by word, because every clause is a design decision that shows up later as a limitation:

Key idea — the definition

HDFS is a filesystem designed for storing very large files with streaming data access patterns, running on clusters of commodity hardware.

Four assumptions follow from that sentence, and they are the ones to quote at the exam:

AssumptionWhat the slides say
Very large files A typical file in HDFS is gigabytes to terabytes in size. There are Hadoop clusters running today that store petabytes of data.
Streaming access Applications that run on HDFS need streaming access to their data sets. HDFS is designed more for batch processing rather than interactive use. The emphasis is on high throughput of data access rather than low latency.
Write-once-read-many HDFS applications need a write-once-read-many access model. A file once created, written and closed need not be changed. This assumption simplifies data coherency issues and enables high throughput data access.
Failure is the norm Hardware failure is the norm rather than the exception. Therefore, detection of faults and quick, automatic recovery from them is a core architectural goal of HDFS.

Read those four together and you get a system that is unapologetically specialised. It will happily stream a terabyte to a hundred machines at once; it will refuse, politely but firmly, to be a database. The fourth assumption is the cultural one: on a commodity cluster (Chapter 2) failure is not an emergency to be handled by an operator, it is a routine event that the filesystem must absorb by itself while the job keeps running.

2. The three goals of a DFS

Before looking at any implementation detail, the deck states what any distributed file system must achieve, and — crucially — the typical solution for each. This table is the architecture of HDFS in miniature; everything in sections 3 to 8 is one of these three rows expanded.

GoalRequirementTypical solution
Big data support Files may be bigger than single disks Split files into smaller blocks, store blocks on different machines
Abstraction Interact with disks on different machines as if it were one, unified disk Master-slave architecture with high availability
Fault-tolerance Disks can fail, machines can be unreachable, but data should always be available Store multiple copies of each block (replication)
For the exam

If you are asked “what are the goals of a distributed file system”, answer with the three pairs — goal, requirement, solution. Naming the goal without the mechanism (blocks, master-slave with HA, replication) is half an answer.

3. Blocks, and why they are so big

Why blocks at all? Two reasons. Files can be larger than disks, so a file must be breakable across machines. And by splitting files into blocks, storage management is simplified and replication is easier: you replicate a fixed-size unit, not a file of arbitrary length.

The block is the minimum amount of data read or written. The scale of that unit is where HDFS parts company with everything you have used before:

LevelTypical block size
Disk blocksnormally 512 B
Filesystem blockstypically a few KB
HDFS blocksbetween 64 MB and 1 GB — default 128 MB

An important detail that surprises people: if a file is smaller than a block, it will occupy the necessary disk space, not the full block size. The 128 MB is a maximum unit of splitting, not a minimum unit of allocation.

Why this big?

Because large files split into many small blocks require a huge number of seeks. The slides make it concrete: with a block size of 4 KB, a 1 GB file is 250,000 blocks — and therefore up to 250,000 seeks. On spinning media a seek costs milliseconds while streaming a megabyte costs a fraction of that, so a small block size turns a throughput problem into a latency problem. Big blocks amortise the seek over a large sequential read, which is exactly what the “streaming access” assumption asked for.

Editor’s note — why not bigger?

The slide asks “why not bigger?” and leaves it hanging. The answer is already in the course, one chapter ahead: in MapReduce the optimal split size is the DFS block size and one map task is created per split (Chapter 4). Bigger blocks therefore mean fewer, longer tasks: less parallelism, coarser load balancing, and a slower recovery when one task has to be redone. The block size is a negotiation between seek amortisation and parallelism — 128 MB is where the two curves cross for typical hardware.

4. NameNode and DataNodes

Nodes in an HDFS cluster operate in a master-slave pattern, and the split of responsibilities is unusually clean: one machine knows where everything is, and all the others hold the actual bytes.

RoleResponsibilities
NameNode (NN)
the master
Persistently maintains the filesystem tree and all files’ and directories’ metadata.
Keeps in memory the location of each block for a given file — the block pool.
DataNodes (DNs)
the slaves
Store and retrieve blocks.
Periodically report to the NN with the list of blocks they are storing; heartbeats signal their active state (e.g., every 10 minutes).

Two words in that table carry a lot of weight. Persistently: the tree and the metadata survive a restart, they are written to disk. In memory: the block pool — which block lives on which DataNode — is not persisted, it is rebuilt from the reports the DataNodes send. That is why a NameNode restart is slow (section 5) and why the amount of RAM on the NameNode is the hard ceiling on the number of files in the cluster (sections 6 and 9).

5. The single point of failure and its remedies

The design has an obvious weak spot, and the course names it directly: the NN is a single point of failure. Without it the filesystem cannot be used — there is no way to reconstruct the files from the blocks in the DataNodes. The blocks are still there, intact and replicated, and they are useless: nothing records which block belongs to which file, in which order.

Three remedies, in increasing order of cost and effectiveness:

The NN writes its persistent state to multiple filesystems, preventing loss of data. This protects the metadata from disappearing, but it does nothing for downtime: somebody still has to bring a NameNode back up from those files.

A separate machine regularly connects with the primary NN to build snapshots of its persistent data and saves them to local or remote directories. These checkpoints can be used to restart a failed NN without having to replay the entire journal of filesystem actions.

Still, restarting a NN could take 30+ minutes. Note the name is misleading: the secondary NameNode is not a standby that takes over, it is a checkpointing helper.

High Availability (HA) indicates a system that can tolerate faults: the service never stops while the fault — hardware or software — is detected, reported, masked and repaired off-line. Most big data tools offer HA, not just HDFS.

HA is supported by configuring two separate machines as NameNodes: one active, one standby. If the active NN fails, the standby takes over. The standby keeps its metadata up to date by:

  • reading the edit log files written by the active NN in a shared storage — edit logs are typically stored in Journal Nodes, which are themselves replicated;
  • receiving the data block locations and the heartbeats directly from the DataNodes, which are configured to report to both NameNodes.

An HA solution recovers far more efficiently, but requires more resources and communications.

For the exam

The second bullet of HA is the one people forget. It is not enough for the standby to read the edit log: the edit log carries the namespace, not the block locations, because block locations live only in memory and are rebuilt from DataNode reports. So the DataNodes must report to both NameNodes — otherwise the standby would take over with a complete file tree and no idea where any block physically is, and would need the same 30+ minutes to find out.

6. Federation

High availability solves the failure problem of a single master. It does not solve the capacity problem: the size of the block pool is limited by the memory size of the NameNode, which may incur scaling issues on large clusters with many files.

The solution is federation: configure additional NameNodes, where each NN manages a portion of the filesystem (a namespace), and the NNs are independent of each other. The DataNodes are shared; the namespaces are not.

AdvantageWhy federation gives it
PerformanceA single NN can become a bottleneck for data access; several NNs share the metadata traffic
AvailabilityIn case of one NN failure, the others are available
ScalabilityBy storing metadata in memory, the size of the NN heap limits the number of files and blocks — more heaps, more files
Maintainability, security & flexibilityEach namespace is isolated and not aware of the others
Watch out

Federation and High Availability answer different questions and are not alternatives. HA gives you two NameNodes serving the same namespace (one active, one standby) so the service survives a crash. Federation gives you several NameNodes serving different namespaces so the metadata fits in memory. A large production cluster runs both: each federated namespace has its own active/standby pair.

7. Replication and block placement

Each data block is independently replicated at multiple DataNodes in order to improve performance and robustness. Two properties matter: replication is aware of the cluster topology, and for each data block the NN stores the list of DNs storing it.

The default replication factor is 3, and the placement is not random. Memorise the rule — it is a classic exam question:

ReplicaPlacement ruleWhat it buys
Replica 1On the node (n1) where the client issued the write command, if the client resides within the clusterThe first copy costs no network traffic at all
Replica 2On a node (n2) in a rack (r2) different from the one of n1 — off-rackSurvives the loss of an entire rack (switch, power)
Replica 3On a node different from n2 but belonging to the same rack r2The third copy is written over the cheap intra-rack link, not a second expensive inter-rack hop

Replicas can be rebalanced when nodes are added or become unavailable.

Read the three rules as a single sentence and the reasoning appears: one copy where it is free, one copy far enough to survive a rack failure, and the third copy next to the second because the expensive hop has already been paid. Placing replica 3 in a third rack would double the inter-rack traffic to buy protection against the simultaneous loss of two racks — a trade the default configuration refuses. This is Chapter 2 speaking again: intra-rack bandwidth is much greater than inter-rack bandwidth, so the topology of the network dictates the topology of the data.

HDFS block placement with the default replication factor of 3 A file split into three blocks; the three replicas of block B1 are placed on the writing node in rack 1, and on two different nodes in rack 2. The NameNode holds only metadata, and the client exchanges data directly with the DataNodes. NameNode tree + metadata (on disk) block pool (in memory) file.dat = B1 B2 B3 client, inside the cluster, running on node n1 metadata only — never the data Rack r1 Rack r2 n1 (writer) B1 replica 1 n2 n3 n4 n5 B1 replica 2 n6 B1 replica 3 n7 n8 1 inter-rack hop (costly) then intra-rack (cheap)
Plate 3.1 — Default block placement. Replica 1 is free (it stays on the writing node), replica 2 pays the one expensive inter-rack hop to survive a rack failure, and replica 3 rides the cheap intra-rack link next to replica 2. Meanwhile the NameNode only ever exchanges metadata: the bytes travel client↔DataNode.

Redundancy explorer: replication vs erasure coding

Move the sliders to see how much raw storage a logical dataset actually consumes, and what you buy with it. The erasure coding defaults are the ones in the slides: 3 parity chunks every 6 data chunks.

8. Erasure Coding

Replication is simple and fast, and it is also brutally expensive: the default factor of 3 means 200% overhead — you buy three disks to store one disk of data. HDFS therefore supports Erasure Coding (EC) as an alternative, a mechanism similar to RAID 5-6.

Instead of replicating each block, blocks are striped. A stripe is a sequence of chunks, composed of m data chunks + k parity chunks. Note the consequence the slides call out explicitly: the data in a block may end up in multiple stripes.

Erasure coding compared with three-way replication Above, three identical copies of one block occupying three nodes. Below, a stripe of data chunks plus parity chunks spread across nodes in two racks, with a much smaller total footprint. 3x REPLICATION — store 1 unit, occupy 3 B1 B1 B1 redundancy 200% ERASURE CODING — a stripe of m data chunks + k parity chunks (default 6 + 3) C1 C2 C3 C4 C5 C6 P1 P2 P3 6 data chunks 3 parity chunks → redundancy 50% Rack r1 C1 C4 P1 C5 Rack r2 C2 C3 P2 C6 P3 no single node holds a whole block any more → data locality is lost
Plate 3.2 — Replication buys locality with space; erasure coding buys space with locality. Once a block is striped, reading it means fetching chunks from several machines, which is exactly what MapReduce and Spark spend their scheduling effort trying to avoid.

EC works best for cold datasets with relatively low I/O activities. The trade-offs, exactly as the slides list them:

Main advantagesMain disadvantages
  • Sensibly reduces data redundancy, from 200% to 50% with default setups. If m parity chunks are created every k chunks, the redundancy factor is m/k; the default is 3 parity chunks every 6 chunks.
  • Faster writes, because chunks are distributed.
  • Higher CPU cost in both reading and writing.
  • Longer recovery time in case of failure.
  • Loss of data locality: to read a block you need to get its stripes from multiple machines.
For the exam

Be ready to compute the redundancy. With replication factor r the overhead is (r−1)×100%, so r=3 gives 200%. With EC the redundancy factor is m/k parity over data, so the default 3-every-6 gives 50%. And be ready to say when to use EC: cold data with low I/O — never for a dataset that a Spark job scans repeatedly, because you would pay CPU and lose locality on every pass.

9. Where HDFS is not a good fit

The deck is refreshingly honest about the limits. “Although this may change in the future, there are areas where HDFS is not a good fit today.”

Weak spotWhy
Low-latency data access HDFS is optimized for delivering a high throughput of data, and this may be at the expense of latency. Applications that require access to data in the tens of milliseconds range will not work well with HDFS.
Lots of small files The limit to the number of files in a filesystem is governed by the amount of memory on the NameNode, because it holds filesystem metadata in memory. Storing millions of files is feasible; billions is beyond the capability of current hardware.
Key idea

Both weaknesses are the direct cost of the design choices in section 1. Big blocks and streaming access give throughput and take latency. A single in-memory block pool gives one authoritative, fast metadata lookup and takes a ceiling on the file count. When your workload hits either wall, the answer is not to tune HDFS — it is to use a different storage service, which is why the course later devotes an entire chapter to NoSQL systems (Chapter 11) built for exactly those two profiles.

10. HDFS I/O: the read path

An application client wishing to read a file (or a portion thereof) must first contact the NN to determine where the actual data is stored. In response, the NN returns:

The client then contacts the DataNode to retrieve the data. Blocks are themselves stored on standard single-machine file systems: HDFS lies on top of the standard OS stack — a DataNode is, at bottom, an ordinary Linux box holding ordinary files.

Key idea — three features of the design

Data is never moved through the NN. All data transfer occurs directly between clients and DataNodes. Communications with the NN only involve transfer of metadata.

This is what keeps a single master from becoming a bandwidth bottleneck: the master is on the control plane, never on the data plane. A thousand clients streaming a terabyte each cost the NameNode nothing but a few thousand small lookups.

11. Why big data file formats

Storage is only half the story. Data usually comes in standard file formats — structured CSV files, semi-structured JSON files, unstructured textual files — and those formats are, for this setting, actively bad. Three properties separate a big data format from a text file:

PropertyWhat it givesWhat you pay without it
Binary serialization Compact streams of bytes occupy less space Large overhead for storing data as text: more space required, and type-conversion required when reading and writing
Splittability Metadata headers allow skipping unnecessary I/O You cannot access single portions of files
Compression Block-level (distinctly compress the single blocks) and record-level (distinctly compress the single records within a block) The whole file must be compressed and decompressed for reading

Splittability deserves a second look, because it connects directly to everything in Chapter 4. If a file cannot be split, the framework cannot create one map task per block: the whole file has to go to a single task, and the cluster idles. A gzipped CSV is the classic trap — perfectly compressed, and perfectly unparallelisable.

12. Row-oriented vs column-oriented

Everything starts from a physical fact: disks store data sequentially — they are 1-dimensional. Tables cannot be directly represented, so cells are stored either row-by-row or column-by-column. That single choice determines which workload the file is good at.

Row-oriented versus column-oriented layout on a one-dimensional disk The same three-column table serialised twice: row by row, where an analytical query on one column must touch every record, and column by column, where it reads one contiguous run. the table dept name salary d1 n1 s1 d2 n2 s2 d3 n3 s3 the query needs only the salary column ROW-ORIENTED on disk d1 n1 s1 d2 n2 s2 d3 n3 s3 3 useful cells out of 9 read COLUMN-ORIENTED on disk d1 d2 d3 n1 n2 n3 s1 s2 s3 one contiguous run homogeneous → compresses better too
Plate 3.3 — The same table, serialised two ways. Column orientation does not make the disk faster; it makes the query read less, and it puts values of the same type next to each other, which is what makes the compression ratios so good.

Row orientation is better for transactional workloads (OLTP), because applications work on a row basis — updating a customer’s profile, adding a new order. Column orientation is better for analytical workloads (OLAP), because analyses are done on a columnar basis — the average salary of employees by department.

The advantages of columnar formats, as listed in the deck:

The format families

Row-oriented. Binary key-value pairs, specifically designed to work with MapReduce (others are available, e.g. MapFiles). No support besides Java. Mostly used for intermediate data storage and for packing many small files into a single SequenceFile — which is the classic workaround for the “lots of small files” weakness of section 9.

Row-oriented. Apache Thrift (Facebook) and Protocol Buffers (Google) were born to facilitate communication between programs (RPC). They offer multi-language support, but their key features are available only through external libraries. Their schema is similar to Parquet’s but more complex, defining also Maps, Lists and Sets.

Row-oriented. Apache Avro addresses the major drawbacks of Writable, the default serialization of SequenceFiles. Multi-language support, more integrated with Hadoop, supports schema evolution with no need to compile code.

The distinctive property: in Avro, data is always serialized with its schema. Each file is self-describing, and different schemas can be used for serialization and deserialization — Avro will autonomously handle the missing, extra or modified fields.

Column-oriented. ORC Files (Optimized Record Columnar Files) were the first columnar format on Hadoop, initially as RC Files. No support for schema evolution, and originally designed for Hive alone.

Column-oriented. Apache Parquet is general-purpose and supports schema evolution. It is the format the rest of this chapter — and most of the practical work in the course — is about.

13. Parquet: history and data model

Parquet is a columnar storage format for efficient querying of nested structures in a flat format. Hold on to the tension in that sentence: nested structures, stored flat. Sections 14 and 15 exist entirely to resolve it.

Parquet ensures interoperability through several frameworks (Spark, MapReduce, etc.) and query engines (Hive, Impala, Pig, Presto, etc.) — which is precisely why it became the default currency of the data lake, and why Chapter 12 will find it again underneath the lakehouse.

The data model

The model is deliberately small — three concepts:

With only those three frequencies you can express the collection types other systems build in: a list is a repeated field, a map is a repeated group of key-value pairs. The slides show both:

message ExampleList {
  repeated string list;
}

Data [ "a", "b", "c", ... ] is stored as:

{
  list: "a",
  list: "b",
  list: "c",
  ...
}
message ExampleMap {
  repeated group map {
    required string key;
    optional string value;
  }
}

Data [ "AL" => "Alabama", ... ] is stored as:

{
  map: {
    key: "AL",
    value: "Alabama",
  },
  ...
}

14. Unnesting: definition and repetition levels

The problem. Can we store nested data structures in columnar format? We need to map the schema to a list of columns in a way that we can write records to flat columns and read them back to their original nested data structure. And with repeatable fields, how do I know when one record ends and another begins?

The starting rule is simple: one column per primitive-type field in the schema — if we represent the schema as a tree, the primitive types are the leaves of this tree. But a flat column of values loses two things: which nulls happened at which nesting depth, and where each repeated list starts.

Key idea — the solution

Each value is associated with two integers: repetition level and definition level. These integers allow to fully reconstruct the nested structures while still being able to store each primitive separately.

Definition level

Consider a column a.b.c where all fields are optional and can be null:

message ExampleDefLevel {
  optional group a {
    optional group b {
      optional string c;
    }
  }
}

When c is defined, then necessarily a and b are defined too. But when c is null, we need to save the level of the null value — is a itself missing, or is a present with b missing, or are both present and only c missing? There are 3 nested optional fields, so the maximum definition level is 3.

Now make b required:

message ExampleDefLevel {
  optional group a {
    required group b {
      optional string c;
    }
  }
}

The maximum definition level is now 2, as b does not need one: a required field cannot be missing on its own, so it contributes no level. Remember: the smaller the definition levels, the less the required bits. Marking a field required is not documentation, it is a storage optimisation.

Repetition level

The presence of repeated fields requires storing when new lists are starting in a column of values. The repetition level indicates at which level we have to create a new list for the current value — it can be seen as a marker of when to start a new list and at which level:

Repetition levelMeaning
0marks every new record; implies creating a new level1 and level2 list
1marks every new level1 list; implies creating a new level2 list as well
2marks every new element in a level2 list

The AddressBook example

Take the schema and the two records from the slides:

message AddressBook {
  required string owner;
  repeated string ownerPhoneNumbers;
  repeated group contacts {
    required string name;
    optional string phoneNumber;
  }
}
AddressBook {
  owner: "Julien Le Dem",
  ownerPhoneNumbers: "555 123 4567",
  ownerPhoneNumbers: "555 666 1337",
  contacts: {
    name: "Dmitriy Ryaboy",
    phoneNumber: "555 987 6543",
  },
  contacts: {
    name: "Chris Aniszczyk"
  }
}
AddressBook {
  owner: "A. Nonymous"
}

For the column contacts.phoneNumber, the definition levels are:

SituationDefinition level
Defined phone number2
A contact without phone number1
No contacts at all0

So that column, stripped of everything except its two integers and its value, holds exactly what is needed to rebuild:

AddressBook {
  contacts: {
    phoneNumber: "555 987 6543"
  }
  contacts: { }
}
AddressBook { }

And here is the reconstruction, iterating through the column, exactly as the slides walk it:

(R, D, Value)How it is read
R=0, D=2, Value = "555 987 6543" R = 0 means a new record: we recreate the nested records from the root until the definition level (here 2). D = 2, which is the maximum: the value is defined and is inserted.
R=1, D=1 R = 1 means a new entry in the contacts list at level 1. D = 1 means contacts is defined but not phoneNumber: we just create an empty contact.
R=0, D=0 R = 0 means a new record: we create the nested records from the root until the definition level (here 0). D = 0 means contacts is actually null: we only have an empty AddressBook.

Rebuild the AddressBook column, one triple at a time

Step through the three (R, D, value) triples and watch the record take shape. This is the exact walkthrough from the slides, made clickable.

For the exam

Two sentences that answer most level questions. Definition level = how deep the path is actually defined, so it tells you where a null happened; only optional and repeated fields contribute to it, never required. Repetition level = at which level a new list starts, so 0 always means “a new record begins here”. Every value in a column carries both; the pair is enough to reconstruct the nesting without storing any structure.

15. Two exercises on levels

Both exercises use the same schema. Try each one before opening the solution — this is exactly the kind of question that appears at the exam, and reading the answer is not the same as producing it.

message Person {
  required string name;
  repeated group orders {
    required string item;
    optional int quantity;
  }
}

Exercise 1 — define the columns

Given the following data, define the columns (DL, RL, Value) for item and quantity:

[
  { "name": "Alice",
    "orders": [
      { "item": "Book", "quantity": 1 },
      { "item": "Pen", "quantity": null }
    ]
  },
  { "name": "Bob",
    "orders": null
  },
  { "name": "Carol",
    "orders": [
      { "item": "Notebook", "quantity": 3 }
    ]
  },
  { "name": "Dan",
    "orders": []
  }
]
Solution to Exercise 1
DLRLitemDLRLquantity
10Book201
11Pen11null
00Null00Null
10Notebook203
00null00null

Things to notice. Alice contributes two rows: the first has RL=0 because a new record starts, the second RL=1 because it is a new entry in the orders list. Since item is required inside a repeated group, its maximum definition level is 1, while quantity is optional so its maximum is 2 — which is why a present quantity shows DL=2 and a null quantity inside an existing order shows DL=1. Bob (orders null) and Dan (orders empty) both collapse to (0,0,null): from the point of view of the column, an absent list and an empty list are indistinguishable.

Exercise 2 — rebuild the messages

Now go the other way. Rebuild the messages starting from the (DL, RL, Value) of item and quantity:

DLRLitemDLRLquantity
10A10null
11B21101
00null00null
10C20102
00null00null
10D20103
11E11null
11F21104
11G11null
Solution to Exercise 2
[
  { "orders": [
      { "item": "A", "quantity": null },
      { "item": "B", "quantity": 101 }
  ]},
  { "orders": null },
  { "orders": [
      { "item": "C", "quantity": 102 }
  ]},
  { "orders": null },
  { "orders": [
      { "item": "D", "quantity": 103 },
      { "item": "E", "quantity": null },
      { "item": "F", "quantity": 104 },
      { "item": "G", "quantity": null }
  ]}
]

The method is mechanical: every RL=0 opens a new Person; every RL=1 appends another order to the current person. The last record therefore swallows D, E, F and G, because after D the repetition level never returns to 0. The definition level then decides whether the value is present (DL=2 for quantity) or null at that depth.

Watch out

The most common mistake in Exercise 2 is to read the item and quantity columns as if they were rows of a table read independently. They are two separate columns of the same records: you rebuild the record skeleton from the repetition levels (which agree across columns), and then fill each leaf using its own definition level.

16. Encodings and the physical file layout

The cost of levels, and how it is paid back

Definition and repetition levels cause overhead: each primitive type corresponds to three columns (the value, its definition level, its repetition level). Two facts keep that overhead small:

Then compression takes care of condensing data efficiently:

EncodingWhere it is useful
Bit PackingRepetition and definition levels, dictionary keys
Run Length Encoding (RLE)The definition level of sparse columns
Dictionary EncodingColumns with few (< 50k) distinct values

The definition of new encodings is supported.

The file format hierarchy

The Parquet physical layout A Parquet file containing two row groups; each row group holds one column chunk per column; each column chunk is divided into pages; the file metadata sits at the end. FILE — an HDFS file ROW GROUP 1 — a horizontal partitioning of the rows (512MB–1GB recommended) column chunk: dept page page page column chunk: name page page column chunk: salary page page contiguous in the file ROW GROUP 2 column chunk: dept column chunk: name column chunk: salary FILE METADATA schema, and where every column chunk lives — this is what lets a reader skip whole chunks optimized read setup: 1GB row groups · 1GB HDFS block size · 1 HDFS block per HDFS file a page is the indivisible unit of compression and encoding
Plate 3.4 — File → row group → column chunk → page. The row group is the unit of horizontal partitioning, the column chunk the unit of columnar locality, and the page the unit of compression. Sizing the row group to the HDFS block is what keeps a “read one row group” from becoming a multi-machine operation.
LevelDefinition from the slides
FileAn HDFS file. Includes the metadata for the file.
Row groupA logical horizontal partitioning of the data into rows. No physical structure is guaranteed for a row group. Contains a column chunk for each column in the dataset.
Column chunkA chunk of the data for a particular column. These live in a particular row group and are guaranteed to be contiguous in the file.
PageColumn chunks are divided up into pages. A page is an indivisible unit in terms of compression and encoding.

Configuration

Row group size: larger row groups → larger column chunks → larger sequential I/O. Large row groups (512MB–1GB) are recommended, since an entire row group might need to be read and we want it to completely fit on one HDFS block. The optimized read setup named in the deck is: 1GB row groups, 1GB HDFS block size, 1 HDFS block per HDFS file.

Data page size: data pages should be considered indivisible. Smaller data pages → more fine-grained reading (e.g., single row lookup). Larger page sizes → less space overhead (fewer page headers) and less parsing overhead (fewer headers to process).

Key idea — the whole chapter in one line

Every number in this chapter is the same negotiation seen from a different level. A 128 MB HDFS block balances seeks against parallelism; a 1 GB row group is sized to fit one block so a columnar read stays on one machine; a page is small enough to compress well and large enough that its header is not the payload. Storage design at this scale is not about clever data structures, it is about choosing the size of the unit at each level.

Test your knowledge

State the four design assumptions of HDFS and one consequence of each.

1. Very large files (gigabytes to terabytes; clusters store petabytes) → the block is huge, 128 MB by default. 2. Streaming data access, designed for batch rather than interactive use, emphasis on high throughput rather than low latency → HDFS is a poor fit for applications needing tens of milliseconds. 3. Write-once-read-many: a file once created, written and closed need not be changed → this simplifies data coherency issues and enables high throughput. 4. Hardware failure is the norm rather than the exception → detection of faults and quick automatic recovery is a core architectural goal, which is why replication is built in rather than bolted on.

Why are HDFS blocks so much larger than filesystem blocks, and what stops them from being larger still?

Large files split into many small blocks require a huge number of seeks: with a 4 KB block a 1 GB file needs 250,000 seeks. Big blocks amortise the seek over a long sequential read, which is what streaming access needs. What stops them from growing further is parallelism: in MapReduce the optimal split size is the DFS block size and one map task is created per split, so bigger blocks mean fewer, longer tasks — less concurrency, worse load balancing, and a more expensive re-execution when a task fails.

What exactly does the NameNode keep on disk, and what only in memory? Why does the distinction matter?

On disk, persistently: the filesystem tree and all files’ and directories’ metadata. In memory only: the location of each block for a given file — the block pool, rebuilt from the block lists the DataNodes periodically report. It matters twice: a restarted NameNode has the namespace immediately but must wait for DataNode reports before it can serve data (part of why a restart takes 30+ minutes), and the NameNode heap size is the hard limit on the number of files and blocks the cluster can hold.

Compare the three answers to the NameNode single point of failure.

Backup: the NN writes its persistent state to multiple filesystems, preventing loss of data but not downtime. Secondary NameNode: a separate machine regularly builds snapshots (checkpoints) of the primary NN persistent data, which allow restarting a failed NN without replaying the entire journal of filesystem actions — but restarting could still take 30+ minutes; it is a checkpointing helper, not a standby. High Availability: two machines configured as NNs, one active and one standby; the standby stays current by reading the edit logs the active writes to shared storage (typically replicated Journal Nodes) and by receiving block locations and heartbeats directly from the DataNodes, which report to both. HA recovers far better but requires more resources and communications.

What problem does federation solve, and how is it different from High Availability?

Federation solves a capacity problem: the block pool size is limited by the NameNode memory, which causes scaling issues on large clusters with many files. It configures additional NameNodes, each managing a portion of the filesystem (a namespace), independent of each other — giving performance, availability, scalability, maintainability, security and flexibility, since each namespace is isolated and unaware of the others. HA solves a failure problem instead: two NameNodes serving the same namespace, one active and one standby. They are complementary, not alternatives.

Give the default replica placement rule and justify each of the three positions.

With replication factor 3: replica 1 on the node where the client issued the write (if the client resides within the cluster) — it costs no network traffic; replica 2 on a node in a different rack (off-rack) — this is what survives the loss of a whole rack; replica 3 on a node different from the second but in the same rack as the second — the expensive inter-rack hop has already been paid, so the third copy travels the cheap intra-rack link. Replication is topology-aware, and replicas can be rebalanced when nodes are added or become unavailable.

What is erasure coding, what does it save, and when should you not use it?

An alternative to simple replication, similar to RAID 5-6: instead of replicating each block, blocks are striped, where a stripe is a sequence of chunks made of m data chunks + k parity chunks (the data of one block may end up in multiple stripes). It reduces redundancy from 200% to 50% with default setups — the redundancy factor is m/k and the default is 3 parity chunks every 6 chunks — and gives faster writes since chunks are distributed. Do not use it for hot data: it costs more CPU on both read and write, recovery after a failure takes longer, and data locality is lost because reading a block requires fetching stripes from multiple machines. It works best for cold datasets with relatively low I/O activity.

Describe the HDFS read path and the three design features it illustrates.

The client first contacts the NameNode, which returns the relevant block id and the location (which DataNode) where the block is held; the client then contacts the DataNode directly to retrieve the data. Blocks are themselves stored on standard single-machine filesystems, so HDFS lies on top of the standard OS stack. The three features: data is never moved through the NN; all data transfer occurs directly between clients and DataNodes; communications with the NN only involve transfer of metadata. This keeps the single master off the data plane, so it never becomes a bandwidth bottleneck.

Why are CSV and JSON poor choices at this scale? Name the three properties a big data format adds.

Binary serialization: compact streams of bytes occupy less space, whereas text costs extra space plus type conversion on every read and write. Splittability: metadata headers allow skipping unnecessary I/O; without it you cannot access single portions of files, which also means the framework cannot create one task per block. Compression: block-level compresses single blocks distinctly and record-level compresses single records within a block, whereas otherwise the whole file must be compressed and decompressed just to read part of it.

Row or column orientation: which for OLTP, which for OLAP, and what are the three advantages of columnar?

Disks are 1-dimensional, so cells go either row-by-row or column-by-column. Row orientation suits OLTP, because applications work on a row basis (updating a customer profile, adding an order). Column orientation suits OLAP, because analyses are done on a columnar basis (average salary by department). Columnar advantages: better compression (data is more homogeneous, type-specific encodings, savings very noticeable at cluster scale); reduced I/O for analytical queries (only a subset of columns is read, unnecessary deserialization skipped); the ability to operate on encoded data, e.g. with dictionary encoding.

What makes Avro distinctive among row-oriented formats?

Avro addresses the major drawbacks of Writable (the default serialization of SequenceFiles), has multi-language support, is more integrated with Hadoop, and supports schema evolution with no need to compile code. Its defining property is that data is always serialized with its schema: each file is self-describing, and different schemas can be used for serialization and deserialization, with Avro autonomously handling missing, extra or modified fields. Contrast with SequenceFiles (no support besides Java) and ORC (no support for schema evolution).

What problem do definition and repetition levels solve, and what does each of them encode?

They solve the unnesting problem: storing nested structures in a columnar format such that records can be written to flat columns and read back into their original nested structure, and — with repeated fields — knowing when one record ends and another begins. There is one column per primitive field (the leaves of the schema tree), and each value carries two integers. The definition level records how deep the path is actually defined, i.e. at which level a null occurred; only optional and repeated fields contribute to it, so making a field required lowers the maximum level and therefore the number of bits. The repetition level records at which level a new list starts: 0 marks a new record (implying a new level1 and level2 list), 1 marks a new level1 list (implying a new level2 list), 2 marks a new element in a level2 list.

In the AddressBook example, what are the definition levels for contacts.phoneNumber, and how is the column read back?

A defined phone number has DL 2, a contact without a phone number has DL 1, and no contacts at all has DL 0. Reconstruction: R=0, D=2, "555 987 6543" — R=0 means a new record, so nested records are recreated from the root down to the definition level (2), and since D is the maximum the value is defined and inserted. R=1, D=1 — a new entry in the contacts list at level 1, with contacts defined but phoneNumber not, so an empty contact is created. R=0, D=0 — a new record, and D=0 means contacts is actually null, so we only have an empty AddressBook.

Describe the Parquet physical hierarchy and the recommended sizing.

File (an HDFS file, including the metadata for the file) → row group (a logical horizontal partitioning of the data into rows, with no guaranteed physical structure, containing one column chunk per column) → column chunk (the data for a particular column within a row group, guaranteed contiguous in the file) → page (an indivisible unit in terms of compression and encoding). Sizing: large row groups of 512MB–1GB are recommended because an entire row group might need to be read and should fit on one HDFS block; the optimized read setup is 1GB row groups, 1GB HDFS block size, one HDFS block per HDFS file. For pages, smaller means more fine-grained reading (single row lookup), larger means less space and parsing overhead from headers.

Levels cost storage. What keeps that cost small, and which encodings help?

Each primitive type corresponds to three columns (value, definition level, repetition level), but the level values are bound by the depth of the schema, so only a few bits are used — and when all fields are required in a flat schema, levels can be omitted entirely. Then compression condenses the rest: Bit Packing for repetition and definition levels and dictionary keys, Run Length Encoding for the definition level of sparse columns, and Dictionary Encoding for columns with fewer than about 50k distinct values. New encodings can also be defined.