🏗️ 00 · 大数据体系与 HDFS

> 大数据面试第一题常是"数据湖/数仓/Hadoop 体系"。本节搭**大数据技术全景**框架,
> 再用**模拟实现**讲清 **HDFS 的块/副本/读写流程**——不依赖集群,numpy 就能跑。

📋 本节要解决的问题

| # | 问题 | 方法 |
|---|---|---|
| 1 | 大数据技术栈有哪些层? | 采集/存储/计算/调度/OLAP |
| 2 | HDFS 怎么存大文件? | 分块 + 副本 |
| 3 | 读写怎么走? | NameNode 元数据 + DataNode 数据 |
| 4 | 副本策略为什么重要? | 容错 3 副本 |

0️⃣ 背景知识:为什么需要"大数据技术"

**大数据的 4V 特征**:数据量巨大(Volume,TB/PB 级)、类型多样(Variety,日志/文本/图像/表)、
增速快(Velocity,秒级持续产生)、价值密度低(Value,需要挖掘提炼)。

**单机为什么撑不住**?一台服务器再强也有上限:

| 资源 | 单机瓶颈 | 分布式的解法 |
|---|---|---|
| 磁盘容量 | 单盘最大 ~20TB | 数据切分(分块)存到多机 |
| 读写吞吐 | 单盘 ~几百 MB/s | 多机并行读写 |
| 可靠性 | 一块盘坏=数据丢 | 多副本冗余 |
| 计算能力 | CPU 核数有限 | 计算任务拆分并行执行 |

一句话:**分而治之 + 冗余容错**,是整个大数据体系的两块基石。HDFS 负责"存得多、存得稳",
MapReduce/Spark/Flink 负责"算得快"。

分块数怎么算(公式)

设文件大小为 $S$,块大小为 $B$,则分块数

$$n = \left\lceil \frac{S}{B} \right\rceil = \left\lfloor \frac{S + B - 1}{B} \right\rfloor$$

除末块外每块大小均为 $B$,末块大小 $S - (n-1)B$。
**为什么块要设大(128MB)**?经典推导:设磁盘寻址约 10ms、传输率 100MB/s,
若让"寻址时间 : 传输时间 ≈ 1:100"(即寻址开销只占 1%),块大小约需 100MB 量级——
这就是 Hadoop 把块从 64MB(1.x)提升到 128MB(2.x+,`dfs.blocksize`)的原因:
**块越大,元数据越少,顺序传输占比越高,吞吐越好**。

%matplotlib inline
import matplotlib
import matplotlib.pyplot as plt
matplotlib.rcParams['font.sans-serif'] = ['Microsoft YaHei', 'SimHei', 'PingFang SC', 'Noto Sans CJK SC', 'Arial Unicode MS']
matplotlib.rcParams['axes.unicode_minus'] = False
import numpy as np
import pandas as pd
import sqlite3
import os
rng = np.random.default_rng(42)

1️⃣ 大数据技术栈全景(必背框架)

stack = [
    ('采集', 'Flume / Kafka / Logstash / DataX / Sqoop'),
    ('存储', 'HDFS / HBase / Kudu / Delta / Iceberg / Hudi'),
    ('计算', 'MapReduce / Spark / Flink / Hive / Presto / StarRocks'),
    ('调度', 'Airflow / DolphinScheduler / YARN / K8s'),
    ('查询', 'Presto / Trino / Impala / Doris / ClickHouse'),
    ('治理', '数仓分层 / 元数据 / 数据质量 / 数据安全'),
]
for layer, tools in stack:
    print(f'  {layer:<4} | {tools}')
print('\n➡️ 面试串讲:先说全貌,再说"我负责的模块",最后讲联动')
  采集   | Flume / Kafka / Logstash / DataX / Sqoop
  存储   | HDFS / HBase / Kudu / Delta / Iceberg / Hudi
  计算   | MapReduce / Spark / Flink / Hive / Presto / StarRocks
  调度   | Airflow / DolphinScheduler / YARN / K8s
  查询   | Presto / Trino / Impala / Doris / ClickHouse
  治理   | 数仓分层 / 元数据 / 数据质量 / 数据安全

➡️ 面试串讲:先说全貌,再说"我负责的模块",最后讲联动

2️⃣ 存储 vs 计算分离思想

