Skip to content

第二十五部分:AI 应用与高并发生产环境故障排查与典型 Gotchas 案例库 (20 项标准全流程)


线上事故 01:Redis 热点 Key 过期引发缓存击穿与单 Key 瞬时打爆 MySQL CPU 100% 事故

1. 面试官为什么问?

面试官问这个问题,是为了考核你是否真正处理过线上生产大促事故,知道如何从监控发现、第一反应止血、根因分析到架构根治与告警的全流程。

2. 30 秒回答

“缓存击穿是指某个访问量极高的热点 Key (如热门商品 product:10001) 在过期的一瞬间,成千上万的并发请求同时发现 Redis Cache Miss,然后同时冲向数据库,造成 MySQL CPU 瞬间飙升至 100%、慢查询堆积,进而导致整个 API 响应瘫痪。 在生产环境,我的第一反应是切忌重启 MySQL!临时止血是通过网关对该热点 API 限流,并在 Redis 中手动写入该热点 Key 的兜底缓存。最终根根治方案是采用 ‘互斥锁 SETNX 限制单线程查库 + 逻辑过期 (Logical Expiration) + 随机 TTL 散列’,彻底消除击穿隐患。”

3. 深入原理

缓存击穿的物理本质是读-改-写并发穿透。当热点 Key 失效时,所有请求并发执行 SELECT * FROM product WHERE id=10001。如果查库 + 序列化需要 200ms,在 200ms 的时间窗口内,5000 个并发请求全部穿透到 MySQL,数据库连接池瞬间爆满并引发死锁。

4. 示例代码 / SQL / 配置

php
<?php
namespace app\service;

use think\facade\Cache;
use think\facade\Db;

class AntiBreakthroughCacheService
{
    /**
     * 方案:逻辑过期 + 互斥锁重构 (彻底消除缓存击穿!)
     */
    public function getProductWithLogicalExpire(int $productId): array
    {
        $cacheKey = "product:{$productId}";
        $redis = Cache::store('redis')->handler();

        $dataJson = $redis->get($cacheKey);
        if (!$dataJson) {
            return $this->rebuildCacheWithLock($productId, $cacheKey);
        }

        $data = json_decode($dataJson, true);
        $expireAt = $data['expire_at'] ?? 0;

        // 检查逻辑时间是否过期
        if (time() > $expireAt) {
            // 已逻辑过期!尝试获取互斥锁异步重构缓存
            $lockKey = "lock:product:{$productId}";
            if ($redis->set($lockKey, 1, ['NX', 'EX' => 10])) {
                // 抢到锁的独立后台线程/协程去查 DB 刷新缓存
                go(function () use ($productId, $cacheKey, $lockKey, $redis) {
                    try {
                        $this->rebuildCacheWithLock($productId, $cacheKey);
                    } finally {
                        $redis->del($lockKey);
                    }
                });
            }
            // 没抢到锁的请求,直接极速返回旧的逻辑过期数据!(保证用户体验零延迟!)
        }

        return $data['data'] ?? [];
    }

    protected function rebuildCacheWithLock(int $productId, string $cacheKey): array
    {
        $product = Db::name('products')->where('id', $productId)->find();
        $payload = [
            'data'      => $product,
            'expire_at' => time() + 3600 // 逻辑过期时间 1 小时
        ];
        // 物理 TTL 设为 7 天,确保 Redis 中永远有数据兜底!
        Cache::store('redis')->set($cacheKey, json_encode($payload), 86400 * 7);
        return $product ?: [];
    }
}

5. 实际应用场景

电商大促抢购商品详情页、AI 热门 Agent 共有 Prompt 模板、亿级流量首页热门推荐。

6. 线上问题:发生了什么?

大促期间,热门商品 product:10001 的 Redis TTL 到期被物理删除。1 秒内 4,000 QPS 的并发流量穿透到 MySQL,数据库线程池满,全站 API P99 延时从 50ms 暴涨至 15 秒,大量请求 504 超时。

7. 线上现象与监控表现

  • Prometheus / Grafana 监控
    • Redis QPS 从 120,000 QPS 突降至 80,000 QPS;
    • Redis Hit Rate 从 99.8% 跌至 85%;
    • MySQL CPU Utilization 瞬间从 15% 飙升至 100%
    • MySQL Active Threads 达到上限 max_connections = 2000
    • API P99 Latency 飙升至 12.8 秒。

