The idea: one map keeper, many byte keepers

The Google File System (GFS, 2003) and its open-source copy, the Hadoop Distributed File System (HDFS), store huge files on hundreds of cheap machines that fail all the time. They split the job in two. One NameNode (GFS: master) keeps only the metadata: which files exist and where their pieces are. Many DataNodes (GFS: chunkservers) keep the bytes. File data never passes through the NameNode.

The file /logs/day1.log, 200 MB, is cut into blk_1001 of 128 MB and blk_1002 of 72 MB. The NameNode holds the namespace and the block map. Rack /r1 has DN1 and DN2, rack /r2 has DN3 and DN4. blk_1001 is on DN1, DN3, DN4; blk_1002 is on DN1, DN4, DN3
The NameNode knows which blocks make a file and where they are; the DataNodes only store blocks.

The page has four DataNodes on two racks and one file, /logs/day1.log, 200 MB.

Blocks and replicas

A file is cut into blocks of dfs.blocksize = 128 MB (GFS used 64 MB chunks). The 200 MB file becomes two blocks: 128 MB and 72 MB. The last block only takes the space it needs. Big blocks keep the metadata small: the NameNode holds every file and block in RAM, about 150 bytes each, so one NameNode can track hundreds of millions of blocks.

Each block is stored dfs.replication = 3 times as replicas on different DataNodes. The NameNode's block map says which DataNodes hold each block. It is not saved to disk: DataNodes report what they hold when they start and after each new block (blockReceived), and the NameNode rebuilds the map from that.

Rack-aware placement

Machines in one rack share a switch and a power feed, so a whole rack can fail at once. The default placement policy puts the 1st replica on the writer's own node (or a random one if the writer is outside the cluster), the 2nd on a different rack, and the 3rd on the same rack as the 2nd, on another node. Each block then lives on two racks, but only one copy crosses between racks during the write.

Left: placement of blk_1001 with the writer on DN1: replica 1 on DN1 in rack r1, replicas 2 and 3 on DN3 and DN4 in rack r2. Right: rack r2 fails; DN1 still has the block, so the NameNode copies it to DN2 and nothing is lost
Two racks per block: losing a whole rack still leaves a copy to read and to copy from.

Demo 5: a whole rack fails, no block is lost kills DN3 and DN4. Every block still has its copy on DN1, and the NameNode copies it to DN2. With only two live nodes the blocks stay at 2 of 3 replicas.

The write pipeline

A write is a few small RPCs to the NameNode and a big stream to the DataNodes (Demo 1: write a 200 MB file):

  1. create: the NameNode adds the path, marked under construction, and gives the client a lease (one writer per file).
  2. addBlock for each block: the NameNode picks the target DataNodes and returns a new block id.
  3. The client streams the block as 64 KB packets to the first DataNode. Each node writes a packet to disk and forwards it to the next: a pipeline. Acks flow back the other way; a node acks only when it and everyone after it have the packet.
  4. When the block is full, each DataNode finalizes its replica and sends blockReceived; the NameNode records the locations.
  5. complete: the file is closed and can be read by everyone.
A time-down sequence with columns client, DN1, DN3, DN4 and NameNode. The client sends addBlock to the NameNode and gets blk_1001 with DN1, DN3, DN4. Packets go client to DN1 to DN3 to DN4. Acks go DN4 to DN3 to DN1 to client. Then DN1, DN3 and DN4 send blockReceived to the NameNode
Data flows down the chain and acks flow back up; the NameNode only sees two small messages per block.

A pipeline uses each node's network link once: the client sends the block one time, not three. Files are write-once: a second create on the same path fails with FileAlreadyExistsException (press Write File twice). HDFS can append, but never change bytes in the middle of a file.

Reads and locality

