ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

QAS 质量属性场景:把非功能需求 NFR 写成可验证指标

QAS 质量属性场景:把非功能需求 NFR 写成可验证指标

1. 为什么"性能要好"这句话在评审会上永远吵不出结果

NFR 规格化这件事,我最早是在一次需求评审上被逼着正视的。产品在需求文档里写"页面加载要快、系统要稳定、高峰期不能崩",开发点头说没问题,测试问"那我怎么判定不通过",运维问"那我告警阈值设多少",一圈问下来,会议室安静了。这三个词背后没有任何一个能被写成测试用例,也没有任何一个能被写成监控指标。后来我们内部把研发流程这套东西叫 DevFlow,其中专门划出一块来治理非功能需求,核心工具就是 Quality Attribute Scenarios,也就是质量属性场景,业内简称 QAS。它的作用非常朴素:把"性能要好"这种形容词,改写成"谁在什么情况下对哪个部分做了什么,系统必须在多久内给出什么结果"这种可以被测量、被验收、被追溯的句子。这篇文章适合三类人看:正在被 NFR 扯皮折磨的产品和项目经理、需要把质量要求翻译成用例的测试同学、以及要给 SLO 定阈值的后端和运维同学。哪怕你完全没有接触过质量属性这套方法论,看完也能上手把手上那几条模糊要求改写掉。

1.1 一个形容词,至少三种人的三种想象

"性能要好"这四个字,落到不同角色脑子里呈现的画面完全不同。开发想的是"我这个接口 P95 控制在 200 毫秒以内",运维想的是"高峰期 CPU 别打满、连接池别耗尽",产品想的是"用户点下去不要转圈超过一秒",而测试拿到这句话只能干瞪眼——他没有基准,没有场景,没有判定条件,最后大概率写一条"页面能打开"的冒烟用例交差。这不是谁不负责,是这句话本身的信息量太低了,低到无法承载任何工程决策。

更要命的是,这三种想象之间经常是互相矛盾的。开发为了把接口 P95 压到 200 毫秒,加了本地缓存,结果数据一致性窗口拉长到 30 秒;运维为了保证稳定,把超时时间设得很短,结果高峰期大量请求被熔断;产品看到用户投诉"偶尔白屏",回来要求"必须绝对稳定"。三边都在做正确的事,但因为没有共同的度量语言,最后变成互相甩锅。

QAS 解决的第一个问题就是统一语言。它不负责告诉你 P95 应该是多少,它负责强制你把"多少"这个数字写出来,并且写清楚这个数字在什么条件下、对谁、针对哪个部分成立。一旦数字被写出来,讨论就从"我觉得不够快"变成了"这个数字合不合理",性质完全变了。

1.2 模糊 NFR 的真实成本,比想象中高

很多人觉得 NFR 写不清楚无非就是文档难看点,实际成本远不止。我梳理过我们团队踩过的几类:验收阶段扯皮,上线前一周产品说"感觉还是慢",开发说"需求里没写具体指标",最后靠加班做了一轮没有基准的优化,改完也不知道有没有变好;容量规划失控,因为没有明确的峰值吞吐目标,采购和扩容全靠拍脑袋,要么买多了浪费,要么大促当天被打穿;告警阈值玄学,运维把 CPU 告警设在 80%,结果业务本身是 CPU 密集型,天天告警天天被无视,真出事那次反而没人看;回归测试没有基线,每次发版跑完压测,报告上写"平均响应 340ms",然后没人知道这个 340ms 是好还是坏,因为上一版是多少没人记录。

这几类问题的共同根因是:质量要求没有变成一个可以被证伪的命题。QAS 的价值就在于它天然是可证伪的——它写明了度量项和阈值,达不到就是达不到,没有解释空间。这听起来很冷酷,但对工程团队来说是好事,因为模糊带来的从来不是宽容,而是无休止的争论。

1.3 QAS 到底是什么:一句话拆成六个槽位

Quality Attribute Scenario 的结构不复杂,一共六个要素:刺激源(Source)、刺激(Stimulus)、制品(Artifact)、环境(Environment)、响应(Response)、响应度量(Response Measure)。翻译成人话就是:谁,在什么情况下,对系统的哪个部分,做了什么,系统应该怎么反应,这个反应要在什么指标上达到什么值。