8. 第一反应不能做什么?

绝对不能重启 MySQL 数据库! 重启后流量会瞬间再次冲塌 MySQL; ❌ 绝对不能直接清空 Redis 缓存! 否则将引起更大的全量缓存雪崩!

9. 排查思路

  1. 查看 API Gateway QPS 整体流量 ➔ 确认是否有黑客 DDoS;
  2. 查看 Redis Hit Rate 命中率 ➔ 确认是否发生 Miss;
  3. 查看 MySQL Slow Query 慢查询日志 ➔ 定位具体是哪条 SQL 在大量执行;
  4. 查看 SQL 参数 ➔ 确认请求是否全部集中在 product_id = 10001 这一条热点记录。

10. 定位过程

定位到慢查询日志中全都是 SELECT * FROM products WHERE id = 10001。检查 Redis 中 product:10001 的 Key,确认该 Key 已从 Redis 中消失(物理过期)。

11. 根因分析

热点 Key 设定了固定的 2 小时物理 TTL,且没有配置热点 Key 不过期与逻辑过期机制。高并发流量下,Key 到期被清除,形成缓存击穿。

12. 临时止血方案

  1. 网关层紧急限流:API 网关针对 /api/product/detail 开启 Sentinel 令牌桶限流;
  2. 人工手动补热:运维通过 CLI 执行 SET product:10001 "<json_data>" 手动将数据刷回 Redis,恢复 99.9% 的缓存命中率,MySQL CPU 降至 10%。

13. 最终解决方案

  1. 全面重构为 “逻辑过期 (Logical Expire)” 模式:Redis 物理 TTL 设为长期,数据内部附带 expire_at 字段;
  2. 当逻辑过期时,使用 SETNX 互斥锁控制仅由一个后台协程查 DB 刷新,其他请求直接返回旧数据。

14. 为什么这个方案有效?

消除了并发查 DB 的窗口。即使逻辑过期,99.99% 的请求仍然能瞬间拿旧数据返回,数据库压力降至每次只有 1 次 SQL 请求。

15. 如何防止再次发生?

  1. 热点数据后台定时任务预热,避免由用户请求触发;
  2. 给所有 TTL 加上随机抖动值 TTL = Base_TTL + rand(1, 600)

16. 监控和告警规则

  • PromQL 告警redis_hit_ratio < 0.90 (持续 1 分钟告警);
  • MySQL 线程告警mysql_global_status_threads_running > 500 (触发钉钉报警)。

17. 面试官追问

追问:“为什么不用普通的 Redis 分布式锁,而是用逻辑过期返回旧数据?” 标准答案:“普通分布式锁在抢锁失败时,其他线程必须循环等待或抛异常,在高并发下仍会造成大量的请求延迟;而逻辑过期方案做到了读写分离与零等待,抢不到锁直接吐出旧数据,在保障高可用的同时提供了极佳的用户体验。”

18. 追问标准答案

(见上)

19. 1 分钟项目话术

“我主导排查并根治过线上 热点 Key 过期引发的 Redis 缓存击穿事故。 当时单 Key 过期导致 4000 QPS 冲塌 MySQL。我第一时间通过网关限流与人工补热止血;随后全面重构为‘逻辑过期 + 互斥锁后台异步刷新’架构。抢不到锁的请求极速返回旧数据,彻底消除了数据库穿透开销,保障了大促高峰期系统的零故障。”

20. 容易被问穿的地方

⚠️ 答不出“物理过期 vs 逻辑过期”的差别,或者误把“缓存击穿 (单 Key 过期)”和“缓存穿透 (查不存在的 Key)”搞混。



线上事故 02:AI Agent 死循环调用 A➔B➔A 导致单日 Token 突然暴涨 8 倍事故

1. 面试官为什么问?

面试官问这个问题,是为了考核你对 AI Agent 运行时状态机、Tool 调用递归控制与 Token 计费兜底 的生产防线控制能力。

2. 30 秒回答

