业务系统性能改造手册 · 请求减量

识别并量化无效请求

2026-08-052 min read请求减量
摘要

把入口请求、服务内部调用和数据库访问串成请求成本链,用业务意图与证据区分合理并发、必要重试和可消失请求,并建立后续减量验收口径。

订单状态接口 QPS 很高,能不能先把轮询频率降下来?同一个订单在短时间内被查询多次,看起来像重复请求,但其中可能包含用户刷新、客服查询、支付回调后的状态确认,也可能只是前端组件重复挂载。只凭 URL 和时间间隔,很容易把合理并发和无效放大混在一起。

上一章已经固定实验条件、构造负载并建立了瓶颈证据链。进入请求减量后,小编更愿意先做一件不那么“像优化”的工作:让每个入口请求能够回答是谁发起、想完成什么业务动作、触发了多少内部调用和数据库工作。缺少这层对应关系,删请求只能靠猜。

本文停在识别与量化。缓存怎样设计留到 02-02,并发回源怎样合并留到 02-03

一、QPS 只能说明请求多,不能说明请求无效

请求是否值得保留,取决于业务意图和它发生时的状态。同一用户同时打开两个订单详情页,两次查询路径相同,却服务于两个不同订单;两个客服同时查看同一订单,也可能是合理并发。反过来,一个页面组件在一次渲染中对同一订单发出三次详情查询,即使每次都返回成功,也没有创造三份业务价值。

先把“无效请求”收窄成可验证定义:在给定业务语义、状态和时间窗口内,去掉某次请求后,用户可见结果、业务不变量与已承诺的时效都不发生不可接受变化,并且系统工作量确实减少。这个定义要求证据,不能从“看起来重复”直接跳到删除。

可以先把候选分成几类,但分类只是调查入口:

  • 重复:同一业务意图被多次表达,后续请求没有带来新的状态或一致性要求;
  • 过期:请求到达时,它所服务的页面、任务或上游操作已经取消或被新请求取代;
  • 过度查询:业务只需要少量字段、较低频率或较窄范围,却读取了更多数据;
  • 放大调用:一个入口请求在服务内部重复调用同一依赖,或产生远超业务动作所需的数据库访问。

超时重试不能直接归入无效。前一次请求是否执行成功可能未知,重试也许是调用方履行可靠性责任的一部分。我们要记录重试链和重复工作,再判断哪些成本能通过后续设计消失。

二、先给请求补上业务意图

请求是否无效要由业务意图状态和一致性要求判断不能只看 URL 与时间间隔 按 URL、用户和固定时间窗去重,误伤风险很高。订单状态查询和订单详情查询即使指向同一订单,业务动作也不同;同一路径携带不同一致性要求,也不能算同一意图。

一条可用于盘点的意图记录,至少包含这些字段:

yaml
1requestIntent: 2 occurredAt: ${TIMESTAMP} 3 experimentId: ${EXPERIMENT_ID_OR_NONE} 4 requestId: ${REQUEST_ID} 5 traceId: ${TRACE_ID} 6 source: 7 application: ${CALLER} 8 channel: ${WEB_APP_JOB_OR_SERVICE} 9 screenOrJob: ${SCREEN_OR_JOB_NAME} 10 actor: 11 type: ${USER_SERVICE_OR_JOB} 12 pseudonymousId: ${HASHED_ACTOR_ID} 13 action: ${LIST_DETAIL_CREATE_OR_STATUS} 14 resource: 15 type: order 16 businessKeyHash: ${HASHED_ORDER_OR_QUERY_KEY} 17 normalizedInputHash: ${HASH_WITH_VOLATILE_FIELDS_REMOVED} 18 consistencyRequirement: ${CURRENT_SNAPSHOT_OR_BOUNDED_STALENESS} 19 parentOperationId: ${USER_ACTION_OR_UPSTREAM_OPERATION_ID} 20 retry: 21 retryOf: ${PREVIOUS_REQUEST_ID_OR_NONE} 22 attempt: ${ATTEMPT} 23 reason: ${TIMEOUT_CONNECTION_FAILURE_OR_OTHER} 24 lifecycle: 25 canceledBeforeStart: ${TRUE_OR_FALSE} 26 supersededBy: ${REQUEST_ID_OR_NONE}

businessKeyHashnormalizedInputHash 用于关联,不应把订单号、用户标识或敏感查询条件直接塞进高基数指标标签。明细放在有访问控制和保留期的事件日志里;聚合指标只保留接口、调用来源、业务动作和分类等有限维度。

