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

【系统架构设计】什么是软件架构:从代码结构到系统决策

文章导航

分类入口
architecture
标签入口
#software-architecture#4+1-view#architecture-definition#architecture-decision#IEEE-42010

目录

你刚入职一家电商公司,接手一个订单系统。代码仓库里有 200 多个微服务,你花了三天才搞清楚一个下单请求经过哪些服务。第四天,你想把订单状态从数据库轮询改成事件驱动,技术经理告诉你:“这是架构决策,不能随便改。”

你困惑了:写一个发布-订阅的消费者,代码量不过几十行,为什么叫”架构决策”?把 MySQL 换成 PostgreSQL 算架构决策吗?把一个 for 循环改成并发处理算吗?

这个困惑不是你一个人的。Martin Fowler 在 2003 年写道:架构这个词,是软件行业里最被滥用的术语之一。二十多年过去了,这个判断依然成立。每个团队都在”做架构”,但对”什么是架构”的理解差异巨大——有人觉得画个系统拓扑图就是架构,有人觉得选技术栈就是架构,有人觉得写代码分层就是架构。

这篇文章不打算给出一个”终极定义”。它的目标是:把几种主流定义摊开来比较,找出它们真正的分歧点,建立一个判断框架——当你面对一个具体的技术决策时,能回答”这算不算架构问题”。


一、三种定义,三种视角

软件架构(Software Architecture)的定义,学术界和工业界至少提出过上百种。SEI(Software Engineering Institute)曾经维护过一个架构定义的集合页面,收录了超过 150 个定义。但真正有影响力的、被反复引用和讨论的,主要是以下几种。

Grady Booch:架构是重大设计决策

Grady Booch 是 UML 的三位创始人之一,在《Object-Oriented Analysis and Design with Applications》以及后续多次演讲中反复表达过这个观点:

Architecture represents the significant design decisions that shape a system, where significant is measured by cost of change.

—— Grady Booch

翻译成中文:架构是那些塑造系统的重大设计决策,而”重大”的衡量标准是变更成本。

这个定义的核心词是 cost of change(变更成本)。一个决策是不是架构决策,不看它多”高层”,而看改它要花多少代价。按这个逻辑:

Booch 定义的优势是可操作性强:你不需要争论某个决策”够不够高层”,只需要估算改变它的成本。但它也有局限——变更成本是事后才能精确衡量的,事前只能估算。而估算本身就依赖经验,新手和老手对同一个决策的成本估算可能差距很大。

Martin Fowler:架构是”重要的东西,不管它是什么”

Fowler 在《Patterns of Enterprise Application Architecture》(2002)的开篇就讨论了架构的定义,并在后续博客文章中反复回到这个话题。他引用了 Ralph Johnson 的邮件讨论,给出了一个看起来很”滑头”但实际上非常深刻的判断:

Architecture is about the important stuff. Whatever that is.

—— Martin Fowler, 引用 Ralph Johnson 的讨论

这个定义的关键不在于它说了什么,而在于它拒绝说什么——它拒绝给出一个客观标准,因为 Fowler 认为”什么是重要的”本身就是主观的、上下文相关的。同一个决策,在一个系统里是架构问题,在另一个系统里可能只是实现细节。

Fowler 在同一篇文章中补充了另一个定义视角:

Another common style of definition for architecture is that it’s “the design decisions that need to be made early in a project.” I complain about that too, because it’s more like the decisions you wish you could get right early in a project.

—— Martin Fowler, “Who Needs an Architect?”, IEEE Software, 2003

这句话里暗含了一个判断:架构决策之所以重要,不是因为它们”应该”在早期做出,而是因为它们做错了之后很难改。

Ralph Johnson:架构是你希望一开始就做对的决策

Ralph Johnson 是”四人帮”(GoF)设计模式的作者之一。他对架构的定义经常通过 Fowler 的引用被讨论:

Architecture is the decisions that you wish you could get right early in a project, but that you are not necessarily more likely to get them right than any other.

—— Ralph Johnson, via Martin Fowler

这个定义比 Booch 的更悲观,也更诚实。它承认了两件事:

  1. 架构决策确实很重要,做错了代价很大。
  2. 但架构师并不比其他人更擅长在早期做出正确的架构决策。

这个观点直接挑战了”架构师应该在项目早期确定架构”的传统信念。如果架构决策注定带有不确定性,那么架构工作的重点就不应该是”在第一天就做出完美的决策”,而应该是”让决策可以被修正”和”降低错误决策的代价”。

这里有一个工程判断,不是学术结论:Johnson 的定义在敏捷开发语境下尤其有价值。瀑布模型假设需求是已知的,所以架构可以在早期确定;敏捷方法论承认需求会变,但往往没有明确回答”架构决策怎么办”。Johnson 的定义给出了一个方向——既然决策会错,就把精力放在降低”做错”的代价上,而不是追求”做对”。

IEEE 1471/ISO 42010:标准化定义

IEEE 1471(后来演变为 ISO/IEC/IEEE 42010:2011)给出了标准化的定义:

Architecture: the fundamental organization of a system, embodied in its components, their relationships to each other and the environment, and the principles governing its design and evolution.

—— ISO/IEC/IEEE 42010:2011

这个定义更偏描述性:架构是系统的基本组织方式,体现在组件、组件间关系、以及指导设计与演进的原则之中。

ISO 42010 的定义不像前三者那样强调”决策”,而是强调”结构”。它更适合用来描述一个已有系统的架构,而不是指导如何做架构决策。

ISO 42010 另一个重要贡献是引入了”架构描述”(Architecture Description)的概念框架。它明确区分了三层关系:

这个三层区分的实际价值在于:它提醒我们,一张架构图不等于架构本身。架构图只是对架构的一种描述,可能是不完整的、过时的、甚至是错误的。系统的真实架构存在于运行中的代码和基础设施里,不存在于 PPT 里。