我习惯用点外卖来类比。模糊说法是"送餐要快"。QAS 化的说法是:一位住在城郊的顾客(刺激源),在周五晚高峰(环境)下了一单三公里外的订单(刺激),配送系统(制品)应当在骑手接单后(响应)保证送达时间不超过 45 分钟(响应度量)。你看,这么一说,能不能做到、怎么考核、超时了算谁的责任,全都清楚了。而且它自动暴露了一个隐含前提:如果这个顾客住在二十公里外,45 分钟就是不合理的,场景必须重新定义。

这个类比里最重要的启示是:环境这个槽位不是凑数的。绝大多数"性能要好"翻车,都是因为漏了环境约束——把工作日白天的指标和秒杀瞬间的指标混为一谈,把冷启动的首请求和缓存预热后的请求混为一谈。

2. 六要素逐项拆解与写法规范

QAS 看着简单,但真让你坐下来写,六成的人会在"响应度量"这一栏卡住,三成的人会把"制品"写成整个系统。这一章我按我们团队实际用的规范,把这六个槽位逐个拆开讲,每个都给出合格写法和典型反例。

2.1 Source 与 Stimulus:把"谁、在什么情况下"钉死

刺激源要写的是"谁发起了这个刺激",注意是角色而不是具体某个人。常见的刺激源包括:终端用户、内部定时任务、上游服务调用方、运维人员、攻击流量、第三方回调。区分刺激源的意义在于,不同来源的请求,系统的处理策略本来就该不一样——用户请求要保体验,定时任务可以降级排队,运维操作要能中断。

刺激本身要写清楚动作的性质和规模。写"用户访问系统"太笼统,写"用户提交订单"就具体多了,如果能补上规模更好,比如"用户在促销开始后 10 秒内集中提交订单"。规模信息非常关键,因为它直接决定了后面度量的量级。

这里有个容易犯的错:把刺激写成功能需求。比如写成"用户点击下单按钮,系统创建订单并扣减库存",这就跑到功能需求的地盘去了。QAS 里的刺激只描述"发生了什么事件",不描述"系统应该完成什么业务逻辑",业务逻辑是功能需求的事,QAS 只关心这个事件引发的质量表现。

我的经验是,写刺激的时候多问一句"这个动作的量级和突发性如何"。同样是"提交订单",平时是每秒十几个,大促是每秒几千个,这两个是完全不同的两个场景,必须分开写两条 QAS,不能合并。

2.2 Artifact 与 Environment:划清边界,防止"全场景通吃"

制品指的是这个刺激作用在系统的哪个部分。这一栏最容易写得太粗,我看过大量写成"整个系统"的 QAS,这种写法基本等于没写,因为整个系统的度量值没法归因,出问题了你不知道是网关、应用还是数据库拖的后腿。

合格的制品粒度应该细到"能被单独观测和优化"的程度。比如"订单提交接口"、"商品详情页的读链路"、"订单库的主库写入"、"消息队列的消费端"。粒度太细也不好,比如写到某个具体函数的执行时间,那已经不是架构层面的质量属性了,那是性能剖析的范畴。

环境这一栏是我认为最被低估的。它要回答的是"在什么条件下"。常见的环境维度有:负载水平(正常负载 / 峰值负载 / 超载)、系统状态(全部正常 / 部分节点故障 / 降级模式)、数据规模(冷缓存 / 热缓存 / 数据量增长到千万级)、时间窗口(日常 / 大促 / 批量任务运行期间)。环境写得越具体,这条 QAS 的可验证性就越强,同时也越容易暴露"其实我们根本没考虑过降级状态下应该是什么表现"这种盲区。

提示:如果一条 QAS 的环境栏写的是"任何情况下",那大概率是没想清楚。真实系统不存在"任何情况下都成立"的质量承诺,硬要写只会让这条需求失去约束力。

2.3 Response 与 Response Measure:可度量才是需求,不可度量只是愿望

响应描述的是系统在受到刺激后表现出的行为,注意是可观测的行为,不是内部实现。比如"接口返回成功响应"、"请求被排队而非直接拒绝"、"系统自动切换到备用链路"、"返回降级内容并附带提示"。响应里可以包含一定的策略意图,比如"优先保证读请求,写请求进入队列",这实际上是在表达质量属性之间的取舍。

