微服务架构的优势与不足
优势
1.更简单:通过分解巨大单体式应用为多个服务方法解决了复杂性问题。在功能不变的情况下,应用被分解为多个可管理的分支或服务。
2.自由选型:这种架构使得每个服务都可以有专门开发团队来开发。开发者可以自由选择开发技术,提供API服务
3.部署独立:微服务架构模式是每个微服务独立的部署。开发者不再需要协调其它服务部署对本服务的影响
4.服务独立扩展:你可以根据每个服务的规模来部署满足需求的规模。甚至于,你可以使用更适合于服务资源需求的硬件。
劣势
1.微服务强调了服务大小:拆太小?
2.微服务应用是分布式系统,由此会带来固有的复杂性:开发者需要在RPC或者消息传递之间选择并完成进程间通讯机制。更甚于,他们必须写代码来处理消息传递中速度过慢或者不可用等局部失效问题。当然这并不是什么难事,但相对于单体式应用中通过语言层级的方法或者进程调用,微服务下这种技术显得更复杂一些。
3.分布式交易/事务问题
4.测试复杂:同样的服务测试需要启动和它有关的所有服务
5.微服务架构模式应用的改变将会波及多个服务:比如,假设你在完成一个案例,需要修改服务A、B、C,而A依赖B,B依赖C
6.部署一个微服务应用也很复杂:一个分布式应用只需要简单在复杂均衡器后面部署各自的服务器就好了。每个应用实例是需要配置诸如数据库和消息中间件等基础服务。相对比,一个微服务应用一般由大批服务构成。
使用API Gateway
客户端到微服务直接通信
不幸的是,这个方案有很多困难和限制。其中一个问题是客户端的需求量与每个微服务暴露的细粒度API数量的不匹配。如图中,客户端需要7次单独请求。在更复杂的场景中,可能会需要更多次请求。例如,亚马逊的产品最终页要请求数百个微服务。虽然一个客户端可以通过LAN发起很多个请求,但是在公网上这样会很没有效率,这个问题在移动互联网上尤为突出。这个方案同时会导致客户端代码非常复杂。
另一个存在的问题是客户端直接请求微服务的协议可能并不是web友好型。一个服务可能是用Thrift的RPC协议,而另一个服务可能是用AMQP消息协议。它们都不是浏览或防火墙友好的,并且最好是内部使用。应用应该在防火墙外采用类似HTTP或者WEBSocket协议。
这个方案的另一个缺点是它很难重构微服务。随着时间的推移,我们可能需要改变系统微服务目前的切分方案。例如,我们可能需要将两个服务合并或者将一个服务拆分为多个。但是,如果客户端直接与微服务交互,那么这种重构就很难实施。
采用一个API Gateway
通常来说,一个更好的解决办法是采用API Gateway的方式。API Gateway是一个服务器,也可以说是进入系统的唯一节点。这跟面向对象设计模式中的Facade模式很像。API
Gateway封装内部系统的架构,并且提供API给各个客户端。它还可能有其他功能,如授权、监控、负载均衡、缓存、请求分片和管理、静态响应处理等。下图展示了一个适应当前架构的API
Gateway。
API Gateway负责请求转发、合成和协议转换。所有来自客户端的请求都要先经过API Gateway,然后路由这些请求到对应的微服务。API
Gateway将经常通过调用多个微服务来处理一个请求以及聚合多个服务的结果。它可以在web协议与内部使用的非Web友好型协议间进行转换,如HTTP协议、WebSocket协议。
API Gateway可以提供给客户端一个定制化的API。它暴露一个粗粒度API给移动客户端。以产品最终页这个使用场景为例。API
Gateway提供一个服务提供点(/productdetails?productid=xxx)使得移动客户端可以在一个请求中检索到产品最终页的全部数据。API
Gateway通过调用多个服务来处理这一个请求并返回结果,涉及产品信息、推荐、评论等。
一个很好的API Gateway例子是Netfix API Gateway。Netflix流服务提供数百个不同的微服务,包括电视、机顶盒、智能手机、游戏系统、平板电脑等。起初,Netflix视图提供一个适用全场景的API。但是,他们发现这种形式不好用,因为涉及到各式各样的设备以及它们独特的需求。现在,他们采用一个API
Gateway来提供容错性高的API,针对不同类型设备有相应代码。事实上,一个适配器处理一个请求平均要调用6到8个后端服务。Netflix
API Gateway每天处理数十亿的请求。
API Gateway的优点和缺点
API Gateway的一个最大好处是封装应用内部结构。相比起来调用指定的服务,客户端直接跟gatway交互更简单点。API
Gateway提供给每一个客户端一个特定API,这样减少了客户端与服务器端的通信次数,也简化了客户端代码。
API Gateway也有一些缺点。它是一个高可用的组件,必须要开发、部署和管理。还有一个问题,它可能成为开发的一个瓶颈。开发者必须更新API
Gateway来提供新服务提供点来支持新暴露的微服务。更新API Gateway时必须越轻量级越好。否则,开发者将因为更新Gateway而排队列。但是,除了这些缺点,对于大部分的应用,采用API
Gateway的方式都是有效的。
实现一个API Gateway
性能和可扩展性
创建一个支持同步、非阻塞I/O的API Gateway是有意义的。已经有不同的技术可以用来实现一个可扩展的API
Gateway
1.JVM:采用基于NIO技术的框架,如Netty,Vertx,Spring
Reactor或者JBoss Undertow
2.Node.js
3.nginx_plus
采用反应性编程模型
对于有些请求,API Gateway可以通过直接路由请求到对应的后端服务上的方式来处理。对于另外一些请求,它需要调用多个后端服务并合并结果来处理
对于一些请求,例如产品最终页面请求,发给后端服务的请求是相互独立的。为了最小化响应时间,API Gateway应该并发的处理相互独立的请求。但是,有时候请求之间是有依赖的。API
Gateway可能需要先通过授权服务来验证请求,然后在路由到后端服务。
利用传统的同步回调方法来实现API合并的代码会使得你进入回调函数的噩梦中。这种代码将非常难度且难以维护。一个优雅的解决方案是采用反应性编程模式来实现。
类似的反应抽象实现有:
反应抽象
1.Scala的Future
2.Java8的CompletableFuture
3.JavaScript的Promise
4.基于微软.Net平台的有Reactive Extensions(Rx)。
Netflix为JVM环境创建了RxJava来使用他们的API Gateway。同样地,JavaScript平台有RxJS
,可以在浏览器和Node.js平台上运行。采用反应编程方法可以帮助快速实现一个高效的API Gateway代码。
其他问题
服务发现: API Gateway需要采用系统的服务发现机制,要么采用服务端发现,要么是客户端发现.
处理部分失败: 这个问题发生在分布式系统中当一个服务调用另外一个服务超时或者不可用的情况。API Gateway不应该被阻断并处于无限期等待下游服务的状态
深入微服务架构的进程间通信
交互模式
客户端和服务器之间有很多的交互模式,我们可以从两个维度进行归类。第一个维度是一对一还是一对多:
1. 一对一:每个客户端请求有一个服务实例来响应。
2. 一对多:每个客户端请求有多个服务实例来响应
第二个维度是这些交互式同步还是异步:
1. 同步模式:客户端请求需要服务端即时响应,甚至可能由于等待而阻塞。
2. 异步模式:客户端请求不会阻塞进程,服务端的响应可以是非即时的。
一对一的交互模式有以下几种方式
1. 请求/响应:一个客户端向服务器端发起请求,等待响应。客户端期望此响应即时到达。在一个基于线程的应用中,等待过程可能造成线程阻塞。
2. 通知(也就是常说的单向请求):一个客户端请求发送到服务端,但是并不期望服务端响应。
3. 请求/异步响应:客户端发送请求到服务端,服务端异步响应请求。客户端不会阻塞,而且被设计成默认响应不会立刻到达。
一对多的交互模式有以下几种方式:
1. 发布/ 订阅模式:客户端发布通知消息,被零个或者多个感兴趣的服务消费。
2. 发布/异步响应模式:客户端发布请求消息,然后等待从感兴趣服务发回的响应。
API的演化
如果你正在使用基于基于HTTP机制的IPC,例如REST,一种解决方案是把版本号嵌入到URL中。每个服务都可能同时处理多个版本的API。
部分失败
分布式系统中部分失败是普遍存在的问题。因为客户端和服务端是都是独立的进程,一个服务端有可能因为故障或者维护而停止服务,或者此服务因为过载停止或者反应很慢。
Netfilix方案:
1. 网络超时:当等待响应时,不要无限期的阻塞,而是采用超时策略。使用超时策略可以确保资源不会无限期的占用。
2. 限制请求的次数:可以为客户端对某特定服务的请求设置一个访问上限。如果请求已达上限,就要立刻终止请求服务。
3. 断路器模式(Circuit Breaker Pattern):记录成功和失败请求的数量。如果失效率超过一个阈值,触发断路器使得后续的请求立刻失败。如果大量的请求失败,就可能是这个服务不可用,再发请求也无意义。在一个失效期后,客户端可以再试,如果成功,关闭此断路器。
4. 提供回滚:当一个请求失败后可以进行回滚逻辑。例如,返回缓存数据或者一个系统默认值。
Netflix Hystrix 是一个实现相关模式的开源库。如果使用JVM,推荐考虑使用Hystrix。而如果使用非JVM环境,你可以使用类似功能的库
服务发现的可行方案以及实践案例
客户端发现模式
当使用客户端发现模式时,客户端负责决定相应服务实例的网络位置,并且对请求实现负载均衡。客户端从一个服务注册服务中查询,其中是所有可用服务实例的库。客户端使用负载均衡算法从多个服务实例中选择出一个,然后发出请求。
Netflix OSS提供了一种非常棒的客户端发现模式。
Netflix Eureka 。Netflix Eureka] 是一个服务注册表,为服务实例注册管理和查询可用实例提供了REST
API接口。 Netflix Ribbon是一种IPC客户端,与Eureka合同工作实现对请求的负载均衡。我们会在后面详细讨论Eureka。
客户端发现模式也是优缺点分明。这种模式相对比较直接,而且除了服务注册表,没有其它改变的因素。除此之外,因为客户端知道可用服务注册表信息,因此客户端可以通过使用哈希一致性(hashing
consistently)变得更加聪明,更加有效的负载均衡。
而这种模式一个最大的缺点是需要针对不同的编程语言注册不同的服务,在客户端需要为每种语言开发不同的服务发现逻辑。
服务端发现模式
AWS Elastic Load Balancer(ELB)是一种服务端发现路由的例子,ELB一般用于均衡从网络来的访问流量,也可以使用ELB来均衡VPC内部的流量。客户端使用DNS,通过ELB发出请求(HTTP或者TCP)。ELB负载均衡器负责在注册的EC2实例或者ECS容器之间均衡负载,并不存在一个分离的服务注册表,而EC2实例和ECS实例也向ELB注册。
HTTP服务和类似NGINX和NGINX Plus的负载均衡器都可以作为服务端发现均衡器。例如,这篇博文就描述如何使用Consul
Template来动态配置NGINX反向代理。Consul Template是周期性从存放在Consul
Template注册表中配置数据重建配置文件的工具。当文件发生变化时,会运行一个命令。在如上博客中,Consul
Template产生了一个nginx.conf文件,用于配置反向代理,然后运行一个命令,告诉NGINX重新调入配置文件。更复杂的例子可以用HTTP
API或者DNS动态重新配置NGINX Plus。
服务端发现模式也有优缺点。最大的优点是客户端无需关注发现的细节,客户端只需要简单的向负载均衡器发送请求,实际上减少了编程语言框架需要完成的发现逻辑。而且,如上说所,某些部署环境免费提供以上功能。
这种模式也有缺陷,除非部署环境提供负载均衡器,否则负载均衡器是另外一个需要配置管理的高可用系统功能。
服务注册表
如前所述,Netflix Eureka是一个服务注册表很好地例子,提供了REST API注册和请求服务实例。
服务实例使用POST请求注册网络地址,每30秒必须使用PUT方法更新注册表,使用HTTP DELETE请求或者实例超时来注销。可以想见,客户端可以使用HTTP
GET请求接受注册服务实例信息。
Netflix通过在每个AWS EC2域运行一个或者多个Eureka服务实现高可用性,每个Eureka服务器都运行在拥有弹性IP地址的EC2实例上。DNS
TEXT记录用于存储Eureka集群配置,其中存放从可用域到一系列Eureka服务器网络地址的列表。当Eureka服务启
etcd – 是一个高可用,分布式的,一致性的,键值表,用于共享配置和服务发现。两个著名案例包括Kubernetes和Cloud
Foundry。
consul – 是一个用于发现和配置的服务。提供了一个API允许客户端注册和发现服务。Consul可以用于健康检查来判断服务可用性。
Apache ZooKeeper – 是一个广泛使用,为分布式应用提供高性能整合的服务。Apache
ZooKeeper最初是Hadoop的子项目,现在已经变成顶级项目。
服务注册模式
自注册方式 ->
当使用自注册模式时,服务实例负责在服务注册表中注册和注销。另外,如果需要的话,一个服务实例也要发送心跳来保证注册信息不会过时。下图描述了这种架构。
自注册模式也有优缺点。一个优点是,相对简单,不需要其他系统功能。而一个主要缺点则是,把服务实例跟服务注册表联系起来。必须在每种编程语言和框架内部实现注册代码。
第三方注册模式 ->
服务实例并不负责向服务注册表注册,而是由另外一个系统模块,叫做服务管理器,负责注册。服务管理器通过查询部署环境或订阅事件来跟踪运行服务的改变。当管理器发现一个新可用服务,会向注册表注册此服务。服务管理器也负责注销终止的服务实例。下图是这种模式的架构图
一个服务管理器的例子是开源项目Registrator,负责自动注册和注销被部署为Docker容器的服务实例。Reistrator支持多种服务管理器,包括etcd和Consul。
微服务的事件驱动数据管理
单体式应用一般都会有一个关系型数据库,由此带来的好处是应用可以使用 ACID transactions。
然而,对于微服务架构来说,数据访问变得非常复杂,这是因为数据都是微服务私有的,唯一可访问的方式就是通过API。这种打包数据访问方式使得微服务之间松耦合,并且彼此之间独立。如果多个服务访问同一个数据,schema会更新访问时间,并在所有服务之间进行协调。
数据相关的两个挑战
1.第一个挑战在于如何完成一笔交易的同时保持多个服务之间数据一致性.
2.第二个挑战是如何完成从多个服务中搜索数据.
事件驱动架构
对许多应用来说,这个解决方案就是使用事件驱动架构(event-driven architecture)。在这种架构中,当某件重要事情发生时,微服务会发布一个事件,例如更新一个业务实体。当订阅这些事件的微服务接收此事件时,就可以更新自己的业务实体,也可能会引发更多的事件发布.
事件驱动架构也是既有优点也有缺点,此架构可以使得交易跨多个服务且提供最终一致性,并且可以使应用维护最终视图;而缺点在于编程模式比ACID交易模式更加复杂:为了从应用层级失效中恢复,还需要完成补偿性交易,例如,如果信用检查不成功则必须取消订单;另外,应用必须应对不一致的数据,这是因为临时(in-flight)交易造成的改变是可见的,另外当应用读取未更新的最终视图时也会遇见数据不一致问题。另外一个缺点在于订阅者必须检测和忽略冗余事件.
原子操作Achieving Atomicity
事件驱动架构还会碰到数据库更新和发布事件原子性问题。例如,订单服务必须向ORDER表插入一行,然后发布Order
Created event,这两个操作需要原子性。如果更新数据库后,服务瘫了(crashes)造成事件未能发布,系统变成不一致状态。
2pc
订单服务也可以使用 distributed transactions, 也就是周知的两阶段提交 (2PC)。2PC在现在应用中不是可选性。根据CAP理论,必须在可用性(availability)和ACID一致性(consistency)之间做出选择,availability一般是更好的选择。但是,许多现代科技,例如许多NoSQL数据库,并不支持2PC。在服务和数据库之间维护数据一致性是非常根本的需求,因此我们需要找其他的方案。
使用本地交易发布事件
得原子性的一个方法是对发布事件应用采用multi-step process involving only
local transactions,技巧在于一个EVENT表,此表在存储业务实体数据库中起到消息列表功能。应用发起一个(本地)数据库交易,更新业务实体状态,向EVENT表中插入一个事件,然后提交此次交易。另外一个独立应用进程或者线程查询此EVENT表,向消息代理发布事件,然后使用本地交易标志此事件为已发布.
订单服务向ORDER表插入一行,然后向EVENT表中插入Order Created event,事件发布线程或者进程查询EVENT表,请求未发布事件,发布他们,然后更新EVENT表标志此事件为已发布。
此方法也是优缺点都有。优点是可以确保事件发布不依赖于2PC,应用发布业务层级事件而不需要推断他们发生了什么;而缺点在于此方法由于开发人员必须牢记发布事件,因此有可能出现错误。另外此方法对于某些使用NoSQL数据库的应用是个挑战,因为NoSQL本身交易和查询能力有限。
挖掘数据库交易日志
另外一种不需要2PC而获得线程或者进程发布事件原子性的方式就是挖掘数据库交易或者提交日志。应用更新数据库,在数据库交易日志中产生变化,交易日志挖掘进程或者线程读这些交易日志,将日志发布给消息代理。
此方法的例子如LinkedIn Databus 项目,Databus 挖掘Oracle交易日志,根据变化发布事件,LinkedIn使用Databus来保证系统内各记录之间的一致性。
另外的例子如:AWS的 streams mechanism in AWS DynamoDB,是一个可管理的NoSQL数据库,一个DynamoDB流是由过去24小时对数据库表基于时序的变化(创建,更新和删除操作),应用可以从流中读取这些变化,然后以事件方式发布这些变化。
交易日志挖掘也是优缺点并存。优点是确保每次更新发布事件不依赖于2PC。交易日志挖掘可以通过将发布事件和应用业务逻辑分离开得到简化;而主要缺点在于交易日志对不同数据库有不同格式,甚至不同数据库版本也有不同格式;而且很难从底层交易日志更新记录转换为高层业务事件。
使用事件源
Event sourcing (事件源)通过使用根本不同的事件中心方式来获得不需2PC的原子性,保证业务实体的一致性。
这种应用保存业务实体一系列状态改变事件,而不是存储实体现在的状态。应用可以通过重放事件来重建实体现在状态。只要业务实体发生变化,新事件就会添加到时间表中。因为保存事件是单一操作,因此肯定是原子性的。
为了理解事件源工作方式,考虑事件实体作为一个例子。传统方式中,每个订单映射为ORDER表中一行,例如在ORDER_LINE_ITEM表中。但是对于事件源方式,订单服务以事件状态改变方式存储一个订单:创建的,已批准的,已发货的,取消的;每个事件包括足够数据来重建订单状态。
数据源方法提供了100%可靠的业务实体变化监控日志,使得获取任何时点实体状态成为可能。另外,事件源方法可以使得业务逻辑可以由事件交换的松耦合业务实体构成。这些优势使得单体应用移植到微服务架构变的相对容易。
事件源方法也有不少缺点,因为采用不同或者不太熟悉的变成模式,使得重新学习不太容易;事件存储只支持主键查询业务实体,必须使用
Command Query Responsibility Segregation (CQRS) 来完成查询业务,因此,应用必须处理最终一致数据。
选择微服务部署策略
单主机多服务实例模式
部署微服务的一种方法就是单主机多服务实例模式,使用这种模式,需要提供若干台物理或者虚拟机,每台机器上运行多个服务实例。很多情况下,这是传统的应用部署方法。每个服务实例运行一个或者多个主机的well-known端口,主机可以看做宠物。
单主机多服务实例模式也是优缺点并存。主要优点在于资源利用有效性。多服务实例共享服务器和操作系统,如果进程组运行多个服务实例效率会更高,例如,多个web应用共享同一个Apache
Tomcat Server和JVM。
另一个优点在于部署服务实例很快。只需将服务拷贝到主机并启动它。如果服务用Java写的,只需要拷贝JAR或者WAR文件即可。对于其它语言,例如Node.js或者Ruby,需要拷贝源码。也就是说网络负载很低。
因为没有太多负载,启动服务很快。如果服务是自包含的进程,只需要启动就可以;否则,如果是运行在容器进程组中的某个服务实例,则需要动态部署进容器中,或者重启容器
除了上述优点外,单主机多服务实例也有缺陷。其中一个主要缺点是服务实例间很少或者没有隔离,除非每个服务实例是独立进程。如果想精确监控每个服务实例资源使用,就不能限制每个实例资源使用。因此有可能造成某个糟糕的服务实例占用了主机的所有内存或者CPU。
另一个严重问题在于运维团队必须知道如何部署的详细步骤。服务可以用不同语言和框架写成,因此开发团队肯定有很多需要跟运维团队沟通事项。其中复杂性增加了部署过程中出错的可能性
单容器单服务实例模式
?使用这种模式需要将服务打包成容器映像。一个容器映像是一个运行包含服务所需库和应用的文件系统?。某些容器映像由完整的linux根文件系统组成,其它则是轻量级的。例如,为了部署Java服务,需要创建包含Java运行库的容器映像,也许还要包含Apache
Tomcat server,以及编译过的Java应用。
一旦将服务打包成容器映像,就需要启动若干容器。一般在一个物理机或者虚拟机上运行多个容器,可能需要集群管理系统,例如k8s或者Marathon,来管理容器。集群管理系统将主机作为资源池,根据每个容器对资源的需求,决定将容器调度到那个主机上。
Serverless 部署
AWS Lambda是serverless部署技术的例子,支持Java,Node.js和Python服务;需要将服务打包成ZIP文件上载到AWS
Lambda就可以部署。可以提供元数据,提供处理服务请求函数的名字(一个事件)。AWS Lambda自动运行处理请求足够多的微服务,然而只根据运行时间和消耗内存量来计费。当然细节决定成败,AWS
Lambda也有限制。但是大家都不需要担心服务器,虚拟机或者容器内的任何方面绝对吸引人。
Lambda 函数 是无状态服务。一般通过激活AWS服务处理请求。例如,当映像上载到S3 bucket激活Lambda函数后,就可以在DynamoDB映像表中插入一个条目,给Kinesis流发布一条消息,触发映像处理动作。Lambda函数也可以通过第三方web服务激活。
有四种方法激活Lambda函数:
直接方式,使用web服务请求
自动方式,回应例如AWS S3,DynamoDB,Kinesis或者Simple Email Service等产生的事件
自动方式,通过AWS API网关来处理应用客户端发出的HTTP请求?
定时方式,通过cron响应?--很像定时器方式
可以看出,AWS Lambda是一种很方便部署微服务的方式。基于请求计费方式意味着用户只需要承担处理自己业务那部分的负载;另外,因为不需要了解基础架构,用户只需要开发自己的应用。
然而还是有不少限制。不需要用来部署长期服务,例如用来消费从第三方代理转发来的消息,请求必须在300秒内完成,服务必须是无状态,因为理论上AWS
Lambda会为每个请求生成一个独立的实例;必须用某种支持的语言完成,服务必须启动很快,否则,会因为超时被停止。 |