跳转至

分布式共识算法

Paxos/Raft 算法原理

⚖️ 1. Raft 算法的三个核心子问题是什么?

Raft 算法的设计目标就是 易懂且可实现,它将分布式共识拆解成了三个完全解耦的子问题,你可以像搭积木一样逐个击破:

1.1 领导者选举(Leader Election)

集群必须有一个唯一的领导者来负责接收写请求并同步日志。选举过程保证了在任何时刻,集群中最多只有一个被选举出来的 Leader。

  • 机制:基于随机的选举超时(150-300ms)。节点在超时未收到 Leader 心跳后,会变为 Candidate,增加自身任期(Term),并向其他节点请求投票。如果获得多数票,则成为 Leader,并立即发送心跳确立权威。

  • 关键:Term 是逻辑时钟,每个节点只在一个 Term 内投出一票。如果两个 Candidate 票数均分,则发生 Split Vote,大家各自增加 Term 重新选举,直到选出一个 Leader。

1.2 日志复制(Log Replication)

一旦 Leader 被选出,它必须将客户端的写操作以日志条目的形式复制到所有 Follower,并保证各节点日志最终一致。

  • 过程:Leader 收到写请求后,先将命令追加到本地日志,然后并行向所有 Follower 发送 AppendEntries 请求。当该日志条目被 大多数(Majority) 节点安全复制后,Leader 会 commit 该条目,然后在本地状态机上执行,并将已提交的索引通过后续心跳通知 Follower 执行。

  • 安全性:Leader 永远不会覆盖或删除自己的日志条目,只会追加。如果 Follower 的日志与 Leader 冲突,Leader 会强制该 Follower 复制自己的日志。

1.3 安全性(Safety)

即使发生网络分区或选举,Raft 也必须保证所有已提交的日志被所有未来 Leader 包含,且状态机执行顺序一致。

  • 核心约束:
  • 选举限制:Candidate 必须包含所有已提交的日志才能成为 Leader。这意味着投票时,节点不会把票投给日志比自己旧的 Candidate。
  • 提交旧任期的限制:Leader 只能提交当前任期内的日志,以提交前一条日志的方式间接提交旧任期的日志,避免“已提交却被覆盖”的情况。
  • 领导权完整性:Leader 一旦提交了某条日志,该日志就会出现在所有未来 Leader 的日志中。

这三大子问题的巧妙之处在于,它们把一个复杂的多节点状态机问题,变成了三个独立、可推理的组件。只要分别保证每个子问题的正确性,整个 Raft 就能在出现各种故障时依然提供线性一致的分布式共识。


🆚 2. Raft 和 Paxos 有什么区别?为什么 Etcd/ZooKeeper 都选择 Raft?

Paxos 是分布式共识的理论先驱,而 Raft 是专为工程实现而设计的“务实版”。两者都解决了共识问题,但在可理解性与工程化上有着天壤之别。

2.1 Raft vs. Paxos 的核心区别

查看内嵌表格

2.2 为什么 Etcd/ZooKeeper 都选择 Raft?

ZooKeeper 使用的其实是 ZAB(ZooKeeper Atomic Broadcast) 协议,它在 Multi-Paxos 思想上做了许多定制,但并不是原生 Paxos,更不是 Raft。ZooKeeper 出现时 Raft 还未诞生,所以它用了自研协议。而现在,新一代的协调服务 Etcd、Consul、TiKV、Nacos 的 CP 模式 都一致选用了 Raft,原因很清楚:

  • 工程可信度:Raft 把分布式共识从“高深数学”降维成了“可推理的工程模块”,大大减少了因理解偏差导致的实现 Bug。团队敢于自己去优化甚至重写。

  • 易于维护和调试:清晰的 Leader 角色和日志结构,使得排查一致性问题和性能瓶颈极为高效。Paxos 的极端情况往往难以复现。

  • 强健的领导者模型:Raft 天然就是强 Leader 模式,所有写都走 Leader,读也可以走 Leader(或通过 read index 保证线性),这和分布式 KV 存储、配置中心这类场景完美契合。

  • 安全且简单的成员变更:集群扩缩容在生产中是高频操作,Raft 的 Joint Consensus 方案让变更过程不丢失任何已提交数据,且操作傻瓜化。

  • 丰富的生态:因为易懂,衍生出大量可靠的开源实现,降低了项目锁定的风险。

归根结底,工程领域选择 Raft,不是因为 Paxos 不正确,而是因为 Raft 更易于正确实现。一致性算法的“可用性”往往取决于开发的正确率,Raft 大幅降低了把理论变成可靠代码的门槛。


🧩 3. 如何用 Etcd 实现 Agent 服务的选主和元数据管理?

回到我们的 Agent 平台,很多场景下我们需要多个 Agent 实例高可用,但只有一个实例能执行某些定时任务(如清理过期会话)或作为对外公布的主节点。Etcd 内嵌 Raft,提供了原生的选主和分布式锁能力,我们可以很方便地用它。

3.1 基于 Etcd 的选主

利用 Etcd 的租约(Lease)和事务机制实现选主:

// 伪代码:创建会话
long leaseId = etcdClient.grant(timeoutSeconds).get().getID();
// 尝试在指定 key 上创建自己为主
Cmp cmp = new Cmp("agent/leader", Cmp.Op.EQUAL, CmpTarget.version(0));
PutOption option = PutOption.newBuilder().withLeaseId(leaseId).build();
Txn txn = etcdClient.txn().If(cmp).Then(Op.put("agent/leader", instanceId, option));
txn.commit().thenAccept(response -> {
    if (response.isSucceeded()) {
        becomeLeader(); // 成为主,启动任务
    } else {
        watchLeader(); // 监视 key 变化,准备接替
    }
});

当一个实例成为 Leader 后,它会定时续约。如果实例宕机,租约过期,Etcd 会自动删除 agent/leader 键,其他监听该键的实例会立刻触发竞选,实现故障转移。

3.2 元数据管理(如模型服务列表、灰度配置)

Agent 平台需要维护可用的模型服务实例列表,或者一些动态灰度规则。这些数据完全可以用 Etcd 的 Watch 机制做一个轻量级配置中心:

  • 所有模型服务启动时,将自己的地址和能力元数据写入 /models/<model-name>/<instance-id>,并绑定租约。

  • Agent 编排服务通过 get 拉取全量,然后 watch 增量变化,实时更新本地缓存。

  • 灰度规则同样可以存为 /config/gray-rules,修改后 Watcher 通知所有 Agent 实例热生效,无需重启。

与 ZooKeeper 相比,Etcd 的优势在于 更简洁的 HTTP/gRPC API、更快的 Raft 实现、以及对 TLS 和 RBAC 的天然支持,更适合作为云原生基础设施的“坚实底座”。

分布式共识不是纸上谈兵,当一个 Agent 集群用 Etcd 扛起选主和配置同步时,你就真正把 Raft 的安全性承诺转化成了服务的可靠保障。理解算法是为了信任它,落地应用则是为了驾驭它。


分布式 ID 生成方案

雪花算法与号段模式

🎯 1. 分布式 ID 有哪些生成方案?各自适用什么场景?

分布式 ID 没有银弹,选型要看对 唯一性、趋势递增、高性能、高可用 这四点的权衡。我把主流方案分成了四类,每一类都有自己的“舒适区”。

查看内嵌表格

选型思路:

如果对有序性要求不高,UUID 是最简单的;若要高性能且 趋势递增,雪花算法是头号种子;若想彻底避免时钟问题又不差那一点延迟,号段模式(Leaf)更省心。


⏰ 2. 雪花算法(Snowflake)的时钟回拨问题怎么解决?

雪花算法的命门在于时钟,一旦服务器时间回拨(NTP 调整、虚拟机迁移),就可能产生重复 ID。解决手段从简单粗暴到精巧设计,一共有四种主流打法。

2.1 直接抛异常(硬拒绝)

最直白的方案:发现时钟回拨时立刻停止服务,报错退出或拒绝生成 ID。

  • 优点:逻辑干净,不可能产生重复 ID。

  • 缺点:可用性大幅下降。生产环境一个 NTP 回调就可能让服务雪崩。

  • 适用:对可用性要求不极致,但有外部监控能快速恢复的场景。

2.2 等待时钟追上(阻塞等待)

如果回拨时长较小(比如 1-2 秒),可以让线程 sleep 一段时间,等时钟追回到上次最大时间再继续生成。

  • 优点:简单,对业务无侵入。

  • 缺点:在等待期间 ID 生成暂停,接口变慢甚至超时。

  • 适用:偶尔发生的小幅度回拨。

2.3 备用机器 ID(迂回法)

预先分配一个额外的 workerId,检测到时钟回拨时,切换到备用 workerId 继续生成。因为不同 workerId 生成的 ID 不会冲突(哪怕时间回退,机器位不同保证唯一)。

  • 优点:避免了服务中断,高可用。

  • 缺点:需要提前规划机器位数量,备用 ID 用尽后有风险。

  • 适用:机器资源相对宽裕,回拨偶尔发生的场景。

2.4 使用历史最大时间(百度 UidGenerator 的“借用未来时间”)

