微服务笔记-Chris Richardson's Blog

256 阅读33分钟

来自15年的文章,针对微服务架构总结通俗易懂的解释,文章出处:

www.nginx.com/blog/introd… 对文章进行翻译,比总结自己的理解.

微服务

构造巨石程序

构造一个应用程序,此程序具有模块化六边形建筑 应用程序的核心是业务逻辑,它由服务划分、域对象和事件的模块来实现.核心的周围设计是与外部世界接口交互的适配器.适配器的例子包括数据库组件、MQ组件等.

应用程序的部署取决于应用程序的语言和框架,将其作为一个整体打包部署.

局限性:随着时间的推移而程序体积增长,并最终变得巨大.

带来的缺陷

  • 开发成本高,需对整个系统熟知
  • 修复bug与实现新功能困难
  • 结构复杂,导致无法随意更改原来设计
  • 可靠性低,如某个模块发生重大问题整个系统不可用
  • 无法拆分部署,出现性能瓶颈后往往只能够增加服务器或增加集群节点,但是DB问题难解决

微服务–应对复杂性

不同的特性或功能实现不同的服务,如订单管理、客户管理等.每个微服务都是一个小型应用程序,有自己的六边形体系结构.有的服务会提供别的服务调用的API接口.有的服务通过网页UI实现.在运行时,每个实例通常是一个云虚拟机或者Docker容器.

拆分上述系统 应用程序的每个功能领域现在都由其各自的微服务来实现.

每个后端服务提供REST API,服务使用其他服务提供的API.例如,驾驶员管理使用通知服务器告诉可用的驾驶员潜在的行程.用户界面服务调用其他服务来呈现网页.

在运行时,如行程管理服务由多个服务实例组成.每个服务实例都是一个Docker容器.为了获得高可用性,容器在多个云虚拟机上运行.服务实例前面是一个负载平衡器,如NGINX在实例之间分发请求.负载均衡器还可处理其他问题,例如访问控制,计量和监视.

微服务架构模式下.每个服务都有自己的数据库模式,而不是与其他服务共享一个数据库模式.每个服务都有一个数据库模式是必不可少的,因为它确保了松耦合.

微服务是在SOA上做的升华,微服务架构强调业务系统需要彻底的组件化和服务化,原有的单个业务系统会拆分为多个可以独立开发、设计、运行的小应用.这些小应用之间通过服务完成交互和集成.SOA更注重于水平架构划分,前后端,数据库,测试.微服务在SOA的基础上加强垂直拆分,主要按照业务能力划分.

优点

  • 解决复杂性问题,分解巨石程序.服务模块化,以RPC等通讯方式明确定义的界限.单个服务开发效率快
  • 服务按照业务拆分,使团队更注重自己业务开发
  • 服务可单独部署
  • 服务独立扩展,根据硬件限制控制服务实例

缺点

  • 整体系统变为复杂,所有设计没有银弹,微服务主要出发点是拆分服务系统,规模较小需斟酌
  • 服务之间的数据管理复杂化,为保证CAP,需增强数据操作手段,如分布式事务
  • 测试复杂化
  • 服务变更导致服务之间依赖的变化,需提前规划和协调每个服务的变更
  • 部署更复杂,需增加负载均衡,服务发现等组件

使用API网关构建微服务

客户端执行一个功能如商品详情,则需要显示

  • 购物车中的商品数量
  • 订单历史记录
  • 客户评论
  • 低库存警告
  • 运输选项
  • ... ...

客户端如果去访问每个请求对应的微服务,是否合理?

客户端与微服务的直接通信

