1.架构的发展历程
随着应用的规模不断扩大,系统架构也从从单一应用,到垂直拆分,到分布式服务,到SOA,以及现在火热的微服务架构。集中式架构只需一个应用,将所有功能都部署在一起,以减少部署节点和成本。

当访问量逐渐增大,单一应用无法满足需求,此时为了应对更高的并发和业务需求,我们根据业务功能对系统进行拆分。

分布式的架构将基础服务进行了抽取,系统间相互调用,提高了代码复用和开发效率,但是系统间耦合度变高,调用关系错综复杂,难以维护。

SOA一个面向服务的架构,使用服务治理实现服务自动注册、发现、订阅、推送和监控,解决了调用关系的错综复杂。但是一旦某个环节出错就会影响较大,运维和测试都会变得很困难,不符合DevOps思想。

微服务以对服务的拆分来解决服务的自治,使用容器让每一个服务都能够做到自己对应的业务能力,实现单一的职责。

每个服务之间的调用方式一共有两种-RPC和HTTP,RPC一种远程调用的方式,早期的webservice,现在的dubbo框架都是使用这种调用。HTTP一种网络传输协议,对服务的提供和调用方没有任何技术限定,现在的Rest风格,就可以通过http协议来实现。
2.Spring Cloud的简介
Spring Cloud一个微服务架构的集大成者,简单介绍一下他的五大组件,分别是Eureka注册中心、Zuul服务网关、Ribbon负载均衡、Feign服务调用、Hystix熔断器。以下是其中一部分的架构图。
Eureka
Eureka简称为注册中心,当我们后台的服务越来越多的时候,我们就必须对这些服务进行管理,目前使用较多的有zookeeper和Eureka,今天我们介绍优化很好的Eureka,他实现了服务的自动注册、发现和状态监控。其下是基本架构图。

其中的Eureka-Server就是服务的注册中心,对外暴露自己的地址;提供者就是启动后向Eureka注册自己信息,并且定期进行服务续约;消费者就是服务调用方,会定期去Eureka拉取服务列表,然后使用负载均衡算法选出一个服务进行调用;心跳续约就是提供者定期通过http方式向Eureka刷新自己的状态,当某个服务提供方出现问题,Eureka自然会把它从服务列表中剔除。Eureka-Server还可以是一个集群,实现高可用,多个Eureka-Server之间互相注册为服务,当服务提供者注册到Eureka Server集群中的某个节点时,该节点会把服务的信息同步给集群中的每个节点。其下架构实体图。

其实就是把service-url的值改成除自己之外的一台或者多台EurekaServer的地址。服务注册就是服务提供者在启动时,会检测配置属性中的eureka.client.register-with-erueka=true参数是否正确,事实上默认就是true。如果值确实为true,则会向EurekaServer发起一个Rest请求,并携带自己的元数据信息,Eureka Server会把这些信息保存到一个双层Map结构中。第一层Map的Key就是服务id,一般是配置中的spring.application.name属性,第二层Map的key是服务的实例id,值则是服务的实例对象,也就是说一个服务,可以同时启动多个不同实例,形成集群。在注册服务完成以后,服务提供者会维持一个心跳(定时向EurekaServer发起Rest请求),告诉EurekaServer:“我还活着”。这个我们称为服务的续约。其实是有两个重要参数可以修改服务续约的行为。下图展示。

第一个是服务失败时间,默认值为90秒,第二个是服务续约的间隔,默认为30秒。当服务消费者启动是,会检测eureka.client.fetch-registry=true参数的值,如果为true,则会从Eureka Server服务的列表只读备份,然后缓存在本地。并且每隔30秒会重新获取并更新数据。我们也可以通过下面的参数来修改。

当服务进行正常关闭操作时,它会触发一个服务下线的REST请求给Eureka Server,告诉服务注册中心:“我要下线了”。服务中心接受到请求之后,将该服务置为下线状态。有时我们的服务可能由于内存溢出或网络故障等原因使得服务不能正常的工作,而服务注册中心并未收到“服务下线”的请求。相对于服务提供者的“服务续约”操作,服务注册中心在启动时会创建一个定时任务,默认每隔一段时间(默认为60秒)将当前清单中超时(默认为90秒)没有续约的服务剔除,这个操作被称为失效剔除。Eureka也有自我保护机制。当服务未按时进行心跳续约时,Eureka会统计服务实例最近15分钟心跳续约的比例是否低于了85%。在生产环境下,因为网络延迟等原因,心跳失败实例的比例很有可能超标,但是此时就把服务剔除列表并不妥当,因为服务可能没有宕机。Eureka在这段时间内不会剔除任何服务实例,直到网络恢复正常。生产环境下这很有效,保证了大多数服务依然可用,不过也有可能获取到失败的服务实例,因此服务调用者必须做好服务的失败容错。可以通过下面的配置来关停自我保护。
负载均衡Ribbon
当我们在开启多集群的服务情况下,此时我们获取的服务列表中就会有多个,这个时候访问就出现了问题。这时候我们需要编写负载均衡算法,在多个实例列表中进行选择。不过Eureka中已经帮我们集成了负载均衡组件:Ribbon,简单修改代码即可使用。下图是Ribbon的简介。

具体使用的话,首先我们在RestTemplate的配置方法上添加@LoadBalanced注解,然后修改调用方式,不再手动获取ip和端口,而是直接通过服务名称调用。最后启动即可,当我们对其进行源码跟踪的时候,发现根据服务名称获取到了服务实例的ip和端口,其实就是LoadBalancerInterceptor对RestTemplate的请求进行拦截,然后从Eureka根据服务id获取服务列表,随后利用负载均衡算法得到真实的服务地址信息,替换服务id。当我们进一步跟踪的时候就会看到获取服务通过一个getServer方法,再跟踪的时候就会看到chooseServer方法里的rule,而rule默认值是一个RoundRobinRule,通过类的介绍我们发现他是一个轮询实现了IRule接口。