维护一个本地变量 lastTimestamp,记录上一次生成 ID 时的最大时间戳。如果当前时间 now < lastTimestamp,说明发生了回拨。算法不抛异常,而是 继续使用 lastTimestamp 作为“逻辑时钟” 来生成序列号,直到真实时间追平再切换。

  • 优点:服务零中断,无重复 ID。

  • 缺点:生成的 ID 中时间戳会比实际时间偏大,可能会破环趋势递增的“因果顺序”,但 ID 依然是全局唯一且趋势递增的。

  • 适用:对趋势递增要求不苛刻(允许略微超前),但对可用性要求极高的业务。

2.5 美团 Leaf 方案:混合模式

美团 Leaf 既提供了 Leaf-segment(号段模式),也提供了 Leaf-snowflake。Leaf-snowflake 通过 ZooKeeper 持久顺序节点 来注册 workerId,并在发现时钟回拨时 主动上报报警,同时拒绝生成,等待人工或自动策略介入。它结合了高可用架构与安全兜底。

我的实践建议:一般中大型系统会采用 备用 workerId + 逻辑时钟兜底 的组合策略。比如默认用历史最大时间,同时监控回拨次数,超过阈值就切换 workerId 并告警,这样在安全性和可用性之间取得平衡。


🤖 3. Agent 任务 ID 需要全局唯一且趋势递增,怎么设计?

Agent 任务的特点是高并发(大量对话任务同时创建)、需要按时间排序(趋势递增便于分页和追踪),并且可能跨租户、跨会话。我们可以设计一个 带业务语义的复合 ID,兼顾可读性和性能。

3.1 方案一:改良雪花算法 + 业务标记

基础 ID 用雪花算法保证高性能和唯一性,再在外部包装一层业务可读性。

  • 格式:task-{timestamp}-{workerId}-{sequence}T{yyyyMMddHHmmss}{machine}{seq}

  • 实现:

  • 使用 Redis 或 ZooKeeper 自动分配 workerId(避免手动配)。
  • 内部集成“逻辑时钟”应对回拨。
  • 最终 ID 如 T202606071430152B09F,其中 2B 是机器位,09F 是序列号。可读性略差,但胜在高性能。

3.2 方案二:号段模式 + 时间前缀(推荐用于任务中心)

任务场景下,我们往往希望 ID 能一眼看出创建时间,并且严格递增,方便追踪。号段模式正合适:

  • ID 结构:{日期前缀}{业务码}{自增序号}

  • 例子:20260607-AI-00001234

  • 实现:

  • 使用 Leaf-segment 思想,从数据库批量获取一段号段(如1000个),本地内存分配。
  • 日期前缀每天变化,可以定期重置号段起始点,避免位数无限增长。
  • 需要多活场景时,可以给不同实例分配不同的 业务码 段,如 AIB1 等,既保持唯一又区分源头。

3.3 架构设计要点

  • 高可用:号段服务本身需要集群化,使用 Nacos/Eureka 注册,客户端负载均衡。当号段服务全挂时,本地缓存号段仍可支撑一段时间。

  • 性能:号段模式单机可达千万 QPS,远超任务生成需求。

  • 趋势递增:自然按照 日期+自增 排序,数据库索引友好,前端分页查询按 ID 降序即可获取最新任务。

  • 扩展性:未来如果需要按租户隔离,可以在 ID 中加入租户标识,如 TNT123_20260607_0012

针对 Agent 任务这个具体场景,我更倾向于 号段模式 + 日期前缀。既满足了全局唯一和趋势递增的技术硬指标,又让日志和监控中的 ID 自带时间戳和业务信息,排查问题事半功倍。

技术选型永远是和业务形态强相关的——当你把 ID 设计从纯粹的“机器码”提升到“业务标识”时,你会发现它不仅是个主键,更是分布式追踪的第一块拼图。


一致性 Hash 与数据分片

分布式缓存与数据路由

1、基础题:一致性 Hash 算法解决什么问题?虚拟节点有什么作用?

难度级别:⭐⭐(节点增减时的数据迁移、环结构、虚拟节点解决数据倾斜)


2、进阶题:Redis Cluster 的数据分片方案和一致性 Hash 有什么区别?

难度级别:⭐⭐⭐(哈希槽、Gossip 协议、重哈希机制、数据迁移成本对比)

1️⃣ Common Answer

一致性 Hash 是把节点和数据都映射到环上,Redis Cluster 是用 16384 个哈希槽,节点增减时迁移槽,都差不多是为了解决数据分布问题。

2️⃣ Impressive Answer

两者的核心差异在于"数据迁移的最小单位"和"节点发现机制":

  1. 数据分布机制对比| 维度 | 一致性 Hash | Redis Cluster || --- | --- | --- || 分布单位 | 键→环上位置,节点管理一段弧 | 键→16384 个槽,节点管理一组槽 || 节点增减 | 影响相邻节点的数据,迁移量不确定 | 只影响迁移的槽,范围精确可控 || 数据倾斜 | 需要虚拟节点均衡 | 槽数量固定,天然均匀 |

  2. 为什么 Redis Cluster 选哈希槽而非一致性 Hash

  3. 槽是离散单位:迁移时可以精确控制一次迁移几个槽,便于限流和回滚
  4. 扩容可预测:16384 槽,新节点分几个槽、从哪个节点迁移,都能提前计算
  5. 去中心化:用 Gossip 协议同步槽映射,不需要一致性 Hash 的元数据服务

  6. Agent 系统的应用场景

  7. Agent 记忆分片:用一致性 Hash 将用户记忆分布到多个向量库实例,用户维度聚合
  8. 任务路由:用哈希槽思想将任务类型映射到不同处理节点(如对话类/分析类/工具调用类)

3️⃣ Key Differences

查看内嵌表格


分布式事务最终一致性

消息队列与事务消息

1、基础题:最终一致性的核心实现思路是什么?

难度级别:⭐⭐(本地消息表、事务消息、最大努力通知、补偿机制)


2、进阶题:RocketMQ 事务消息的原理是什么?如何保证不丢失?

难度级别:⭐⭐⭐⭐(半消息机制、事务状态回查、本地事务回滚、消息可靠性保证)

1️⃣ Common Answer

事务消息就是先发个半消息,然后执行本地事务,成功了就提交,失败了就回滚。RocketMQ 会定期回查事务状态。

2️⃣ Impressive Answer

RocketMQ 事务消息的核心是"两阶段提交 + 事务状态回查",我从流程、可靠性、边界情况三个角度分析:

  1. 两阶段提交流程\``阶段一:发送半消息 → MQ 持久化但消费者不可见阶段二:执行本地事务 → 根据结果提交/回滚半消息异常情况:MQ 定时回查事务状态(被动变主动)``
  2. 半消息用独立 Topic(RMQSYSTRANSHALFTOPIC)存储,对消费者透明
  3. 提交/回滚是幂等的,网络抖动可重试

  4. 不丢失的三重保证

  5. 半消息持久化:半消息和普通消息一样走同步刷盘 + 主从同步
  6. 事务状态回查:超过阈值(默认 60s)未收到提交/回滚,主动回查事务表
  7. 本地事务表:业务表 + 事务消息表放在同一本地事务,用定时任务兜底补偿

  8. 边界情况处理

  9. 本地事务执行前宕机:MQ 回查发现事务不存在,主动回滚半消息
  10. 提交消息丢失:MQ 回查发现事务已成功,补发提交指令
  11. 重复消费:事务消息的提交操作必须幂等(用唯一业务 ID 去重)

  12. 与 Agent 任务的结合

  13. Agent 任务状态变更 + 记忆写入:用事务消息保证状态变更后一定触发记忆异步写入
  14. 多 Agent 协作结果汇聚:子任务完成消息用事务消息发送,主任务订阅后聚合

3️⃣ Key Differences

查看内嵌表格


3、场景题:Agent 任务执行成功后要通知多个下游系统,如何保证不丢失通知?

难度级别:⭐⭐⭐⭐(消息队列可靠性投递、本地消息表、事务消息、最大努力通知)

1️⃣ Common Answer

用消息队列发通知就行了,失败了就重试,或者加个死信队列存失败的消息。

2️⃣ Impressive Answer

多下游通知的核心是"可靠投递 + 幂等消费",我设计三层保障:

  1. 第一层:事务消息保证必发
  2. 任务状态变更(DB)+ 发送通知消息(MQ)放在同一事务
  3. 用 RocketMQ 事务消息或本地消息表 + 定时任务兜底
  4. 消息结构带任务唯一 ID,下游按 ID 去重

  5. 第二层:消息可靠性投递

  6. MQ 层:同步刷盘 + 主从同步,消费者手动 ACK
  7. 重试策略:指数退避(1s, 5s, 30s, 5min, 30min),避免瞬间压力
  8. 死信队列:重试 16 次仍失败则转 DLQ,人工介入

  9. 第三层:下游幂等处理

  10. 下游系统用 Redis 记录已处理的任务 ID(setnx,过期时间 7 天)
  11. 数据库加唯一索引(如 task_id),重复插入自动失败
  12. 关键通知(如计费)加对账任务,定时扫描遗漏

