微服务的设计的IDEALS
本文翻译自InfoQ,原文参考 ->【这里】。
关键词
面向对象设计中有SOLID
原则,对于微服务,推荐使用IDEALS
原则,这是每个单词开头的缩写,它们分别是:
- 接口隔离(interface segregation)
- 可部署(deployability)
- 事件驱动(event-driven)
- 可用性(availability over consistency)
- 松耦合(loose-coupling)
- 单一原则(and single responsibility)
下面我们来简单看看:
- 接口隔离:不同类型的客户端(手机app、web应用、命令行程序)都能通过合适的契约跟服务端交互
- 可部署性:在微服务时代(也是DevOps时代),开发者要对打包、部署、运行微服务做出设计决策和技术选型。
- 事件驱动:尽量用异步或者事件的方式来触发服务,而不是同步方式的调用
- 高可用:高可用要比一致性更重要,大多数用户更看重系统的可用性,而不是强一致性,因为最终一致性对他们来说也是ok的。
- 松耦合:对微服务来说,输入和输出是一个重要的设计点
- 单一责任:用这种方式对微服务建模,使得微服务有很好的内聚性,同时服务不会太小或太大。
在2000年的时候 Robert C. Martin 提出过面向对象的5大原则;Michael Feathers 将这些原则总结为SOLID
(5大原则的首字母缩写)。之后就有各种书籍对SOLID
做了详细描述,并且这个理论在工业界也流传开了。
- 单一责任原则(Single responsibility principle)
- 开闭原则(Open/closed principle)
- 里氏替换原则(Liskov substitution principle)
- 接口隔离原则(Interface segregation principle)
- 依赖反转原则(Dependency inversion principle)
很多年前,我在做微服务课程培训时,有一个学生问我面向对象的SOLID
是否也适用于微服务呢?
我的回答是,只有一部分。
在那之后,我就开始寻找微服务的基本设计原则,以及像SOLID
这样的简单缩写,为什么要这么做呢?
我们设计、实施基于微服务解决方案有6年多了。这些年来,各种工具、框架、平台、支持的产品围绕着微服务建立了一套丰富的生态。
但对于微服务新手来说,却要面对各种设计决策和技术选型。如果能给开发者们提供一套核心的设计原则,那么开发者们就不至于跑偏。
尽管SOLID
原则也可以用于微服务设计,但是面向对象只是一种设计范式,它处理的是类、接口、层次结构这些。跟分布式系统中的元素有很大的不同。
因此,对于微服务我们提出了下面的设计原则:
- 接口隔离
- 可部署性
- 事件驱动
- 高可用胜过一致性
- 松耦合
- 单一责任
虽然这些理论没有涵盖到微服务设计的所有方面,但他们涉及了创建微服务的关键点和成功要素。
接口隔离
在面向对象中,就建议不要使用胖接口这种设计。它的意思是不要将各种功能都放到一个接口里面,接口应该尽量保持独立。
微服务架构风格是一种特殊的面向服务的架构,其中接口设计是最重要的。
从2000年开始,SOA文献就要求所有的客户端都应该遵循标准规范和模型。
但在SOA之后,服务契约的设计方式已经发生了很大变化。 在微服务时代,经常是一个服务端有多个客户端(前端),这也是将接口隔离应用于微服务的主要原因。
微服务的接口隔离
微服务的接口隔离目标是让每种类型的前端都能看到最适合他们的契约。比如说:
- 一个移动的native app希望返回的是短的JSON格式数据
- web应用希望返回一个有更多内容的JSON格式数据
- 桌面端应用服务端能返回一个完整的XML格式数据
- 不同的客户端也可能使用不同的协议,外部客户端想用HTTP调用gRPC服务
我们可以让所有的客户端用相同的方式访问服务端;但更好的做法是使用接口隔离,这样不同的客户端看到的是最适合他们的接口。
怎么做呢?一个推荐的替代方案是使用API网关。在网关中可以进行消息格式转换、消息结构转换、协议桥接、消息路由等等。目前流行的方案是 Backed for Frontends(BFF) 模式。
我们可以为每种类型的客户端(web应用、移动应用、第三方应用)提供一个专有的API网关,这里的网关就是BFF,如下图:
可部署性
从整个软件历史来看,软件设计的重点集中在:单元(模块)如何组织、运行期的元素(组件)如何交互。
架构策略、设计模式、以及各种设计策略都是用于指导软件如何分层组织,避免过度依赖;避免将指定的角色和内容分配给特定特定类型的元素,以及软件空间中的其他设计决策。
对于微服务开发人员来说,有一些关键的设计角色已经超过了软件范畴。
作为开发人员,我们很早就知道部署、打包对于软件运行的重要性。
但是,从来没像今天对待微服务这样,如此关注部署和监控。
这个领域的技术叫:可部署性,它对于微服务已经变的非常重要了。
主要是微服务的部署单元相比单机时代,增加的可不是一点半点。
因此,IDELAS
中的D
表示开发者也有责任确保软件,及最新的版本能高效可用的交付给用户。
总之,部署包括:
- 配置运行时的基础设施,包括:容器、pod、集群、持久化、安全、网络
- 微服务的扩展性,以及从一个运行环境迁移到另一个环境
- 加速 提交、编译、测试、部署的整个过程
- 减少更新当前版本的时间
- 同步相关软件的版本变更
- 监控微服务的健康,能快速识别问题并修复
实现可部署性
自动化是有效部署微服务的关键,自动化包括有效的使用工具和技术,这是自微服务出现以来我们看到的最大变化。 因此,开发者应该关注工具和平台方面的新方向,但对这些新技术也要保持谨慎态度。
下面这个列表,可以帮助开发提高部署能力的策略和技术:
- 容器化和服务编排:将微服务容器化后,会非常容易在各种平台、云环境中部署和复制。平台提供了共享的资源、路由、伸缩、复制、负载均衡等极致。Docker和k8s是现在容器化的事实标准
- Service Mesh:这类的工具可以用于流量监控、策略实施、身份认证、RBAC、路由、断路器 等在容器和编辑平台中通讯的工具。目前流行的工具包括
- Istio
- Liknerd
- Consul Connct
- API网关:通过拦截对微服务的调用,API网关提供一系列丰富的特性,包括:消息转换、协议桥接、流量监控、安全控制、路由、缓存、请求调节、API配额、断路器等。相关产品有:
- Ambassador
- Kong
- Apiman
- WSO2 API Manager
- Apigee
- Amazon API Gateway
- traefik
- Serverless 架构:将服务部署到serverless平台,可以避免容器编排的复杂性和操作成本。他们遵循FaaS范式,产品包括:
- AWS Lambda
- Azure Functions
- 谷歌云Functions
- 监控工具:微服务可能是私有化部署也可能部署在公有云上,能预测、检测、通知系统健康会非常重要,一些不错的监控工具包括:
- New Relic
- CloudWatch
- Datadog
- Prometheus
- Grafana
- 日志整合:微服务可能会使部署单元增加一个数量级,我们需要有工具来整合这些组件的日志输出,并有搜索、分析、生成报警的功能。这个领域流行的工具包括:
- Fluentd
- Graylog
- Splunk
- ELK (Elasticsearch, Logstash, Kibana)
- Tracing工具:这些工具用于检查微服务,用于提供、收集、可视化显示跨服务调用的运行时跟踪数据。可以帮助你更好的发现性能问题(也能帮你理解系统架构),流行的工具包括:
- Zipkin
- Jaeger
- AWS X-Ray.
- DevOps:开发和运行团队更紧密的沟通和协作时,从基础设施配置到事件处理,可以使微服务工作的更好
- 蓝绿部署和金丝雀发布:这些部署策略在发布微服务时,允许 0 到接近 0 的停机时间,并且在出问题时快速切换
- 代码即架构(IaC):使部署时候人工交互最小化,从而变得更快、更不容易出错、更可审计
- 持续交付:缩短提交到部署的时间,并能保持必备的质量,传统的CI/CD工具包括(现在Weaveworks和Flux等GitOps工具也增加进来,并融合了CD和IaC):
- Jenkins
- GitLab CI/CD
- Bamboo
- GoCD
- CircleCI
- Spinnaker
- 外部化配置:运行配置存储在微服务部署单元之外,便于管理
事件驱动
微服务风格架构创建的后端服务,一般通过下面三种调用触发:
- HTTP调用
- RPC调用,如gRPC、GraphQL
- 通过消息中间件的异步消息
前两种方式是同步调用。HTTP
方式也是最常用的方式,一个组件需要调用其他组件形成一个服务组合,很多时候服务之间的交互是同步的。
如果我们创建(适配)参与的服务来连接消息队列中的topic,那么这个架构就是基于事件驱动的架构(Apache KafkaRabbitMQ、Amazon SNS)。
事件驱动的好处是系统的伸缩性、吞吐量都会有提升。消息的发送者不会因为等待响应而阻塞。采用发布-订阅模式后,同样的消息可以并行的被多个接受者消费。
事件驱动微服务
IDEALS
中的字母E
提醒我们要建立事件驱动的微服务架构,因为他们更有可能满足当今软件解决方案的可伸缩性和性能需求。
这种类型的设计也提升了松耦合,消息的发送者和接受者彼此都是独立的,不需要知道对方存在。
可靠性也有了提升,因为这种设计可以应对微服务的临时中断。
但是,事件驱动微服务(也成为响应式微服务)却不那么容易设计。
因为处理的事件都是异步、且并行的被执行的,某些时候需要有一些相关标识和同步点。
这种架构需要考虑到失败和消息丢失,那么像修正事件,并有回滚撤销数据修改的机制(Saga模式)都是必可不少的。
事件驱动的架构承载了面向用户的事务,需要仔细考虑用户体验,让最终用户知道当前的处理进度以及故障情况。
可用性胜过一致性
CAP
理论本质上给了两个选择:可用性 或者 一致性。
我们看到工业界做出了巨大的努力,使得你可以选择可用性,从而实现最终一致性。
这个道理很简单,现在的用户无法忍受不可用。
设想一个场景,在线商店的促销活动,如果我们让浏览器显示商品总量,和更新商品实际库存之间保持强一致性,那么在数据更新上会有重大开销。
如果更新商品的服务临时不可用,那么目录无法展示商品的信息,服务会不可用锅
或者,我们选择可用性(接受偶尔不一致的风险),用户可能会基于有点过时的库存信息购买。
几百、几千个交易中可能有一个不幸的用户会交易失败,随后他会收到一封抱歉邮件,通知因为库存不一致下单被取消。
从用户后者商业视角看,这种场景比让所有用户感觉到系统不可用或者超级慢更好(系统强制保持一致性)。
可能某些业务操作确实需要强一致性。但是,根据 Pat Helland 的观点。当面对:想要正确的答案,还是马上就要时。人们一般会选择马上就要。
具有最终一致性的高可用
对于微服务,高可用的主要策略是数据复制,可以采用不同的设计模式,有时也可以将这些模式混合使用:
1、数据复制模式:当一个微服务要访问其他服务时,就可以使用这种模式。
我们创建数据的副本,并使其对服务可用。这种方案还需要数据同步机制(ETL工具、程序、发布-订阅、物化视图),它们周期的或者基于触发器的方式,使副本和主数据保持一致。
2、命令查询分离模式(CQRS): 我们将更改数据的操作(commands)和查询数据的操作(query)分离。
CQRS通常基于数据复制进行查询,这样可以提高性能和自主性。
3、事件溯源模式:不在数据库中存储对象的当前状态,而是存储一系列追加的、不可变的对象状态事件,获取对象当前状态是通过重放实现的,我们这样是为了提供数据的“查询视图”。
因此事件溯源模式一般是基于CQRS
设计的。
CQRS
的架构如下图。
前端发送更改数据的请求到一个REST
服务上,这个服务用来操作一个集中式的Oracle数据库。 读请求则发送到一个不同的后端上,这个后端读取的数据是存储在Elasticsearch
上的。
当Oracle数据库被更新后,一个Spring Batch Kubernetes cron job会定期触发并检查到这次更新,然后将这个更新发送到Elasticsearch
。
这种机制使得在两个不同的数据存储之间达到最终一致性。甚至当Oracle数据库或者Cron job都不可用时,读请求仍然可以工作。
松耦合
在软件工程中,耦合指的是两个软件元素之间的依赖程度。
对于一个服务系统来说,传入耦合与服务用户如何与服务交互有关。
组件之间的交互一般是通过定义的接口(契约),那么契约不应该跟实现细节、具体技术紧耦合。一个服务是一个分布式组件,这个组件可以被其他程序调用。有时,服务的管理者甚至不知道使用者在什么位置(比如开放API)。
因此,一般建议是避免契约的更改。如果契约跟服务逻辑和技术紧耦合,那么当服务端的技术或逻辑更新时,契约可能也会跟着变。
服务通常需要跟其他服务或各种类型的组件交互,这样就产生出了耦合。这种交互产生出了运行时的依赖关系,依赖关系会直接影响到服务的自主性。如果一个服务缺乏自主性,那么它自身的行为将会很难预测。这个服务可能变得跟 最慢的、缺乏可靠性、可用性的组件一样。
服务的松耦合策略
IDEALS
中的L
提示我们要注意微服务的耦合性,可以使用和组合下面这些策略来改善微服务的耦合性:
- 点对点和发布-订阅:这些构建消息传输的方式改善了服务的耦合性,因为发送方和接收方都不需要知道彼此的存在。响应式微服务的契约现在变成了消息队列名、消息的结构了。
- API网关和BFF:这是一种中间件的方式,这种方式解决了契约的服务方和客户端的差异,这样客户端看到的就是他们期望的协议和格式。
- 契约优先设计:通过设计独立于任何现有代码的契约,可以避免创建的API紧耦合于现有的技术和实现。
- 超媒体:对于REST服务来说,超媒体使得前端对于后端更加独立。
- 门面模式、适配器、装饰模式:微服务架构中的设计模式变体用于定义内部组件和事件服务,防止不必要的耦合在微服务实现中扩散。
- 独立数据库:这种模式下,微服务不仅有自主性,也避免了跟共享数据库直接耦合。
单一责任原则
原始的单一责任原则指的是一个类中的函数有内聚性。如果一个类中包含了太多功能,那么可能会带来紧耦合,这种设计就会变得很脆弱。
因为想更改类就变得很难,在更改的时候可能会出现一些意想不到的情况。
Bob大叔说,单一责任原则很好理解,但是实施起来却不容易。
单一责任概念也可以扩展到微服务中的服务内聚性。微服务架构风格规定部署单元应该只有一个,或者是几个内聚性的服务。
如果一个微服务做了太多的事情,那就是说有太多的缺乏内部的服务,这就变成了单体应用。
一个庞大的微服务会让技术和功能上难以发展,由于很多的开发人员在一个部署单元内操作各种组件,这也会使得持续交付很难实施。
反过来,如果一个微服务的粒度太细,那么一个用户请求就会在不同的微服务之间交互。
最坏的情况下,一个数据更新操作可能会跨不用的微服务,这样就需要创建一个分布式事务。
微服务的合适粒度
微服务设计成熟度的重要指标,创建出的微服务不太粗、也不太细。这部分的解决方案并不是技术或工具,而是适当的建模。
有多种方式为后端服务建模和定义,对于边界定义目前业界流行的方案是:领域驱动设计(DDD),简而言之:
- 一个服务(REST服务)具有DDD聚合范围
- 一个微服务具有DDD有界上下文的作用域,该微服务内的服务,对应于有界上下文中的聚合
- 对于微服务之间的交互,当异步消息符合要求时使用领域事件,当请求-响应更适合时,API调用使用某种形式的反污染层,当微服务需要其他服务大量数据时,使用最终一致性的复制方式。
总结
IDEALS
是大多数微服务设计的核心理论,但是IDEALS
也不是万能的。
我们总是需要深刻理解需求,并在他们的权衡中做出设计决策。我们也需要学习设计模式和架构策略,它可以帮助我们更好的了解设计原理,此外我们还需要了解技术选型。
我用IDEALS
设计、实施、部署微服务有好几年了,在研讨会和讲座中,跟来自不同组织的几百位开发者讨论过这些理论和策略。
有时候微服务的工具、框架、模式会让人觉得五花八门,我相信深刻理解微服务的IDEALS
后,对这些技术将会有更清晰的认识。