三种视角的深层分歧

四种定义表面上是在定义同一个概念,实际上它们反映了三种不同的哲学立场:

本质主义(Essentialism): IEEE 42010 的定义隐含一个假设——架构是系统的客观属性,可以被准确描述。就像一栋建筑的承重结构是客观存在的,不管有没有图纸。

实用主义(Pragmatism): Booch 的定义不关心”架构是什么”的本体论问题,而是关心”什么决策值得被当做架构来对待”。他给出了一个操作性标准:变更成本。

社会建构主义(Social Constructivism): Fowler 和 Johnson 的定义走得最远——“重要的东西,不管它是什么”意味着架构不是系统的客观属性,而是团队共同认定的主观判断。同一个系统,不同团队可能对”什么是架构决策”有完全不同的理解。

这三种立场不存在谁对谁错的问题,但它们在实际工作中会导致不同的行为:

四种定义的对比

维度 Booch Fowler/Johnson Johnson(悲观版) IEEE 42010
核心关注 变更成本 重要性(上下文相关) 早期决策的风险 系统结构
判断标准 改它要花多少钱 由相关人员主观判断 做错了能不能改回来 组件与关系的组织方式
隐含假设 架构可以被识别 架构是社会建构 架构决策本质上有风险 架构可以被客观描述
对架构师的要求 识别高变更成本的决策 理解上下文,判断什么重要 设计可演进的架构 维护架构文档
适用场景 评估单个决策的架构性 团队沟通对齐 长期演进的系统 架构评审与文档化

这张表不是要给出”哪个定义最好”的结论。四种定义各有适用场景。但如果只能选一个作为日常工作的判断框架,Booch 的变更成本视角最具可操作性——面对一个决策,估算一下改变它的代价,就能初步判断它是不是架构级的。

这里有一个综合判断:在实际项目中,三种非标准的定义不是互相排斥的,而是互补的。Booch 的定义告诉你如何识别架构决策(看变更成本);Fowler 的定义提醒你上下文很重要(同一个决策在不同项目中可能有不同的架构性);Johnson 的定义指导你如何应对架构决策(降低错误成本,而不是追求一次做对)。把三者结合起来:

  1. 先用 Booch 的标准识别出高变更成本的决策。
  2. 用 Fowler 的视角评估这个决策在当前项目上下文中的重要性。
  3. 用 Johnson 的策略设计决策的执行方式——能不能延迟?能不能让它更可逆?

二、架构与设计的边界

“架构和设计有什么区别?” 这个问题在技术社区被问了几十年,至今没有共识答案。但问题本身的提法就有误导性——它暗示架构和设计之间有一条清晰的分界线,实际上并没有。

从连续谱看架构与设计

更准确的理解是:所有技术决策构成一个连续谱(Spectrum),一端是纯粹的实现细节,另一端是系统级的架构决策。中间有大量灰色地带。

实现细节 ◄──────────────────────────────────────► 架构决策
                                                    
变量命名     算法选择     API 设计     通信模式     数据存储选型
代码格式     数据结构     模块边界     部署拓扑     一致性模型
局部重构     错误处理     依赖方向     技术栈       数据分区策略
                                                    
变更成本:低 ◄──────────────────────────────────► 变更成本:高
影响范围:小 ◄──────────────────────────────────► 影响范围:大
可逆性:  高 ◄──────────────────────────────────► 可逆性:  低

在这个谱上,没有一条硬性的分界线。但有几个可以辅助判断的维度:

维度一:影响范围。 一个决策影响的代码量、团队数、服务数越多,越偏向架构端。在一个函数里选用快排还是归并排序,只影响这一个函数;选用 REST 还是 gRPC 做服务间通信,可能影响所有服务的接口定义、错误处理、序列化方式和监控方案。

维度二:变更成本。 改一个变量名的成本是 IDE 的全局替换,几分钟完成;把同步调用改成异步消息队列,可能需要几个团队协调好几个迭代。

维度三:可逆性。 数据库从 MySQL 迁移到 PostgreSQL,数据要全量迁移、SQL 方言要适配、ORM 层可能要换、存储过程要重写——这基本是不可逆的(虽然技术上可行,但成本高到几乎没人愿意做第二次)。

维度四:决策的传播性。 有些决策会”传染”——你选了 Kafka 做消息中间件,下游的消费者要适配 Kafka 的消费模型(consumer group、offset 管理、分区策略),上游的生产者要适配 Kafka 的发送语义。一个决策导致大量后续决策被约束,这通常就是架构决策。

一个具体的例子:日志库的选择

看一个容易引起争议的例子:选择日志库。

在一个只有 3 个开发者的小项目里,选 logrus 还是 zap 几乎不是架构决策——换一个库的成本可能是半天时间。

但在一个有 200 个微服务的组织里,统一日志库是架构决策:

同一个决策,在不同上下文中,可以是实现细节,也可以是架构决策。这正是 Fowler 说”whatever that is”的原因。

另一个例子:单体应用里的模块边界

再看一个更微妙的例子。在一个单体应用里,你要决定”用户管理”和”权限管理”是放在同一个模块里还是拆成两个模块。

如果两个模块共享同一张数据库表(比如 users 表里既有用户信息又有权限字段),那么将来拆分它们就要做数据模型拆分——这个成本取决于有多少查询是跨两个概念做 JOIN 的。

如果两个模块通过清晰的接口交互,各自管理自己的数据表,那么将来拆分成独立服务的成本就低很多。

所以”模块怎么划分”看起来是设计问题,但一旦模块间通过共享数据库表产生了隐式耦合,它就悄悄变成了架构问题——因为解耦的成本已经积累起来了。

这个例子说明了一个重要现象:架构性不是决策本身的属性,而是决策在特定上下文中产生的后果的属性。 一个看似无关紧要的设计选择,在系统演进过程中可能积累出高昂的变更成本,事后回看,它就是一个架构决策。

