压测相关面试 Q&A#
基于 2026-06-30 真实压测数据整理。配置: 4 uvicorn workers + 3 diagnosis workers,手动并发槽 16,Worker 并发槽 32。
每个问题的回答都遵循 STAR 结构 (Situation → Task → Action → Result),重点数据加粗。
一、压测方法论#
Q1: 你是如何进行压测的?用了什么工具?#
我没用 k6、wrk 这类通用工具,而是用 Python asyncio + httpx 自己写了一套压测脚本。原因有三:
- 和业务贴合: 压测需要构造 Alertmanager 标准 payload (带 fingerprint、severity、receiver 等字段) 和诊断提交 payload (带 query、severity、session_id),通用工具构造复杂 JSON 比较别扭。
- 多种流量模型: 我需要 flat (固定并发)、ramp (阶梯加压)、sustained (恒定 QPS)、burst (突发脉冲)、mixed (混合流量) 五种模式来模拟不同生产场景,自己写更灵活。
- 可观测集成: 脚本在压测前后自动调
/api/v1/queue/status拉取队列快照 (depth、各优先级分布、并发槽占用、Worker 存活数),可以直观看到压力如何穿过 API 层→队列→Worker 的全链路。
脚本核心用 asyncio.Semaphore 控制并发度,asyncio.gather 批量发压,收集每个请求的状态码和延迟后统一计算 P50/P95/P99/吞吐等指标。
Q2: 这些压测的数据是如何得到的?能保证准确性吗?#
数据来自真实运行——本机启动完整服务栈 (Postgres + Redis + 4 uvicorn worker + 3 diagnosis worker + 5 个 MCP 工具服务),用压测脚本直接打本地端口。
准确性保障:
- 客户端计时: 每个请求用
time.perf_counter()记录从发出到收到响应的 wall-clock 时间,包含完整的网络+序列化+业务逻辑耗时。 - 全量统计: 不采样,每一个请求的状态码和延迟都收集,百分位用排序后直接取索引计算。
- 可复现: 同样的命令、同样的服务配置,任何人都可以在本地复现。脚本支持
--n、--concurrency、--qps等参数精确控制。
局限性我也清楚: 本机压测走的是 loopback 不经过真实网络,所以延迟比生产会低; 另外单机 4 个 uvicorn worker 共享 CPU,并发数太高时会出现本地资源争抢,不能完全模拟多节点部署。
Q3: 为什么不用 k6/wrk/JMeter 这些专业压测工具?#
不排斥这些工具,但这个项目有几个特殊需求用自己的脚本更合适:
| 需求 | 通用工具 | 自写脚本 |
|---|---|---|
| 构造 Alertmanager v4 payload | 需要写 JS/Lua 模板 | 直接 Python dict |
| 5 种流量模型 (ramp/burst 等) | k6 支持但配置复杂 | 100 行 Python 搞定 |
| 压测前后拉队列快照 | 要额外写 hook | 直接嵌在脚本里 |
| 和 CI 集成 | 需要装额外 binary | 项目已有 httpx 依赖 |
| 混合场景 (submit + webhook 并发) | 需要多场景编排 | 一个 asyncio.gather |
如果团队扩大、需要分布式压测 (多台机器同时打) 或更复杂的报告,我会考虑迁到 k6,它的 scenario API 和 threshold 机制确实更成熟。
Q4: 压测场景是怎么设计的?为什么选这 5 种模式?#
每种模式对应一个真实的生产风险:
| 模式 | 模拟场景 | 验证目标 |
|---|---|---|
| flat | 运维团队集体提交诊断 (如大规模故障) | API 层在高并发下不崩、入队不丢 |
| ramp | 逐步增加负载找系统拐点 | 吞吐 vs 并发的关系,定位性能拐点 |
| sustained | 日常稳态流量 (如 Alertmanager 持续推送) | 长时间运行下内存/延迟不漂移 |
| burst | 告警风暴 (网络设备批量掉线) | 限流是否精准、突发后是否能恢复 |
| mixed | 真实生产: SRE 手动诊断 + Alertmanager 自动推送 | 两条通道互不干扰、不降级 |
二、压测数据解读#
Q5: 你的核心压测数据是什么?能不能一句话总结?#
一句话: Webhook 稳态 50 QPS 全量通过 P99 < 37ms,突发 30 倍流量 (300 QPS) 限流零误杀,回落秒级恢复,全链路 8000+ 请求零 5xx。
核心数据卡片:
| 场景 | 请求数 | 成功率 | 吞吐 | P50 | P99 |
|---|---|---|---|---|---|
| Webhook 稳态 50qps × 30s | 1,500 | 100% | 48.1 req/s | 13ms | 37ms |
| Webhook 突发 300qps × 8s | 2,400 | 19.3% (限流正常) | 275.9 req/s | 4ms | 30ms |
| Submit 限额内 | 20 | 100% | 312.8 req/s | 37ms | 46ms |
| Submit 500 并发冲击 | 500 | 4% (=精确限额) | 738.8 req/s | 118ms | 228ms |
| 混合 (submit+webhook) 30s | 2,550 | 62.5% | 41.7 req/s | 9ms | 29ms |
Q6: Webhook 稳态 100% 成功但突发只有 19.3%,这个数据怎么解释?#
这恰恰说明限流在正确工作:
- 稳态 50 QPS: 低于限额 (50/sec/IP + 500/min/source),所以 1500 个请求全部放行,P99 仅 37ms。
- 突发 300 QPS: 远超 50/sec 的 IP 限额,Lua 脚本精准拦截超额部分,只有窗口内第一批请求通过。
关键指标: 拦截的 1936 个请求全部返回 429 + Retry-After 头,没有 500/502/连接超时,说明限流保护了后端不被打穿。突发结束后 10 QPS 回落阶段又恢复 100% 成功,说明系统没有被突发打出亚健康状态。
Q7: submit 接口 500 并发只有 20 个成功,是不是性能太差了?#
不是性能差,是 限流设计如此: 单 IP 每分钟最多 20 个手动诊断请求 (rate_limit_manual_per_ip_per_min=20)。
500 个请求来自同一个 IP、在同一个分钟窗口内,所以只有前 20 个拿到 200,其余 480 个被 429。这个 20 是精确值——不是 19 也不是 21,说明 Lua 原子脚本的计数是准确的。
而且注意被拦截的 480 个请求的处理延迟——整体 738.8 req/s 的吞吐,说明 429 是快速返回的 (不消耗后端资源),限流层没有成为瓶颈。
Q8: 阶梯加压测试里 c=100 时吞吐最高 (663 req/s),c=200 反而降到 243,为什么?#
典型的并发过高导致资源争抢:
| 并发 | 吞吐 | P50 | 分析 |
|---|---|---|---|
| 20 | 300 req/s | 49ms | CPU 空闲多,请求排队少 |
| 100 | 663 req/s | 100ms | 甜蜜点: pipeline 效率最高 |
| 200 | 243 req/s | 453ms | 瓶颈: 本机 4 worker 共享 CPU,上下文切换开销激增 |
c=100 是这个配置下的性能甜蜜点。c=200 时 P50 从 100ms 涨到 453ms (4.5 倍),说明请求在 uvicorn worker 内部排队等 CPU 了。如果是多节点部署 (每节点 4 worker, 前面加 LB),这个拐点会线性右移。
Q9: 混合场景下 submit 和 webhook 有互相影响吗?#
从数据看没有:
| 通道 | 单独跑 P99 | 混合跑 P99 | 变化 |
|---|---|---|---|
| submit | 46ms | 44ms | -4% (波动范围内) |
| webhook | 37ms | 27ms | -27% (混合 QPS 更高但更均匀) |
原因: submit 和 webhook 是不同的 FastAPI 路由,限流用不同的 scope key (manual vs webhook_key / webhook_src),进入的是同一个 Redis Streams 但用不同的 priority level。两条通道逻辑隔离,只在 uvicorn worker 和 Redis 连接池层面共享资源。在目前的 QPS 下,这些共享资源远未饱和。
三、限流设计深度#
Q10: 限流为什么用固定窗口而不是滑动窗口或令牌桶?#
三个考虑:
- 简单够用: 这是运维平台不是交易系统,固定窗口的 “窗口边界突刺” (理论上窗口交界处瞬时 2x) 对我们可接受。正常使用不会刚好在窗口边界集中请求。
- 单次 Redis 往返: 一个 Lua 脚本 INCR + EXPIRE 搞定,不需要多次 ZADD/ZRANGEBYSCORE (滑动窗口) 或 MULTI/EXEC (令牌桶)。
- 可演进: 接口是
hit(scope, identity, limit, window_sec)→(bool, retry_after),后端 Lua 换成滑动窗口,调用方一行不改。
如果面试追问 “窗口边界怎么办”: 实际限额设为 20/min,即使边界突刺到 40/min,这个量级对后端毫无压力。真正的保护在下一层——分布式并发槽 (限的是正在执行的 LLM 调用,不是入队请求)。
Q11: 为什么限流用 Lua 脚本而不是 Redis 的 INCR + 单独 EXPIRE?#
这是一个真实踩过的坑:
问题: 如果用两步操作 INCR key → if count == 1: EXPIRE key window,在高并发下会出竞态: 多个请求同时看到 count == 1,都去 EXPIRE,窗口被反复重置。极端情况下 EXPIRE 丢失 (网络闪断),key 永不过期,该 IP 永远被封。
解法: 合成一个 Lua 脚本原子执行:
local n = redis.call('INCR', KEYS[1])
if n == 1 then
redis.call('EXPIRE', KEYS[1], tonumber(ARGV[1]))
end
return nluaRedis 执行 Lua 是单线程原子的,不可能有两个请求同时拿到 n == 1。
我在 tests/test_rate_limiter.py 里有对应的测试: 并发 5 个请求打 limit=5 的窗口,验证恰好 5 个通过 0 个多放。
Q12: Fail-open 策略——Redis 挂了就不限流,LLM 不会被打爆吗?#
分层设计——限流层 fail-open 不意味着没有保护:
请求 → [IP 限流] → [入队] → [分布式并发槽] → [LLM/RAG/MCP]
↑ ↑
fail-open 也 fail-open (但此时 Redis 挂了队列也不工作)plaintext关键洞察: 限流层和队列用的是同一个 Redis。如果 Redis 挂了:
- IP 限流 fail-open → 请求放进来
- 但入队操作 (XADD 到 Redis Streams) 也会失败 → 请求走不到 Worker
- Worker 的 XREADGROUP 也拿不到任务
所以 Redis 挂了,限流层放行的请求会在入队环节失败返回 500,LLM 根本不会被调到。两层 fail-open 在 Redis 不可用时效果等价于”全链路不可用”,而不是”无限流打爆 LLM”。
Q13: 429 响应里的 Retry-After 是怎么算的?#
retry_after = window_sec - (now % window_sec)python固定窗口的桶号是 now // window_sec,当前窗口的剩余时间就是 window_sec - (now % window_sec)。客户端拿到这个值后等那么多秒,新窗口计数器归零,就可以重试了。
这个值写在 HTTP 响应头 Retry-After 和 body 的 retry_after 字段里,前端可以直接用来做倒计时 UI。
四、分布式并发控制#
Q14: 为什么需要分布式并发槽?asyncio.Semaphore 不够吗?#
不够,一旦多进程就失效:
uvicorn --workers 4
├── worker-1: Semaphore(2) ← 各有各的
├── worker-2: Semaphore(2)
├── worker-3: Semaphore(2)
└── worker-4: Semaphore(2)
总并发 = 2 × 4 = 8 ← 配置写的是 2,实际跑了 8plaintext加上 3 个独立 diagnosis worker 进程,共 7 个进程,如果每个都本地限 2,全局就是 14 个 LLM 调用同时跑,会超 API 的 RPM/TPM 配额。
所以用 Redis ZSET 做全局槽:
- 所有进程共享一个
aiops:limiter:worker_diagnosisZSET - Lua 原子脚本: 清过期 → 数当前 → 没满则 ZADD 占槽
- token 格式
hostname:pid:uuid,可以看出哪个进程占了哪个槽
Q15: 并发槽的 Lua 脚本具体做了什么?#
一个 Lua 脚本原子完成三步:
-- 1. 清掉已过期的槽 (score < 当前时间)
redis.call('ZREMRANGEBYSCORE', KEYS[1], '-inf', now_ms)
-- 2. 数还有多少存活槽
local count = redis.call('ZCARD', KEYS[1])
-- 3. 没满就占一个 (score = 当前时间 + TTL)
if count < limit then
redis.call('ZADD', KEYS[1], now_ms + ttl_ms, token)
return 1 -- 抢到了
end
return 0 -- 满了lua用 Redis 服务端的 TIME 命令取时间戳,避免各客户端时钟漂移。整个操作在 Redis 单线程里原子执行,不存在 “check-then-act” 竞态。
Q16: 进程崩溃了槽位怎么办?会不会永久泄漏?#
不会。每个槽有 TTL (默认 90 秒):
- 正常运行: 后台任务每 30 秒心跳续期 (Lua
_REFRESH_LUA把 score 往后推) - 进程崩溃: 心跳停止 → 90 秒后 score 过期 → 下次有人 acquire 时被
ZREMRANGEBYSCORE清掉
TTL 90s > 心跳 30s × 2,有一跳的容错余量。诊断任务最长 600 秒 (10 分钟),心跳会持续续期,不会被误清。
Q17: Pause/Resume 是什么?什么场景用?#
当诊断流程中遇到需要人工审批的高危操作时:
Worker 占槽 → Agent 跑诊断 → 遇到高危命令 → 等待审批 (可能几分钟到几小时)plaintext如果等审批时一直占着槽,其他任务就被卡住了。所以设计了 Pause/Resume:
await handle.pause() # 释放槽 + 停心跳 (让给别人用)
# ... 等待人工审批 ...
await handle.resume() # 重新抢槽 (满了会阻塞等待)python通过 ContextVar 暴露 current_slot,tool_runner 在检测到需要审批时可以拿到当前槽句柄并 pause。
五、队列与优先级#
Q18: 为什么用 Redis Streams 而不是 Kafka?#
| 维度 | Redis Streams | Kafka |
|---|---|---|
| 运维成本 | 项目已有 Redis (限流/缓存),零额外部署 | 需要 ZooKeeper/KRaft + broker 集群 |
| 日吞吐量 | 我们的场景 5k-20k 条/天 | 设计目标百万级/天 |
| 优先级 | 4 条 stream 模拟 4 级优先级,Worker 按优先级顺序 XREADGROUP | 原生不支持,要 N 个 topic + 消费权重 |
| 消费确认 | XACK 逐条确认,pending 可重试 | offset commit,重平衡可能重复 |
| 持久性 | AOF 持久化,够用 | 多副本分区,更强 |
一句话: Kafka 是 “大炮打蚊子”。Redis Streams 在当前规模下够用,而且和限流、并发槽共享同一个 Redis 实例,运维复杂度低。
如果日吞吐超过 10 万,或者需要跨数据中心消费,会考虑迁 Kafka。
Q19: 4 级优先级是怎么实现的?#
用 4 条独立的 Redis Stream:
aiops:incident:critical ← severity=critical/page/p0
aiops:incident:high ← severity=warning/p1/p2
aiops:incident:normal ← severity=info 或手动提交
aiops:incident:low ← severity=低优先级plaintextWorker 的消费策略:
- Phase 1 (非阻塞): 按 critical → high → normal → low 顺序 XREADGROUP count=1,有消息就拿
- Phase 2 (阻塞): 如果全空,XREADGROUP BLOCK 5000ms 等新消息
这样 critical 告警即使后到,也会被优先消费。
Q20: 压测数据怎么验证优先级插队?#
在 burst 压测后看队列快照:
depth=215 by_level={'critical': 50, 'high': 56, 'normal': 53, 'low': 56}
pending=3 ← 3 个 Worker 正在消费plaintext告警用 --severity mix 随机分配,4 个级别的堆积量大致均匀。但 Worker 消费总是先清 critical,可以在队列深度变化曲线中观察到 critical 最先回落。
六、全链路架构#
Q21: 请求从进来到被 Worker 执行,完整链路是怎样的?#
客户端 POST /webhook/alertmanager
→ [IP 限流] rate_limiter.enforce (Lua INCR+EXPIRE, 50/sec)
→ [来源限流] rate_limiter.enforce (Lua INCR+EXPIRE, 500/min/source)
→ [归一化] _format_alert_as_query → 结构化告警 → 诊断 query 文本
→ [去重] fingerprint 查 Postgres,重复告警跳过
→ [落库] Postgres INSERT incident + task
→ [入队] Redis XADD 到对应优先级 stream
→ API 返回 200 + task_id (整体 P50 13ms)
Worker (独立进程, 常驻循环):
→ [领任务] XREADGROUP 按优先级扫 4 条 stream
→ [抢槽] distributed_limiter Lua 抢全局并发槽 (满了等待)
→ [心跳] 后台协程每 30s 续期槽 TTL
→ [诊断] Planner → Executor(并行 Skill/MCP 工具) → Reporter
→ [写回] Postgres UPDATE task status + result
→ [释放] ZREM 释放槽 + XACK 确认消息plaintext压力在每一层被逐级消化:
- API 层: 限流挡掉超额流量
- 队列层: Redis Streams 削峰,API 快速返回
- 执行层: 分布式并发槽限制真正的 LLM 调用数
Q22: 你说 “API 快速返回”——submit 的 P50 才 37ms,这是因为没调 LLM 对吧?#
对。submit 接口是纯 “入账+入队”:
- Pydantic 校验 JSON (< 1ms)
- Lua 限流检查 (1 次 Redis 往返,~1ms)
- Postgres INSERT (一次网络往返,~5-10ms)
- Redis XADD (一次往返,~1ms)
- 返回 task_id
不调 LLM、不做 RAG 检索、不调 MCP 工具。这就是”队列削峰”的设计意图——把昂贵的诊断推到后台 Worker 异步执行,API 层做最轻量的事。
如果走同步 SSE 入口 (/diagnose),则 API 进程内跑完整诊断 (30s~5min),这时候就需要并发槽来控制了。
七、系统瓶颈与调优#
Q23: 你这个系统的瓶颈在哪?#
分层来看:
| 层 | 瓶颈 | 当前上限 | 扩容方式 |
|---|---|---|---|
| API 接入 | uvicorn worker 数 × CPU | ~700 req/s (本机 4 worker) | 加节点 + LB |
| 限流 | Redis 单线程 | 10 万+ QPS (Redis 自身) | 不是瓶颈 |
| 队列写入 | Redis XADD | 几十万 QPS | 不是瓶颈 |
| 队列消费 | Worker 数量 | 3 个 Worker | 加 Worker 进程/节点 |
| 诊断执行 | LLM API 配额 (RPM/TPM) | 百炼约 200 RPM | 换更高配额 / 多 Key 轮转 |
真正的瓶颈是 LLM API 配额,不是系统本身。这也是为什么我把执行并发槽 (32) 设得远低于系统吞吐能力——不是系统扛不住,是 LLM 调用量要控制。
Q24: 如果要把这套系统从本机搬到生产环境,需要改什么?#
按优先级:
- 多节点 API: Kubernetes Deployment replicas=3+,前面加 Ingress/LB。限流已经基于 Redis 全局,多实例天然支持。
- Worker 横向扩展: Worker 进程可以运行在任意节点上,共享同一个 Redis 的并发槽。加节点只需增加
WORKER_DIAGNOSIS_CONCURRENCY配额。 - Redis 高可用: 现在单节点,生产上 Sentinel 或 Redis Cluster。限流和队列都依赖 Redis,单点是最大风险。
- Postgres 连接池: 用 PgBouncer,多 Worker 的场景下直连会打满连接数。
- 监控: Prometheus 采集 API 延迟 / 队列深度 / 槽占用 / Worker lag,Grafana 看板 + AlertManager 告警。
- 限流策略调整: 滑动窗口替代固定窗口 (消除窗口边界突刺);按用户/租户而不是纯 IP 限流。
Q25: 并发槽从 2 改到 16/32,凭什么选这个数?#
基于 LLM API 配额倒推:
百炼 RPM ≈ 200 (经测试的实际约束)
单次诊断平均 LLM 调用数 ≈ 5-8 次 (Planner + Executor iterations + Reporter)
单次诊断耗时 ≈ 30s-2min
Worker 并发 32 × 平均 6 次调用 × (60s / 60s每次诊断) ≈ 192 RPM ← 接近但不超百炼上限
手动并发 16 + Worker 并发 32 = 最大可能 48 个同时诊断 ← 不会真的同时都在调 LLMplaintext留有余量: 不是所有 48 个诊断都同时在调 LLM (有的在等工具返回、有的在做 RAG 检索),实际 LLM 并发会低于 48。
Q26: 压测延迟里,哪些是网络延迟,哪些是业务逻辑延迟?#
本机 loopback 网络延迟约 0.1ms,可以忽略。所以压测延迟 ≈ 纯业务逻辑:
| 操作 | 耗时 |
|---|---|
| Pydantic 校验 | < 1ms |
| Lua 限流 (1 次 Redis eval) | ~1ms |
| Postgres INSERT | ~5-10ms |
| Redis XADD | ~1ms |
| FastAPI 序列化响应 | < 1ms |
| 合计 (P50) | ~13ms (webhook) / ~37ms (submit) |
submit 比 webhook 慢是因为 submit 的校验逻辑更多 (session_id 处理、severity 映射等)。生产环境需要加上真实网络延迟 (同 Region ~1-3ms,跨 Region ~30-100ms)。
八、容错与降级#
Q27: 压测过程中出过事故吗?#
跑了 8000+ 请求,全部都是 200 或 429,零 5xx、零连接超时、零进程崩溃。
但之前开发过程中踩过坑:
- Lua 竞态 (已修复): 早期限流用 INCR + 单独 EXPIRE,并发压测时出现过窗口被反复重置的 bug,改成 Lua 原子脚本后再没出现。
- Worker 不重连 (已知限制): Redis 重启后 Worker 的连接会断,进程直接退出。目前靠
start.sh的启动顺序规避,生产上应该加重连逻辑或用 supervisor 进程管理。
Q28: 如果某个 Worker 执行诊断时突然被 kill -9,会发生什么?#
四层兜底:
- 并发槽 TTL 过期: 心跳停止 → 90s 后槽位被自动回收 → 不会永久泄漏
- Redis Streams pending: 消息已 XREADGROUP 但没 XACK → 留在 pending list → 其他 Worker 可以 XCLAIM 接管
- Postgres 任务状态: task status 停在
running→ 定时扫描器发现 timeout (600s) 后标记失败 → 重新入队 (最多 3 次) - DLQ: 重试 3 次仍失败 → 进入死信队列 → 人工排查
Q29: 你怎么验证 “零 5xx” 不是偶然的?#
- 多次复跑: 5 种场景各跑了多次,结论一致。
- 覆盖极端: burst 打了 300 QPS (正常的 30 倍)、flat 打了 500 并发,都是故意超出正常范围。
- 错误分类完备: 脚本统计了所有状态码分布,只有
{200: N, 429: M}两种,没有第三种。 - 连接异常也统计:
except Exception会标记为code=0,最终结果里没有出现过 0。
九、与简历的衔接#
Q30: 简历上写 “663 req/s”,面试官可能问 “这是什么条件下的?”#
准确回答: 本机 macOS,4 uvicorn worker,100 并发打 webhook 入口 (只做入账+入队,不调 LLM),663 req/s 是阶梯加压中 c=100 那一级的数据。c=200 时降到 243 req/s,说明 100 是单机甜蜜点。
如果追问”生产环境能到多少”: 线性估算,3 节点各 4 worker = 12 worker,理论上 ~2000 req/s。但实际受 Postgres 写入、Redis 带宽、网络延迟影响,需要在真实环境再跑一轮。
Q31: 面试官问 “这个数字有什么意义”,怎么回答?#
对运维诊断平台来说,入口吞吐不是核心指标,核心是”诊断任务的全链路 SLA”。663 req/s 证明的是:
- API 层不是瓶颈: 即使告警风暴 (网络设备批量掉线,几百条告警同时涌入),入口也扛得住。
- 削峰设计有效: 高并发进来的请求被快速入队,不会因为 API 层卡住导致告警丢失。
- 限流精准: 超额流量被干净地 429 掉,不会穿透到后端。
真正有价值的数字是 “P99 37ms 的稳态延迟” 和 “突发后秒级恢复”——说明系统在正常负载下足够快,异常负载下能自保且快速恢复。
Q32: 面试官问 “这个项目你实际在生产跑过吗”,如果没有怎么回答?#
诚实说: 这是个人项目,没有在生产环境跑过。但我在设计时按生产标准来:
- 压测验证: 不只是写完就算,用多种流量模型做了真实压测,拿到量化数据。
- 容错设计: fail-open、TTL 过期回收、心跳续期、DLQ——这些不是纸上谈兵,都有代码和测试。
- 可运维: 队列深度/并发槽/Worker 存活数都暴露在
/queue/status,和 Prometheus 集成只差一个 exporter。 - 参数化: 所有限额都通过环境变量配置,不硬编码,部署时可以按实际环境调整。
和生产的差距我也清楚: 缺少多节点压测、缺少持续运行的稳定性验证、缺少真实网络环境下的尾延迟数据。如果给我一个月,我优先补 Redis 高可用和 Worker 自动重连。
十、设计取舍与反思#
Q33: 你觉得这套限流+队列+并发槽的设计过度了吗?#
对”个人项目”来说确实重了,但设计意图是 展示工程深度:
| 如果不做 | 后果 |
|---|---|
| 不做 IP 限流 | 一个脚本就能刷爆 API,占满 LLM 配额 |
| 不做队列 | 高并发时 API 进程被 LLM 长调用拖垮 |
| 不做分布式并发槽 | 多 Worker 同时打 LLM,超 RPM 限额 |
三层缺任一层,系统在 10 QPS 以上就会出问题。如果只能保留一个,我选队列削峰——它解决的是最根本的”快接口 vs 慢诊断”矛盾。
Q34: 如果重来,你会改什么?#
- 滑动窗口限流: 固定窗口的边界突刺虽然在当前场景可接受,但换成滑动窗口 Lua 脚本只多几行代码,没理由不换。
- Worker 自动重连: 当前 Worker 进程 Redis 断连直接退出,应该加 backoff 重试 + supervisor 重启。
- 分级 QPS 控制: 现在只有 “单 IP 20/min” 这种粗粒度,生产上应该支持租户级别的配额,甚至对接外部的 API Gateway。
- 压测脚本输出: 目前只打印文本,应该输出 JSON/CSV 方便画图对比历史数据。
Q35: 这套架构最大的亮点和最大的风险分别是什么?#
亮点: 三层递进式压力治理 (IP 限流 → 队列削峰 → 并发槽),每层都用 Lua 保证原子性,每层都 fail-open 不成为新的单点。有真实压测数据而不是 “理论上能扛住”。
风险: Redis 单点。限流、队列、并发槽全押在一个 Redis 上。Redis 挂了三层同时失效。生产必须上 Sentinel 或 Cluster,这是目前最大的待补项。