土法炼钢兴趣小组的算法知识备份

【系统架构设计百科】架构质量属性:不只是"高可用高性能"

文章导航

分类入口
architecture
标签入口
#quality-attributes#architecture-design#availability#performance#modifiability#security#testability#trade-off#SEI#ATAM

目录

上周的需求评审会上,产品经理说:“这个系统要高可用、高性能、高安全。”技术负责人点头,在文档里写下三个词,然后散会。

两个月后,系统上线。第一次大促,数据库主从延迟导致用户看到过期库存,下单失败率飙到 5%。复盘会上,开发说”我们做了主备切换,可用性没问题”;DBA 说”主从延迟是正常的,一致性和可用性本来就冲突”;产品说”我说的高可用就是用户下单不能失败”。

三个人说的”高可用”,是三件不同的事。

这不是沟通问题,是架构设计的输入就错了。“高可用”“高性能”“高并发”不是架构需求,它们是未经分解的愿望。架构师的工作,是把这些愿望变成可量化、可验证、可权衡的质量属性场景(Quality Attribute Scenario)。

为什么这件事重要?因为架构决策的代价是不对称的——前期花两天做质量属性分析,可能避免后期花两个月改架构。质量属性分析不到位,你的架构方案就是在猜——猜对了是运气好,猜错了是技术债。

本文从 SEI/CMU(Software Engineering Institute, Carnegie Mellon University)的质量属性理论出发,回答三个问题:

  1. 质量属性到底包含哪些维度,怎么定义才算”定义清楚了”?
  2. 怎么用 stimulus-response 模型把模糊需求拆成可执行的场景?
  3. 质量属性之间怎么冲突,怎么权衡?

上一篇中,我们讨论了架构的本质是一系列 trade-off 决策。质量属性就是 trade-off 的坐标系——没有坐标系,你连”这个决策牺牲了什么”都说不清。


一、质量属性:功能需求之外的”隐形需求”

一个系统的需求可以分成两类:

功能需求决定了系统的行为,质量属性决定了系统的架构。

Bass, Clements, Kazman 在《Software Architecture in Practice》(第四版,Addison-Wesley,2021)中给出了一个关键判断:架构主要由质量属性需求驱动,而非功能需求。 原因很直接——同一组功能需求,可以用完全不同的架构实现。一个订单系统,用单体可以做,用微服务也可以做,功能上都能满足”用户能下单”。但如果你要求”单节点故障后 30 秒恢复”和”新增支付方式两周上线”同时成立,架构选择就被约束了。

这里容易产生一个误解:功能需求不影响架构。不是的。功能需求影响架构的方式是间接的——通过功能的复杂度、功能之间的关联性、功能的变化频率来影响。但在同一组功能需求下,真正把你逼到”必须选微服务”或”必须选事件驱动”的,是质量属性。

质量属性不是形容词

工程中最常见的错误,是把质量属性当形容词用:

错误写法 问题
系统要高可用 多高?对谁?在什么条件下?
接口要快 多快?P50 还是 P99?空载还是满载?
代码要好维护 改什么类型的需求?改一次需要多少人天?
系统要安全 防什么?防外部攻击还是防内部越权?

这些写法的共同问题:不可测试,不可验证,不可用于架构决策。 架构评审时你拿着”系统要高可用”去讨论,五个人会理解成五种不同的东西。

质量属性必须是可量化的约束,而不是模糊的期望。下一节会介绍怎么做到这一点。


二、六大核心质量属性

SEI 在多年的架构研究和 ATAM(Architecture Tradeoff Analysis Method)实践中,识别出六个对架构影响最大的质量属性。这不是说只有这六个——可靠性、互操作性、可伸缩性(Scalability)等属性同样重要——而是这六个属性几乎在每个系统中都会出现,且它们之间的冲突最为典型。

可用性(Availability)

定义:系统在需要时可以正常提供服务的能力。

可用性不只是”服务器没挂”。Bass 等人的定义更精确:可用性关注的是故障(Fault)与恢复。一个系统的可用性取决于两个时间:

可用性 = MTTF / (MTTF + MTTR)

这个公式揭示了一个重要的工程洞察:提升可用性有两条路——要么减少故障频率(增大 MTTF),要么缩短恢复时间(减小 MTTR)。 很多团队把精力全花在”减少故障”上,但在复杂系统中,故障是不可避免的(Netflix 的 Chaos Engineering 实践正是基于这个前提)。真正高可用的系统,靠的是快速恢复。

SLA 中常见的”几个 9”对应的停机时间:

SLA 年停机时间 月停机时间 适用场景
99%(两个 9) 3.65 天 7.3 小时 内部工具、开发环境
99.9%(三个 9) 8.76 小时 43.8 分钟 一般业务系统
99.99%(四个 9) 52.6 分钟 4.38 分钟 核心交易系统
99.999%(五个 9) 5.26 分钟 26.3 秒 电信级、金融核心

从三个 9 到四个 9,停机时间缩短了 10 倍,但工程成本可能增加 10 倍以上。这就是典型的边际收益递减。

还需要注意一点:SLA 的计算口径会严重影响结论。“月度可用性 99.99%”和”年度可用性 99.99%“是不同的要求。月度 99.99% 允许你每月有 4.38 分钟停机,但如果你把全年的 52.6 分钟额度集中在一个月用完(比如一次大事故停了 50 分钟),那你月度 SLA 就爆了,尽管年度 SLA 可能还在线内。

性能(Performance)

定义:系统在指定时间约束内完成事件处理的能力。