判断框架

综合以上讨论,可以用一个简单的检查清单来判断一个决策是否属于架构层面:

□ 变更成本是否超过一个迭代的工作量?
□ 变更是否需要多个团队协调?
□ 变更是否要求修改外部接口或数据格式?
□ 变更是否影响系统的质量属性(性能、可用性、安全性)?
□ 这个决策是否约束了后续大量决策的选择空间?

如果有 3 个以上回答”是”,这大概率是一个架构决策。这不是严格定义,但在实际工作中比抽象的定义更有用。


三、架构决策的不可逆性

前面反复提到”变更成本”和”不可逆性”。这一节具体分析:到底什么让某些架构决策难以逆转?

不可逆性的三个来源

来源一:数据的引力。 数据一旦按照某种模型存储,迁移成本会随数据量线性(甚至超线性)增长。一个存储了 10TB 订单数据的 MySQL 集群,迁移到 MongoDB 的成本不只是重写代码——还包括数据转换、验证、双写过渡期、回滚方案。数据量越大,迁移窗口越难找,风险越高。这就是所谓的”数据引力”(Data Gravity):数据量越大,系统围绕数据源移动的阻力就越大。

Dave McCrory 在 2010 年提出”数据引力”这个概念时,原本是描述云服务选型的锁定效应。但同样的逻辑适用于所有涉及数据存储的架构决策。你的系统在某个数据库上运行了三年,积累了 TB 级的数据、几百条查询语句、一套基于数据库特性的性能优化策略(比如 MySQL 的索引提示、PostgreSQL 的 CTE 优化)——这些都是”引力”的来源。换数据库不只是换一个驱动,是换整个数据生态。

来源二:接口的扩散。 一旦你发布了一个 API(不管是 REST、gRPC 还是消息格式),外部消费者就会依赖它。消费者越多,修改接口的协调成本越高。这就是 Hyrum’s Law(海勒姆定律)的体现:

With a sufficient number of users of an API, it does not matter what you promise in the contract: all observable behaviors of your system will be depended on by somebody.

—— Hyrum Wright, Google

你的 API 返回的 JSON 字段顺序、错误码的具体数值、甚至响应时间的分布——只要有足够多的消费者,就有人会依赖这些”非契约”行为。

一个具体的例子:某团队的 API 返回时间字段一直用 Unix 时间戳(秒级),后来升级时改成了毫秒级的 Unix 时间戳。这个变更没有违反任何文档契约——文档里只写了”返回时间戳”,没有指定单位。但下游有 12 个服务直接把这个字段当秒级时间戳来用,升级后全部解析出错。接口扩散得越远,这类”非契约依赖”就越多。

来源三:团队和组织的路径依赖。 Conway 定律(Conway’s Law)指出,系统的结构趋向于反映组织的沟通结构。反过来,一旦系统被拆分成某种服务边界,团队也会按照这个边界来组织。重新划分服务边界,往往意味着重新组织团队——这已经不是技术问题,而是组织问题。

Mel Conway 在 1968 年的原始论文中这样表述:

Any organization that designs a system (defined broadly) will produce a design whose structure is a copy of the organization’s communication structure.

—— Melvin Conway, “How Do Committees Invent?”, 1968

这条定律的逆向推论(Inverse Conway Maneuver)在现代架构实践中同样重要:如果你想让系统变成某种架构,先把团队组织成和目标架构一致的结构。ThoughtWorks 在多个咨询项目中验证了这个策略的有效性。但这也意味着:一旦团队结构和系统架构已经对齐,改变其中一个而不改变另一个几乎不可能。

常见架构决策的可逆性分析

下面这张表梳理了常见架构决策的可逆性程度。评估基于三个因素:数据迁移成本、接口变更范围、组织协调难度。

架构决策 可逆性 主要阻力 典型变更成本
编程语言选择 极低 全量重写、团队技能、生态依赖 以年计的重写项目
关系型 vs 文档型数据库 低 数据模型重建、查询逻辑重写、数据迁移 数月,取决于数据量
同步调用 vs 异步消息 低 错误处理模型、一致性保证、调用链路径全部不同 涉及所有相关服务的改造
单体 vs 微服务 低 部署流水线、监控体系、团队结构、数据所有权 几个月到几年
SQL 方言(MySQL vs PostgreSQL) 中 方言差异、存储过程、特有功能依赖 数周到数月
HTTP 框架(Gin vs Echo) 中高 中间件、路由、错误处理约定不同 数天到数周
日志库 高 接口简单,可逐步替换 数小时到数天
配置格式(YAML vs TOML) 高 解析逻辑简单,影响范围有限 数小时

这张表不是绝对的——可逆性取决于具体项目的规模和上下文。但它提供了一个粗略的排序:越靠表头的决策越需要慎重,越靠表尾的决策越可以快速试错。

注意”可逆性”不等于”重要性”。一个可逆性高的决策可能仍然非常重要——比如缓存策略的选择(Redis 的淘汰策略是 LRU 还是 LFU)虽然容易改,但选错了可能导致缓存命中率从 95% 降到 60%,直接影响线上性能。可逆性判断的价值在于决定”什么时候做决策”和”投入多少精力做决策”,而不是判断”这个决策重要不重要”。

工程案例:同步 vs 异步通信的不可逆性

一个真实的例子能说明不可逆性如何积累。

假设一个电商系统的订单服务在下单时同步调用库存服务扣减库存:

