微服务架构-Spring Cloud

为何须要服务发现

简单来讲,服务化的核心就是将传统的一站式应用根据业务拆分红一个一个的服务,而微服务在这个基础上要更完全地去耦合(再也不共享DB、KV,去掉重量级ESB),而且强调DevOps和快速演化。这就要求咱们必须采用与一站式时代、泛SOA时代不一样的技术栈,而Spring Cloud就是其中的佼佼者。java

DevOps是英文Development和Operations的合体,他要求开发、测试、运维进行一体化的合做,进行更小、更频繁、更自动化的应用发布,以及围绕应用架构来构建基础设施的架构。这就要求应用充分的内聚,也方便运维和管理。这个理念与微服务理念不谋而合。nginx

接下来咱们从服务化架构演进的角度来看看为何Spring Cloud更适应微服务架构。redis

从使用nginx提及

最初的服务化解决方案是给提供相同服务提供一个统一的域名,而后服务调用者向这个域名发送HTTP请求,由Nginx负责请求的分发和跳转。NginxArch.png缓存

这种架构存在不少问题:服务器

Nginx做为中间层,在配置文件中耦合了服务调用的逻辑,这削弱了微服务的完整性,也使得Nginx在必定程度上变成了一个重量级的ESB。网络

服务的信息分散在各个系统,没法统一管理和维护。每一次的服务调用都是一次尝试,服务消费者并不知道有哪些实例在给他们提供服务。这不符合DevOps的理念。架构

没法直观的看到服务提供者和服务消费者当前的运行情况和通讯频率。这也不符合DevOps的理念。app

消费者的失败重发,负载均衡等都没有统一策略,这加大了开发每一个服务的难度,不利于快速演化。负载均衡

为了解决上面的问题,咱们须要一个现成的中心组件对服务进行整合,将每一个服务的信息汇总,包括服务的组件名称、地址、数量等。服务的调用方在请求某项服务时首先经过中心组件获取提供这项服务的实例的信息(IP、端口等),再经过默认或自定义的策略选择该服务的某一提供者直接进行访问。因此,咱们引入了Dubbo。框架

基于Dubbo实现微服务

Dubbo是阿里开源的一个SOA服务治理解决方案,文档丰富,在国内的使用度很是高。

使用Dubbo构建的微服务,已经能够比较好地解决上面提到的问题:

  • 调用中间层变成了可选组件,消费者能够直接访问服务提供者。
  • 服务信息被集中到Registry中,造成了服务治理的中心组件。
  • 经过Monitor监控系统,能够直观地展现服务调用的统计信息。
  • Consumer能够进行负载均衡、服务降级的选择。

可是对于微服务架构而言,Dubbo也并非十全十美的:

Registry严重依赖第三方组件(zookeeper或者redis),当这些组件出现问题时,服务调用很快就会中断。

DUBBO只支持RPC调用。使得服务提供方与调用方在代码上产生了强依赖,服务提供者须要不断将包含公共代码的jar包打包出来供消费者使用。一旦打包出现问题,就会致使服务调用出错。

最为重要的是,DUBBO如今已经中止维护了,对于技术发展的新需求,须要由开发者自行拓展升级。这对于不少想要采用微服务架构的中小软件组织,显然是不太合适的。

目前Github社区上有一个DUBBO的升级版,叫DUBBOX,提供了更高效的RPC序列化方式和REST调用方式。可是该项目也基本中止维护了。

新的选择——Spring Cloud

做为新一代的服务框架,Spring Cloud提出的口号是开发“面向云环境的应用程序”,它为微服务架构提供了更加全面的技术支持。

结合咱们一开始提到的微服务的诉求,咱们把Spring Cloud与DUBBO进行一番对比:

Spring Cloud抛弃了Dubbo的RPC通讯,采用的是基于HTTP的REST方式。严格来讲,这两种方式各有优劣。虽然从必定程度上来讲,后者牺牲了服务调用的性能,但也避免了上面提到的原生RPC带来的问题。并且REST相比RPC更为灵活,服务提供方和调用方的依赖只依靠一纸契约,不存在代码级别的强依赖,这在强调快速演化的微服务环境下,显得更加合适。