2.1 “同一业务意图”需要项目自己的规则

可以把候选指纹写成:

text
1调用来源 + 业务动作 + 资源键 + 规范化输入 + 一致性要求 + 业务状态

时间窗口只负责限制比较范围,不负责证明重复。窗口应该来自业务行为,例如页面一次交互的生命周期、一次批处理任务或订单状态允许变化的间隔。没有真实行为数据时保留 ${DEDUP_CANDIDATE_WINDOW},不要发明一个通用秒数。

业务状态尤其容易漏掉。两次状态查询之间订单发生了变化,第二次请求就可能有价值;两次详情查询的一致性要求不同,也不能合并成一个意图。候选生成器应保留状态版本、更新时间或其他可核验依据,无法取得时把结论标成“待验证”。

2.2 重试链与合理并发要单独记录

识别重试,优先使用调用方生成的操作 ID、retryOfattempt,而不是在服务端看到相似请求就猜。前一次超时但服务端已经完成写入时,后一次请求可能造成重复工作;前一次根本没有到达服务端时,重试则承担了必要恢复。

合理并发也不能仅靠“来自不同 requestId”放行。需要看调用来源、用户动作、资源键和消费方:两个独立用户查看同一热门订单,是两个业务意图;同一页面的两个组件为展示相同字段各查一次,可能是一个意图被前端结构放大。证据不足时先保留,删错请求的成本通常高于多收集一轮数据。

三、把入口请求一直追到数据库成本

减量空间必须沿入口内部调用到数据库的完整成本链量化 只统计入口 QPS,会漏掉服务内部的放大。一条订单列表请求可能调用多次详情查询,也可能执行一条主查询后为每行补一次关联数据。反过来,多条入口请求也可能命中同一份已有结果。减量空间要沿成本链计算。

text
1业务意图 2 └─> 入口请求 3 ├─> 服务内部调用 4 │ ├─> Redis 操作 5 │ ├─> 下游 HTTP / RPC 6 │ └─> 消息投递 7 └─> 数据库访问 8 ├─> SQL 执行次数 9 ├─> 返回行数 10 └─> 扫描行数或等价代价证据

每层通过 traceId、父子 span、显式调用标识或等价关联键连接。没有 trace 系统时,也可以在入口过滤器、客户端拦截器和数据访问层写结构化计数事件;但要统一采样窗口和运行编号,避免把不同请求的成本拼在一起。

成本事件可以采用下面的通用结构:

json
1{ 2 "occurredAt": "${TIMESTAMP}", 3 "experimentId": "${EXPERIMENT_ID}", 4 "requestId": "${REQUEST_ID}", 5 "traceId": "${TRACE_ID}", 6 "parentId": "${PARENT_ID}", 7 "layer": "entry|internal|database|redis|downstream|message", 8 "operation": "${NORMALIZED_OPERATION}", 9 "count": 1, 10 "durationMs": "${OBSERVED_DURATION}", 11 "rowsReturned": "${VALUE_OR_UNKNOWN}", 12 "rowsExamined": "${VALUE_OR_UNKNOWN}", 13 "terminalState": "${SUCCESS_REJECTED_ERROR_TIMEOUT}", 14 "sampled": "${TRUE_OR_FALSE}" 15}

rowsExamined 并非所有数据库和采集方式都能直接提供,拿不到就记 unknown,不要用返回行数冒充扫描成本。耗时、调用次数、返回行数和资源等待分别表达不同代价,也不应被加成一个没有单位的“成本分”。

3.1 用放大系数描述问题,不用它替代判断

在同一采样窗口内,可以计算几组有明确单位的比值:

text
1入口放大 = entry_requests / logical_intents 2内部调用放大 = internal_calls / entry_requests 3SQL 放大 = sql_executions / entry_requests 4下游放大 = downstream_calls / entry_requests

分母为零时该窗口不计算。比值应按业务动作和调用来源拆分,并同时保留分子、分母与采样覆盖率。较高的 SQL 放大值不自动等于有问题;某个订单提交本来就可能需要多条语义不同的语句。它的价值是指出工作量集中在哪里,再回到业务意图检查每一份工作是否必要。

四、从候选到“可消失请求”要经过证据门禁

候选请求可以按相同意图指纹和调查窗口聚类。每一组保留第一条请求、后续请求、期间业务状态变化、消费方和完整成本链,然后逐项判断:

  1. 后续请求是否服务于同一个用户动作或上游操作;
  2. 输入、一致性要求和业务状态是否允许复用前一次结果;
  3. 去掉后续请求是否影响用户可见结果、业务不变量或时效承诺;
  4. 请求是否属于失败恢复、独立用户并发、审计或安全校验;
  5. 入口减少后,内部调用和数据库工作是否也会随之减少。