print('''存算分离(存储计算分离):
  HDFS = 存储(DataNode 只存数据)
  Spark/Flink = 计算(用完即释放,不持有数据)
  好处:弹性扩缩容、成本低、统一存储(湖仓一体基础);
  代价:网络 IO 变大 → 数据本地性优化(把任务调度到数据所在节点)。
''')
存算分离(存储计算分离):
  HDFS = 存储(DataNode 只存数据)
  Spark/Flink = 计算(用完即释放,不持有数据)
  好处:弹性扩缩容、成本低、统一存储(湖仓一体基础);
  代价:网络 IO 变大 → 数据本地性优化(把任务调度到数据所在节点)。

3️⃣ HDFS 分块:把大文件切成块

# 模拟一个大文件,按 128MB 块大小分块(这里用字符数模拟)
file_size_mb = 500
block_size_mb = 128
n_blocks = (file_size_mb + block_size_mb - 1) // block_size_mb
sizes = [block_size_mb] * (n_blocks - 1) + [file_size_mb - block_size_mb * (n_blocks - 1)]
print(f'文件 {file_size_mb}MB:块大小 {block_size_mb}MB -> 分成 {n_blocks} 个块,末块 {sizes[-1]}MB')
assert sum(sizes) == file_size_mb
# 模拟块数据(字节数组)
block_data = [np.random.default_rng(i).integers(0, 256, 64, dtype=np.uint8) for i in range(n_blocks)]
print(f'每个块 64 字节样例:块0 = {block_data[0][:8]}...')
print('➡️ 真实 HDFS 默认块 128MB/256MB,块是分布式存储与容错的基本单位')
文件 500MB:块大小 128MB -> 分成 4 个块,末块 116MB
每个块 64 字节样例:块0 = [ 95 130 194 217 207 235  15 163]...
➡️ 真实 HDFS 默认块 128MB/256MB,块是分布式存储与容错的基本单位

机架感知副本放置(默认策略,必背)

设副本数为 3,写块时按如下规则选节点:

1. **第 1 副本**:客户端所在节点(客户端在集群外则随机选一节点);
2. **第 2 副本**:**另一个机架**的某节点(防整机架断电/断网);
3. **第 3 副本**:与第 2 副本**同机架的另一个节点**(同机架带宽高,传输快)。

效果:任一节点坏→同机架还有 1 副本;任一机架坏→另一机架仍有 1 副本。
兼顾**容错**(跨机架)与**传输效率**(同机架第 3 副本少走一次跨机架交换机)。

NameNode 元数据与 HA

• NameNode 把**命名空间(目录树)与块映射**放在**内存**,经验上 1 亿个块需数百 GB 级内存压力,故大集群要 Federation 分命名空间;

• 持久化 = **fsimage**(镜像)+ **editlog**(增量编辑),启动合并;

• 块的实际位置**不持久化**,由 DataNode 周期性**心跳汇报**;

• 单点故障 → **HA**:Active/Standby 两台 NameNode + JournalNode 共享 editlog,ZKFC 做 Failover。

# 可视化:3 副本机架感知放置(色点=该节点存有的块副本)
fig, ax = plt.subplots(figsize=(9.5, 4.6))
colors = ['#C00000', '#7030A0', '#2F5597', '#548235']
racks = [('机架A', {'DataNode0': [0, 2], 'DataNode1': [0, 3]}),
         ('机架B', {'DataNode2': [0, 1, 2], 'DataNode3': [1, 3]}),
         ('机架C', {'DataNode4': [1, 2, 3]})]
ry = 84
for rname, nodes in racks:
    ax.add_patch(plt.Rectangle((8, ry - 4), 84, 24, fc='#FAFAFA', ec='#7F7F7F', ls='--'))
    ax.text(10, ry + 22, rname, fontsize=10, color='#7F7F7F')
    for j, (n, holds) in enumerate(nodes.items()):
        x = 14 + j * 42
        ax.add_patch(plt.Rectangle((x, ry), 32, 14, fc='#E2EFDA', ec='#548235'))
        ax.text(x + 16, ry + 5, n, ha='center', va='center', fontsize=10)
        for k, b in enumerate(holds):
            ax.plot(x + 5 + k * 7, ry + 12, 'o', ms=8, color=colors[b])
    ry -= 30
handles = [plt.Line2D([], [], marker='o', ls='', color=colors[b], label=f'块{b}的副本') for b in range(4)]
ax.legend(handles=handles, loc='lower left', fontsize=9, ncol=4, bbox_to_anchor=(0, -0.02))
ax.set_xlim(0, 100); ax.set_ylim(-6, 100); ax.axis('off')
ax.set_title('3 副本机架感知放置示意(每块 3 副本,分布在至少 2 个机架)', fontsize=12)
plt.show()
Notebook 已保存的运行结果