性能不是”越快越好”。架构层面关注的性能有两个维度:

延迟和吞吐量不是独立的。当系统接近吞吐量上限时,排队效应会导致延迟急剧上升——这是排队论(Queueing Theory)的基本结论。Little’s Law 告诉我们:L = λ × W(系统中的平均请求数 = 到达率 × 平均等待时间)。当到达率 λ 接近服务速率 μ 时,平均等待时间 W 会趋向无穷大。

工程实践中的一个常见错误是只关注平均延迟。Gil Tene(Azul Systems CTO)在他的演讲”How NOT to Measure Latency”中指出:延迟分布通常是长尾的,P99.9 可能比 P50 高 100 倍。 如果你的 SLA 是”平均响应时间 < 100ms”,你可能在 P99 处给了用户 5 秒的等待体验。正确的做法是用分位数定义性能目标:P50 < 50ms,P99 < 200ms,P99.9 < 1s。

可修改性(Modifiability)

定义:对系统进行修改的成本和风险。

这里说的”修改”包括:新增功能、修复缺陷、适配新环境、重构。可修改性直接影响系统的长期维护成本。

可修改性的两个关键参数:

可修改性经常被混淆为”代码写得好不好”。不是。可修改性是一个架构属性——它取决于模块的划分方式、模块之间的依赖关系、接口的抽象程度。一个模块内部代码写得再好,如果它和其他 10 个模块紧耦合,改一个需求还是要动 10 个地方。

Robert C. Martin(Uncle Bob)在《Clean Architecture》(2017)中提出的依赖规则(Dependency Rule)——内层不依赖外层——本质上就是一个可修改性战术。它让你能替换外层的实现(比如换数据库、换 Web 框架),而不影响内层的业务逻辑。

衡量可修改性有一个简单但有效的启发式方法:画出最近 10 个需求的变更影响范围。 如果大多数需求只涉及 1-2 个模块,可修改性还行;如果经常涉及 5 个以上模块,架构大概率需要调整。

安全性(Security)

定义:系统抵御未授权访问,同时向合法用户提供正常服务的能力。

CIA 三要素(Confidentiality, Integrity, Availability)是安全性的经典框架:

安全性属性的特殊之处在于:它总是有一个主动的对手。其他属性面对的是”系统故障”“用户请求”“需求变更”,安全性面对的是”试图绕过你防线的人”。这意味着安全性需求会随时间变化——新的攻击手段出现,原来”够安全”的设计可能变得脆弱。

安全性还有一个特性:它是一个非对称问题。防守方需要保护所有入口,攻击方只需要找到一个漏洞。这对架构设计有深刻的影响——你不能只在”关键”位置做安全防护,而忽略”不重要”的接口,因为攻击者恰恰会选择你最薄弱的环节。

可测试性(Testability)

定义:通过测试发现系统缺陷的难易程度。

可测试性不是”写了多少测试用例”,而是架构本身是否便于测试。一个高度耦合的系统,即使你想写单元测试,也做不到——因为你没法把一个模块单独拎出来跑。

可测试性的核心指标:

依赖注入(Dependency Injection)之所以被广泛使用,本质上就是为了提高可测试性——它让你能用 mock 替换真实依赖,从而控制测试环境。

一个架构级的可测试性问题的典型信号:团队说”我们这个系统只能做端到端测试,没法做单元测试或集成测试”。这通常意味着模块之间的耦合度太高,每个模块都直接依赖其他模块的具体实现,而不是依赖抽象接口。

易用性(Usability)

定义:用户完成目标任务的难易程度。

易用性在传统架构讨论中经常被忽略,因为它看起来像”前端的事”。但实际上,架构决策直接影响易用性。举几个例子:

六个属性之外

ISO/IEC 25010:2011 定义了八大质量特性,除了上面六个之外,还包含可靠性(Reliability)、兼容性(Compatibility)、可移植性(Portability)等。SEI 的六个属性不是”标准答案”,而是在大量架构评估实践中发现最常驱动架构决策的六个维度。

在实际项目中,你可能还需要考虑:


三、质量属性场景:把模糊需求变成可执行约束

SEI 提出的质量属性场景(Quality Attribute Scenario)模型,是把”高可用”“高性能”这类模糊需求变成可测试约束的标准方法。

一个完整的质量属性场景包含六个要素:

要素 英文 含义
激励源(Source of Stimulus) Source 谁/什么触发了这个场景?
激励(Stimulus) Stimulus 具体触发了什么事件?
制品(Artifact) Artifact 系统的哪个部分被影响?
环境(Environment) Environment 事件发生时系统处于什么状态?
响应(Response) Response 系统应该做什么?
响应度量(Response Measure) Response Measure 怎么判断响应是否合格?

下面这张图展示了六要素之间的关系:

flowchart LR
    A["激励源<br/>Source of Stimulus"] --> B["激励<br/>Stimulus"]
    B --> C["制品<br/>Artifact"]
    C --> D["响应<br/>Response"]
    D --> E["响应度量<br/>Response Measure"]
    F["环境<br/>Environment"] --> C

    style A fill:#388bfd,stroke:#388bfd,color:#fff
    style B fill:#a371f7,stroke:#a371f7,color:#fff
    style C fill:#f0883e,stroke:#f0883e,color:#fff
    style D fill:#3fb950,stroke:#3fb950,color:#fff
    style E fill:#f85149,stroke:#f85149,color:#fff
    style F fill:#636e7b,stroke:#636e7b,color:#fff