To read, the client calls getBlockLocations. The NameNode returns every block with its DataNodes sorted by network distance: the same node is 0, the same rack 2, another rack 4. The client then reads each block straight from the first DataNode on the list (Demo 2: read it back: both blocks come from DN1, the client's own node, with no network at all). This is why MapReduce and Spark try to run each task on a node that holds its block.

If a DataNode does not answer, the client puts it on the stream's dead-nodes list and tries the next location (Demo 4: read while a DataNode is down). Each replica also carries a CRC checksum per 512 bytes; a corrupt replica is reported to the NameNode and the client reads another one. With a client outside the cluster every DataNode is equally far (Demo 6).

Heartbeats and re-replication

Each DataNode sends a heartbeat every 3 s (dfs.heartbeat.interval). The NameNode answers it, and the answer is how it gives orders. A node that has been silent for 2 × dfs.namenode.heartbeat.recheck-interval (5 min) + 10 × 3 s = 630 s is declared dead. The wait is long on purpose: a node that restarts within a few minutes should not trigger terabytes of copying.

When a node is declared dead, its replicas leave the block map. Each block that now has fewer than its replication factor is under-replicated; the NameNode sends a DNA_TRANSFER command to a DataNode that still has it, and that node copies the block to another one. No client takes part.

what failswho noticeswhat happens
DataNode, between writesNameNode, after 630 sreplicas copied from survivors
DataNode, during a readclient, at oncenext location on the list
DataNode, during a writepipeline, at oncepipeline recovery (below)
a whole rackNameNode, after 630 scopies from the other rack
every replica of a blockclientBlockMissingException

Pipeline recovery and generation stamps

If a DataNode dies in the middle of a write, the node before it gets no ack and sends an error ack that names it (Demo 3: a DataNode dies in the write pipeline). The client removes it, asks the NameNode for a new generation stamp for the block, and resends the unacknowledged packets through the nodes that are left. The partial replica on the dead node keeps the old stamp, so it is stale: if that node comes back, the NameNode will see the old stamp and delete it.

With the DEFAULT replace-datanode-on-failure policy and replication 3, the client goes on with 2 nodes instead of waiting for a replacement; the NameNode adds the third copy later, after the heartbeat timeout. The next block's addBlock lists the failed node as excluded, so it is not chosen again.

GFS and HDFS

GFS (2003)HDFS
metadata servermasterNameNode (plus a standby for HA)
storage serverchunkserverDataNode
unit64 MB chunk128 MB block
who orders writesa primary replica holding a leasethe single writer's pipeline
writesrandom writes and record appendwrite-once, append only
successorColossus (sharded metadata)Federation, Ozone

Replicating whole blocks costs 3× the space. HDFS can also store cold data with erasure coding, as on Reed-Solomon Erasure Coding: RS(6,3) survives three lost shards at 1.5× the space. Compare Kafka replication, where a leader replica orders writes, and quorum reads and writes, where no single node keeps the map.

What the page leaves out

NameNode high availability (standby NameNode, JournalNodes, ZooKeeper failover), the fsimage and edit-log checkpointing, federation, block reports in full, packets and per-chunk checksums drawn one by one, hflush and hsync, appends, lease recovery when a writer dies, decommissioning, the balancer, short-circuit local reads, erasure-coded files, and the random choices inside the real placement policy (the page picks fixed nodes so the demos repeat).

On the mind map

Open the Distributed Systems mind mapDistributedSystemsReplication: copies of data · what reads seeReplicationPartitioning: split data over nodesPartitioningTime and membership: order events, know who is aliveTime and membershipConsensus: agree on one value or logConsensusTransactions: one change across many servicesTransactionsMessaging: send events between systemsMessagingKafka internals: a replicated append-only logKafka internalsRedis internals: one thread, data in memoryRedis internalsKubernetes: reconcile desired stateKubernetesResilience and speed: stay up and fast under loadResilience and speedPartitioning vs shardingPartitioning vs shardingone server vs many; shard key mattersConsistent hashingConsistent hashingring: adding a node moves few keysRange vs hash partitioningRange vs hash partitioningrange: fast scans; hash: no hot spotRebalancing partitionsRebalancing partitionsmod N vs fixed vs dynamic: keys movedRedis ClusterRedis Cluster16384 hash slots, MOVED and ASKMapReduceMapReducemap, shuffle by key, reduceMapReduce joinsMapReduce joinssort-merge, skew, broadcast and merge joinsS3 object storageS3 object storagedata plus parity shards rebuild a fileRendezvous hashingRendezvous hashinghighest hash score of key+node winsHDFS / GFS file layoutNamenode maps blocks; datanodes pipelineOpen the Distributed Systems mind mapDistributed Systemssplit data over nodesPartitioningPartitioning vs shardingPartitioning vs shardingone server vs many; shard key mattersConsistent hashingConsistent hashingring: adding a node moves few keysRange vs hash partitioningRange vs hash partitioningrange: fast scans; hash: no hot spotRebalancing partitionsRebalancing partitionsmod N vs fixed vs dynamic: keys movedRedis ClusterRedis Cluster16384 hash slots, MOVED and ASKMapReduceMapReducemap, shuffle by key, reduceMapReduce joinsMapReduce joinssort-merge, skew, broadcast and merge joinsS3 object storageS3 object storagedata plus parity shards rebuild a fileRendezvous hashingRendezvous hashinghighest hash score of key+node winsHDFS / GFS file layoutNamenode maps blocks; datanodes pipeline
This page on the Distributed Systems mind map · 79 pages in 10 branches · open it to see the whole map and test yourself in recall mode.