2026-10-26 AI Agent 面试训练
今日学习主题
判定对象与生效对象的一致性。
今天 5 道题共用一条主线:凡是”看起来已经完成/已经通过/已经均匀”的判断,都要追问一句——被判断的那个东西,和真正生效的那个东西,是同一个吗?
- B01:写入成功判定的是页缓存,生效的是磁盘;目录项是另一个对象。
- B02:标称上下文窗口是上限判定,位置编码的训练长度才是能力生效点。
- S01:路径白名单校验的是规范化后的字符串,真正被打开的是原始字符串。
- S02:分区键决定的”逻辑分布”与真实数据分布不是一回事。
- L01:区间增量的生效位置是
last + 1,写错一格全盘皆错。
题目目录
| 编号 | 题目 ID | 题型 | 一级类型 | 二级主题 | 题目名称 | 难度 |
|---|---|---|---|---|---|---|
| B01 | Q-20261026-B01 | 非 Agent 基础 | 操作系统与网络 | 页缓存、mmap 与持久化边界 | write() 返回成功,数据就到磁盘了吗 | 基础题(中高) |
| B02 | Q-20261026-B02 | Agent 基础 | AI 与大模型基础 | 位置编码与长度外推 | 标称 128k 窗口,承诺了什么 | 基础题(中高) |
| S01 | Q-20261026-S01 | Agent 场景 | AI 安全与治理 / 工具与互操作协议 | MCP 工具服务攻击面评审与逐条收敛 | 官方参考实现一天修 8 个漏洞之后 | 场景题(高) |
| S02 | Q-20261026-S02 | 非 Agent 场景 | 数据库与存储 / 分布式系统 | 分片键倾斜、路由版本化与在线重分布 | 一个客户打满一个分片 | 场景题(高) |
| L01 | Q-20261026-L01 | LeetCode | 差分数组 / 前缀和 | 区间增量与还原 | 1109. 航班预订统计 | medium |
B01 · 非 Agent 基础定义题
题目 ID:Q-20261026-B01 一级类型:操作系统与网络 二级主题:页缓存、持久化边界与 mmap 语义 题型:非 AI Agent 基础定义题
题目
一个数据接入服务把上游消息落地到本地文件后才向上游返回”已接收”。核心逻辑是:open(path, O_CREAT|O_WRONLY|O_APPEND) → write() 返回写入字节数 → close() → 返回 ACK。上线后出现两类现象:
- 机器非正常断电重启后,最近几分钟已经返回过 ACK 的记录在文件里要么内容缺失、要么长度不足;进程被
kill -9时却从不丢数据。 - 容器内存监控显示常驻内存长期维持在限额的 85%,重启后立刻降到 20%,业务侧排查确认没有任何对象泄漏。
请回答:
write()返回成功到底承诺了什么?close()之后磁盘上就一定有数据吗?- 要让”已持久化”这个判断成立,正确的判定点在哪里?
fsync与fdatasync该怎么选? - 如果改用
mmap写入,语义有什么不同? - 第二类现象如何解释,工程上应该怎么观测和处置?
先思考
- 先区分三种失败:进程崩溃、操作系统崩溃或掉电、存储设备掉电。这三种情况下”已经 write 过的数据”分别还在吗?
- 数据从用户态到磁盘要经过几层缓冲?每一层由谁负责刷出、由谁返回的”成功”?
- 创建一个新文件时,落盘的除了文件内容,还有哪些元数据?它们和文件内容是同一个对象吗?
mmap之后内存里的写,什么时候变成文件里的写?映射区的最后半页归谁管?- 容器的内存统计口径里,除了匿名页还有什么?哪些内存是”可以扔掉”的?
参考答案
结论:write() 只承诺数据已进入内核页缓存,不承诺落盘;close() 也不做持久化。真正的持久化判定点是 fsync()(或按场景用 fdatasync())成功返回,而且如果涉及新建或重命名文件,还需要对所在目录单独 fsync()。容器内存”虚高”通常是页缓存被计入 cgroup 内存统计,它可回收,不等于泄漏。
定义:write() 是把数据从用户态缓冲复制到内核页缓存(buffer cache),返回值为写入字节数时表示内核已接收。此后内核按自己的回写策略(脏页比例、时间周期等)异步写回块设备。
适用边界:
- 进程崩溃:页缓存属于内核,进程没了数据还在,不丢。
- 操作系统崩溃、内核 panic、掉电:未回写的脏页丢失,会丢。
- 存储设备自身缓存未刷出:即使内核已下发写请求也可能丢,所以
fsync()的定义包含了”刷掉磁盘缓存”这一步。
简洁示例:
write(fd, buf, n) -> 数据进入页缓存,返回 nfsync(fd) -> 阻塞直到设备报告传输完成(含刷新磁盘缓存)Linux 手册对 fsync() 的原文表述是:把文件的全部 modified in-core data 传送到磁盘设备,“so that all changed information can be retrieved even if the system crashes or is rebooted. This includes writing through or flushing a disk cache if present. The call blocks until the device reports that the transfer has completed.”
相近概念的区别:
| 调用 / 标志 | 作用对象 | 是否阻塞到设备 | 是否需要额外操作 |
|---|---|---|---|
write() | 用户态 → 页缓存 | 否 | 需要 fsync/fdatasync 才谈持久 |
fsync() | 文件数据 + 关联元数据 | 是 | 目录项需单独 fsync |
fdatasync() | 数据 + 保证后续读取正确所必需的元数据 | 是 | 不保证 st_atime/st_mtime 落盘 |
msync(MS_SYNC) | MAP_SHARED 映射的脏页 | 是 | 尾部不满页的越界写不回文件 |
O_SYNC | 每次 write 自带同步语义 | 是 | 延迟代价高,逐次写放大 |
sync() | 整个系统的脏页 | 是 | 粒度是全局,不适合作为单次提交的判定点 |
两个容易漏掉的边界:
fsync(fd)不保证包含该文件的目录项已落盘。手册原文:Calling fsync() does not necessarily ensure that the entry in the directory containing the file has also reached disk. For that an explicit fsync() on a file descriptor for the directory is also needed.所以”写临时文件 →fsync→rename→fsync(目录)”才是完整的原子替换流程。fdatasync()不刷元数据,除非该元数据是后续正确读取数据所必需的。手册给出的例子是:改变文件大小(st_size)需要刷元数据,而改变st_atime/st_mtime不需要。因此纯追加写(改变 size)用fdatasync()并不丢数据,但新建/重命名场景必须用fsync()并单独处理目录。
mmap 的语义差别:
MAP_SHARED下映射区的更新对其他映射同一区域的进程可见,并会回写到文件;但手册明确说明,要精确控制”何时”回写到底层文件,必须使用msync(2)。MAP_PRIVATE是写时复制,更新既不回写文件也不可见于其他进程,且mmap()返回之后再对文件做的修改在已有映射区中是否可见,标准未指定。- 文件按页映射,末尾不满一页的部分在映射时被清零,且对该区域的修改不会写回文件;文件被截断后访问原映射超出新末尾的页会触发
SIGBUS。 - 因此 mmap 适合大块顺序读写与只读共享,不适合作为”我已持久化”的判定依据。
典型应用(通用工程):
- 数据库与消息队列的 WAL:提交点必须绑定一次成功的
fsync,而不是write。 - 配置与元数据的原子替换:写新文件 →
fsync→rename→fsync(目录)。 - 检查点(checkpoint)与快照:把”多久 fsync 一次”显式化为持久性级别(每条 / 每批 / 每秒),并与 SLA 对齐。
fdatasync()用于日志类追加写场景,减少元数据写放大。
面试现场回答(1–2 分钟):
write()返回成功只表示数据进了内核页缓存,进程崩溃不丢,但机器掉电或内核崩溃会丢,因为脏页还没回写。close()也不做同步。要真正判定持久化,必须在对外宣称完成之前调用fsync()并确认返回 0;如果只关心数据、不关心时间戳这类元数据,可以用fdatasync()减少写放大;但涉及新建或重命名文件时,光fsync文件还不够,目录项是另一个对象,需要对目录 fd 单独fsync,所以原子替换的完整流程是写新文件、fsync 文件、rename、再 fsync 目录。用 mmap 时更要小心:MAP_SHARED的回写时机不确定,要精确控制必须msync,而且映射末尾不满一页的修改根本不会写回文件。至于容器内存长期 85%,那多半是页缓存被计入了 cgroup 的内存统计——文件读写越多它越高,回收即可释放,不是泄漏;判断泄漏要看匿名页而不是总量。
追问
追问:如果出于性能考虑只能保留一次同步调用,你会放在哪一步?什么时候可以用 fdatasync() 替代 fsync()?
回答要点:同步调用必须紧邻”对外宣称完成”的那一步之前,而且是同一条路径上唯一的一次——放在开头等于没放,放在末尾则宣称已完成时仍未持久。替代判据是”元数据是否影响后续正确读取”:纯追加写改变 st_size,fdatasync() 会刷它,可以替代;但如果涉及新建、重命名、硬链接,目录项是独立对象,此时既不能省 fsync(),还必须对目录 fd 再补一次。性能上真正有效的优化不是删掉同步,而是合批:把多条记录攒成一批后一次 fdatasync(),把持久性级别显式化为”每批/每秒”并与 SLA 对齐。
关键点
- 判定点前移:凡是对外宣称”已完成”的位置,前面必须有且只有一次成功的同步调用;
write()与close()都不是判定点。 - 目录项是独立对象:文件内容的持久化和目录项的持久化是两个判定,原子替换缺一不可。
fdatasync()的豁免是有限的:只在元数据不影响后续正确读取时可省,st_size变化必须刷。- mmap 不是持久化的快捷方式:回写时机由内核决定,尾部不满页不回写,截断后访问会
SIGBUS。 - 容器内存口径:内存统计包含可回收的页缓存,“RSS 高”不等于泄漏;同时也要意识到页缓存挤占限额会在内存压力下触发回收甚至 OOM。
常见误区
- 认为
close()会刷盘。不会。close()只是释放文件描述符;更糟的是如果fsync返回过EIO,在已经close的路径上这个错误可能再也没被检查。手册还指出,自 Linux 4.13 起回写错误会报告给所有可能写过该数据的文件描述符,也就是说一次fsync的EIO有可能来自”别的 fd 写的数据”。 - 认为
fsync()一次等于整个路径都持久化了。创建新文件、重命名、硬链接都涉及目录项,需要单独处理;只fsync文件是典型的不完整实现。 - 把容器内存使用率高直接认定为内存泄漏。文件 I/O 密集的服务会把页缓存计入 cgroup 内存统计,这部分在压力下可回收;正确做法是区分匿名页与文件页后再判断。
延伸学习
- Linux 手册
fsync(2):https://man7.org/linux/man-pages/man2/fsync.2.html (已核验;fsync/fdatasync的定义、目录项边界、错误语义) - Linux 手册
mmap(2):https://man7.org/linux/man-pages/man2/mmap.2.html (已核验;MAP_SHARED/MAP_PRIVATE语义、尾部不满页、SIGBUS)
B02 · AI Agent 基础定义题
题目 ID:Q-20261026-B02 一级类型:AI 与大模型基础 二级主题:位置编码与长度外推 题型:AI Agent / LLM 基础定义题
题目
团队在做一个长文档问答 Agent。模型标称支持 128k 上下文,有同学主张把 60k token 的整份合同一次性塞进上下文做多跳问答,理由是”窗口够大就没必要切”;另一位同学实测发现切成若干段落、先检索再回答的效果明显更好,于是认为”长上下文本来就会退化,长窗口是营销”。
请回答:
- 位置编码在 Transformer 里解决什么问题?绝对位置编码与相对位置编码的差别是什么?
- 模型标注的”128k 上下文窗口”到底承诺了什么、没有承诺什么?
- 为什么一个用 RoPE 的模型在超出训练长度时容易崩,而 ALiBi 这类方法能外推?
- Position Interpolation、YaRN 这类上下文扩展方法做了什么,代价是什么?
- 在 Agent 系统里,什么时候该”整体塞入”,什么时候该”检索后拼装”?
先思考
- 自注意力本身对 token 顺序是置换不变的吗?如果不是,顺序信息是从哪一步加进去的?
- 训练时最长见过 4096 个 token,推理时给 8192 个,多出来的那些位置索引模型”见过”吗?
- “能塞进去”和”塞进去还能用”是两个命题,分别对应什么约束?
- 把位置索引整体压缩到原来的区间里,和让模型去猜没见过的位置,哪种更稳?
- 长上下文的成本由什么决定?它和”效果”是同一个维度的限制吗?
参考答案
结论:标称上下文窗口是长度上限,不是质量承诺。窗口能否真正用满,取决于位置编码在训练长度之外的外推能力:RoPE 类模型直接外推会出现注意力分数灾难性升高;ALiBi 这类按距离加线性惩罚的方法天然更可外推;PI / YaRN 通过”内插 + 少量微调”把窗口扩展,代价是需要微调预算并且必须在原窗口内做回归评测。工程上应把可用预算定得明显小于标称窗口,并按位置分布安排关键信息。
定义:位置编码是把 token 在序列中的位置信息注入模型的机制。自注意力计算本身不携带顺序信息,需要通过位置编码(加到输入上,或直接在注意力分数上施加偏置)让模型感知”谁在前、谁在后、相距多远”。
- 绝对位置编码:为每个绝对位置生成一个表示加到词向量上(正弦式、可学习式)。
- RoPE(Rotary Position Embedding):用旋转矩阵编码绝对位置,同时在自注意力的形式中显式引入了相对位置依赖。论文摘要原文:它 “encodes the absolute position with a rotation matrix and meanwhile incorporates the explicit relative position dependency in self-attention formulation”,并具备序列长度灵活、随相对距离增大而衰减的 token 间依赖等性质。
- ALiBi(Attention with Linear Biases):不向词向量加位置嵌入,而是对 query-key 注意力分数施加一个与两者距离成正比的惩罚。论文标题即为《Train Short, Test Long》,摘要给出:训练长度 1024 的 1.3B 模型可外推到 2048,困惑度与直接在 2048 上训练的正弦位置编码模型相当,但训练快 11%、省 11% 内存。
适用边界:
- 窗口是硬约束:超过就报错或被截断,这是确定的、可由传统程序保证的。
- 窗口内的质量是概率性的、随位置分布不均的:这是模型能力,不是协议能保证的。
- 长度外推能力取决于位置编码方案与训练/后处理数据,不能跨模型默认。
简洁示例:
训练长度 4096(RoPE)推理 4096 以内 -> 位置索引都在训练分布内,稳定推理 8192(直接外推) -> 位置索引超出训练分布,注意力分数可能失控推理 8192(PI/YaRN) -> 位置索引被内插回训练区间 / 频率重组,需微调支撑相近概念的区别:
| 概念 | 作用位置 | 是否需微调 | 代价 / 风险 |
|---|---|---|---|
| 标称上下文窗口 | 长度上限(部署与计费口径) | — | 只保证”能塞”,不保证”能用” |
| 长度外推 | 让模型处理超出训练长度的序列 | 否(理想情况) | RoPE 直接外推退化明显;ALiBi 类天然更可外推 |
| 位置内插(PI) | 线性下缩输入位置索引以匹配原窗口 | 是(少量) | 需微调;原窗口内质量需回归评测 |
| YaRN | RoPE 频率重组的内插扩展 | 是(更少) | 论文称所需 token 少 10 倍、训练步少 2.5 倍 |
| 上下文压缩 / 检索拼装 | 减少实际进入模型的长度 | 否 | 属工程手段,与位置编码正交 |
关键区别:外推是”去没见过的地方”,内插是”把没见过的地方压缩进见过的地方”。位置插值论文的摘要明确指出,之所以不选择外推,是因为在训练长度之外外推”may lead to catastrophically high attention scores that completely ruin the self-attention mechanism”,并给出理论结果:内插的上界至少比外推小约 600 倍。
典型应用(Agent 系统):
- 上下文预算制定:把可用预算定在标称窗口之下,而不是顶格使用;把最关键的约束、目标与标识符放在注意力最容易覆盖的位置。
- 整体塞入 vs 检索拼装:需要跨段全局依赖(全文一致性检查、跨章节对照)时倾向整体塞入,但要先验证该模型在该长度上的实际表现;只需要局部证据时走检索拼装,成本与稳定性都更好。
- 长会话与长轨迹:压缩后的历史会被重新排列,位置分布随之改变,因此压缩策略必须配套”关键信息置位”的评测,而不是只看压缩率。
- 选型:如果业务强依赖”短训练长推理”,应优先考察模型的长度外推手段与官方给出的有效长度口径,而不是只看标称窗口数字。
面试现场回答(1–2 分钟):
标称窗口是长度上限,不是质量承诺。自注意力本身不带顺序信息,位置信息靠位置编码注入。RoPE 用旋转矩阵编码绝对位置、同时在注意力里表达相对位置,ALiBi 则是按 query-key 的距离对注意力分数加线性惩罚。差别在超出训练长度时暴露出来:RoPE 直接外推,位置索引落在训练分布之外,位置插值论文的说法是可能产生灾难性偏高的注意力分数、直接破坏自注意力机制,所以他们选择把位置索引线性下缩回训练区间,也就是内插,论文给出的理论上界比外推小约 600 倍,代价是要做少量微调。ALiBi 不一样,它天生带距离惩罚,论文里 1024 训练的模型可以外推到 2048,困惑度和直接训 2048 的正弦编码模型持平,还更快更省。YaRN 是 RoPE 的改进扩展,所需 token 和训练步都少一个量级。落到工程上,我会把可用上下文预算定得明显小于标称窗口,把关键约束放在注意力可靠的位置;需要跨段全局依赖才考虑整体塞入,并且必须先做该长度上的回归评测,否则走检索拼装更稳也更省。
追问
追问:如果只能在”做位置插值微调扩窗”和”切段 + 检索拼装”之间选一个,你怎么决策? 回答要点:先看任务是否需要跨段全局依赖——合同前后条款一致性、跨章节数值勾稽这类问题,检索拼装天然会丢证据,倾向扩窗;只需局部证据的问答,拼装更稳也更省。再看三个硬约束:有没有微调预算与回归集(扩窗必须评测原窗口内质量)、能不能接受长上下文的 KV Cache 显存与首字延迟、有没有把跨段依赖结构化外置到状态文件的工程能力。三者都满足不了时,正确答案是不要扩窗也不要硬塞,而是把”跨段依赖”变成确定性的结构化状态(例如先抽取出条款清单再回答),把模型要处理的有效长度压下来。
关键点
- 窗口是上限、不是预算:标称长度只保证不被截断,质量分布由位置编码与训练决定。
- 外推与内插是两条路:外推省事但 RoPE 上风险高;内插要微调但上界显著更低。
- 外推能力不是模型的通用属性:ALiBi 与 RoPE 表现不同,不能跨架构类推。
- 扩窗要回归原窗口:PI 论文说的是在原窗口内”relatively well”地保持质量,这本身就是需要被评测的命题,不是默认结论。
- 与 KV Cache、位置偏差配套看:长上下文同时带来显存与延迟成本,以及中部信息利用下降的问题;三者要一起做预算,而不是只谈长度。
常见误区
- “窗口够大就该整体塞满”。窗口大只说明不会溢出;在接近上限的长度上,模型对信息的实际利用能力是另一回事,而且成本随长度上升。正确做法是先建立该长度上的评测基线,再决定是否整体塞入。
- “长上下文一定退化,所以长窗口没用”。这是把 RoPE 直接外推的结论推广到所有位置编码方案。ALiBi 这类方法的论文明确展示了外推能力,说明退化与否取决于方案与训练,不能一概而论。
- “扩窗之后原窗口内的表现不变”。上下文扩展通常伴随微调,原窗口内的质量是需要回归评测的对象;把它当成免费升级,是最容易在上线后才暴露的错误。
延伸学习
- RoFormer / RoPE:arXiv:2104.09864(Su Jianlin 等,2021-04-20 提交,v5 2023-11-08)
- ALiBi《Train Short, Test Long: Attention with Linear Biases Enables Input Length Extrapolation》:arXiv:2108.12409(Ofir Press、Noah A. Smith、Mike Lewis,2021-08-27 提交,v2 2022-04-22)
- 《Extending Context Window of Large Language Models via Positional Interpolation》:arXiv:2306.15595(Shouyuan Chen 等,2023-06-27 提交,v2 2023-06-28)
- YaRN:arXiv:2309.00071(Bowen Peng 等,2023-08-31 提交,v3 2026-02-06)
S01 · AI Agent 真实研发场景题
题目 ID:Q-20261026-S01 一级类型:AI 安全与治理 / 工具与互操作协议 二级主题:MCP 工具服务攻击面评审与逐条收敛 题型:AI Agent 场景题(场景类型:自建与第三方 MCP 工具服务的攻击面评审) 难度:场景题(高)
题目
公司内部 Agent 平台接入了 40 多个 MCP 工具服务,其中 18 个来自第三方或社区实现,包含官方参考实现里的 filesystem 与 everything 两类服务。部署形态混杂:一部分以 stdio 跑在开发者笔记本上,一部分以 streamable HTTP 跑在内网 Kubernetes,个别服务被开发同学临时映射到了公网做联调。
安全评审给了一个明确触发事件:2026-10-25,官方参考服务器 v2.1.0 一次性修复了三个服务器上的 8 个漏洞,并发布了 8 份安全公告。抽查其中几份可以看到:
- filesystem 服务的
validatePath在normalizePath(absolute)上做白名单包含性检查,但后续fs.realpath与返回值用的是原始absolute字符串;而normalizePath会剥掉首尾的引号或空白字符。于是请求/allowed/proj'能通过检查(规范化为/allowed/proj),实际文件操作却落在/allowed/proj'——允许目录的兄弟路径上。全部 14 个文件系统工具都经过validatePath。公告标注 CWE-22 与 CWE-436,CVSS 4.2。 - 另一份公告显示:大小写变体可以绕过符号链接检查,在允许目录之外创建文件,标注 CWE-59 与 CWE-178。
- 还有一份是父目录回退逻辑中的 TOCTOU,允许越界写入,严重级别 Low(CVSS 2.2)。
- everything 服务同时绑
0.0.0.0、通配 CORS(Access-Control-Allow-Origin: *)、无鉴权,并提供get-env工具返回process.env全部内容。任意网页可以跨站完成 MCP 握手并调get-env,一次性拿走全部密钥,CVSS 6.5。公告还指出:更早的版本虽然不发宽松 CORS 头,但同样不校验Origin与Host,因此仍然暴露于 DNS rebinding。 - 多份公告都附了同一段升级说明:这些包在 1.0.0 时从日期版本号(2025.x、2026.x)切换到语义化版本,包管理器会把 2.0.1 排在所有 2025.x 与 2026.x 之下,因此不会把它作为升级项提供,Dependabot 也不会开 PR;必须从日期版本显式执行安装指定版本。
你需要在两周内完成一次攻击面收敛,并把结论固化成可复用的准入清单。已知现象与约束:
- 平台规模:40+ 工具服务、日均 8 万次工具调用、跨 6 个业务租户;
- 没有统一网关,各业务线自己拉起服务,配置散落在各处;
- 安全团队只有 2 人可投入两周,不能逐个服务做完整代码审计;
- 业务侧要求 Agent 的文件读写能力不能整体下线;
- 已有审计日志,但只记录工具名与参数原文,不记录”实际生效的对象”。
请给出:问题拆解、收敛方案与执行流程、技术选型与取舍、失败路径与恢复、安全与成本分析、验证指标,并说明哪些能力属于模型、哪些属于 Agent 编排、哪些必须由确定性系统强制。
先思考
- 抽查公告里的三类文件越界,表面原因不同(尾部字符、大小写、父目录回退),它们有没有共同的结构性缺陷?
- “配了 allowedDirectories” 为什么没有成为有效边界?检查发生在哪个字符串上,使用发生在哪个字符串上?
- 一个只监听 localhost 的服务,凭什么会被浏览器攻击到?这里涉及哪两种攻击?
- 依赖自动升级为什么在这批包上失效?这是不是安全问题?
- 如果把”不要读取敏感文件”写进系统提示词,它落在哪一层?一旦模型被注入,这层还成立吗?
- 8 个漏洞分布在 3 个服务上,两周 2 个人力,你打算按什么顺序处理?
参考答案
结论:这批漏洞的共同根因不是某个 API 用错,而是校验对象与生效对象不一致,以及安全边界被放在了概率性组件里。两周内的可行做法不是逐服务审计,而是把边界从各服务内部上移到统一的确定性执行点(工具网关 + 运行时沙箱 + 版本门禁),按”跨站调用 / 路径与地址约束一致性 / TOCTOU / 出网 SSRF”四条主线逐条收敛,并用公告自带的可复现 PoC 作为回归门禁。
问题拆解:
- 边界在哪里失效:filesystem 的三类越界都是”检查 A、使用 B”。
normalizePath会剥掉首尾引号与空白,检查跑在规范化结果上,文件系统调用却跑在原始字符串上;realpath 之后的二次检查又用同一个会剥字符的函数,于是一起通过。大小写变体同理——符号链接检查用的大小写形式与实际解析路径不一致。父目录回退的 TOCTOU 则是”检查—使用”之间存在可写窗口。 - 本地服务不等于无攻击面:everything 的问题不是”有网络接口”,而是通配 CORS + 不校验
Origin/Host+ 无鉴权三者叠加。浏览器可以跨站发起调用;DNS rebinding 则让攻击者把自己的域名解析到 127.0.0.1,绕过同源判断。公告明确指出早期版本虽不发宽松 CORS 头,仍暴露于 DNS rebinding。 - 供应链修复不会自动到达:版本方案从日期版本号切到语义化版本后,
2026.8.31 > 2.0.1在包管理器的排序规则下成立,于是修复版本既不会被提示也不会被 Dependabot 提出。这意味着”我们装了 Dependabot”在这次事件里是无效控制。 - 审计不闭环:现有日志记参数原文,但真正发生作用的是解析后的路径 / 实际连接的目标地址。参数原文与生效对象不一致时,审计无法回答”到底读了哪个文件”。
- 约束:人力与时间有限、能力不能整体下线、配置散落。
核心方案:
- 第一层:版本与供应链门禁(当天可完成)。对所有 MCP 包生成 SBOM,显式 pin 到修复版本(如公告给出的显式安装命令),禁止
latest与日期版本号;在 CI 里加一条”版本方案检查”:任何仍停留在2025.x/2026.x的依赖直接失败。这一条成本最低、覆盖面最广,因为它把 8 个漏洞中的大部分一次性关掉。 - 第二层:统一工具网关(确定性执行点)。所有 HTTP 形态的 MCP 服务统一收口到网关:鉴权、
Origin白名单、Host白名单、按租户工具白名单、速率限制、审计。网关是传统软件系统,行为确定、可测试,不依赖模型。 - 第三层:路径与地址约束的”单一生效对象”原则。在服务端或网关侧强制:先
realpath(或对地址做归一化)得到唯一生效对象,然后所有包含性判断与所有后续操作都使用这个已解析的值,不再回退到原始输入。规范化函数必须幂等且不改变语义(不得剥引号与空白),大小写变体与 Unicode / IDNA 形式要在比较前统一到同一形式。 - 第四层:TOCTOU 与出网边界。对文件系统类服务,避免”先检查路径再打开路径”,改为对已打开的文件描述符做判定(比对
dev/ino,或使用不跟随符号链接的打开方式),并承认跨平台修复的不完全性,配合容器只读挂载与最小权限兜底。对出网类工具,把IPv4-mapped、NAT64 与 IPv6 特殊地址统一按其携带的地址判定,并限制重定向跳数、每跳重做策略校验。 - 第五层:能力默认关闭。
get-env这类”读取全部进程环境”的能力默认不注册;本地开发强制 stdio;HTTP 传输必须同时满足鉴权 + 回环/内网绑定 + 明确Origin白名单。
执行流程(两周):
D1 资产盘点:传输形态 / 部署位置 / 数据来源 / 是否第三方 / 是否有写能力D1 版本门禁:SBOM + 显式 pin 修复版本 + CI 校验(先止血)D2-3 网关落地:Origin + Host 白名单、鉴权、工具白名单、审计字段改造D3-5 四条主线逐条过:跨站调用 / 路径一致性 / TOCTOU / 出网地址族与重定向D5-8 红队回归集:用公告 PoC 改造为常驻用例,接入 CID8-10 剩余服务分类处置:可替代的换自建、不可替代的加沙箱D10-14 准入清单固化为文档 + CI 规则 + 评审模板技术选型与依据:
| 环节 | 选择 | 依据 | 备选与代价 |
|---|---|---|---|
| 依赖修复 | 显式 pin + CI 版本方案检查 | 自动升级在版本方案切换后已失效,必须显式 | 等待 Dependabot:无效,不可接受 |
| 安全边界位置 | 网关 + 沙箱(模型之外) | 授权边界若放在提示词里,一旦被注入即失效 | 提示词约束:只能作为体验优化,不能作为边界 |
| 路径校验 | 解析后校验并使用解析值 | 直接修复 check/use mismatch | 只增强正则过滤:仍存在新的解释差异 |
| 符号链接 | 打开后按 dev/ino 比对 / 不跟随打开 | 消除”检查—使用”窗口 | 仅做字符串检查:TOCTOU 仍在 |
| 出网校验 | 地址族统一判定 + 逐跳重校验 | 绕过常来自表示形式差异与重定向 | 只校验首跳 URL:可被重定向绕过 |
| 传输形态 | 默认 stdio,HTTP 收口到网关 | stdio 无 HTTP 攻击面 | 各服务自管 HTTP:无法统一策略 |
方案取舍:
- 统一网关 vs 各服务自查:网关增加一跳延迟与运维成本,但换来可测试、可审计、可回滚的单一执行点;在 40+ 服务、2 人力的约束下,自查不可行。
- 禁止 HTTP 传输 vs 保留远程能力:一刀切最安全,但会砍掉远程工具与集中托管能力。折中是”HTTP 必须过网关”,而不是全面禁止。
- 白名单目录 + 只读挂载 vs 功能性:写能力是业务刚需,因此选择”目录白名单 + 解析后校验 + 容器只读根 + 独立可写卷 + 审计”,而不是”全局只读”。
- 自建替换 vs 第三方加沙箱:能力不可替代的保留并加沙箱;能替换的优先自建,因为可控性优先于便利性。
失败路径、边界条件与恢复:
- 升级失败:所有服务以容器镜像 + 版本 pin 交付,回滚目标是上一个内部镜像 tag,而不是
latest;回滚判据是健康检查与回归集,不是”镜像拉取成功”。 - 校验失败必须 FAIL-CLOSED:路径解析异常、大小写比较异常、地址族判定异常,一律拒绝并记录,绝不能降级为”放行并记日志”。这是本场景最容易犯的工程错误——日志里写着”已告警”,实际数据已经出去了。
- 重定向链:设置最大跳数(例如 3)与总超时;每一跳都对解析后的目标重做策略校验,且重定向目标不得由请求方以参数形式指定内网地址。
- 超时与重试:工具调用超时按三类分离——连接超时、读取超时、整体截止时间;只有确定性失败(连接被拒、鉴权失败、策略拒绝)不重试;网络类可重试错误带抖动与重试预算;写类工具不得依赖重试做去重,必须有幂等键。
- 幂等:写文件、移动文件、创建目录这类操作在网关侧生成语义指纹(工具名 + 归一化生效对象 + 内容摘要 + 租户 + 时间窗),重复请求返回首次结果;幂等键由确定性系统生成,不由模型提供。
- 审计与恢复:审计字段必须包含 归一化后的生效对象,而不只是参数原文;发生越界事件时,恢复动作包括吊销该服务凭证、摘除路由、按审计回溯影响集(哪些会话读过哪些对象)、通知数据所有者。
- 灰度:网关策略按服务维度灰度,先
只审计不拒绝观察一周的拒绝率与误伤率,再切拒绝;策略本身配置化并版本化,可一键回退。
安全、权限、成本、延迟与可靠性:
- 权限:Agent 使用独立身份与代持令牌,不复用调用者权限;网关按租户 × 工具 × 生效对象三元组判定,判定在模型之外。
- 成本:网关增加约一跳内网延迟与少量 CPU;逐跳重定向校验会增加出网类工具的延迟,用跳数上限与缓存(缓存键必须是解析后的目标而非输入字符串)压住。
- 可靠性:统一网关引入单点风险,因此网关自身按无状态多副本部署,策略配置走版本化下发与本地 last-known-good 缓存;网关不可用时的默认动作是拒绝高风险工具而非放行。
评测指标与上线后观测:
- 依赖版本合规率(pin 到修复版本的服务占比,目标 100%);
- 红队回归集通过率(含尾部引号、大小写变体、符号链接、IPv4-mapped 地址、重定向到链路本地地址等用例,CI 每次构建必跑);
- 网关拒绝率按工具 × 租户分桶,异常升高要能定位到具体策略版本;
- 审计完整率(记录到”归一化生效对象”的调用占比);
- 误伤率(被网关拒绝但人工判定为正常的比例),灰度期重点看这个指标。
三层能力的职责区分(本题的核心判分点):
| 层 | 承担什么 | 不能承担什么 |
|---|---|---|
| 模型能力 | 理解文档与用户意图、选择合适的工具与参数、从工具结果里提取信息 | 不能判定越权、不能保证不读越界路径、不能抵御提示注入 |
| Agent 编排能力 | 工具白名单、参数模板、重试与降级、把失败反馈给模型 | 不能自行放宽策略、不能把”重试”当成去重手段 |
| 确定性系统能力 | 鉴权、Origin/Host 校验、路径解析与包含性判定、地址族判定、重定向逐跳校验、沙箱与文件权限、依赖版本门禁、审计 | — |
一句话:凡是”必须成立”的约束,都要落在模型之外。
追问
追问 1:三类文件越界表面原因完全不同,为什么你说它们有共同根因? 回答要点:它们都是”校验对象 ≠ 生效对象”。尾部引号与空白是规范化函数改变了语义却只用于检查分支;大小写变体是比较双方的表示形式不一致;TOCTOU 是检查之后、使用之前对象被替换。统一修法是”解析出唯一生效对象,之后所有判断与所有操作都只用这个值”,并且把规范化函数做成幂等且不改变语义。
追问 2:服务只监听 127.0.0.1,为什么还会被浏览器攻击到?怎么修?
回答要点:两个独立攻击面。一是跨站调用——通配 CORS 允许任意源的页面读取工具响应,配合无鉴权即可调用任意工具;二是 DNS rebinding——攻击者域名先解析到外网 IP 通过同源检查,再重新绑定到 127.0.0.1,此时请求已是”同源”。公告明确指出早期版本不发宽松 CORS 头但也不校验 Origin 与 Host,因此仍然暴露于后者。修法:CORS 改为显式 Origin 白名单(禁止通配)、同时校验 Host、绑定回环或内网、HTTP 传输加鉴权、敏感能力默认关闭。
追问 3:为什么”装了 Dependabot”没能阻止这次事件?你们的依赖治理要改什么?
回答要点:因为版本方案从日期版本号(2025.x、2026.x)切换到语义化版本后,包管理器的排序规则把 2.0.1 排在所有 2025.x / 2026.x 之下,修复版本不会被识别为升级项,自动升级与告警都静默失效。要改的是:依赖清单显式 pin 精确版本并禁止 latest;CI 增加”版本方案检查”,仍在日期版本号上的依赖直接失败;SBOM 与许可证/公告订阅纳入例行流程;对安全修复一律显式指定版本安装并跑 PoC 回归,而不是等工具提示。
关键点
- 校验对象必须等于生效对象:检查跑在哪个字符串上,操作就必须用同一个字符串;规范化不得改变语义。
- 边界必须在模型之外:提示词约束只能改善体验,不能作为授权边界。
- 自动升级不是控制手段:版本方案切换会让排序倒置,必须显式 pin + CI 校验。
- 本地 ≠ 无攻击面:跨站调用与 DNS rebinding 正是针对本地服务。
- 审计要记生效对象:否则参数原文与真实操作不一致时无法追责与回溯。
常见误区
- “配了 allowedDirectories 就是沙箱”。白名单只是策略输入,若检查与使用落在不同字符串上,策略等于不存在。判断沙箱是否成立的唯一标准是”是否存在一条输入能让校验通过而实际访问越界”。
- “本地服务不需要鉴权”。浏览器可以在用户无感知的情况下跨站调用本地 HTTP 服务;DNS rebinding 还能绕过基于域名的判断。localhost 减少的是网络可达性,不消除调用来源不可信这一事实。
- “升级交给依赖机器人就够了”。这次事件里自动升级因为排序规则失效而完全静默,是典型的”控制手段存在但无效”。安全修复必须显式指定版本并验证。
- “在系统提示词里写清楚不能读敏感文件”。提示注入的存在意味着模型层约束是概率性的;授权判定必须放在网关与操作系统权限里。
延伸学习
- GitHub 安全公告 GHSA-7rgq-gccq-qphx(filesystem
validatePathcheck/use 不匹配,CWE-22 + CWE-436,CVSS 4.2,2026-10-25 发布;含可复现 PoC 与”从日期版本升级”说明) - GitHub 安全公告 GHSA-mvf5-2qqv-rg3c(everything 服务通配 CORS +
get-env跨站泄露,CWE-200 + CWE-942,CVSS 6.5,2026-10-25 发布;含 DNS rebinding 与升级说明) - GitHub 安全公告 GHSA-53mr-w429-c597(filesystem 父目录回退 TOCTOU,CVSS 2.2)与 GHSA-4gxg-m5jx-9j57(大小写变体绕过符号链接检查,CWE-59 + CWE-178)
30–60 分钟小型实践:
- 起一个本地目录
allowed/proj,同时创建兄弟目录allowed/proj'(带尾部单引号),在后者的SECRET.txt里写入OUTSIDE。 - 写一个最小
validate函数复刻缺陷:对normalizePath(absolute)做包含性检查,但返回fs.realpath(absolute)的结果。 - 用
/allowed/proj'调用它,观察返回路径落在允许目录之外,并能读到SECRET.txt。 - 改造为”先
realpath得到生效对象 → 在生效对象上做包含性检查 → 之后所有操作都使用这个值”,重跑第 3 步,确认被拒绝。 - 再把服务的 HTTP 传输绑定到
127.0.0.1,CORS 改为仅允许http(s)://localhost(:port),用curl -H "Origin: http://evil.com"验证响应头不再回显通配来源。
S02 · 非 AI Agent 真实研发场景题
题目 ID:Q-20261026-S02 一级类型:数据库与存储 / 分布式系统 二级主题:分片键倾斜、路由版本化与在线重分布 题型:非 AI Agent 场景题(核心方案不依赖模型或 Agent) 难度:场景题(高)
题目
订单库单表 3.2 亿行,日均写入 400 万,峰值 6000 TPS。当前按 user_id % 64 分到 64 个物理分片,落在 8 台机器(每台 8 个库)上。近两周的监控表现:
- 分片 37 的 CPU 长期 78%,慢查询数是其他分片的 30 倍,主从延迟最高 40 分钟;
- 其余 63 个分片利用率普遍在 20% 以下;
- 运维已经给分片 37 加了 2 个从库并升了规格,慢查询没有明显改善。
排查结论:4 个企业客户(B2B 批量下单)贡献了分片 37 约 61% 的写入,其中最大的一个客户单日订单量仍在以每周 8% 的速度增长。订单号由中心发号器生成,是全局唯一但不携带分片信息。
目标:4 周内消除热点,恢复各分片负载的均衡性,并保证可回滚。
约束:
- 全程不能停写;允许总计不超过 5 秒的写阻塞窗口(且只能出现在切换瞬间)。
- 所有订单必须可查询,包括按用户的历史订单列表与跨用户的运营查询。
- 有两个下游系统直接订阅物理分片的 binlog,重分布会产生重复与乱序事件,他们只能容忍有限重放。
- 硬件预算只够新增 2 台机器。
- 不允许长时间持有跨分片事务。
请给出问题拆解、方案取舍、执行流程、失败路径与回滚、验证指标,并说明为什么”加机器”和”加从库”都解决不了这个问题。
先思考
user_id % 64保证的是”键的均匀分布”还是”负载的均匀分布”?这两者什么时候不是一回事?- 把分片数从 64 扩到 128,需要迁移多少数据?迁移之后热点客户的订单会分布到多个分片上吗?
- 一个客户的订单必须落在同一个分片吗?这个约束是谁提出的,能不能改?
- 如果允许改分区键,数据要怎么搬?搬的过程中新写入怎么办?
- 下游直接消费物理分片 binlog,这个耦合在重分布时会引发什么问题?
- “加从库”解决的是读压力还是写压力?主从延迟 40 分钟说明什么?
参考答案
结论:这是典型的分片键与业务分布不匹配导致的负载倾斜,不是容量不足。加机器和加从库都不解决问题——前者不改变哈希映射、热点仍落在同一分片,后者不减少主库写入且会因延迟恶化读一致性。正确路径是:先止血(热点客户单独路由 + 限流 + 解耦 binlog),再重构路由(外置版本化路由表 + 一致性哈希虚拟节点),然后用双写 + 回填 + 校验做在线重分布,最后按”影子读 → 灰度读 → 灰度写 → 全量”切换。全程可回滚的关键是:切换的只是路由版本,数据在切换前后两侧都存在。
问题拆解:
- 倾斜的成因:
user_id % 64假设用户维度上负载均匀,但 B2B 场景的订单量在用户维度上是重尾分布的——4 个客户占一个分片 61% 的写入。哈希函数保证的是键空间均匀,不是负载均匀。 - 扩容为什么无效:整数取模分片从 64 扩到 128 会导致几乎所有键重新映射,迁移量约 50%;更关键的是,单个客户的订单仍然全部落在同一个分片上——倾斜不会被扩容消除,只会被复制到新拓扑上。
- 读扩展为什么无效:主从延迟 40 分钟说明主库写入压力已经压过了复制链路的处理能力;加从库增加了读容量和复制负担,却没有减少写入,且延迟使”读己之所写”更难满足。
- 下游耦合:直接订阅物理分片 binlog,意味着物理拓扑变化会直接变成下游的重复与乱序事件,这是重分布最大的外部约束。
- 真实目标:把”逻辑路由”与”物理分布”解耦,使未来调分布不必再搬全量数据;同时把热点从单点拆开。
核心方案(分三步):
第一步 · 止血(第 1 周内)
- 在路由层引入租户 pinning:把 TOP N 热点客户显式映射到专用分片(可指向新增的 2 台机器),映射记在版本化的路由表而非代码里。
- 对热点分片做写入整形:批量下单走队列削峰,按客户维度限流,优先级低于普通下单。
- 把下游从”订阅物理分片 binlog”切到”统一变更流”:变更流以事件序号排序并携带幂等键,物理拓扑对下游透明。这一步必须先于任何数据搬迁完成,否则后面的每一步都会打爆下游。
- 把慢查询里可下推的聚合迁到只读副本/OLAP 侧,给主库减负。
第二步 · 重构路由(第 2–3 周)
- 路由规则从代码里的
user_id % 64改为外置路由表 + 一致性哈希虚拟节点:- 虚拟节点数按机器权重配置,新增机器只需迁移约 1/N 的数据;
- 路由表整体原子替换,带单调递增版本号,重复或乱序推送幂等(与配置热推送同一套治理方式);
- 路由信息在客户端缓存 + 服务端兜底校验:命中旧版本返回明确的”路由过期”错误码,客户端强制刷新后重试。
- 对最大客户采用复合分区键(
(tenant_id, order_bucket)):把单个超级租户的写入在 K 个 bucket 上打散,单用户查询扇出 K 路后合并。K 按该租户写入量与单分片承载能力推导,并保留”K=1”的降级能力。 - 全局 ID 到分片的映射:由于订单号不含分片信息,需要一张全局索引表(自身按
order_id分片 + 缓存),或由发号器在生成时写入路由提示。前者通用但有额外一跳,后者更快但耦合发号器。
第三步 · 在线重分布(第 3–4 周)
- 双写:在切换窗口内同时写旧位置与新位置,以主键 upsert + 版本号比对(低版本不得覆盖高版本)。
- 回填:按 keyset 分页(不用 OFFSET)扫描历史数据,速率按主从延迟、IO 与复制滞后自适应节流。
- 校验:按主键 + 内容校验和做全量对账,不一致行数必须为 0 才允许切读。
- 切换顺序:影子读(读两侧并比对,只返回旧侧)→ 灰度读(按租户 1% → 10% → 50% → 100%)→ 灰度写 → 全量 → 停止旧写、下线旧数据。
- 唯一的 5 秒写阻塞窗口用于:路由版本原子切换 + 连接池重建 + 双写开关翻转。
技术选型与依据:
| 环节 | 选择 | 依据 | 备选与代价 |
|---|---|---|---|
| 止血手段 | 租户 pinning + 写入整形 | 见效快、改动小、可回滚 | 只做限流:会伤业务吞吐 |
| 路由模型 | 外置路由表 + 一致性哈希虚拟节点 | 拓扑变化只迁移部分数据,且路由可灰度可回滚 | 维持 %N:扩容即全量重分布 |
| 超级租户 | 复合分区键打散 | 从根上拆开单点写入 | 整租户独占分片:热点会迁移,需持续人工维护 |
| ID → 分片 | 全局索引表 + 缓存 | 与发号器解耦,支持后续任意重分布 | 发号器写入路由提示:快但耦合且历史数据无提示 |
| 数据搬迁 | 双写 + 回填 + 校验 | 可回滚(切回旧路由即可) | 停机迁移:不满足约束 |
| 下游解耦 | 统一变更流 + 幂等键 | 物理拓扑变化对下游透明 | 继续订阅物理 binlog:迁移期必然重复乱序 |
方案取舍:
- 改分区键 vs 就地打散:改键需要重分布全量数据、且按用户查询会变跨片;就地打散(复合键)改动小但单用户查询扇出放大。本题选择”路由外置化 + 仅对超级租户打散”,把代价集中在少数租户上。
- 一致性哈希 vs 整数取模:一致性哈希的代价是路由更复杂、范围查询与热点仍需额外处理;收益是扩容迁移量从约 50% 降到约 1/N,且支持按权重配置。
- 强一致跨片事务 vs 最终一致 + 对账:不允许长事务,因此跨片操作一律走 Saga/异步补偿,唯一性由发号器或全局唯一索引保证,一致性由对账兜底。
失败路径、边界条件与恢复:
- 双写一侧失败:失败侧写入待补队列并由对账修复,绝不静默丢弃;待补队列长度是上线后必须告警的指标。
- 回填限速:按主从延迟阈值与 IO 水位自适应降速;触发阈值时暂停而非报错退出。
- 路由版本回退:因为双写期间两侧都有数据,回退只是把路由版本指回旧值,不需要反向搬迁;这是本方案可回滚的根本原因。
- 幂等:所有写操作带业务幂等键(订单号)并在目标侧建唯一索引抢占;回填用 upsert 而非 insert。
- 超时与重试:DB 操作按 SQLSTATE 分类重试——锁等待超时与序列化失败可重试且必须带抖动与重试预算,唯一约束冲突应幂等返回成功,其余一律不可重试。连接池按操作类型拆分,避免回填任务耗尽在线池。
- 边界条件:回填期间被修改的行可能再次变脏,需要对账做多轮直到收敛;热点租户在迁移期仍在增长,pinning 映射要有容量上限告警。
- 下游重复事件:迁移期下游必然收到重复与乱序事件,因此统一变更流必须携带幂等键与事件序号,并提供”迁移窗口”标记让下游切换去重策略。
性能、可用性与验证指标:
- 分片维度的 CPU、IOPS、主从延迟的分位差(热点分片 vs 中位数),目标是差距收敛到 1.5 倍以内;
- 慢查询数与写入 P99;
- 回填速率、剩余行数、预计完成时间;
- 对账不一致行数(切读前必须为 0);
- 路由版本命中率与”路由过期”错误码占比(反映客户端缓存刷新是否健康);
- 下游重复事件率与乱序率;
- 回滚演练耗时(必须实测,而不是声称可回滚)。
追问
追问 1:为什么加机器和加从库都解决不了这个热点?
回答要点:加机器不改变 user_id % 64 的映射关系,热点客户的所有写入仍然落在同一个分片,新机器的容量拿不到这部分流量;加从库扩展的是读容量,写入仍全部打到主库,而且主从延迟 40 分钟说明复制链路已经被写入压垮,再加从库只会增加复制负担。热点的本质是”单个不可再分的键承载了不成比例的负载”,只能从键的分布或路由上解决。
追问 2:把 64 扩到 128 是不是就够了? 回答要点:不够,且代价很大。整数取模扩容会让几乎所有键重新映射,迁移量约 50%,本质上是一次全量重分布;更关键的是,单个客户的订单在扩容后仍然全部落在同一个分片,倾斜被原样复制到新拓扑。正确做法是先把路由从”取模”换成”外置路由表 + 一致性哈希虚拟节点”,让扩容只迁移约 1/N 的数据,再针对超级租户做键打散。
追问 3:重分布期间怎么保证下游 binlog 消费者不被打爆? 回答要点:根本解法是解耦——下游不再订阅物理分片 binlog,而是消费统一变更流,事件按序号排序并携带业务幂等键,物理拓扑对下游透明。迁移期仍然会产生重复与乱序,因此下游必须按幂等键去重、按序号排序,并在”迁移窗口”内提高去重缓存的保留时长。这一步必须在任何数据搬迁之前完成,否则每一步搬迁都会直接冲击下游。
关键点
- 哈希均匀 ≠ 负载均匀:分片键要按业务负载分布选,而不是按”看起来随机”选。
- 扩容不修倾斜:只改物理拓扑不改键分布,热点会被原样复制。
- 路由要外置并版本化:路由是配置,必须可灰度、可回滚、可幂等推送;客户端缓存要有过期错误码。
- 可回滚来自双侧有数据:双写保证切换只是改路由版本,不需要反向搬迁。
- 下游不要绑物理拓扑:物理分片 binlog 是内部实现细节,一旦被外部消费,重分布就不再是内部操作。
常见误区
- “负载高就加机器/加从库”。容量问题与倾斜问题是两类问题;倾斜下加资源是纯粹的成本浪费,因为新增容量拿不到热点流量。
- “改了分区键数据就会自动重分布”。分区键决定的是新写入的落点,历史数据必须显式搬迁;只改路由不搬数据会造成”按新路由查不到旧数据”的静默错误。
- “对账差不多就行”。对账不一致行数必须为 0 才能切读,因为这类错误在切换后会扩散成长期的数据不一致,且难以定位产生时间。
- “让下游直接消费物理 binlog 很简单”。这会把内部拓扑变成对外契约,之后任何扩容、重分布、故障切换都会变成外部故障;正确做法是提供逻辑变更流。
延伸学习
- Google SRE Book《Handling Overload》:https://sre.google/sre-book/handling-overload/ (已核验;按 criticality 分级拒绝、客户端自适应限流、重试预算)
- PostgreSQL 官方文档(当前版)13.3 显式锁定与 19.12 锁管理(已核验于 2026-10-23:一致性加锁顺序、
deadlock_timeout默认 1 秒、仅40P01/40001可重试)
说明:分片与在线重分布没有统一官方规范,本题答案为工程实践总结;涉及具体数据库参数时请以所用版本的官方文档为准。
L01 · LeetCode
题目 ID:Q-20261026-L01 LeetCode 题号:1109 中文名:航班预订统计 英文名:Corporate Flight Bookings 难度:Medium 官方链接:
- 中文:https://leetcode.cn/problems/corporate-flight-bookings/
- 英文:https://leetcode.com/problems/corporate-flight-bookings/
- 核验说明:两页均于本次访问取得完整题目、示例与约束,中英文描述一致。注意:两页本次均未渲染难度标签,难度 Medium 按 LeetCode 标准分类记录(区间差分类题目),该点本次不可直接核验;英文页显示通过率约 69.1%(会随时间变化)。
题目
有 n 个航班,编号从 1 到 n。给定航班预订表 bookings,其中 bookings[i] = [first_i, last_i, seats_i] 表示在从 first_i 到 last_i(包含两端)的每个航班上预订了 seats_i 个座位。
请返回一个长度为 n 的数组 answer,其中 answer[j] 是第 j+1 个航班预订的座位总数。
输入、输出、约束与示例
输入:bookings: List[List[int]],n: int
输出:List[int],长度 n
约束:
1 <= n <= 2 * 10^41 <= bookings.length <= 2 * 10^4bookings[i].length == 31 <= first_i <= last_i <= n1 <= seats_i <= 10^4
示例 1:
输入:bookings = [[1,2,10],[2,3,20],[2,5,25]], n = 5输出:[10,55,45,25,25]示例 2:
输入:bookings = [[1,2,10],[2,2,15]], n = 2输出:[10,25]先思考
- 如果按最直观的做法,对每条预订记录把区间内每个航班都加一遍,复杂度是多少?在
n和bookings.length都是 2 万时会发生什么? - 一条
[first, last, seats]的影响是”从 first 到 last 都加 seats,之后不再加”。能不能不逐个加,而是只在两个位置做标记,最后一次性还原? - 那两个标记位置分别在哪?如果
last == n,第二个标记要落在数组之外,数组该开多大? - 座位数的最大值是
10^4,记录数 2 万,累加结果的最大值是多少?你用的整数类型会不会溢出?
参考答案
题号:1109 |中文名:航班预订统计 |英文名:Corporate Flight Bookings |难度:Medium
解题思路:
直观方案是按记录展开:对每条 [first, last, seats],循环 for i in range(first-1, last) 给 ans[i] 加 seats。当区间很长、记录很多时,复杂度是 O(记录数 × 平均区间长度),最坏约 4×10^8 次加法,会超时。
关键观察:每条记录的影响是”一段连续区间整体加同一个常数”。对一次区间加法,可以在差分数组上只改两个点:
- 在
first处+seats(从这里开始生效); - 在
last + 1处-seats(从这里开始失效)。
做完所有标记后,对差分数组求一次前缀和,就得到每个位置的实际值。这就是差分数组:把”区间更新 O(区间长度)“变成”区间更新 O(1) + 一次 O(n) 还原”。
实现上用长度为 n + 1 的数组(下标 0 对应航班 1),diff[last] 正好是”第 last 个航班的下一格”在 0 基下标下的位置,天然落在数组内;最后只取前 n 项。
正确性说明:
对任意航班 k,前缀和的第 k 项等于所有满足 first_i <= k 的 +seats_i 与所有满足 last_i < k(即 last_i + 1 <= k)的 -seats_i 之和。前者是所有”起点不晚于 k”的记录,后者是所有”终点早于 k”的记录,两者相减恰好是所有满足 first_i <= k <= last_i 的 seats_i 之和,与题目定义一致。
时间复杂度:O(n + m),其中 m 为记录数。每条记录 O(1),一次前缀和 O(n)。 空间复杂度:O(n)(差分数组;返回值不计入)。
Python 参考代码(可运行,已在本机通过 7 个定向用例 + 200 组随机对拍 + 2 万×2 万压力测试):
from typing import List
class Solution: def corpFlightBookings(self, bookings: List[List[int]], n: int) -> List[int]: # diff[i] 表示第 i+1 个航班相对第 i 个航班的增量 # 开 n + 1 长度:last == n 时,diff[last] 是"失效位置"的哨兵,不会被计入答案 diff = [0] * (n + 1) for first, last, seats in bookings: diff[first - 1] += seats # 从 first 开始生效 diff[last] -= seats # last + 1 起失效(0 基下标下即 diff[last]) ans: List[int] = [] cur = 0 for i in range(n): cur += diff[i] ans.append(cur) return ans关键边界情况:
first == last:只有一条记录覆盖单个航班,两个标记分别落在first-1与first,必须都生效。last == n:失效标记落在diff[n],这正是数组多开一格的用途;若数组只开n会越界。n == 1:数组长度为 2,答案只取 1 项。- 累加溢出:最坏约 2×10^4 × 10^4 = 2×10^8,Python 整数无此问题;用固定宽度整型时要注意位数。
- 区间端点必须按”包含两端”处理,
-seats落在last + 1而不是last,这是最容易写错的一格。
测试用例(均在本机执行通过,并与暴力解交叉验证):
| # | bookings | n | 期望输出 | 覆盖点 |
|---|---|---|---|---|
| 1 | [[1,2,10],[2,3,20],[2,5,25]] | 5 | [10,55,45,25,25] | 官方示例 1,多区间叠加 |
| 2 | [[1,2,10],[2,2,15]] | 2 | [10,25] | 官方示例 2,first == last |
| 3 | [[1,1,5]] | 1 | [5] | n == 1 最小规模 |
| 4 | [[1,3,1],[1,3,2],[1,3,3]] | 3 | [6,6,6] | 多条完全重叠区间 |
| 5 | [[5,5,7],[1,1,3]] | 5 | [3,0,0,0,7] | 两端单点 + 中间为空 |
| 6 | [[2,4,10]] | 4 | [0,10,10,10] | 区间不从 1 开始 |
| 7 | [[1,5,10000]] | 5 | [10000,10000,10000,10000,10000] | seats 取上界 |
变体 / 追问:
如果每条记录不再是”区间加常数”,而是”区间内的值必须都不超过某个上限”(例如每个航班的容量有上限,问是否所有预订都能被满足),差分还适用吗?
回答要点:仍然用差分求出每个航班的实际预订量,再与容量逐项比较即可;若进一步要求”给出第一个超载的航班”,可以在求前缀和的过程中一旦超过容量就返回当前下标,把 O(n) 的还原与判定合并成一次扫描。这就是同骨架的区间占用/资源预留问题(例如会议室预订、带宽预留)。若区间端点不是整数而是时间点,则需要先离散化再做扫描线。
可迁移的解题能力:
- 把”区间批量更新”建模为”两端点标记 + 一次还原”:这是处理所有区间增量问题的通用骨架,把 O(区间长度) 压成 O(1)。
- 哨兵格的取舍:多开一格可以让”last + 1”永远落在数组内,省掉边界分支;代价是要清楚答案只取前 n 项。
- 先建模再优化:先写出暴力解明确语义,再用差分替换内层循环,并用随机对拍验证两者一致——这是能迁移到任何”优化型”题目的验证方法。
与 AI Agent 工程的联系(自然联系):
区间增量 + 前缀和的骨架,对应 Agent 系统里”按时间窗口聚合”的一类需求:并发度与配额占用的时段统计、按时间分桶的 token 与成本归因、长任务在时间轴上的资源预留。共同点是:写入时只记增量(O(1)),查询时一次性还原(O(n)),适合”写多读少、且必须能按任意区间回溯”的场景。需要注意的边界与本题完全一致——失效位置是”结束点的下一格”,写错一格会让归因结果整体平移。
关键点
- 区间加常数 → 差分数组两端点标记:
first处+,last + 1处-。 - 数组开
n + 1作为哨兵,答案只取前n项。 - 复杂度从 O(n·m) 降到 O(n + m)。
- 端点语义是”包含两端”,
-的位置是最容易写错的一格。
常见误区
- 把
-seats写在diff[last - 1]或diff[last + 1]。区间是闭区间,last这个航班仍然要被加,失效点是last + 1;在 0 基下标下即diff[last]。写错会让结果整体少一格或多一格。 - 数组只开
n并在last == n时特判。虽然可以用条件判断绕开,但多开一格更不容易出错;特判分支往往是边界 bug 的来源。 - 先记标记再查值导致顺序错误。差分必须先完成所有标记再统一求前缀和;边标记边读取会得到不完整的中间结果。
延伸学习
- LeetCode 1109 官方页面(中英文):题目、示例与约束一致,见上方链接。
- 延伸方向(同骨架,本轮未核验页面正文):1094. 拼车(区间占用判定)、扫描线算法在区间端点非整数时的离散化处理。
今日延伸资料
按优先级排序,均为本轮核验过的官方或原始来源。
| 主题 | 资料 | 类型 | 核验状态 |
|---|---|---|---|
| 持久化边界 | Linux 手册 fsync(2):https://man7.org/linux/man-pages/man2/fsync.2.html | 官方手册 | 已核验(man-pages 6.19) |
| mmap 语义 | Linux 手册 mmap(2):https://man7.org/linux/man-pages/man2/mmap.2.html | 官方手册 | 已核验(man-pages 6.19) |
| 位置编码 | RoFormer / RoPE:arXiv:2104.09864 | 原始论文 | 已核验(2021-04-20 v1 / 2023-11-08 v5) |
| 长度外推 | ALiBi《Train Short, Test Long》:arXiv:2108.12409 | 原始论文 | 已核验(2021-08-27 v1 / 2022-04-22 v2) |
| 上下文扩展 | Position Interpolation:arXiv:2306.15595 | 原始论文 | 已核验(2023-06-27 v1 / 2023-06-28 v2) |
| 上下文扩展 | YaRN:arXiv:2309.00071 | 原始论文 | 已核验(2023-08-31 v1 / 2026-02-06 v3) |
| MCP 攻击面 | GHSA-7rgq-gccq-qphx(路径校验 check/use 不匹配) | 官方安全公告 | 已核验(2026-10-25 发布,CWE-22 + CWE-436,CVSS 4.2) |
| MCP 攻击面 | GHSA-mvf5-2qqv-rg3c(通配 CORS + 环境变量跨站泄露) | 官方安全公告 | 已核验(2026-10-25 发布,CWE-200 + CWE-942,CVSS 6.5) |
| MCP 攻击面 | GHSA-53mr-w429-c597(TOCTOU)、GHSA-4gxg-m5jx-9j57(大小写变体绕过符号链接检查) | 官方安全公告 | 已核验(CVSS 分别 2.2;CWE-59 + CWE-178) |
| 过载治理 | Google SRE Book《Handling Overload》 | 官方工程手册 | 已核验(2026-10-23 复核;注意该章未提及 Little’s Law) |
| LeetCode | 1109 中英文官方页 | 官方题目页 | 已核验(题目、示例、约束一致;难度标签两页均未渲染,按标准分类记为 Medium) |
本轮未核验、不得作为事实引用:MCP 参考服务器 v2.1.0 的完整发布说明正文(仅从配置变更记录与安全公告间接取得);其余 GHSA 公告(GHSA-hhwq-wxfx-cj7g、GHSA-v38p-wf49-hx2j、GHSA-66qx-2266-pmj6、GHSA-3jqg-r5pp-rmp2)本轮未逐份抓取;LeetCode 1094 页面正文;具体到某数据库版本的在线重分布工具能力。
当日重点
- 持久化的判定点必须前移到”对外宣称完成”之前:
write()与close()都不是判定点;目录项是独立对象,原子替换需要fsync文件 +fsync目录。 - 上下文窗口是上限不是预算:RoPE 直接外推风险高,ALiBi 天然更可外推,PI / YaRN 走内插 + 微调;扩窗必须回归原窗口内的质量。
- 校验对象必须等于生效对象:MCP 工具服务的越界类漏洞共享这一根因;修复要落在解析后的唯一值上,并且判定要在模型之外。
- 自动升级在版本方案切换后会静默失效:必须显式 pin 版本并在 CI 中检查版本方案。
- 分片倾斜不是容量问题:加机器与加从库都不改变键分布;解法是路由外置化 + 一致性哈希 + 超级租户打散,并用双写保证”切路由即可回滚”。
- 差分数组:区间增量的两端点标记法,失效点是
last + 1。
本次运行信息
配置读取状态:成功。读取 ../题库配置.md,实际版本 version 7(last_maintained: 2026-10-26)。本次为该版本生效后的首次每日出题(配置变更记录明确:“2026-10-26 当日任务若尚未执行则即时生效”),故本次按 v7 执行。配额仍为 5 道(B01 / B02 / S01 / S02 / L01)。无暂停类型;「观察中的新技术」8 项均未标记「允许试学」,未从中出题。题库配置.md 未被修改。
去重窗口:30 天(2026-09-26 至 2026-10-26)。
本次参考的历史题目来源:
题目索引.md(读取时 65 条,indexed_runs: 11,窗口 2026-07-23 至 2026-10-23);精选/README.md(4 条精选题);- 未逐字读取更早历史训练文件正文。
去重结果:
| 编号 | 一级类型 | 与近期 30 天题目的关系 |
|---|---|---|
| B01 | 操作系统与网络 / 页缓存与持久化边界 | 首次考察。该类型近期题目为 DNS TTL(10-19)、优雅终止(10-19)、mTLS 轮换(10-20)、连接池(10-16)、CFS 限流(10-14),二级主题不重合;与 10-16 B01 同为”连接与缓冲”但对象不同(网络缓冲 vs 页缓存) |
| B02 | AI 与大模型基础 / 位置编码与长度外推 | 首次考察,填补索引标注的缺口。与 10-19 B02(Attention / KV Cache / 窗口容量)相邻但角度不同:前者是机制与外推能力,后者是容量与成本 |
| S01 | AI 安全与治理 / 工具与互操作协议 | v7 新增场景类型「自建与第三方 MCP 工具服务的攻击面评审」首次出题。与 10-20 S01(Server Card 跨组织发布)同属工具协议但二级主题不同(发布发现 vs 攻击面收敛);与 10-15 S01(工具调用失败语义)不重合 |
| S02 | 数据库与存储 / 分布式系统 | 首次考察分片键倾斜与在线重分布。与 10-14 S02(零停机 schema 变更)方法上有双写回填的共性,但核心命题不同(列拆分 vs 键分布与路由);已通过”路由外置化 + 一致性哈希 + 租户打散”构造差异化方案 |
| L01 | LeetCode 1109(差分数组) | 已用题号:146、3、215、739、875、124、207、142、547、560、39(另 322 已删未登记,仍规避)。1109 首次出现,算法模式为差分数组 / 前缀和还原,与近期的前缀和计数(560)、回溯(39)、并查集(547)不同 |
LeetCode 轮换:本轮按索引建议从「区间 DP、差分数组、贪心、链表操作类、Trie 或单调队列」中选取差分数组,避免连续使用回溯、前缀和哈希、并查集、图论、DP、栈与二分主导。
到期精选提醒(本次未自动执行任何精选或复习动作):
Q-20261012-B02(建议复习日 2026-10-19)、Q-20261013-B03(2026-10-20)、Q-20261015-B03(2026-10-23)、Q-20261015-S01(2026-10-24)均已过建议复习日,且精选/README.md未记录复习完成。
建议复习日(本次生成):
| 题目 ID | 建议首次复习 | 复习角度建议 |
|---|---|---|
| Q-20261026-B01 | 2026-11-02 | 从”定义与边界”转为”故障定位”:给定”磁盘空间充足但写入报 ENOSPC / 掉电后文件长度回退”的现象,要求分层归因 |
| Q-20261026-S01 | 2026-11-02 | 从”攻击面评审”转为”准入体系设计”:要求把四条收敛主线固化为可执行的 CI 规则与评审模板 |
| Q-20261026-S02 | 2026-11-04 | 从”分片倾斜”转为”跨片查询与一致性”:要求设计全局索引表与跨片事务的替代方案 |
归档:2026-10/2026-10-26-AI-Agent-面试训练.md。索引更新在训练文件成功写入后执行。写入 Obsidian 本地目录不等于远端同步成功,本文件未经远端同步验证。