客户端可以直接向每个微服务发出请求。每个微服务都有一个公共地址(https://服务名. api.company.name).该URL将映射到微服务的负载均衡器,该负载平衡器将请求分布在可用的实例中.

实现存在的挑战与限制

  • 客户端的需求和每个微服务公开的细粒度API之间的不匹配.例如.亚马逊描述了数百种服务是如何参与呈现其产品页面的.虽然客户端可以通过局域网发出这么多请求,但在外网上可能效率太低.这种方法也使得客户端代码更加复杂.
  • 有些协议不适合外网使用.比如RPC,AMQP消息协议.应用程序应该在防火墙之外使用诸如HTTP和WebSocket之类的协议.
  • 难重构微服务.不同服务使用不同协议的化,以后如果合并服务需对应客户端同步处理.

使用API接口网关

API接口网关为系统提供单一入口点.类似于设计模式的门面模式.API网关封装了内部系统架构,并为客户端定制的API.其还有其他职责,如身份验证、监控、负载平衡、缓存、请求整形和管理以及静态响应处理.

优点

  • 封装了应用程序的内部结构.客户端不需要调用特定的服务,只需与网关对话.
  • API为每种客户端提供了特定的应用编程接口.
  • 减少了客户端和应用程序之间的交互,简化客户端代码.

缺点

  • API网关本身必须是一个可以独立开发、设计、运行的高可用性组件
  • API网关可能会成为性能瓶颈

微服务之间的通信

在巨石程序中,组件通过语言层面方法或函数调用相互调用.相比之下,基于微服务的应用程序是运行在多台机器上的分布式系统.每个服务实例通常都是一个流程.因此,服务必须使用进程间通信(IPC)机制进行交互.

交互形式

为服务选择IPC机制时,首先考虑服务如何交互.有多种client-service互动风格. 分为两个维度

  • 维度一
    • 一对一:每个客户端请求仅由一个服务实例处理
    • 一对多:每个请求由多个服务实例处理
  • 维度二
    • 同步:客户端要求服务及时响应,在等待时阻塞
    • 异步:客户端在等待响应时不会阻塞,并且响应(如果有)不一定会立即发送
一对一一对多
同步的请求/响应
异步的通知发布/订阅
请求/异步响应发布/异步响应

一对一的交互:

  • 请求/响应:客户端向服务发出请求并等待响应.客户期望及时收到回复.在基于线程的应用程序中,发出请求的线程甚至可能在等待时阻塞.
  • 通知(也称为单向请求):客户端向服务发送请求,但没有收到或发送回复.
  • 请求/异步响应:客户端向服务发送请求,服务异步回复.客户端在等待时不会阻塞,并且客户端在设计时不期望响应立即送达.

一对多的交互:

  • 发布/订阅:客户端发布通知消息,该消息被零个或多个感兴趣的服务使用.
  • 发布/异步响应:客户端发布请求消息,然后等待一定时间,等待感兴趣的服务做出响应.

对于某些服务,单一的IPC机制就足够了.同样可能需要结合使用IPC机制. 例子:当用户请求出行时,打车应用程序中的服务是如何交互的. 这些服务使用通知、请求/响应和发布/订阅的组合. 例如,乘客的智能手机向行程管理服务发送通知,请求接机.行程管理服务通过使用请求/响应调用乘客服务来验证乘客的帐户是否处于活动状态.然后,行程管理服务创建行程,并使用发布/订阅来通知其他服务.包括定位可用驱动程序的调度程序.

定义与迭代APIs

API是服务和客户之间的约定.与IPC机制无关,以接口的形式(IDL)定义服务作为API.

API总是随着时间而变化,在单机应用程序中,很容易更改API并更新所有调用方.在基于微服务的应用程序中,在正常迭代中无法强制所有客户端与服务同步升级,还有随着版本的迭代,在部署过程中使得服务的旧版本和新版本将同时运行.

如何处理API变更取决于变更的程度.有些更改很小,并且向后兼容以前的版本.例如,可以向请求或响应添加属性. 设计客户和服务需要考虑未来的可能因素Robustness Principle(稳健原则),使用旧应用编程接口的客户端应该继续使用新版本的服务. 该服务为缺失的请求属性提供默认值,客户端忽略任何额外的响应属性.重要的是使用IPC机制和消息格式,能够轻松地改进的API.

当对API进行重大的、不兼容的更改时,由于不能强制客户端立即升级,所以服务必须在一段时间内支持旧版本API. 例子,使用基于HTTP的机制如REST,一种方法是在URL中嵌入版本号.每个服务实例可能同时处理多个版本.或者,可以部署不同的实例,每个实例处理一个特定的版本.

处理故障(降级)

在分布式系统中,始终存在部分故障的风险.由于客户端和服务是独立的进程,服务可能因为故障或维护而停止无法及时响应客户端的请求,或者服务可能过载对请求的响应速度极慢。

例如,假设推荐服务没有响应,客户端可能会无限期地阻塞等待响应.产生糟糕的用户体验,且消耗宝贵的资源,例如线程.最终耗尽线程并变得无响应 处理部分故障的策略包括:

  • 网络超时:不无限期阻塞,在等待响应时设置超时.超时可以确保资源不会被无限期占用.
  • 限制未完成请求的数量:对客户端,对特定服务的未完成请求数量施加上限.如果已经达到了限制,那么进行额外的请求立即失败.
  • 断路器模式:跟踪成功和失败的请求数量.如果错误率超过配置的阈值打开断路器,后续请求立即失败.一段时间后,客户端再次尝试,如果成功,请关闭断路器.
  • 提供降级:当请求失败时执行降级.例如,返回缓存的数据或默认值,如空的建议集.

IPC技术选型

有许多不同的IPC技术可供选择.服务可以使用基于同步请求/响应的通信机制,例如基于HTTP的REST或Thrift.可以使用异步的、基于消息的通信机制,如AMQP或STOMP.还有各种不同的消息格式.服务可以使用可读性较好的文本的格式,如JSON或XML.可以使用二进制格式(效率更高),如Avro或Protocol Buffers.

异步、基于消息的通信

使用消息传递时,进程通过异步发送消息进行通信,客户端通过发送消息向服务发出请求.如果服务需要回复,则向客户端发送一条单独的消息作为回复.由于通信是异步的,客户端不会阻塞等待等待回复,客户端的代码是在服务端不会立即收到回复的情况下设计的.

一个消息由标头(元数据)和消息正文组成.通过channel交互,任何数量的生产者都可以向一个channel发送消息,同样,任何数量的消费者都可以从一个channel接收消息.

有两种channel:

  • 点对点:点对点信道将消息准确地传递给正在从该信道阅读的消费者之一,对于前面描述的一对一交互样式,服务使用点对点通道
  • 发布-订阅:发布-订阅通道将每条消息传递给所有附加的使用者。服务将发布-订阅通道用于上述一对多交互样式。

发布-订阅 channel例子 行程管理服务通过向new trips-publish/subscribeChannel写入行程创建消息,向相关服务(如调度程序)通知新行程. 同样调度程序通过dispatch-publish/subscribe channel通知其他服务.

有许多信息中间件可供选择,建议选择一个支持多种编程语言的.部分消息中间件支持标准协议,如AMQP和STOMP.部分消息中间件有日志功能.开源消息中间件包括RabbitMQ, Apache Kafka, Apache ActiveMQ,NSQ.中间件都尽可能做到高可用、高性能和可扩展.但是,每个broker的消息传递模型的细节存在很大的不同.

消息传递的优点:

  • 客户端与服务端解耦:客户端只需通过向指定的channel发送消息代替发送请求.客户端可以不知道服务实例的存在.不需要使用发现机制来确定服务实例的位置.
  • 消息缓冲:使用同步请求/响应协议,例如HTTP,客户端和服务在交互期间都必须可用.消息broker将写入channel的消息串行化,直到消费者可以处理它们.例如,在线商店可以接受来自客户的订单,即使订单执行系统执行缓慢或不可用,不会影响信息后续执行.
  • 显式进程间通信:基于RPC的机制试图使调用远程服务看起来与调用本地服务相同.然而存在不定因素实际上两者是完全不同的.消息传递使这些差异变得非常明显,这样开发人员可快速排查问题.
  • 灵活的客户端-服务交互:消息传递支持前面描述的所有交互风格(一对一,一对多).

缺点:

  • 增加系统复杂性:消息中间件本身是一个安装、配置和操作的系统组件.消息代理必须高可用,否则系统可靠性会受到影响.
  • 实现请求/响应的交互复杂:每个request message必须包含一个回复channel ID和一个request相关联的ID.服务将request相关联ID的响应消息发送至回复channel.客户端使用相关联ID将响应与请求进行匹配.

同步、请求/响应 IPC

当使用基于请求/响应的同步IPC机制时,客户端向服务发送请求,服务处理请求并发回响应.在许多客户端中,发出请求的线程会在等待响应时阻塞.其他客户端可能使用由Future或Rx Observables封装的异步、事件驱动框架.但是,与使用消息传递不同,客户端假设响应会及时到达.

REST

REST是一种IPC机制,使用HTTP作为通讯方式. REST中的一个关键概念是资源,它通常代表一个业务对象,如客户或产品,或者业务对象的集合.REST使用HTTP来操作资源,这些资源是使用URL表示. 例如一个GET请求代表返回资源,可能是用XML或JSON格式描述资源.一个POST请求代表创建一个新资源和一个PUT请求代表更新资源. 引用REST的创造者Roy Fielding的话:

REST provides a set of architectural constraints that, when applied as a whole, emphasizes scalability of component interactions, generality of interfaces, independent deployment of components, and intermediary components to reduce interaction latency, enforce security, and encapsulate legacy systems.

乘客的智能手机通过发出POST请求至/trips,其代表行程管理服务的资源.该服务通过发送一个GET请求向乘客管理服务请求乘客信息.在验证乘客被授权创建行程后,行程管理服务创建行程并返回201对智能手机的回应.

使用HTTP的优点:

  • HTTP简单,熟悉
  • 可使用浏览器,curl测试HTTP API
  • HTTP 直接支持请求/响应式通信
  • HTTP 不需要中间代理,简化了系统的架构

缺点:

  • 只直接支持请求/响应交互.可以使用HTTP进行通知,但服务器必须发送HTTP响应
  • 客户端,服务端在交互期间必须可用
  • 客户端需定位服务端位置,客户端必须使用服务发现机制来定位服务实例

Thrift

Apache Thrift是一门跨语言的RPC框架. Thrift为API提供C-style IDL,根据编译器生成多语言代码,如C++、Java、Python、PHP、Ruby、Erlang和Node.js.

Thrift支持各种消息格式:JSON、二进制和紧凑二进制.二进制比JSON更快,因为它解码更快.紧凑二进制是一种节省空间的格式.JSON更人性化的,对浏览器也很友好. 节俭还提供了传输协议的选择,包括原始的TCP和HTTP.原始TCP可能比HTTP更有效.

消息格式

微服务系统中跨语言的肖消息格式是很重要的.

  • 文本:XML,JSON,可读性较高
  • 二进制:Thrift,Protocol Buffer,Avro

服务发现

当调用REST API或Thrift API时,为了发出请求需要知道服务实例的网络位置(IP地址和端口).在运行于物理硬件上的传统应用程序中,服务实例的网络位置相对静态.例如从偶尔更新的配置文件中读取网络位置.

在基于云的微服务应用程序中,服务实例动态分配的网络位置.由于自动缩放、故障和升级,服务实例集会动态变化.

客户端发现模式

客户端发现,客户端负责确定可用服务实例的网络位置以及进行负载均衡请求.客户端查询服务注册表然后客户端使用负载均衡算法选择一个可用的服务实例并发出请求. 服务实例的网络位置在启动时向服务注册表注册,当实例终止时从服务注册表中删除.服务实例的注册通常使用心跳机制定期刷新. Netflix的Eureka供了一个用于管理服务实例注册和查询可用实例的REST应用编程接口.Ribbon作为IPC客户端与Eureka配合,在可用的服务实例之间发送负载均衡请求.

优点

  • 实现简单
  • 客户端实现负载均衡算法,更智能,更具有针对性

缺点

  • 客户端与服务注册耦合性高,需针对不同语言实现与发现逻辑实现

服务器端发现模式

客户端通过负载均衡器向服务发出请求,负载均衡器查询服务注册表,并将每个请求路由到可用的服务实例.与客户端发现一样,服务实例在服务注册表中注册和注销.

AWS Elastic Load Balancer ,ELB通常用于负载均衡来自互联网的外部流量.同样可以使用ELB负载均衡虚拟私有云(VPC)内部的流量.客户端使用其域名通过ELB发出请求(HTTP或TCP),ELB对一组已注册的EC2实例或EC2容器服务进行负载均衡分发流量.EC2实例和ECS容器在ELB注册。

HTTP反向代理和负载平衡器(如NGINX)也可以用作服务器端发现负载平衡器.服务注册表可以将路由信息推送到NGINX,并调用配置更新.例如使用Consul Template.NGINX Plus支持动态重新配置机制可以使用DNS从注册表中提取有关服务实例的信息,并为提供了一个API远程重新配置.

在部署环境中,KubernetesMarath集群中代理每台运行的实例.代理充当服务发现和负载均衡器的角色.为了向服务发出请求,客户端使用主机的IP地址和服务分配的端口通过代理路由请求.然后代理将请求透明地转发给集群中某处运行的可用服务实例.

优点

  • 客户端与服务端解耦,消除多语言实现服务发现逻辑
  • 生态发展趋势好

缺点

  • 除非负载均衡器由部署环境提供,否则需要搭建和管理一个高可用性系统组件

服务注册中心

服务注册是服务发现的关键部分.它是一个包含服务实例的网络位置的数据库.服务注册中心需要高可用且实时性.客户端可以缓存从服务注册表获取的网络位置.但是可能出现时间差带来的问题.因此,服务注册表使用replica来保持一致性的服务器集群组成.

Netflix Eureka提供了一个用于注册和查询服务实例的REST API.服务实例使用注册其网络位置POST请求.设置30s的更新时间.通过使用HTTP DELETE请求或由实例注册超时.客户端可以通过使用HTTP GET请求来获取注册的服务实例.

Eureka的高可用是通过在可用性区域运行一个或多个EC2服务器,每个Eureka运行在一个EC2服务器上,该实例有一个弹性IP地址.域名解析器DNS文本记录Eureka集群配置.当Eureka启动时,查询DNS查找到其他实例,之后为自己分配一个未使用的弹性IP地址.

服务注册中心的其他示例:

  • etcd:用于共享配置和服务发现的高可用性、分布式、一致的键值存储.使用etcd的两个著名项目是Kubernetes
  • consul:发现和配置服务的工具.它提供了一个允许客户端注册和发现服务的应用编程接口.Consul可以执行运行状况检查来确定服务的可用性.
  • zookeeper:一种广泛使用的高性能分布式应用程序协调服务.Apache Zookeeper最初是Hadoop的一个子项目,但现在是一个独立项目.

服务注册选择

自主注册模式

当使用自主注册模式,服务实例负责向服务注册表注册和注销自己.如果需要,服务实例会发送心跳请求以防止其注册过期. 如Netflix Eureka的客户端,使用只需要引入注解@EnableEurekaClient

优点

  • 实现简单

缺点

  • 注册客户端与服务端耦合
  • 不同语言的使用需单独实现

第三方注册模式

当使用第三方注册模式,服务实例不负责向服务注册表注册自己.相反由一个被称为服务注册员办理注册.服务注册器通过轮询部署环境或订阅事件来跟踪对运行实例集的更改.当注意到一个新的可用服务实例时,它会向服务注册表注册该实例.服务注册器还注销终止的服务实例. 如开源项目Registrator,它自动注册和注销Docker容器部署的服务实例.Registrator支持多个服务注册服务器,包括etcd和Consul.

Kubernetes内置服务注册器,自动缩放组创建的EC2实例可以自动注册到ELB.

优点

  • 客户端与注册中心解耦

缺点

  • 除非内置于部署环境中,否则需要搭建和维护注册者

微服务的事件驱动数据管理

微服务与分布式数据管理的问题

使用关系数据库的一个主要好处是应用程序可以使用ACID事务,为应用程序提供保障:

  • 原子性:原子化改变
  • 一致性:数据库的状态总是一致的
  • 隔离性:即使事务是并发执行的,它们似乎都是串行执行的
  • 持久性:事务一旦提交,就不会撤消

应用对事务的使用简单化

  • 启动事务
  • 更改(插入,更新或者删除)数据
  • 提交事务

关系数据库为应用程序提供SQL语法,SQL本身是一种丰富的,声明性和标准化的查询语言.轻松编写单表或联表查询,RDBMS查询器计划最优方式执行查询.调用者不在注重实现细节.

在微服务体系结构中,数据访问变的复杂,这是因为每个微服务的数据都是私有部署,其他服务如需访问需要调用目标数据对应服务的API.封装数据好处就是服务之间可以松耦合,独立化服务.同样当多个服务访问相同数据的时候,

往细的看,不同的微服务经常使用不同的数据库.由于业务的多样化,关系数据库已不再是最佳的选择.对于特定的业务,NoSQL可以提供更方便的数据结构,并提供更好的性能与伸缩性.对于储存和查询文本的服务使用文本搜索引擎更方便,如ES.对于社交图形数据的服务使用图数据库,如Neo4j.所以,基于微服务的应用程序通常是多钟数据库混合使用,即所谓的多语言持久化.

这种分区性,多语言的数据储存体系带来的松耦合,可伸缩的好处,同样带来很多分布式数据管理方面的挑战.

挑战一:如何实现跨多个服务的业务事务保证一致性.

例子:一个B2B商店,客户服务维护有关客户信息,包括其信用额度.订单服务管理订单,且必须验证新订单的金额不得超过用户的信用额度.

在单机系统中,订单服务可以利用ACID事务来检查可用信用并创建订单. 在微服务系统中,Order表和Customer表是各自服务私有的.

订单服务不能直接访问Customer表,它只能使用客户服务提供的API.订单服务可能会使用分布式事务,如两阶段提交(2pc)但2pc并不是一个很好地选择.当在可用性和ACID的一致性之间进行选择时,可用性是更好的选择.此外大多数NoSQL数据库,都不支持2PC.

挑战二:如何实现多个服务检索数据的查询

假设应用程序需要显示客户和他最近的订单.如果Order Service提供了用于检索客户订单的API可以使用应用程序端连接来检索这些数据.应用程序从CustomerService检索客户,从OrderService检索客户的订单.但是如果Order Service只支持通过主键查找订单(也许它使用的是只支持基于主键的检索的NoSQL数据库),即无办法来检索所需的数据.

事件驱动架构

很多系统中,解决方案是使用事件驱动体系结构.当微服务在发生一些值得注意的事情时发布事件,例如更新业务实体时,其他微服务订阅了这些事件.当微服务接收到事件时,就可以更新自己的业务实体.

可以使用事件来实现跨多个服务的业务事务.事务由一系列步骤组成.每个步骤由一个微服务更新一个业务实体和发布一个触发下一步的事件组成.

  1. Order Service创建状态为New的订单,并发布创建的订单事件.

  2. 客户服务监听订单创建事件,为订单保留信用,并发布信用预留事件.

  3. 订单服务将监听信用保留事件,并将订单状态更改为打开.

随着更复杂的情景步骤也会随之增多,比如在检查客户信用时保存库存.

只要每个服务原子地更新数据库并发布一个事件(后续会有更多的消息)和MessageBroker保证事件至少交付一次,那么就可以实现跨多个服务的业务事务.ency).这就是所谓弱状态,比如最终一致性.此事务模型称为Base模型