响应度量是整条 QAS 的灵魂,也是最难写的一栏。它必须包含三个东西:度量项、度量值、度量口径。度量项就是指标名,比如响应时间、吞吐量、错误率、恢复时间、资源占用率。度量值是具体数字。度量口径最容易被忽略,但它决定了这个数字到底怎么算——响应时间是平均值还是 P95 还是 P99?错误率的分母是总请求数还是成功请求数?恢复时间从故障发生算起还是从监控告警算起?

我见过太多因为口径不一致导致的争议。开发说"我们接口平均响应 80 毫秒",测试说"我压测出来 P99 是 1.2 秒",两边都没说谎,就是口径不同。所以响应度量这一栏,我要求必须写清楚分位数和统计窗口,比如"P95 响应时间不超过 800 毫秒,统计窗口为连续 5 分钟"。

下面是我们团队内部用的 QAS 模板,用 YAML 描述,好处是能直接进需求库做结构化检索:

qas_id: NFR-PERF-014 quality_attribute: performance source: 终端用户 stimulus: 在促销开始后的 60 秒内集中刷新商品详情页 artifact: 商品详情读链路(含缓存层与应用层,不含支付链路) environment: 峰值负载,缓存命中率不低于 85%,全量节点在线 response: 正常返回详情内容;超时请求返回上一次缓存快照而非错误页 response_measure: metric: 服务端响应时间 percentile: P95 threshold: "<= 300ms" window: "连续 3 分钟" error_rate: "5xx 比例 <= 0.1%"

这份模板看着有点啰嗦,但好处是一次性把口径全定死了。上线之后测试照着跑,运维照着配告警,谁都不用再问"你说的这个 300 毫秒是平均值吗"。

2.4 一条完整 QAS 的逐行注释与反例对照

光看模板还是抽象,我拿一条真实的改写过程走一遍。原始需求原文是:「搜索结果要快,不能卡」。

改写后的版本是:当终端用户(刺激源)在搜索结果页发起翻页请求(刺激)时,搜索服务(制品)在缓存命中率正常的日常负载环境下(环境),应当在 500 毫秒内返回结果(响应),且 P95 响应时间不超过 500 毫秒、P99 不超过 1200 毫秒,5xx 错误率低于 0.05%(响应度量)。

对比一下差异在哪。原文的"快"没有主体,改写后明确了是翻页请求而不是首屏搜索——这两者的性能特征完全不同,首屏可能要走向量检索,翻页基本是缓存或游标。原文没有环境,改写后限定了缓存命中正常,这实际上隐含了另一条 QAS:缓存未命中时应该是什么表现。原文没有口径,改写后给了 P95 和 P99 双阈值,还附带错误率。

我特别建议给每条 QAS 都配一个反例,也就是"什么情况下这条 QAS 判定不通过"。这招是从测试同学那里学来的,效果出奇地好,因为写反例的过程会逼着你重新审视度量值是不是定得太松或太紧。上面这条的反例就是:连续 3 分钟内 P95 超过 500 毫秒,或出现一次持续 30 秒以上的 5xx 比例超过 0.5%,即判定不通过。

3. 从形容词到可验证需求:实操改写流程

方法论讲完了,落到执行层面还是得有流程。我们内部沉淀了一套五步法,从收集原始噪声到挂接监控,每一步都有明确产出物。这套流程跑顺之后,一条 NFR 从提出到进入需求库大概需要 40 分钟,比反复开会扯皮划算太多。

3.1 第一步:把原始噪声归类成质量属性清单

先别急着写场景,第一步是把所有模糊表述收集起来。来源很杂:需求文档里的形容词、用户投诉、线上事故复盘、老板的口头要求、竞品对比。把它们原封不动贴到一个表里,不要在这一步做任何美化。

然后按质量属性归类。业界常见的分类大概有这些:性能(响应时间、吞吐量)、可用性(正常运行时间比例、故障恢复)、可靠性(错误率、数据一致性)、可扩展性(负载增长时的表现)、安全性(认证授权、数据保护)、可维护性(变更成本、部署频率)、易用性(任务完成率、操作步骤数)、可观测性(问题定位时间)。这个分类不用太死板,关键是让每条噪声都能找到归属。

