🏗️ 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()> **动手验证**:把上面 `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 高频问题