主题
时长约 60 分钟 · 11 题 + 反问。本轮新增实习深挖与场景题,提问按正式面试风格顺回答追问。总评:实习深挖三题全部达标(8.5 / 9 / 9);聊天助手模块第五次失分(并发隔离),仍是唯一明确的洞;OOM 有真实故事但缺定位证据链。
一面
1. 自我介绍 — 9.5 分
问题:简单介绍一下自己,两分钟以内。
我的回答:
面试官您好,我叫 XX,杭州某高校计算机技术硕士,27 届。竞赛拿过 ACM、CCPC 省赛银奖,蓝桥杯全国三等奖。技术栈分两块:后端是 Java、Spring Boot、MySQL、Redis、ES、Kafka 这一套;AI 应用方向做过 MCP、Function Calling、ReAct 和 RAG 全链路。实习在某互联网大厂数据中台做了近半年后端研发,负责 MySQL 到 Hive 数据同步链路的落地,包含 Binlog 采集、消息队列和 Spark 计算。核心难点是全量和增量有一段天然的重叠窗口,靠批次锚点配合下游主键幂等去重做到不丢不重,数据入湖延迟 P95 稳定在 3 分钟以内。个人做了两个 AI 项目。一个是终端 CodeAgent CLI,基于 ReAct 和工具调用,做了分层记忆和任务编排;安全上做了 Git 快照回滚——快照放在一个独立的仓库里,回滚全程不碰用户自己的 Git 历史;长上下文超限则用动态阈值的压缩来解决。另一个是 RAG 知识库,大文件断点续传把 1GB 上传从 15 秒优化到 3 秒,向量加 BM25 混合检索让 recall@5 从 63% 提到 85%,回答支持流式和原文溯源。我应聘 AI 应用开发岗位,希望能把后端工程能力和大模型应用经验落地到业务中。谢谢。
复盘:
- 上一轮两处口播问题(「分两种」「等」字)已修正,稿子到顶,边际收益为零
- 四个钩子全埋在弹药位;追问全部顺钩子展开,介绍的任务完成
2. 实习·重叠窗口与批次锚点 — 8.5 分
问题:为什么全量和增量之间有重叠窗口?批次锚点怎么解决的?全量导入跑的几小时里,增量变更去哪了?
我的回答:
因为全量是通过快照机制实现的,而增量是通过 Canal 进行 Binlog 采集实现的,两个步骤无法同时进行。我把它放在任务创建时的第二步和第八步:第二步时 Canal 采集这条链路创建完成,第八步时才给 DataX 配置完成信息并挂到调度系统,而这个时候增量数据就会在快照之前就在进行采集。等首次调度实例才真正跑 DataX 写入 Hive 首个分区,完成时才会记录批次时间作为锚点。如果全量导入跑了几个小时,增量数据还是会照常采集,但此时全量里面没有这些增量变更,只有全量导入完成之后依次执行调度任务,这些增量记录才会被慢慢写入 Hive 表,如果此时已有这条记录,将会按照位点最大进行覆盖。
完整答案:
重叠是结构性的:Canal 采集链路在发布流程第 2 步就建好,从那一刻起 Binlog 持续进 MQ;而全量是第 8 步才执行的快照导入,DataX 写完 Hive 首个分区才记录批次时间。所以从采集开始到全量完成之间,同一行数据既在快照里、又在 MQ 的 Binlog 流里——这个窗口消灭不了,只能收敛。
锚点的角色是把「不丢」钉死:全量完成时记录 wholeLoadBatchTime 作锚点,增量任务从锚点对应的位点之后开始消费,位点之后的变更一条不少。位点之前、已经进 MQ 的部分和快照天然重复,靠下游 Spark 合并按主键保留位点最大的末态收敛——重复写一百遍结果一样,这就是幂等。所以整条链路的口径是 at-least-once 投递加主键幂等,业务上不丢不重。
顺带说为什么锚点用位点不用时间戳:位点在单库内是全序单调的,业务时间戳可能重复、可能回拨,拿时间戳做切分会出事故。
全量跑的几个小时里,增量一直在 MQ 里按「表加主键」分区有序积压;全量完成、锚点记录后,增量调度任务从锚点位点开始消费,这些变更按调度周期逐步写成 Hive 分区——同一主键全量里已有的,位点大的覆盖位点小的。
复盘:
- 12 阶段流程的事实全对,守住了「我负责编排」的边界
- 「锚点管不丢、幂等管不重」的二段分工没点透——窗口不是被消灭,是被收敛
- 升维点没抛:位点全序单调 vs 业务时间戳可能回拨
- 关联:Binlog2Hive链路设计 Q4/Q6
3. 死信队列修复成功率 98% — 9 分(毕业)
问题:这个数怎么统计的?分母是什么?修不了的 2% 是什么情况?
我的回答:
这个数据是修复成功重新入湖的条数 / 死信总条数得出来的。像解析失败的异常事件,如果直接留在主流程里重试,会反复失败反复重试,最后会把整个分区消费堵死;如果直接丢弃,就是丢数据。死信队列就是折中的办法:异常事件原样转存 + 记录位点,主链路继续走,事后集中修复。修不了的 2% 是源头数据本身损坏无法解析,标记跳过并告警,不阻塞主链路。
完整答案:
口径:死信修复成功率 = 修复成功重新入湖的条数 ÷ 进入死信的总条数。
为什么要有死信:解析失败的异常事件是「毒丸」——留在主流程重试,每次消费到它都失败,反复重试会把整个分区的消费堵死;直接丢弃就是丢数据。所以折中:异常事件原样转存死信、记录源端偏移量,主链路跳过继续消费,事后集中修复。
修复的闭环:死信里带着原始事件和偏移量,大多数修复是「修好解析逻辑后按偏移量从源端重放」,重放走正常入湖链路,可观测、可审计。修不了的 2% 是源头数据本身损坏、怎么解析都不合法的——标记跳过加告警,人工确认后归档,不阻塞主链路。
复盘:
- 从上一场「无托底数字」到本场口径完整——分子分母、折中逻辑、2% 边界全部接住,这类题毕业
- 唯一可补半句:偏移量的用途是「修好解析逻辑后按偏移量重放」,转存为了审计、偏移量为了重放
4. 延迟劣化排查 — 9 分
问题:入湖延迟从 3 分钟涨到两小时,怎么排查?要有序的思路。
我的回答:
首先我们有告警基线进行预防,如果超出基线的事件数据还没入湖,会优先通知任务负责人,让其自身判断是否数据有问题或队列资源不足。自身解决不了才会来咨询我们。我一般会先看采集节点,也就是 Canal 到 Kafka 这一块:采集的位点是不是有延迟、MySQL 是不是突然有大批量的 Binlog 写入;然后看 Kafka 是否存在消费延迟,可能是分区倾斜等情况,让 Kafka 团队判断是否要提高并发;然后是 HDFS 和 Spark:看 Dsink 落地文件是否按时生成,Spark 调度有没有失败或资源正在排队。一般而言都是 MySQL 批量写入洪峰和 Spark 资源不足两种情况居多。
完整答案:
分诊先说:告警三阈值(延迟超基线、错误记录数、数据量低于下限)先于人工发现问题,超基线先通知任务负责人自判——是不是自己源库在批量写;处理不了升级到平台侧,按链路四段从上游往下逐段看水位。
第一段看源:MySQL 写入 QPS 是否突增——大事务、定时批量 job,源头洪峰属正常延迟,等峰值过去或临时扩下游消费。第二段看 Canal:采集位点滞后多少,实例有没有重启或报错。第三段看 MQ:消费堆积是否持续增长——增长说明消费能力不足或消费者挂了;再看分区倾斜,热点表打满单个分区。处置优先加消费者实例,不随便扩分区——扩分区改变 key 到分区的映射,同一主键的新旧消息可能跨分区,分区内有序被打破,而末态正确性依赖这个有序性。第四段看 HDFS 和 Spark:Dsink 落地文件是否按时生成,Spark 调度有没有失败、资源是否排队——资源不足就提并行度。
实践中大多数就两类:源头批量写入的洪峰,等或扩消费;Spark 资源不足排队,提并行度。
复盘:
- 四段顺序完整,还多答了一层「分诊流程」(负责人自判 → 升级平台)——真面试加分项
- 每段只给了「看什么」,处置动作补一半:洪峰等或扩消费、Spark 不足提并行度
- Canal 实例重启/报错这个检查点漏了
- 主动堵坑:「提并发是加消费者实例,不是扩分区」——扩分区破坏分区内有序,末态正确性依赖它
- 关联:Binlog2Hive链路设计 Q8/Q9
5. Plan-and-Execute 的 DAG — 7.5 分
问题:拓扑排序怎么实现?有环怎么办?批次之间怎么保证下游拿到上游结果?
我的回答:
首先是提示词中严格说明,让 LLM 生成的时候将每个节点的任务以 JSON 字段进行返回,有没有依赖、依赖列表等信息,然后将每个节点的数据清洗出来,如果存在依赖存进依赖列表里。拓扑排序主要用深搜来判断是否存在环,如果存在直接把计划整理丢给 LLM 重新生成规划。没有依赖的任务分批并行执行,然后将上游结果 result 带给下游节点一起丢给 LLM 生成回答。
完整答案:
规划阶段,提示词强制 LLM 返回 JSON 计划:任务列表、每个任务的类型和依赖。解析分两遍:第一遍只创建所有任务节点,第二遍再按依赖字段建边——LLM 可能前向引用还没出现的任务,而且生成的任务 ID 可能重复或格式不统一,要重新映射。
建完图拓扑排序判环:DFS 三色标记,visited 存处理完的、visiting 是递归栈中的;撞见还在 visiting 里的节点就是回边、有环,直接作废这次计划让 LLM 重新生成。
执行是个循环:每轮找出「依赖已全部完成」的任务按拓扑序排好,这一批进固定线程池并行跑,上限四个并发;invokeAll 阻塞等整批完成才提交下一批。批次间的正确性就靠这个 join 屏障:批 N 工作线程写入结果,Future.get() 返回建立 happens-before,编排线程可见;提交批 N+1 时任务入队对出队工作线程再建立一次 happens-before——批 N+1 必然看到批 N 的全部结果。不靠并发容器,靠批次隔离加 join 屏障。每个任务的输入会把所有前置任务的描述和结果原文拼进上下文——没有这步,DAG 就只是好看的流程图。
兜底:找不到可执行任务就退出,按三态收尾;运行中节点失败且进度没过半,带着已完成结果和失败原因重新规划,过半就部分完成收尾。
复盘:
- 流程骨架对,但三连里最值钱的第三连答薄了
- 判环只报名字没讲实现:DFS 三色标记(visiting 里撞见即回边)
- 「怎么保证下游拿到结果」只答了数据传递(result 注入),漏了并发正确性:invokeAll join 屏障的 happens-before 链——这是弹药表里挂着的钩子,本轮没接住
- 解析健壮性一笔带过:两遍解析防前向引用、任务 ID 重新映射
- 关联:plan和DAG「追问补强|批次间的可见性」
6. 安全四层 — 8.5 分
问题:审批通过了是不是就一定能执行?四层各自拦什么?为什么不全交人工审批?
我的回答:
人工审批主要是用户视角来判断这个任务是否符合预期、是否给予权限、是否访问或者创建不该访问的文件,主观地决定 LLM 可执行的范围及权限。路径围栏直接将 LLM 限制在一定的目录内干活,只读项目内的内容,防止读项目外的内容。命令黑名单主要避免了删除文件和修改文件的高危操作,避免重要文件删除不可恢复。审计日志让操作和内容有迹可循。其中就算人工审批通过了,触发了底层路径围栏和命令黑名单也不能执行。如果全交给人工审批,那将是没有保障的情况:用户一个不小心观察不仔细或者觉得麻烦,就会执行不该执行的内容,这是恰恰不安全的,交给机器代码能够客观地避免这类情况。
完整答案:
四层有明确的先后:拦截链是人工审批在前、策略层在后——审批通过不等于能执行,PathGuard 和 CommandGuard 在执行前还会再拦一次,用户无法批准一个策略拒绝的请求。一句话分工:人管「要不要做」,策略管「能不能做」,互不替代。
路径围栏是硬规则:所有路径类工具必经,把路径约束在项目根内,拦 ..、符号链接这些逃逸手段,没有商量余地。命令黑名单是快速失败的第一道网:几条正则拦 sudo、rm -rf 全盘、dd 写盘、curl 管道执行这类常识性危险,在便宜处直接拦掉、减少弹窗——它的定位我清楚:黑名单可以被 base64、变量拼接绕过,所以不做完备性承诺,完备性由围栏和人工审批兜底。人工审批管策略之外的主观判断:写文件、跑命令这类危险操作,把工具名、参数、风险说明弹给用户,可批准、拒绝、改参数;外部 MCP 工具不知道会干什么,默认全部审批。审计日志按天滚动 JSONL,记工具名、参数、结果和审批人,写入前敏感信息脱敏——事后能完整回放谁在什么时候放行了什么。
为什么不全靠人工审批:一是人的不可靠——每条都审会造成弹窗疲劳,用户无脑按 y 才是最危险的;所以读类工具不审,还支持工具维度、server 维度全部放行,但放行后围栏和黑名单依然生效。二是策略的客观性——安全规则要确定、可审计,不能交给不确定的模型或一个疲劳的人。
复盘:
- 题眼主动答对:「审批通过仍会被策略拦截」——本题核心
- 黑名单的诚实定位(可绕过、便宜处快速失败的第一道网)没讲——预设加分点
- 审批疲劳的机制化解(分级减负:读类不审、维度放行、放行后围栏仍生效)没给
- 收拢句:「人管要不要做,策略管能不能做」
- 关联:人工审批
7. Kafka 流水线幂等 — 8 分
问题:消息重复消费 ES 会不会重复数据?消费到一半失败重试会不会叠上残留?
我的回答:
同一条消息被重复消费没有关系,ES 中的命名规则是以文件分片 MD5 存放的,就算重复了也只是被覆盖重写而已。如果消费到一半失败了,会将这个脏状态清除,然后触发重试逻辑,所以不会和上次的残留叠在一起。
完整答案:
不会重复,幂等是这条流水线的设计前提,两层机制配合。
写入层:ES 文档 ID 不是随机生成的,是文件指纹加块序号的确定性 ID——同一文件的同一分块,无论写多少次,落到的都是同一个文档 ID,重复消费的写入变成覆盖而不是追加,天然幂等。
流程层:消费端每次重新解析前,先清掉上一次运行的部分产物——旧的文本块和 ES 文档——所以处理到一半失败触发 Kafka 重试时,整条链是在干净的地面上重跑,不会和上次残留叠加。
这两层还互为兜底:就算清理那一刻 ES 或对象存储抖动导致清理失败,写入层的覆盖语义也能保证最终不重复。管理端重建索引用的是同一套语义——清干净再写、写了就覆盖。一条原则管三个场景:重复消费、失败重试、索引重建。
复盘:
- 两层机制都在、方向全对
- 确定性 ID 构成说得不精确:是文件指纹(整文件 MD5)+ 块序号,不是「分片 MD5」
- 漏两层互为兜底(清理失败也不怕,覆盖语义兜底)——本题深度点
- 关联:文件上传模块「同一条消息被重复消费」
8. 聊天并发隔离与可中断 — 忘记 (聊天助手模块第五次失分)
问题:连发两条消息怎么互不干扰?点停止怎么真停?页面关了再开怎么办?
我的回答:不知道。
完整答案:
三个难题同一个解法:每条消息创建一个 generationId,所有异步路径都围绕这个可查询、可取消的状态对象转。
连发两条:任务级隔离——每条消息独立的 generationId、独立生成任务和 Redis 快照,互不干扰,停止只停那一个。连接级:推送按 userId 路由到当前 WebSocket,写同一连接用 synchronized 串行化——WS 会话非线程安全。容量级:生成线程池有界(4 核心 16 上限),满则显式拒绝回「系统繁忙」,不挂起连接线程。
点停止:带内部令牌(防伪造)和 generationId 的 WS 指令,服务端标记取消并 cancel 上游模型流——真取消,token 立刻停止计费;已生成的半截回答正常收尾落库。等待模型响应用 200ms 短轮询而非无限阻塞,取消和超时亚秒级感知。
页面直接关闭:靠生成快照兜底——Redis 30 分钟过期的状态对象,模型吐出的每个片段三路同步:内存累积全文、Redis 快照追加、WebSocket 推前端。重连后凭 generationId 查快照,完整记录在 MySQL 历史页找得回。已知边界:注册表是单连接模型,第二个标签页顶掉第一个的推送,但生成任务不受影响——改进方向是用户到连接集合的广播。
记忆口诀:generationId 贯穿三层——任务级隔离靠它,停止要带它,断线凭它找快照。
- 关联:聊天助手模块「多轮并发隔离」「用户发起对话流程」
9. OOM 定位经历 — 6 分
问题:技能栏写「可独立定位线上 OOM」——讲一次实际定位内存问题的经历。
我的回答:
我在公司跑链路流程时发现有些数据同步时发生了 OOM 的现象导致同步失败。首先我会判断用户的数据量是否太大,观察之后发现用户的数据量是合理的,他是内部表格写入到 Hive 这条链路。接着我判断中间是如何实现的,最后发现我们会一下子读取所有表格的数据再进行写入而不是一条一条写入,然后我向 mentor 提出了 EasyExcel 的方案,最后解决了这个之前存在的遗留问题。
完整答案(真实实习故事 + 定位证据链):
现象:实习时有一条内部表格写入 Hive 的同步链路,某些表同步时任务报 OOM——java heap space,失败重跑也复现。
定位:先排除数据量——出问题的表数据量在正常范围,说明不是「数据真的大」,而是处理方式有问题。然后看堆:jstat 观察到堆占用随读取直线上升、Full GC 频繁;jmap 拉堆直方图,最大的一类是 byte 数组和大批业务对象;MAT 看支配树,引用链指到同步任务里的一次性读取——代码是把整张表全部读进内存、攒完再统一写入,内存占用是 O(整表),数据量一大就顶到堆上限。
解决:读写改成流式——EasyExcel 按行读、分批写,内存占用从 O(整表) 降到 O(批大小)。这是个遗留问题,我定位分析后向 mentor 提了方案,确认后落地,之后没再复现。
沉淀:这类问题的通用判断是「内存占用是否和数据规模同阶增长」——全量进堆这种处理方式迟早被数据规模打死,要改成流式或分批,让内存和批次大小相关、和数据规模脱钩。
复盘:
- 真实实习故事比 self-project 版本更可信,选材对;「向 mentor 提方案」对校招也是加分诚实
- 定位环节没有证据链(题眼缺失):从「发现 OOM」直接跳到「发现全量读取」,没有 jstat 曲线、jmap 直方图、MAT 支配树——「怎么定位的」答成了「猜中了」
- EasyExcel 机制没讲:为什么能解决——流式逐行读、分批写,内存 O(整表)→O(批大小)
- 现象薄(heap space、Full GC 频繁一句没提)、缺沉淀句
- 简历写「可独立定位」,故事里定位靠「发现」+ 问 mentor——补上工具链后「独立」才立得住
- OOM定位案例的工具链直接嫁接到这个故事中段,两个故事共用同一套方法论
10. 场景题:搜不到上传的文档 — 7.5 分
问题:用户反馈「明明上传了文档,搜里面一段内容搜不到」,排查思路?
我的回答:
首先我会判断是不是用户自己上传的,然后分组标签的归类是否正确,这影响到用户是否有权限搜寻到这个文档的数据。接着我会看这个文件在不在存储空间中,避免中途上传失败被删掉的情况。接着我会判断它是否解析切块成功,将用户想搜的一段内容人为地快速定位。最后看检索模块是否出了问题,用户的提问是否成功进行向量化后进行查询。
完整答案:
先定总原则:二分「入库问题还是检索问题」——拿文件指纹直接查 ES,文档 ID 就是文件指纹加块序号,一段查询看全该文件所有块,内容在不在索引里一步判定。顺手先看 MySQL 状态表,状态字段直接告诉你卡在哪环、失败原因是什么。
入库侧:状态失败看错误原因——解析失败(扫描件没走 OCR、特殊格式)、向量化失败(配额拦截、embedding 异常),失败链都收敛在状态字段;状态完成但内容搜不到,就人工定位那段内容在不在某个块里——不在,说明被截断或被页眉页脚过滤误杀;再核对 MinIO 里合并文件是否存在。
检索侧:先复现——拿那段内容当查询直接查 ES。能命中而用户搜不到,查查询侧三处:向量化是否成功(embedding 挂了退化纯 BM25,语义问法召回变差)、must 条件是否杀掉了改写问法、权限过滤是否算错——有效组织标签是登录时展开缓存在 Redis 的,缓存脏了就是「列表看得到、检索搜不到」的经典错位。
排查原则一句话:先便宜后贵,先用设计好的观测点(状态表、确定性 ID),先二分再细分。
复盘:
- 四个检查点选对、顺序大体从便宜到贵,「人为快速定位内容」就是二分动作
- 缺二分框架的显式表述;漏了两个设计好的观测点(状态表、确定性 ID)——自己做了的设计没拿来用
- 检索侧漏三细分:embedding 降级、must 杀改写、权限缓存脏
- 关联:知识库检索模块、文件上传模块
11. Redis 整体挂了 — 部分忘记
问题:Redis 承担会话指针、生成快照、权限缓存、限流、配额——整体挂了会怎样?哪些降级、哪些不可用、数据丢吗?
我的回答:
像我的文件上传模块基本不会受到任何影响,最多就是将状态位跟随文件分片上传时进行修改。其他的我都忘记了。
完整答案:
按数据角色分三类。记忆骨架:Redis 在我系统里全是缓存或游标,事实全在 MySQL——挂了不丢数据,丢的是体验;唯一要认真对待的是限流和配额这两个安全闸。
第一类,可重建的缓存,挂了自愈:上传位图(MySQL 分片元数据回填重建,断点进度不丢)、权限缓存(重新从库里层级展开实算,功能正常性能降级)、对话历史窗口(最近 20 条从 MySQL 事实层回读重建)。
第二类,降级但有兜底:会话指针——发消息时取不到会惰性创建会话,不丢消息,只是「切换历史会话」暂不可用;生成快照——写不进去,「断线续看半截回答」没了,但流式推送还在、MySQL 全量落库照常。
第三类,安全闸:限流计数和 token 配额不是缓存,是保护 LLM 依赖和成本的闸,Redis 一挂闸就开了,必须显式决策 fail-open(故障流量打穿依赖、成本失控)还是 fail-close(可用性受损)。我的选择是安全闸退化成保守默认值——全局固定阈值加收紧配额;功能缓存放开自愈,安全闸宁紧勿松。
复盘:
反问环节
我的反问:「这个岗位入职后半年主要会负责哪类工作?校招生进来一般怎么安排导师和任务节奏?」
复盘:合规、安全,任何面试官都适用;信息增量不如技术向反问(团队 RAG/Agent 落地阶段 + 评测体系——还能把 recall@5 评测的钩子再递一次)。两道备选以后由面试官在反问环节直接给出。
总复盘
得分曲线:9.5 / 8.5 / 9 / 9 / 7.5 / 8.5 / 8 / 忘 / 6 / 7.5 / 部分忘,均分 7.9(上一场 7.8)。
本轮新信息:
- 实习深挖达标:三题 8.5 / 9 / 9,「原理我掌握」的口径守住了;死信 98% 从无托底变成有口径(毕业)
- 聊天助手模块第五次失分(并发隔离/generationId):两场共五次失分全部集中在这一个模块——重读它仍然是第一行动项,没有之一
- OOM 有真实故事但缺证据链:故事选材对,把 OOM定位案例 的工具链(jstat → jmap → MAT)嫁接进中段即可,同时「可独立定位」的宣告才立得住
- join 屏障 / happens-before 这轮没接住——CodeAgent 并发正确性是下一个要过的话术
行动项:
- [ ] 重读 聊天助手模块:generationId 三层隔离、引用溯源、自主性交易、四层稳定性——两场五次失分都在这一篇里
- [ ] 把 OOM定位案例 的工具链嫁接进实习 OOM 故事,背熟 jstat/jmap/MAT 三步
- [ ] 过一遍 plan和DAG 的「批次可见性」追问补强:invokeAll 屏障 + happens-before 链
- [ ] 场景题先说二分框架,再报两个设计好的观测点(状态表、确定性 ID)