归类完之后你会发现一个有意思的现象:大量噪声其实集中在性能 and 可用性这两类上,而可维护性、可观测性这类"不出事就没人管"的属性基本没人提。这时候可以主动补几条,比如"新成员上手到能独立提交变更需要多久"、"一次线上问题从告警到定位根因的平均耗时",这些同样是质量需求,只是平时没人用形容词表达出来。

这一步的产出物是一张归类表,每条噪声后面标注质量属性类别和提出人。我建议把提出人也记上,后面优先级排序和争议裁决时会用得到。

3.2 第二步:用场景优先级矩阵筛掉"都要"

归完类你会发现需求多到做不完,这时候必须排序。我们用的矩阵是两个维度:业务影响(高/中/低)和实现成本(高/中/低)。业务影响看的是这条质量属性失效会造成什么后果——直接影响收入的、影响核心流程可用性的算高;影响体验但不阻断的算中;纯优化性质的算低。实现成本不用精确估算,架构师凭经验给个量级就行。

排完之后重点做两件事。第一,明确放弃。低影响高成本的条目直接在文档里写"本期不做,理由是……",写下来比不写强一百倍,因为半年后一定有人重新提。第二,找出冲突对。性能和成本冲突、可用性和一致性冲突、安全性和易用性冲突,这三对是永恒的。找出冲突对之后,要在文档里明确当前的取舍方向,比如"本期优先保证读链路的响应时间,允许写链路有最多 3 秒的最终一致延迟"。

注意:取舍一定要写成明确的数字边界,不要写成"优先保证性能"。后者等于没取舍,出事的时候两边都能拿这句话当依据。

3.3 第三步:度量指标选型与参数计算

这一步是硬功夫,度量值不能拍脑袋。我按三类最常见的质量属性分别讲怎么算。

性能类的延迟预算分解。假设端到端目标是 P95 ≤ 800 毫秒,那就要把 800 毫秒拆到链路的每一跳上,每一跳都留出预算,加起来不能超过总额,通常还要留 10% 到 15% 的缓冲应对抖动。我们一个真实项目的分解表大致长这样:

链路环节预算(P95)说明
客户端渲染与网络往返150ms弱网环境按 4G 估算,不含 DNS 首次解析
接入层(负载均衡与网关)30ms含 TLS 握手摊销
鉴权与限流20ms走本地缓存,不查库
应用逻辑处理220ms含编排与两次下游调用
缓存读取10ms命中时生效,未命中走数据库
数据库查询280ms已含索引优化后的预期值
序列化与响应组装40ms大对象序列化是隐藏大头
缓冲余量50ms约占总预算 6%
合计800ms—

这张表最有价值的地方不是数字本身,而是它把"800 毫秒"这个目标变成了七个可以分别派给不同人的子目标。数据库那 280 毫秒归 DBA 管,应用逻辑那 220 毫秒归后端管,客户端 150 毫秒归前端管,谁超了谁负责优化,扯皮空间直接归零。

可用性类的停机时间换算。很多人对"几个 9"没有体感,换算成停机时间就直观了:

可用性目标年累计停机月累计停机适用场景举例
99.5%约 43.8 小时约 3.6 小时内部管理系统
99.9%约 8.76 小时约 43.2 分钟一般业务系统
99.95%约 4.38 小时约 21.6 分钟核心交易链路
99.99%约 52.6 分钟约 4.3 分钟强依赖型基础服务

计算方式很直白:一年 365 天乘以 24 小时等于 8760 小时,乘以(1 减可用性目标)就是允许的停机时长。月停机按 30 天 43200 分钟算。这里有个坑要提醒:这个数字是允许的总停机时长,包含计划内维护。如果你的发布流程每次都要停服 5 分钟,一个月发四次就把 99.9% 的额度用掉一半了,所以定了可用性目标之后,灰度发布和不停机部署就不是"最佳实践",而是硬性前置条件。

吞吐量类的峰值估算。这个必须结合业务量算,不能直接抄别人的 QPS。举个具体的:假设某业务日活 20 万,人均每天触发 6 次关键请求,那么日均请求量是 120 万次,平均 QPS 约等于 1200000 除以 86400,约 13.9。但这个数字毫无意义,因为流量不是均匀分布的。取峰值因子 8(可以根据历史监控数据反推,没有历史数据就取保守值),峰值 QPS 约 111。再留 3 倍容量余量应对突发和增长,目标定在 350 QPS 比较稳妥。