Agent 场景扩展:多 Agent 协作中,子任务完成通知主任务、工具调用结果回调等场景都适用这套方案

3️⃣ Key Differences

查看内嵌表格


服务熔断与降级

Sentinel 高级用法与自适应保护

1、基础题:熔断和降级的区别是什么?

难度级别:⭐⭐(熔断是自动保护机制、降级是主动业务策略、触发条件不同)


2、进阶题:Sentinel 的自适应限流是怎么实现的?和固定阈值有什么区别?

难度级别:⭐⭐⭐⭐(系统负载感知、CPU 使用率、响应时间动态调整、保护系统不宕机)

1️⃣ Common Answer

自适应限流就是根据系统负载自动调整限流阈值,负载高就降低阈值,负载低就提高,比固定阈值更智能。

2️⃣ Impressive Answer

自适应限流的核心是"系统保护优先于业务吞吐",我从算法原理和工程实践两个维度分析:

  1. 自适应限流的判断指标
  2. CPU 使用率:当前样本 CPU > 阈值(默认 70%)且 当前流入 QPS > 最小 QPS
  3. 系统 Load:系统 Load1 > CPU 核心数(说明系统已过载)
  4. 响应时间 RT:平均 RT 飙升说明系统处理能力下降

  5. 核心算法:PID 控制思想\``动态阈值 = 基础阈值 × (1 - 系统压力系数)压力系数 = f(CPU 使用率,Load, RT)``

  6. 不是简单的线性调整,而是根据指标变化率预测趋势
  7. 避免阈值频繁抖动(加平滑窗口)

  8. 与固定阈值的本质区别| 维度 | 固定阈值 | 自适应限流 || --- | --- | --- || 配置成本 | 需要压测确定阈值,环境变化要重新调 | 自动感知系统容量,零配置 || 保护效果 | 阈值设高易宕机,设低浪费资源 | 始终在系统临界点附近运行 || 适用场景 | 资源隔离好的容器化部署 | 物理机/虚拟机等共享资源场景 |

  9. Agent 系统的应用

  10. LLM API 调用:用固定阈值(受限于供应商 RPM)
  11. Agent 内部资源(如向量检索):用自适应限流,根据 CPU/RT 动态调整
  12. 混合部署场景:自适应限流 + 固定阈值双保险

3️⃣ Key Differences

查看内嵌表格


分布式锁

Redis 分布式锁与 ZooKeeper 分布式锁

1、基础题:Redis 分布式锁的实现方式?SETNX + 过期时间有什么坑?

难度级别:⭐⭐(SETNX 原子性、过期时间设置、锁续期、解锁误删)


2、进阶题:Redisson 看门狗机制是怎么解决锁续期问题的?和 ZooKeeper 临时节点锁有什么区别?

难度级别:⭐⭐⭐(看门狗线程、锁续期策略、临时节点特性、性能对比、可靠性保证)

1️⃣ Common Answer

Redisson 的看门狗就是开个线程定时检查锁,如果快过期了就续期。ZooKeeper 的临时节点是客户端断开连接就自动删除锁,两种都能保证锁释放。

2️⃣ Impressive Answer

我从续期机制、可靠性保证、性能三个维度对比:

  1. Redisson 看门狗机制详解
  2. 续期触发:加锁成功后启动看门狗线程,默认每 10 秒(lockWatchdogTimeout/3)检查一次
  3. 续期条件:锁未被主动释放且持有者线程仍存活
  4. 续期策略:每次续期延长 lockWatchdogTimeout(默认 30 秒),避免业务未完成锁过期
  5. 线程终止:业务线程执行完毕或异常终止,看门狗线程自动停止,不再续期

  6. ZooKeeper 临时节点锁的特性

  7. 自动释放:客户端 Session 超时(默认 30 秒)或主动断开,临时节点立即删除
  8. Watch 通知:其他客户端监听节点删除事件,实时感知锁释放
  9. 顺序性:用临时顺序节点,按序号获取锁,避免羊群效应

  10. 核心差异对比| 维度 | Redisson 看门狗 | ZooKeeper 临时节点 || --- | --- | --- || 锁释放触发 | 主动续期 + 异常检测 | Session 超时自动删除 || 可靠性 | Redis 宕机可能锁残留 | ZK 宕机后 Leader 选举不影响锁 || 性能 | 单机 Redis 10 万+ QPS | ZK 写性能约 1 万 QPS || 适用场景 | 高并发、对性能要求高 | 对可靠性要求极高、并发适中 |

  11. Agent 系统选型建议

  12. 工具调用资源锁:用 Redisson,并发高(如多个 Agent 同时调用同一限流工具)
  13. 全局任务锁:用 ZooKeeper,任务执行时间长(如 Agent 工作流编排),需要强可靠性

3️⃣ Key Differences

查看内嵌表格


3、场景题:Agent 并发调用同一工具时如何防止资源冲突?分布式锁怎么选型?

难度级别:⭐⭐⭐(工具资源冲突、锁粒度设计、锁超时设置、死锁预防、性能优化)

1️⃣ Common Answer

用分布式锁锁住工具调用,加个超时时间,避免死锁。可以用 Redis 锁,性能好。

2️⃣ Impressive Answer

