场景描述
假设你正在开发一个大型服务端企业应用,有如下需求:
- 必须支持多种客户端,包括:WEB 端浏览器、WAP 端浏览器以及原生移动 APP。
- 对外暴露公共 API 用于调用
- 处理 HTTP 请求,或者消息,执行对应的业务逻辑。
- 访问数据库,缓存或者持久化响应的数据
- 与其他系统进行通信,交换所需的信息
- 返回 HTTP 响应,指定好特定的序列化方式,例如 JSON、 XML 等等
- 根据业务逻辑与功能,设计并划分出不同逻辑模块
这样的一个应用,你会如何设计架构并部署呢?
考虑因素
- 这是一个团队开发的项目,有一个独立团队负责
- 团队成员会发生变化,新加入的成员必须快速上手项目
- 应用程序必须易于理解并修改
- 期望能实现应用的持续集成与部署
- 必须可以多实例部署应用程序,以满足可伸缩性和可用性要求。
- 想用比较新的技术(框架、编程语言等)
解决方案
定义一个将应用程序构造为一组松散耦合的微服务协作架构,每个微服务满足:
- 高度可维护和可测试:支持快速和频繁的开发和部署。
- 与其他微服务松耦合:使团队能够在大部分时间独立地工作在他们自己的微服务上,而不受其他微服务更改导致的影响,同时也不会影响其他微服务。
- 独立部署:使团队能够部署他们的服务,而不必与其他团队进行协调。
- 减少沟通成本:可以拆分成小团队专注于各自的微服务,减少大团队内部沟通成本。
服务使用同步协议(如 HTTP/REST )或异步协议(如 AMQP )进行通信。服务可以彼此独立开发和部署。每项服务都有其自有数据库以便与其他服务分离。服务之间的数据一致性使用 SAGA 模式
举例
假设现在正在设计一个电商应用,功能包括接收来自客户的订单(StoreFrontUI),验证并维护库存余额(Inventory Service),验证并维护用户可用余额(Accounting Service),下单成功并发货(Shipping Service)。这个应用被设计成一个微服务架构应用,如图中所示:
分析
好处
- 支持大型复杂应用程序的持续交付和部署
- 可维护性增高:每个服务相对较小,因此更容易理解和更改。
- 更容易被测试:服务更小,测试速度更快。
- 更好的可部署性:服务之间可以独立部署。
- 工作分工与模块业务边界更加明确,可以将某个微服务交付与一个或者多个团队维护。每个团队都可以独立于所有其他团队开发、测试、部署和扩展他们的服务。
- 每个微服务相对更小:
- 开发人员更容易理解
- IDE 负载更低,更快,提高开发效率
- 应用程序启动得更快,提高了 Debug 效率,也提高了部署速度。
- 故障隔离。例如,如果一个微服务中存在内存泄漏,那么只有该微服务会受到影响。其他服务可以继续处理请求。
- 更容易更新技术栈。当开发新的微服务模块可以采用新的技术栈进行试验开发并使用,稳定后,可以逐步推广到其他微服务。
坏处
- 开发人员必须面对分布式系统带来的额外复杂性:
- 开发人员必须熟悉 RPC 通信,并且写好故障处理逻辑。
- 实现跨多个服务的请求更加困难。
- 测试服务之间的交互更加困难。单元测试不能覆盖全部场景,集成测试部署起来更加麻烦。
- 实现跨多个服务的请求需要团队之间的仔细联调。
- IDE 大多面向构建单块应用程序,不提供对开发分布式应用程序的明确支持。
- 部署复杂性。在生产中,部署和管理由许多不同服务组成的系统也具有操作复杂性。目前的容器化以及容器编排方案就是为了解决这一问题。
- 增加内存等资源消耗。假设每个微服务都独占一个 JVM,那么相比与单体应用,JVM 本身占用的资源(例如 GC 占用的 CPU 和内存,还有元空间,代码高速缓存等等)比原来是更多了的。如果是一个微服务占用一个容器,乃至一个虚拟机,一个机器,这个资源浪费会更多。
需要考虑的问题
什么时候使用微服务架构?
使用微服务架构的一个挑战是决定到底什么时候使用它。在开发应用程序的第一个版本时,通常不会遇到需要这种方法解决的问题。此外,使用更为精细的分布式体系结构设计将减缓开发速度。对于初创企业来说,这可能是一个重大问题,它们面临的最大挑战往往是如何快速发展业务和快速迭代应用程序。但是,随着产品不断迭代,这个单体应用程序将会变得越来越大,团队的规模也越来越大,需要使用功能分解为微服务架构时,复杂的依赖关系可能会让应用程序很难分解成一组服务。
如何将应用程序分解为服务?
另一个挑战是决定如何将系统划分为微服务。这在很大程度上是一门艺术,但有许多策略可以参考:
- 按业务分解并定义与业务功能相对应的微服务。
- 按领域驱动的设计的子域分解
- 按用户行为与用例分解,并定义负责特定操作的微服务。例如 Shipping Service 负责运送完整的订单。
- 定义一个负责对某个类型的实体/资源进行操作的微服务。例如 Account Service 负责管理用户帐户。
理想情况下,每个服务应该只有一小部分责任。Bob Martin 谈到应该使用单一职责原则。就一个类而言,应该仅有一个引起它变化的原因。将单一责任原则应用于服务设计也是有意义的。
另一个有助于服务设计的类推是 Unix 实用程序的设计。Unix提供了大量实用程序,如 grep、cat 和 find。每个实用程序只做一件事情,并且复杂的任务是通过使用shell脚本与其他实用程序组合来实现的。
如何保持数据的一致性?
为了确保松散耦合,每个服务都有自己的数据库。保持服务之间的数据一致性是一个挑战,因为对于许多应用程序来说,两阶段提交(2PC)或者分布式事务并不是一种很好的选择。应用程序必须使用 SAGA 模式,服务在其数据更改时发布事件,其他服务使用该事件并更新其数据。有几种可靠更新数据和发布事件的方法,包括事件溯源(Event Sourcing)和事务日志跟踪(Transaction Log Tailing).
如何实现查询?
另一个挑战是实现需要检索多个服务拥有的数据的查询。
相关的设计模式
- 微服务拆解模式
- 每个微服务数据库独立设计模式:每个服务如何拥有自己的数据库,以确保松散耦合。
- 统一 API 网关模式:定义客户端如何访问微服务体系结构中的服务。
- 客户端服务发现和服务器端服务发现模式用于将客户端的请求路由到可用的服务实例。
- 每个主机的单一服务和每个主机多个服务模式,是关于部署策略的设计模式
- 横切关注点设计模式(cross-cutting concerns):例如面向切面的设计,两个非常不一样的组件存在一些类似的功能,这时候我们需要切面设计来统一这些类似的功能。
- 断路器
- 存取令牌
- 可观测模式
- UI 相关模式
- 测试相关设计模式:服务组件测试和服务集成契约测试(Contract Testing)