订阅更新

Hive 迁 Spark SQL 怎么做?——百台集群、千个作业、零故障的迁移方案

数仓开发手册 第 17 题 约 9 分钟读完

本文目录
  1. 先说结论
  2. 业务场景:我在某出海硬件品牌的 EMR → Databricks 全链路迁移
  3. 段落一:Step 1——现状盘点和分类
  4. 段落二:Step 2——方言差异摸底
  5. 段落三:Step 3——双写双读灰度方案
  6. 段落四:Step 4——性能调优(迁移的核心收益)
  7. 段落五:Step 5——切流量和老引擎下线
  8. 面试高频追问
  9. 实战踩坑:一个 decimal 精度问题差点让财务出错
  10. 总结

先说结论

迁移的核心原则是『双写双读 + 灰度切流』。绝不能一次性切——数据对不齐、SQL 方言差异、性能差异都可能炸,灰度能把风险限制在可控范围。

最大的坑不是引擎,是方言差异。Hive UDF、日期函数、NULL 处理、类型转换这些细节在两个引擎里经常不一致。我的经验是 20% 的 SQL 因为方言差异需要微调。

迁移不是目的,性能和成本才是。如果迁完性能持平、成本不降,业务方没理由支持你。最好准备好具体的收益数字(执行时间降低 / 资源成本降低)。

先把这段讲顺;面试官追问时,再展开下面的原理和细节。

详细讲解

引擎迁移是资深岗面试最爱问的架构题——因为做过一次引擎迁移基本就证明了你扛过长周期大项目。面试官会问:『如果让你把 100+ 个 Hive 作业迁到 Spark SQL,你怎么做?』这题考三样:方案设计、风险控制、协作推进。答好了是架构师画像,答不好说明只做过小作业。

业务场景:我在某出海硬件品牌的 EMR → Databricks 全链路迁移

我在某出海硬件品牌主导过一次 EMR(Hive + Spark on YARN)到 Databricks 的全链路迁移,规模 100+ Spark 作业、200+ Hive 作业、日均处理 50TB 数据。整个迁移分 3 个月完成,零故障。

Plain

迁移前状态:
  引擎:AWS EMR (Hive 2.3 + Spark 2.4 on YARN)
  规模:100+ Spark 作业、200+ Hive 作业
  痛点:集群运维重、Spark 性能一般、成本偏高
 
迁移后状态:
  引擎:Databricks (Spark 3.2 + Delta Lake)
  收益:
    - ETL 执行时间减少 40%
    - 存储成本降低 35%(Delta Lake 压缩 + Z-Ordering)
    - 查询延迟降低 70%(AQE + Z-Ordering + Broadcast Join)
    - 运维工作量减半(托管服务)
 
方案核心:
  双写双读灰度 + 100+ Spark 作业分批迁
  20% 的 SQL 因为方言差异需要微调

段落一:Step 1——现状盘点和分类

迁移的第一步是把所有要迁的作业盘清楚,然后按『难易 × 重要性』分类。

分类特征迁移策略
简单 × 不重要纯 SQL、非核心链路、小数据量先迁、练手、积累经验
简单 × 重要纯 SQL、核心链路、数据量大第二批、双写对比严格
复杂 × 不重要有 UDF / DataStream、非核心第三批、深度测试
复杂 × 重要UDF 密集、核心链路最后一批、灰度时间最长

按这个分类,我第一周先迁了 20 个简单非核心作业积累经验;第 2-4 周迁简单重要的;第 5-8 周迁复杂非核心;最后 4 周留给复杂核心。这个顺序能最大化控制风险。

段落二:Step 2——方言差异摸底

Hive 和 Spark SQL 90% 语法一致,但 10% 的差异能让你的 SQL 算出不同结果。最容易踩的几个:

-- 差异 1: 日期函数语义
-- Hive: date_sub('2024-03-15', 1) = '2024-03-14'(字符串日期)
-- Spark 3: 严格模式下要求显式 DATE 转换
-- 迁移时:SELECT date_sub(CAST(dt AS DATE), 1)
 
-- 差异 2: NULL 和空串
-- Hive: '' != NULL(空串不是 NULL)
-- Spark: 一般也是这样,但某些 CSV 导入会把空串读成 NULL
 
-- 差异 3: 隐式类型转换
-- Hive: WHERE str_col = 123(字符串和数字比较不报错)
-- Spark 3: 默认 ansi 模式下会报错,要显式 CAST
 
-- 差异 4: ORDER BY NULL 处理
-- Hive: NULL 默认排最前面
-- Spark: NULL 默认排最后
-- 需要显式 ORDER BY col NULLS FIRST/LAST
 
-- 差异 5: 字符串函数
-- Hive: size(split('a,b,c', ',')) = 3
-- Spark: split 遇到空字符串会多返回一个空元素

我当时的做法:写了一个方言差异 checklist(30+ 项),每个 SQL 迁移前必过 checklist;同时写了个自动化 diff 工具,对比 Hive 和 Spark 两边跑的结果,差异率 > 0.01% 就告警。

段落三:Step 3——双写双读灰度方案

迁移期最关键的设计是双写双读。老引擎和新引擎同时跑,老引擎出的结果给下游(保持业务不变),新引擎出的结果单独存用于对比,直到数据一致后再切换下游。

