Day28-35|7.3.3 调用/返回体系结构风格:以控制流组织构件协作

对应教材:《系统架构设计师教程(第2版)》第7章“系统架构设计基础知识”——7.3.3 调用/返回体系结构风格。

学习定位:调用/返回风格是软件系统中最常见、最容易理解,也最容易被滥用的一类架构风格。它以调用者发起请求、被调用者执行处理并返回结果为主要协作方式,覆盖主程序—子程序、面向对象、分层、客户机/服务器等典型结构。高级系统架构设计师需要掌握其构件、连接件、控制关系、优缺点和适用范围,并能分析调用链过长、循环依赖、故障传播、并发控制和接口演化等问题。

一、调用/返回风格的基本思想

调用/返回体系结构风格将系统行为组织为一系列控制流:一个构件调用另一个构件,请求其执行某项功能;被调用构件完成处理后,将结果、异常或状态返回给调用者。控制权通常沿调用链向下传递,返回时沿相反方向回到上层。

其抽象要素包括:

  1. 调用者:发起调用、准备参数、处理返回结果或异常的构件。
  2. 被调用者:提供操作、处理业务或访问资源的构件。
  3. 调用连接件:本地函数调用、对象消息、进程间调用、远程过程调用、HTTP 请求或其他同步协议。
  4. 控制规则:调用方向、参数契约、返回条件、异常传播、并发方式和生命周期。

与数据流风格相比,调用/返回风格更强调控制权和协作顺序;与事件驱动风格相比,它通常更容易得到即时结果和明确的调用栈,但调用者会更直接地依赖被调用者的可用性、延迟和接口变化。

调用/返回并不等于一定是同步。异步调用也可以有请求与回调、Future、任务句柄或查询结果等返回形式,但如果调用者必须等待远端完成,系统仍然承受同步链路的延迟和故障传播。

二、主程序—子程序风格

主程序—子程序风格由一个主程序控制整体流程,并调用多个子程序完成具体任务。主程序负责顺序、分支、循环和异常处理,子程序负责相对独立的计算或功能。

例如一个报表程序可以由主程序依次调用读取数据、校验数据、计算指标、生成文件和发送结果等子程序。这种结构简单直观,适合规模较小、流程稳定、控制关系明确的系统。

它的优点是控制逻辑容易理解、调用过程容易调试、运行时开销较小;缺点是主程序容易膨胀,子程序之间可能共享大量全局数据,局部变化会传播到整体,复用和并行能力有限。如果主程序承担过多业务规则、事务协调和错误处理,就会变成难以维护的“大泥球”。

改进主程序—子程序结构时,应减少全局变量,明确参数和返回值,控制子程序职责,使用模块化接口和异常边界。对于复杂系统,可以将主程序的编排职责与领域规则、基础设施访问和通用能力分离。

三、面向对象风格

面向对象风格将系统组织为一组对象或类,对象封装状态和行为,通过消息或方法调用协作。对象通常具有身份、状态和职责,类通过继承、组合、多态和接口实现复用与变化隔离。

面向对象架构的关键不只是“把数据和函数放在一起”,而是合理分配职责并控制对象之间的依赖。高内聚、低耦合、面向接口、依赖倒置和组合优于继承等原则,能够改善可修改性和可测试性。

面向对象风格适合领域概念丰富、状态变化复杂、需要长期演化的系统。它能用对象封装变化,把不同实现隐藏在统一接口之后;但如果对象粒度过细、继承层次过深、对象之间互相调用形成网络,系统会变得难以理解。分布式环境中,对象调用还可能跨越网络,导致本地调用与远程调用语义不一致,必须显式考虑延迟、失败和序列化。

面向对象设计与架构设计的边界也要注意。一个类使用策略模式是局部设计决策;多个领域对象、服务、数据库和部署节点如何组织,则属于更高层架构决策。不能因为系统使用面向对象语言,就认为整体架构已经合理。

四、分层体系结构风格

分层风格将系统按抽象层次组织,每层为上层提供服务,并使用下层提供的服务完成自身职责。典型层次包括表现层、应用层、领域层、数据访问层和基础设施层,也可以根据系统特点设置其他层。

分层的核心约束是依赖方向和可见性。严格分层要求一层只调用直接相邻的下一层;宽松分层允许上层调用更低层,但必须控制这种绕过,否则层次会逐渐失去意义。层的划分应基于职责和变化原因,而不是简单按文件类型堆目录。

分层风格的优点包括职责清晰、便于理解、局部替换、复用和团队分工;缺点包括跨层调用增加延迟,业务变化可能在多层传播,严格分层在复杂查询和高性能场景下可能产生不必要的转发。分层也不能自动保证安全、可用和性能,必须将认证、权限、缓存、事务、监控和错误处理落实到相应层次。

典型分层约束可以写成:表现层只负责协议适配和输入输出;应用层负责用例编排和事务边界;领域层负责业务规则,不直接依赖具体数据库;基础设施层负责持久化、消息、外部接口和平台能力。这样的规则可通过依赖检查和代码评审验证。