维护视图的服务订阅相关事件并更新视图可以可视化事务发展过程.例如,维护Customer-Orders视图的更新服务订阅Customer服务和Order服务发布的事件.

优点

  • 支持跨多个服务并提供最终一致性的事务的实现
  • 应用程序可维护事务视图(MySQL中借用undo log存储事务信息)

缺点

  • 编程实现复杂
  • 出现故障时,必须实现补偿事务以从应用程序级故障中恢复.例如,如果信用检查失败,则必须取消订单
  • 订阅者需进行幂等性处理
  • 存在中间状态,需要处理一致性问题,读取的数据不一定是一致性状态.例如,查询数据时先判断状态看数据是否可取

实现原子性

针对原子更新数据和发布事件的问题.例如,Order Service必须向Order表中插入一行数据并发布创建的订单事件.这两个操作必须以原子方式完成.如果服务在更新数据库后但在发布事件之前崩溃,则系统将变得不一致.

解决方式:本地事务表. 设计

  • 在存储业务实体状态的数据库中新增一个作为消息队列的事件表
  • 应用程序启动(本地)数据库事务,更新业务实体的状态,将事件插入事件表,并提交事务
  • 单独的应用程序线程或进程查询事件表,将事件发布到MessageBroker,然后使用本地事务将事件标记为已发布的事件