查看一下定义的负载均衡的规则接口,他有以下的实现。

SpringBoot也帮我们提供了修改负载均衡规则的配置入口。

格式是:{服务名称}.ribbon.NFLoadBalancerRuleClassName,值就是IRule的实现类。Ribbon默认是采用懒加载,即第一次访问时才会去创建负载均衡客户端。往往会出现超时。如果需要采用饥饿加载,即项目启动即创建,可以这样配置。

最后附上一张负载均衡的源码流程图。
Hystrix
Hystix是Netflix开源的一个延迟和容错库,用于隔离访问远程服务、第三方库,防止出现级联失败。在微服务中,服务间调用关系错综复杂,一个请求,可能需要调用1个或多个微服务接口才能实现,会形成非常复杂的调用链路。如果当某个服务出现异常,请求就会阻塞,线程就不会释放,当越来越多的用户请求这个服务的时候,就会让越来越多的线程阻塞,导致服务器资源耗尽,从而使所有其它服务都不可用,形成雪崩效应。而Hystix是可以解决雪崩的问题,其手段主要是服务降级,包括线程隔离和服务熔断。其下是线程隔离示意图。

Hystrix为每个依赖服务调用分配一个小的线程池,如果线程池已满调用将被立即拒绝,默认不采用排队.加速失败判定时间。
用户的请求将不再直接访问服务,而是通过线程池中的空闲线程来访问服务,如果线程池已满,或者请求超时,则会进行降级处理。服务的降级就是优先保证核心服务,而非核心服务不可用或弱可用。而触发Hystix服务降级只有线程池已满或者请求超时这两种情况。使用的话,第一步引依赖;

第二步在启动类上添加注解@EnableCircuitBreaker;第三步编写降级逻辑,在降级的方法或者类上添加@HystrixCommand(fallbackMethod = "queryByIdFallBack")注解来声明一个降级逻辑的方法。上面介绍了Hystrix解决雪崩问题的第一种办法,线程隔离-服务降级,现在介绍第二种办法,服务熔断。熔断器也叫断路器,下图是其介绍。

Hystix的熔断状态机模型:

他有三种状态,第一种为关闭状态,断路器关闭所有请求都正常访问。第二种为打开状态,断路器打开所有请求都会被降级。Hystix会对请求情况计数,当一定时间内失败请求百分比达到阈值,则触发熔断,断路器会完全关闭。默认失败比例的阈值是50%,请求次数最少不低于20次。第三种为半开状态,断路器进入这种状态后,会释放1次请求通过,若这个请求是健康的,则会关闭断路器,否则继续保持打开,再次进行5秒休眠计时。
Feign
服务与服务之间的调用,前面我们使用的是RestTemplate➕Ribbon,当服务之间调用越来越多的时候,我们就会编写这种类似的大量重复的代码。为了避免这一问题,feign把Rest的请求进行隐藏,伪装成似SpringMVC的Controller一样。你不用再自己拼接url,拼接参数等等操作,一切都交给Feign去做,而且他集成了Ribbon依赖和自动配置。具体使用的话,首先我们还是老规矩,第一步导入依赖;

第二步加注解,在feign的客户端的接口上添加@FeignClient(value=“”),声明这是一个Feign客户端,同时通过value属性指定服务名称。第三步在启动类上添加@EnableFeignClients注解,开启Feign功能。最后测试即可。在整个过程中Feign会通过动态代理帮我们生成实现类,这点和mybatis的通用mapper很类似。客户端接口中的方法完全采用SpringMVC的注解,Feign会根据注解帮我们生成URL,并访问获取结果。Feign也有对Hystix的集成,请求的压缩和日志级别的定义。
Zuul网关
Zuul网关是对前端请求的拦截和处理,看到请求的拦截是不是想到了前面我介绍的Filter,其实Zuul就是对Filter的封装。下面就是其架构图。

其实不管是来自客户端(PC或移动端)的请求,还是服务内部调用。一切对服务的请求都会经过Zuul这个网关,然后再由网关来实现 鉴权、动态路由等等操作。Zuul就是我们服务的统一入口。使用的话还是老规矩,第一步添加依赖;

第二步在启动类上添加@EnableZuulProxy注解,开启Zuul功能;第三步编写配置;

我们将符合path规则的一切请求,都代理到url参数指定的地址。如果当同一个服务有多个实例的话,路径对应的服务地址就不能写死,我们需要把其注册到Eureka上。使用通过服务名称来获取服务。因为往往路由名称和服务名一样,所以我们可以这样简化配置。

又因为在默认情况下,一切服务的映射路径就是服务名本身,所以我们可以不用配置。在代码层面的话,ZuulFilter是过滤器的顶级父类,shouldFilter方法返回一个Boolean值,判断该过滤器run是否需要执行。返回true执行,返回false不执行。
run是过滤器的具体业务逻辑,决定是否放行。filterType是返回字符串,代表过滤器的类型,包括pre请求在被路由之前执行,route在路由请求时调用,post在routing和error过滤器之后调用,error在处理请求时发生错误调用。filterOrder是通过返回的int值来定义过滤器的执行顺序,数字越小优先级越高。下面是官网提供的请求生命周期图。

当我们了解过了过滤器执行的生命周期后,我们也可以自定义一个过滤器类,首先编写一个类,然后在类上加上@Component注解,继承ZuulFilter。接下来在类中写入filterType、filterOrder、shouldFilter、run四个方法,在run的方法里写入请求拦截的具体业务。Zuul也可以使用集群,实现高可用。