// 同步调用:下单时直接扣库存
func CreateOrder(ctx context.Context, req *OrderRequest) (*Order, error) {
    // 1. 校验参数
    if err := validate(req); err != nil {
        return nil, err
    }

    // 2. 同步调用库存服务
    resp, err := inventoryClient.Deduct(ctx, &DeductRequest{
        SKU:      req.SKU,
        Quantity: req.Quantity,
    })
    if err != nil {
        return nil, fmt.Errorf("deduct inventory: %w", err)
    }
    if !resp.Success {
        return nil, ErrInsufficientStock
    }

    // 3. 创建订单
    order := &Order{
        ID:     generateID(),
        SKU:    req.SKU,
        Status: OrderStatusCreated,
    }
    if err := orderRepo.Save(ctx, order); err != nil {
        // 问题:库存已扣,但订单保存失败
        // 需要补偿:回滚库存
        inventoryClient.Rollback(ctx, &RollbackRequest{
            SKU:      req.SKU,
            Quantity: req.Quantity,
        })
        return nil, fmt.Errorf("save order: %w", err)
    }

    return order, nil
}

这段代码运行了两年,业务从每天 1000 单增长到每天 100 万单。现在团队想改成异步事件驱动:下单时发一个 OrderCreated 事件,库存服务订阅这个事件来扣库存。

改成异步后,代码看起来更”简洁”了:

// 异步模式:下单时只发事件,不直接扣库存
func CreateOrder(ctx context.Context, req *OrderRequest) (*Order, error) {
    if err := validate(req); err != nil {
        return nil, err
    }

    order := &Order{
        ID:     generateID(),
        SKU:    req.SKU,
        Status: OrderStatusPending, // 注意:状态是 Pending,不是 Created
    }
    if err := orderRepo.Save(ctx, order); err != nil {
        return nil, fmt.Errorf("save order: %w", err)
    }

    // 发送事件,库存扣减由下游异步处理
    event := &OrderCreatedEvent{
        OrderID:  order.ID,
        SKU:      req.SKU,
        Quantity: req.Quantity,
    }
    if err := eventBus.Publish(ctx, "order.created", event); err != nil {
        // 问题:订单已保存,但事件发送失败
        // 需要:发件箱模式(Outbox Pattern)保证至少一次投递
        return nil, fmt.Errorf("publish event: %w", err)
    }

    return order, nil
}

代码量差不多,但注释里已经暴露了新的复杂性——事件发送失败怎么办?需要引入发件箱模式(Outbox Pattern)。而这只是冰山一角:

看起来只是”发个消息”的事情,但实际上需要改的东西远超预期:

错误处理模型完全不同。 同步调用里,库存不足会立即返回错误给用户。异步模式下,用户下单后看到”创建成功”,但库存扣减可能几秒后才失败——需要设计异步通知机制(推送、短信、站内信)告诉用户订单被取消。

一致性保证要重新设计。 同步调用虽然有补偿逻辑的问题,但至少是一个请求内的强一致性尝试。改成异步后,要引入最终一致性(Eventual Consistency)——需要对账机制、补偿任务、死信队列处理、幂等性保证。

监控和排障路径不同。 同步调用的链路追踪是一条直线;异步消息的链路追踪需要关联(Correlation)机制,把消息的生产者和消费者关联起来。原有的 Jaeger 追踪配置要改,告警规则要改,SLA 的计算口径也要改——同步模式下 P99 延迟包含库存扣减时间,异步模式下不包含。

上下游的依赖关系要重新梳理。 同步模式下,订单服务直接依赖库存服务的可用性;异步模式下,订单服务依赖消息队列的可用性,库存服务也依赖消息队列。故障域变了:库存服务挂了,同步模式下订单服务立刻不可用;异步模式下订单服务还能正常接单,但库存扣减会延迟。这是好事还是坏事,取决于业务对”超卖”的容忍度。

一个”通信模式”的变更,最终牵动了错误处理、一致性模型、监控体系、运维流程、甚至业务规则。这就是为什么它是架构决策——不是因为它”很高层”,而是因为它的变更成本呈网络效应式扩散。

工程案例:单体到微服务的不可逆陷阱

通信模式的不可逆性是渐进积累的,但有些架构决策的不可逆性从第一天就开始锁定。单体到微服务的拆分就是典型案例。

2015-2018 年间,大量团队跟随”微服务”浪潮把单体应用拆成了几十个甚至上百个服务。拆分之后,很多团队发现微服务带来的运维复杂性远超预期:分布式追踪、服务发现、配置管理、CI/CD 流水线数量爆炸、跨服务事务处理——每一项都是新的工程投入。

但要”合并回去”几乎不可能。原因是不可逆性的三个来源同时发力:

  1. 数据已经分散。 每个服务有自己的数据库,数据模型各自演化。合并回单一数据库意味着:表结构合并、数据迁移、ID 冲突处理、外键关系重建。如果服务间的数据已经产生了不一致(最终一致性的常见副作用),合并时还要做数据清洗和对账。

  2. 接口已经固化。 服务间的 gRPC/HTTP 接口被多个调用方依赖。合并服务意味着这些接口要么被废弃(调用方要改代码),要么要在合并后的单体里维护兼容层。

  3. 团队已经按服务边界组织。 “支付团队”只负责支付服务,“订单团队”只负责订单服务。合并服务意味着合并团队的职责边界,涉及汇报关系、绩效考核、on-call 轮转——这些组织层面的变更比代码变更更难推动。

Shopify 的案例值得参考。他们选择了”模块化单体”(Modular Monolith)而不是微服务。在一个单体代码库内,通过严格的模块边界和依赖规则来管理复杂性,同时保留了单体部署的简单性。这个选择的一个核心考量就是:保持架构的可逆性。如果将来某个模块确实需要拆成独立服务(比如它的扩展需求和其他模块差异巨大),从模块化单体中拆出一个模块的成本远低于从一堆微服务中合并几个服务。

这里的教训是:拆分容易合并难。 当你不确定是否需要微服务时,选择模块化单体是一个更可逆的决策。将来真的需要拆时,有明确边界的模块可以相对容易地变成独立服务。但如果已经拆成了微服务,合并回去的成本通常高到不现实。


四、Krutchten 4+1 视图模型