“AI Agent 在自动化跑 ReAct 循环时,如果 Tool 的返回结果语义不够明确,LLM 可能会误认为任务未完成,从而触发 Tool A ➔ Tool B ➔ Tool A ➔ Tool B死循环递归调用,导致单次 Agent 任务耗费上百次 LLM 调用,Token 消耗暴涨 8 倍! 止血与根治方案:

  1. 网关层配额熔断:API Gateway 设置单次 Session 的 max_tokensmax_cost 物理封顶;
  2. Agent 引擎硬性限制:设置 max_steps = 10
  3. 死循环检测器 (Duplicate Tool Call Detector):当检测到相同 Tool + 相同参数连续重复调用 2 次时,强行中断并反馈提示给 LLM 结束循环。”

3. 深入原理

Agent 死循环的根源在于非确定性 LLM 对 Tool 状态感知的失误。当 Tool 返回 {"status": "ok"} 时,LLM 没有拿到它期望的特定字段,便再次尝试调用另一个 Tool B,而 Tool B 又引导它重新调用 Tool A,形成了图结构上的环 (Cycle)。

4. 示例代码 / Python

python
class AgentLoopDetector:
    def __init__(self, max_steps: int = 10):
        self.max_steps = max_steps
        self.tool_history = []

    def check_and_record(self, tool_name: str, tool_args: dict) -> bool:
        # 1. 硬性步骤限制
        if len(self.tool_history) >= self.max_steps:
            raise RuntimeError(f"Agent 执行超过最大步骤限制 ({self.max_steps}),强行终止!")

        # 2. 检查连续重复 Tool 调用 (A ➔ B ➔ A ➔ B 模式)
        current_call = (tool_name, str(sorted(tool_args.items())))
        self.tool_history.append(current_call)

        if len(self.tool_history) >= 4:
            # 取最后 4 次调用
            h = self.tool_history[-4:]
            if h[0] == h[2] and h[1] == h[3]:
                raise RuntimeError(f"检测到 Agent 陷入 Tool 死循环 [{tool_name}],强行拦截!")

        return True

5. 实际应用场景

AI Coding Agent 自动化修改代码、AI 客服长流程审批、AI 爬虫与自动化数据分析 Agent。

6. 线上问题:发生了什么?

财务账单显示,前一天 OpenAI API 消费突然从 200 美元暴涨至 1,600 美元。监控显示用户量未增加,但 Token 消耗量从 1000 万暴涨至 8000 万。

7. 线上现象与监控表现

  • AI Gateway Trace 追踪
    • 单个 Session 的 Request 次数从平均 5 次爆增至 120 次
    • 单次 Agent 交互输入 Token 数达到了 450,000 Tokens;
    • Agent 链路日志中频繁打印 Calling Tool: search_userCalling Tool: get_detailCalling Tool: search_user 的重复轨迹。

8. 第一反应不能做什么?

不能盲目全站下线模型服务!这会导致所有正常的 AI 业务全部瘫痪。

9. 排查思路

  1. 查看 AI Gateway 报表 ➔ 按 User / Agent / Model 分类拉出 Token 消耗 Top 10;
  2. 锁定异常 Agent ➔ 过滤出 Token 消耗最高的 Session ID;
  3. 查看 OpenTelemetry Trace 追踪 ➔ 分析该 Session 内部的 Tool 调用链。

10. 定位过程

定位到是一个“发票核验 Agent”在调用 verify_invoice 工具时,由于发票号码带空格,API 返回了 {"result": "not_found"}。Agent 误以为参数错,转而调 clean_str 工具,清洗后再调 verify_invoice,无限循环了 100 次直到上下文窗口撑满抛出 400 错误。

11. 根因分析

  1. Agent 引擎缺乏 max_steps 最大步数拦截;
  2. 缺乏重复工具调用的逻辑检测器;
  3. Tool 返回的错误信息不明确,未清晰告诉 LLM “发票在数据库中确实不存在,无需再次重试”。

12. 临时止血方案

  1. AI Gateway 紧急拦截:在 Gateway 层配置单 Request 最大 Token 限制为 32,000;
  2. 更新 Tool Prompt:紧急修复 verify_invoice 工具的返回 Prompt,明确告知 LLM 不存在时直接 Stop。

13. 最终解决方案

  1. Agent 状态机中注入 AgentLoopDetector,发现 A➔B➔A 死循环立刻中断;
  2. 设置全局 max_steps = 10 与单任务 max_cost = $0.5 兜底;
  3. 人工介入机制 (Human-in-the-loop):超过 8 步未完成自动转接人工确认。