图中展示的是一个线性流程:激励源产生激励,激励作用于制品,制品在特定环境下产生响应,响应用度量来验证。环境作为上下文条件约束整个过程。

这个模型的价值不在于复杂——它看起来很简单——而在于强迫你回答那些你一直回避的问题。“系统要高可用”只回答了响应(“要可用”),其余五个要素全是空白。

在实际使用中,填写这六个要素的过程本身就是需求分析。你会发现:

可用性场景示例

把”系统要高可用”拆成一个完整的质量属性场景:

要素 内容
激励源 硬件故障(机房内单台应用服务器)
激励 服务器宕机,进程停止响应
制品 订单服务
环境 大促期间,系统处于峰值负载(日常流量的 10 倍)
响应 自动摘除故障节点,将流量转移到健康节点,不丢失已接收的订单
响应度量 故障检测到恢复完成不超过 30 秒;恢复期间用户可感知的错误率不超过 0.1%

同样是”高可用”,下面是另一个完全不同的场景:

要素 内容
激励源 运维团队
激励 执行数据库版本升级,需要重启数据库实例
制品 整个交易链路
环境 凌晨低峰期,流量为日常的 5%
响应 数据库主从切换,应用层自动重连,交易链路不中断
响应度量 切换期间订单创建成功率不低于 99.95%;切换耗时不超过 10 秒

两个场景都叫”高可用”,但它们对架构的要求完全不同。第一个场景要求的是故障检测和自动恢复,第二个场景要求的是计划内维护的零停机。如果你只写”系统要高可用”,开发团队很可能只做了其中一个。

性能场景示例

把”接口要快”拆成场景:

要素 内容
激励源 终端用户
激励 发起商品搜索请求
制品 搜索服务
环境 正常负载,搜索索引包含 5000 万 SKU
响应 返回匹配结果,按相关性排序
响应度量 P99 延迟不超过 500ms;支持 2000 QPS 并发搜索

注意”环境”这个要素的重要性:同一个搜索接口,在 100 万 SKU 和 5000 万 SKU 下的性能表现可能差一个数量级。不写清楚环境,性能指标就是空中楼阁。

可修改性场景示例

把”代码要好维护”拆成场景:

要素 内容
激励源 产品经理
激励 需要接入一种新的支付渠道(如数字人民币)
制品 支付模块
环境 正常开发周期,团队有 2 名后端开发
响应 开发、测试、部署新支付渠道,不影响现有支付渠道的正常运行
响应度量 从需求确认到上线不超过 2 周;变更涉及的代码文件不超过 10 个;现有支付渠道的回归测试全部通过

这个场景的”响应度量”部分值得仔细看。“不超过 2 周”是时间约束,“不超过 10 个文件”是变更范围约束,“回归测试全部通过”是风险约束。三个度量从不同角度定义了”好维护”的含义。

如果你的支付模块高度耦合——支付渠道的选择逻辑、参数组装、签名算法、回调处理全部写在一个大类里——那加一种新渠道可能要改 30 个文件,测试可能破坏已有逻辑。这就说明当前架构不满足这个可修改性场景,需要重构(比如引入策略模式或插件机制)。

安全性场景示例

把”系统要安全”拆成场景:

要素 内容
激励源 外部攻击者
激励 尝试通过 API 越权访问其他用户的订单数据
制品 订单查询接口
环境 攻击者已通过合法渠道获取了自己的认证令牌
响应 系统拒绝越权请求,返回 403;记录攻击行为到安全日志;连续 5 次越权尝试后临时封禁该令牌
响应度量 越权请求 100% 被拦截;安全日志在 1 分钟内可查询;封禁动作在第 5 次尝试后 10 秒内生效

怎样算一个”好”的场景

一个好的质量属性场景必须满足三个条件:

  1. 可测试:你能写出一个测试(自动化或手动)来验证这个场景是否被满足。
  2. 无歧义:团队中任何两个人读到这个场景,理解应该一致。
  3. 和架构相关:这个场景的满足与否,取决于架构决策,而不仅仅是代码实现细节。

“接口响应时间不超过 200ms”满足前两条,但不一定满足第三条——如果瓶颈只是一条慢 SQL,那这是实现问题,不是架构问题。但如果瓶颈是跨服务的同步调用链太长,那就是架构问题了。

一般性场景 vs 具体场景

Bass 等人区分了两种场景:

在需求分析阶段,先用一般性场景做头脑风暴——“我们的系统在什么故障场景下需要自动恢复?”“我们的哪些接口有延迟要求?”——然后把它们细化成具体场景。

如果团队对一般性场景都列不出来,说明大家对系统面临的质量挑战缺乏认知。这时候可以用 SEI 提供的一般性场景清单(《Software Architecture in Practice》第四版第 4-9 章每个质量属性都给了完整的一般性场景表)作为检查清单,逐一对照。


四、质量属性树:从目标到场景的分解工具

质量属性场景解决的是”怎么定义清楚一个需求”,但在一个真实系统中,你可能有几十个场景。怎么组织它们?哪些更重要?哪些之间有冲突?

质量属性树(Quality Attribute Utility Tree)是 SEI 在 ATAM 方法中提出的分解工具。它的结构是:

效用(Utility)
├── 质量属性 1
│   ├── 属性细化 1.1
│   │   ├── 场景 1.1.1 (H, H)
│   │   └── 场景 1.1.2 (H, M)
│   └── 属性细化 1.2
│       └── 场景 1.2.1 (M, L)
├── 质量属性 2
│   └── ...
└── ...

每个叶子节点是一个具体的质量属性场景,用 (重要性, 实现难度) 两个维度打分。H(High)、M(Medium)、L(Low)。