既然架构决策涉及这么多维度,如何系统性地描述一个系统的架构?Philippe Kruchten 在 1995 年发表的论文”Architectural Blueprints—The ‘4+1’ View Model of Software Architecture”(IEEE Software, 12(6), pp. 42-50)提出了一个至今仍被广泛使用的框架。

为什么需要多视图

Kruchten 的核心观察是:一个系统的架构不可能用单一的图来完整表达。不同的利益相关者(Stakeholder)关心不同的方面:

用一张图同时满足所有人的需求,结果往往是一张什么都有、什么都看不清的”架构全景图”。

这种”全景图”在架构评审中很常见:一张巨大的 Visio 图上画满了服务、数据库、消息队列、负载均衡器,箭头交错成蜘蛛网。你问”这个系统怎么处理高并发”,看这张图找不到答案;你问”代码仓库怎么组织”,看这张图也找不到答案——因为它试图回答所有问题,结果一个都没回答好。

Kruchten 的方案是:用四个独立的视图(View)分别描述架构的不同方面,再用场景(Scenario)将它们串联验证。每个视图针对特定受众、回答特定问题。

四个视图加一个场景

graph TB
    S["场景视图<br/>Scenarios<br/>(用例驱动,验证其他四个视图)"]

    L["逻辑视图<br/>Logical View<br/>(功能需求 → 类、模块、领域对象)"]
    P["过程视图<br/>Process View<br/>(并发、同步、运行时行为)"]
    D["开发视图<br/>Development View<br/>(代码组织、构建、依赖管理)"]
    PH["物理视图<br/>Physical View<br/>(部署拓扑、节点、网络)"]

    S --- L
    S --- P
    S --- D
    S --- PH

    L -.- P
    D -.- PH

    style S fill:#388bfd,stroke:#388bfd,color:#ffffff
    style L fill:#3fb950,stroke:#3fb950,color:#ffffff
    style P fill:#f0883e,stroke:#f0883e,color:#ffffff
    style D fill:#a371f7,stroke:#a371f7,color:#ffffff
    style PH fill:#f85149,stroke:#f85149,color:#ffffff

上图展示了 4+1 视图模型的结构:场景视图(蓝色)位于中心,连接并验证其他四个视图;逻辑视图(绿色)和过程视图(橙色)描述运行时的不同方面;开发视图(紫色)和物理视图(红色)描述开发和部署的不同方面。

下面逐个拆解。

逻辑视图(Logical View)

关注点: 系统的功能需求——系统提供什么能力给用户?

主要元素: 类、接口、模块、领域对象、它们之间的关系(继承、组合、依赖)。

典型表示: 类图、对象图、包图。

受众: 开发者、架构师。

以订单系统为例,逻辑视图展示的是:

┌─────────────────────────────────────────────────┐
│                   订单领域                        │
│                                                   │
│  ┌──────────┐    ┌──────────┐    ┌──────────┐    │
│  │ Order     │───>│ OrderItem│    │ Payment  │    │
│  │ 订单聚合根│    │ 订单行项  │    │ 支付记录  │    │
│  └──────────┘    └──────────┘    └──────────┘    │
│       │                               ▲           │
│       │ 创建                          │ 关联       │
│       ▼                               │           │
│  ┌──────────┐                    ┌──────────┐    │
│  │ Inventory │                    │ Coupon   │    │
│  │ 库存检查   │                    │ 优惠券    │    │
│  └──────────┘                    └──────────┘    │
└─────────────────────────────────────────────────┘

逻辑视图不关心这些对象运行在哪台机器上、用什么语言实现、用什么数据库存储。它只关心领域概念之间的关系。

过程视图(Process View)

关注点: 系统运行时的并发、同步、通信行为。

主要元素: 进程、线程、协程、消息队列、同步机制。

典型表示: 活动图、序列图、通信图。

受众: 性能工程师、SRE。

同样是订单系统,过程视图关心的问题完全不同:

sequenceDiagram
    participant U as 用户请求
    participant GW as API 网关
    participant OS as 订单服务<br/>(线程池: 200)
    participant IS as 库存服务<br/>(线程池: 100)
    participant MQ as 消息队列
    participant PS as 支付服务

    U->>GW: POST /orders
    GW->>OS: 转发(限流: 5000 QPS)
    OS->>IS: gRPC 同步调用(超时: 200ms)
    IS-->>OS: 扣减结果
    OS->>MQ: 发送 OrderCreated 事件
    OS-->>GW: 201 Created
    GW-->>U: 订单创建成功
    MQ->>PS: 消费 OrderCreated
    PS-->>MQ: ACK

过程视图暴露了逻辑视图看不到的问题:超时设置、限流阈值、线程池大小、消息投递语义——这些都是架构决策,但不会出现在类图里。

举一个具体的架构问题:上图中订单服务对库存服务的 gRPC 调用超时设置为 200ms。这个数字从哪来的?如果库存服务的 P99 延迟是 150ms,那 200ms 的超时意味着大约 1% 的请求会超时——在日均 100 万单的规模下,每天有 1 万单因超时失败。这个失败率能接受吗?把超时改成 500ms 会怎样?订单服务的线程池会被慢请求占满吗?

这类问题只有在过程视图里才能被发现和讨论。逻辑视图里,“订单服务调用库存服务”只是一条简单的依赖关系线。

开发视图(Development View)

关注点: 代码如何组织、模块如何划分、构建和依赖如何管理。

主要元素: 源代码模块、包、库、构建产物、代码仓库结构。

典型表示: 包图、组件图。

受众: 开发者、构建工程师。

开发视图回答的问题包括:

order-system/
├── api/                    # API 定义(protobuf / OpenAPI)
│   ├── order/v1/
│   └── inventory/v1/
├── cmd/                    # 各服务入口
│   ├── order-service/
│   ├── inventory-service/
│   └── payment-service/
├── internal/               # 内部实现,不对外暴露
│   ├── order/
│   │   ├── domain/         # 领域模型
│   │   ├── repository/     # 数据访问
│   │   └── service/        # 业务逻辑
│   ├── inventory/
│   └── payment/
├── pkg/                    # 可被外部引用的公共库
│   ├── middleware/
│   └── observability/
└── deploy/                 # 部署配置
    ├── kubernetes/
    └── terraform/

开发视图和逻辑视图的区别在于:逻辑视图里的”订单”可能是一个领域概念,开发视图里的”订单”是 internal/order/ 目录下的一组 Go 文件。两者的映射关系不一定是一对一的。

一个常见的架构问题就发生在这个映射关系上:逻辑视图里两个独立的领域概念(比如”用户”和”认证”),在开发视图里却共享了代码模块或数据库访问层。这种不一致不是一定有问题——有时候是刻意的简化——但架构师需要意识到它的存在,并判断未来是否会成为拆分的阻力。

物理视图(Physical View)

关注点: 软件如何映射到硬件基础设施——部署在哪些节点、通过什么网络连接。

主要元素: 服务器、容器、Pod、可用区(AZ)、负载均衡器、数据库实例。

典型表示: 部署图。

受众: 运维工程师、SRE、网络工程师。

┌──────── AZ-1 ────────┐    ┌──────── AZ-2 ────────┐
│                       │    │                       │
│  ┌─────────────────┐  │    │  ┌─────────────────┐  │
│  │ K8s Node x3     │  │    │  │ K8s Node x3     │  │
│  │ ┌─────────────┐ │  │    │  │ ┌─────────────┐ │  │
│  │ │Order Pod x4 │ │  │    │  │ │Order Pod x4 │ │  │
│  │ └─────────────┘ │  │    │  │ └─────────────┘ │  │
│  │ ┌─────────────┐ │  │    │  │ ┌─────────────┐ │  │
│  │ │Inv. Pod x2  │ │  │    │  │ │Inv. Pod x2  │ │  │
│  │ └─────────────┘ │  │    │  │ └─────────────┘ │  │
│  └─────────────────┘  │    │  └─────────────────┘  │
│                       │    │                       │
│  ┌─────────────────┐  │    │  ┌─────────────────┐  │
│  │ MySQL Primary   │──│────│──│ MySQL Replica    │  │
│  └─────────────────┘  │    │  └─────────────────┘  │
│                       │    │                       │
│  ┌─────────────────┐  │    │  ┌─────────────────┐  │
│  │ Kafka Broker x3 │──│────│──│ Kafka Broker x3 │  │
│  └─────────────────┘  │    │  └─────────────────┘  │
└───────────────────────┘    └───────────────────────┘

物理视图暴露的架构决策包括:是否跨可用区部署、数据库主从的拓扑、消息队列的集群规模。这些决策直接影响可用性和性能,但在逻辑视图和开发视图中完全不可见。

看上面的部署拓扑,几个需要回答的架构问题:

这些问题都是架构决策,但它们只有在物理视图里才可见。一个只关注逻辑视图的架构师,在画板上画出完美的领域模型,但可能完全忽略了跨可用区部署带来的延迟和一致性挑战。

场景视图(+1)

关注点: 用关键用例来验证和串联其他四个视图。

场景视图不是一个独立的”架构图”,而是一组重要用例的端到端描述,用来检验:四个视图是否一致?是否有遗漏?

例如”用户下单”这个场景会穿过所有四个视图:

  1. 逻辑视图:涉及 Order、OrderItem、Inventory、Payment 这些领域对象
  2. 过程视图:请求经过 API 网关、订单服务同步调用库存服务、异步发送事件到支付服务
  3. 开发视图:代码变更涉及 internal/order/service/ 和 api/order/v1/
  4. 物理视图:请求从 AZ-1 的负载均衡器进入,可能路由到 AZ-2 的 Pod

场景视图的价值在于暴露视图间的不一致。比如:逻辑视图里 Order 和 Payment 是两个独立聚合,但物理视图里它们共享同一个数据库——这个不一致是有意的设计,还是架构漂移(Architecture Drift)的信号?

四个视图之间的关系

四个视图不是独立存在的,它们之间存在映射关系:

关系 说明 典型问题
逻辑 → 开发 领域对象如何映射到代码模块 一个聚合根跨了两个代码模块?
逻辑 → 过程 领域操作如何映射到运行时行为 一个同步的领域操作背后其实是异步的?
开发 → 物理 代码模块如何映射到部署单元 一个代码仓库产出了三个独立部署的服务?
过程 → 物理 运行时进程如何映射到物理节点 两个高并发的服务部署在同一台机器上?

架构问题往往出现在视图间映射不一致的地方。一个在逻辑视图里独立的领域概念,如果在开发视图里和另一个概念共享了代码库,在物理视图里又部署在同一个进程里,那么它的”独立性”就只是纸面上的——任何修改都可能影响另一个概念。

4+1 视图的局限

Kruchten 自己在后续的讨论中也承认,4+1 视图模型有局限性:

但 4+1 视图的核心思想——用多个正交的视角来描述架构,而不是试图用一张图说清一切——至今仍然是架构文档化的基础理念。Simon Brown 的 C4 模型、arc42 模板,都或多或少继承了这个思想。本系列的 第六篇 会详细讨论 C4 模型如何在 4+1 的基础上演进。

实际工作中怎么用 4+1

在实际项目中,不需要为每个视图都画出完整的 UML 图。一个更实际的做法是:

  1. 逻辑视图:用领域模型图或模块依赖图表达,重点画清楚模块边界和关键领域对象的关系。
  2. 过程视图:用序列图或活动图描述 3-5 个最关键的用例的运行时行为,重点标注并发、超时、重试策略。
  3. 开发视图:用代码仓库结构图和模块依赖图表达,重点标注哪些模块可以独立构建和部署。
  4. 物理视图:用部署拓扑图表达,重点标注可用区分布、数据复制方向、负载均衡策略。
  5. 场景视图:选择 5-10 个关键用例,验证它们在其他四个视图中的一致性。