五、客户机/服务器风格

客户机/服务器风格把系统分为请求服务的客户机和提供资源或服务的服务器。客户机通常负责交互或部分处理,服务器负责集中管理数据、业务能力或共享资源。两者通过网络协议通信。

两层客户机/服务器适合规模较小、业务简单的应用;三层或多层客户机/服务器将表现、业务和数据职责分开,改善维护和扩展能力。浏览器、应用服务器和数据库服务器组成的结构,就是常见的多层客户机/服务器系统。

优点是资源集中管理、数据一致性相对容易控制、客户端可以较轻量;缺点是服务器可能成为性能瓶颈或故障集中点,网络延迟和连接管理会影响体验,客户端与服务器版本兼容也需要治理。

设计客户机/服务器系统时,要明确连接管理、会话、认证、超时、重试、负载均衡和服务器扩展策略。不能因为服务器“集中”就忽略横向扩展和故障隔离;也不能把大量业务规则放在客户端,否则客户端版本不一致会造成规则分裂。

六、调用链、控制权与异常传播

调用/返回系统的质量很大程度取决于调用链。一个请求可能经过网关、认证、订单、库存、支付和通知等多个构件;每次调用都会带来延迟、序列化、连接和失败概率。如果各层都采用固定超时和重试,整体超时可能远大于用户可接受范围,重试还可能放大流量。

复审调用链时,应记录:调用方向、同步或异步、协议、超时、重试次数、幂等要求、熔断策略、错误分类和追踪标识。异常要明确是向上抛出、转换为业务结果、降级处理还是进入补偿流程。

可以用简单的关系理解端到端风险:若一条同步链路依赖多个外部构件,任一构件变慢或不可用都可能影响调用者;调用层次越深,延迟和故障传播路径通常越复杂。因此不能只优化单个方法的平均耗时,要看端到端场景和最坏情况。

七、调用/返回风格的优点

7.1 控制流程清晰

调用者明确知道何时发起请求、需要哪些参数以及何时获得结果,适合需要即时反馈和明确业务流程的场景。调试时可以沿调用栈定位问题,理解成本较低。

7.2 事务和一致性容易表达

在同一进程或清晰的服务边界内,调用链可以围绕事务边界组织。对于必须立即完成的校验、扣减和状态转换,同步调用比异步事件更容易向用户返回确定结果。

7.3 接口契约明确

方法、服务或 API 的输入输出可以形式化描述,并通过类型检查、契约测试和版本管理维护兼容性。

7.4 适合层次化和职责分解

主程序、应用服务、领域对象、基础设施和远程服务都可以通过调用/返回建立明确边界,便于团队分工和渐进实现。

八、缺点与架构风险

8.1 强耦合与故障传播

调用者通常依赖被调用者的接口、可用性和响应时间。服务数量或调用层次增加后,单个下游故障可能沿同步链传播,形成级联故障。

8.2 循环依赖

A 调用 B,B 又调用 A,或多个模块互相调用,会造成初始化、测试、发布和故障定位困难。循环依赖往往说明职责边界不清,需要提取抽象、重新分配职责或引入事件/协调者。

8.3 难以独立扩展

如果多个功能被绑定在同一进程或共享资源上,只能整体扩展,容易造成资源浪费。远程服务虽然可以独立扩展,但要承担数据、一致性和运维成本。

8.4 版本和兼容性压力

被调用者接口变化会影响多个消费者;消费者数量越多,发布协调越困难。应采用向后兼容、版本协商、契约测试和废弃窗口,而不是直接修改接口并要求所有团队同时升级。

8.5 远程调用伪装成本地调用

将网络调用封装成普通方法可能隐藏延迟、超时、重试和部分失败,开发人员容易按本地调用思维编写代码。远程接口必须显式表达失败和不确定性。

九、调用风格的设计与治理措施

风险 设计措施 验证方式
调用链过长 合并低价值跳转、异步化非关键路径、设置预算 链路追踪与端到端压测
级联故障 超时、限流、熔断、隔离舱和降级 故障注入和恢复演练
重试放大 只对可重试错误重试,设置上限和退避 高峰与下游变慢测试
重复副作用 幂等键、状态机、去重和补偿 重复提交和超时重试测试
循环依赖 依赖倒置、抽取接口、引入事件或协调者 静态依赖检查
接口破坏 兼容字段、版本、契约测试、迁移窗口 消费者回归测试
数据不一致 明确事务边界、所有权和一致性等级 对账、异常和恢复测试

调用/返回风格并不排斥缓存、消息和事件。关键是确定哪一段需要即时结果,哪一段可以异步传播;同步调用负责确定性高、必须立即反馈的核心步骤,异步机制负责解耦、削峰和非关键通知。

十、案例分析:医院预约系统

某医院预约系统包含患者查询、号源锁定、支付、通知和医生排班。患者在提交预约时希望立即知道是否成功;支付平台偶发超时;通知可以延迟;医生排班每天会调整,查询流量在放号时突然升高。