Plain

双写双读灰度架构(我那次的设计):
 
  ODS ──┬── [老引擎 Hive] ──── dwd_v1 ──→ 下游(真实消费)
        └── [新引擎 Spark] ─── dwd_v2 ──→ 对比表(不消费)
 
  每日自动对比:
    count(dwd_v1) vs count(dwd_v2)
    sum(核心指标) vs sum(核心指标)
    逐行 diff(采样 1000 条)
 
  连续 7 天一致 → 切流量
    下游改为消费 dwd_v2
    dwd_v1 再保留 14 天(防止回滚)
    dwd_v1 下线

双写的代价:存储 + 计算翻倍,持续 1-2 周。这个代价是必须付的——比起迁移失败导致业务故障,翻倍成本可接受。财务上要提前和老板沟通好这笔『迁移期预算』。

段落四:Step 4——性能调优(迁移的核心收益)

新引擎上线后性能调优是关键收益点。Spark 3 上有很多 Hive 没有或者弱的特性:AQE / Broadcast Join / Z-Ordering / Delta Lake 压缩。

-- 调优 1: 开启 AQE(Adaptive Query Execution)
SET spark.sql.adaptive.enabled = true;
SET spark.sql.adaptive.skewJoin.enabled = true;
-- 自动处理数据倾斜
 
-- 调优 2: 调大 Broadcast 阈值
SET spark.sql.autoBroadcastJoinThreshold = 200MB;
-- 默认 10MB 太小,大部分维表无法 Broadcast
-- 调到 200MB 后 Join 自动 Broadcast,作业时间减少 40%
 
-- 调优 3: Delta Lake Z-Ordering
OPTIMIZE dwd_order ZORDER BY (country, dt);
-- 查询延迟从 8s 降到 2s
 
-- 调优 4: Shuffle 优化(列裁剪)
-- 原来 SELECT * 改列裁剪
-- Shuffle 数据量减少 30-45%

段落五:Step 5——切流量和老引擎下线

切流量是整个迁移的终点仪式。做法:(1)核心链路按批次切,一次切 20% 流量;(2)每批次切完观察 3 天;(3)切完 100% 后老链路保留 14 天回滚窗口;(4)14 天无回滚需求,正式下线。老引擎下线一定要有明确的日期和公告,否则下线不掉。

面试高频追问

追问一:迁移过程中老 SQL 和新 SQL 算出来结果不一样怎么办?

先不要改代码。分四步排查:(1)确认差异规模——是全表差异还是局部?1 条差异和 1 万条差异处置不同;(2)定位差异行——用 EXCEPT 找出 diff 的具体行;(3)定位字段——看哪一列算错了;(4)定位原因——大概率是方言差异。我处理过最麻烦的一次是 NULL 排序导致 TopN 结果不同,修改 ORDER BY 加 NULLS LAST 解决。

追问二:迁移失败了怎么回滚?

回滚分两种:(1)切流量前回滚——直接停新链路即可,业务无感知;(2)切流量后回滚——把下游消费的表指回老引擎的输出表。所以我一定会保留老引擎输出 14 天。最坏的情况是发现问题时已经切了 7 天,老表还在就能一键切回。这 14 天的存储成本相比回滚风险,非常划算。

追问三:UDF 迁移最痛的是什么?

Hive UDF(Java)迁到 Spark UDF 大部分能直接 jar 包引入,但有几类需要重写:(1)依赖 Hive 特有的 context 的 UDF;(2)依赖 Hive 特定配置的;(3)性能敏感的——Spark 的 native 函数比 UDF 快 5-10 倍,能原生化的都原生化。我的经验是 30% 的 Hive UDF 可以直接替换成 Spark 内置函数,40% 直接迁移,30% 需要重写。

实战踩坑:一个 decimal 精度问题差点让财务出错

我在那次 EMR → Databricks 迁移里踩过一个坑:Hive 的 decimal 默认精度和 Spark 的不一样。有一个 SUM(price * quantity) 的 SQL 在 Hive 上结果是 decimal(38,2),在 Spark 上变成了 decimal(38,6),看起来更精确,但财务系统下游按 decimal(38,2) 存,溢出了。幸亏双写对比发现差异,没影响到生产。

教训:数值类型的细节在迁移里是最容易漏的。我后来的 checklist 里专门加了一条『所有 decimal 字段必须显式 CAST 到原类型』。这种看起来吹毛求疵的规则,在金额场景就是救命的。

总结

引擎迁移这道题答好的关键是讲出完整的五步流程(盘点 → 方言 → 双写双读 → 调优 → 切流)+ 真实收益数字(执行时间减少 40%、存储成本降低 35%、查询延迟降低 70%)+ 至少一个踩坑故事(decimal 精度 / NULL 排序)。能讲这一套,这题基本就是架构师档次。

下一篇:『实时数仓要不要建?』——架构题最后一道。最近几年每个公司都在讨论要不要上实时,这题考的是你对『实时价值』和『实时代价』的判断。

你做过引擎迁移吗?过程中踩过最深的坑是什么?是方言、UDF 还是性能?

关注「Wood聊大数据」| 9年大数据老兵,聊技术、聊职场、聊成长

刷题进度保存在当前浏览器