很明显,Spring Cloud的功能比DUBBO更增强大,涵盖面更广,并且做为Spring的拳头项目,它也可以与Spring Framework、Spring Boot、Spring Data、Spring Batch等其余Spring项目完美融合,这些对于微服务而言是相当重要的。前面提到,微服务背后一个重要的理念就是持续集成、快速交付,而在服务内部使用一个统一的技术框架,显然比把分散的技术组合到一块儿更有效率。更重要的是,相比于Dubbo,它是一个正在持续维护的、社区更加火热的开源项目,这就保证使用它构建的系统,能够持续地获得开源力量的支持。

Spring Cloud Netflix 组件

Netflix和Spring Cloud是什么关系呢?Netflix是一家成功实践微服务架构的互联网公司,几年前,Netflix就把它的几乎整个微服务框架栈开源贡献给了社区。Spring背后的Pivotal在2015年推出的Spring Cloud开源产品,主要对Netflix开源组件的进一步封装,方便Spring开发人员构建微服务基础框架。

对于微服务的治理而言,核心就是服务的注册和发现。因此选择哪一个组件,很大程度上要看它对于服务注册与发现的解决方案。在这个领域,开源架构不少,最多见的是Zookeeper,但这并非一个最佳选择。

在分布式系统领域有个著名的CAP定理:C——数据一致性,A——服务可用性,P——服务对网络分区故障的容错性。这三个特性在任何分布式系统中不能同时知足,最多同时知足两个。

Zookeeper是著名Hadoop的一个子项目,不少场景下Zookeeper也做为Service发现服务解决方案。Zookeeper保证的是CP,即任什么时候刻对

Zookeeper的访问请求能获得一致的数据结果,同时系统对网络分割具有容错性,可是它不能保证每次服务请求的可用性。从实际状况来分析,在使用Zookeeper获取服务列表时,若是zookeeper正在选主,或者Zookeeper集群中半数以上机器不可用,那么将就没法得到数据了。因此说,Zookeeper不能保证服务可用性。

诚然,对于大多数分布式环境,尤为是涉及到数据存储的场景,数据一致性应该是首先被保证的,这也是zookeeper设计成CP的缘由。可是对于服务发现场景来讲,状况就不太同样了:针对同一个服务,即便注册中心的不一样节点保存的服务提供者信息不尽相同,也并不会形成灾难性的后果。由于对于服务消费者来讲,能消费才是最重要的——拿到可能不正确的服务实例信息后尝试消费一下,也好过由于没法获取实例信息而不去消费。因此,对于服务发现而言,可用性比数据一致性更加剧要——AP赛过CP。而Spring Cloud Netflix在设计Eureka时遵照的就是AP原则。

Eureka自己是Netflix开源的一款提供服务注册和发现的产品,而且提供了相应的Java封装。在它的实现中,节点之间是相互平等的,部分注册中心的节点挂掉也不会对集群形成影响,即便集群只剩一个节点存活,也能够正常提供发现服务。哪怕是全部的服务注册节点都挂了,Eureka Clients上也会缓存服务调用的信息。这就保证了咱们微服务之间的互相调用是足够健壮的。

除此以外,Spring Cloud Netflix背后强大的开源力量,也促使咱们选择了Spring Cloud Netflix:

前文提到过,Spring Cloud的社区十分活跃,其在业界的应用也十分普遍(尤为是国外),并且整个框架也经受住了Netflix严酷生产环境的考验。

除了服务注册和发现,Spring Cloud Netflix的其余功能也十分强大,包括Ribbon,hystrix,Feign,Zuul等组件,结合到一块儿,让服务的调用、路由也变得异常容易。

Spring Cloud Netflix做为Spring的重量级整合框架,使用它也意味着咱们能从Spring获取到巨大的便利。Spring Cloud的其余子项目,好比Spring Cloud Stream、Spring Cloud Config等等,都为微服务的各类需求提供了一站式的解决方案。