重要的不是画出漂亮的图,而是通过多视角思考暴露出单一视角看不到的问题。


五、架构师到底在做什么

“画框图”是外界对架构师工作最常见的刻板印象。但画框图充其量只占架构师工作的 10%。真正的架构工作是什么?

架构师的四个核心职责

职责一:识别和做出关键决策。

前面已经讨论过,架构决策的特征是高变更成本和低可逆性。架构师的第一个职责,就是在项目中识别出这些决策点——哪些决策一旦做了就很难改,哪些可以延后——然后确保关键决策得到充分的分析和讨论,而不是被某个开发者在 PR 里顺手做了。

这不意味着架构师要亲自做所有决策。更常见的情况是:架构师识别出决策点,组织相关人员进行讨论,确保决策有充分的依据,然后记录下来(这就是 ADR,Architecture Decision Record,后续文章会展开讨论)。

职责二:管理技术债务和架构演进。

系统不是一次设计好就完成了。随着业务增长、需求变化、技术栈更新,架构需要不断演进。架构师需要判断:哪些技术债务需要现在偿还,哪些可以容忍,哪些会在特定条件下爆发成生产事故。

这需要对系统的”承重墙”和”隔断墙”有清晰的认知。有些模块可以随时重构(隔断墙),有些模块一旦动了就牵一发动全身(承重墙)。架构师要知道哪些是承重墙,并在团队不小心要拆承重墙时提出警告。

一个实用的做法是维护”架构适应度函数”(Architecture Fitness Functions),这是 Neal Ford 和 Rebecca Parsons 在《Building Evolutionary Architectures》中提出的概念:用自动化检查来持续验证架构约束是否被违反。比如用 ArchUnit 检查依赖方向、用自定义脚本检查循环依赖、用 SLA 监控来验证性能约束。这些检查在 CI/CD 流水线里运行,每次提交都验证,比事后架构审查有效得多。

职责三:在约束之间做权衡。

架构决策几乎从来不是”A 好 B 差”的简单选择。更常见的情况是”A 在维度 X 上更好,B 在维度 Y 上更好”——然后你要根据业务上下文判断 X 和 Y 哪个更重要。

例如,选择强一致性(Strong Consistency)还是最终一致性(Eventual Consistency):

强一致性:
  + 编程模型简单,不需要处理冲突
  + 数据始终正确
  - 延迟更高(需要跨节点协调)
  - 可用性更低(CAP 定理的约束)

最终一致性:
  + 延迟更低
  + 可用性更高
  - 需要处理临时不一致
  - 业务逻辑更复杂(补偿、对账、幂等)

架构师的价值不在于”知道这两者的区别”——任何看过 CAP 定理的工程师都知道——而在于能根据具体业务场景做出判断:这个系统的库存扣减能接受 3 秒的不一致吗?超卖 100 件的业务损失和每笔订单多 50ms 延迟的用户体验损失,哪个更大?

职责四:确保架构意图被正确实现。

架构设计写在文档里是一回事,代码里是不是真的按照设计来实现是另一回事。架构漂移是所有长期项目的常见问题:随着时间推移,实际代码逐渐偏离设计意图。

架构师需要通过代码评审、自动化检查(ArchUnit、Dependency-Check)、定期架构审查来发现和纠正漂移。例如,如果架构设计要求”领域层不依赖基础设施层”,但某个开发者在领域模型里直接 import 了 Redis 客户端——这就是架构违规,需要被发现和纠正。

// ArchUnit 规则示例:确保领域层不依赖基础设施层
@ArchTest
static final ArchRule domain_should_not_depend_on_infrastructure =
    noClasses()
        .that().resideInAPackage("..domain..")
        .should().dependOnClassesThat()
        .resideInAPackage("..infrastructure..")
        .because("领域模型不应该依赖具体的基础设施实现");

架构师的反模式

知道架构师应该做什么,也要知道不应该做什么:

反模式一:象牙塔架构师。 不写代码、不参与实现、只出架构文档和规范。这种架构师的设计往往脱离实际,因为他们感受不到自己决策带来的痛苦。Martin Fowler 对此的批评很直接:一个不写代码的架构师做出的决策,和一个不上手术台的外科医生画的手术方案一样可疑。

反模式二:技术选型狂热者。 把架构工作等同于技术选型——整天在评估新框架、新中间件、新数据库,却不关心现有系统的真实瓶颈和核心问题。技术选型只是架构决策的一小部分。

反模式三:过度设计。 为还不存在的需求预留扩展性,在流量只有 100 QPS 的系统上设计支持 100 万 QPS 的架构。YAGNI(You Aren’t Gonna Need It)原则在架构层面同样适用——你预留的”扩展点”大概率不会以你预想的方式被用到,但维护它们的成本从第一天就开始产生。

反模式四:一致性洁癖。 要求所有服务使用相同的技术栈、相同的框架版本、相同的代码结构。适度的一致性有助于降低认知负荷,但过度的一致性会抑制团队根据具体场景选择最合适工具的能力。架构治理的目标是”在需要一致的地方一致”(比如监控和日志格式),而不是”在所有地方都一致”。

架构师角色的光谱

在实际工作中,“架构师”不是一个单一的角色。不同的组织、不同的系统阶段,对架构师的定位差异很大:

角色类型 核心工作 适用阶段 风险
编码架构师 写核心模块代码,通过代码影响架构 早期,团队 < 20 人 成为单点瓶颈
引导型架构师 组织讨论、促成共识、记录决策 中期,多团队协作 决策速度慢
治理型架构师 定规范、做审查、推工具链 成熟期,大组织 脱离一线
咨询型架构师 跨项目提供专项建议 任何阶段 缺乏上下文

