业务系统性能改造手册 · 请求减量
识别并量化无效请求
把入口请求、服务内部调用和数据库访问串成请求成本链,用业务意图与证据区分合理并发、必要重试和可消失请求,并建立后续减量验收口径。
订单状态接口 QPS 很高,能不能先把轮询频率降下来?同一个订单在短时间内被查询多次,看起来像重复请求,但其中可能包含用户刷新、客服查询、支付回调后的状态确认,也可能只是前端组件重复挂载。只凭 URL 和时间间隔,很容易把合理并发和无效放大混在一起。
上一章已经固定实验条件、构造负载并建立了瓶颈证据链。进入请求减量后,小编更愿意先做一件不那么“像优化”的工作:让每个入口请求能够回答是谁发起、想完成什么业务动作、触发了多少内部调用和数据库工作。缺少这层对应关系,删请求只能靠猜。
本文停在识别与量化。缓存怎样设计留到 02-02,并发回源怎样合并留到 02-03。
一、QPS 只能说明请求多,不能说明请求无效
请求是否值得保留,取决于业务意图和它发生时的状态。同一用户同时打开两个订单详情页,两次查询路径相同,却服务于两个不同订单;两个客服同时查看同一订单,也可能是合理并发。反过来,一个页面组件在一次渲染中对同一订单发出三次详情查询,即使每次都返回成功,也没有创造三份业务价值。
先把“无效请求”收窄成可验证定义:在给定业务语义、状态和时间窗口内,去掉某次请求后,用户可见结果、业务不变量与已承诺的时效都不发生不可接受变化,并且系统工作量确实减少。这个定义要求证据,不能从“看起来重复”直接跳到删除。
可以先把候选分成几类,但分类只是调查入口:
- 重复:同一业务意图被多次表达,后续请求没有带来新的状态或一致性要求;
- 过期:请求到达时,它所服务的页面、任务或上游操作已经取消或被新请求取代;
- 过度查询:业务只需要少量字段、较低频率或较窄范围,却读取了更多数据;
- 放大调用:一个入口请求在服务内部重复调用同一依赖,或产生远超业务动作所需的数据库访问。
超时重试不能直接归入无效。前一次请求是否执行成功可能未知,重试也许是调用方履行可靠性责任的一部分。我们要记录重试链和重复工作,再判断哪些成本能通过后续设计消失。
二、先给请求补上业务意图
按 URL、用户和固定时间窗去重,误伤风险很高。订单状态查询和订单详情查询即使指向同一订单,业务动作也不同;同一路径携带不同一致性要求,也不能算同一意图。
一条可用于盘点的意图记录,至少包含这些字段:
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}businessKeyHash 和 normalizedInputHash 用于关联,不应把订单号、用户标识或敏感查询条件直接塞进高基数指标标签。明细放在有访问控制和保留期的事件日志里;聚合指标只保留接口、调用来源、业务动作和分类等有限维度。
2.1 “同一业务意图”需要项目自己的规则
可以把候选指纹写成:
1调用来源 + 业务动作 + 资源键 + 规范化输入 + 一致性要求 + 业务状态时间窗口只负责限制比较范围,不负责证明重复。窗口应该来自业务行为,例如页面一次交互的生命周期、一次批处理任务或订单状态允许变化的间隔。没有真实行为数据时保留 ${DEDUP_CANDIDATE_WINDOW},不要发明一个通用秒数。
业务状态尤其容易漏掉。两次状态查询之间订单发生了变化,第二次请求就可能有价值;两次详情查询的一致性要求不同,也不能合并成一个意图。候选生成器应保留状态版本、更新时间或其他可核验依据,无法取得时把结论标成“待验证”。
2.2 重试链与合理并发要单独记录
识别重试,优先使用调用方生成的操作 ID、retryOf 和 attempt,而不是在服务端看到相似请求就猜。前一次超时但服务端已经完成写入时,后一次请求可能造成重复工作;前一次根本没有到达服务端时,重试则承担了必要恢复。
合理并发也不能仅靠“来自不同 requestId”放行。需要看调用来源、用户动作、资源键和消费方:两个独立用户查看同一热门订单,是两个业务意图;同一页面的两个组件为展示相同字段各查一次,可能是一个意图被前端结构放大。证据不足时先保留,删错请求的成本通常高于多收集一轮数据。
三、把入口请求一直追到数据库成本
只统计入口 QPS,会漏掉服务内部的放大。一条订单列表请求可能调用多次详情查询,也可能执行一条主查询后为每行补一次关联数据。反过来,多条入口请求也可能命中同一份已有结果。减量空间要沿成本链计算。
1业务意图
2 └─> 入口请求
3 ├─> 服务内部调用
4 │ ├─> Redis 操作
5 │ ├─> 下游 HTTP / RPC
6 │ └─> 消息投递
7 └─> 数据库访问
8 ├─> SQL 执行次数
9 ├─> 返回行数
10 └─> 扫描行数或等价代价证据每层通过 traceId、父子 span、显式调用标识或等价关联键连接。没有 trace 系统时,也可以在入口过滤器、客户端拦截器和数据访问层写结构化计数事件;但要统一采样窗口和运行编号,避免把不同请求的成本拼在一起。
成本事件可以采用下面的通用结构:
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 用放大系数描述问题,不用它替代判断
在同一采样窗口内,可以计算几组有明确单位的比值:
1入口放大 = entry_requests / logical_intents
2内部调用放大 = internal_calls / entry_requests
3SQL 放大 = sql_executions / entry_requests
4下游放大 = downstream_calls / entry_requests分母为零时该窗口不计算。比值应按业务动作和调用来源拆分,并同时保留分子、分母与采样覆盖率。较高的 SQL 放大值不自动等于有问题;某个订单提交本来就可能需要多条语义不同的语句。它的价值是指出工作量集中在哪里,再回到业务意图检查每一份工作是否必要。
四、从候选到“可消失请求”要经过证据门禁
候选请求可以按相同意图指纹和调查窗口聚类。每一组保留第一条请求、后续请求、期间业务状态变化、消费方和完整成本链,然后逐项判断:
- 后续请求是否服务于同一个用户动作或上游操作;
- 输入、一致性要求和业务状态是否允许复用前一次结果;
- 去掉后续请求是否影响用户可见结果、业务不变量或时效承诺;
- 请求是否属于失败恢复、独立用户并发、审计或安全校验;
- 入口减少后,内部调用和数据库工作是否也会随之减少。
前三项无法核验时,保持 candidate。明确不应删除的标为 necessary,避免重复调查;证据足够且存在可行减量方向的,才标为 validated_waste。这三个状态比直接贴“重复请求”标签更诚实。
对于客户端重试,还要补一项:前一次执行的服务端终态是否可知。如果未知,当前问题往往是幂等与结果查询能力不足,而不是调用方“多发了一次”。本文只记录这条风险,不展开实现。
五、建立能验收减量收益的计数器
后续改造不能只比较入口 QPS。业务流量本身可能变化,入口下降也可能是请求失败或用户流失造成的。验收计数器需要同时守住业务意图、入口和下游成本。
建议在相同实验协议和负载模型下记录:
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 数量不变,则说明成本可能被转移或服务内部仍有放大。
采集覆盖率同样要进入结论。只有少量请求带意图键,或者成本链大量断裂时,可以列候选,不能给出精确减量比例。
六、交付一张能推动下一步的请求清单
交付时把证据整理成逐项可追溯的清单:
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 和下游工作,又会不会误伤业务结果。