优点

  • 不依赖2pc保证每个事件发布
  • 发布业务级别的事件,不需要进行业务判断

缺点

  • 事件表与业务耦合性高,开发人员必须详细明白之间的联系
  • 数据库必须有事务能力

数据库日志监听

另一种解决不依赖2pc的解决方式:事件由监听数据库事务或提交日志的线程或进程发布. MySQL可以使用阿里的Canal

优点

  • 同样不使用2pc发布事件
  • 事件与业务分离,降低耦合

缺点

  • 不同数据库日志格式不同,需要对应适配
  • 存在业务偏差,低级业务更新数据库(可能更新一行数据),高级业务更新数据库(可能更新多行,包含低级业务的更新范围),监听线程很难判断此时更新是否是高级业务.

微服务部署策略

背景

部署巨石系统意味着运行单个(通常是大型)应用程序的多个相同副本.调配N台服务器(物理或虚拟),并在每台服务器上运行M个应用程序实例.

一个微服务系统由几十个甚至几百个服务组成.服务是用多种语言和框架编写的.每个都是一个小型应用程序,有自己特定的部署、资源、扩展和监控要求.例如,需要根据对服务的需求运行每个服务的特定数量的实例.必须为每个服务实例提供适当的CPU、memory、IO资源.

巨石->微服务 重构