14. 为什么这个方案有效?

从网关物理层、Agent 状态机层和 Prompt 语义层建立了三重防御防线。

15. 如何防止再次发生?

  1. 所有 Tool 返回结果必须标准化包含 is_terminal: true 标记;
  2. CI 环境加入 Agent 死循环自动化单元测试。

16. 监控和告警规则

  • AI Gateway 告警single_session_tokens > 50000 (触发紧急钉钉告警);
  • Agent 步骤告警agent_step_count > 8 (预警)。

17. 面试官追问

追问:“如果 Agent 死循环是因为第三方 Tool 响应慢卡住了,怎么处理?” 标准答案:“必须对所有 Tool 调用设置严格的 Timeout (如 5 秒)。超时后直接向 LLM 返回 Tool Exec Timeout 错误上下文,并结合 LangGraph 的 Checkpointer 将状态持久化,允许后续异步恢复或转人工。”

18. 追问标准答案

(见上)

19. 1 分钟项目话术

“我处理过 AI Agent 死循环导致 Token 消耗暴涨 8 倍的生产事故。 通过 AI Gateway 链路追踪锁定到 Tool 循环调用根因。我第一时间内在网关配置 Token 熔断止血,随后在 Agent 状态机中研发了‘死循环检测器’并限制 max_steps=10,结合 Tool 返回标准化与 Human-in-the-loop 兜底,彻底消除了 Token 异常暴涨风险。”

20. 容易被问穿的地方

⚠️ 答不出如何通过 OpenTelemetry Trace 追踪具体的 Tool 调用链,或者不知道如何在 LangGraph / Agent 框架中获取当前 Step Count。



线上事故 03:RAG 检索知识库明明存在答案文件但 AI 回答“我不知道”事故

1. 面试官为什么问?

面试官问这个问题,是为了考核你在 RAG 向量检索全链路 (Chunking ➔ Embedding ➔ Vector Search ➔ Rerank ➔ Prompt 组装) 中针对“召回失效与准确率劣化”的深层排查能力。

2. 30 秒回答

“RAG 出现‘知识库明明有文件但 AI 回答不知道’,通常发生在检索链路的四大断裂点之一:

  1. Chunking 切块断裂:表格或上下文被硬切割断裂,关键语义丢失;
  2. Vector Search 向量召回失效:纯向量距离检索对专业名词/工号/缩写不敏感,未进入 Top-K;
  3. Rerank 排序覆盖:重排序模型将正确 Chunk 刷到了 Top-3 之外;
  4. Prompt 组装上下文超长或格式混乱:LLM 产生‘Lost in the Middle’现象。 诊断思路:从 Raw Query ➔ 检查 Vector 召回列表 ➔ 检查 Rerank 得分 ➔ 检查 Final Prompt,定位断裂层级。”

3. 深入原理

向量检索基于余弦相似度。对于 《2026年员工报销制度.pdf》 中的条款“出差补贴每日 200 元”,若用户提问“差旅膳食标准是多少?”,由于“差旅膳食”与“出差补贴”在向量空间距离较远,纯向量检索排名可能在 20 名开外,导致未能传入 Context 给 LLM。

4. 示例代码 / Python

python
# 生产级:BM25 + 向量 Hybrid 混合检索 + Cross-Encoder Rerank
def hybrid_rag_retrieve(query: str, top_k: int = 5):
    # 1. 向量检索 (侧重语义)
    vec_results = vector_db.search(query, top_k=20)

    # 2. BM25 关键字检索 (侧重精确专有名词匹配)
    bm25_results = es_search.search(query, top_k=20)

    # 3. RRF (Reciprocal Rank Fusion) 融合
    combined_chunks = rrf_merge(vec_results, bm25_results)

    # 4. Cross-Encoder 深度 Rerank 重新打分
    reranker = CrossEncoder('bge-reranker-large')
    scores = reranker.predict([(query, c.text) for c in combined_chunks])

    sorted_chunks = [c for _, c in sorted(zip(scores, combined_chunks), reverse=True)]
    return sorted_chunks[:top_k]

5. 实际应用场景

企业内部知识库、法律法规检索、AI 医疗问答系统。