一个电商系统的质量属性树

下面是一个中型电商系统的质量属性树示例。这棵树是在项目初期、架构评审前构建的,目的是让团队对”什么最重要”“什么最难”达成共识。

graph TD
    U["系统效用<br/>Utility"] --> AV["可用性<br/>Availability"]
    U --> PF["性能<br/>Performance"]
    U --> MD["可修改性<br/>Modifiability"]
    U --> SE["安全性<br/>Security"]

    AV --> AV1["故障恢复"]
    AV --> AV2["计划维护"]
    AV1 --> AV1a["单节点宕机 30s 恢复<br/>(H, M)"]
    AV1 --> AV1b["机房级故障 5min 切换<br/>(H, H)"]
    AV2 --> AV2a["数据库升级零停机<br/>(M, H)"]

    PF --> PF1["延迟"]
    PF --> PF2["吞吐量"]
    PF1 --> PF1a["下单接口 P99 < 200ms<br/>(H, M)"]
    PF1 --> PF1b["搜索接口 P99 < 500ms<br/>(H, H)"]
    PF2 --> PF2a["支持 5万 QPS 下单<br/>(H, H)"]

    MD --> MD1["业务扩展"]
    MD --> MD2["技术演进"]
    MD1 --> MD1a["新增支付方式 ≤ 2周<br/>(H, L)"]
    MD1 --> MD1b["新增商品类型 ≤ 1周<br/>(M, L)"]
    MD2 --> MD2a["替换消息队列 ≤ 4周<br/>(L, M)"]

    SE --> SE1["数据保护"]
    SE --> SE2["访问控制"]
    SE1 --> SE1a["用户密码加密存储<br/>(H, L)"]
    SE2 --> SE2a["越权访问检测<br/>(H, M)"]

    style U fill:#388bfd,stroke:#388bfd,color:#fff
    style AV fill:#3fb950,stroke:#3fb950,color:#fff
    style PF fill:#f0883e,stroke:#f0883e,color:#fff
    style MD fill:#a371f7,stroke:#a371f7,color:#fff
    style SE fill:#f85149,stroke:#f85149,color:#fff

图中展示了质量属性从顶层效用分解到具体场景的过程。每个叶子节点标注了 (重要性, 实现难度)。重点关注那些标记为 (H, H) 的节点——机房级故障切换、搜索延迟、5 万 QPS 下单——它们是架构设计的核心挑战。

质量属性树的使用要点

谁来建这棵树? 不是架构师一个人,而是业务方、开发、运维、测试一起。质量属性树的核心价值不是那棵树本身,而是建树过程中的讨论——“你说的高可用到底是什么意思?”“P99 < 200ms 在大促期间还能保证吗?”“这个安全需求真的值得为此把整个架构改了吗?”

打分标准是什么? 没有绝对标准。重要性由业务价值决定:如果这个场景不满足,业务损失有多大?实现难度由技术团队判断:在当前架构下,满足这个场景需要多大的改动?

打分过程中最有价值的是争论。如果业务方认为”机房级故障 5 分钟切换”是 (H, H),而运维认为是 (M, H),这个分歧本身就是信息——它说明团队对灾难恢复的紧迫性认知不一致,需要拉齐。

常见的误区:

  1. 把所有场景都标成 (H, H)。如果什么都重要,就等于什么都不重要。一棵树上 (H, H) 的叶子节点不应该超过总数的 30%。
  2. 只关注重要性,忽略实现难度。一个 (H, L) 的场景和一个 (H, H) 的场景,虽然同样重要,但在架构设计中投入的精力应该天差地别。前者用成熟方案直接解决,后者才需要深入讨论和原型验证。
  3. 树建完后束之高阁。质量属性树不是文档,是沟通工具。它应该贴在项目 wiki 上,每次架构讨论时拿出来对照。

树建完之后呢? 质量属性树是 ATAM 架构评估的输入。在下一篇关于架构决策中会进一步讨论如何基于这些场景做出可追溯的决策。在架构评估一文中会详细展开 ATAM 的完整流程。


五、质量属性之间的冲突与联动

质量属性之间不是独立的。提升一个属性,往往要以牺牲另一个属性为代价。这是架构设计中最核心的 trade-off。

冲突矩阵

下面这张表列出了常见的质量属性冲突关系。“冲突”不是说它们绝对不可兼得,而是说在有限的资源和约束下,你必须做出取舍。

属性对 冲突表现 典型场景
性能 vs 安全性 加密、签名、鉴权都消耗计算资源 TLS 握手增加首次连接延迟 50-100ms;每次 API 调用做 JWT 验签增加 CPU 开销
可用性 vs 一致性 CAP 定理的直接体现 分布式数据库在网络分区时,要么拒绝写入(牺牲可用性),要么接受脑裂(牺牲一致性)
可修改性 vs 性能 抽象层、接口隔离都有运行时代价 六边形架构的端口-适配器模式引入额外的间接调用层;依赖注入的反射查找增加初始化耗时
安全性 vs 易用性 更严格的安全控制意味着更多的用户操作步骤 多因素认证(MFA)增加登录步骤;频繁的 session 过期要求用户重新登录
性能 vs 可修改性 高性能优化往往引入紧耦合 为了减少序列化开销而直接共享内存结构,导致模块间紧耦合;为了减少网络调用而将服务合并
可测试性 vs 性能 为测试留出的观测点和注入点有运行时成本 日志采集、指标上报、分布式追踪的 span 注入,都占用 CPU 和内存
可用性 vs 安全性 安全机制本身可能成为可用性的瓶颈 证书过期导致整个服务不可用;WAF 误判导致正常流量被拦截

