🎤 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()
Notebook 已保存的运行结果

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()
Notebook 已保存的运行结果

7️⃣ 自测清单

• [ ] 能把"一条数据从产生到应用"讲 3 分钟

• [ ] 能答 18 个高频速答题

• [ ] 能手写 TopN 分治、两阶段聚合

• [ ] 能画出数仓四层与 Kafka 架构

• [ ] 能说出 Flink watermark/窗口/精确一次