将单一应用程序转换为微服务的过程是循序渐进的.逐步构建一个由微服务组成的新应用程序,并结合整体应用程序运行它。随着时间的推移,巨石应用程序实现的功能数量会减少,直到完全消失或者变成另一个微服务.

停止原项目功能增加

当实现新的功能时,不应该添加更多的代码.相反,这个策略的大思想是将新代码放在独立的微服务中. 除了新的服务和传统的整体,还有另外两个组件

  • 请求路由器,负责处理传入的(HTTP)请求,对应于新功能的请求路由器向新服务发送,未迁移的请求路由到原项目
  • 防腐层,针对迁移过程中数据的访问负责数据集成.微服务服务通过防腐层来读写巨石系统拥有的数据

服务可以使用三种策略来访问整块数据:

  • 调用整体提供访问数据的API
  • 直接访问整块的数据库
  • 维护自己的数据副本,该副本与整体数据库同步

防腐层防止了服务中领域模型的概念污染.防腐层在两个不同的模型之间转换.反腐败层一词最早出现在必读书籍中领域驱动设计埃里克·埃文斯.发展反腐败层可能是一项不小的任务.

将新功能实现为轻量级服务的好处.防止巨石系统增大而难以管理.服务可以独立于整体进行开发、部署和扩展.