深入分析三组典型冲突

性能 vs 安全性

每一次 HTTPS 请求都包含 TLS 握手开销。在 TLS 1.2 中,完整握手需要 2-RTT;TLS 1.3 把它压缩到 1-RTT(甚至 0-RTT 恢复)。但即使是 1-RTT,在跨地域场景下也意味着几十毫秒的额外延迟。

更实际的例子:一个支付系统,每笔交易需要做以下安全操作:

请求签名验证      → 约 0.5ms(RSA-2048)
权限校验          → 约 1-2ms(RBAC 规则匹配)
请求参数加密传输  → TLS 层已覆盖
敏感字段脱敏日志  → 约 0.3ms(正则替换)
审计日志落盘      → 约 1-3ms(异步写入)

单独看每一项都不多,加起来就是 3-6ms 的固定开销。如果你的性能目标是 P99 < 10ms,安全开销就占了一半。

工程上的常见做法是分级处理:不是所有接口都需要相同级别的安全措施。查询商品详情和发起支付,安全要求差一个等级。架构上用网关层做统一的基础鉴权,在业务层对高敏感操作做额外的安全校验。

这个”分级”思路其实是一个通用模式——在多个质量属性之间做 trade-off 时,不要在系统全局做”一刀切”的决策,而是按业务场景分级。不同的接口、不同的数据、不同的用户群,对同一个质量属性的要求可能差好几个等级。把这些差异识别出来,分别用不同的架构战术覆盖,是避免过度设计和设计不足的关键。

可用性 vs 一致性

这是分布式系统中讨论最多的冲突。Eric Brewer 在 2000 年提出的 CAP 定理(Brewer, “Towards Robust Distributed Systems”, PODC 2000 Keynote;Gilbert & Lynch 于 2002 年给出形式化证明)指出:在网络分区(Partition)发生时,系统只能在一致性(Consistency)和可用性(Availability)之间二选一。

但 CAP 定理经常被误用。它讨论的是网络分区期间的行为,而不是日常运行时的行为。日常运行时,一致性和可用性不是非此即彼。更实用的思考框架是 Daniel Abadi 提出的 PACELC(Abadi, “Consistency Tradeoffs in Modern Distributed Database System Design”, IEEE Computer, 2012):

第二个问题才是日常开发中真正要回答的。你的系统在正常运行时,用户读到的数据允许有多少延迟的”旧”?1 秒?5 秒?还是必须是最新的?

实际工程中,很多系统不需要强一致性。用户的购物车内容延迟 1 秒更新,没有人会在意。但库存扣减如果不一致,就会导致超卖。同一个系统内的不同数据,对一致性的要求可能完全不同。 这就是为什么你不能简单地说”我们选 AP”或”我们选 CP”——你需要按数据粒度做决策。

可修改性 vs 性能

“好的架构就是多加几层抽象”——这句话只对了一半。每一层抽象都有代价。

举一个实际例子。一个数据处理管道,最初的设计是直接调用:

func ProcessOrder(order *Order) error {
    validated := validateOrder(order)
    enriched := enrichOrder(validated)
    saved := saveOrder(enriched)
    return notify(saved)
}

后来为了”可修改性”,改成了管道模式:

func ProcessOrder(order *Order) error {
    pipeline := NewPipeline(
        ValidateStep{},
        EnrichStep{},
        SaveStep{},
        NotifyStep{},
    )
    return pipeline.Execute(order)
}

管道模式让你可以灵活增删处理步骤,但引入了接口分发、上下文传递、错误聚合的开销。在每秒处理几百个订单的系统里,这个开销可以忽略。在每秒处理几十万条数据的实时管道里,这些间接调用层可能导致 GC 压力、cache miss 增加,成为性能瓶颈。

Go 语言的设计哲学提供了一个有趣的视角。Go 有意限制了抽象层级——没有泛型继承、没有复杂的类型系统——部分原因就是为了性能的可预测性。Rob Pike 曾说:“A little copying is better than a little dependency.” 这本质上就是在可修改性和性能之间选择了性能。

但这不意味着”少抽象就是对的”。在业务系统中(相对于基础设施),需求变化的速度远远超过性能优化的收益。一个交易系统的订单处理逻辑每个月都在变(新的促销规则、新的合规要求、新的支付渠道),为了省几毫秒的抽象开销而把代码写成铁板一块,两个月后就会变成改不动的泥球。

我的经验判断是:可修改性优先是默认策略,除非性能瓶颈出现在这些抽象层上——用 profiler 证明了才算。过早的性能优化牺牲可修改性,是大多数系统架构腐化的起点。

联动关系:不只有冲突

质量属性之间也存在正向联动:

属性对 联动表现
可测试性 → 可修改性 系统可测试性高,意味着模块隔离度好;模块隔离度好的系统,修改一个模块的成本自然更低
可修改性 → 可测试性 低耦合的模块更容易编写独立测试,形成正循环
可观测性 → 可用性 故障检测依赖可观测性——如果你看不到系统内部状态,故障恢复的 MTTR 就无法缩短
安全性 → 可用性 安全机制(如 DDoS 防护、WAF)在拦截攻击流量的同时,也保护了服务的可用性

理解联动关系的实际意义在于:当你投资于某个质量属性时,可能同时获得另一个属性的提升。反过来,当你在某个属性上偷懒时,可能同时损害了另一个属性。


六、架构战术:每个质量属性的”工具箱”