压测线程数也别拍脑袋,用 Little's Law 算:并发数等于 QPS 乘以平均响应时间。350 QPS 乘以 0.8 秒等于 280,也就是压测时至少要有 280 个并发才能打出目标吞吐。很多人压测时开 50 个线程就说"打不上去",其实是线程数不够,不是系统不行。

缓存命中率为什么要单独写一条 QAS。这个值得单独说。续上面的例子,假设数据库能扛 100 QPS 的读,缓存命中率 90% 时,数据库实际承受的读压力是 350 乘以 10% 等于 35 QPS,很安全。但如果命中率因为某个热点 key 失效掉到 70%,数据库压力变成 105 QPS,直接就超了。所以"缓存命中率不低于 85%"这条必须作为独立的质量需求写进 QAS,并且配独立的监控告警,而不能当成性能需求的附属品。

3.4 第四步:把 QAS 翻译成验收标准与测试用例

QAS 写完了如果只躺在需求库里,那它和原来的形容词没本质区别。必须往下翻译成两样东西:验收标准和测试用例。

验收标准是给产品和业务方看的,用自然语言写,比如"在模拟峰值负载的压测环境下,商品详情接口 P95 响应时间不超过 300 毫秒,5xx 错误率不超过 0.1%,连续压测 30 分钟不出现错误率突增"。这段话里包含了场景、指标、口径和持续时间,产品能看懂,也能据此决定是否签字。

测试用例是给测试同学执行的,要具体到工具配置和判定脚本。比如压测场景要写清楚:负载模型(阶梯加压还是恒定压力)、加压曲线(从 50 并发逐步加到 350 并发,每级持续 5 分钟)、数据准备(预置 10 万条商品数据,缓存预热到命中率 90%)、采集指标(P50/P95/P99、吞吐量、错误率、CPU 与内存)、通过条件。这里特别强调数据准备,我见过太多压测用的是几百条测试数据,跑出来飞快,上线后真实数据量一上来直接崩,这种压测还不如不做。

3.5 第五步:挂接监控与告警阈值

最后一步是让 QAS 在线上持续生效。做法是把响应度量直接映射成监控指标和告警规则。继续用商品详情的例子,SLO 配置文件大概长这样:

slo: name: product-detail-read-latency qas_ref: NFR-PERF-014 sli: metric: http_server_request_duration_seconds filter: 'route="/api/product/detail" and status < 500' aggregation: histogram_quantile(0.95, ...) objective: target: 0.3 window: 5m alerting: - name: latency-burn-fast condition: 'p95 > 0.3 for 3m' severity: warning runbook: RB-0142 - name: latency-burn-critical condition: 'p95 > 0.6 for 5m' severity: critical runbook: RB-0142

注意这里的告警条件直接引用了 QAS 的编号和阈值,这就形成了追溯关系——告警响了,值班的人顺着编号就能查到这条需求的完整背景、当初为什么定这个值、对应哪条测试用例。

告警阈值的设计有个经验:不要只设一条线。设两条,一条是"接近阈值"(比如 1 倍目标值)作为预警,一条是"明确超标"(比如 2 倍目标值)作为紧急。只设一条线的结果要么太吵要么太钝,两条线能给你留出介入的窗口。

4. 可追溯性设计:让每条 QAS 都能被追到

规格化只做了一半的工作,另一半是追溯。没有追溯的规格文档,三个月后就是一堆没人看的表格。这一章讲我们怎么把 QAS 挂进整条研发链路。

4.1 编号体系与双向追溯矩阵

编号规则看着是小事,实际上是追溯的基础。我们的规则是NFR-<属性缩写>-<三位序号>,属性缩写用 PERF、AVAIL、REL、SEC、MAINT、OBS 这类固定词表。编号一旦分配就永久保留,需求作废了也不复用,状态标记为废弃即可。

光有编号还不够,要建立双向追溯:从 QAS 能查到它对应的功能需求、测试用例、监控指标、线上事故;反过来从一次线上事故也能查到它违反了哪条 QAS。这个矩阵用表格维护最省事:

