主题
实习项目深挖。同目录其他实习模块陆续补充;面试话术主线见文末「追问自测清单」。
Binlog2Hive 是滴滴同步中心平台里的一条同步链路:把 MySQL 的记录变更通过 Binlog 同步到 Hive 表。链路分两段——MySQL → Canal → Kafka → HDFS 是实时的(变更采集与落地不间断);HDFS → Spark → Hive 是按小时或天调度的批处理(可配置)。核心模式是「快照全量 + Binlog 增量」——快照保证数据完整性,Binlog 保证时效性。
我负责的是把这条链路在平台内跑通:任务创建、发布流程的编排与全量/增量衔接。分工上,Canal 采集由采集团队负责,Spark 计算引擎由引擎团队提供——我的价值是把四段异构组件编排成一条可配置、可运维、开箱即用的同步链路,也因此对每一环的原理和衔接点都建立了端到端的理解。
实习介绍(面试口述稿)
30 秒版
我在滴滴数据中台的同步中心实习,负责 Binlog2Hive 这条数据同步链路:把 MySQL 的变更数据实时同步到 Hive 数仓。链路是 Canal 采集 Binlog、经 Kafka/DDMQ 解耦、Spark 按小时/天调度清洗入仓,采用「快照全量 + Binlog 增量」混合模式,数据入湖延迟 P95 控制在 3 分钟内。我的工作是把采集、消息、存储、计算四段异构组件编排成一条可配置的同步链路——12 步创建流程、全量增量衔接、三种平滑迁移模式,加上数据一致性保障和故障排查。
2 分钟版
背景:公司内部各业务线的数据分散在 MySQL 里,分析要用得进数仓。我所在的同步中心提供 MySQL→Hive 的同步能力,我负责其中 Binlog2Hive 这条实时链路。
架构与我的角色:链路四段——Canal 采集 Binlog、Kafka/DDMQ 中转解耦、HDFS 落地存储、Spark 调度计算入 Hive。采集和计算引擎由专职团队提供,我负责把四段编排成一条完整链路:任务创建的 12 步流程(从 RDS 读权限检查、Canal 到 MQ 的路由选择、自动建表,到全量导入和 Spark 任务注册)、全量与增量的衔接、以及整条链路的联调排查。
两个核心设计:一是快照全量 + Binlog 增量的混合模式——Binlog 只有增量没有历史,首次同步用 DataX 全量打底,完成后记录批次时间作为锚点,增量从锚点续传,配合下游主键幂等去重,做到不丢不重;二是数据完整性信号——每个小时的数据采集完整后才会打出 Topic Tag,调度中心检查到 Tag 才执行对应批次,新建任务卡在 55 分之后就要等下下个周期,保证调度的数据一定是完整的。
一致性与修复:异常解析事件进死信队列、记录偏移量事后重放(修复成功率 98%);支持历史批次补跑、时间范围回刷和全量回溯,形成死信重放、批次补跑、全量重导的三级修复粒度。
收尾:这段实习给我最大的收获是跨存储的数据一致性思维——不依赖分布式事务,用写入顺序、位点锚点和幂等设计把 at-least-once 做成业务上的不丢不重;以及把"延迟排查"变成方法论:链路四段逐段看水位,先定位段落再深入。
追问衔接
| 追问 | 去处 |
|---|---|
| Binlog 格式 / Canal 原理 / 位点断点续传 | Q1 / Q2 / Q16 |
| 为什么要 MQ / Tag 怎么判断数据完整 | Q3 / Q22 |
| 全量增量衔接 / 一致性口径 | Q4 / Q7 |
| 延迟排查 / Kafka 堆积 | Q8 / Q9 |
| Schema 变更 / Spark vs Flink / 双写比对 | Q12 / Q13 / Q14 |
我的工作与边界("你具体做了什么"的标准答案)
分工对照与讲述口径
| 链路段 | 谁做的 | 你能讲的口径 |
|---|---|---|
| Canal 采集(伪装 slave、binlog 解析、位点管理) | 采集团队 | "对接 + 配置是我做的":RDS 读权限配置、Canal→Kafka/DDMQ 的链路路由选择,是我在发布流程里执行的;binlog 三格式、位点机制等原理我掌握到设计级 |
| Kafka/DDMQ 消息中转 | 采集团队投递 + 平台配置 | "链路选型是我做的":走 Kafka 还是 DDMQ 的判断逻辑(isCanalDirectKafka 的优先级)就是发布流程里的配置分支 |
| Kafka→HDFS 落地(Dsink、目录/分区/压缩) | 存储团队 | "落地策略是我配置的":HDFS 存储路径、时间分区粒度、数据格式与压缩,是发布阶段 3 的配置项 |
| Spark 计算(binlog 解析、去重合并、写 Hive) | 引擎团队提供 jar | "任务是我发布和调优的":jar 是引擎团队的,但任务参数(saveDelete、concatenate、TTL、Spark3)由我配置调度;处理逻辑的设计我能讲到伪代码级 |
| 任务创建/发布编排、全量导入衔接、Snap2Binlog 迁移、监控告警、联调排查 | 我 | 完全自有:12 步创建流程、wholeLoadBatchTime 衔接、DataX 全量、三种迁移模式、故障排查 |
三档动词(防止讲过头也防止讲太虚)
- 我负责:只用在完全自有的部分(编排、衔接、迁移、排查);
- 我参与对接/配置:用在采集段和存储段(权限、路由、落地路径、任务参数——这些都是发布流程里的真实配置动作);
- 其原理我掌握:用在"别人实现的内核"(Canal 解析、Spark 去重算法)——讲得出设计思想,但主动说明实现由专职团队负责,反而显得职业边界清晰。
标准答案(60 秒版)
"这条链路在滴滴同步中心平台里,我负责把 Canal 采集、MQ 中转、HDFS 存储、Spark 计算这四段既有能力编排成一条可配置的同步链路。具体三块:一是任务创建与发布流程——12 步创建流程从 RDS 读权限、Canal 到 MQ 的路由选择、HDFS 落地配置、自动建表,到全量导入和 Spark 增量任务的注册,都是我打通的;二是全量和增量的衔接机制——wholeLoadBatchTime 作锚点,加上 Snap2Binlog 三种迁移模式;三是联调与故障排查——延迟按四段链路逐段定位,数据不一致做比对重导。Canal 内部的采集实现和 Spark jar 的计算逻辑分别是采集团队和引擎团队的,但选型、配置、上下游契约都是我的工作范围,原理我也掌握到设计级。"
核心设计思路
- 增量同步为主,全量导入为辅:Binlog 只记录变更(INSERT/UPDATE/DELETE),不含历史数据,所以首次同步、数据修复必须靠快照;两者结合才完整。
- 解耦架构:Canal(采集)→ Kafka/DDMQ(缓冲分发)→ HDFS(存储)→ Spark(计算)→ Hive(数仓),四层各自可扩展——消息队列可换、计算引擎可换(MR/Spark)。
- 快照的三个必要性:① Binlog 没有历史数据,首次同步必须全量打底;② Binlog 丢失/损坏时全量重导修复;③ snap2binlog 迁移时用全量补齐基础数据。
任务创建流程(12 步)与首次调度规则
用户在页面填完内容点提交后,系统自动依次执行 12 步,全部走完任务才算创建成功,之后才出现调度任务;创建过程中没有任何数据流入 Hive(但 Hive 表在第 5 步的一瞬间就建好了;第 8 步的全量导入在创建流程里也只是配置与注册,真正执行要等首次调度实例)。中途卡住一般是创建参数有问题,平台会反馈给用户,修改后重新提交即可。
12 步依次是:
- 检查 RDS 是否有读权限——没有读权限,采集无从谈起;
- 建立 Canal 到消息队列的链路——可选 Kafka 或内部 DDMQ,默认 Kafka;从这一步起变更开始实时进 Kafka,下一步 Dsink 建起来后持续落 HDFS,中间的间隔由 Kafka 缓冲——这正是消息队列做缓冲层的价值;
- 创建 Dsink 任务——常驻流式消费 Kafka,按时间滚动写 HDFS 文件,按时间分区;
- 创建调度 Topic Tag(注册 Tag 规则)——在调度中心注册这个 Tag 的名字与检查规则(相当于装信号灯);运行时该小时数据采集完整后才由采集侧打出这个小时的 Tag 实例(亮灯),它是调度中心判断"本批次数据是否就绪"的依据;
- 自动建 Hive 表——获取 MySQL 建表语句自动建出 Hive 表,任务创建完的一瞬间表就已经存在;
- 添加系统字段——
leo_update_time标记这条记录的更新时间(值是 Canal 采集时打的事件时间,用户查 Hive 时用它做增量过滤)、delete标记是否删除; - 加密字段处理(可选项);
- 全量导入(配置与注册)——创建 DataX 全量导入任务并挂到调度系统;执行不在创建流程内,等首次调度实例(55 分规则决定的下一个整点)才真正跑 DataX、写入 Hive 首个分区(分区值为任务建成当天/当小时的日期),完成时记录 wholeLoadBatchTime;
- Spark 增量任务——把 HDFS 上的 Binlog 数据清洗后转换为 Hive 分区数据;
- 注册 Topic-Job 关系——JobId 是同步中心的任务,Topic Tag 是调度中心的任务,1 对多:一个同步任务下挂多个调度实例(小时/天);
- 敏感字段检查——阻断性校验:存在敏感字段必须处理后才能建出任务;
- Topic Tag 状态校验。
首次调度时间规则(55 分 cutoff):以小时调度为例——55 分之前创建,下一次调度在下一个整点(10:30 创建 → 11:00 首次调度,开始导数据);55 分之后创建,则到下下个小时(10:56 创建 → 12:00)。这个 5 分钟缓冲是给"该小时数据采集完整 → 打 Tag → 调度检查 Tag"留的时间。天调度同理,看创建时间在当天检查点(11 点)之前还是之后。首次调度实例执行的就是全量导入(DataX 写首个分区),完成时记录 wholeLoadBatchTime,后续增量批次依赖它接续——所以 cutoff 影响的不只是增量首跑,是全量什么时候跑。
MySQL → Hive 建表类型映射(第 5 步的实现细节)
| MySQL | Hive | 说明 |
|---|---|---|
| TINYINT / SMALLINT / INT / BIGINT | 同名整型 | 直接映射 |
| FLOAT / DOUBLE | FLOAT / DOUBLE | 直接映射 |
| DECIMAL(p,s) | DECIMAL(p,s) | 精度保留 |
| VARCHAR / CHAR / TEXT / ENUM / JSON | STRING | Hive 侧统一用 string,最稳 |
| DATE | DATE | 直接映射 |
| DATETIME / TIMESTAMP | STRING(或 TIMESTAMP) | 数仓惯例用 string 存,避开时区转换坑 |
| BLOB / BINARY | BINARY | |
| BIT(1) | BOOLEAN / TINYINT |
三个注意点:Hive 没有主键、索引、自增——这些不映射,去重逻辑由 Spark 任务承担;MySQL 的 UNSIGNED BIGINT 有溢出风险,映射成 DECIMAL 或 STRING;emoji(utf8mb4)在 string 下无压力,建库时注意 Hive 表的编码配置。
全量与增量的衔接
全量导入(挂在首次调度实例执行)完成后记录批次时间(wholeLoadBatchTime)——它标记的是全量快照对应的 binlog 位点:值对应快照读视图的时刻,记录动作发生在导入完成之后(同 mysqldump --master-data 的语义)。增量处理只做事件时间在锚点之后的落湖数据。另一个容易误解的点:实时落湖(Dsink 写 HDFS)从建链(第 2 步)就开始了,不等全量——增量流早于锚点就在积累,早出的这一段(第 2 步→快照读视图)是"落了湖但不处理"的保险余量;真正两边都进数据的重叠窗口 = 全量导入的执行期间,由主键去重幂等收敛。等全量的只是 Spark 写 Hive 这一步。调度周期支持小时/天两级;支持补跑历史批次(Gap Batch,自动处理依赖关系)与全量回溯(覆盖历史数据)。
Snap2Binlog:快照任务平滑迁移
为什么会有这个迁移:历史上的快照任务每次同步都把整表全量写一遍 Hive,资源开销极大;Binlog 链路只需要对比当前 binlog 增量和上个分区的 Hive 表,就能得到当前分区的完整数据——同样的产出,开销省了一大截。所以平台提供 Snap2Binlog 能力,把存量的快照任务平滑迁到 Binlog 链路。
三种模式,按业务对停机窗口的容忍度选:
- 双写(Double Write):快照与 Binlog 任务并行,Binlog 数据写
_bak备份表,用于数据比对验证后再切; - 切换(Handover):等快照任务历史批次执行完 → 停快照 → 起 Binlog → 全量导入补齐 → 完成切换;
- 迁移(Migrate):直接建 Binlog 任务 + 全量导入,停旧任务(容忍窗口最小)。
迁移是状态机驱动(MIGRATING → DONE),配置里的 snapshotState / doubleWrite 字段支撑这套流程。
Spark 增量处理
按调度周期读 HDFS 对应分区的 Binlog 数据,解析三类事件后处理:
- UPDATE 转最新状态、DELETE 标记或物理删除(
saveDelete配置是否保留删除记录); - 按主键去重保留最新——同一主键短时间多次变更只留末态;
- 写 Hive 分区,支持 concatenate 小文件合并与 TTL 生命周期;新任务默认 Spark3 引擎。
监控告警与故障处理
三维监控:任务状态、数据量、数据质量(错误记录数/延迟/完整性)。告警三阈值:延迟超 delayAlarm、错误记录超 errorRecordThreshold、数据量低于 recordLowerLimit。故障三板斧:Binlog 丢失 → 全量重导后从批次时间续;任务延迟 → 按链路四段逐段排查(Canal 采集 → MQ 消费 → HDFS 落地 → Spark 调度);数据不一致 → 比对 + 重导。
规模与延迟的量级(理论估算)
实习生不必知道全公司精确数字,但要能给量级和推导过程。
- 任务规模:公司内部所有有 MySQL→Hive 同步需求的团队都会用这个链路,任务量级在几十到几百个(按业务线数量 × 核心表比例估);
- 单表规模:OLTP MySQL 业务表通常百万到千万行、1~50GB,核心大表上亿行;binlog 日志量随写入 QPS 走,日均几十 GB 量级是常态;
- 全量导入时长:DataX 限流下按 20~50MB/s 估,10GB 的表约 5~20 分钟,千万行大表放低峰窗口 1 小时内可完成;
- 端到端延迟分段看:入湖段(binlog→Kafka→HDFS)秒到分钟级,P95 < 3 分钟;入仓段由调度周期决定——小时任务最坏 1~2 小时、天任务到次日;下游用
leo_update_time做增量过滤,只扫新数据; - 消息速率:写高峰时 binlog 事件速率可达每秒数千到数万条,MQ 的分区数与 Dsink 的消费并发按峰值 3 倍冗余配置。
下游与业务价值
Hive 表的消费方主要是数据分析师,典型场景四类:
- BI 报表与指标看板:业务核心指标的日报/周报,小时级同步让指标时效从 T+1 提升到 T+1h;
- 即席查询(ad-hoc):产品和运营自助写 SQL 查数——"上周各城市的完单量",不用再找业务方从 MySQL 导出;
- 专项分析:用户行为漏斗、留存、A/B 实验效果评估,需要多表 join 的离线分析;
- 数据对账:数仓侧与业务库侧的数据一致性核对。
业务价值一句话:分析师统一在 Hive 里用 SQL 拿数,不再直连业务库或找人工导出——既把查询负载从线上 MySQL 剥离(保护线上库),又通过统一链路保证了全公司数据口径一致(这正是数据中台"消除数据孤岛、统一口径"使命在这条链路上的落点)。
面试官追问自测清单(33 题)
用法:按主题分组自测;先自己口述作答,再对照下方参考答案。每题答案都从本项目的实际设计出发,可以直接背。
| # | 追问 | 主题 | 方向提示 |
|---|---|---|---|
| Q1 | Binlog 为什么必须用 row 格式? | 数据源与采集 | (采集细节由采集团队负责,但原理必须懂——被问到就这么答 |
| Q2 | Canal 的底层原理? | 数据源与采集 | 采集链路的对接配置是我做的,原理我了解到协议层:Canal 的本质是把自己伪装成… |
| Q16 | Binlog 位点是怎么管理的?"断点续传"具体怎么实现? | 数据源与采集 | 位点就是「消费到了 binlog 的哪个位置」——由 binlog 文件名 + … |
| Q3 | 为什么中间要过一层 Kafka/DDMQ,Canal 直接写 HDFS 不行吗? | 链路架构与选型 | 先说 DDMQ 是什么:滴滴架构部基于 Apache RocketMQ 构建的自… |
| Q13 | 为什么用 Spark 批处理而不是 Flink 实时写?这个方案的"实时"到底是什么级别? | 链路架构与选型 | 先说清架构事实:链路的前半段(Canal → Kafka → HDFS)本来就是… |
| Q29 | 为什么不能直接在 MySQL 上做分析?非要同步到 Hive? | 链路架构与选型 | 一句话开场:不是技术上完全不能,是两种负载放在一起会互相伤害——隔离本身就是这条… |
| Q31 | 业务方的实时性要求怎么满足?最高只能到小时级吗? | 链路架构与选型 | 分层满足,而不是一条链路硬扛所有时效 |
| Q17 | 任务创建为什么要拆成 12 步?顺序能调整吗? | 任务创建与调度 | 不是随意排列,是按依赖关系分层的 |
| Q22 | "数据采集完整才打 Tag"——怎么判断数据完整了?第 4 步不就已经创建 Tag 了吗? | 任务创建与调度 | 先拆矛盾:第 4 步创建的是 Tag 的"定义",数据完整时打的是 Tag 的"… |
| Q30 | 按小时/天调度,每调度一次具体执行什么?表数据是全量刷新吗?前置快照来自哪里? | 任务创建与调度 | 以默认的 latest 快照模式为例,一次调度 Spark 做五件事: |
| Q4 | 全量和增量之间怎么保证不丢数据、不重复? | 全量导入与衔接 | 不丢靠位点锚点,重复靠下游幂等收敛 |
| Q11 | 全量导入会不会打爆源库?怎么控制? | 全量导入与衔接 | 会,不控制就是一次对生产库的表扫描攻击 |
| Q27 | "快照全量 + Binlog 增量"这个混合模式是怎么想出来的? | 全量导入与衔接 | 关键话术:这个方案不是想象出来的,是被约束一步步推出来的 |
| Q6 | 同一主键一分钟内变更 5 次,怎么保证保留的是"最新"? | 一致性与幂等 | 顺序性分两层保证 |
| Q7 | 整条链路是 exactly-once 还是 at-least-once? | 一致性与幂等 | 诚实回答:链路整体是 at-least-once,靠下游幂等把效果做到 exac… |
| Q33 | 这条链路的一致性和性能分别怎么保障?(总纲题) | 一致性与幂等 | 一致性三个机制(详见 Q4/Q7):批次锚点(全量完成记录 wholeLoadB… |
| Q34 | Spark 怎么知道读哪个分区?HDFS 按分区存好了吗? | 数仓存储 | 目录即分区,落地时已按小时组织;Spark 按日期模板读路径清单 |
| Q18 | 采集侧的"位点重置与回放"怎么做的?重放不会把下游写重复吗? | 修复与容错 | 位点重置就是把消费位点拨回某个历史位置(按位点或按时间戳定位),Canal 从那… |
| Q19 | 死信队列的机制展开讲讲?偏移量记录了什么? | 修复与容错 | 先讲为什么需要:解析失败的异常事件(数据损坏、字段截断、schema 不兼容)如… |
| Q20 | Gap Batch 补跑是怎么工作的?补跑和增量会冲突吗? | 修复与容错 | 背景:Spark 是按调度周期(小时/天)处理分区的,如果某个批次调度失败、或者… |
| Q32 | 调度任务异常停了好几天,积压的数据怎么补回来? | 修复与容错 | 先纠偏:Kafka 不会堆积(Dsink 实时落湖);3 天窗口内分批补跑,超期全量重导 |
| Q5 | Hive 不支持行级 UPDATE/DELETE,变更怎么落地? | 数仓与存储 | Hive 是只追加的分区表,行级变更有两种落地模式 |
| Q12 | MySQL 表加字段了,同步链路和 Hive 表怎么办? | 数仓与存储 | 分变更类型 |
| Q10 | 小文件问题怎么产生的?concatenate 合并的代价? | 数仓与存储 | 小文件 = 调度频率 × 分区粒度 × task 数 |
| Q23 | Kafka 落 HDFS 用的是什么数据格式? | 数仓与存储 | 两层的格式分工,各取所长:落地层(Dsink 写 HDFS)用 JSON 文本行… |
| Q15 | 敏感字段加密的密钥怎么管理?加密在哪一层生效? | 安全与合规 | 密钥和配置分离:任务创建时通过加密配置指定哪些字段要加密,CreateSecre… |
| Q8 | 数据延迟了怎么排查? | 可观测与排查 | 按链路四段从上游往下逐段看水位 |
| Q9 | Kafka 消息堆积怎么处理?扩分区有什么坑? | 可观测与排查 | 堆积先定位原因:消费能力不足就加消费者实例或优化消费逻辑(批量拉取、异步写);生… |
| Q21 | Spring AOP 埋点是怎么实现的?会不会影响接口性能? | 可观测与排查 | 切面设计:@Aspect + @Around 环绕通知,切点挂在核心接口层(任务… |
| Q14 | 双写模式的数据比对具体怎么做?不一致以哪边为准? | 迁移与存量 | 双写期两条链路同源:快照链路写原表,Binlog 链路写 _bak 表 |
| Q25 | 滴滴内部之前没有 MySQL→Hive 这条链路吗?(项目合理性质疑) | 迁移与存量 | 这是必问的合理性拷问,答不好项目可信度就塌 |
| Q24 | 实习里遇到的最大挑战是什么?(STAR 模板,细节替换成你的真实经历) | 项目叙事与总纲 | 框架与话术通用;细节必须换成你记忆里真实发生的事——面试官会顺着一个细节往下挖两… |
| Q26 | 为什么会想到做这条链路?(立项动线) | 项目叙事与总纲 | 诚实的话术:立项不是实习生拍的,但立项逻辑必须讲得出 |
| Q28 | 实习期最大的收获是什么?(收尾必问) | 项目叙事与总纲 | 不要罗列五条——挑两条深的,每条都拿链路里的真实设计当证据,最后收一句"对我之后… |
追问 · 数据源与采集
Q1 Binlog 为什么必须用 row 格式?
(采集细节由采集团队负责,但原理必须懂——被问到就这么答。)Binlog 有三种格式。statement 记录原始 SQL 语句,问题是语句重放不保证确定——NOW()、UUID() 这类非确定性函数,以及不带 ORDER BY 的 LIMIT,在下游重放出来的数据和主库不一致。row 记录的是每一行的实际变更(变更前后镜像),数据是确定的,而且带着主键和每个字段的值。mixed 是默认 statement、遇到不安全语句自动切 row。
数据同步场景必须用 row,因为下游要的就是「哪一行变成了什么」这个行级事件——只有 row 格式带完整的行镜像,statement 里一条 UPDATE t SET x=x+1 根本没法还原出受影响的每一行。代价是日志量更大,但这是同步场景必须付的成本。
关联:Q2
Q2 Canal 的底层原理?
采集链路的对接配置是我做的,原理我了解到协议层:Canal 的本质是把自己伪装成 MySQL 的一个 slave。MySQL 主从复制时,从库会向主库发送 dump 协议请求(带上 binlog 文件名和位点),主库就把 binlog 事件流持续推给从库。Canal 用同样的协议向 MySQL 要 binlog,收到事件流后自己解析成结构化的变更对象——库名、表名、事件类型(INSERT/UPDATE/DELETE)、字段列表、变更前后镜像——再投递到消息队列。
两个关键点:一是位点管理,Canal 记录消费到的 filename + position(或 GTID),重启后从上次位点续传,这是不丢数据的基础;二是高可用依赖 ZooKeeper 做抢占锁,同一时刻只有一个 Canal 实例在消费。
Q16 Binlog 位点是怎么管理的?"断点续传"具体怎么实现?
位点就是「消费到了 binlog 的哪个位置」——由 binlog 文件名 + 文件内偏移量组成(开启 GTID 的实例也可以用 GTID 集合表达,语义等价且更容易迁移)。Canal 在消费到一批事件后定期提交位点——持久化到 ZooKeeper 或服务端存储,不是只放内存里。
断点续传的实现分两个层面。Canal 服务层:实例重启后从上次提交的位点继续向 MySQL 发 dump 请求,主库从这个位置之后推送——已确认消费的数据不会重复采集。任务配置层:任务配置里有 offset 字段(Binlog 起始位置),首次创建任务时可以指定起始位点;位点丢失或要重放历史时,按锚点(批次时间)定位后把位点拨回去重新消费,配合下游幂等收敛——正常链路不靠改 offset 切全量/增量:Dsink 从建链持续落湖,切分发生在处理侧(锚点为界)。
位点丢了怎么办是常见追问:位点丢失只能从配置的起始位点(或最早位点)重放,靠下游主键幂等收敛——这就是为什么下游的幂等设计是整个方案可信的前提。
追问 · 链路架构与选型
Q3 为什么中间要过一层 Kafka/DDMQ,Canal 直接写 HDFS 不行吗?
先说 DDMQ 是什么:滴滴架构部基于 Apache RocketMQ 构建的自研消息队列服务,上层代理兼容 Kafka 协议等多种接入方式、支持 7 种语言客户端,底层统一 RocketMQ 存储,附带多租户与运维体系。链路里走 Kafka 还是 DDMQ 的现实逻辑:默认 Kafka;部分业务线的消息已经统一收编在 DDMQ 平台上(统一运维、多租户管控),这些团队的任务就走 DDMQ 链路。
四个理由。一是削峰:源库一次大事务或批量 update 会瞬间产生海量 binlog,直接落 HDFS 既写不进去也会把 Canal 压垮,MQ 是缓冲池。二是解耦:Canal 的生命周期和数据消费完全分离,下游处理出错不影响采集。三是回放:下游处理逻辑有 bug 时,重置消费位点就能重放数据——这是后面数据修复能力的地基。四是一份数据多订阅:同一份 binlog 可以同时给实时数仓、索引构建、缓存失效等多个消费方。
还有一个隐含原因:Dsink 是按 micro-batch 落 HDFS 的,天然需要 MQ 做聚合缓冲;直连的话小文件问题和可用性问题都会爆炸。
Q13 为什么用 Spark 批处理而不是 Flink 实时写?这个方案的"实时"到底是什么级别?
先说清架构事实:链路的前半段(Canal → Kafka → HDFS)本来就是实时的,数据已经实时入湖;Hive 这一段按小时/天调度是刻意的批处理设计,不是时效短板。选 Spark 批处理是四个权衡:一是数仓下游本来就是离线分析,小时级时效够用,而实时性已经由 Kafka→HDFS 入湖段保证了——Hive 只是消费湖里数据的批量视图;二是批处理模型简单稳定,失败重跑语义清晰,和公司调度体系、Hive 生态原生兼容;三是 Flink 流式写 Hive 要处理 checkpoint、barrier 对齐、小文件更碎、事务性 sink 这一堆复杂度;四是团队技术栈。
什么时候该换 Flink:单表时效要求到分钟级、或下游开始做实时报表/实时特征——那时正确的演进是 Flink + Iceberg/Hudi 这类支持流写的湖表格式,而不是硬把 Spark 调度压到分钟级(小文件和调度压力会先爆)。能讲清楚「什么场景该换」比坚持某个技术选型更加分。
Q29 为什么不能直接在 MySQL 上做分析?非要同步到 Hive?
一句话开场:不是技术上完全不能,是两种负载放在一起会互相伤害——隔离本身就是这条链路的第一价值。
第一层,存储结构为完全不同的负载设计。MySQL 是行存 + B+ 树索引,为高频小事务优化——按索引点查、小范围读写,毫秒级返回。分析查询是反过来的:全表或大范围扫描、百万行级 group by 和多表 join——行存要把整行读出来才能取一列,B+ 树索引用不上。Hive 底层是列存(ORC/Parquet),只读分析涉及的列、压缩比高,还靠分区裁剪直接跳过无关数据。同样的聚合,两边差的是数量级。
第二层,资源互抢,而业务库是要保命的。一个分析大查询会打爆 IO 和 buffer pool、占满连接池,直接拖慢线上交易——InnoDB 的 MVCC 虽然让读写不互相阻塞锁,但脏页刷新、缓存污染是实打实的。分析查询慢一点无所谓,交易慢一秒就是事故,所以 DBA 根本不会给业务库开分析权限。
第三层,能力与生命周期都不匹配。业务库为了交易性能通常分库分表——数据物理上散了,跨库 join 和全局聚合做不了;而且业务库要定期归档清理保容量,分析要的恰恰是全历史。数仓正好相反:全历史留存、多业务线数据进同一个仓才能跨源 join——这也是数据中台"统一口径"能成立的前提,我们的链路就是把多源数据按统一口径送进仓里那一步。
收尾补一个诚实的边界:"当然,如果数据量很小、就是跑个简单日报,直接在 MySQL 从库上查也行——杀鸡不用数仓。但当表到了千万行、分析要跨多源 join、或者分析负载会干扰线上交易时,就必须入仓。我们平台是公司级多业务线,量级早就过了这个线。"
(衔接:这一题答完顺势就能接 Q13——"那为什么不直接 Flink 实时写 Hive"是同一族的权衡题。)
Q31 业务方的实时性要求怎么满足?最高只能到小时级吗?
分层满足,而不是一条链路硬扛所有时效。先澄清:Hive 这一段最高小时级,但数据本身是实时入湖的(Kafka→HDFS 段秒到分钟级),所以有三种按需取用的方式:
- 报表/看板/即席分析(绝大多数需求):小时级 Hive 分区够用;
- 分钟级时效的场景:升级该表为 Flink + 湖表(Iceberg/Hudi)链路,分钟级可见——这是平台的演进方向,代价是事务性 sink 和运维复杂度(详见 Q13);
- 秒级真实时(在线服务、实时风控):直接消费 Kafka——数据本来就在 MQ 里,根本不必等 Hive。
判断标准是下游用途:分析师写 SQL → 小时级;在线链路 → 走消息直读。一条同步链路不可能也不应该满足所有时效等级,分层才是正解。
追问 · 任务创建与调度
Q17 任务创建为什么要拆成 12 步?顺序能调整吗?
不是随意排列,是按依赖关系分层的。权限类必须最先(RDS 读权限不通过,采集无从谈起);采集与存储链路次之(Canal→MQ 建链、Dsink 落地要先于任何数据入仓);元数据类在中间(Topic Tag、建 Hive 表、系统字段);全量导入任务的注册必须在建表和系统字段之后——表都没有导什么;Spark 增量任务的注册在全量任务之后,执行顺序由调度依赖保证——增量批次依赖全量完成才接续;敏感字段检查和 Tag 状态校验是阻断性门槛,收尾把关。
拆成独立步骤的好处:失败可精确定位(卡住时能报出具体哪一步、哪个参数有问题,反馈给用户修改后重新提交即可,而不是一个黑盒);可扩展(后来要加敏感字段加密,就是插入"加密字段处理"和"创建密钥 ID"两步,主流程不动)。
还有一个常被追问的时序细节:Hive 表在第 5 步就建好了,但数据要等首次调度才开始进——全量导入本身就挂在首次调度实例上(55 分规则决定是下个整点还是下下个)。中间的空窗期表是空表,这是设计内的:表先就位,调度系统才能把它注册为写入目标。
Q22 "数据采集完整才打 Tag"——怎么判断数据完整了?第 4 步不就已经创建 Tag 了吗?
先拆矛盾:第 4 步创建的是 Tag 的"定义",数据完整时打的是 Tag 的"实例"。第 4 步在调度中心注册 Tag 的名字与检查规则(装信号灯、定规则);运行时每小时数据到齐后由采集侧打出该小时的 Tag 实例(亮灯),调度中心看到信号才触发依赖它的调度任务——一个是装灯,一个是亮灯,不矛盾。
再答"怎么判断完整":依据是 Binlog 是严格有序的单调流。采集团队只要收到了下一个小时的第一条事件,就能断定上一个小时的数据必然全部到齐——位点已经越过小时边界,不可能再有迟到的上小时数据插队。所以"完整"的判断本质是消费水位越过小时边界。静默表也不会卡住:MySQL 定期产生心跳/日志滚动事件,位点持续推进,水位照常越界。
这个判断和 55 分 cutoff 是同一套机制:调度中心在整点附近运行检查,采集侧确认"上小时最后一个事件已消费、水位已越过边界"后打 Tag。10:56 提交的任务来不及在 11:00 检查点前完成注册并纳入本轮调度计划,首次调度只能到 12:00——打 Tag 时机、55 分规则、错过检查点是同一机制的三种表现。再补一层门槛分工:首次实例跑全量导入(走任务就绪检查,不依赖数据 Tag);首个增量批次读的是全量执行之后的那个小时的目录、依赖那个小时的 Tag——cutoff 决定全量落在哪个整点,Tag 决定增量在哪个周期可跑,两套门槛各管各的。
(注:水位上报的具体实现属采集团队与调度中心的细节,面试话术讲到"水位越过小时边界即完整"这一层即可。)
Q30 按小时/天调度,每调度一次具体执行什么?表数据是全量刷新吗?前置快照来自哪里?
以默认的 latest 快照模式为例,一次调度 Spark 做五件事:
- 定位输入:读当前调度周期对应的 HDFS binlog 分区目录(Dsink 已实时落好的增量数据);
- 解析事件:INSERT/UPDATE/DELETE 三类事件还原成行级变更;
- 周期内去重:按主键去重,同一主键多次变更按 binlog 位点保留末态;
- 与上一份快照合并:把本周期变更应用到上一个调度周期产出的快照分区上——插入新增、更新覆盖、删除按
saveDelete配置剔除或打标记; - 写出新分区:合并结果写成一个新的完整快照分区,顺手 concatenate 合并小文件。
所以"表是全量刷新吗"的准确回答是:分区级快照刷新——每个调度周期产出一个"当前时点的完整数据镜像"分区,下游查最新分区就是查全表;但生成它的输入只有"增量 + 上一份快照",不是把全表重新导一遍。前置快照就来自上一个调度周期的产出分区,链式衔接:第一天是 DataX 全量导入的首个分区,之后每期合并滚动。表太大时快照模式成本会涨,可以切"保留变更模式"(事件原样追加、下游自己聚合)或演进到 Hudi/Iceberg 的增量合并。
追问 · 全量导入与衔接
Q4 全量和增量之间怎么保证不丢数据、不重复?
不丢靠锚点,重复靠下游幂等收敛。 全量导入(挂在首次调度实例执行)完成时,系统记录批次时间(wholeLoadBatchTime)——它标记的是全量快照对应的 binlog 位点:值对应快照读视图的时刻,记录动作在导入完成之后(mysqldump --master-data 同款语义)。增量任务只处理事件时间在锚点之后的数据——锚点之后的变更一条不少,这是不丢的记账;锚点之前、建链之后落的那段湖是保险余量,不处理。
不丢还有一层结构保障:增量流从建链(第 2 步)就开始落湖,早于快照——所以快照读视图之后的每个事件必然已被采集,这是重叠能存在、不丢能成立的地基。重复出现在重叠窗口(=全量导入执行期间):同一条记录可能既在全量快照里、又在增量处理范围里,但下游 Spark 合并按主键去重、保留最新,重复写入会被幂等地收敛掉。所以整个链路的口径是:at-least-once 投递 + 主键幂等 = 业务上的不丢不重。wholeLoadBatchTime 同时还是数据修复的基准——补跑、回溯都以它为参照。
再补一个时序细节:实时落湖(Dsink)从建链就开始,不等全量。全量导入期间 binlog 的增量变更已经同时落进 HDFS——增量流盖住建链以后的一切(不丢的地基),增量处理只做锚点之后(不重的来源),重叠窗口=全量导入执行期间,由"锚点为界 + 主键去重保末态"收敛。这层还和"目录即分区"咬合:全量挂在整点实例、读视图在整点后不久,首个增量批次读的是下一个小时的目录——保险余量(建链→读视图)落在更早的目录里,天然不被任何增量批次读到,"落湖但不处理"在目录层面自动成立。所以"全量期间的数据去哪了"的答案是:一部分在全量快照里、一部分在增量分区里,合并后不重不丢。
Q11 全量导入会不会打爆源库?怎么控制?
会,不控制就是一次对生产库的表扫描攻击。四层控制:一是 DataX 分片限流——按主键 range 把表切成多个 task,每个 task 独立限速(字节/记录数),总并发可控;二是低峰窗口执行,避开业务高峰;三是读从库/备库,不碰主库;四是失败按分片断点重试,不用整表重来。全量完成后记录批次时间再切增量,所以「慢慢导」没有正确性代价,只有时效代价——这是全量链路可以放心限流的原因。
Q27 "快照全量 + Binlog 增量"这个混合模式是怎么想出来的?
关键话术:这个方案不是想象出来的,是被约束一步步推出来的。
第一环是硬约束:binlog 天然只记录变更、不携带历史——所以"只靠增量"从原理上就不可能,首次同步必须有全量打底。这不是设计选择,是数据源的物理事实。
第二环是现成遗产:平台原有的 DataX 快照导入能力是现成的、被生产验证过的——"全量打底"这一半直接复用,不需要发明。
第三环是衔接点:全量导入完成时记录批次时间锚点(T 对应全量快照的读视图时刻)——全量数据 = 快照 ≤ T,增量数据 = binlog > T,两段拼接整体完整;重叠窗口的重复由主键幂等收敛。
点睛句(非常加分):"其实 MySQL 自己搭从库就是这个流程——先全量备份,再从备份位点回放 binlog 追平。我们是把数据库领域验证了几十年的模式,搬到了数仓链路上。"——把设计的权威性锚定到数据库的成熟实践,比说"我设计的"高一个层级。
再能补一刀:"后来了解到 Flink CDC、DataX+Canal 组合这些业界方案,底层都是同一个思想——全量快照 + 增量追加 + 幂等合并。区别只在实时程度和合并发生在哪一层。"
追问 · 一致性与幂等
Q6 同一主键一分钟内变更 5 次,怎么保证保留的是"最新"?
顺序性分两层保证。传输层:Canal 投递 MQ 时按「表 + 主键」做分区键,同一主键的所有事件永远落在同一个分区,分区内严格有序。计算层:Spark 合并时按 binlog 位点排序取末态——位点(filename + position)在单库内是全序单调的,比业务时间戳可靠(时间戳可能重复、可能回拨)。
所以不管 5 次变更落在几个调度批次里,最终收敛到的都是位点最大的那条。这里能引申的点:如果分区键设计错了(比如用随机 key),同一主键的事件分散到不同分区,跨分区就没有顺序保证,末态就可能是错的——分区键是这个方案里最隐蔽的关键设计。
Q7 整条链路是 exactly-once 还是 at-least-once?
诚实回答:链路整体是 at-least-once,靠下游幂等把效果做到 exactly-once。MQ 在消费者失败时会重放、Spark 任务失败重跑会重读分区,这些都会产生重复投递——追求端到端 exactly-once 需要事务性 sink(比如 Flink 两阶段提交 + 事务表),复杂度和运维成本都高。
我们的做法是把「去重」放在最确定的位置:Spark 合并按主键保留末态,天然幂等——同一批数据重跑一百遍,结果都一样。再配合全量批次时间锚点和分区覆盖写,「可重跑、可重放、结果收敛」,对数仓场景来说,这套「at-least-once + 幂等」的性价比远高于强 exactly-once。
Q33 这条链路的一致性和性能分别怎么保障?(总纲题)
一致性三个机制(详见 Q4/Q7):批次锚点(全量完成记录 wholeLoadBatchTime,增量自锚点续传,不丢);主键幂等去重(重放/重叠窗口收敛,容重);写入顺序(先内容、再元数据、后缓存——任何一步失败停在可恢复状态)。整体口径 at-least-once + 幂等 = 业务不丢不重。
性能按三段各有关键手段:入湖段(Dsink 常驻流式 + 按时间滚动文件 + 消费限流,秒到分钟级);入仓段(Spark 只处理增量输入 + 列存 + concatenate 小文件合并 + 并行度调优);源库保护(全量导入 DataX 分片限流、读从库、低峰窗口,Q11)。延迟量级:入湖 P95 < 3 分钟,入仓由调度周期决定(Q13/Q30)。
追问 · 修复与容错
Q18 采集侧的"位点重置与回放"怎么做的?重放不会把下游写重复吗?
位点重置就是把消费位点拨回某个历史位置(按位点或按时间戳定位),Canal 从那里重新投递数据。重放一定产生重复投递——重复是设计内允许的,靠下游两级幂等收敛:Spark 合并按主键保留末态(重放的一百条和原来一条效果一样),分区覆盖写。
使用场景主要有两个:处理逻辑有 bug——修复代码后,把位点重置到 bug 引入前,重放那段时间的数据,用正确逻辑重新计算;binlog 数据丢失或损坏——重放可达的历史,更早的走全量回溯。约束也要说,而且是硬约束:原始 Binlog 数据在 HDFS 只保留 3 天,重放窗口超出 3 天的数据已经被清理,只能全量重导。
这套"位点重置 + 幂等收敛"和 Gap Batch 补跑、全量回溯一起,构成了修复能力的三层粒度:单事件级(死信重放)、批次级(Gap Batch)、全量级(回溯重导)。
Q19 死信队列的机制展开讲讲?偏移量记录了什么?
先讲为什么需要:解析失败的异常事件(数据损坏、字段截断、schema 不兼容)如果留在主流程里重试,会变成"毒丸消息"——反复失败反复重试,把整个分区的消费堵死;如果直接丢弃,就是丢数据。死信队列是「不阻塞主链路」和「不丢数据」之间的折中:异常事件原样转存 + 记录来源,主链路继续走,事后集中修复。
记录的偏移量是修复的关键:每条死信记录它原始的 Kafka topic / partition / offset(以及对应的 binlog 位点)。作用有两个——一是可追溯,出问题能查到原始消息长什么样;二是可重放,修复了处理逻辑(或拿到了能解析的补发数据)之后,按偏移量把事件重新投递回主链路,从失败点继续,而不是全量重来。
修复流程:死信告警 → 定位解析失败原因(多为脏数据、字段超长、schema 不兼容)→ 修正逻辑或拿到正确数据 → 按偏移量重放 → 重新入湖 → 复核。98% 的修复率就是「修复成功重新入湖的条数 / 死信总条数」;剩下 2% 是源头数据本身损坏无法解析的,标记跳过并告警,不阻塞主链路。
Q20 Gap Batch 补跑是怎么工作的?补跑和增量会冲突吗?
背景:Spark 是按调度周期(小时/天)处理分区的,如果某个批次调度失败、或者上游数据晚到,这个批次的分区就缺数据——Gap Batch 就是把指定范围内缺失的历史批次补跑。实现上按批次时间生成要补的任务序列,自动处理批次间的依赖关系(前一批没成功,后一批补跑没有意义),按序执行。
和增量的关系:补跑的对象是历史分区,增量调度读的是当前周期分区,两者写的是不同的分区目录,物理上不冲突。真正的冲突场景是「补跑 + 期间又有增量」——这时靠主键去重收敛:历史批次补的是旧事件,增量是新事件,合并时按位点排序,末态依然正确。指定时间范围回刷是同一套机制的更粗粒度用法:圈定一段时间,把范围内的批次全部重跑。
硬边界要说清:原始 Binlog 数据在 HDFS 只保留 3 天,所以 Gap Batch 只能补 3 天以内的缺失批次;超过 3 天的分区数据已被清理,唯一的修复手段是全量回溯重导。这也是为什么延迟告警阈值必须远小于 3 天——故障必须在数据过期窗口内被发现和闭环。
这三层修复粒度可以连起来记:单事件级(死信重放)→ 批次级(Gap Batch / 范围回刷)→ 全量级(回溯重导),按故障的影响面选最小成本的修复手段。
Q32 调度任务异常停了好几天,积压的数据怎么补回来?
先纠正一个常见误解:调度停了几天,Kafka 并不会堆积"要追的消息"——因为 Dsink 是常驻流式消费,一直在把消息实时落到 HDFS,Kafka 的消费位点正常推进。要"追"的不是 Kafka 的消息,而是 HDFS 上积压的几天分区没被合并进 Hive——压力在 HDFS→Spark 段,不在 Kafka。
恢复动作有一个硬前提:故障必须在 3 天内被发现并恢复——原始 Binlog 数据在 HDFS 只保留 3 天,超期已被清理,那时唯一的手段是全量重导。在 3 天窗口内,恢复动作是:Gap Batch 按批次时间生成缺失批次序列,分批限流补跑——数据都在 HDFS 分区里,完全不碰 Kafka(不重放、不动位点)。四条缓解纪律:
- 分批限流:按依赖序补跑,控制并发与写入速率,避免一口气打爆 Spark 队列和 Hive;
- 不碰 Kafka:历史数据已在 HDFS,无需重放消息;
- 补跑期间实时调度照常:当前周期继续正常跑,补完旧的自动对齐,交叉部分主键幂等兜底;
- 极端兜底:只有 HDFS 数据也丢了才需要从 Kafka 位点重置回放——此时按偏移量分段重放 + 下游幂等收敛,且 Kafka 的保留期必须大于最大可容忍故障时长,这是运维红线。
这道题的元认知:链路里"实时落湖"这一层的隐藏价值就是把消息系统的压力和计算系统的故障解耦了——计算层停摆,消息层数据毫发无损地躺在湖里等它(在 3 天保留窗口内)。而"3 天"这个保留期又反过来定义了告警阈值的设计约束:delayAlarm 必须让团队在窗口内响应,这套设计是一环扣一环的。
追问 · 数仓与存储
Q5 Hive 不支持行级 UPDATE/DELETE,变更怎么落地?
Hive 是只追加的分区表,行级变更有两种落地模式。合并模式(latest):每次调度时,Spark 读本周期分区的 binlog 增量,和历史最新状态按主键做合并(merge),保留每条主键的最新版本,写出新分区——下游看到的是「当前状态」表。保留变更模式:所有事件原样落表,带事件类型标记(INSERT/UPDATE/DELETE),下游自己做聚合——适合审计场景。
DELETE 的处理由 saveDelete 配置控制:开启时删除事件作为一条带删除标记的记录保留(可审计、可回溯),关闭时物理剔除、不进结果集。如果场景要求真正的行级更新,现在业界会选 Hudi/Iceberg 这类支持 ACID 的湖表格式,我们当时用分区+合并模型是平台既有体系的取舍。
关联:Q30
Q12 MySQL 表加字段了,同步链路和 Hive 表怎么办?
分变更类型。加字段(含中间插入):binlog 的 row 事件和落 HDFS 的 JSON 都带字段名,Spark 解析按字段名取值,与列顺序无关——所以就算 MySQL 把字段插在表的中间,平台比对 schema 发现新列后自动 ADD COLUMNS 追加到 Hive 表尾部即可:新数据写新列,旧分区该列读出来是 NULL,全程不停同步。两层按名对齐是位置无关的保证:数据层 JSON 带字段名、元数据层 Hive 是 schema-on-read 按列名匹配视图。真正要人工处理的是改类型(string→int 历史数据解析失败,需重导)、重命名(旧名数据语义断裂)、删后重加同名(历史分区语义混乱)这三种破坏性变更;尾部加字段则是最常见的低危变更,自动兼容。改类型、改字段名、删字段:破坏性变更,自动兼容做不到,会触发告警走人工——通常停写、重建表、全量重导。autoCreateTable 配置就是这套自动建表/加列能力的开关。
关联:Q30
Q10 小文件问题怎么产生的?concatenate 合并的代价?
小文件 = 调度频率 × 分区粒度 × task 数。小时级调度 + 按天分区,每天 24 轮调度、每轮每分区至少写一批文件;Dsink 的并发写入进一步放大文件数。危害是 NameNode 元数据压力、Hive 查询规划开销、MapReduce/Spark 的 map task 数暴涨。
concatenate 开启后,Spark 任务写完分区会触发小文件合并——本质是把分区内的小文件重写成少量大文件。代价是合并本身要多一遍完整的读写 IO,占用集群资源和时间窗口;但它换来后续所有查询的效率。所以这是个开关参数:查询频繁的核心表开,写入量巨大的原始层按需控制。更上游的治理是合理设计分区粒度,别让调度频率和分区粒度相乘出天文数字。
Q23 Kafka 落 HDFS 用的是什么数据格式?
两层的格式分工,各取所长:落地层(Dsink 写 HDFS)用 JSON 文本行存——binlog 事件原样序列化落盘。选 JSON 的理由:① 保真——原始事件一字不改地存下来,schema 无关,后面想怎么解析都行(这也是位点重放能成立的前提);② Dsink 流式写文本最简单、吞吐高,不用关心 schema 演进。数仓层(Spark 写 Hive)用列存(ORC/Parquet)——清洗转换后的结构化数据按列存压缩,查询快、存储省,这才是分析师直接查的表。
一句话:落湖保真用行存文本,入仓查快用列存——两层格式分工是数据湖仓的通行做法。(若被问"为什么不直接落 Parquet":落原始层时还没有统一的 schema 语义——binlog 事件是"表 + 行变更",保持 JSON 原样可以让解析逻辑全部收敛在 Spark 一层,改解析逻辑只需重放,不用动 Dsink。)
关联:Q30
Q34 Spark 怎么知道读哪个分区的数据?HDFS 上是按分区存好的吗?
是的,分区在 Dsink 落地的那一刻就物理组织好了,Spark 只是按确定性规则读路径清单。三层机制:
一、目录即分区。Dsink 常驻消费 Kafka 时,按 binlog 事件的时间把消息写进对应的小时目录(形如 …/dt=20260917/hour=13/),文件按时间加大小滚动切分——"哪个分区有什么数据"在写入那一刻就确定了,不靠 Spark 去发现。
二、Spark 按调度周期算出目录清单。路径是确定性的日期模板:小时任务读上一个小时的目录、天任务读前一天的所有小时目录;Gap Batch 补跑就是把这条规则换成缺失批次的目录列表(所以 Q32 里"追几天的数据"落到物理层,就是读那几天的目录,碰都不用碰 Kafka)。
三、顺序的权威仍是 binlog 位点,目录只是物理组织。极晚到的变更落在后面的目录里也不影响正确性——合并时按主键加位点保末态,位点才是排序的权威。
加分细节(半截文件防护):Dsink 滚动写文件时用临时名,写完才改成正式名,Spark 只读已封闭的文件——保证不会读到写了一半的数据。(目录粒度、命名规范以平台实现为准,判断逻辑"目录即分区、路径模板可推导"是通用做法。)
追问 · 安全与合规
Q15 敏感字段加密的密钥怎么管理?加密在哪一层生效?
密钥和配置分离:任务创建时通过加密配置指定哪些字段要加密,CreateSecretIdTaskStageBean 向密钥服务申请一个 SecretId——任务配置里只存这个引用,明文密钥不落任务配置、不落数据平台。加密在计算层生效:Spark 任务处理时通过 SecretId 从密钥服务取密钥,在数据写入 Hive 之前完成字段级加密,所以 HDFS 和 Hive 里落的一直是密文。发布流程里加密字段处理和密钥创建是独立的两个 Stage(第 7 阶段),保证「要加密的表必然先有密钥」这个顺序约束。密钥轮换通过 SecretId 版本化实现——新版本密钥加密增量数据,读取时按版本选择密钥解密。
追问 · 可观测与排查
Q8 数据延迟了怎么排查?
按链路四段从上游往下逐段看水位。第一段看源:MySQL 写入 QPS 是否突增(大事务、定时批量 job),源头洪峰属于正常延迟,等峰值过去;第二段看 Canal:采集位点滞后多少、实例有没有重启或报错;第三段看 MQ:消费堆积量(consumer lag)——堆积持续增长说明消费能力不足或消费者挂了,还要看分区倾斜,某个热点表把单个分区打满;第四段看 HDFS 和 Spark:Dsink 落地文件是否按时生成、Spark 调度有没有失败或资源排队。
实践中大部分延迟是两类:源头批量写入的洪峰(等或扩消费),和 Spark 任务资源不足排队(提并行度)。平时靠三个告警阈值(延迟、错误数、数据量)先于人工发现问题。
Q9 Kafka 消息堆积怎么处理?扩分区有什么坑?
堆积先定位原因:消费能力不足就加消费者实例或优化消费逻辑(批量拉取、异步写);生产突增就靠 MQ 缓冲等峰值过去;分区倾斜(热点 key 打满单个分区)要调整分区键或做热点拆分。
扩分区的坑:Kafka 按 key 的哈希路由分区,扩分区后 key 到分区的映射变了——同一个主键的新旧消息可能落到不同分区,分区内有序性被打破,而我们行级末态的正确性恰恰依赖这个有序性。所以扩分区不是随便做的:要么只对新建 topic 生效,要么扩容后配合一次全量重导修复末态。我们优先的做法是垂直扩容——消费者实例内提高并发,不动分区数。
Q21 Spring AOP 埋点是怎么实现的?会不会影响接口性能?
切面设计:@Aspect + @Around 环绕通知,切点挂在核心接口层(任务创建、任务发布、状态查询、数据预览等 40+ 接口),统一拦截而不是每个接口手写日志。Around 里记录五要素:入参(脱敏 + 超长截断)、出参(截断)、耗时、执行状态(成功/失败)、异常栈,加上调用方标识,标准化成统一的日志结构输出。
三个工程细节。一是大报文处理:入参出参直接序列化可能很大,截断到阈值并打标记,既保留排查线索又不撑爆日志。二是不阻塞业务:日志写入走异步(内存队列 + 后台落盘),切面本身只做捕获和投递,微秒级开销,接口无感。三是统一执行状态:接口返回统一响应包装(状态码 + 消息),切面从包装里读状态,标准化输出让"接口成功率"变成可聚合的指标——调用统计看板就是对这些日志按接口维度聚合出来的,线上排查时按 trace 串起一次调用的完整入参出参,不用再登服务器翻日志。
被追问"为什么不用 SkyWalking 这类现成 APM"时的答法:APM 解决的是通用性能观测,我们要的是业务语义的执行状态(任务创建成功/失败、发布卡在哪个阶段),自研埋点更轻、字段更贴合排查需求,两者不冲突。
追问 · 迁移与存量
Q14 双写模式的数据比对具体怎么做?不一致以哪边为准?
双写期两条链路同源:快照链路写原表,Binlog 链路写 _bak 表。比对方法:把两边对齐到同一批次边界后,按主键分块算 checksum 做全量比对,差异块再做字段级 diff;数据量大时先抽样。以源库为准——MySQL 是事实源,比对发现差异说明 binlog 链路有丢或错,处理方式是差异行重导,严重时整表重新全量。
切换的门槛是连续多个批次比对一致才允许 Handover(停快照、Binlog 接管)——比对的本质不是修复数据,而是给「Binlog 链路可信」这个判断提供证据,切换决策靠数据说话。
Q25 滴滴内部之前没有 MySQL→Hive 这条链路吗?(项目合理性质疑)
这是必问的合理性拷问,答不好项目可信度就塌。标准答案分三层。
第一层:有,但形态不同。 在 Binlog2Hive 之前,平台主流的同步方式是快照全量——按天定时把整表全量导入 Hive,DataX 全量导入这套能力平台早就有,分析师的 T+1 报表都靠它。
第二层:快照链路的三个痛点,就是做 Binlog2Hive 的理由。 一是浪费——表越大每天全量重写的存储和计算开销越大,而数据里绝大多数根本没变;二是时效只有 T+1,业务要小时级时效给不了;三是每次全量都要完整扫描业务表,源库压力大。
第三层:所以定位是"长出增量链路 + 承接存量迁移",不是替代。 快照继续负责首次打底和数据修复,日常变更走 Binlog 增量,两边用批次时间锚点衔接;平台里大量存量任务还是快照任务,所以专门设计了 Snap2Binlog 迁移能力(双写/切换/迁移三种模式)让老任务平滑过来。
收尾一句:"所以这不是从零发明一个同步工具,而是在平台既有原子能力上长出一种新的链路形态,并给存量任务设计好迁移路径——大厂内部基础设施的演进本来就是这个模式,也是我在这段实习里学到的工作方式。"
(衔接:这题答完自然引到 Q14 双写比对和 Snap2Binlog 三种模式——那里是你负责的部分。)
追问 · 项目叙事与总纲
Q24 实习里遇到的最大挑战是什么?(STAR 模板,细节替换成你的真实经历)
框架与话术通用;细节必须换成你记忆里真实发生的事——面试官会顺着一个细节往下挖两层。
S(背景):有一次接到反馈,某张表的数据不对——Hive 里的数据比 MySQL 源库少了一部分。
T(任务):我负责定位是链路哪一段丢的,并修复数据。
A(行动):按链路四段逐段查。先看 Canal 采集正常、位点在推进;再看 Kafka 消费也没有堆积;然后对 HDFS 落地文件和 Hive 分区的数据量做比对,发现 HDFS 里其实有这条数据——问题出在 Spark 合并这一层。最后定位到是一批历史数据补跑时,和当天的增量分区在同一个调度周期里撞车,合并逻辑按位点排序时补跑批次被当成了"旧数据"没有生效。
R(结果):和引擎团队确认了合并逻辑的排序键设计,把补跑数据的排序权重修正后重跑该分区,数据对齐。之后我把这类"补跑 + 增量撞车"的场景沉淀进了修复机制文档——也就是页面里说的三层修复粒度(死信重放 / Gap Batch / 全量重导)各自的适用边界。
这段故事的面试价值:① 展示了"链路四段排查方法论"不是背出来的,是真用过的;② 展示了跨团队协作(和引擎团队确认逻辑);③ 展示了把个案沉淀成机制的意识——正好呼应一致性那套设计。
简历表述与自证要点
简历上用精简版(每点 1~1.5 行,细节留给面试——对应上方 Q1~Q24):
滴滴 · 后端研发实习生(数据中台 / 同步中心) 2025.11 - 2026.04 面向公司内部业务团队构建企业级数据中台,统一多业务线数据口径、消除数据孤岛,对外提供统一的数据服务能力。
- 实时数据同步主链路:负责 MySQL → Hive 同步链路(Binlog2Hive)的编排与落地——Canal 采集 Binlog 变更、Kafka/DDMQ 解耦采集与计算、Spark 3 ETL 按时间分区落地 Hive,采用"快照全量 + Binlog 增量"混合模式与位点断点续传,数据入湖延迟 P95 < 3 分钟。
- 数据一致性保障与修复机制:以全量批次时间为锚点衔接全量/增量,主键幂等去重实现最终一致;支持位点重置回放、死信队列修复(成功率 98%)与历史批次补跑,覆盖单事件 / 批次 / 全量三级修复粒度。
- 可观测性建设:基于 Spring AOP 实现统一日志埋点,覆盖 40+ 核心接口,支撑调用统计看板与线上问题排查。
投数据 / 后端岗、想把采集段纳入职责时,把第 1 点开头换成:"负责 Canal 采集链路搭建与 MySQL → Hive 全链路落地——基于 Canal 订阅 Binlog 变更、配置位点实现断点续传,……(后半句同上)"。两条红线:不写"开发 / 实现了 Canal",不写"自研采集框架";被深挖 Canal 原理就答 Q2,补一句"采集团队提供 Canal 服务,我负责链路搭建配置与上下游契约"。
取舍原则:简历只留「关键词 + 数字 + 独特机制名」——Canal/Kafka/Spark/Binlog/Hive 是筛选关键词,P95/98%/40+ 是证据,"锚点衔接""位点重置""死信队列"是勾提问的钩子;删掉的细节全部变成上方 Q1~Q24 的面试弹药。
三个数字的自证准备(面试官必问"怎么测的/怎么统计的"):
- P95 3 分钟:埋点统计 binlog 事件时间戳 → HDFS 落地时间的差值分布,取 P95;口径是入湖段。追问"到 Hive 呢"→ 答"Hive 按小时/天调度,时效由调度周期决定,数据已实时入湖"(Q13)。
- 98% 修复率:死信事件中修复成功重新入湖的条数 / 死信总条数。追问"剩下 2%"→ 源头数据本身损坏无法解析,标记跳过并告警,不阻塞主链路(Q19)。
- 40+ 接口:随口举两三个例子(任务创建、任务发布、状态查询、数据预览)+ 切面设计(@Around 记录入参/出参/耗时/异常 → 统一格式 → 看板聚合)(Q21)。
Q26 为什么会想到做这条链路?(立项动线)
诚实的话术:立项不是实习生拍的,但立项逻辑必须讲得出。 三步动线:
一、需求端先疼——存量快照任务每天全量重写,资源开销随表规模线性上涨,业务对时效的要求又在提高(T+1 不够用了),这是团队立项的直接动因。
二、平台盘点发现"能力都有、链路没有"——Canal 采集(采集团队维护)、Kafka/DDMQ、HDFS、Spark、DataX 全量导入,原子能力全在;但「MySQL 变更 → Hive 分区」端到端、可配置、数据就绪可控的链路形态是缺的:任务怎么创建、全量增量怎么衔接、下游怎么知道数据就绪,这些编排层是空白。
三、技术可行性有确定解——binlog 本身就是数据库变更的完整有序日志(主从复制就靠它),Canal 这类伪装 slave 的采集方式在业界非常成熟。
我的角色话术:"立项是团队基于存量痛点的判断,我负责把这条链路的形态落地——任务创建的 12 步流程、发布编排、全量增量衔接的打通。"不说"我想到了做这个",说"我把它从立项变成能跑的东西",既真实又不减分。
Q28 实习期最大的收获是什么?(收尾必问)
不要罗列五条——挑两条深的,每条都拿链路里的真实设计当证据,最后收一句"对我之后的影响"。
第一条(技术层面):跨存储的数据一致性思维。
"这条链路横跨 MySQL、Kafka、HDFS、Hive 四个存储,我们没有引入任何分布式事务框架,但做到了不丢不重。方法是把'一致性'这个大词拆成三个具体机制:全量批次时间做锚点,保证增量不丢;主键幂等去重,容忍重复投递;写入顺序先内容、再元数据、后缓存,保证任何一步失败都停在可恢复的状态。这件事让我明白:大多数业务场景的分布式一致性,不需要 Seata 或 TCC,需要的是把顺序和幂等设计清楚。"
第二条(工作方式层面):按链路而不是按模块建立知识结构。
"我的职责是链路编排——采集和引擎是专职团队的,但链路的产出以整条为准,所以我被迫把每一环的契约都搞懂:采集的位点语义、消息的分区有序性、调度的数据就绪信号。后来排查延迟告警时,我能按四段链路逐段看水位定位,而不是只会看自己写的那段代码。这个『按链路建立知识结构』的习惯是我带走的最重要的工作方式——之后我接任何系统,都会先把端到端的数据流走一遍。"
顺带一小条(实用级,加完就停):"还有一个很实用的小收获:按故障影响面选修复手段的分级意识——死信重放、批次补跑、全量重导,能用小动作修的绝不用大动作,每个危险操作都自带后悔药。"
反面提醒:别说"学会了公司的技术栈""熟悉了大数据组件"——这是体验不是收获;也别吹"独立撑起了整条链路"——和你前面讲的分工矛盾。两条收获 + 一条小补充,收在"这些会直接影响我之后做项目的方式",把话题交还给面试官。