没有哪种类型”更好”。一个初创团队需要编码架构师——技术负责人就是架构师,通过写代码来体现架构意图。一个几百人的工程组织需要治理型架构师——通过规范和工具来确保架构一致性。关键是角色定位和组织阶段匹配。


六、从定义到实践:一个判断框架

前面的讨论涉及了定义、边界、不可逆性、多视图和架构师的职责。把这些内容综合起来,可以形成一个实用的判断框架。

面对一个技术决策时的检查流程

flowchart TD
    A["面对一个技术决策"] --> B{"变更成本高吗?"}
    B -->|低| C["实现细节<br/>开发者自行决定"]
    B -->|高| D{"影响范围大吗?"}
    D -->|小| E["局部架构决策<br/>团队内讨论决定"]
    D -->|大| F{"可逆性低吗?"}
    F -->|高| G["重要但可调整的决策<br/>记录 ADR,定期回顾"]
    F -->|低| H["关键架构决策<br/>多方评审,记录 ADR<br/>制定回滚方案"]

    style A fill:#388bfd,stroke:#388bfd,color:#ffffff
    style C fill:#3fb950,stroke:#3fb950,color:#ffffff
    style E fill:#f0883e,stroke:#f0883e,color:#ffffff
    style G fill:#a371f7,stroke:#a371f7,color:#ffffff
    style H fill:#f85149,stroke:#f85149,color:#ffffff

这个流程图不是要把架构工作变成机械的检查清单。它的价值在于:当团队对”这算不算架构问题”争论不休时,有一个可以共同使用的思考框架。

三条可行动的建议

第一,延迟不可逆的决策。 既然架构决策的风险在于不可逆性,那么尽量推迟那些不可逆的决策,直到有足够的信息来做出判断。这不是拖延——这是在用时间换信息。在项目早期,对数据量的预估、对流量的预测、对业务方向的判断都有很大的不确定性。过早做出不可逆的决策(比如在第一天就决定用微服务),等于在信息最少的时候做影响最大的决定。

但延迟也有代价:如果一个决策影响后续所有的开发工作,延迟它会让团队在不确定中摸索。关键是判断”延迟的成本”和”做错的成本”哪个更大。

第二,用可逆性替代正确性。 既然 Ralph Johnson 说架构师并不比其他人更擅长在早期做出正确决策,那么一个务实的策略是:尽量让决策变得可逆。

具体做法包括:通过接口隔离降低组件间耦合(换掉一个组件不需要改动所有调用方);通过数据抽象层降低对具体数据库的依赖;通过特性开关(Feature Flag)实现渐进式发布,降低新方案的试错成本。

这些做法不是免费的——抽象层有维护成本,接口设计需要额外工作。但和”做错一个不可逆的架构决策然后花半年重写”比起来,通常是值得的。

Amazon 的”两种决策”框架(Two Types of Decisions)提供了一个更简洁的表达。Jeff Bezos 在 2015 年致股东信中把决策分成两类:

大多数决策是 Type 2,但很多组织把所有决策都当 Type 1 来处理——这会导致决策速度过慢。架构师的一个重要能力就是准确区分 Type 1 和 Type 2:对 Type 1 决策投入足够的分析,对 Type 2 决策快速决策、快速验证。

第三,记录决策,而不只是记录结构。 传统的架构文档往往只记录”系统的结构是什么样的”——类图、部署图、数据流图。但这种文档缺少最关键的信息:为什么是这样的?当时考虑了哪些备选方案?为什么没有选另一个方案?

Architecture Decision Record(ADR)就是为了填补这个空白。一个 ADR 记录的不只是”我们选了 Kafka”,而是”我们在 Kafka、RabbitMQ 和 Pulsar 之间做了对比,基于 X、Y、Z 的考虑选了 Kafka,已知的风险是什么,如果需要换的话退出策略是什么”。这个话题会在本系列的 第三篇 中详细展开。


七、总结

回到开头的问题:架构和设计的边界在哪里?

没有一条硬性的分界线。但 Booch 的定义提供了最实用的判断标准:架构是那些变更成本高的设计决策。变更成本来自数据的引力、接口的扩散和组织的路径依赖。

架构师不是”画框图的人”。架构师的核心工作是在约束之间做权衡、识别关键决策点、管理决策的不可逆性——以及在做错的时候,确保系统有能力纠正。

Kruchten 的 4+1 视图告诉我们:架构不是一张图能说清的。逻辑视图、过程视图、开发视图、物理视图各有侧重,场景视图把它们串起来。忘掉”画一张完美的架构全景图”的执念——用多个正交视角来描述、用关键场景来验证,这才是正确的方法。

最后补充一点工程判断:如果你所在的团队还在争论”什么是架构”,不必追求一个统一的定义。更有价值的做法是就以下两件事达成共识:

  1. 哪些决策需要集体讨论,而不是个人自行决定?
  2. 决策做出后,记录在哪里,以什么格式记录?

这两个问题达成一致,比定义”什么是架构”有用得多。

下一篇将从架构定义转向架构评判标准:质量属性(Quality Attributes)。可用性、性能、安全性、可维护性——这些”非功能需求”如何量化、如何权衡、又如何落到架构决策中。


下一篇:【系统架构设计】架构质量属性:不只是”高可用高性能”


参考资料

书籍

论文与标准

博客与在线资源

同主题继续阅读

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

2026-07-07 · database / distributed

【分布式 OLAP 查询引擎】引擎选型与数据平台阅读地图

用决策树收束 Trino/Spark/ClickHouse/DuckDB/DataFusion/PostgreSQL 的适用边界:交互式联邦、批 ETL、嵌入式分析、流批一体各走哪条路径;给出能力对照表(无吞吐排名)与 postgresql→columnar→lakehouse→stream→query-engine 全栈阅读顺序,闭合数据平台栈。


By .