适合采用分层加调用/返回的主结构:表现层接收请求,应用层编排预约用例,领域层管理号源状态和预约规则,基础设施层访问排班、支付和通知适配器。号源锁定必须在明确事务边界内完成,并使用预约请求号保证幂等;支付受理可以同步返回“受理中”状态,后续通过查询或回调完成状态推进;通知采用异步事件,不阻塞预约主链路。

复审调用链时,患者查询不应同步串联排班、医生画像和营销推荐等所有服务,否则放号高峰会放大延迟;可以建立面向查询的读模型和缓存。支付超时时,不能简单重试扣款,应依据支付请求号查询渠道状态,并通过状态机和对账完成补偿。通知服务故障不能回滚已成功的预约,只需进入可靠消息和重试队列。

此案例中,主导风格是调用/返回与分层组合,局部使用事件驱动和缓存。架构边界通过同步核心流程、异步非关键路径、数据所有权和故障隔离来实现,而不是将所有功能都串成同步调用链。

十一、综合知识练习

练习1:单项选择

调用/返回体系结构风格最核心的协作特征是什么?

A. 数据必须经过一组无状态过滤器
B. 构件通过请求调用并获得返回结果、异常或状态
C. 所有构件只能写入同一个共享仓库
D. 系统只能采用异步事件通信

答案:B。 调用/返回风格围绕控制流和请求—响应关系组织构件协作,既可以在本地,也可以通过网络实现。

练习2:多项选择

治理分布式调用链时通常需要考虑哪些机制?

A. 超时与有限重试
B. 熔断、限流和隔离
C. 幂等与补偿
D. 链路追踪和错误分类
E. 无条件无限重试

答案:A、B、C、D。 无限重试会放大故障和流量,不能作为可靠性措施。

练习3:案例题

某电商下单接口同步调用库存、支付、积分、优惠券、物流和通知六个服务。任何一个服务超时都会导致下单失败,系统通过统一重试三次解决偶发错误,结果高峰期出现请求堆积和重复扣款。请分析问题并提出改进方案。

参考答案:

  1. 调用链过长,非核心能力阻塞核心下单流程,增加端到端延迟和故障传播概率。
  2. 所有服务统一重试没有按错误类型和幂等性分类,可能对不可重试错误和非幂等操作重复执行。
  3. 支付等外部副作用没有以业务请求号和状态机保证幂等,超时后再次调用可能重复扣款。
  4. 缺少超时预算、限流、熔断、隔离和降级,故障时请求不断占用线程和连接。
  5. 通知、积分等非核心操作不应阻塞下单成功判定,应用没有区分同步核心路径和异步后置流程。

改进时应先定义下单成功的业务条件,将参数校验、价格确认、号源或库存锁定等必要步骤放入明确事务边界;积分、通知和部分营销处理改为可靠事件异步执行;支付采用唯一请求号、幂等接口、状态查询和对账补偿,不能盲目重试;为不同依赖设置超时预算、有限退避重试、熔断和隔离舱,对非关键依赖提供降级;通过链路追踪、队列监控、重复扣款测试、下游故障注入和峰值压测验证改进效果。

十二、论文写作素材

论文中论述调用/返回风格时,应结合一个具有明确主流程和质量约束的项目。先写为什么采用调用/返回组织核心业务,再写分层、接口、事务、超时、重试和异常处理;接着描述调用链过长、外部依赖不稳定或接口版本变化带来的问题,以及如何通过异步化、缓存、熔断、幂等和契约测试改进。

可直接使用的句式:

对于必须向用户立即返回确定结果的核心业务,我采用调用/返回方式组织主流程,并通过分层明确协议适配、用例编排、领域规则和基础设施职责。针对远程调用带来的延迟和部分失败,我为每个依赖设置超时预算和错误分类,对可安全重试的操作实施有限退避,对具有外部副作用的操作使用业务幂等键和状态机。非核心通知和统计功能通过可靠事件异步处理,避免同步调用链扩大故障范围。

结尾应写出风格边界:调用/返回适合即时交互和明确事务,但对于削峰、多消费者传播和长耗时处理,应与事件或数据流风格组合。这样的论述能体现不是机械套用一种架构风格,而是根据质量属性进行结构权衡。

十三、今日复盘

本节主线是:

调用者 → 接口契约 → 被调用者 → 返回结果或异常 → 调用链治理 → 事务与幂等 → 同步与异步组合。

请回答以下问题:

  1. 主程序—子程序、面向对象、分层和客户机/服务器分别如何体现调用/返回思想?
  2. 为什么远程调用不能简单当作本地方法调用?
  3. 如何控制调用链过长、级联故障和重试放大?
  4. 哪些功能应该保留同步调用,哪些功能适合改为异步处理?

最后,选择一个你熟悉的业务流程,画出或写出完整调用链,标注每次调用的同步/异步性质、超时、重试、幂等和故障处理。再删除一两个非核心同步依赖,思考如何通过事件、缓存或读模型替代它们,这一步能把调用/返回风格真正转化为案例分析中的架构权衡能力。