Spring Cloud 服务发现

Spring Cloud Netflix的核心是用于服务注册与发现的Eureka,接下来咱们将以Eureka为线索,介绍Eureka、Ribbon、Hystrix、Feign这些Spring Cloud Netflix主要组件。

服务注册与发现——Eureka

Eureka这个词来源于古希腊语,意为“我找到了!我发现了!”,据传,阿基米德在洗澡时发现浮力原理,高兴得来不及穿上裤子,跑到街上大喊:“Eureka(我找到了)!”。

Eureka由多个instance(服务实例)组成,这些服务实例能够分为两种:Eureka Server和Eureka Client。为了便于理解,咱们将Eureka client再分为Service Provider和Service Consumer。以下图所示:EurekaRole.png

Eureka Server:服务的注册中心,负责维护注册的服务列表。

Service Provider:服务提供方,做为一个Eureka Client,向Eureka Server作服务注册、续约和下线等操做,注册的主要数据包括服务名、机器ip、端口号、域名等等。

Service Consumer:服务消费方,做为一个Eureka Client,向Eureka Server获取Service Provider的注册信息,并经过远程调用与Service Provider进行通讯。

Service Provider和Service Consumer不是严格的概念,Service Consumer也能够随时向Eureka Server注册,来让本身变成一个Service Provider。

Spring Cloud针对服务注册与发现,进行了一层抽象,并提供了三种实现:Eureka、Consul、Zookeeper。目前支持得最好的就是Eureka,其次是Consul,最后是Zookeeper。

Eureka Server

Eureka Server做为一个独立的部署单元,以REST API的形式为服务实例提供了注册、管理和查询等操做。同时,Eureka Server也为咱们提供了可视化的监控页面,能够直观地看到各个Eureka Server当前的运行状态和全部已注册服务的状况。

Eureka Server的高可用集群

Eureka Server能够运行多个实例来构建集群,解决单点问题,但不一样于ZooKeeper的选举leader的过程,Eureka Server采用的是Peer to Peer对等通讯。这是一种去中心化的架构,无master/slave区分,每个Peer都是对等的。在这种架构中,节点经过彼此互相注册来提升可用性,每一个节点须要添加一个或多个有效的serviceUrl指向其余节点。每一个节点均可被视为其余节点的副本。

若是某台Eureka Server宕机,Eureka Client的请求会自动切换到新的Eureka Server节点,当宕机的服务器从新恢复后,Eureka会再次将其归入到服务器集群管理之中。当节点开始接受客户端请求时,全部的操做都会进行replicateToPeer(节点间复制)操做,将请求复制到其余Eureka Server当前所知的全部节点中。

一个新的Eureka Server节点启动后,会首先尝试从邻近节点获取全部实例注册表信息,完成初始化。Eureka Server经过getEurekaServiceUrls()方法获取全部的节点,而且会经过心跳续约的方式按期更新。默认配置下,若是Eureka Server在必定时间内没有接收到某个服务实例的心跳,Eureka Server将会注销该实例(默认为90秒,经过eureka.instance.lease-expiration-duration-in-seconds配置)。当Eureka Server节点在短期内丢失过多的心跳时(好比发生了网络分区故障),那么这个节点就会进入自我保护模式。下图为Eureka官网的架构图

什么是自我保护模式?默认配置下,若是Eureka Server每分钟收到心跳续约的数量低于一个阈值(instance的数量*(60/每一个instance的心跳间隔秒数)*自我保护系数),就会触发自我保护。在自我保护模式中,Eureka Server会保护服务注册表中的信息,再也不注销任何服务实例。当它收到的心跳数从新恢复到阈值以上时,该Eureka Server节点就会自动退出自我保护模式。它的设计哲学前面提到过,那就是宁肯保留错误的服务注册信息,也不盲目注销任何可能健康的服务实例。该模式能够经过eureka.server.enable-self-preservation = false来禁用,同时eureka.instance.lease-renewal-interval-in-seconds能够用来更改心跳间隔,eureka.server.renewal-percent-threshold能够用来修改自我保护系数(默认0.85)。