质量属性场景定义了”要达到什么”,架构战术(Architecture Tactics)回答的是”怎么达到”。Bass 等人在《Software Architecture in Practice》中把战术定义为:影响单个质量属性响应的设计决策。

战术不是架构模式。模式(如微服务、事件驱动)是一组协调的设计决策,通常同时影响多个质量属性。战术是更原子的单元——一个模式往往由多个战术组合而成。

可用性战术

可用性战术分三类:故障检测、故障恢复、故障预防。

故障检测: - 心跳(Heartbeat):定期发送存活信号,超时则判定故障。简单但有误判风险——网络抖动可能导致误摘。 - Ping/Echo:主动探测,比心跳多了应用层的验证。健康检查(Health Check)就是这个战术的实现。 - 异常检测(Exception Detection):通过监控指标(错误率、延迟分位数)发现降级趋势,在完全故障前预警。

故障恢复: - 主备切换(Active-Passive Redundancy):维护一个热备节点,故障时切换。关键问题是切换期间的数据一致性。 - 主主复制(Active-Active Redundancy):多个节点同时处理请求。提高了可用性,但引入了数据同步和冲突解决的复杂性。 - 重试(Retry):对瞬时故障有效,但必须配合退避策略(Exponential Backoff),否则可能引发重试风暴。

故障预防: - 从服务中移除(Removal from Service):主动将即将到期或出现异常的节点从服务池中移除,计划性替换。Netflix 的 Chaos Monkey 会随机杀掉生产实例,目的之一就是验证这个战术是否真正生效。 - 事务(Transaction):通过 ACID 事务保证操作的原子性,避免部分失败导致的数据不一致。 - 进程监控(Process Monitor):监控进程健康状态,异常退出时自动重启。Systemd 的 Restart=always、Kubernetes 的 restartPolicy 都是这个战术的实现。

选择哪些战术,取决于你的质量属性场景。如果场景要求”30 秒恢复”,你需要心跳检测(秒级发现故障)+ 多副本冗余(故障发现后秒级切换)。如果场景要求”不丢数据”,你还需要在故障恢复战术上加同步复制或持久化队列。

性能战术

性能战术分两类:控制资源需求、管理资源。

控制资源需求: - 管理采样率:不是每个事件都需要处理。监控系统采样 1% 的 trace,搜索引擎不需要实时索引每一条变更。采样率的设定本身就是一个 trade-off——采样太少会漏掉问题,采样太多会浪费资源。 - 限制事件响应:限流(Rate Limiting)、过载保护(Load Shedding)——在资源不足时主动丢弃低优先级请求。这需要事先定义好请求的优先级——哪些请求可以丢,哪些绝对不能丢。 - 减少计算开销:缓存计算结果,避免重复运算。CDN 和本地缓存都是这个战术的实例。HTTP 的 ETagIf-None-Match 也是这个战术在协议层的体现。

管理资源: - 引入并发:多线程、异步 I/O、协程——让多个请求并行处理。但并发引入了自身的复杂性:竞争条件、死锁、上下文切换开销。并发不是免费的午餐。 - 增加资源:水平扩展(加机器)、垂直扩展(加 CPU/内存)。水平扩展的前提是架构支持无状态设计,这又和可修改性挂钩。 - 维护数据的多个副本:读写分离、缓存层——用空间换时间。但副本意味着一致性挑战,这又和可用性 vs 一致性的冲突挂钩。

可以看到,性能战术几乎每一个都会牵动其他质量属性。这就是为什么孤立地优化一个属性行不通——你必须在全局视角下做权衡。

可修改性战术

可修改性战术的核心思路:减少耦合,增加内聚。

延迟绑定值得多说一句。它是可修改性和可部署性的交叉点。如果你的架构支持运行期绑定(比如通过 Feature Flag 控制新功能的灰度发布),那你就能做到”不重新部署也能修改系统行为”。这在高频迭代的互联网产品中极其有价值。

安全性战术

安全性战术覆盖”抵御、检测、恢复”三个阶段:

抵御攻击: - 认证(Authenticate):验证请求方身份。密码、证书、令牌、生物特征。 - 授权(Authorize):验证请求方权限。RBAC、ABAC、ACL。 - 维护数据机密性:传输加密(TLS)、存储加密(AES)、字段级加密。 - 维护完整性:数字签名、消息认证码(HMAC)、校验和。 - 限制暴露:最小权限原则、网络分段、零信任架构。

检测攻击: - 入侵检测系统(IDS):基于规则或行为基线检测异常流量。 - Web 应用防火墙(WAF):过滤已知攻击模式(SQL 注入、XSS 等)。 - 异常行为分析:基于用户行为基线检测偏离(如某个账号突然从陌生 IP 大量请求)。

从攻击中恢复: - 审计追踪(Audit Trail):记录所有安全相关事件,用于事后分析和取证。 - 恢复(Restore):在数据被篡改后恢复到已知良好状态。

可测试性战术

战术组合的实际思路

单独一个战术很少能满足一个完整的质量属性场景。实际架构设计中,需要把多个战术组合起来。

以前面”单节点宕机 30 秒恢复”的可用性场景为例,涉及的战术组合:

故障检测:心跳(Kubernetes liveness probe,每 5 秒探测一次)
         + 异常检测(Prometheus 监控错误率,超阈值告警)

故障恢复:主主冗余(多副本部署,至少 3 个 Pod)
         + 从服务中移除(摘掉故障节点,流量不再路由过去)

故障预防:事务(确保在途请求不会因为节点下线而丢失)