前后端分离

将表示层与业务逻辑和数据访问层分开。典型的企业应用程序至少由三种不同类型的组件组成:

  • Controller:处理HTTP请求并实现(REST)API或基于HTML的网络用户界面的组件.在具有复杂用户界面的应用程序中,表示层通常是大量的代码.
  • 业务逻辑层:作为应用程序核心并实现业务规则的部分.
  • 数据访问层:访问数据库和消息中间件等基础架构组件的组件.

API是一个天然的开口,可以把整块分成两个较小的应用程序.一个应用程序包含表示层,另一个应用程序包含业务和数据访问逻辑.拆分后,表示逻辑层的应用程序对业务逻辑应用程序进行远程调用. 重构后: 好处

  • 独立开发、部署和扩展这两个应用程序,公开的API可以由开发的微服务调用

提取服务

将整体中现有的模块转变为独立的微服务.每次提取一个模块把它变成一个服务,整体会缩小.一旦转换了足够的模块,整块将完全消失,要么变得足够小成为另一种服务.

确定模块

一个大型、复杂的巨石系统由数十或数百个模块组成,所有这些模块都是提取的候选模块.

从收益的角度去提取模块,一旦把一个模块转换成一个服务,就可以独立于整体开发和部署它,这将加速开发.

例如,将具有内存数据库的模块转变为服务,然后可以将其部署在具有大量内存的主机上.同样,提取实现计算型算法的模块转为服务可以部署在具有强劲CPU的主机上.通过将具有特定资源需求的模块转化为服务,可以使应用程序更容易扩展.