QAS 编号关联功能需求测试用例 ID监控指标近期事故
NFR-PERF-014FR-PRODUCT-003 商品详情TC-PERF-021详情接口 P95INC-2024-031
NFR-AVAIL-006FR-ORDER-011 下单主链路TC-AVAIL-008下单成功率INC-2024-019
NFR-REL-003FR-PAY-002 支付回调TC-REL-014回调重复率—

这张表的价值在事故复盘时体现得最明显。以前复盘写"系统响应变慢导致用户投诉",现在写"INC-2024-031 违反了 NFR-PERF-014 中 P95 ≤ 300ms 的承诺,实测峰值期间达到 1.4 秒,根因是缓存命中率从 90% 跌至 62%"。后者能直接推导出改进项,前者只能推导出一句"加强监控"。

4.2 需求到用例到监控的三方对齐表

追溯矩阵里最容易断链的是"用例"和"监控"这两环。测试用例通常在测试管理系统里,监控规则在监控平台上,两边各说各话。我的做法是强制要求用例 ID 出现在监控规则的注释里,同时监控告警的 runbook 里必须写明对应的测试用例编号。这样值班的人处理告警时,能直接翻到当初验证这条指标用的压测方案,照着复现,比盲猜快得多。

三方对齐还有个隐性收益:它能暴露"有需求无用例"和"有用例无监控"的空档。我们第一次做这个对齐时发现,将近四成的质量需求压根没有对应的测试用例,还有三成的用例跑完之后的数据没有进入任何监控。这两个数字说明之前的质量保障基本是断的。

4.3 变更时的追溯维护

需求会变,追溯关系必须跟着变。我们定了两条规矩:QAS 的阈值调整必须走变更评审,评审记录里要写清楚调整原因(是业务量涨了还是原阈值不合理),并且必须同步更新用例和监控;QAS 作废时,关联的用例和监控规则要在同一个变更里一起下线,不能留孤儿。

这条规矩听起来官僚,但真的很必要。我们之前有一次把某个接口的延迟目标从 500 毫秒放宽到 800 毫秒,监控改了、文档改了,但压测脚本没改,结果下一次回归测试照着 500 毫秒判定,报了一堆"性能退化",排查了两天才发现是阈值不同步。

4.4 这套东西落地后,影响的是哪些角色

值得单独说说影响范围,因为这决定了推行的阻力在哪。

对产品经理来说,最大的变化是不能再写形容词了。提需求时必须回答"这个快是给谁看的、在什么情况下、多快算快",前期会很不适应,但后期会爱上这套东西,因为验收时终于有据可依。

对开发来说,好处是目标明确了,坏处是没法再含糊其辞。以前说"这个优化空间不大"可以糊弄过去,现在延迟预算表摆在那,你这一跳占了多少、还剩多少余量,一目了然。同时它也保护了开发——有人在评审时说"感觉还是慢",你可以直接反问"具体是哪条 QAS 的哪个指标不达标"。

对测试来说,这是收益最大的一环。NFR 从"没法测"变成"有明确用例",性能测试从"跑一下看看"变成"对照阈值判定",回归测试有了基线可以对比。

对运维来说,告警阈值终于有了业务依据,不再是"经验值 80%"。同时 SLO 和 QAS 挂钩之后,容量规划和扩容决策也有了数据支撑。

对架构决策来说,影响更深远。QAS 一旦成体系,架构评审的关注点就从"用了什么技术"转向"这个设计能不能满足这 N 条场景",技术选型有了明确的判定标准,而不是靠技术栈的新旧来争论。

5. 常见踩坑实录与速查表

方法论和流程讲完,最后这部分是我觉得最有价值的部分——都是真实踩过的坑。有些坑当时觉得是小事,后来发现影响挺大。

5.1 度量值拍脑袋,以及"伪精确"的陷阱

第一个坑是把度量值当成许愿。有次评审,业务方要求"订单提交接口响应时间不超过 50 毫秒",我问依据是什么,回答是"用户等不了太久"。50 毫秒意味着什么?意味着一整个下单链路(校验库存、创建订单、扣减库存、发消息)必须在 50 毫秒内跑完,这基本要求所有环节全内存操作,成本极高。后来拆开看,用户真实能感知的阈值在 800 毫秒左右,我们最终定的是 P95 ≤ 600 毫秒,成本降了一个数量级。

