一、口径偏差:监控值与用户体感的系统性背离
某代购集运系统的建单接口,监控显示平均耗时 210 毫秒、P99 约 300 毫秒;同期用户投诉提交需三秒以上。对原始请求逐条核对后确认,真实 P99 为 3.2 秒。
偏差的来源是埋点位置:计时起始于业务处理函数首行,而请求在此之前已经历网关转发、限流器等待、工作线程排队、连接池获取四个环节。这些等待全部未进入统计口径。监控测得的是执行时长,用户感知的是排队加执行的总和。
二、耗时结构:五段与两个加数
| 段位 | 典型耗时 | 监控可见性 |
| 客户端到网关 | 20-300ms | 不可见 |
| 网关前置(限流、鉴权) | 0-3000ms | 不可见 |
| 等工作线程 | 0-数千 ms | 不可见 |
| 等下游资源 | 0-数千 ms | 部分可见 |
| 业务代码执行 | 20-200ms | 唯一被统计段 |
该案例中五段耗时分别为 120 / 2800 / 300 / 0 / 80 毫秒,排队占比 97%。排队占 97% 时,将执行时长优化 50%,总耗时仅下降约 1.5%:执行从 80 毫秒降至 40 毫秒后,P99 由 3.20 秒变为 3.16 秒,用户无感知。
三、定位方法:利特尔法则的减法应用
在途请求数 = 到达率 × 单请求总耗时。以到达率 145 单/秒、体感 3.2 秒计:
在途请求数 = 145 × 3.2 ≈ 464
线程池上限 128,差额 336 个请求位于限流器、网关与连接池队列中。进一步核对容量:线程池 128 × 单次占用 0.8 秒 = 160 单/秒,高于到达率;限流器放行 120 单/秒且超出部分原地等待,每秒净积压 25 个。瓶颈判定为放行速率而非线程池与业务代码,扩容与慢查询优化均非对症手段。
四、实现:中间件级分离度量
import time from contextvars import ContextVar queue_wait_var = ContextVar("queue_wait_ms", default=0.0) def ms_now() -> float: return time.time() * 1000.0 class QueueWaitMiddleware: """在业务代码之前,计算请求的排队时长""" def __init__(self, app): self.app = app async def __call__(self, scope, receive, send): if scope["type"] != "http": return await self.app(scope, receive, send) headers = {k.decode(): v.decode() for k, v in scope["headers"]} arrived = headers.get("x-arrived-at") try: arrived_ms = float(arrived) if arrived else ms_now() except ValueError: arrived_ms = ms_now() wait_ms = max(0.0, ms_now() - arrived_ms) token = queue_wait_var.set(wait_ms) begin = ms_now() try: return await self.app(scope, receive, send) finally: observe_wait(scope.get("path", "-"), wait_ms) observe_service(scope.get("path", "-"), ms_now() - begin) queue_wait_var.reset(token)
排队与执行两条指标分开上报、分别设阈值:排队占大头判定为容量与放行策略问题,执行占大头判定为代码与下游问题。
五、处置原则
- 限流器:超限请求要么快速失败并返回等待时长,要么将等待时长单独打点并设上限,不采用无指标的原地等待。
- 重试:次数与退避均设上限;重试会放大到达率,其等待时间计入排队指标。
- 池容量:按「到达率 × 目标总耗时」估算并留三成余量,不按经验值配置。
- 上游劣化:本地容量随单次占用上升而下降,应主动限流,避免队列积压超过超时线。
六、验收标准
每月抽取一条用户投诉,比较「用户等待时长」与「监控上报值」的差额。**该差额若长期存在,说明指标体系仅覆盖执行段,需要补齐排队度量。