在确定要提取哪些模块时,要确定业务上下文(即粗粒度边界).利用边界可低成本模块转换服务.

如何提取模块

提取模块的第一步是在模块和整体之间定义一个粗粒度的接口.它很可能是一个双向API,因为整块数据需要服务拥有的数据,服务需要整块的数据. 难点在于模块和应用程序其余部分之间错综复杂的依赖关系和细粒度的交互模式. 最难就是实现的业务逻辑领域模型模式重构,因为域模型类之间有许多关联.通常需要进行重大的代码改动来打破这些依赖关系.

一旦实现了粗粒度接口,就可以将模块转变为独立的服务.要做到这一点必须编写代码,使整体和服务能够通过使用进程间通信(IPC)机制 模块Z是要提取的候选模块.它本身被模块X使用,它使用模块y.第一步重构是定义一对粗粒度的API.第一个接口是模块X用来调用模块Z的被调用接口.第二个接口是模块Z用来调用模块y的调用接口.

第二个重构步骤将模块转变为独立的服务.调用和被调用接口由使用IPC机制的代码实现.很可能还要处理模块Z与微服务模式框架下的交叉问题,如服务发现.

一旦提取了一个模块,就有了另一个可以独立于整体和任何其他服务进行开发、部署和扩展的服务.甚至可以从头重写服务,之后,将服务与整体集成在一起的API代码变成了一个反腐败层,可以在两个域模型之间转换.随着时间慢慢推移,逐渐向微服务架构靠近.