Eureka Server的Region、Zone

Eureka的官方文档对Regin、Zone几乎没有说起,因为概念抽象,新手很难理解。所以,咱们先来了解一下Region、Zone、Eureka集群三者的关系,以下图所示:

region和zone(或者Availability Zone)均是AWS的概念。在非AWS环境下,咱们能够先简单地将region理解为Eureka集群,zone理解成机房。上图就能够理解为一个Eureka集群被部署在了zone1机房和zone2机房中。

Service Provider

服务注册

Service Provider本质上是一个Eureka Client。它启动时,会调用服务注册方法,向Eureka Server注册本身的信息。Eureka Server会维护一个已注册服务的列表,这个列表为一个嵌套的hash map:

第一层,application name和对应的服务实例。

第二层,服务实例及其对应的注册信息,包括IP,端口号等。

当实例状态发生变化时(如自身检测认为Down的时候),也会向Eureka Server更新本身的服务状态,同时用replicateToPeers()向其它Eureka Server节点作状态同步。

续约与剔除

前面提到过,服务实例启动后,会周期性地向Eureka Server发送心跳以续约本身的信息,避免本身的注册信息被剔除。续约的方式与服务注册基本一致:首先更新自身状态,再同步到其它Peer。

若是Eureka Server在一段时间内没有接收到某个微服务节点的心跳,Eureka Server将会注销该微服务节点(自我保护模式除外)。

Service Consumer

Service Consumer本质上也是一个Eureka Client(它也会向Eureka Server注册,只是这个注册信息可有可无罢了)。它启动后,会从Eureka Server上获取全部实例的注册信息,包括IP地址、端口等,并缓存到本地。这些信息默认每30秒更新一次。前文提到过,若是与Eureka Server通讯中断,Service Consumer仍然能够经过本地缓存与Service Provider通讯。

实际开发Eureka的过程当中,有时会碰见Service Consumer获取到Server Provider的信息有延迟,在Eureka Wiki中有这么一段话:

All operations from Eureka client may take some time to reflect in the Eureka servers and subsequently in other Eureka clients. This is because of the caching of the payload on the eureka server which is refreshed periodically to reflect new information. Eureka clients also fetch deltas periodically. Hence, it may take up to 2 mins for changes to propagate to all Eureka clients.

最后一句话提到,服务端的更改可能须要2分钟才能传播到全部客户端,至于缘由并无介绍。这是由于Eureka有三处缓存和一处延迟形成的。

  1. Eureka Server对注册列表进行缓存,默认时间为30s。
  2. Eureka Client对获取到的注册信息进行缓存,默认时间为30s。
  3. Ribbon会从上面提到的Eureka Client获取服务列表,将负载均衡后的结果缓存30s。
  4. 若是不是在Spring Cloud环境下使用这些组件(Eureka, Ribbon),服务启动后并不会立刻向Eureka注册,而是须要等到第一次发送心跳请求时才会注册。心跳请求的发送间隔默认是30s。Spring Cloud对此作了修改,服务启动后会立刻注册。

基于Service Consumer获取到的服务实例信息,咱们就能够进行服务调用了。而Spring Cloud也为Service Consumer提供了丰富的服务调用工具:

  1. Ribbon,实现客户端的负载均衡。
  2. Hystrix,断路器。
  3. Feign,RESTful Web Service客户端,整合了Ribbon和Hystrix。

接下来咱们就一一介绍。

服务调用端负载均衡——Ribbon

