千万 QPS 架构里,最值得先研究的不是某个框架有多快,而是“链路分析”怎么做。这一讲的主题叫“从查询到智能”,意思是用户发起的每一次查询,都不会只走过一个服务,而是从入口网关开始,经过路由、鉴权、限流、缓存、业务服务、存储,再到可能的模型推理、语义检索、Agent 编排,最后把结果组装返回。链路分析要解决的问题,就是让这条复杂链路里的每一跳都可见、可度量、可治理。
如果已经带过千万级流量系统,或者正在设计一个未来可能到千万 QPS 的系统,这篇文章会比较合适。你会看到一条查询到底经过哪些节点,每个节点应该关注什么指标,链路追踪的 trace 和 span 应该怎么设计,以及加入 AI 或 Agent 之后,链路分析为什么会变得更难。
我先把结论放在前面:千万 QPS 的难点从来不是“某个服务扛不住”,而是“你无法快速判断请求卡在哪一跳”。链路分析不是追个日志那么简单,它是容量规划、故障定位、成本控制和质量治理的基础。没有链路分析,调大并发只是在赌。
1. 先搞清楚千万 QPS 场景下的“链路”到底指什么
很多同学刚开始接触高并发时,习惯把系统拆成“客户端、nginx、后端、数据库”四层。这种理解在小流量下没问题,但在千万 QPS 级别,链路会被拆得细得多,而且每一层都可能存在多个实例、多级缓存、多个版本和多种协议。
1.1 一条查询从客户端到响应的完整路径
一条典型的查询请求,在完整链路上大概会经过这些节点:
- 客户端 SDK 或浏览器发起请求。
- DNS 解析、CDN 或全局负载均衡。
- 接入网关:TLS 终止、鉴权、限流、灰度路由。
- API 网关或 BFF 层:协议转换、聚合编排、参数校验。
- 微服务调用链:可能经过 A 服务、B 服务、C 服务,服务之间通过 HTTP、gRPC 或消息队列通信。
- 数据访问层:Redis 缓存、本地缓存、数据库、搜索引擎、对象存储。
- 异步处理链路:消息队列、事件订阅、离线任务。
- 智能处理层:向量检索、模型推理、Agent 工具调用、语义缓存命中。
这里每一跳都可能出现 10 毫秒到 100 毫秒不等的耗时。看似单跳不多,乘起来之后,P99 就会变得很难看。链路分析的第一步,就是把这条完整路径画出来,哪怕画在纸上,也要把每一个调用关系写清楚。
1.2 为什么高并发系统必须做链路分析
没有链路分析的系统,遇到问题时的典型表现是:数据库 CPU 飙高,但数据库本身没有慢查询;某个接口 P99 上涨,但业务日志里没有任何异常;缓存命中率很高,但响应时间还是很慢。
这些现象背后往往是链路问题。比如:
- 某个服务把下游超时时间设成了 5 秒,导致线程池被打满。
- 网关重复解析了 JSON body,导致序列化开销放大。
- 缓存穿透打到数据库,数据库连接池等待时间飙升。
- 模型推理服务批量参数设置不合理,导致排队时间急剧增加。
- Agent 编排里的工具调用串行执行,多个大模型请求叠加后延迟失控。
链路分析就是为了在几秒钟内把这类问题定位到具体节点。它是可观测性的核心,也是团队协作的语言:你说“接口很慢”,我说“具体是哪一跳慢”,对话效率完全不同。
1.3 千万 QPS 和普通接口调用的本质区别
普通接口调用,QPS 可能只有几十到几千,偶尔慢几个请求影响不大。千万 QPS 级别的系统,真正改变的是概率和范围:
- 就算错误率只有 0.01%,每秒也可能有 1000 个请求失败。
- 就算单个请求多消耗 1 毫秒 CPU,千万 QPS 也会增加约 10000 核秒的 CPU 开销。
- 就算缓存命中率掉了 1 个百分点,也会造成大量请求穿透到下游。
- 一次性放量或者代码上线引入一个小问题,影响面积可能是千万级用户。
所以千万 QPS 架构里的链路分析,必须从“能不能查到日志”升级为“能不能自动聚合、自动归因、自动预警”。传统的人工翻日志方式,在这个规模下基本失效。
2. 链路里的关键节点:从查询入口到智能决策
要读懂链路分析,必须先把链路上的节点理解到位。这里不是说要把每个组件都玩透,而是要知道每个节点在链路里承担什么职责,以及哪些指标会影响整条链路的吞吐和延迟。
2.1 接入层:网关、鉴权、限流、路由
接入层是流量的第一站。在千万 QPS 场景下,网关通常会处理这些事:
- TLS 卸载:把加密握手消耗放在网关层,避免每个业务服务都做加解密。
- 鉴权与风控:从 token 或者签名中提取用户身份,判断是否有权限。
- 限流与熔断:按用户、按接口、按来源 IP 做配额控制。
- 灰度路由:根据用户 ID、请求参数或版本号把流量打到不同服务版本。
接入层最容易出现的问题是“网关变成单点”。即使网关集群部署了 100 个实例,如果每个请求在这里多消耗 2 毫秒,整体延迟预算就被吃掉一大块。链路分析在接入层要重点看几个指标:每秒处理的请求数、连接数、超时数、鉴权失败数,以及网关处理耗时占整体耗时的比例。
在实际压测里,我通常建议先把网关单独压一遍。因为网关一旦出现线程池耗尽,后面所有服务的压力都会异常增高,让你误以为问题是数据库或者缓存造成的。
2.2 服务层:微服务、RPC 调用和依赖治理
过了接入层之后,请求会进入业务服务或 BFF 层。这一层的顺序通常是:先做本地逻辑,再调下游接口,再聚合结果。服务之间的调用关系,必须用链路追踪完整记录下来。
微服务架构下,服务层链路分析需要注意三个问题:
- 依赖深度:A 调 B,B 调 C,C 调 D,一旦 D 抖动,整条链路都会被拖住。
- 超时设置:每个服务自己的超时时间叠加,最终可能远大于客户端能接受的超时时间。
- 重试风暴:服务 B 失败了,服务 A 自动重试 3 次,流量瞬间放大 3 倍。
我在处理线上故障时,第一反应不是看 CPU,而是看服务间调用耗时分布。这样可以快速判断是某个下游变慢,还是当前服务自身阻塞。
2.3 数据层:缓存、存储、消息队列
数据层是千万 QPS 链路的另一个关键瓶颈。很多请求最终不会打到数据库,但为了支撑千万 QPS,你必须为数据库设计“缓冲垫”。
常见的数据层链路结构是:
- 本地缓存(Caffeine、Go 的 sync.Map 等),命中后返回。
- 分布式缓存 Redis,Key 设计、淘汰策略、热点 Key 管理。
- 数据库分库分表或 NewSQL,承接最终一致性要求高的写入。
- 搜索引擎或倒排索引,处理复杂查询需求。
- 消息队列,在写入高峰时削峰填谷。
链路分析在数据层要看的是:本地缓存命中率、Redis 命中率、Redis 平均耗时、数据库连接池活跃数、消息队列积压量。这里有一个容易被忽略的坑:本地缓存命中率很高时,Redis 的 QPS 可能不高,但本地缓存过期时间设置得太长,会导致数据不一致。链路分析只能告诉你“数据从哪一层返回”,至于是否“新鲜”,还需要结合业务指标判断。
2.4 智能层:模型推理、语义缓存、Agent 编排
这一部分就是“从查询到智能”的核心变化。传统的查询链路是确定性的:参数进去,结果出来,规则不变。但加入模型推理或者 Agent 之后,链路会变得非常不同。
智能层通常包含这些组件:
- 模型推理服务:把用户 query 编码成向量,或者直接生成答案。
- 向量检索服务:用 embedding 做相似度检索,把最相关的上下文找出来。
- 语义缓存:判断两个 query 是否语义等价,如果等价就直接返回缓存结果,避免重复走模型推理。
- Agent 编排:把用户意图拆成多个子任务,按顺序或并行调用多个工具。
智能层对链路分析带来的最大挑战是“不确定性”。同一个 query,模型推理的耗时可能因为输入长度、并发排队、缓存命中情况而波动很大。Agent 编排可能调用 3 个工具,也可能只调用 1 个工具,甚至可能因为模型幻觉导致多绕几个来回。传统 RPC 的耗时预测方法在这里不太适用。
3. 落地链路追踪时,先解决 trace 的生成、透传和采样
要做链路分析,不能靠感觉,必须有一套链路追踪系统。现在常见的思路是参考 Dapper 那套模型:一个 trace 表示一次完整请求,trace 下包含多个 span,每个 span 表示一个具体的调用单元。
3.1 trace ID 和 span 的核心设计
链路追踪的第一件事是生成全局唯一的 trace ID。这个 ID 要贯穿整个请求链路,从网关开始生成也好,从客户端 SDK 生成也好,关键是后续每一跳都要带着它。
span 是链路上的最小工作单元,比如“调用 Redis”“调用订单服务”“执行 Agent 工具调用”。每个 span 需要记录:
- span ID 和 parent span ID。
- 服务名、方法名、资源名。
- 开始时间和结束时间。
- 状态:成功、失败、超时。
- 关键标签:用户 ID、请求参数摘要、错误码、重试次数。
在设计 trace ID 时,尽量不要把用户 ID 直接作为 trace ID。因为用户 ID 可能暴露业务信息,也可能导致一次请求内部的多个并发子任务无法区分。更好的方式是生成随机 ID,把用户 ID 作为 tag 附加到 span 上。
3.2 header 透传:HTTP 头、消息队列 header、异步线程上下文
链路追踪最容易出错的地方是“透传”。一旦某个环节没有把 trace ID 传下去,整条链路就断了。
在 HTTP 调用中,通常通过自定义请求头传递,比如X-Trace-Id、X-Span-Id。在 gRPC 中,可以放到 metadata 里。在 Kafka 或 RocketMQ 这类消息队列中,需要放到消息的 header 里,而不是塞进业务消息体。
这里有几个常见坑:
- 异步线程池中没有把 trace ID 从主线程复制到子线程,导致子任务丢失上下文。
- 用了线程池后,context 传递不完整,日志里能看到 trace ID,但 span 关系是错的。
- 服务 A 改了请求头名称,服务 B 还没有同步,导致链路断在中间。
- 使用消息队列时,消费者消费消息后新开了一个 trace,而不是沿用生产者 trace。
我建议在链路的 SDK 层面统一封装透传逻辑,不要在每个业务代码里手工赋值。否则只要有一个服务忘记透传,整个链路图就少掉一块,排查故障时又得靠猜。
3.3 采样策略:千万 QPS 下不能全量记录
到了千万 QPS 这个规模,一个很现实的问题是:如果每个请求都全量记录完整的 span,存储成本会高到难以接受。假设平均一个请求产生 30 个 span,每个 span 50 字节的索引和标签,千万 QPS 一天会产生 PB 级的数据。
常见的采样策略有几种:
- 固定比例采样:比如 1% 或 0.1%,实现简单,但可能漏掉罕见的慢请求。
- 基于延迟的采样:P99 以上的慢请求全量记录,正常请求按比例采样。
- 基于错误状态的采样:只记录错误或超时的 span,成功请求尽量少记。
- 动态采样:根据系统负载实时调整采样率,高负载时降低采样率。
实际落地时,我会把“全部错误必采”和“慢请求必采”作为底线。因为故障排查最需要的就是错误和慢链路,正常链路少记几条影响不大。全量数据可以用于离线分析,但不能把所有流量都打到在线存储里。
注意:采样策略必须和日志系统一起设计。如果 trace 采样了,日志却没有按 trace ID 关联,排查问题还是要翻多个系统。
4. 压测与容量规划:千万 QPS 不是调大并发就能得到的
很多团队在规划千万 QPS 时,第一个动作是把连接数调大、把线程池调大、把限流阈值放开。这样做往往只会让系统提前崩掉。容量规划必须从链路分析出发,先算出每一跳的容量边界。
4.1 先做链路容量拆解
链路容量拆解的核心方法是:把一条查询从入口到出口拆成多个节点,估算每个节点单机可以支撑多少 QPS,然后倒推需要多少实例。
举个例子,假设一个查询链路是:
- 网关层单机可以支撑 5 万 QPS,需要 200 台才能支撑千万 QPS。
- 业务服务单机可以支撑 1 万 QPS,需要 1000 台。
- Redis 单集群可以支撑 50 万 QPS,可能需要 20 个集群。
- 数据库单库支撑 5000 QPS,需要做大量分片,且只能承接部分查询。
这里的数字只是示意,实际一定要基于你自己的压测数据。链路分析的价值在于:你不能只按“总 QPS 除以单机 QPS”来算,还要考虑请求比例和热点分布。比如 90% 的流量集中在 10 个接口上,那这 10 个接口的容量必须单独保障。
4.2 单机基准、链路压测和全链路压测
容量规划不能只看理论,必须分三个层次做压测。
一是单机基准压测。把每个服务单独部署到测试环境,用固定 QPS 打过去,观察 CPU、内存、GC、连接池、P99 等指标,得出单机基准值。
二是局部链路压测。把网关到业务服务、业务服务到 Redis 的链路串联起来,模拟真实调用参数。目的是验证依赖组件的吞吐是否匹配。
三是全链路压测。在隔离的压测环境或线上低峰期,模拟完整请求路径,包括消息队列、异步任务、模型推理服务。只有全链路压测才能发现问题,因为真实场景里的队列积压、线程池争抢、数据库连接数在单机测试里很难暴露。
我见过不少团队只做了单机压测就上线,结果全链路压测时发现消息队列写入成为瓶颈。原因是单机压测时的下游压力不够,没有把消息积压和重试放大算进去。
4.3 负载均衡、连接池、线程池、超时与重试参数
链路分析最终要落到参数上。不同节点有不同的关键参数:
| 节点 | 关键参数 | 建议判断标准 |
|---|---|---|
| 网关 | 连接数、线程池大小、限流阈值 | 看 CPU 是否还有余量,P99 是否达标 |
| 服务层 | RPC 超时、重试次数、并发数 | 看下游超时与失败率,不要无限重试 |
| Redis | maxclients、超时时间、淘汰策略 | 看命中率和命令耗时,避免大 Key 阻塞 |
| 数据库 | 连接池大小、慢查询阈值 | 看活跃连接数和慢查询比例 |
| 消息队列 | 生产端并发、消费端线程数、批量大小 | 看积压量和消费延迟 |
| 模型推理 | batch size、并发数、超时时间 | 看排队延迟和 GPU 利用率 |
在设置超时时,要遵循一个原则:从客户端到最下游,超时时间应该是递减的。客户端允许 2 秒,网关就不能等 3 秒;服务 A 调服务 B 的超时,必须小于服务 A 允许给客户端的剩余时间。重试次数也要谨慎,线上故障时,重试带来的流量放大往往会压垮下一个服务。
5. 从查询到智能的实际落地形态
“从查询到智能”这句话听起来抽象,但在架构里可以拆成非常具体的落地形态。我理解它至少包括三层含义:传统查询链路的智能治理、语义缓存、以及 Agent 编排链路。
5.1 传统查询链路的“智能”是什么
第一种智能不是上大模型,而是让查询链路具备自动决策能力。比如:
- 根据实时流量自动调整限流阈值。
- 根据缓存命中率和数据新鲜度自动切换缓存策略。
- 根据下游错误率自动熔断、降级或切到备用集群。
- 根据查询参数自动路由到不同存储引擎,比如少量数据走 Redis,复杂查询走搜索引擎。
这种“智能”是规则和策略层面的智能,它依赖的正是链路分析数据。没有实时流量指标,就没有办法做动态决策。所以做传统服务治理时,已经需要把链路数据采集到实时监控系统,再交给控制面做决策。
5.2 语义缓存:把重复查询挡在模型推理之前
加入大模型或向量检索之后,查询成本会明显上升。模型推理服务的单次请求耗时通常比 Redis 查询高几个数量级,所以必须用缓存机制把重复请求挡在外面。
传统的缓存是 key-value 精确匹配,比如“北京今天天气”和“北京天气今天”会被当成不同 key。语义缓存则用 embedding 向量来判断两个 query 是否等价。如果相似度超过阈值,就直接返回之前的结果。
在链路分析中,语义缓存是一个新的节点,需要单独监控:
- 缓存命中率:决定绕过模型推理的请求比例。
- 相似度阈值:阈值太高命中率低,阈值太低容易误命中,影响答案质量。
- 缓存失效策略:语义缓存过期时间怎么设计,不同主题的缓存是否隔离。
- 向量检索延迟:从 embedding 到向量库查询,是否成为新的瓶颈。
我建议第一次接入语义缓存时,先只对高频、低时效性要求的查询生效,不要一股脑把所有 query 都缓存。否则相似度误命中导致用户看到过时答案,业务投诉会比性能问题更严重。
5.3 Agent 编排链路:工具调用、上下文传递和失败降级
Agent 架构是“从查询到智能”更复杂的一种形态。用户提交一个任务,Agent 把任务拆解成多个步骤,每一步可能调用不同工具,比如搜索、查数据库、调用业务 API、生成代码。每一步之间还要传递中间结果,最后汇总输出。
这种链路的分析难度在于:
- 执行路径不固定。同一个 query,今天走 2 个工具,明天可能走 5 个工具。
- 每一步的耗时差异巨大。一个工具可能 100 毫秒,另一个工具可能 30 秒。
- 上下文大小会影响模型推理耗时。历史消息越长,每次推理的延迟越高。
- Agent 失败时,可能触发重新规划,导致整个链路重复执行多轮。
在链路追踪系统里,我会把“一次 Agent 任务”作为一个独立 trace,把“每次工具调用”作为一个 span,把“模型推理的输入输出 token 数量”作为 span 的关键 tag。这样可以看到任务整体耗时、每一步耗时、重新规划次数和 token 成本。
注意:Agent 链路里必须设置总超时限制。比如整个任务最多 60 秒,超过后直接返回部分结果或失败提示。如果没有总超时,某个工具卡住时,整个任务会一直挂着,连接池和队列会被打满。
6. 常见故障排查链路和治理清单
最后聊一下具体排查。千万 QPS 系统的故障现象往往是相似的,但定位路径可以整理成一套固定流程,避免每次上线都像第一次排查。
6.1 拿到一个慢查询,先看哪四层
当一个接口 P99 上涨时,我一般按下面顺序排查:
- 看链路追踪图:这个请求到底经过了几跳,哪一跳耗时最长。先确定问题发生在网关、服务、数据层还是智能层。
- 看资源指标:问题节点的 CPU、内存、GC、连接池、线程池是否达到瓶颈。如果 CPU 很低但请求很慢,大概率是等待下游或锁等待。
- 看上下游依赖:问题节点依赖的 Redis、数据库、模型推理服务是否有异常。很多时候是某个共享依赖抖动,拖慢了所有上游服务。
- 看参数和代码变更:最近是否调整过超时时间、重试次数、批量大小、缓存过期时间。上线发布记录往往能直接给出答案。
这里的关键是不要一上来就翻日志。日志只能告诉你业务层发生了什么,无法告诉你整个调用瀑布图的耗时分布。没有链路数据,只能靠猜。
6.2 链路分析常见的三类误判
第一类误判:看到某个服务耗时高,就认为是这个服务代码写得慢。实际上,这个服务可能只是在等待下游响应。一定要看服务自身的 CPU 和应用线程状态,如果 CPU 很低但耗时高,问题很大概率在下游。
第二类误判:看到 Redis 慢,就认为是 Redis 容量不够。实际上可能是请求侧出现了大 Key、热 Key,或者使用了复杂度很高的命令,导致单个命令阻塞。
第三类误判:看到模型推理耗时高,就认为是 GPU 不够。实际上可能是输入 prompt 太长,batch size 设置不合理,或者向量检索被放在推理服务内部串行执行。需要把模型输入的 token 数量和推理耗时放一起看。
这三类误判的共同点,是没有把某一跳的“现象”和“原因”区分开。链路分析的数据粒度越细,就越容易区分。
6.3 我建议的治理顺序
如果你接手一个还没有做链路分析的千万 QPS 系统,不要想着一次全部搞定。我建议按以下顺序推进:
第一步,先统一 trace ID 的生成和透传,把日志、监控、链路追踪系统串起来。这一步收益最大,成本相对可控。
第二步,先给核心接口和核心依赖打点。不要一开始就追求所有接口全量接入,先把流量最高的前 20 个接口覆盖掉。
第三步,建立慢查询和错误自动记录策略。保证每次出现故障,都能从链路系统里翻出完整调用链。
第四步,做局部链路压测,摸清每个节点的容量基线,形成容量表。
第五步,再接入语义缓存、Agent 编排这类智能组件。每加一个新节点,都要先想清楚它的 trace、采样、监控和降级方案,不要先上线再补可观测性。
很多问题看起来是功能不支持,实际上往往是前置链路没有设计好。千万 QPS 不是某个黑科技能带来的,而是把链路拆清楚、把参数调明白、把故障定位时间降下来之后,系统自然呈现的一个结果。
我个人更建议先从一条最简单的核心查询链路开始。把它从入口到出口每一跳都打点完整,再逐步扩展到智能层。链路分析做得越早,后续加模型、加 Agent、加缓存的时候,就越不会手足无措。