本质上就是一种用于构造应用的架构方案,这种架构不同于传统的单体式架构将各种模块功能放置在一个应用里面,然后将应用的实例部署在多个服务器上,通过一个负载均衡的代理服务器将请求分发到各个服务器上,而是:
这样,对一个前端应用来说,每一个页面,甚至是页面里面的一个元素,都可能对应一个服务,而前端只需要专注于业务逻辑的实现,调用对应的服务即可。因此,微服务架构具有非常高的可扩展性和代码利用率。
不过,尽管这样的架构看起来非常理想,但是在实际应用上依然存在很多问题,例如:
另外一个与网关类似的东西——代理。
网关与代理的区别:代理是纯粹的数据透传,协议不会发生变化;网关在数据透传的背景下,还会设计协议的转换,比如上图中用户请求传输到网关的协议是HTTP,通过网关透传到下游则可能已经转换成企业内部的RPC了(比如JSF、Dubbo等企业自研的RPC框架)。
通过 API 网关,不仅可以在不受协议限制的情况下进行API请求的转发(routing),还可以针对所有的API接入进行统一的鉴权,并且还可以根据接入的流量进行流量限制以及流量的监控和相关告警等。这样,不仅可以将每个网关在鉴权,权限等一些通用的逻辑剥离出来,统一到网关上完成,还可以让前端更加专注于业务逻辑上判断,所有请求都发送到一个统一的入口,而不需要与每个微服务都进行通信,具体接入的API由 API 网关路由确定。
显然,这样带来的另外一个问题就是:当所有流量都往 API 网关汇聚时,API 网关自然将会成为限制访问流量的瓶颈,并且当面对突发的大量访问必须保证 API 网关的稳定性和高可用性,否则这个单点的故障很有可能导致后续的微服务将不可用。另外,单个微服务的故障,也可能拖累整个 API 网关导致相应的功能受限。
Eureka 事实上也是一个服务,这是一个在 AWS 提供服务定位功能的服务,以实现负载均衡和服务器故障转移,即服务的高可用保障,通常我们称之为这是一个 Eureka 服务器,即 Eureka Server。此外,Eureka 还包含一个客户端,Eureka Client,用于和企业内部或者是外部的业务服务进行更加高效的交互,而且,在这个客户端中还包含了一个负载均衡器,执行基本的循环负载平衡。
所以,Eureka 就是一个服务发现的框架,Eureka Server 即整个 AWS 的服务注册中心,由一个去中心化的服务器集群组成,每个节点同步服务注册信息,然后扮演 服务提供者 的微服务依靠 Eureka Client 和服务注册中心进行交互,提供地址以及端口等元数据作为注册信息,相应的服务,从而实现 服务注册(Register)。
并且服务注册中心为了保证对外提供的服务一直处于可用的状态,因此客户端必须定时在注册中心更新自己的信息,表示自己依然处于可用状态,这就是 Eureka 框架中的 服务续约(Renew)。默认情况下,注册中心要求客户端每隔 30 s 发送心跳在注册中心更新一次自己的信息,而当注册中心经过 90 s ,即三个 续约周期 没有接收到客户端的心跳信息时,注册中心将会认为这个服务已经停用或者处于不可用的状态,删除对应的注册信息,并更新到每个 Eureka Server 集群的节点上,这就是 服务剔除(Eviction)。
另外,每个客户端还会定期向注册中心获取可用的服务列表并缓存在本地内存中,从而通过相应的服务信息(即前面所讲到的IP地址,端口等元数据信息)实现对其他微服务的远程调用。通常客户端每隔 30 s 获取服务端的服务列表信息,客户端自动对信息进行处理,并更新本地缓存的服务信息。而当因为特殊情况客户端发现注册信息不匹配或者不可用时,客户端将会重新拉取整个列表并进行更新。默认情况下服务端的注册中心和客户端通过压缩的 JSON 格式数据来进行通讯,获取注册信息,这就是 获取注册列表信息(Fetch Registry)。
当然,客户端也可以主动向服务端发送请求进行服务下线,即在客户端进行程序关闭的时候,向服务端发送注册取消请求。服务端接收到取消请求之后,即将客户端实例从注册列表中剔除。下线请求不会自动完成,需要调用相应的函数实现。
Eureka 框架基本,活动图如下:
图 2-1 Eureka Server 服务注册,更新,订阅
通过 Eureka 这样的一个服务注册框架,不仅使得外部的的请求能够顺利获得所需的每一个微服务的位置,而且每一个服务之间也可以通过从服务注册中心拉取服务信息注册列表并缓存在本地内存中,从而根据其他服务的注册信息借助远程服务调用工具实现不同微服务之间的互相调用。
对于微服务框架来说,每个微服务除了需要通过在注册中心进行注册从而实现 ”被发现“ 之外,对于一个微服务自己本身来说,有时候还需要调用其他服务来实现一些关于业务上的功能,这就涉及到了服务与服务之间的相互调用,往往需要对一个服务针对目标服务设计相对应的请求以及响应等相关处理,设计相应的接口,从而在每次调用特定的服务的时候,在服务调用前后进行其他相关服务的调用。
事实上这样子的机制就是一种 服务代理 ,简单的服务代理往往可以通过 静态代理 来实现服务通信。但是静态代理存在一个致命的缺点:开发人员在对每一个服务进行服务代理的设计时,都需要添加几乎相同的静态代理,而这些逻辑的相关代码几乎是完全相同的。不管从工作量的角度,还是代码设计的角度来看,都不是一种理想的解决方法。
这时我们就需要用到 动态代理,不再需要对于每种服务都需要进行相应的代理设置,并创建相应的代理类,而是在运行的时候动态的创建代理类,并通过 Java 的反射机制将这个代理类动态的加载到内存当中,从而实现动态代理。
而 Feign 就是建立在动态代理的基础上的一个服务之间的远程调用框架,本质上是 java 的反射原理,即:反射+动态加载+代理 的一种机制,针对一个服务到另外一个服务之间的一系列行为的复杂代码进行了封装,从而可以非常简单的完成服务到服务的请求,请求处理,响应以及响应处理等机制的实现。
Open Feign 虽然可以顺利的解决服务之间的访问问题,但是,有一些服务往往需要部署在多台主机上,例如一些高可用的分布式服务,而 Feign 本身并没有机制可以决定自己应该访问那一台主机上面的服务。而如果此时大量的服务都请求到一台主机上面,导致主机崩溃,而事实上另外几台服务器一直都处于可用的状态,这样对于集群服务的高可用等优越性将完全没有发挥任何作用。
因此,对于集群服务的访问来说,负载均衡 至关重要,比如 Nginx 在服务器端这边通过一个负载均衡器,将发送到这台服务器上的所有请求集中进行负载均衡,从而使得请求均匀分配到每台服务器上,保证了集群服务的高可用性。
图 2-2 Nginx 负载均衡
而 Ribbon 本质上是一个运行在客户端的负载均衡器,通过类似 Round Robin(轮询)或者是其他的负载均衡算法,将当前业务线程的请求分配到目标服务的每一台主机上,从而既保证了对集群服务的有效访问,同时也保证了集群服务高可用性的有效利用。
图 2-3 Robbin 客户端上的负载均衡
当一个微服务因为某种原因出现故障的时候,这时来自外部的请求虽然可以通过 Eureka 的服务注册中心发现并访问这个微服务,但是由于故障影响将会导致所有相关的请求都会被阻隔在这个微服务上,对于用户的客户端来说将会一直处于未响应的状态,这时该服务需要处理的请求线程数量不断增加最终达到上限,这将使得上游的服务,例如需要调用到这个微服务的其他微服务,都将会在这里阻塞,无法响应。这就是微服务框架中最为严重的 服务雪崩 问题。
例如,一个业务 1,这个业务主要分为两个服务 a 和 b,而对用户来说,有的用户并不总是需要同时使用这两种服务,而只需要借助其中一个服务就可以完成自己的业务需求。但是如果这个时候,服务 b 出现了问题,这时所有和服务 b 相关的用户请求都将因为服务 b 的故障问题使得其业务的相关线程处于阻塞状态;此外,对于每个业务来说,其对应的线程池中分配的线程数量显然是有限的,因为阻塞问题,线程数量必然不断增加,最终导致整个线程池中的线程都被阻塞线程占满。这时问题就非常明显了:此时已经没有线程可以为使用服务 a 的用户正常提供服务了,而事实上服务 a 始终都处于可用的状态。如下图所示,可以看到最后步骤 9 时,服务 a 即因线程池可用线程被占满导致不可用:
图 2-4 服务雪崩 例
而从用户的角度上来看,显然每个用户更希望请求的相关业务能够得到尽可能快的响应,并且自己的需求可以得到满足即可,而不是因为一些额外的服务导致自己需要办理的核心业务反而一直处于未响应的状态,这显然是不合理的。所以这个时候,对于这个故障服务来说,更加有效的做法是直接告诉后续到来的请求,目前当前服务不可用,直接向客户端返回一个包含对应信息的结果,这样,客户端就可以继续正常完成业务的后续操作,保证了核心业务正常运行。
这就是 “熔断” 机制,即当,当前服务出现问题的时候,线程只需要直接对后续的请求直接返回相应的信息即可,而不是持续等待故障服务恢复导致线程阻塞最终线程池中所有线程被用满:
图 2-5 熔断
但是这样依然存在另外的一个问题,虽然前端的业务通过熔断避开了故障接口,顺利的完成了操作,但是微服务的故障问题还是没有得到有效的解决,这显然不是一个完备的解决方法,因为微服务所作的操作以及相关的结果无法得到有效的保存,即使在微服务的故障得到顺利修复之后,也无法对故障期间跳过的请求的相应操作和结果进行有效的恢复。这时就需要用到 服务降级,此时服务依然处于不可用的状态,但是线程将会临时对客户端的操作进行记录,缓存相应的结果,并对客户端进行正常响应,直到微服务的故障得到有效修复,服务重新启动之后,再根据服务降级期间的记录进行数据恢复,这样,服务就依然可以在故障期间保证数据和操作的同步,可以从而使得服务在熔断期间保证前端业务正常运转的同时,故障服务的基本功能也可以得到有效保障。
图 2-6 降级
这就是 Hystrix 的核心机制,Hystrix 通过一个 Hystrix 线程池对每个业务线程进行管理,每个服务请求的线程只能和该特定服务进行通信,当外部对于某个微服务的请求的失败率达到某个阈值的时候, Hystrix 将会开启对应的熔断器,并启用特定的降级方法进行服务降级,从而既保证了前端业务的正常运转,又保证了微服务的数据和操作同步。
Ribbon 常用的负载均衡策略如下:
RoundRobbinRule
:轮询策略,默认采用的策略,如果经过 10 轮轮询没有找到可用的服务 provider
,则返回空值provider
失败之后在指定时间限制内重试,默认为 500 毫秒Zuul 在 Netflix 的官网定义就是:Router and Filter,即路由和过滤的功能,这就是 Zuul 的主要功能,也是 API 网关概念的关键技术部分,通过向外界请求提供一个统一的入口,使得请求客户端不需要关注微服务架构中包含的具体服务细节,记录每一个服务对应的地址和接口,而只需要和统一的微服务网关入口进行对接即可。
因此,Zuul 需要做的第一件事情就是,在 Eureka 的服务注册中心中进行注册,通过同样注册成为一项服务,这样 Zuul 就可以通过服务订阅得到所有相关微服务的 IP,端口等接口调用信息,从而进行路由映射实现接口的统一调度。