🎤 07 · 大数据面试八股与高频题
> 最后冲刺:把**体系/存储/计算/实时/数仓/管道/平台**的考点串成速答体系,
> 并手写两个高频手撕(TopN、两阶段聚合处理倾斜)。
1️⃣ 30 秒框架串讲
print('''一条数据从产生到应用的全链路:
业务库/日志 -> 采集(Kafka/Flume) -> 存储(HDFS/湖) -> 离线计算(Hive/Spark)
-> 数仓分层(ODS->DWD->DWS->ADS) -> 报表/算法特征
另加实时链路:-> Flink 实时计算 -> 实时指标/风控
调度(Airflow/DolphinScheduler) + 血缘 + 质量 + 权限 贯穿始终
''')一条数据从产生到应用的全链路:
业务库/日志 -> 采集(Kafka/Flume) -> 存储(HDFS/湖) -> 离线计算(Hive/Spark)
-> 数仓分层(ODS->DWD->DWS->ADS) -> 报表/算法特征
另加实时链路:-> Flink 实时计算 -> 实时指标/风控
调度(Airflow/DolphinScheduler) + 血缘 + 质量 + 权限 贯穿始终
2️⃣ 高频题速答(一)体系与存储
%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
rng = np.random.default_rng(42)print('''1) Hadoop 三件套? -> HDFS(存储)+YARN(资源)+MapReduce(计算);
2) HDFS 副本策略? -> 3 副本:同机架 2/另一机架 1(默认策略描述);
3) 小文件问题? -> 大量小文件=海量元数据;合并/归档/湖格式;
4) 存算分离? -> 存储与计算独立扩缩容;云原生趋势;
5) 冷热数据? -> 冷数据低成本存储(对象存储),热数据快;
6) 数据湖 vs 数仓? -> 湖:原始格式+ACID(Iceberg/Hudi);仓:治理后分层。
''')1) Hadoop 三件套? -> HDFS(存储)+YARN(资源)+MapReduce(计算);
2) HDFS 副本策略? -> 3 副本:同机架 2/另一机架 1(默认策略描述);
3) 小文件问题? -> 大量小文件=海量元数据;合并/归档/湖格式;
4) 存算分离? -> 存储与计算独立扩缩容;云原生趋势;
5) 冷热数据? -> 冷数据低成本存储(对象存储),热数据快;
6) 数据湖 vs 数仓? -> 湖:原始格式+ACID(Iceberg/Hudi);仓:治理后分层。
3️⃣ 高频题速答(二)计算与实时
print('''7) Spark vs MapReduce? -> 内存计算+DAG,快 10-100x;
8) RDD 血缘有什么用? -> 容错重算/故障恢复;
9) 宽窄依赖? -> 窄=流水线无 shuffle;宽=shuffle;
10) Flink watermark? -> 处理乱序;低水位=等迟到数据;
11) 窗口类型? -> 滚动/滑动/会话;
12) 精确一次语义? -> 幂等+事务/两阶段提交。
''')7) Spark vs MapReduce? -> 内存计算+DAG,快 10-100x;
8) RDD 血缘有什么用? -> 容错重算/故障恢复;
9) 宽窄依赖? -> 窄=流水线无 shuffle;宽=shuffle;
10) Flink watermark? -> 处理乱序;低水位=等迟到数据;
11) 窗口类型? -> 滚动/滑动/会话;
12) 精确一次语义? -> 幂等+事务/两阶段提交。
4️⃣ 高频题速答(三)数仓与管道
print('''13) 数仓四层? -> ODS/DWD/DWS/ADS;
14) 星型 vs 雪花? -> 冗余/查询快 vs 规范/join 多;
15) 数据倾斜怎么办? -> 两阶段聚合/打散 key/随机前缀;
16) Kafka 为什么快? -> 顺序写+零拷贝+批量压缩;
17) 消息积压怎么解? -> 扩分区扩消费者/批量/降级;
18) 慢变维 SCD? -> Type1 覆盖/Type2 版本化。
''')13) 数仓四层? -> ODS/DWD/DWS/ADS;
14) 星型 vs 雪花? -> 冗余/查询快 vs 规范/join 多;
15) 数据倾斜怎么办? -> 两阶段聚合/打散 key/随机前缀;
16) Kafka 为什么快? -> 顺序写+零拷贝+批量压缩;
17) 消息积压怎么解? -> 扩分区扩消费者/批量/降级;
18) 慢变维 SCD? -> Type1 覆盖/Type2 版本化。
5️⃣ 手撕一:全局 TopN(分治思路)
# 每分区先取局部 TopN,再归并取全局 TopN
def topn_partition(nums, n):
return sorted(nums, reverse=True)[:n]
partitions = [[5, 9, 3], [10, 2, 7], [8, 1, 6]]
k = 3
local = [topn_partition(p, k) for p in partitions]
print('各分区局部 Top3:', local)
global_top = topn_partition([x for p in local for x in p], k)
print('全局 Top3:', global_top)
assert global_top == [10, 9, 8]
print('✅ TopN 分治法正确(局部降量 -> 归并)')各分区局部 Top3: [[9, 5, 3], [10, 7, 2], [8, 6, 1]]
全局 Top3: [10, 9, 8]
✅ TopN 分治法正确(局部降量 -> 归并)
# 可视化:TopN 分治——局部归约再全局归并
fig, ax = plt.subplots(figsize=(9.5, 4))
for i, p in enumerate(partitions):
x = 5 + i * 22
ax.add_patch(plt.Rectangle((x, 62), 16, 20, fc='#D9E2F3', ec='#2F5597'))
ax.text(x + 8, 72, f'分区{i}\n{p}', ha='center', va='center', fontsize=10)
ax.text(x + 8, 65.5, f'局部Top3={sorted(p, reverse=True)[:3]}', ha='center', fontsize=8.5, color='#C00000')
ax.annotate('', xy=(42 + (i - 1) * 6, 42), xytext=(x + 8, 61),
arrowprops=dict(arrowstyle='-|>', color='#7F7F7F'))
ax.add_patch(plt.Rectangle((32, 26), 24, 14, fc='#E2EFDA', ec='#548235'))
ax.text(44, 33, f'归并 -> 全局Top3={global_top}', ha='center', va='center', fontsize=11)
ax.set_xlim(0, 100); ax.set_ylim(20, 90); ax.axis('off')
ax.set_title('分治:每个分区只上传局部 TopN(数据量 n -> N*分区数)', fontsize=12)
plt.show()6️⃣ 手撕二:数据倾斜两阶段聚合
手撕三:SQL 高频两连(对应面试题 84/85)
**分组 Top1(每个城市最近一单)**——窗口函数 `row_number()`:
```sql
SELECT city, order_id, ts
FROM (
SELECT city, order_id, ts,
ROW_NUMBER() OVER (PARTITION BY city ORDER BY ts DESC) AS rn
FROM dwd_orders
WHERE is_valid = 1
) t
WHERE rn = 1;
```
**日活/留存**——去重计数 + 自连接:
```sql
-- 日活
SELECT dt, COUNT(DISTINCT user_id) AS dau
FROM dwd_orders GROUP BY dt;
-- 次日留存:首日用户中第 2 天仍活跃的比例
SELECT a.dt AS d1,
COUNT(DISTINCT a.user_id) AS new_users,
COUNT(DISTINCT b.user_id) AS retained,
ROUND(COUNT(DISTINCT b.user_id) / COUNT(DISTINCT a.user_id), 3) AS retention_1d
FROM user_active a
LEFT JOIN user_active b
ON a.user_id = b.user_id AND b.dt = DATE_ADD(a.dt, 1)
GROUP BY a.dt;
```
答题要点:先说**口径**("活跃"怎么定义、按谁去重),再写 SQL,最后提性能
(大表 join 先过滤/分区裁剪,`COUNT(DISTINCT)` 大时用两阶段去重:先 group by user 再 count)。
面试答题万能模板(结构化表达)
**原理题**:定义(是什么)→ 过程(怎么工作,最好画图)→ 为什么这么设计(解决什么问题)
→ 局限与替代方案(对比)。
例:RDD 血缘 = "记录父 RDD 与转换操作" → "分区丢了沿血缘重算" → "免副本、容错轻量"
→ "血缘过长恢复慢,用 checkpoint 截断"。
**方案/排查题**:先定位(指标/日志/血缘)→ 分级处理(止血→根因)→ 复盘预防(监控/规范)。
例:报表数字异常 = 先确认口径与数据时间 → 查调度是否漏跑/上游表是否更新 → 修复后补数 → 加质量监控。
**手撕题**:先讲思路(分治/贪心/两阶段)→ 写核心 10 行 → 主动说边界(空输入/单元素/倾斜)。
# 普通 groupBy 会 skew:某 key 集中在 1 个 task
# 两阶段:先加随机前缀局部聚合,再去前缀全局聚合
records = [('hot', 1)] * 100 + [('a', 1)] * 10 + [('b', 1)] * 5 # 'hot' 倾斜
def two_phase_agg(records, n_rand=3):
phase1 = {}
for k, v in records:
p = f'{k}#{rng.integers(0, n_rand)}'
phase1[p] = phase1.get(p, 0) + v
phase2 = {}
for pk, v in phase1.items():
k = pk.split('#')[0]
phase2[k] = phase2.get(k, 0) + v
return phase2
res = two_phase_agg(records)
print('两阶段聚合结果:', res)
assert res['hot'] == 100 and res['a'] == 10 and res['b'] == 5
print('✅ 两阶段聚合:先按加前缀的 key 分散计算,再合并,结果一致且不再倾斜')两阶段聚合结果: {'hot': 100, 'a': 10, 'b': 5}
✅ 两阶段聚合:先按加前缀的 key 分散计算,再合并,结果一致且不再倾斜
# 可视化:倾斜前后对比(task 处理条数)
fig, axes = plt.subplots(1, 2, figsize=(10.5, 4))
axes[0].bar(['task-0(hot)', 'task-1(a)', 'task-2(b)'], [100, 10, 5],
color=['#C00000', '#2F5597', '#2F5597'], width=0.5)
axes[0].set_title('一阶段聚合:hot 全落同一 task(倾斜)', fontsize=11.5, color='#C00000')
axes[1].bar(['hot#0', 'hot#1', 'hot#2', 'a', 'b'], [34, 33, 33, 10, 5],
color=['#ED7D31'] * 3 + ['#2F5597'] * 2, width=0.6)
axes[1].set_title("两阶段聚合:前缀打散后 task 均衡,合并后 hot=100", fontsize=11.5, color='#548235')
for ax in axes:
ax.set_ylabel('task 处理条数')
ax.set_ylim(0, 115)
plt.tight_layout(); plt.show()7️⃣ 自测清单
• [ ] 能把"一条数据从产生到应用"讲 3 分钟
• [ ] 能答 18 个高频速答题
• [ ] 能手写 TopN 分治、两阶段聚合
• [ ] 能画出数仓四层与 Kafka 架构
• [ ] 能说出 Flink watermark/窗口/精确一次