需求评审开到第四十分钟,有人对着一行需求念出声:“系统性能要好,响应要快。”会议室安静了三秒。开发抬头问“多快算快”,测试问“那我按什么标准提缺陷”,运维问“压测上来算谁的问题”。这个场面我见过太多次,问题不在人,在那行字本身——它是一条非功能需求(NFR),却长得像一句口号。后来我们把这类条目全部强制转写成质量属性场景(Quality Attribute Scenario,下文简称 QAS),情况才开始有变化:需求评审从“感觉够不够快”变成“这个阈值在什么负载下、按什么口径统计”,争论的焦点终于落在了同一个坐标系里。这篇就把这套做法完整拆一遍:QAS 到底是什么、它为什么能治“性能要好”这种病、在我们内部那套需求流转流程(内部代号 DevFlow)里怎么落地、落地过程中踩过哪些坑。不管你是写需求的产品、接需求的开发,还是负责验收的测试和运维,都能从里面抄到可以直接用的东西。
1. 那句“性能要好”缺的到底是什么:一次评审僵局的事后解剖
1.1 模糊 NFR 的三层失效:测不了、拆不开、追不回
很多人以为“性能要好”的问题只是“说得不够细”,其实它同时在三层上失效,而且这三层是连锁的。第一层是测不了:没有度量项、没有阈值、没有统计窗口,验收阶段根本无法判定“通过”还是“不通过”,最后只能靠某个人的主观感受拍板,一旦拍板的人换了,结论就可能翻转。第二层是拆不开:模糊的形容词没法分配到具体组件和具体人头上,“系统要快”到底是网关要快、数据库要快,还是前端首屏要快?架构评审时没人能拿它做取舍依据,因为它不指向任何具体制品。第三层是追不回:需求条目和验收用例、代码模块、线上监控之间没有链路,等到线上真的慢了,谁也说不清当初承诺的到底是什么标准,只能重新吵一遍。
这三层失效叠在一起,后果比想象中严重。我见过一个“系统要稳定”的需求,在需求池里躺了四个月,期间被三个团队认领过又退回来,因为谁都说不清自己那部分要承担多少。后来把它拆成六条 QAS 之后,两天就分完了:三条归网关,两条归订单服务,一条归数据库运维。这不是因为团队突然变勤快了,而是因为条目变得可以归属了——你能指着“order-query-service 在缓存预热完成、主从延迟低于 100ms 的环境下,承受 800 QPS 时 P95 不超过 300ms”这条说,这是我的,那也是你的。
所以判断一条 NFR 是不是废的,有个很简单的自测:把它原样贴到缺陷管理系统里,看测试能不能据此写出一个必然通过或必然失败的用例。写不出来,它就还是形容词。
1.2 加了“响应时间小于 200 毫秒”为什么还是不够
有人会说,那我们加上数字不就行了?“接口响应时间小于 200 毫秒”——这句话看起来已经很具体了,但它依然会在验收阶段引发新的争吵。原因有四个,每一个都对应 QAS 里的一个槽位缺失。
缺环境。这 200ms 是冷启动后的第一次请求,还是缓存预热完成之后的稳定态?是单实例压测,还是集群满配?是空库跑出来的,还是千万级数据量下跑出来的?同一个接口,这四种情况的实测值能差出五倍以上。缺制品边界。是接口自身的服务端处理耗时,还是从客户端发起请求到收到完整响应的端到端耗时?网关的鉴权、序列化、网络往返算不算在里面?缺刺激规模。1 个并发也是“小于 200ms”,1000 个并发也是“小于 200ms”,但这两件事的工程难度完全不是一个量级。没有负载描述,这条需求等于没有下限也没有上限。缺统计口径。平均值 200ms 和 P95 是 200ms,是两种截然不同的系统:前者允许 5% 的请求慢到几秒,后者不允许。
我做过一次复盘,把同一个接口的四个数字摆在一起:平均值 180ms、P95 是 420ms、P99 是 1.2s、端到端 P95 是 610ms。四个数字都对,都“小于 200ms”以外的表述也都能解释得通。那次复盘之后,我们内部定了一条死规矩:任何性能类 NFR,不写清统计口径、负载规模、环境前置条件、制品边界这四项,一律不进开发排期。
2. QAS 的六个槽位:把形容词拆成能直接跑测试的六元组
2.1 刺激源与刺激:谁、在什么条件下、干了什么
QAS 的第一组槽位是刺激源(Source)和刺激(Stimulus)。刺激源回答“谁或者什么引发了这件事”,刺激回答“具体引发了什么”。这两个槽位必须成对出现,因为脱离刺激源的刺激是没有意义的。同样是“800 QPS 的请求”,来自真实终端用户和来自内部批量任务,对系统的要求完全不同:前者有突发的尖峰和极不均匀的分布,后者可以精确控制速率、可以错峰、可以限流。
写刺激源时最容易犯的错是写成“用户”。这跟没写差不多。合格的刺激源至少要带上身份属性和规模属性,比如“促销开始瞬间涌入的移动端下单用户,预计峰值同时在线 3 万人”。如果是内部触发的场景,刺激源要写成具体角色或系统,比如“运维人员在变更窗口内执行主库切换”或者“上游对账系统在凌晨 2 点发起批量文件拉取”。刺激源的粒度直接决定了你后面能不能找到对应的验证手段——真实用户需要压测工具模拟,上游系统需要契约测试或者故障注入。
刺激本身则要带上量级、分布和时间特征。以性能场景为例,“以 50 QPS 线性递增到 800 QPS,持续 10 分钟”比“高并发访问”有用一百倍,因为它直接对应了一个可以执行的压测脚本参数。可用性场景的刺激通常是离散事件,比如“主库实例在 3 秒内不可用”或者“下游服务连续 5 次返回超时”。可修改性场景的刺激是变更请求,比如“新增一个支付渠道枚举值”。安全性场景的刺激是一次具体的异常请求构造,比如“携带他人订单号发起详情查询”。这些写法有一个共同点:刺激可以被单独复现出来,不需要依赖上下文脑补。
2.2 环境与制品:在哪个运行状态下,作用在哪一块
**环境(Environment)**这个槽位是最容易被跳过、也是事后扯皮最多的。它描述的是场景发生时系统所处的状态:正常运营、降级运行、启动中、变更窗口、灾备切换后。很多团队写 QAS 时会下意识填“正常情况”,这个填法等于没填,因为它既没有说清前置条件,也没有排除掉那些真正会出问题的边界状态。我一般要求环境槽位至少包含三类信息:运行态(正常/降级/维护)、数据态(空库/常规量级/历史峰值量级)、依赖态(上下游是否健康、缓存是否预热)。
举个具体的对比。同一个订单查询接口,环境写“正常情况”时,你测出来的数字很漂亮,但线上出事永远发生在数据态和依赖态异常的时候。把环境写成“缓存预热完成、主从复制延迟低于 100ms、单表数据量 5000 万”之后,压测脚本的造数逻辑就必须跟着改,你会发现原本 300ms 的 P95 在真实数据量下变成了 900ms。这个差距不是被 QAS 制造出来的,它一直存在,只是以前没人要求你去看见。
**制品(Artifact)**回答“作用在哪一块”。这里的粒度需要拿捏:写“系统”太粗,写“第 137 行代码”太细,合适的粒度通常是“一个可以独立部署和独立观测的单元”,比如某个服务、某个接口、某张表、某个前端页面。写制品的时候有个技巧,同时写出它的下游:order-query-service 及其依赖的 order-db 与 Redis 缓存。这样写的好处是,当响应不达标时,排查范围天然被框定了,不会一上来就全链路漫无目的地找。
2.3 响应与响应度量:可观测的动作加上带单位的阈值
**响应(Response)**是系统在受到刺激后做出来的动作,**响应度量(Response Measure)**是这个动作的量化标准。这两者经常被混为一谈,写成“系统快速返回结果,响应时间小于 300ms”,实际上前半句是响应、后半句才是度量,混在一起写会导致一个隐蔽的问题:降级行为没有被描述。真实系统在压力下往往不是“变慢”,而是“改变了行为”——返回缓存旧值、返回兜底默认值、拒绝部分请求、异步化处理。这些行为如果不写进响应槽位,验收时就只能拿“有没有超时”这一把尺子量,而那些优雅降级的设计反而成了没人认可的额外付出。
所以响应槽位要写成可观测的动作序列,比如“服务返回订单详情;当查询耗时超过 500ms 时,降级返回缓存中的最近一次结果,并在响应头标记 stale=true”。这样写完之后,响应度量自然就分成两组:正常路径的延迟指标,和降级路径的触发率与正确性指标。
响应度量的写法有三条硬要求。带单位,毫秒、百分比、次/秒、人日,不写单位的数字没有意义。带统计窗口,“P95 不超过 300ms”要补上“在 10 分钟统计窗口内”,否则瞬时抖动和持续劣化无法区分。带边界,错误率要写清分母是总请求数还是有效请求数,可用性要写清是按自然月还是按滚动 30 天。这三条看着琐碎,但每一条我都见过因为没写而返工的案例。
2.4 六个槽位不是清单而是因果链
很多人第一次接触 QAS,会把它当成一张要填满六个空格的表格,填完就完事了。这样用出来的效果很一般,因为六个槽位之间存在因果关系:刺激源决定了刺激的形态,刺激和环境的组合决定了响应的合理范围,响应又决定了度量该测什么。填表时如果发现某个槽位怎么写都别扭,通常是上游槽位没想清楚。
举个真实例子。有个团队写了一条可用性场景,度量写“系统可用性达到 99.95%”,但刺激写的是“机房断电”。这两者对不上:机房断电这种级别的故障,单靠应用层是扛不住的,99.95% 这个月度指标也不是靠单次故障场景能验证的。后来把刺激改成“单台应用实例进程崩溃”,度量改成“30 秒内被负载均衡摘除,期间错误请求占比低于 0.5%”,整条场景立刻变得可执行了。机房断电那种场景则被单独拆成一条灾备演练场景,度量换成 RTO 和 RPO。这就是因果链在起作用——刺激的级别决定了响应的能力边界,也决定了度量该用什么量纲。
| 槽位 | 常见的糊弄写法 | 可执行的写法 | 自检问题 |
|---|---|---|---|
| 刺激源 | 用户 | 促销开始瞬间涌入的移动端下单用户,峰值同时在线 3 万 | 谁产生的,有没有身份和规模描述 |
| 刺激 | 高并发访问 | 5 分钟内从 100 QPS 线性爬升至 800 QPS | 什么动作,量级、分布、时间特征是什么 |
| 环境 | 正常情况 | 生产环境,缓存预热完成,主从延迟低于 100ms,单表 5000 万行 | 运行态、数据态、依赖态分别是什么 |
| 制品 | 系统 | order-query-service 及 order-db、Redis 缓存 | 能不能独立部署、独立观测 |
| 响应 | 快速返回 | 返回订单详情;超过 500ms 降级返回缓存值并标记 stale | 有没有描述降级和异常路径的行为 |
| 响应度量 | 要快 | 10 分钟窗口内 P95 ≤ 300ms,P99 ≤ 800ms,5xx 率 < 0.1% | 单位、窗口、分母边界是否齐全 |
3. DevFlow 里的四道加工:从需求池条目到 QAS 卡片的转化流水线
3.1 第一道加工:用固定词表把形容词归到质量属性下
需求池里的原始条目通常是这样的:“系统要稳定”“页面要快”“以后好扩展”“不能出事”。这些句子的共同点是,它们描述的是感受而不是属性。转化的第一步不是急着补数字,而是先归类——把它归到哪一个质量属性下。我们内部用一张固定词表,只有八个词:性能、可用性、可修改性、安全性、可测试性、易用性、可运维性、互操作性。词表固定的好处是,归类结果可以统计,能看出一个版本里 NFR 的分布是否失衡。
归类这一步看着简单,实际最容易出错。比如“页面要快”,产品心里的意思是首屏内容尽快可见,开发理解成了接口响应快,测试按接口耗时写了用例。三个人的理解偏差在归类阶段就能暴露出来:如果归到性能,那要问是加载性能还是交互性能;如果归到易用性,那度量的就不是毫秒而是任务完成率。我遇到过一条“用户操作要顺畅”,争论了半天,最后归到易用性,度量写成“典型下单流程的点击次数不超过 5 次、无需滚动即可看到提交按钮”,跟性能一点关系都没有。
归类之后要给每条 NFR 打上来源标记:来自业务方诉求、来自历史故障复盘、来自合规审计要求,还是来自架构演进规划。这个标记非常重要,因为它决定了这条 NFR 的优先级怎么定。历史故障复盘出来的可用性场景,优先级天然高于拍脑袋想出来的性能指标,因为前者已经用真实损失证明过自己的价值。
3.2 第二道加工:五个追问补齐场景上下文
归类完成后进入补上下文阶段。我们固定用五个追问,几乎能把所有缺失的槽位逼出来:谁触发的?触发的时候量级多大?当时系统处在什么状态?作用在哪一块?期望它做什么、做到什么程度?这五个问题一一对应 QAS 的六个槽位(第二个问题同时覆盖刺激和部分环境)。
追问的过程中有个很实用的技巧,叫边界值逼问。不要问“正常情况下要多快”,要问“最慢不能慢过多少”和“数据量涨到十倍的时候还能不能守住”。人对区间的感知远不如对边界的感知敏锐,一旦被问到“什么时候可以报警”,真实标准往往就出来了。另一个技巧是反例锚定:让对方回忆一次印象深刻的线上问题,然后问“那次的情况如果重演,你希望系统怎么表现”。这个方法特别好用,因为人对故障的记忆比对指标的记忆清晰得多,描述出来的场景天然带着环境、制品和响应细节。
第二个追问“量级多大”经常被敷衍成“挺大的”。这时候不要接受,换成具体参照物:日活多少、峰值订单多少、历史最高并发是多少、压测跑过多少。如果对方确实不知道,那就把它标成待定项,同时约定一个取值来源——从监控里取近 90 天的峰值再上浮 30%,或者按大促预估。待定项必须带责任人和截止时间,否则它会永远待定下去。
3.3 第三道加工:度量项必须落在已有的观测手段上
这一步是整套流程里最关键、也最容易被跳过的一环。很多团队写完 QAS 很兴奋,结果验收时发现度量项根本测不出来——要么监控没埋这个点,要么压测环境造不出那种数据,要么统计口径和现有看板对不上。所以我们有一条硬规则:每一条响应度量,必须能指向一个现存的观测手段(监控指标、日志字段、压测脚本、契约测试、人工检查表),指不出来的,要么补可观测性改造任务,要么换一个能测的度量。
这条规则一开始被不少架构师反对,理由是“不能因为测不了就降低要求”。但实际跑下来,它反而推动了可观测性建设:一个季度里,因为 QAS 度量项指不出来而新增的埋点和看板任务有十几项,比专门提“加强监控建设”这种口号有效得多。因为每一条埋点都挂在一份具体的验收承诺上,做的人知道自己在为哪条需求服务。
度量选型上还有个经验:优先选已经被用于线上告警的指标。如果一个指标平时没人看、不告警、不进值班手册,那它在验收时大概率会被质疑“这个数字代表什么”。反过来,如果 QAS 的度量项和线上告警用的是同一个指标、同一个统计口径,验收结论和线上表现就天然一致,不会出现“验收通过但线上还是被投诉”的尴尬。
3.4 第四道加工:评审卡点与打回规则
最后一道是评审。我们设了一个轻量卡点,不搞大评审会,就是需求进入开发排期前的一次结构化检查,六条规则,任何一条不过就打回。
| 检查项 | 打回条件 |
|---|---|
| 刺激源可识别 | 写成“用户”“系统”“外部”等泛指,未标明身份或规模 |
| 刺激可复现 | 无法据此写出压测脚本参数或故障注入脚本 |
| 环境有前置条件 | 只写“正常情况”,未描述运行态、数据态、依赖态 |
| 制品可独立观测 | 粒度粗到“系统”,或细到无法对应到部署单元 |
| 响应含异常路径 | 只描述成功路径,未说明降级、拒绝、兜底行为 |
| 度量可指向观测手段 | 找不到对应的监控指标、压测脚本或测试用例 |
这套卡点刚上线时被打回率接近七成,两个月后降到三成左右,不是因为标准放松了,而是提需求的人逐渐内化了这六条。这个过程没什么捷径,就是反复打回、反复给示例。我们做了一件很有用的事:把被打回的条目和改写后的版本成对存到一个共享文档里,新人进来先看二十组对比,比讲一小时方法论管用。
一条合格的 QAS 落成卡片大概是这个样子:
qas_id: QAS-PERF-001 quality_attribute: performance title: 订单查询接口在促销峰值下的响应能力 source: 促销开始瞬间涌入的移动端下单用户,峰值同时在线 3 万 stimulus: 压测流量在 5 分钟内从 100 QPS 线性爬升至 800 QPS environment: 生产同构环境,缓存预热完成,主从延迟低于 100ms,单表 5000 万行 artifact: order-query-service 及 order-db、Redis 缓存 response: 返回订单详情;查询耗时超过 500ms 时降级返回缓存值并标记 stale=true response_measure: latency_p95: "<= 300ms(10 分钟统计窗口)" latency_p99: "<= 800ms(10 分钟统计窗口)" error_rate_5xx: "< 0.1%(分母为有效请求数)" cpu_saturation: "<= 70%(单实例峰值)" verification: perf/order-query-peak.js,纳入 nightly 回归 trace: requirement: REQ-NFR-0231 test_case: TC-PERF-0087 adr: ADR-0014 dashboard: order-query-latency4. 四类高频质量属性的写法差异:性能、可用性、可修改性、安全性逐个对照
4.1 性能场景:P95、负载剖面和饱和度要一起写
性能是最好写也最容易写歪的一类。好写是因为指标现成,容易写歪是因为大家习惯只写一个延迟数字。我的经验是性能场景至少要三个维度同时约束:延迟分位数、负载剖面、资源饱和度。只写延迟不写负载,等于开了一张空白支票;只写负载不写饱和度,压测时机器跑满了你还以为系统扛住了。
延迟指标优先用分位数而不是平均值,这一点在第一章已经说过,但分位数选哪个也有讲究。P95 适合面向用户的交互接口,因为 5% 的尾部体验还能接受;P99 适合涉及资金、库存这类强一致操作的接口,因为长尾请求往往对应着锁等待或重试;P999 一般只在极高频的底层服务上才要求,写多了会让团队把精力耗在无意义的优化上。同一个系统里不同接口用不同分位数,是完全合理的,不要为了整齐划一全部写 P99。
负载剖面比很多人想的更重要。同样是 800 QPS,平稳流量和尖峰流量的工程应对完全不同:前者可以靠弹性扩容慢慢加机器,后者必须靠预热、限流和排队。所以刺激槽位要写清流量的时间形态,是“5 分钟线性爬升”还是“10 秒内瞬间打到峰值”。我们内部有个默认约定:凡是涉及营销活动的性能场景,必须包含一次“瞬间打峰”的刺激描述,因为真实世界里的促销就是这么来的。
饱和度指标是最后一道保险。CPU 70%、内存 80%、连接池使用率 85%、磁盘 IO 等待时间,这些数字不达标的意义在于:即使延迟指标侥幸守住了,系统也已经没有余量应对下一波流量。把饱和度写进 QAS 的好处是,压测报告里必须包含资源曲线,而不是只给一张漂亮的延迟分布图。
场景: 促销峰值下的订单查询 假设 缓存预热完成且数据库主从延迟低于 100 毫秒 并且 订单表数据量为 5000 万行 当 压测流量在 5 分钟内从 100 QPS 线性爬升至 800 QPS 那么 统计窗口内 P95 响应时间不超过 300 毫秒 并且 P99 响应时间不超过 800 毫秒 并且 5xx 错误率低于 0.1% 并且 单实例 CPU 使用率峰值不超过 70%4.2 可用性场景:把“高可用”翻译成故障注入剧本
“系统要 99.99% 可用”这句话几乎是所有 NFR 文档里的标配,但它作为需求几乎没有执行力,因为它既没说清故障模型,也没说清降级边界。可用性的本质是在什么故障下、保持什么程度的服务、多久恢复,所以它的 QAS 写出来天然像一份故障注入剧本。
刺激源在可用性场景里通常是两类:内部的基础设施故障(进程崩溃、实例失联、磁盘写满、线程池打满),和外部的依赖故障(下游超时、下游返回错误、第三方接口限流)。这两类的响应策略不一样:前者靠冗余和自动摘除,后者靠熔断和降级。写的时候要分开写,不要混成一条“系统要能应对故障”。
响应度量在可用性场景里有四个常用量纲,各有各的用途。检测时间衡量监控灵敏度,恢复时间衡量自愈能力,影响面衡量故障隔离做得如何(比如“受影响的请求占比低于 0.5%”),数据一致性损失衡量最坏情况下丢了什么。第四条最容易被漏掉,但对涉及资金和库存的系统来说恰恰是最重要的。我坚持要求所有写操作相关的可用性场景必须有一条数据一致性度量,比如“主库切换过程中不产生重复扣款,允许的最长不一致窗口为 0”。
降级行为必须在响应槽位写清楚,而且要写到“降级到什么程度算成功”的程度。举个具体的:推荐服务不可用时,商品详情页应该返回什么?是返回空列表、返回热门兜底、还是隐藏整个推荐区块?这三种选择对用户体验的影响差别很大,对下游系统的压力也完全不同。写清楚之后,测试才能设计出对应的故障注入用例,否则只能测“页面没白屏”这种最粗的标准。
4.3 可修改性场景:用变更波及面量化“好不好改”
可修改性是四类里最抽象的一类,很多人写不出来,因为它不像性能和可用性那样有现成的运行指标。但其实它有一套很成熟的量化方式:把变更本身当作刺激,把变更的波及面当作度量。
刺激源是开发人员,刺激是一次具体的变更请求。这里的关键是变更要具体到真实发生过的类型,比如“新增一个支付渠道”“给订单增加一个状态”“把某个字段从必填改为选填”。抽象地说“系统要易于扩展”是没有意义的,而“在现有支付模块中新增一个渠道,要求不改动订单核心流程代码”就非常具体,因为它划定了波及面的边界。
响应度量常用四个数字:受影响文件数、涉及模块数、预估人日、需要回归的用例数。这四个数字可以从事后的变更记录里统计出来,所以它不只是事前承诺,还能做事后校准。我们做过一次统计,把过去半年里“新增一个业务枚举值”这类变更的实际波及面拉出来,发现中位数是 7 个文件、跨 3 个模块。这个数字一摆出来,架构评审时关于“要不要抽一层配置”的争论立刻有了依据——现状是 7,目标是 3,差距清晰可见。
环境槽位在可修改性场景里通常写“设计时”或“编译时”,这两个词对应的是不同层级的可修改性:设计时可变意味着改配置或改模型,编译时可变意味着改代码重新构建。如果一条变更场景要求“设计时可变”,那它实际上是在要求系统具备运行期配置能力,这个约束会直接影响架构决策。很多团队写可修改性场景时忽略了环境槽位,结果做出来的东西确实“好改”,但每次改都要重新发版,跟预期完全不符。
4.4 安全性场景:从异常请求的构造视角写刺激源
安全性场景的写法和前三类有个明显区别:刺激源往往是恶意或异常的。这时候用“正常用户”的视角是写不出来的,必须切换到“构造一个什么样的异常请求”的视角。常见的几类包括:未认证请求访问受保护资源、已认证用户访问他人资源、参数篡改、批量高频请求、异常格式输入。
刺激要写成具体的请求构造方式,比如“携带 A 用户的会话凭证,请求 B 用户的订单详情接口,订单号通过遍历获得”。这种写法看起来有点“攻击说明书”的味道,但它恰恰是可测试的前提——测试人员拿到这句话就能直接写出用例,安全评审也能据此判断防护措施是否覆盖。
响应槽位在安全性场景里要写清是拒绝还是脱敏返回。这两者的选择直接影响用户体验和实现复杂度:拒绝实现简单但可能误伤,脱敏返回体验好但要求数据分级做得足够细。无论选哪种,响应里都应该包含审计动作,比如“记录一条包含请求方标识、目标资源、时间戳的审计日志”。度量项则围绕拦截率、误拦截率、审计完整性和响应时效来写,其中误拦截率最容易被忽略,但它是唯一能防止安全策略把正常用户挡在门外的指标。
有一类场景特别值得单独立项:越权访问。它的特点是攻击者已经通过了身份认证,只是权限不够,所以传统的认证防护完全失效。写这类 QAS 时,刺激源要写成“已认证但无权限的用户”,刺激写成“直接构造请求访问不属于自己的资源 ID”,响应写成“拒绝并记录审计日志,不泄露资源是否存在的信息”。最后半句很重要,因为“资源不存在”和“无权限访问”返回不同的错误码,本身就构成了一次信息泄露。
| 质量属性 | 刺激源典型写法 | 核心度量项 | 最容易被漏掉的槽位 |
|---|---|---|---|
| 性能 | 峰值时段终端用户、压测流量 | P95/P99 延迟、错误率、资源饱和度 | 环境中的数据量与预热状态 |
| 可用性 | 基础设施故障、下游依赖超时 | 检测时间、恢复时间、影响面、数据损失 | 响应中的降级成功标准 |
| 可修改性 | 开发人员发起的具体变更 | 受影响文件数、模块数、人日、回归用例数 | 环境中的设计时/编译时区分 |
| 安全性 | 已认证但无权限的用户、异常请求构造 | 拦截率、误拦截率、审计完整性 | 响应中的错误信息泄露控制 |
5. 可追溯的落地形态:QAS 与用例、架构决策、线上指标的三角绑定
5.1 编号与映射规则:QAS-PERF-001 这类 ID 怎么定
可追溯的前提是每条 QAS 有一个稳定且可读的标识。我们的编号规则很土但很好用:QAS-<属性缩写>-<三位序号>,比如QAS-PERF-001、QAS-AVAIL-007、QAS-MOD-003。属性缩写用固定词表里的八个词,序号在属性内自增,永不复用。永不复用这一条很重要——如果一条 QAS 被废弃了,序号也保留,状态标成废弃,这样历史引用不会指向一个不存在的编号。
编号之外还有三组关联 ID 必须写进 QAS 卡片:需求 ID(来自需求池)、测试用例 ID(来自测试管理系统)、架构决策 ID(来自决策记录库)。这三组 ID 构成了追溯链的骨架:需求往上追溯到业务目标,用例往下追溯到具体验证手段,决策横向关联到架构选择。有了这三组 ID,任何一条 QAS 都能在三个方向上被检索到——从需求反查验证情况、从用例反查它保障的是哪条承诺、从架构决策反查它到底在支撑什么。
实际执行中最容易断的是需求 ID 这一环,因为需求池里的原始条目往往在转写成 QAS 之后就被标记成“已转化”然后不再维护。我们的做法是让原始条目保留,但在描述末尾追加一行“已转写为 QAS-XXX-NNN”,形成双向指针。这样产品经理看需求池时能看到转化结果,架构师看 QAS 时能回溯到最初的业务表述。
5.2 验收用例从 QAS 直接生成
这一步是整个体系能不能落地的分水岭。如果 QAS 写完之后,验收用例还是要测试人员从零开始设计,那这套东西就只是多了一层文档负担。我们的做法是给每一类质量属性配一个用例模板,把 QAS 的槽位映射成用例的给定条件、操作步骤和断言。
以性能为例,模板的映射关系是:环境槽位 → 前置条件与造数逻辑,刺激槽位 → 压测脚本参数,制品槽位 → 被测服务与观测对象,响应度量 → 断言阈值。测试人员拿到 QAS 卡片,填参数就能生成用例骨架,剩下的工作是补充异常路径和边界组合。我们在实际运行中发现,一条合格的性能 QAS 大约能生成三到五条具体用例:正常路径一条、边界负载一条、超限降级一条、依赖异常一条。
可用性场景生成的用例更接近故障演练脚本:注入什么故障、观察什么信号、判定恢复的标准是什么、数据一致性怎么校验。这类用例的执行成本比功能测试高得多,所以不能全量跑。我们的策略是分两级:核心链路的可用性场景进每日回归(用轻量的进程级故障注入),全量场景每月跑一次(包含主从切换、机房级演练)。这个分级本身就是从 QAS 的重要性排序里来的,不是拍脑袋定的。
5.3 与架构决策记录的互相引用
QAS 和架构决策记录(ADR)之间应该是双向引用关系。一方面,每条 ADR 都应该写明它在支撑哪些 QAS——如果一个架构决策找不到对应的 QAS,那它大概率是过度设计或者技术偏好驱动的。另一方面,每条 QAS 也应该写明它依赖哪些 ADR——这让后来的人明白,这个指标之所以能达成,是因为当初做了哪些取舍。
举个具体的例子。ADR-0014:订单查询引入读写分离与本地缓存这条决策,支撑的 QAS 是QAS-PERF-001(峰值查询延迟)和QAS-AVAIL-004(主库故障时的查询可用性)。反过来,QAS-PERF-001的卡片上会标注“依赖 ADR-0014,缓存一致性窗口允许 3 秒”。这个 3 秒的窗口就是取舍的痕迹:为了性能接受了短暂的数据陈旧。如果没有这行记录,半年后有人发现查询结果有 3 秒延迟,会当成缺陷来修,把当初的设计意图整个推翻。
这种双向引用还解决了一个很实际的问题:需求变更时的冲击分析。当业务方要求把缓存一致性窗口从 3 秒收紧到 500 毫秒,你能立刻顺着引用链找到受影响的 QAS、ADR 和用例,评估出改动范围。没有这条链的时候,这种变更往往要等到开发阶段才被发现,代价高得多。
5.4 上线后用同一套度量回填
QAS 最有价值的地方在于它是贯穿全生命周期的,而不是只在需求和验收两端出现。上线之后,同一条 QAS 的度量项应该直接对应到监控看板和告警规则上。这意味着告警阈值不是运维凭经验拍的,而是从需求承诺里来的;也意味着线上指标一旦持续劣化,能立刻定位到是哪条质量承诺在被侵蚀。
我们做的具体事情是给每条 QAS 卡片加一个dashboard字段和一个alert_rule字段,指向对应的看板和告警规则 ID。上线验收时做一次基线采集,把当时的实测值写进卡片,之后每个季度回看一次。这个季度回看机制抓出过不少问题:有个接口的 P95 在三个季度里从 280ms 慢慢爬到了 460ms,中间没有任何一次发布看起来是“罪魁祸首”,但趋势线很清楚。如果没有基线,这种缓慢劣化几乎不可能被发现,因为它从来不会触发任何单次告警。
回填还有一个副作用是正向的:它让 QAS 从“一次性文档”变成了“活的资产”。团队会主动去更新卡片上的实测值,因为那是他们工作的证明;新人接手时看卡片就能知道这个系统的真实水位在哪里,不用重新压一遍。这个习惯养成之后,需求评审时引用历史数据的频率明显上升,争论也从“我觉得”变成了“上次实测是”。
6. 三个月里踩到的坑:过度规格化、环境槽位错位与口径打架
6.1 坑一:试图把每条 NFR 都写成 QAS
这套方法刚推的时候,我们犯的第一个错误是用力过猛——要求所有 NFR 条目一律转写成 QAS。结果一个月内积累了上百条卡片,其中大量是“日志要可检索”“配置要能热更新”这种低风险、低争议的条目,为它们补场景、定度量、建用例的成本远超收益。团队的抵触情绪主要来自这里:不是方法不好,是它被用在了不需要它的地方。
后来我们定了一个筛选标准,只有同时满足两个条件的条目才强制转 QAS:一是存在明确的取舍(比如快与一致性之间的权衡),二是有可量化的失败代价(比如超时会直接导致下单失败)。剩下那些低风险条目走轻量通道,只写一句明确的验收标准就够了,比如“所有对外接口的日志必须包含 traceId 字段”。这个二分法把 QAS 的数量压缩到了原来的三成左右,但覆盖了绝大部分真正会引发争议的场景。
判断一条 NFR 该不该走重流程,有个很实用的问句:“如果这条没达成,会不会有人在半夜被叫起来?”会,就走 QAS;不会,走轻量通道。这个标准粗糙但极其有效,因为它直接锚定在后果上,而不是锚定在文档形式上。
6.2 坑二:环境槽位写“正常运行”,等于整条场景作废
第二个坑更隐蔽。有一批 QAS 在评审时全票通过,验收时也顺利达标,但上线后连续出问题。复盘的时候把卡片翻出来一看,环境槽位清一色写着“正常运行状态”。这意味着压测是在空库、缓存全热、依赖全健康的情况下跑的,而线上的真实状态是数据量五千万、缓存命中率七成、下游时不时超时。
这个坑的根因是把环境理解成了“背景描述”而不是“约束条件”。修正的做法是给环境槽位定一个最小信息集,必须包含三项:数据规模(表行数或数据总量)、缓存状态(冷/热/命中率区间)、依赖健康度(正常/降级/部分不可用)。三项缺一,评审直接打回。这个规则执行之后,压测脚本的造数逻辑被迫升级,工作量增加了不少,但测出来的数字终于和线上对得上了。
还有个衍生问题值得单独提:环境槽位写得太理想,会导致架构评审做出错误的容量规划。曾经有个服务按“缓存命中率 95%”的环境做容量设计,实际上线后命中率只有 70%,多出来的请求全打到数据库,直接把主库压垮。这个教训之后,我们要求所有涉及缓存的性能场景,环境槽位必须同时写出命中率和穿透时的降级策略。
6.3 坑三:开发和测试量的不是同一个东西
第三个坑是度量口径打架,而且吵起来特别伤感情,因为双方的数字都是真的。开发在自己机器上压出来 P95 是 200ms,测试在测试环境用端到端工具测出来是 600ms,谁也说服不了谁。查了半天发现三个差异叠加:一是统计位置不同,开发测的是服务内部处理耗时,测试测的是包含网络往返和网关鉴权的端到端耗时;二是统计方法不同,开发取的是最近 1000 次请求的均值,测试取的是全量请求的 P95;三是环境不同,测试环境少两台机器。
这三个差异在 QAS 里其实都有对应的槽位可以约束——制品槽位写清边界,度量槽位写清统计方法和窗口,环境槽位写清机器配置。问题在于写的时候没写死,留下了“服务端”和“端到端”这种模糊表述。后来我们加了一条规则:所有延迟类度量必须标注统计位置,可选值只有三个——服务端处理耗时、网关到服务端、客户端到服务端端到端。这个字段一旦必填,口径打架基本消失。
顺带说一个经验:默认情况下,面向用户的 QAS 应该用端到端口径,因为用户感知的就是端到端;面向容量规划的 QAS 应该用服务端口径,因为它更接近资源消耗的真实情况。两种都要有,但不能混用,更不能用一个的达标去证明另一个也没问题。
6.4 坑四:响应度量写成“越快越好”的各种变体
第四个坑比较微妙,度量项看起来有数字,实际上没有约束力。典型写法包括“响应时间尽可能短”“可用性不低于行业平均水平”“变更影响控制在合理范围内”。这些句子的共同特征是用了一个无法证伪的形容词做修饰,验收时怎么解释都行。
要治这个,光靠提醒没用,得用机制。我们的做法是在评审卡点上加一条机械检查:度量项必须能被解析成“指标 + 比较符 + 数值 + 单位 + 统计窗口”的形式,解析不了就打回。这条规则刚上时被吐槽“太死板”,但它确实把那些“尽可能”“合理”“适当”之类的词清出了文档。有意思的是,被迫写具体数字之后,很多需求方自己发现原来的预期根本不合理——“响应时间尽可能短”变成“P99 不超过 500ms”之后,他们才意识到这个要求在大促场景下意味着什么成本。
还有一个相关的坑:度量项写得太全,一条 QAS 挂了八个指标,结果验收时只要有一个不达标就整体不通过,团队就倾向于把指标写得宽松一点来保证通过率。我们后来限制每条 QAS 的度量项不超过四个,并且区分硬指标和观测指标:硬指标不达标即验收失败,观测指标只做记录不做判定。这个区分让团队敢于把真正重要的指标写严,因为不用担心被次要指标拖累。
我在实际操作中的体会是,QAS 这套东西真正的价值不在那六个槽位的格式,而在于它强迫所有人把“我觉得”换成“多少、在什么条件下、怎么量”。格式是可以简化的,可能你们团队最后用的是一张五个字段的表,甚至就是一句结构化的自然语言,但只要那几个关键信息在里面,效果就成立。反倒是那些把模板做得很华丽、字段填得很齐、但没人拿它去写用例和配告警的团队,最后什么也没留下。真正值得投入时间的,是把 QAS 的度量项和线上看板绑在一起这件事——绑上去之后,需求文档就不再是归档材料,而是每天都会被人打开的东西。