Ribbon是Netflix发布的开源项目,主要功能是为REST客户端实现负载均衡。它主要包括六个组件:

  1. ServerList,负载均衡使用的服务器列表。这个列表会缓存在负载均衡器中,并按期更新。当Ribbon与Eureka结合使用时,ServerList的实现类就是DiscoveryEnabledNIWSServerList,它会保存Eureka Server中注册的服务实例表。
  2. ServerListFilter,服务器列表过滤器。这是一个接口,主要用于对Service Consumer获取到的服务器列表进行预过滤,过滤的结果也是ServerList。Ribbon提供了多种过滤器的实现。
  3. IPing,探测服务实例是否存活的策略。
  4. IRule,负载均衡策略,其实现类表述的策略包括:轮询、随机、根据响应时间加权等。咱们也能够本身定义负载均衡策略,好比咱们就利用本身实现的策略,实现了服务的版本控制和直连配置。实现好以后,将实现类从新注入到Ribbon中便可。
  5. ILoadBalancer,负载均衡器。这也是一个接口,Ribbon为其提供了多个实现,好比ZoneAwareLoadBalancer。而上层代码经过调用其API进行服务调用的负载均衡选择。通常ILoadBalancer的实现类中会引用一个IRule。
  6. RestClient,服务调用器。顾名思义,这就是负载均衡后,Ribbon向Service Provider发起REST请求的工具。

Ribbon工做时会作四件事情:

  1. 优先选择在同一个Zone且负载较少的Eureka Server;
  2. 按期从Eureka更新并过滤服务实例列表;
  3. 根据用户指定的策略,在从Server取到的服务注册列表中选择一个实例的地址;
  4. 经过RestClient进行服务调用。

服务调用端熔断——Hystrix

Netflix建立了一个名为Hystrix的库,实现了断路器的模式。“断路器”自己是一种开关装置,当某个服务单元发生故障以后,经过断路器的故障监控(相似熔断保险丝),向调用方返回一个符合预期的、可处理的备选响应(FallBack),而不是长时间的等待或者抛出调用方没法处理的异常,这样就保证了服务调用方的线程不会被长时间、没必要要地占用,从而避免了故障在分布式系统中的蔓延,乃至雪崩。

固然,在请求失败频率较低的状况下,Hystrix仍是会直接把故障返回给客户端。只有当失败次数达到阈值(默认在20秒内失败5次)时,断路器打开而且不进行后续通讯,而是直接返回备选响应。固然,Hystrix的备选响应也是能够由开发者定制的。

除了隔离依赖服务的调用之外,Hystrix还提供了准实时的调用监控(Hystrix Dashboard),Hystrix会持续地记录全部经过Hystrix发起的请求的执行信息,并以统计报表和图形的形式展现给用户,包括每秒执行多少请求多少成功,多少失败等。Netflix经过hystrix-metrics-event-stream项目实现了对以上指标的监控。Spring Cloud也提供了Hystrix Dashboard的整合,对监控内容转化成可视化界面,Hystrix Dashboard Wiki上详细说明了图上每一个指标的含义。

服务调用端代码抽象和封装——Feign

Feign是一个声明式的Web Service客户端,它的目的就是让Web Service调用更加简单。它整合了Ribbon和Hystrix,从而让咱们再也不须要显式地使用这两个组件。Feign还提供了HTTP请求的模板,经过编写简单的接口和插入注解,咱们就能够定义好HTTP请求的参数、格式、地址等信息。接下来,Feign会彻底代理HTTP的请求,咱们只须要像调用方法同样调用它就能够完成服务请求。

Feign具备以下特性:

  • 可插拔的注解支持,包括Feign注解和JAX-RS注解
  • 支持可插拔的HTTP编码器和解码器
  • 支持Hystrix和它的Fallback
  • 支持Ribbon的负载均衡
  • 支持HTTP请求和响应的压缩

如下是一个Feign的简单示例:

@SpringBootApplication
@EnableDiscoveryClient //启用Feign
@EnableFeignClients
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

}
@FeignClient(name = "elements", fallback = ElementsFallback.class)
//指定feign调用的服务和Hystrix Fallback(name即eureka的application name)
public interface Elements {

    @RequestMapping(value = "/index")
    String index();

}
//Hystrix Fallback
@Component
public class ElementsFallback implements Elements {

    @Override
    public String index() {
        return "**************";
    }

}

测试类:

@Component
public class TestController {

    @Autowired
    Elements elements;

    @RequestMapping(value = "/testEureka", method = RequestMethod.GET)
    public String testeureka() {
        return elements.index();
    }

}