架构权衡评估方法(ATAM):如何评估一个系统的质量
共 10382字,需浏览 21分钟
·
2021-11-12 18:34
JAVA前线
欢迎大家关注公众号「JAVA前线」查看更多精彩分享,主要内容包括源码分析、实际应用、架构思维、职场分享、产品思考等等,同时也非常欢迎大家加我微信「java_front」一起交流学习
1 质量属性
在系统设计和开发过程中,我们比较容易关注系统的功能维度,例如有没有实现预期功能,输入参数和输出参数是否匹配等等,这是比较容易测试和衡量的。
但是我们不能就此止步,因为满足功能是系统的基本要求,还需要关注系统的非功能维度,例如系统性能是否优秀,代码可扩展性如何,出现异常是否可以自动降级等等,这些指标决定了系统能否提供高质量的服务。
在之前文章《结构化思维如何指导技术系统优化》提到了三个质量属性:高性能、高可用、高扩展。本文我们充实为六个质量属性:性能、可用性、可修改性、可靠性、安全性、易用性:
1.1 性能
1.1.1 如何定义
性能有两个定义维度,第一个维度是单位时间内可以做多少事情,第二个维度是做完单位数量的事情需要多长时间,常用以下参数进行量化:
QPS:每秒处理请求数
TPS:每秒处理事务数
并发数:同一时刻处理请求数/事务数
响应时间:系统对请求做出响应的时间
并发数 = QPS x RT
1.1.2 如何提升
提升性能可以从两个维度思考,第一个维度是时间维度,从事前、事中、事后三个时间节点进行优化。事前是指在访问最开始就拒绝无效流量。事中可以使用提高并发度(并发编程),增加资源(服务器、分布式缓存、读写分离,分库分表),减少交互(批量请求)等方案。事后是指数据分析需求可以放到离线数据中心进行,不要放在主应用和主数据库进行。
第二个维度是层次维度,每一层都可以进行优化。系统架构一般分为数据层、缓存层、服务层、网关层、客户端、代理层,每一层都可以按照事前、事中、事后进行优化,而不是一提到优化就是加缓存,应该更加全面地思考。
1.2 可用性
1.2.1 如何定义
可用性是指系统正常运行时间占总运行时间比例,业界常用X个9指标进行量化,例如可用性达到5个9,那么全年系统不可用时间只有5分钟:
1.2.2 如何提升
(1) 非线性
我们从另一个概念理解可用性:非线性,这个概念在生活中无处不在。
假设要赶早上8点钟的火车,如果6:30出发可以在7:00到达车站,所以得到一个结论:只要30分钟就可以到达车站。
如果早上睡晚一点7:15出发,那么按照预期7:45可以到达车站。但是最可能的结果是错过这趟火车。因为正好遇上早高峰导致至少需要1个小时才能到达车站。
我们再分析一个互联网秒杀场景。假设秒杀系统当每秒30个请求时,响应时间是10毫秒。如果按照线性思维可以做出如下设计:
每秒30个访问量响应时间10毫秒
每秒300个访问量响应时间100毫秒
每秒3000个访问量响应时间1000毫秒
如果按照这个思路做系统设计可能会发生重大的错误。因为当每秒3000个访问量发生时,响应时间可能不是1000毫秒,而是可能直接导致系统发生崩溃。
这就是非线性,事物不是简单线性叠加关系,当达到某个临界值时会造成一种截然不同的结果。
(2) 提升策略
冗余 + 自动故障转移
最基本冗余策略就是主从模式。原理是准备两台机器,部署了同一份代码,在功能层面是相同的,都可以对外提供相同的服务。
一台机器启动提供服务,这就是主服务器。另一台机器启动在一旁待命,不提供服务,随时监听主服务器的状态,这就是从服务器。当发现主服务器出现故障时,从服务器立刻替换主服务器,继续为用户提供服务。
自动故障转移策略是指当主系统发生异常时,应该可以自动探测到异常,并自动切换为备用系统。不应该只依靠人工去切换成,否则故障处理时间会显著增加。
降级策略
当系统遇到无法承受的压力时,选择暂时关闭一些非关键的功能,或者延时提供一些功能,把此刻所有的资源都提供给现在最关键的服务。
在秒杀场景中下订单就是最核心最关键的功能。当系统压力将要到达临界值时,可以暂时先关闭一些非核心功能如查询功能。
还有一种降级策略,当系统依赖的下游服务出现错误,甚至已经完全不可用了,那么此时就不能再调用这个下游服务了,否则可能导致雪崩。所以直接返回兜底方案,把下游服务直接降级。
延时策略
用户下订单成功后就需要进行支付。假设秒杀系统下订单每秒访问量是3000,有没有必要将每秒3000次访问量压力传递给支付服务器?
答案是没有必要。因为用户秒杀成功后可以稍晚付款,例如可以跳转到一个支付页面,提示用户只要在10分钟内支付完成即可。
这样流量就被分摊至几分钟,有效保护了系统。技术架构还可以使用消息队列做缓冲,让下游系统根据处理能力拉取消息。
隔离策略
物理隔离:应用分别部署在不同物理机、不同机房,资源之间不会互相影响。
线程隔离:不同类型的请求进行分类,交给不同的线程池处理,当一类请求出现高耗时和异常,不影响另一类请求访问。
1.3 可修改性
1.3.1 如何定义
可修改性是指是否能够以较高的性价比对系统进行变更的能力,可以分为以下四种类型:
可扩展性:系统扩展新构件时对其它构件的影响程度
可维护性:系统修改旧构件时对其它构件的影响程度
结构重组:重新组织构件关系的难易程度
可移植性:在不同硬件平台、编程语言、操作系统间移植的难易程度
1.3.2 如何提升
可修改性最终在解决牵一发而动全身的复杂性问题,复杂业务之所以复杂,一个重要原因是涉及角色或者类型较多,如果平铺直叙地进行设计会出现if-else代码块,可读性和可修改性都很低。
我们分析一个下单场景。当前有ABC三种订单类型:A订单价格9折,物流最大重量不能超过9公斤,不支持退款。B订单价格8折,物流最大重量不能超过8公斤,支持退款。C订单价格7折,物流最大重量不能超过7公斤,支持退款。按照需求字面含义平铺直叙地写代码也并不难:
public class OrderServiceImpl implements OrderService {
@Resource
private OrderMapper orderMapper;
@Override
public void createOrder(OrderBO orderBO) {
if (null == orderBO) {
throw new RuntimeException("参数异常");
}
if (OrderTypeEnum.isNotValid(orderBO.getType())) {
throw new RuntimeException("参数异常");
}
// A类型订单
if (OrderTypeEnum.A_TYPE.getCode().equals(orderBO.getType())) {
orderBO.setPrice(orderBO.getPrice() * 0.9);
if (orderBO.getWeight() > 9) {
throw new RuntimeException("超过物流最大重量");
}
orderBO.setRefundSupport(Boolean.FALSE);
}
// B类型订单
else if (OrderTypeEnum.B_TYPE.getCode().equals(orderBO.getType())) {
orderBO.setPrice(orderBO.getPrice() * 0.8);
if (orderBO.getWeight() > 8) {
throw new RuntimeException("超过物流最大重量");
}
orderBO.setRefundSupport(Boolean.TRUE);
}
// C类型订单
else if (OrderTypeEnum.C_TYPE.getCode().equals(orderBO.getType())) {
orderBO.setPrice(orderBO.getPrice() * 0.7);
if (orderBO.getWeight() > 7) {
throw new RuntimeException("超过物流最大重量");
}
orderBO.setRefundSupport(Boolean.TRUE);
}
// 保存数据
OrderDO orderDO = new OrderDO();
BeanUtils.copyProperties(orderBO, orderDO);
orderMapper.insert(orderDO);
}
}
上述代码从功能上完全可以实现业务需求,但是程序员不仅要满足功能,还需要思考代码的可维护性。如果新增一种订单类型,或者新增一个订单属性处理逻辑,那么我们就要在上述逻辑中新增代码,如果处理不慎就会影响原有逻辑。
为了避免牵一发而动全身这种情况,设计模式中的开闭原则要求我们面向新增开放,面向修改关闭,我认为这是设计模式中最重要的一条原则。
需求变化通过扩展,而不是通过修改已有代码实现,这样就保证代码稳定性。扩展也不是随意扩展,因为事先定义了算法,扩展也是根据算法扩展,用抽象构建框架,用实现扩展细节。标准意义的二十三种设计模式说到底最终都是在遵循开闭原则。
如何改变平铺直叙的思考方式?这就要为问题分析加上纵向和横向两个维度,我选择使用分析矩阵方法,其中纵向表示策略,横向表示场景:
(1) 纵向做隔离
纵向维度表示策略,不同策略在逻辑上和业务上应该是隔离的,本实例包括优惠策略、物流策略和退款策略,策略作为抽象,不同订单类型去扩展这个抽象,策略模式非常适合这种场景。本文详细分析优惠策略,物流策略和退款策略同理。
// 优惠策略
public interface DiscountStrategy {
public void discount(OrderBO orderBO);
}
// A类型优惠策略
@Component
public class TypeADiscountStrategy implements DiscountStrategy {
@Override
public void discount(OrderBO orderBO) {
orderBO.setPrice(orderBO.getPrice() * 0.9);
}
}
// B类型优惠策略
@Component
public class TypeBDiscountStrategy implements DiscountStrategy {
@Override
public void discount(OrderBO orderBO) {
orderBO.setPrice(orderBO.getPrice() * 0.8);
}
}
// C类型优惠策略
@Component
public class TypeCDiscountStrategy implements DiscountStrategy {
@Override
public void discount(OrderBO orderBO) {
orderBO.setPrice(orderBO.getPrice() * 0.7);
}
}
// 优惠策略工厂
@Component
public class DiscountStrategyFactory implements InitializingBean {
private Map strategyMap = new HashMap<>();
@Resource
private TypeADiscountStrategy typeADiscountStrategy;
@Resource
private TypeBDiscountStrategy typeBDiscountStrategy;
@Resource
private TypeCDiscountStrategy typeCDiscountStrategy;
public DiscountStrategy getStrategy(String type) {
return strategyMap.get(type);
}
@Override
public void afterPropertiesSet() throws Exception {
strategyMap.put(OrderTypeEnum.A_TYPE.getCode(), typeADiscountStrategy);
strategyMap.put(OrderTypeEnum.B_TYPE.getCode(), typeBDiscountStrategy);
strategyMap.put(OrderTypeEnum.C_TYPE.getCode(), typeCDiscountStrategy);
}
}
// 优惠策略执行
@Component
public class DiscountStrategyExecutor {
private DiscountStrategyFactory discountStrategyFactory;
public void discount(OrderBO orderBO) {
DiscountStrategy discountStrategy = discountStrategyFactory.getStrategy(orderBO.getType());
if (null == discountStrategy) {
throw new RuntimeException("无优惠策略");
}
discountStrategy.discount(orderBO);
}
}
(2) 横向做编排
横向维度表示场景,一种订单类型在广义上可以认为是一种业务场景,在场景中将独立的策略进行串联,模板方法设计模式适用于这种场景。
模板方法模式一般使用抽象类定义算法骨架,同时定义一些抽象方法,这些抽象方法延迟到子类实现,这样子类不仅遵守了算法骨架约定,也实现了自己的算法。既保证了规约也兼顾灵活性,这就是用抽象构建框架,用实现扩展细节。
// 创建订单服务
public interface CreateOrderService {
public void createOrder(OrderBO orderBO);
}
// 抽象创建订单流程
public abstract class AbstractCreateOrderFlow {
@Resource
private OrderMapper orderMapper;
public void createOrder(OrderBO orderBO) {
// 参数校验
if (null == orderBO) {
throw new RuntimeException("参数异常");
}
if (OrderTypeEnum.isNotValid(orderBO.getType())) {
throw new RuntimeException("参数异常");
}
// 计算优惠
discount(orderBO);
// 计算重量
weighing(orderBO);
// 退款支持
supportRefund(orderBO);
// 保存数据
OrderDO orderDO = new OrderDO();
BeanUtils.copyProperties(orderBO, orderDO);
orderMapper.insert(orderDO);
}
public abstract void discount(OrderBO orderBO);
public abstract void weighing(OrderBO orderBO);
public abstract void supportRefund(OrderBO orderBO);
}
// 实现创建订单流程
@Service
public class CreateOrderFlow extends AbstractCreateOrderFlow {
@Resource
private DiscountStrategyExecutor discountStrategyExecutor;
@Resource
private ExpressStrategyExecutor expressStrategyExecutor;
@Resource
private RefundStrategyExecutor refundStrategyExecutor;
@Override
public void discount(OrderBO orderBO) {
discountStrategyExecutor.discount(orderBO);
}
@Override
public void weighing(OrderBO orderBO) {
expressStrategyExecutor.weighing(orderBO);
}
@Override
public void supportRefund(OrderBO orderBO) {
refundStrategyExecutor.supportRefund(orderBO);
}
}
1.4 可靠性
1.4.1 如何定义
可靠性包括容错性和健壮性,系统面对错误输入仍能保证正确输出的能力,可以分为两种类型:系统可靠性和业务可靠性。
系统可靠性是指面对出现基本错误的输入,系统能够识别和拦截,而不是任由其在构件中传递,造成错误数据或者引发系统异常。例如空值引发的空指针异常,不应该出现在系统中。
业务可靠性是指输入参数在基本校验通过的情况下,系统能够进行业务校验,不会引发超出业务预期的输出结果。例如电商系统中的超卖现象,重复创建订单现象都是业务可靠性较低的表现。
1.4.2 如何提升
(1) 拦截
提升可靠性的关键是应该尽早在上层识别并拦截异常数据,阻止其在构件中流动,避免产生系统异常和错误数据,尤其当产生错误数据后,数据修复难度大。
提升系统可靠性可以在服务入口增加判空校验、参数类型校验、范围校验、合法枚举值校验等基本校验,一旦发现异常直接拒绝。
提升业务可靠性可以增强业务校验,例如库存预扣减,活动有效期校验,参与活动次数校验,扣减库存校验,分布式锁控制并发等方案,如果校验规则复杂可以引入规则引擎进行条件组合,不满足业务条件直接拒绝请求。
(2) 告警
如果第一阶段没有将异常输入拦截成功,那么就要在发生异常时及时感知,异常分为系统异常和业务异常。
系统异常是不允许出现的异常,例如空指针,操作数据库失败等异常,一旦出现就要立即告警。
业务异常可以分为以下类型:
业务告警:单位时间出现X次需要告警
延时告警:某指标单位时间内是否变化
数据告警:单位时间数据指标是否正常
1.5 安全性与易用性
安全性是指系统防止非法用户访问的能力,易用性是指系统使用的难易程度,本文不展开论述,下一个章节会通过实例提到。
2 架构评估方法
2.1 三种评估方法
因为涉及到众多变量和场景,所以评估一个复杂技术系统的质量并不是一件容易的事情。业界有以下三种评估方法:
第一是基于问卷的方式,通过问卷调查对系统比较熟悉的相关人员,这种方式主观性很强。
第二是基于度量的方式,对系统指标完全量化,基于量化指标评价系统,这种方式需要评估者对系统非常熟悉。
第三种是基于场景的方式,筛选出系统的关键场景,根据系统在不同场景中的表现进行评估,这种方式具有一定的主观性,需要评估者对系统比较熟悉,这也是目前较为流行的架构评估方法。
架构权衡评估方法(ATAM)的英文全称是:Architecture Tradeoff Analysis Method,由卡梅隆大学软件工程协会提出,是一种基于场景的架构评估方法,核心是结合质量属性效用树对系统进行评价,确定风险点、敏感点、权衡点,并对系统架构做出决策和折中。
ATAM分为以下步骤,其中1、2、3为描述和介绍阶段,4、5、6为调查和分析阶段,7、8为测试阶段,9为报告阶段。
2.2 ATAM
本章节以之前文章《结合DDD讲清楚编写技术方案的七大维度》足球运动员信息管理系统为例看一看ATAM如何实际应用。
第一阶段是描述和介绍阶段,首先由架构师向大家介绍什么是ATAM方法,其次由产品经理介绍开发足球运动员信息管理系统商业动机,最后由架构师介绍系统整体架构,例如怎样划分领域,系统分为持久层、缓存层、中间件、业务中台、服务层、网关层、客户端和代理层等等。
第二阶段是调查和分析阶段,不同需求方均提出了相关需求,所涉及质量场景如下:
(1) 系统在100毫秒内响应用户请求
(2) 当主数据库发生故障后,10秒内自动切换至从数据库
(3) 当主机房发生故障后,5分钟内请求重定向至灾备机房
(4) 新增球员比赛和训练指标,开发工作在5人日内完成
(5) 使用包含SSL数字证书的HTTPS访问协议
(6) 球员信息管理界面要求简单易用
(7) 出现异常引导用户至错误页面,不能展示异常栈信息
(8) 对于球员信息配置功能的灵活度尚未达成共识,影响了系统可修改性
(9) 对于球员比赛指标实时收集响应时间的要求,影响了数据存储设计
(10) 主教练提出了训练指标新模式,影响了系统性能和可修改性
根据上述场景生成质量属性效用树,(1)属于性能,(2)(3)属性可用性,(4)属于可修改性,(5)属于安全性,(6)属于易用性,(7)属于可靠性:
再根据这些场景分析系统的风险点、敏感点、权衡点。风险点是指某些操作会给系统带来隐患和风险,(8)属于风险点。敏感点是指为了实现某个特定质量属性,一个或多个系统组件所具有的特性,(9)属于敏感点。权衡点是指某些操作会影响系统的多个质量属性,(10)属于权衡点。
第三个阶段是测试阶段,根据足球运动员信息管理系统特性,我们首先确定场景优先级,由高到低分别是:性能、可靠性、可用性、可修改性、安全性、易用性。
架构权衡分析方法所谓权衡在此得到了体现,质量属性每个都很重要,但是根据系统特点需要对质量属性有优先级排序,架构设计时需要所有权衡和折中。
确定了优先级之后,我们需要具体阐述针对每个质量属性采取了哪些方案,例如提升性能使用了缓存,提升可修改性使用了策略模式,提升可靠性使用了统一异常处理框架等等,具体方案可以参考本文第一章节。
第四个阶段是报告阶段,我们将评估过程和结果都汇总整理成文档,其中包括质量属性效用树、风险点、敏感点、权衡点、每次评估会议纪要以及最终架构决策。
3 文章总结
第一系统满足功能性需求是最基本的要求,作为架构师不能就此止步,不仅应该关注功能性需求,还应该关注非功能性需求,质量属性就是衡量非功能性需求的重要指标。
第二架构评估方法分为基于问卷、基于度量、基于场景三种方式,目前业内较为流行的是基于场景的评估方法,ATAM是一种优秀的基于场景评估方法。
第三ATAM以质量属性效用树为核心,帮助架构师识别项目风险点、敏感点、权衡点,指导架构师做出合理架构决策。
4 延伸阅读
JAVA前线
欢迎大家关注公众号「JAVA前线」查看更多精彩分享,主要内容包括源码分析、实际应用、架构思维、职场分享、产品思考等等,同时也非常欢迎大家加我微信「java_front」一起交流学习