> **动手验证**:把上面 `replication` 改成 2、`n_nodes` 改成 3,再跑一遍容错循环,
> 观察挂 2 个节点时是否可能出现整块丢失——这正是"3 副本 + 跨机架"的意义。

4️⃣ 副本机制:3 副本容错

# 模拟副本:每块存 3 份在不同"节点"
n_nodes = 5
replication = 3
assign = {}
for b in range(n_blocks):
    nodes = list(rng.choice(n_nodes, size=replication, replace=False))
    assign[b] = nodes
    print(f'块{b}: 副本 -> 节点{nodes}')

# 容错演示:挂掉 1-2 个节点数据是否仍完整
for fail in [[0], [0, 1], [0, 1, 2]]:
    ok = True
    for b in range(n_blocks):
        if not set(assign[b]) & set(range(n_nodes)) - set(fail):
            ok = False
    print(f'节点 {fail} 宕机 -> 数据可读: {ok}')
print('\n➡️ 3 副本=块丢失率极低;面试点:副本放置策略(同机架/跨机架)')
块0: 副本 -> 节点[np.int64(4), np.int64(0), np.int64(3)]
块1: 副本 -> 节点[np.int64(0), np.int64(3), np.int64(2)]
块2: 副本 -> 节点[np.int64(1), np.int64(3), np.int64(4)]
块3: 副本 -> 节点[np.int64(3), np.int64(2), np.int64(0)]
节点 [0] 宕机 -> 数据可读: True
节点 [0, 1] 宕机 -> 数据可读: True
节点 [0, 1, 2] 宕机 -> 数据可读: True

➡️ 3 副本=块丢失率极低;面试点:副本放置策略(同机架/跨机架)

5️⃣ 读写流程(NameNode/DataNode 分工)

print('''读流程:
  1. Client 问 NameNode:文件在哪些块、各块在哪些 DataNode(元数据)
  2. Client 就近(机架感知)从 DataNode 拉块
  3. 拼装成完整文件

写流程:
  1. Client 问 NameNode 申请创建文件
  2. 按块依次写:写块 -> 流式复制到副本节点(pipeline)
  3. 全部写完 -> 告知 NameNode 提交

关键:NameNode 只管元数据(内存),数据流不过 NameNode(否则瓶颈)。
NameNode 单点 -> HA(Active/Standby + JournalNode)。
''')
读流程:
  1. Client 问 NameNode:文件在哪些块、各块在哪些 DataNode(元数据)
  2. Client 就近(机架感知)从 DataNode 拉块
  3. 拼装成完整文件

写流程:
  1. Client 问 NameNode 申请创建文件
  2. 按块依次写:写块 -> 流式复制到副本节点(pipeline)
  3. 全部写完 -> 告知 NameNode 提交

关键:NameNode 只管元数据(内存),数据流不过 NameNode(否则瓶颈)。
NameNode 单点 -> HA(Active/Standby + JournalNode)。

6️⃣ 面试高频:HDFS 相关速答

print('''1) 为什么块大(128MB)?  -> 减少元数据量、减少寻道开销;
2) 小文件问题?           -> 每个文件有元数据,海量小文件压垮 NameNode;
3) 解决小文件?           -> 合并(SequenceFile/Har)、分区归档、Iceberg 等湖格式;
4) HDFS 适合什么?        -> 大文件批量读写;不适合秒级随机访问/低延迟;
5) 与对象存储对比?       -> HDFS 块语义 vs OSS 桶/对象语义。
''')
print('✅ 五个高频点已覆盖')
1) 为什么块大(128MB)?  -> 减少元数据量、减少寻道开销;
2) 小文件问题?           -> 每个文件有元数据,海量小文件压垮 NameNode;
3) 解决小文件?           -> 合并(SequenceFile/Har)、分区归档、Iceberg 等湖格式;
4) HDFS 适合什么?        -> 大文件批量读写;不适合秒级随机访问/低延迟;
5) 与对象存储对比?       -> HDFS 块语义 vs OSS 桶/对象语义。

✅ 五个高频点已覆盖

📝 自测清单

• [ ] 能画出大数据技术栈全景图(六层)

• [ ] 能解释存储计算分离的好处

• [ ] 能手算文件分块数与副本分配

• [ ] 能画出 HDFS 读写流程图

• [ ] 能答 5 个 HDFS 高频问题