Agent 并发调用工具的锁设计要考虑"锁粒度 + 超时策略 + 性能",我分三层设计:

  1. 锁粒度设计
  2. 粗粒度(不推荐)lock:tool:{toolId},所有调用同一工具的 Agent 都互斥,并发度低
  3. 细粒度(推荐)lock:tool:{toolId}:{resourceKey},按资源维度加锁
    • 例如:调用限流工具按用户 ID 加锁,调用数据库工具按表名加锁
  4. 混合粒度:核心资源用细粒度,非核心资源用粗粒度

  5. 锁超时策略

  6. 工具执行时间预估:根据工具历史耗时 P99 设置超时(如 P99 = 3s,锁超时 = 5s)
  7. 看门狗续期:执行时间不确定的工具用 Redisson 看门狗自动续期
  8. 超时回滚:锁超时后触发工具调用回滚,避免部分执行导致资源不一致

  9. 性能优化

  10. 锁降级:高并发场景下,获取锁失败时排队或快速失败(如返回"工具繁忙,稍后重试")
  11. 分段锁:对高频工具用一致性 Hash 分段锁(如 lock:tool:{toolId}:{hash(resourceKey) % 16}
  12. 本地缓存:无状态工具(如计算类)用本地锁(ReentrantLock),减少分布式锁压力

3️⃣ Key Differences

查看内嵌表格


分布式调度

任务调度框架与幂等设计

1、基础题:XXL-Job 和 ElasticJob 的核心区别?

难度级别:⭐⭐(调度中心架构、分片策略、弹性扩容、依赖关系、运维复杂度)


2、进阶题:分布式调度如何保证任务不重复执行?失败重试和幂等怎么设计?

难度级别:⭐⭐⭐(任务唯一性、分布式锁、幂等表设计、重试策略、补偿机制)

1️⃣ Common Answer

用分布式锁保证任务不重复,失败了就重试几次。幂等就是任务执行多次结果一样,可以加个状态字段。

2️⃣ Impressive Answer

我从任务唯一性、幂等设计、失败重试三个层面分析:

  1. 保证任务不重复执行的三重机制
  2. 调度层去重:XXL-Job 的调度中心用数据库锁(INSERT IGNORE 或唯一索引),同一任务同一时间只调度一次
  3. 执行层加锁:任务执行时用 Redis 分布式锁,lock:job:{jobId}:{scheduleTime},避免重复消费
  4. 状态机去重:任务表用状态字段(WAITING/RUNNING/SUCCESS/FAILED),状态流转用乐观锁(CAS 更新)

  5. 幂等设计的三种方案| 方案 | 实现方式 | 适用场景 || --- | --- | --- || 幂等表 | 独立幂等表,记录已执行的业务唯一 ID | 需要强幂等的业务(如计费) || 业务表状态 | 业务表加 executed 标记 + 唯一索引 | 业务表可修改的场景 || Token 机制 | 任务执行前生成 Token,执行后消费 Token | 无法修改业务表的外部调用 |

  6. 失败重试策略

  7. 重试次数:默认 3 次,超过转人工介入
  8. 重试间隔:指数退避(1s, 5s, 30s),避免瞬间压力
  9. 重试范围:只重试可恢复异常(网络超时、临时锁冲突),不可恢复异常(参数错误)直接失败
  10. 补偿任务:定时扫描失败任务,人工确认后触发补偿执行

  11. Agent 定时任务的特殊处理

  12. 记忆清理任务:按用户维度分片,用分布式锁保证同一用户不被重复清理
  13. 工作流触发任务:用幂等表记录已触发的工作流 ID,避免重复触发

3️⃣ Key Differences

查看内嵌表格


3、场景题:Agent 定时任务(如定期清理记忆、定时触发工作流)的调度方案怎么设计?

难度级别:⭐⭐⭐(任务分片、资源隔离、失败处理、监控告警、弹性扩容)

1️⃣ Common Answer

用 XXL-Job 或 ElasticJob 调度就行了,配置好 Cron 表达式,任务失败了就重试。

2️⃣ Impressive Answer

Agent 定时任务要考虑"分片执行 + 资源隔离 + 可观测性",我设计完整方案:

  1. 任务分片策略
  2. 记忆清理任务:按用户 ID 哈希分片(如 16 个分片),每个执行节点处理一部分用户
  3. 工作流触发任务:按工作流类型分片,避免同一类型任务堆积在单一节点
  4. 动态分片:ElasticJob 支持运行时动态调整分片数,根据任务量自动扩缩容

  5. 资源隔离设计

  6. 线程池隔离:不同类型任务用独立线程池(如 memory-cleanup-poolworkflow-trigger-pool),避免互相影响
  7. 限流保护:对高频任务(如每分钟触发)用 Sentinel 限流,保护下游系统(如向量库)
  8. 优雅停机:任务执行中收到停机信号,等待当前批次完成后再退出

  9. 失败处理与监控

  10. 失败分类
    • 临时失败(如网络超时):自动重试 3 次
    • 业务失败(如用户不存在):记录日志,跳过
    • 系统失败(如数据库宕机):告警 + 人工介入
  11. 监控指标:任务执行耗时、成功率、失败原因分布、下游系统响应时间
  12. 告警策略:连续 3 次失败触发告警,失败率超过 10% 触发升级告警

  13. 弹性扩容

  14. 水平扩容:任务堆积时增加执行节点,ElasticJob 自动重新分片
  15. 垂直扩容:单节点任务量大时增加线程池线程数(注意不要超过 CPU 核心数 × 2)

3️⃣ Key Differences

查看内嵌表格


分布式链路追踪

可观测性与全链路监控

1、基础题:Trace、Span、TraceId 的关系是什么?OpenTelemetry 解决什么问题?

难度级别:⭐⭐(分布式追踪概念、Span 层级关系、TraceId 全局唯一、OpenTelemetry 标准化)


2、进阶题:Agent 系统的全链路追踪怎么做?从用户请求到 LLM 调用到工具执行的 Trace 怎么串联?

难度级别:⭐⭐⭐⭐(Trace 传递、跨进程追踪、异步任务追踪、LLM 调用埋点、工具执行追踪)

1️⃣ Common Answer

用 OpenTelemetry 生成 TraceId,在各个服务间传递,记录每个操作的耗时,就能看到整个链路。

2️⃣ Impressive Answer

Agent 系统的链路追踪要解决"跨进程传递 + 异步追踪 + 多层嵌套",我设计完整方案:

  1. Trace 传递机制
  2. HTTP 传递:用 traceparent Header(OpenTelemetry 标准),格式:00-{traceId}-{spanId}-{flags}
  3. RPC 传递:Dubbo/HSF 用 Attachment 传递 TraceId 和 SpanId
  4. 异步线程传递:用 TransmittableThreadLocal(TTL)或 MDC,确保子线程继承父线程的 Trace 上下文

  5. Agent 系统的 Span 层级设计\``Trace: 用户请求├── Span: Agent 接收请求│ ├── Span: Prompt 构建│ ├── Span: LLM 调用(HTTP)│ │ ├── Span: 模型推理│ │ └── Span: Token 流式输出│ └── Span: 工具执行│ ├── Span: 工具调用(RPC)│ └── Span: 工具结果解析└── Span: 响应返回``

  6. 关键埋点设计

  7. LLM 调用埋点
    • Attributes:model.nameprompt.tokenscompletion.tokenslatency.ms
    • Events:prompt.startprompt.endcompletion.startcompletion.end
  8. 工具执行埋点
    • Attributes:tool.nametool.parametersexecution.status
    • Events:tool.invoke.starttool.invoke.endtool.result
  9. Agent 思考链埋点

    • 用 Span 的 thought Attribute 记录 Agent 的决策过程
    • 用 Span Link 关联相关的记忆检索 Span
  10. 异步任务追踪

  11. 消息队列任务:消息体中携带 TraceId,消费者从消息中提取并创建 Child Span
  12. 定时任务:用任务 ID 作为 TraceId,关联相关的操作 Span
  13. 流式响应:用 Span Link 关联多个流式块,避免 Span 过长

3️⃣ Key Differences

查看内嵌表格


3、场景题:多 Agent 协作场景下,如何追踪一个任务在不同 Agent 间的流转路径和耗时?

难度级别:⭐⭐⭐(跨 Agent Trace 传递、任务状态追踪、协作链路可视化、性能瓶颈分析)

1️⃣ Common Answer

每个 Agent 都记录自己的 Trace,用一个任务 ID 关联起来,就能看到任务在不同 Agent 间的流转。

2️⃣ Impressive Answer

多 Agent 协作的追踪要解决"跨 Agent Trace 串联 + 任务状态可视化 + 性能分析",我设计三层方案:

  1. 跨 Agent Trace 串联
  2. 任务级 Trace:用 task:{taskId} 作为 TraceId,贯穿整个协作流程
  3. Agent 级 Span:每个 Agent 的执行作为一个 Span,记录 Agent 类型、输入、输出
  4. 通信埋点

    • Agent A → Agent B:用 RPC 调用,传递 TraceId,创建 Child Span
    • Agent A → 消息队列 → Agent B:消息体携带 TraceId,消费者创建新 Span 并用 Span Link 关联
  5. 任务状态追踪

  6. 状态机埋点:任务表记录状态(PENDING/ASSIGNED/IN_PROGRESS/COMPLETED/FAILED),每次状态变更创建 Event Span
  7. Agent 分配埋点:记录任务分配给哪个 Agent(agent.idagent.type),追踪任务流转路径
  8. 超时告警:Agent 执行超时(如超过 5 分钟)触发告警,记录超时 Span

  9. 协作链路可视化

  10. 时序图:按时间轴展示各 Agent 的执行顺序和耗时
  11. 拓扑图:展示 Agent 之间的调用关系(如 Orchestrator → Planner → Executor)
  12. 关键指标

    • 端到端耗时:从任务创建到完成的总时间
    • Agent 耗时分布:各 Agent 的平均耗时、P99 耗时
    • 瓶颈识别:耗时最长的 Agent 或步骤
  13. 性能瓶颈分析

  14. LLM 调用瓶颈:LLM 调用 Span 耗时占比高 → 优化 Prompt、切换更快模型
  15. 工具执行瓶颈:工具调用 Span 耗时高 → 优化工具实现、增加缓存
  16. Agent 协作瓶颈:Agent 间通信 Span 耗时高 → 优化消息传递、减少不必要的协作

3️⃣ Key Differences

查看内嵌表格


# 7.9 分布式锁

Redis 分布式锁与 ZooKeeper 分布式锁

1、基础题:Redis 分布式锁的实现方式?SETNX + 过期时间有什么坑?

难度级别:⭐⭐(SETNX 原子性、过期时间设置、锁续期、解锁误删)


2、进阶题:Redisson 看门狗机制是怎么解决锁续期问题的?和 ZooKeeper 临时节点锁有什么区别?

难度级别:⭐⭐⭐(看门狗线程、锁续期策略、临时节点特性、性能对比、可靠性保证)

1️⃣ Common Answer

Redisson 的看门狗就是开个线程定时检查锁,如果快过期了就续期。ZooKeeper 的临时节点是客户端断开连接就自动删除锁,两种都能保证锁释放。

2️⃣ Impressive Answer

我从续期机制、可靠性保证、性能三个维度对比:

  1. Redisson 看门狗机制详解
  2. 续期触发:加锁成功后启动看门狗线程,默认每 10 秒(lockWatchdogTimeout/3)检查一次
  3. 续期条件:锁未被主动释放且持有者线程仍存活
  4. 续期策略:每次续期延长 lockWatchdogTimeout(默认 30 秒),避免业务未完成锁过期
  5. 线程终止:业务线程执行完毕或异常终止,看门狗线程自动停止,不再续期

  6. ZooKeeper 临时节点锁的特性

  7. 自动释放:客户端 Session 超时(默认 30 秒)或主动断开,临时节点立即删除
  8. Watch 通知:其他客户端监听节点删除事件,实时感知锁释放
  9. 顺序性:用临时顺序节点,按序号获取锁,避免羊群效应

  10. 核心差异对比| 维度 | Redisson 看门狗 | ZooKeeper 临时节点 || --- | --- | --- || 锁释放触发 | 主动续期 + 异常检测 | Session 超时自动删除 || 可靠性 | Redis 宕机可能锁残留 | ZK 宕机后 Leader 选举不影响锁 || 性能 | 单机 Redis 10 万+ QPS | ZK 写性能约 1 万 QPS || 适用场景 | 高并发、对性能要求高 | 对可靠性要求极高、并发适中 |

  11. Agent 系统选型建议

  12. 工具调用资源锁:用 Redisson,并发高(如多个 Agent 同时调用同一限流工具)
  13. 全局任务锁:用 ZooKeeper,任务执行时间长(如 Agent 工作流编排),需要强可靠性

3️⃣ Key Differences

查看内嵌表格


3、场景题:Agent 并发调用同一工具时如何防止资源冲突?分布式锁怎么选型?

难度级别:⭐⭐⭐(工具资源冲突、锁粒度设计、锁超时设置、死锁预防、性能优化)

1️⃣ Common Answer

用分布式锁锁住工具调用,加个超时时间,避免死锁。可以用 Redis 锁,性能好。

2️⃣ Impressive Answer

Agent 并发调用工具的锁设计要考虑"锁粒度 + 超时策略 + 性能",我分三层设计:

  1. 锁粒度设计
  2. 粗粒度(不推荐)lock:tool:{toolId},所有调用同一工具的 Agent 都互斥,并发度低
  3. 细粒度(推荐)lock:tool:{toolId}:{resourceKey},按资源维度加锁
    • 例如:调用限流工具按用户 ID 加锁,调用数据库工具按表名加锁
  4. 混合粒度:核心资源用细粒度,非核心资源用粗粒度

  5. 锁超时策略

  6. 工具执行时间预估:根据工具历史耗时 P99 设置超时(如 P99 = 3s,锁超时 = 5s)
  7. 看门狗续期:执行时间不确定的工具用 Redisson 看门狗自动续期
  8. 超时回滚:锁超时后触发工具调用回滚,避免部分执行导致资源不一致

  9. 性能优化

  10. 锁降级:高并发场景下,获取锁失败时排队或快速失败(如返回"工具繁忙,稍后重试")
  11. 分段锁:对高频工具用一致性 Hash 分段锁(如 lock:tool:{toolId}:{hash(resourceKey) % 16}
  12. 本地缓存:无状态工具(如计算类)用本地锁(ReentrantLock),减少分布式锁压力

3️⃣ Key Differences

查看内嵌表格


分布式调度

任务调度框架与幂等设计

1、基础题:XXL-Job 和 ElasticJob 的核心区别?

难度级别:⭐⭐(调度中心架构、分片策略、弹性扩容、依赖关系、运维复杂度)


2、进阶题:分布式调度如何保证任务不重复执行?失败重试和幂等怎么设计?

难度级别:⭐⭐⭐(任务唯一性、分布式锁、幂等表设计、重试策略、补偿机制)

1️⃣ Common Answer

用分布式锁保证任务不重复,失败了就重试几次。幂等就是任务执行多次结果一样,可以加个状态字段。

2️⃣ Impressive Answer

我从任务唯一性、幂等设计、失败重试三个层面分析:

  1. 保证任务不重复执行的三重机制
  2. 调度层去重:XXL-Job 的调度中心用数据库锁(INSERT IGNORE 或唯一索引),同一任务同一时间只调度一次
  3. 执行层加锁:任务执行时用 Redis 分布式锁,lock:job:{jobId}:{scheduleTime},避免重复消费
  4. 状态机去重:任务表用状态字段(WAITING/RUNNING/SUCCESS/FAILED),状态流转用乐观锁(CAS 更新)

  5. 幂等设计的三种方案| 方案 | 实现方式 | 适用场景 || --- | --- | --- || 幂等表 | 独立幂等表,记录已执行的业务唯一 ID | 需要强幂等的业务(如计费) || 业务表状态 | 业务表加 executed 标记 + 唯一索引 | 业务表可修改的场景 || Token 机制 | 任务执行前生成 Token,执行后消费 Token | 无法修改业务表的外部调用 |

  6. 失败重试策略

  7. 重试次数:默认 3 次,超过转人工介入
  8. 重试间隔:指数退避(1s, 5s, 30s),避免瞬间压力
  9. 重试范围:只重试可恢复异常(网络超时、临时锁冲突),不可恢复异常(参数错误)直接失败
  10. 补偿任务:定时扫描失败任务,人工确认后触发补偿执行

  11. Agent 定时任务的特殊处理

  12. 记忆清理任务:按用户维度分片,用分布式锁保证同一用户不被重复清理
  13. 工作流触发任务:用幂等表记录已触发的工作流 ID,避免重复触发

3️⃣ Key Differences

查看内嵌表格


3、场景题:Agent 定时任务(如定期清理记忆、定时触发工作流)的调度方案怎么设计?

难度级别:⭐⭐⭐(任务分片、资源隔离、失败处理、监控告警、弹性扩容)

1️⃣ Common Answer

用 XXL-Job 或 ElasticJob 调度就行了,配置好 Cron 表达式,任务失败了就重试。

2️⃣ Impressive Answer

Agent 定时任务要考虑"分片执行 + 资源隔离 + 可观测性",我设计完整方案:

  1. 任务分片策略
  2. 记忆清理任务:按用户 ID 哈希分片(如 16 个分片),每个执行节点处理一部分用户
  3. 工作流触发任务:按工作流类型分片,避免同一类型任务堆积在单一节点
  4. 动态分片:ElasticJob 支持运行时动态调整分片数,根据任务量自动扩缩容

  5. 资源隔离设计

  6. 线程池隔离:不同类型任务用独立线程池(如 memory-cleanup-poolworkflow-trigger-pool),避免互相影响
  7. 限流保护:对高频任务(如每分钟触发)用 Sentinel 限流,保护下游系统(如向量库)
  8. 优雅停机:任务执行中收到停机信号,等待当前批次完成后再退出

  9. 失败处理与监控

  10. 失败分类
    • 临时失败(如网络超时):自动重试 3 次
    • 业务失败(如用户不存在):记录日志,跳过
    • 系统失败(如数据库宕机):告警 + 人工介入
  11. 监控指标:任务执行耗时、成功率、失败原因分布、下游系统响应时间
  12. 告警策略:连续 3 次失败触发告警,失败率超过 10% 触发升级告警

  13. 弹性扩容

  14. 水平扩容:任务堆积时增加执行节点,ElasticJob 自动重新分片
  15. 垂直扩容:单节点任务量大时增加线程池线程数(注意不要超过 CPU 核心数 × 2)

3️⃣ Key Differences

查看内嵌表格


分布式链路追踪

可观测性与全链路监控

1、基础题:Trace、Span、TraceId 的关系是什么?OpenTelemetry 解决什么问题?

难度级别:⭐⭐(分布式追踪概念、Span 层级关系、TraceId 全局唯一、OpenTelemetry 标准化)


2、进阶题:Agent 系统的全链路追踪怎么做?从用户请求到 LLM 调用到工具执行的 Trace 怎么串联?

难度级别:⭐⭐⭐⭐(Trace 传递、跨进程追踪、异步任务追踪、LLM 调用埋点、工具执行追踪)

1️⃣ Common Answer

用 OpenTelemetry 生成 TraceId,在各个服务间传递,记录每个操作的耗时,就能看到整个链路。

2️⃣ Impressive Answer

Agent 系统的链路追踪要解决"跨进程传递 + 异步追踪 + 多层嵌套",我设计完整方案:

  1. Trace 传递机制
  2. HTTP 传递:用 traceparent Header(OpenTelemetry 标准),格式:00-{traceId}-{spanId}-{flags}
  3. RPC 传递:Dubbo/HSF 用 Attachment 传递 TraceId 和 SpanId
  4. 异步线程传递:用 TransmittableThreadLocal(TTL)或 MDC,确保子线程继承父线程的 Trace 上下文

  5. Agent 系统的 Span 层级设计\``Trace: 用户请求├── Span: Agent 接收请求│ ├── Span: Prompt 构建│ ├── Span: LLM 调用(HTTP)│ │ ├── Span: 模型推理│ │ └── Span: Token 流式输出│ └── Span: 工具执行│ ├── Span: 工具调用(RPC)│ └── Span: 工具结果解析└── Span: 响应返回``

  6. 关键埋点设计

  7. LLM 调用埋点
    • Attributes:model.nameprompt.tokenscompletion.tokenslatency.ms
    • Events:prompt.startprompt.endcompletion.startcompletion.end
  8. 工具执行埋点
    • Attributes:tool.nametool.parametersexecution.status
    • Events:tool.invoke.starttool.invoke.endtool.result
  9. Agent 思考链埋点

    • 用 Span 的 thought Attribute 记录 Agent 的决策过程
    • 用 Span Link 关联相关的记忆检索 Span
  10. 异步任务追踪

  11. 消息队列任务:消息体中携带 TraceId,消费者从消息中提取并创建 Child Span
  12. 定时任务:用任务 ID 作为 TraceId,关联相关的操作 Span
  13. 流式响应:用 Span Link 关联多个流式块,避免 Span 过长

3️⃣ Key Differences

查看内嵌表格


3、场景题:多 Agent 协作场景下,如何追踪一个任务在不同 Agent 间的流转路径和耗时?

难度级别:⭐⭐⭐(跨 Agent Trace 传递、任务状态追踪、协作链路可视化、性能瓶颈分析)

1️⃣ Common Answer

每个 Agent 都记录自己的 Trace,用一个任务 ID 关联起来,就能看到任务在不同 Agent 间的流转。

2️⃣ Impressive Answer

多 Agent 协作的追踪要解决"跨 Agent Trace 串联 + 任务状态可视化 + 性能分析",我设计三层方案:

  1. 跨 Agent Trace 串联
  2. 任务级 Trace:用 task:{taskId} 作为 TraceId,贯穿整个协作流程
  3. Agent 级 Span:每个 Agent 的执行作为一个 Span,记录 Agent 类型、输入、输出
  4. 通信埋点

    • Agent A → Agent B:用 RPC 调用,传递 TraceId,创建 Child Span
    • Agent A → 消息队列 → Agent B:消息体携带 TraceId,消费者创建新 Span 并用 Span Link 关联
  5. 任务状态追踪

  6. 状态机埋点:任务表记录状态(PENDING/ASSIGNED/IN_PROGRESS/COMPLETED/FAILED),每次状态变更创建 Event Span
  7. Agent 分配埋点:记录任务分配给哪个 Agent(agent.idagent.type),追踪任务流转路径
  8. 超时告警:Agent 执行超时(如超过 5 分钟)触发告警,记录超时 Span

  9. 协作链路可视化

  10. 时序图:按时间轴展示各 Agent 的执行顺序和耗时
  11. 拓扑图:展示 Agent 之间的调用关系(如 Orchestrator → Planner → Executor)
  12. 关键指标

    • 端到端耗时:从任务创建到完成的总时间
    • Agent 耗时分布:各 Agent 的平均耗时、P99 耗时
    • 瓶颈识别:耗时最长的 Agent 或步骤
  13. 性能瓶颈分析

  14. LLM 调用瓶颈:LLM 调用 Span 耗时占比高 → 优化 Prompt、切换更快模型
  15. 工具执行瓶颈:工具调用 Span 耗时高 → 优化工具实现、增加缓存
  16. Agent 协作瓶颈:Agent 间通信 Span 耗时高 → 优化消息传递、减少不必要的协作

3️⃣ Key Differences

查看内嵌表格

---# 7.12 配置中心与服务发现

Nacos 核心原理

1、基础题(⭐⭐)

题目:Nacos 作为配置中心和注册中心的核心原理?长轮询机制是什么?

考察要点

  • Nacos 的配置中心工作原理(发布、订阅、推送)

  • Nacos 的服务注册发现机制

  • 长轮询的原理和优势

  • 配置变更通知机制


2、进阶题(⭐⭐⭐)

题目:Nacos 的 AP/CP 模式切换原理?临时实例和持久实例有什么区别?

1️⃣ Common Answer Nacos 支持 AP 和 CP 两种模式。AP 模式优先保证可用性,CP 模式优先保证一致性。临时实例使用 AP 模式,持久实例使用 CP 模式。临时实例会定期发送心跳,如果超时会被剔除;持久实例不会因为心跳超时被删除。选择哪种模式取决于业务需求,如果是普通服务选 AP,如果是关键服务选 CP。

2️⃣ Impressive Answer Nacos 的 AP/CP 模式切换基于 Raft 和 Distro 协议的架构设计:

AP 模式(Distro 协议)

  • 基于最终一致性模型,优先保证高可用性

  • 使用 Distro 协议进行数据同步,每个节点只负责部分数据

  • 临时实例(ephemeral=true)采用 AP 模式,通过心跳机制保持活性

  • 心跳间隔默认 5 秒,超时时间默认 15 秒,超时后自动剔除

  • 适合无状态服务、微服务注册场景,允许短暂的数据不一致

CP 模式(Raft 协议)

  • 基于强一致性模型,优先保证数据一致性

  • 使用 Raft 协议进行 Leader 选举和日志复制

  • 持久实例(ephemeral=false)采用 CP 模式,实例生命周期由客户端控制

  • 实例删除需要显式调用 API,不会因为心跳超时自动删除

  • 适合有状态服务、核心配置管理场景,要求数据强一致

模式切换机制

  • 客户端通过 ephemeral 参数控制实例类型

  • 服务端根据实例类型选择不同的存储和同步策略

  • 同一个命名空间下,临时实例和持久实例可以共存

工程实践

  • Agent 服务注册使用临时实例(AP),允许短暂的服务不可用

  • Agent 配置中心使用持久实例(CP),确保配置变更的强一致性

  • 核心链路服务使用 CP 模式,边缘服务使用 AP 模式

3️⃣ Key Differences

查看内嵌表格


3、场景题(⭐⭐⭐)

题目:Agent 服务的动态路由(如根据模型版本、负载动态切换 LLM Provider)怎么基于 Nacos 实现?

1️⃣ Common Answer 可以在 Nacos 中配置 LLM Provider 的路由规则。Agent 服务启动时从 Nacos 拉取配置,监听配置变更。当需要切换 Provider 时,修改 Nacos 配置,Agent 服务会收到通知并更新路由。可以使用 Nacos 的配置中心功能,将路由规则作为配置项存储。

2️⃣ Impressive Answer 基于 Nacos 实现 Agent 服务的动态路由,需要结合配置中心和服务发现的双重能力:

架构设计

Agent Gateway
Nacos Config Center (路由规则配置)
Load Balancer (基于 Nacos 权重配置)
LLM Provider Services (OpenAI/Claude/本地模型)

实现方案

  1. 路由规则配置(Nacos Config Center)

  2. 使用 Nacos 配置中心存储路由策略配置

  3. 配置结构示例:\``yamlrouting:rules:- model: "gpt-4"providers:- name: "openai"weight: 70maxQps: 100- name: "claude"weight: 30maxQps: 50- model: "claude-3"providers:- name: "claude"weight: 100maxQps: 200fallback:enabled: truethreshold: 0.95strategy: "round_robin"``

  4. 服务注册与权重管理(Nacos Service Discovery)

  5. 每个 LLM Provider 作为独立服务注册到 Nacos

  6. 使用 Nacos 的权重(weight)机制实现负载均衡

  7. 动态调整权重:根据实时负载、错误率、延迟等指标

  8. 健康检查:定期探测 Provider 服务状态,自动剔除故障节点

  9. 动态路由核心流程

  10. 配置监听:Agent 服务使用 Nacos SDK 监听配置变更(长轮询机制)

  11. 规则解析:配置变更后,解析新的路由规则,更新本地路由表

  12. 流量分发:根据路由规则和权重,选择目标 Provider

  13. 熔断降级:当 Provider 异常率超过阈值,自动切换到备用 Provider

  14. 高级特性实现

基于模型版本的路由

  • 在请求中携带模型版本信息(如 model-version: v1.2

  • Nacos 配置中定义版本路由映射

  • 支持灰度发布:v1.2 流量 10% 到新 Provider,90% 到旧 Provider

基于负载的动态切换

  • 实时监控 Provider 的 QPS、延迟、错误率

  • 使用 Nacos 的动态权重 API 调整流量分配

  • 示例:OpenAI 延迟 > 2s 时,自动将权重从 70% 降到 30%

多维度限流集成

  • Nacos 配置中定义限流规则(按用户、按模型、按 Token)

  • Agent Gateway 从 Nacos 拉取限流配置,实现分布式限流

  • 限流规则变更实时生效,无需重启服务

工程实践

  • 使用 Nacos 的命名空间(Namespace)隔离不同环境的配置(dev/test/prod)

  • 配置版本管理:支持配置回滚,快速恢复到历史版本

  • 配置加密:敏感信息(API Key)使用 Nacos 的加密存储

  • 监控告警:集成 Prometheus,监控路由规则变更和流量切换

3️⃣ Key Differences

查看内嵌表格


分布式限流

多维度限流与流量整形

1、基础题(⭐⭐)

题目:令牌桶和漏桶算法的区别?各自适合什么场景?

考察要点

  • 令牌桶算法的原理和特点

  • 漏桶算法的原理和特点

  • 两种算法的核心区别(处理突发流量能力)

  • 适用场景对比


2、进阶题(⭐⭐⭐)

题目:分布式限流(跨多实例)怎么实现?Redis + Lua 滑动窗口的原理?

1️⃣ Common Answer 分布式限流可以使用 Redis 来存储计数器。每个请求来的时候,检查 Redis 中的计数是否超过阈值,如果没超过就加 1。滑动窗口可以使用 Redis 的 ZSet,用时间戳作为 score,记录每个请求的时间。删除过期的记录,统计窗口内的请求数量。Lua 脚本可以保证原子性,避免并发问题。

2️⃣ Impressive Answer 分布式限流的核心挑战在于保证多实例间的原子性和一致性,Redis + Lua 滑动窗口是业界主流方案:

滑动窗口算法原理

  • 将时间划分为固定大小的窗口(如 1 秒)

  • 每个请求记录其时间戳,存储在有序集合(ZSet)中

  • 统计当前窗口内的请求数量,超过阈值则拒绝

Redis + Lua 实现方案

数据结构

Key: rate_limit:{api_key}:{user_id}
Type: ZSet
Score: 请求时间戳(毫秒)
Member: 请求 ID(UUID)

Lua 脚本核心逻辑

local key = KEYS[1]
local window = tonumber(ARGV[1])  -- 窗口大小(毫秒)
local limit = tonumber(ARGV[2])   -- 限流阈值
local now = tonumber(ARGV[3])     -- 当前时间戳
local request_id = ARGV[4]        -- 请求 ID

-- 1. 删除窗口外的过期数据
redis.call('ZREMRANGEBYSCORE', key, 0, now - window)

-- 2. 统计当前窗口内的请求数
local count = redis.call('ZCARD', key)

-- 3. 判断是否超过限流阈值
if count < limit then
    -- 未超限,记录当前请求
    redis.call('ZADD', key, now, request_id)
    -- 设置过期时间(窗口大小 + 1 秒)
    redis.call('EXPIRE', key, window / 1000 + 1)
    return {1, count + 1}  -- 允许通过,返回当前请求数
else
    return {0, count}  -- 拒绝通过
end

技术要点

  1. 原子性保证

  2. Lua 脚本在 Redis 中单线程执行,保证操作的原子性

  3. 避免了"检查-设置"竞态条件

  4. 多实例并发请求也能保证限流准确性

  5. 精确度优化

  6. 使用毫秒级时间戳,提高窗口精度

  7. 支持动态调整窗口大小和限流阈值

  8. 可扩展为多级限流(秒级 + 分钟级)

  9. 性能优化

  10. ZSet 的 ZREMRANGEBYSCORE 和 ZCARD 都是 O(log N) 复杂度

  11. 合理设置过期时间,避免内存泄漏

  12. 使用 Pipeline 批量执行多个限流检查

工程实践

多维度限流实现

限流维度组合:
- API 级别:/api/chat
- 用户级别:user_id
- 模型级别:model_name
- IP 级别:client_ip

Redis Key 设计:
rate_limit:{api}:{user}:{model}:{ip}

限流策略配置

rate_limits:
  - api: /api/chat
    dimensions: [user, model, ip]
    rules:
      - dimension: user
        limit: 100
        window: 60000  # 1 分钟
      - dimension: model
        limit: 1000
        window: 60000
      - dimension: ip
        limit: 50
        window: 60000
    strategy: "strict"  # strict/loose

降级策略

  • 限流触发时返回 429 状态码

  • 提供 Retry-After 响应头

  • 集成熔断器,连续限流触发降级

3️⃣ Key Differences

查看内嵌表格


3、场景题(⭐⭐⭐⭐)

题目:Agent 调用多个 LLM Provider(OpenAI/Claude/本地模型),如何设计多维度限流(按用户、按模型、按 Token 消耗)?

1️⃣ Common Answer 可以在 Agent Gateway 层面做限流。为每个用户设置 QPS 限制,为每个模型设置并发限制。Token 消耗可以通过记录每次请求的 Token 数,累加计算。使用 Redis 存储计数器,Lua 脚本保证原子性。超过限制就拒绝请求或排队等待。

2️⃣ Impressive Answer Agent 系统的多维度限流需要综合考虑 QPS、并发、Token 消耗、成本等多个维度,设计分层限流架构:

限流架构设计

用户请求 → Agent Gateway
第一层:用户级别限流(QPS + Token 消耗)
第二层:模型级别限流(并发 + QPS)
第三层:Provider 级别限流(配额 + 成本)
LLM Provider Services

多维度限流实现方案

  1. 按用户限流(User-Level Rate Limiting)

QPS 限流

user_rate_limit:
  default:
    qps: 10
    burst: 20
  vip:
    qps: 50
    burst: 100
  free:
    qps: 5
    burst: 10

Token 消耗限流

  • 使用滑动窗口统计用户的 Token 消耗

  • Redis Key 设计:user_token_limit:{user_id}:{period}

  • 支持按小时、天、月多级限流

  • Lua 脚本实现:```lualocal user_id = KEYS[1]local period = ARGV[1] -- hour/day/monthlocal token_limit = tonumber(ARGV[2])local tokens = tonumber(ARGV[3]) -- 本次请求的 Token 数 local key = "usertokenlimit:" .. user_id .. ":" .. periodlocal current = redis.call('GET', key) or 0

if tonumber(current) + tokens <= token_limit then

  redis.call('INCRBY', key, tokens)
  redis.call('EXPIRE', key, get_expire_seconds(period))
  return {1, tonumber(current) + tokens}

else

  return {0, tonumber(current)}

end ```

  1. 按模型限流(Model-Level Rate Limiting)

并发限流

  • 使用 Redis 的 Set 存储正在进行的请求

  • Redis Key 设计:model_concurrent:{model_name}

  • 请求开始时添加到 Set,结束时删除

  • Lua 脚本实现:```lualocal model = KEYS[1]local max_concurrent = tonumber(ARGV[1])local request_id = ARGV[2] local key = "model_concurrent:" .. modellocal count = redis.call('SCARD', key)

if count < max_concurrent then

  redis.call('SADD', key, request_id)
  redis.call('EXPIRE', key, 300)  -- 5 分钟超时
  return {1, count + 1}

else

  return {0, count}

end ```

QPS 限流

  • 每个模型独立限流,避免热门模型占用所有资源

  • 支持动态调整:根据模型负载自动调整限流阈值

  • 示例配置:\``yamlmodel*rate*limit:gpt-4:qps: 100concurrent: 50claude-3:qps: 200concurrent: 100local-model:qps: 500concurrent: 200``

  • 按 Provider 限流(Provider-Level Rate Limiting)

API 配额管理

  • 跟踪每个 Provider 的 API 调用配额

  • 支持按天、按月重置配额

  • 配额耗尽时自动切换到备用 Provider

  • Redis Key 设计:provider_quota:{provider_name}:{period}

成本控制限流

  • 根据模型定价计算请求成本

  • 设置用户/组织的成本预算

  • 超预算时降级到低成本模型

  • 示例:\``yamlcost_control:default:budget: 100 # 美元/月current: 45.6org_abc:budget: 1000current: 234.5``

  • 综合限流决策引擎

限流检查流程

public class RateLimitChecker {
    public RateLimitResult check(Request request) {
        // 1. 用户 QPS 限流
        RateLimitResult userQps = checkUserQps(request.getUserId());
        if (!userQps.isAllowed()) {
            return userQps;
        }

        // 2. 用户 Token 限流
        RateLimitResult userToken = checkUserToken(request.getUserId(),
            request.getEstimatedTokens());
        if (!userToken.isAllowed()) {
            return userToken;
        }

        // 3. 模型并发限流
        RateLimitResult modelConcurrent = checkModelConcurrent(
            request.getModel(), request.getRequestId());
        if (!modelConcurrent.isAllowed()) {
            return modelConcurrent;
        }

        // 4. 模型 QPS 限流
        RateLimitResult modelQps = checkModelQps(request.getModel());
        if (!modelQps.isAllowed()) {
            return modelQps;
        }

        // 5. Provider 配额限流
        RateLimitResult providerQuota = checkProviderQuota(
            request.getProvider(), request.getEstimatedCost());
        if (!providerQuota.isAllowed()) {
            return providerQuota;
        }

        return RateLimitResult.allowed();
    }
}

动态限流策略

  • 根据实时负载自动调整限流阈值

  • 使用 Nacos 配置中心动态更新限流规则

  • 支持 A/B 测试:不同用户组使用不同限流策略

工程实践

监控与告警

  • 实时监控各维度的限流触发率

  • Prometheus 指标:rate_limit_triggered_total{dimension="user|model|provider"}

  • 限流触发率超过阈值时发送告警

优雅降级

  • 限流触发时返回 429 状态码和 Retry-After

  • 提供排队机制:使用 Redis List 实现请求队列

  • 自动重试:指数退避策略,避免雪崩

配额管理界面

  • 为用户提供配额使用情况可视化

  • 支持用户自助升级配额

  • 成本预估和预算提醒

3️⃣ Key Differences

查看内嵌表格


CAP/BASE 理论与实践

分布式系统设计的理论基石

1、基础题(⭐⭐)

题目:CAP 定理的三个要素是什么?为什么不能同时满足?

考察要点

  • 一致性(Consistency)的定义

  • 可用性(Availability)的定义

  • 分区容错性(Partition Tolerance)的定义

  • CAP 定理的核心约束和取舍原则


2、进阶题(⭐⭐⭐)

题目:BASE 理论在实际系统中怎么落地?举例说明最终一致性的工程实现

1️⃣ Common Answer BASE 理论是 Basically Available、Soft state、Eventually consistent。它是对 CAP 的补充,强调最终一致性。在实际系统中,可以通过消息队列、异步处理、补偿机制来实现最终一致性。比如电商订单系统,下单后先返回成功,然后异步处理库存和支付,最终保证数据一致。

2️⃣ Impressive Answer BASE 理论是分布式系统设计的实践指南,通过柔性事务实现高可用和最终一致性:

BASE 理论核心要素

  1. Basically Available(基本可用)

  2. 系统在出现故障时,允许损失部分可用性

  3. 降级策略:功能降级、限流、熔断

  4. 示例:双十一期间,非核心服务降级,保证核心交易链路可用

  5. Soft State(软状态)

  6. 允许系统中的数据存在中间状态

  7. 不要求强一致性,允许数据在不同节点间存在延迟

  8. 示例:订单状态从"待支付"到"已支付"到"已完成"的流转

  9. Eventually Consistent(最终一致性)

  10. 系统保证在没有新更新的情况下,最终所有副本的数据一致

  11. 一致性窗口的时间取决于系统设计和网络延迟

  12. 示例:DNS 解析的最终一致性,通常在几分钟内全球同步

最终一致性的工程实现

  1. 基于 MQ 的异步消息(消息队列模式)
订单服务 → MQ → 库存服务
           → 支付服务
           → 物流服务
  • 实现要点
  • 使用可靠消息队列(RocketMQ、Kafka)
  • 消息发送失败重试机制
  • 消息消费幂等性保证
  • 死信队列处理异常消息

  • 代码示例:```java@Transactionalpublic void createOrder(Order order) {// 1. 保存订单orderMapper.insert(order);

  // 2. 发送消息到 MQ
  Message message = MessageBuilder
      .withPayload(order)
      .setHeader("orderId", order.getId())
      .build();
  rocketMQTemplate.send("order-created", message);

}

@RocketMQMessageListener(topic = "order-created") public class OrderConsumer implements RocketMQListener {

  @Override
  public void onMessage(Order order) {
      // 幂等性检查
      if (isProcessed(order.getId())) {
          return;
      }

      // 扣减库存
      inventoryService.deduct(order.getProductId(), order.getQuantity());

      // 标记已处理
      markProcessed(order.getId());
  }

} ```

  1. 补偿事务模式(Saga 模式)

  2. 将长事务拆分为多个本地事务

  3. 每个本地事务都有对应的补偿操作

  4. 失败时按相反顺序执行补偿

  5. 实现示例:```javapublic class OrderSaga {public void execute(Order order) {try {// 步骤1:创建订单orderService.create(order);

          // 步骤2:扣减库存
          inventoryService.deduct(order);

          // 步骤3:创建支付
          paymentService.create(order);

      } catch (Exception e) {
          // 补偿:回滚支付
          paymentService.cancel(order);

          // 补偿:恢复库存
          inventoryService.restore(order);

          // 补偿:取消订单
          orderService.cancel(order);

          throw e;
      }
  }

} ```

  1. TCC(Try-Confirm-Cancel)模式

  2. Try:资源预留和检查

  3. Confirm:确认执行业务操作

  4. Cancel:取消执行,释放资源

  5. 实现示例:```javapublic interface InventoryTCCService {// Try:冻结库存@Compensableboolean freezeInventory(String productId, int quantity);

  // Confirm:扣减库存
  boolean deductInventory(String productId, int quantity);

  // Cancel:释放冻结库存
  boolean releaseInventory(String productId, int quantity);

} ```

  1. 本地消息表模式(基于数据库的可靠消息)

  2. 将消息和业务操作在同一事务中写入本地消息表

  3. 定时任务扫描消息表,发送消息到 MQ

  4. 消息发送成功后更新消息状态

  5. 实现要点

  6. 消息表和业务表在同一数据库,使用本地事务保证原子性
  7. 定时任务轮询未发送的消息
  8. 消息发送失败重试,超过次数转入死信队列

  9. 读写分离 + 最终一致性

  10. 主库负责写操作,从库负责读操作

  11. 主从复制存在延迟,允许短暂的数据不一致

  12. 适用于读多写少的场景(如电商商品信息)

工程实践

一致性监控

  • 监控数据不一致的窗口时间

  • 告警机制:不一致时间超过阈值时告警

  • 数据对账:定期比对主从数据,修复不一致

一致性级别选择

  • 强一致性:金融交易、库存扣减(使用 CP 模式、分布式事务)

  • 最终一致性:订单状态更新、用户资料修改(使用 MQ、异步处理)

  • 弱一致性:缓存数据、推荐结果(允许过期)

3️⃣ Key Differences

查看内嵌表格


3、场景题(⭐⭐⭐)

题目:Agent 记忆系统(向量库 + 关系库 + 缓存)在 CAP 中如何取舍?不同记忆类型的一致性要求有什么不同?

1️⃣ Common Answer Agent 记忆系统需要高可用性,所以选择 AP 模式。向量库存储长期记忆,关系库存储结构化数据,缓存存储短期记忆。长期记忆可以接受最终一致性,短期记忆要求强一致性。不同记忆类型根据重要程度选择不同的 CAP 策略。

2️⃣ Impressive Answer Agent 记忆系统是一个多层次的存储架构,不同类型的记忆对一致性、可用性的要求不同,需要分层设计 CAP 策略:

Agent 记忆系统架构

┌─────────────────────────────────────────┐
│         Agent Memory System              │
├─────────────────────────────────────────┤
│  Layer 1: Working Memory (Redis Cache)  │  ← 会话级,强一致性
│  Layer 2: Semantic Memory (Vector DB)   │  ← 长期记忆,最终一致性
│  Layer 3: Episodic Memory (Relational DB)│  ← 事件记录,最终一致性
│  Layer 4: Procedural Memory (Knowledge Graph)│  ← 知识图谱,最终一致性
└─────────────────────────────────────────┘

分层 CAP 取舍策略

  1. Working Memory(工作记忆)- Redis Cache

  2. CAP 选择:AP + 最终一致性(可配置为 CP)

  3. 一致性要求:高(同一会话内的记忆必须一致)

  4. 可用性要求:极高(缓存不可用会影响对话流畅度)

  5. 实现方案

  6. 使用 Redis Cluster 模式,支持自动故障转移
  7. 配置 replicate-reads 控制读取策略:
    • master:从主节点读取(强一致性)
    • replica:从从节点读取(最终一致性)
  8. 会话记忆使用 master 模式,保证强一致性
  9. 全局共享记忆使用 replica 模式,允许最终一致性

  10. 工程实践\``java// 会话记忆:强一致性@Cacheable(value = "session_memory", key = "#sessionId",cacheManager = "strongConsistencyCacheManager")public Memory getSessionMemory(String sessionId) {// 从 Redis Master 读取} // 全局记忆:最终一致性@Cacheable(value = "global_memory", key = "#userId",cacheManager = "eventualConsistencyCacheManager")public Memory getGlobalMemory(String userId) {// 从 Redis Replica 读取}``

  11. Semantic Memory(语义记忆)- Vector DB(如 Milvus、Pinecone)

  12. CAP 选择:AP(优先保证高可用性)

  13. 一致性要求:低(向量检索允许短暂的不一致)

  14. 可用性要求:高(向量库不可用时降级到关键词搜索)

  15. 实现方案

  16. 使用分布式向量数据库,支持多副本
  17. 向量索引更新采用异步模式(Write-Behind)
  18. 新增记忆先写入缓冲区,异步构建向量索引
  19. 允许索引更新延迟(通常几秒到几分钟)

  20. 工程实践:```javapublic class SemanticMemoryService {public void addMemory(Memory memory) {// 1. 同步写入向量库(主节点)vectorDB.insert(memory);

      // 2. 异步更新索引(后台任务)
      asyncIndexBuilder.updateIndex(memory);
  }

  public List<Memory> search(String query) {
      // 从副本节点读取,允许索引更新延迟
      return vectorDB.search(query, ReadPreference.REPLICA);
  }

} ```

  • 降级策略
  • 向量库不可用时,降级到关键词搜索(ES)
  • 检索超时时,返回缓存的历史结果

  • Episodic Memory(情景记忆)- Relational DB(如 MySQL)

  • CAP 选择:CP(优先保证一致性)

  • 一致性要求:高(事件记录不能丢失或重复)

  • 可用性要求:中(事件记录失败不影响对话,但需要重试)

  • 实现方案

  • 使用主从复制 + 读写分离
  • 写操作路由到主节点,保证强一致性
  • 读操作路由到从节点,允许短暂延迟
  • 使用分布式事务(Seata)保证跨库一致性

  • 工程实践:```java@Transactionalpublic void recordEvent(Event event) {// 1. 写入主库eventMapper.insert(event);

  // 2. 发送 MQ 消息,异步更新向量库
  mqProducer.send("event-created", event);

}

// 读操作从从库读取 @Transactional(readOnly = true) public List getEvents(String sessionId) {

  return eventMapper.selectBySessionId(sessionId);

} ```

  1. Procedural Memory(程序记忆)- Knowledge Graph(如 Neo4j)

  2. CAP 选择:AP(优先保证高可用性)

  3. 一致性要求:中(知识图谱允许短暂的不一致)

  4. 可用性要求:高(知识图谱不可用时降级到规则引擎)

  5. 实现方案

  6. 使用因果集群(Causal Cluster),支持读写分离
  7. 写操作路由到 Leader 节点,保证一致性
  8. 读操作路由到 Follower 节点,允许最终一致性
  9. 使用异步复制,Leader 和 Follower 间存在延迟

不同记忆类型的一致性要求对比

查看内嵌表格

工程实践总结

  1. 一致性监控

  2. 监控各层存储的数据延迟(主从复制延迟、索引更新延迟)

  3. 告警机制:延迟超过阈值时告警

  4. 数据对账:定期比对各层数据,修复不一致

  5. 降级策略

  6. 向量库不可用 → 降级到 ES 关键词搜索

  7. 关系库不可用 → 降级到本地文件存储

  8. 缓存不可用 → 直接查询持久化存储

  9. 一致性级别动态调整

  10. 根据业务场景动态调整一致性级别

  11. 重要会话使用强一致性,普通会话使用最终一致性

  12. 使用 Nacos 配置中心动态配置

3️⃣ Key Differences

查看内嵌表格