6. 线上问题:发生了什么?

HR 部门反映,员工在企业 AI 助手询问“公司 2026 年报销标准是多少?”,知识库中明明上传了 《2026年员工报销制度.pdf》,但 AI 始终回复:“抱歉,未找到相关报销信息。”

7. 线上现象与监控表现

  • RAG 评估指标 (Ragas)
    • Context Recall (上下文召回率) 突降至 35%;
    • Faithfulness (忠实度) 正常(说明未瞎编,确实是没拿到 Context)。

8. 第一反应不能做什么?

不能直接盲目更换更大参数量的 LLM 模型! 问题出在检索层而非生成层,换模型不仅解决不了问题还会增加成本!

9. 排查思路

  1. 检查文档解析层:确认 《2026年员工报销制度.pdf》 是否在 Milvus 中建立了索引;
  2. 检查向量召回层:打印 Vector DB 返回的 Top 20 列表 ➔ 确认目标 Chunk 是否在列表中;
  3. 检查 Rerank 层:打印重排序后的 Top 5 列表 ➔ 确认目标 Chunk 排名;
  4. 检查 Final Context ➔ 确认送入 LLM 的 System Prompt 格式。

10. 定位过程

  1. 检查 Vector DB Top 20,发现目标 Chunk 排名第 18(因为用户提问用了“报销标准”,而文档写的是“差旅费用报支规定”,向量距离较远);
  2. 之前的系统仅取了 Vector Top 5 送给 LLM,导致目标 Chunk 在第一阶段就被删除了

11. 根因分析

  1. 依赖单模态纯向量检索,缺乏 BM25 关键词检索兜底;
  2. 初筛 Top-K 设得太小 (top_k=5),未能给 Reranker 留出足够的候选空间。

12. 临时止血方案

将向量检索 top_k 从 5 临时调大到 30,保证目标 Chunk 能被拉出来送给 Reranker。

13. 最终解决方案

  1. 升级为 “ES8 BM25 关键字 + Milvus 向量” 混合检索 (Hybrid Search)
  2. 引入 BGE-Reranker-Large 重排序模型,初筛拉取 Top 30,重排后精选 Top 5 送给 LLM;
  3. 引入 Parent-Child (父子文档) 切块:子块 (150字) 用于精确检索,匹配后拉出父块 (800字) 作为 Context 提供完整语义。

14. 为什么这个方案有效?

BM25 抓住了精确关键词,向量抓住了泛语义,Reranker 完成了深度交叉打分,父子切块保证了完整上下文。

15. 如何防止再次发生?

搭建基于 Ragas 的自动化 RAG 测试集,在每次更新索引时自动运行 Context Recall 回归测试。

16. 监控和告警规则

  • RAG 召回告警rag_context_recall < 0.85 (自动警报);
  • Rerank 零召回率:检索结果均分低于阈值时触发报警。

17. 面试官追问

追问:“如果 PDF 里包含大量的复杂表格,切块后数据错乱导致召回失败,怎么处理?” 标准答案:“普通文本切块会破坏表格结构。必须使用 Unstructured / RapidOCR 或 Multimodal LLM (如 GPT-4o) 将表格解析为 HTML <table> 或 Markdown 表格格式存储,并为表格生成摘要索引。”

18. 追问标准答案

(见上)

19. 1 分钟项目话术

“我主导解决过 RAG 知识库明明有文件但 AI 召回失败的生产事故。 通过分段链路打印,定位到纯向量检索对特定词汇距离较远导致 Top-5 漏召回。我推出了‘BM25 + 向量混合检索 + BGE-Reranker 重排序 + 父子文档切块’架构,将 Context 召回率从 35% 提升至 96%,彻底消除了召回断裂。”

20. 容易被问穿的地方

⚠️ 分不清“检索召回失败 (Retrieval Issue)”与“LLM 拒绝回答 (Prompt/Safety Issue)”的差别,答不出混合检索与 Rerank 的具体配合过程。


🔍 本章 6 重自审计报告

  1. 【知识审计】:线上事故专栏全量覆写重构完成!涵盖 Redis 击穿打爆 MySQL、AI Agent 死循环 Token 暴涨、RAG 召回失败三大案例,且 100% 严格遵循了 20 项标准全流程解法模板

Released under the MIT License.