软件架构
概述
软件架构对需求的影响
-
对功能性需求的影响不大
- 由user case和user story体现
-
对非功能性需求影响较大
- 包括可维护性、可测试性、可扩展性、可部署性
软件扩展的方式
- 克隆实例。例子就是服务的弹性扩容。
- 分解功能,每个功能单独实例。例子就是单体转为微服务。
- 数据分区,在分区上克隆实例。例子就是分库然后各自部署微服务。
视图分类及架构风格
-
应用程序通常使用多种架构风格的组合
-
架构风格限定了部分视图中可以选择的元素和关系,比如微服务架构限定了实现视图是多个jar包,然后进而限定了进程视图是一定会有跨进程通信,部署视图会部署多个网络端口。
-
架构风格
-
分层架构风格
- 可以应用于所有的4个视图
-
六边形架构风格
- 应用于逻辑视图
-
单体架构风格
- 应用于实现视图
-
微服务架构风格
- 应用于实现视图
-
-
视图
- 逻辑视图——元素是包和类,考虑设计模式、领域分解等关系
- 实现视图——元素是jar和可执行文件,考虑依赖关系
- 部署视图——元素是机器和进程,考虑网络关系
- 进程视图——元素是进程,考虑进程间通信
业务组织逻辑
-
面向过程的事务脚本模式
- 特征是实现行为的类与存储状态的类是分开的
- 设计风格高度面向过程,仅使用面向对象语言的少量功能
-
面向对象的领域建模模式
-
特征是类具有状态和行为,且状态是私有的,只能通过方法间接访问
-
设计方法
-
传统面向对象设计
-
通过对象直接引用
-
领域模型由一组类组织成包
-
缺少明确的业务边界
- 比如order和库存是两个对象,可以通过调用createorder更新库存,也可以直接改库存对象
-
-
领域驱动设计
-
通过id引用
-
引入聚合这一核心逻辑概念
-
聚合由根实体和一些其他实体构成
- 比如由order作为聚合根,orderItem、paymentInfo等一起构成order聚合
-
聚合通常从数据库整体加载,整体更新
-
聚合对外暴露的只有聚合根
-
聚合有明确的业务规则,更新对象必须满足规则
- 比如order有最小金额
-
聚合代表了一致的边界
- 比如每次更改库存只能在聚合根上通过createOrder来改,然后需要锁定整个聚合
-
-
-
-
领域建模适合做CUD命令,因为这些命令通常是针对单个实例的。而select命令通常会针对满足要求的多个实例,所以为了提升性能,实现时会绕过领域模型,直接访问数据库。比如查找满足要求的实例个数。
-
微服务
微服务区别于单体
-
优点
- 易于并行开发
- 故障隔离
- 代码复杂度降低
- 测试的范围可以更加聚焦
-
缺点
-
服务间的数据库很难做事务和联合处理,数据一致性差
-
不适合大量修改,接口需要向后兼容
-
增加大量网络延迟
- 可以通过容器部署时的亲和性来解决这个问题
-
微服务的拆分
-
服务拆分的原则
- 大小不重要,微服务不一定要小
- 重要的是每个微服务能够由一个小团队开发,然后多个微服务能够并行开发
- 微服务间一定要解耦,不能由一个的变更导致另一个的变更,否则就成了分布式单体程序
- 过程一致性要求高的数据,需要划分到一个服务中
-
操作
-
两种划分方式
- DDD跟微服务天生一对,可以每个子领域就是一个微服务
- 也可以按业务能力进行分解,因为业务实现可能经常变化,但是业务能力通常趋于稳定
-
类拆分的原则
-
单一职责原则
- 定义类的职责时,应该遵循单一职责原则,只有先定义好类,才能考虑类的修改
-
闭包原则
- 如果两个类的修改必须耦合,那就放一个包
- 如果要对一个包做修改,则所有要修改的类都应该位于这个包内
-
-
解决划分的阻碍
-
数据一致性问题
- 无法解决,只能保证数据的最终一致性
- 哪怕微服务不分库都解决不了数据一致性的问题,因为没有事务。所以微服务还是要分库,尽量提高职责隔离,提升数据库性能。避免数据库瓶颈也是微服务的一个优点。
- 如果单纯保证最终一致性不够,那就要考虑划分时分到两个微服务是否有问题。
-
上帝类
- 像Order这种类,在派送、厨房等很多领域都需要用到,合适的做法是让每个子域定义自己需要用到的order,然后通过一个id来进行关联。
-
-
进程间通信
-
交互方式
-
分类依据一对一、一对多,同步、异步。共两个维度4个选项进行
- 请求响应
- 异步请求响应
- 单向通知
- 发布订阅
-
虽然交互方式和底层机制一般是选择相同的,但交互方式跟底层消息机制也没有绝对绑定,比如同步交互的底层可以用异步消息队列实现
-
异步交互可以提高系统的可用性,因为同步交互的可用性是所有组件可用性相乘,而异步是取max。但是我们一般不是很关心可用性,更加关心的是系统的简单性
-
-
底层消息机制
-
同步调用,典型如restful
-
异步消息队列
-
消息去重
- 程序幂等处理
- 通过唯一性id判断
-
消息顺序
- 通过partition控制
-
消息防丢失
- 通过本地事务表,使用数据库的能力实现
-
-
-
消息格式
- 需要考虑跨语言兼容性
-
api
- 需要考虑向后兼容
-
服务发现
-
发现模式
- 服务端定期通过心跳上报可用状况
- 客户端轮训所有服务,自己发现可用的服务
-
功能实现所在层次
-
应用层
- 好处是可以处理多平台部署的问题
- 坏处是需要框架支持
-
平台层如k8s
- 好处是免应用层管理
- 坏处是跟平台绑定
-
-
事务管理
-
分布式事务
-
实现方式
- 同步的本地事务表
- 异步的saga
-
具有的特性
- 原子性
- 最终一致性
- 持久性
-
需要解决的问题
-
不具备隔离性
-
根据业务特性,引入特定状态,作为语义锁
- 比如order的pending状态
-
根据业务本身幂等性进行处理
-
允许不重要数据丢失或错误展示
-
-
-
监控
-
功能点
- 健康检查
- 日志汇聚
- 断路器
- 降级
- 调用链
- 审计日志
- 指标收集
-
实现方式
-
在微服务框架中实现
-
在部署框架中实现
- 只能实现部分功能,比如k8s可以实现服务发现,负载均衡等功能
- 好处是可以不用跟微服务框架绑定
-
单体拆分为微服务
-
难点
-
可能需要重新拆分表结构,涉及到数据的迁移
- 不停服的话则需要考虑引入全新的表,异步导数据的同时,新增数据使用双写的方式
-
分库和拆分服务都会影响数据库事务,需要改成分布式事务,需要关注数据的一致性、事务的隔离性。
- 服务间调用使用本地事务表
- 分析业务本身的幂等特点
- 引入语义锁
-
需要考虑模型变更
- 使用反腐层
-
-
最佳实践
- 1、把单体切换到微服务框架下,作为一个单独的微服务
- 2、对于新增的完全独立的功能,作为单独的微服务实现
- 3、识别数据库各表对应的功能有没有在事务中,是否可以拆分事务,以事务作为服务拆分的重要依据
- 4、如果有大块功能不需要拆表和拆事务,则可以考虑把这块功能单独挪出来作为服务
高级模式
-
event sourcing模式
-
领域事件
- 是指聚合发生的事情,通常代表领域发生的变化
-
与传统应用程序的区别
- 不再通过接受命令方法的调用来更新聚合中的字段,而是通过响应一个事件来处理
- 保留过程中的所有事件
- 启动时,加载最原始的状态以及所有事件,计算得到当前状态,当然,也可以保存过程中的状态快照,加速启动过程
-
优点
- 保留所有的过程
-
缺点
- 对消息通道的可靠性要求高
- 因为不保存最终状态,不支持直接查询满足某个条件的所有实例这种查询
- 对业务逻辑的更改不友好,因为每次更改都要考虑所有历史事件是否有兼容性问题。
-
-
cqrs模式
-
核心是职责隔离
-
隔离方式
-
单独抽取用于查询的服务
- 服务可以通过响应事件的方式,实时刚新数据
-
单独部署用于查询的数据库
- 用于查询的数据库可以进行单独的表结构设计,无需保持和用于响应命令的数据库相同
- 可以生成实时的状态视图,解决eventsourcing中不能复杂查询的问题
- 查询数据库可以使用不同的架构,比如OLAP数据库
-
-
缺点
- 架构更加复杂
- 数据具有滞后性
-
-
api gateway模式
-
最佳实践
-
分层
-
公共层
- 身份验证
- 访问授权
- 缓存
- 日志
- 限流
-
api层
-
谁来维护
- 不同端侧拥有各自的api转发控制,各自维护
- 只有一个公共的api转发模块,由api gateway团队维护。
-
和微服务数据模型解耦
- 微服务返回全量数据,在api层由客户端定制数据聚合和筛选逻辑
-
-
-
-