理解战术的粒度很重要。如果你直接跳到”用 Kubernetes”这个层面思考,你看到的是工具;如果你在战术层面思考,你看到的是设计决策——而同一个战术可以用不同的工具实现。


七、工程案例:一个”高可用”需求如何变成五个互相矛盾的场景

下面这个案例来自一个真实的中型电商系统架构评审过程,已做脱敏处理。

背景

项目启动时,技术总监提了一条需求:系统可用性要达到 99.99%

团队觉得清楚了:做主备、做健康检查、做自动切换,搞定。三个月后,系统上线,开始出问题。

拆解过程

架构评审时,用质量属性场景模板逐一拆解”99.99% 可用性”,发现它实际上包含了五个不同的场景:

场景 A:应用节点故障恢复

要素 内容
激励源 硬件故障
激励 单台应用服务器宕机
环境 正常负载
响应 自动摘除故障节点,流量转移
响应度量 30 秒内恢复,用户无感知

这个场景团队做到了。Kubernetes 的 liveness probe + 滚动部署,基本能覆盖。

场景 B:数据库主从切换

要素 内容
激励源 数据库主节点故障
激励 MySQL 主节点进程 crash
环境 峰值负载
响应 从节点提升为主节点,应用自动重连
响应度量 切换时间不超过 15 秒,切换期间不丢数据

这个场景做了一半。MHA(Master High Availability)能完成切换,但”不丢数据”在半同步复制下无法 100% 保证。团队用的异步复制,切换时平均丢 0.5-1 秒的数据。

场景 C:计划内维护零停机

要素 内容
激励源 运维团队
激励 部署新版本、升级中间件
环境 任何时间
响应 滚动更新,不中断服务
响应度量 部署过程中错误率不超过 0.01%

这个场景在应用层做到了(Kubernetes 滚动更新),但数据库 Schema 变更时没有做到——ALTER TABLE 会锁表,锁表期间写入全部超时。

场景 D:依赖服务降级

要素 内容
激励源 第三方支付网关
激励 支付网关响应超时
环境 大促期间
响应 自动降级到备用支付渠道,或排队重试
响应度量 支付成功率不低于 95%

这个场景完全没做。支付网关超时时,订单服务同步等待,线程池耗尽,连带影响其他接口。

场景 E:跨机房容灾

要素 内容
激励源 机房基础设施故障(光纤被挖断、电力中断)
激励 整个机房不可用
环境 任何时间
响应 流量自动切换到备用机房
响应度量 5 分钟内完成切换,RPO(Recovery Point Objective) < 30 秒

这个场景只存在于 PPT 里。虽然搭了备用机房,但数据同步延迟约 2 分钟,DNS 切换需要手动操作,实际 RTO(Recovery Time Objective)约 30 分钟。

冲突浮现

五个场景摊开后,冲突立刻浮现:

  1. 场景 B 和场景 E 的数据一致性要求互相矛盾。 场景 B 要求”不丢数据”,意味着需要同步复制;场景 E 要求跨机房容灾,但同步复制跨机房的延迟(通常 5-20ms 单程)会严重影响写入性能。
  2. 场景 D 需要异步化,但现有架构是同步调用链。 引入消息队列做异步支付,会改变订单的状态机,影响前端的交互流程——这又触及了易用性。
  3. 五个场景加起来的工程成本,远超团队最初的预估。 团队原本以为”做个主备切换”就能达到 99.99%,实际上需要改造数据库复制模式、引入熔断器、改造部署流程、搭建跨机房同步链路。

最终决策

团队没有试图同时满足五个场景,而是做了优先级排序:

  1. 场景 A 和场景 D 优先——因为它们影响用户体验最直接,且实现成本可控。
  2. 场景 C 降级处理——数据库 Schema 变更暂时安排在凌晨低峰窗口,用 pt-online-schema-change 减少锁表时间。
  3. 场景 B 接受数据丢失风险——将复制模式从异步改为半同步,丢失窗口从 1 秒缩小到接近 0,但不做强同步。
  4. 场景 E 推迟到下一阶段——跨机房容灾需要重构数据同步链路,当前阶段资源不够。

这个过程的核心教训:“99.99% 可用性”不是一个需求,是至少五个需求。 如果不拆,你会用一个架构方案去回应五种不同的故障场景,结果哪个都做不好。

从案例中提炼的方法

这个案例展示了一个可复用的工作流:

第一步:列举。 把一个模糊的质量属性目标拆成尽可能多的具体场景。不要怕列多了——你可以后面砍,但不能遗漏。

第二步:量化。 每个场景都用 stimulus-response 六要素模板填写。填不出”响应度量”的场景,说明你还没想清楚。

第三步:排优先级。 用 (重要性, 难度) 二维打分,识别出 (H, H) 的场景作为架构设计的焦点。

第四步:暴露冲突。 把场景两两对照,找出互相矛盾的地方。冲突不是问题——不知道有冲突才是问题。

第五步:决策并记录。 对每个冲突做出显式的 trade-off 决策,记录在 ADR(Architecture Decision Record)中。为什么选 A 不选 B?牺牲了什么?这些信息对未来的架构演进至关重要。

回头看,这个案例中最大的问题不是技术选型,而是需求阶段的失职。如果在项目启动时就用质量属性场景模板拆解”99.99% 可用性”,那五个场景和三个冲突在第一周就会浮现,而不是上线两个月后在事故复盘会上才被发现。


八、质量属性的实践陷阱

不要在项目初期锁定所有质量属性