前三项无法核验时,保持 candidate。明确不应删除的标为 necessary,避免重复调查;证据足够且存在可行减量方向的,才标为 validated_waste。这三个状态比直接贴“重复请求”标签更诚实。

对于客户端重试,还要补一项:前一次执行的服务端终态是否可知。如果未知,当前问题往往是幂等与结果查询能力不足,而不是调用方“多发了一次”。本文只记录这条风险,不展开实现。

五、建立能验收减量收益的计数器

后续改造不能只比较入口 QPS。业务流量本身可能变化,入口下降也可能是请求失败或用户流失造成的。验收计数器需要同时守住业务意图、入口和下游成本。

建议在相同实验协议和负载模型下记录:

yaml
1requestReductionCounters: 2 window: ${START_END_TIME} 3 experimentId: ${EXPERIMENT_ID} 4 action: ${ACTION} 5 source: ${CALLER_OR_CHANNEL} 6 coverage: 7 totalEntryRequests: ${COUNT} 8 requestsWithIntentKey: ${COUNT} 9 requestsWithCompleteCostChain: ${COUNT} 10 intent: 11 logicalIntents: ${COUNT} 12 necessaryRequests: ${COUNT} 13 candidateRequests: ${COUNT} 14 validatedWasteRequests: ${COUNT} 15 entry: 16 attempted: ${COUNT} 17 completed: ${COUNT} 18 terminalCounts: ${COUNTS} 19 downstreamCost: 20 internalCalls: ${COUNT} 21 sqlExecutions: ${COUNT} 22 redisOperations: ${COUNT} 23 downstreamCalls: ${COUNT} 24 messagePublishes: ${COUNT} 25 businessGuardrails: 26 completedBusinessActions: ${COUNT} 27 businessRejections: ${COUNT} 28 userVisibleErrors: ${COUNT} 29 freshnessOrStateViolations: ${COUNT_OR_QUERY}

在后续改造前后,至少要确认逻辑意图和业务完成量可比较,再观察入口请求、SQL、Redis 与下游调用是否按预期减少。validatedWasteRequests 下降但 logicalIntents 也同步下降,不能直接算收益;入口减少而 SQL 数量不变,则说明成本可能被转移或服务内部仍有放大。

采集覆盖率同样要进入结论。只有少量请求带意图键,或者成本链大量断裂时,可以列候选,不能给出精确减量比例。

六、交付一张能推动下一步的请求清单

交付时把证据整理成逐项可追溯的清单:

yaml
1wastefulRequestInventory: 2 - id: ${CANDIDATE_ID} 3 status: ${CANDIDATE_NECESSARY_OR_VALIDATED_WASTE} 4 action: ${ACTION} 5 source: ${CALLER_SCREEN_OR_JOB} 6 intentFingerprintRule: ${RULE_REFERENCE} 7 investigationWindow: ${WINDOW_AND_REASON} 8 observedPattern: ${FACT_WITH_QUERY_OR_FILE} 9 businessStateEvidence: ${STATE_VERSION_OR_UNKNOWN} 10 retryEvidence: ${RETRY_CHAIN_OR_NONE} 11 costChain: 12 entryRequests: ${COUNT} 13 internalCalls: ${COUNT} 14 sqlExecutions: ${COUNT} 15 downstreamCalls: ${COUNT} 16 whyItMayDisappear: ${REASON} 17 evidenceAgainstRemoval: ${REASON_OR_NONE} 18 misclassificationRisk: ${RISK} 19 candidateDirection: ${CACHE_REUSE_DELAY_COMBINE_OR_CALLER_FIX} 20 acceptanceCounters: ${COUNTER_NAMES} 21 openQuestions: 22 - ${QUESTION}

candidateDirection 只说明下一步调查方向,不在这篇里决定缓存、合并窗口或具体代码。比如“可能复用读取结果”会交给下一篇评估数据时效和一致性;“同一时刻并发回源”则留给 02-03 检查合并边界。

这份清单的价值不在于列出最多的“坏请求”,而在于让每个候选都能回到业务意图、状态证据和成本链。没有这些材料,请求多只是现象;材料齐全后,我们才能知道某次请求消失会减少多少入口、SQL 和下游工作,又会不会误伤业务结果。