反过来也有"伪精确"的坑。有人喜欢写成"P95 ≤ 347 毫秒"这种数字,看着很严谨,其实 347 和 350 没有任何工程差别,纯粹是算出来的。度量值应该定在业务能感知、系统有台阶的位置上,350 就是比 347 好记也好沟通。过度精确只会让后面调整阈值时显得很不严肃。

还有一个隐蔽的坑:只定上限不定下限。某些场景下响应太快反而是问题,比如风控校验如果 20 毫秒就返回了,很可能意味着它压根没做实质校验。这类场景要考虑加下限或者加校验覆盖率指标。

5.2 把场景写成了功能用例

这个坑太常见了。典型症状是 QAS 里出现了大段业务逻辑描述,比如"用户提交订单,系统校验库存充足后扣减库存,生成订单号并返回"。这不是 QAS,这是功能用例。

判定的方法很简单:把响应度量那一栏遮住,看剩下的内容还能不能构成一个完整的功能描述。如果能,说明你写的是功能需求;如果遮住之后发现这句话什么也没说明白,那才是 QAS 该有的样子。QAS 的刺激只描述事件,响应只描述质量相关的行为(比如"成功返回"、"降级返回"、"排队等待"),不描述业务逻辑。

另一个变种是把测试步骤写进场景。我见过写成"第一步打开页面,第二步点击按钮,第三步观察响应时间"的,这是测试用例的写法。QAS 是需求规格,测试用例是实现手段,两者层级不同,不要混。

5.3 质量属性互相打架时,取舍要写进文档

性能和一致性打架、可用性和成本打架、安全性和易用性打架,这些冲突不可能消除,只能明确取舍。

我们遇到过一个典型案例:为了提升商品详情页的读取性能,加了多级缓存,结果是价格和库存的更新有最多 10 秒延迟。这在平时没问题,但在秒杀场景下会导致超卖。最后的处理方式是分场景定义:普通商品详情页允许 10 秒延迟,秒杀商品详情页强制走实时读,性能目标从 P95 ≤ 300 毫秒放宽到 P95 ≤ 800 毫秒,同时把"秒杀期间超卖笔数必须为 0"作为更高级别的硬约束。

关键在于这个取舍必须写进文档,写清楚"A 场景优先性能,接受 B 场景的一致性延迟;C 场景优先一致性,接受性能下降"。如果只在会上口头说了一句,那后面一定会出问题,因为没人记得当时的上下文。

5.4 踩坑速查表

把上面这些零散的经验整理成表,方便对照排查:

症状大概率原因处理方式
度量值高得离谱,开发集体反对用了用户感知阈值以外的理想值回到真实链路上做感知阈值调研,按延迟预算反推
度量值定得很精确但没人遵守阈值缺乏工程台阶,无法落地取整到有意义的档位,同时明确超标的处置流程
QAS 写完测试说没法测环境或口径没写清楚补齐负载模型、数据规模、分位数、统计窗口
上线后指标正常但用户仍投诉慢只测了服务端,忽略了客户端和网络补充端到端场景,把客户端渲染和网络往返纳入预算
压测通过但线上被打穿压测数据规模和真实规模差距大用生产数据脱敏后的等比例数据集,至少要覆盖千万级
告警天天响没人管阈值只设一条线,且定得过紧改成预警与紧急双阈值,预警线设为目标的 1 倍
缓存命中率掉下去引发雪崩命中率没作为独立质量需求管控单独定义命中率 QAS,配独立告警和热点 key 监测
需求变更后监控和用例不同步缺少变更时的联动机制变更单强制勾选用例与监控的同步更新项
场景写了但没人用没有挂进需求库和监控系统编号入需求库,指标入监控,双向追溯矩阵维护

用一个我个人最深的体会收尾:QAS 这套东西最大的价值不是那些表格和模板,而是它强制产生的那个"停顿"——在写下"性能要好"之后,你必须停下来问自己一句"好在哪儿,怎么算好"。这个停顿能省掉后面无数次会议。我们团队推行了大概半年,最直接的变化是需求评审从平均 90 分钟降到 40 分钟,因为大量争议在写 QAS 的阶段就已经被逼着解决了。至于那些实在定不下来的指标,我的建议是先定一个粗略值上线,用真实监控数据跑两周再回来修订,比在会议室里空想三天有用得多。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进