实际工程中,很多质量属性需求在项目初期是模糊的——你不知道”够好”是什么级别。这很正常。

常见的做法是:在项目初期识别出 5-8 个最关键的场景(质量属性树中标记为 (H, H) 和 (H, M) 的),围绕它们做架构设计。其余的场景先给一个”及格线”设计,等系统上线、收集到真实数据后再迭代。

尝试一开始就满足所有质量属性,结果往往是过度设计。一个日活 1000 的内部工具,不需要四个 9 的可用性;一个原型阶段的新产品,不需要在可修改性上花大量精力——因为你还不知道它会不会活下来。

质量属性是需要持续维护的

质量属性不是写在文档里就完事的。业务规模增长 10 倍,原来”够用”的性能指标可能变成瓶颈。安全威胁模型会随着攻击手段的演进而变化。可修改性会随着代码库的膨胀而退化。

我认为,质量属性场景应该像测试用例一样定期回顾。每个季度花半天时间,重新审视那棵质量属性树:哪些场景的重要性变了?哪些场景的实现难度因为技术演进降低了?哪些新的场景需要加进来?

警惕”隐式”质量属性需求

有些质量属性需求不会出现在需求文档里,但它们真实存在:

区分”架构级”质量属性和”实现级”质量属性

不是所有质量问题都需要在架构层面解决。一个关键的判断标准是:修复这个质量问题,是否需要改变系统的结构?

架构评审应该聚焦在架构级的质量属性场景上。把实现级的问题带到架构评审会上,只会浪费所有人的时间。

不要用”最佳实践”替代场景分析

“做了读写分离所以性能没问题”——这种推理方式很危险。读写分离是一个架构战术,它解决的是”读请求压垮主库”这个特定场景。如果你的瓶颈是写入性能,读写分离帮不了你。

每一个架构决策都应该对应至少一个质量属性场景。“我们为什么要做读写分离?因为场景 PF-2 要求搜索接口支持 2000 QPS 并发读取,当前单主库在 500 QPS 时 P99 延迟就超标了。” 这样的表述才是可追溯的。


九、总结

质量属性是架构设计的坐标系。没有它,架构讨论就是各说各话。

本文的核心结论:

  1. “高可用”“高性能”“高并发”不是架构需求,是未分解的愿望。架构师的工作是把它们拆成可量化、可验证的质量属性场景。

  2. 一个完整的质量属性场景包含六个要素:激励源、激励、制品、环境、响应、响应度量。缺了任何一个,场景就无法用于架构决策。

  3. 质量属性树是从目标到场景的分解工具,核心价值在于建树过程中的讨论和共识,而不是那棵树本身。

  4. 质量属性之间存在冲突。性能 vs 安全性、可用性 vs 一致性、可修改性 vs 性能——这些冲突不是要消除的 bug,而是要显式做出的 trade-off。

  5. 架构战术是实现质量属性的原子单元。理解战术,才能在具体场景中组合出合适的方案,而不是盲目套用架构模式。

最后一点个人判断:在我参与过的项目中,质量属性需求分析做得好不好,几乎可以预测项目后期会不会出大问题。做得好的团队,在需求阶段就把冲突暴露出来了,后面的讨论是”怎么取舍”;做得差的团队,到了上线前才发现”原来高可用和高一致性不能同时要”,这时候改架构的成本已经高到不可接受。

质量属性场景和质量属性树,是 ATAM 架构评估方法的核心输入。在架构决策架构评估两篇中,会进一步展开如何基于这些工具做出可追溯的决策、系统性地发现架构风险。


参考资料

书籍

论文

标准与规范


上一篇:什么是软件架构:从代码结构到系统决策

下一篇:架构决策与 ADR:如何做出可追溯的技术决策

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-04-13 · architecture

【系统架构设计百科】架构评估:ATAM 与 trade-off 分析实战

架构评审最怕'感觉还行'。本文完整拆解 ATAM 方法的三阶段九步骤流程,从质量属性效用树的构建、敏感点与权衡点的识别,到风险主题的归纳,用一个电商平台案例走完全过程。同时给出 ATAM 太重时的轻量替代方案。

2026-04-13 · architecture

【系统架构设计百科】连接池设计:被忽视的性能杀手

每一次网络请求的背后,都隐藏着建立连接的成本。当应用服务器需要与数据库通信时,一次完整的连接建立过程可能消耗数十毫秒;在高并发场景下,频繁创建和销毁连接会迅速耗尽系统资源,成为整个架构中最容易被忽视的性能瓶颈。连接池(Connection Pool)技术通过预先创建并复用连接,将单次连接获取的时间从毫秒级压缩到微秒级,…

2026-04-13 · architecture

【系统架构设计百科】告警策略:如何避免"狼来了"

大多数团队的告警系统都在制造噪声而不是传递信号。阈值告警看似直观,实则产生大量误报和漏报,值班工程师在凌晨三点被叫醒,却发现只是一次无害的毛刺。本文从告警疲劳的工业数据出发,拆解基于 SLO 的多窗口燃烧率告警算法,深入 Alertmanager 的路由、抑制与分组机制,结合 PagerDuty 的告警疲劳研究和真实工程案例,给出一套可落地的告警策略设计方法。

2026-04-13 · architecture

【系统架构设计百科】复杂性管理:架构的核心战场

系统复杂性是架构腐化的根源——本文从 Brooks 的本质复杂性与偶然复杂性划分出发,结合认知负荷理论与 Parnas 的信息隐藏原则,系统阐述复杂性的来源、度量与控制手段,并给出可操作的架构策略


By .