SpringCloud


概念

  1. 分布式 - 将一个庞大的系统按照模块进行拆分,拆分成若干个子模块[微服务] , 进行分布式的部署[各个服务部署在不同的服务器]

    着重点 - 有无进行分布式部署 - 部署的方式

  2. 微服务 - 架构风格,微服务不一定是分布式的.但是分布式微服务架构的.

    “每个微服务都是单独的独立的工程项目,可以进行单独的部署 = 不同的人做不同的事情”

  3. 集群 - “很多人做同一件事情” - 分布式上的每个节点[微服务]都是可以进行集群的.

    解决”三高” - 高并发,高性能,高可用

微服务

  1. 什么是我服务?

    • 微服务是一种架构风格,也是一种服务;
    • 微服务的颗粒比较小,一个大型复杂软件应用由多个微服务组成,比如Netflix目前由500多个的微服务组成;
    • 它采用UNIX设计的哲学,每种服务只做一件事,是一种松耦合的能够被独立开发和部署的无状态化服务(独立扩展、升级和可替换)。
  2. 微服务架构图

  3. 微服务好处

    • 技术异构性:在一个由多个服务相互协作的系统中,可以在不同的服务中使用最适合该服务的技术。
    • 弹性:如果系统中的一个组件不可用了,但并没有导致级联故障,那么系统的其他部分还可以正常运行。
    • 扩展:可以只对那些需要扩展的服务进行扩展。
      简化部署:各个服务的部署是独立的,这样就可以更快地对特定部分的代码进行部署。
    • 与组织结构相匹配:可以很好地将架构与组织结构相匹配,避免出现过大的代码库,从而获得理想团队大小及生产力。
    • 可组合性:不同服务模块的接口可以再进行重用,成为其他产品中的一个组件;
    • 对可替代性的优化:可以在需要时轻易地重写服务,或者删除不再使用的服务
  4. 微服务缺点

    • 运维开销
      更多的服务也就意味着更多的运维,产品团队需要保证所有的相关服务都有完善的监控等基础设施,传统的架构开发者只需要保证一个应用正常运行,而现在却需要保证几十甚至上百道工序高效运转,这是一个艰巨的任务。

    • DevOps要求
      使用微服务架构后,开发团队需要保证一个Tomcat集群可用,保证一个数据库可用,这就意味着团队需要高品质的DevOps和自动化技术。而现在,这样的全栈式人才很少。

    • 隐式接口
      服务和服务之间通过接口来“联系”,当某一个服务更改接口格式时,可能涉及到此接口的所有服务都需要做调整。

    • 重复劳动
      在很多服务中可能都会使用到同一个功能,而这一功能点没有足够大到提供一个服务的程度,这个时候可能不同的服务团队都会单独开发这一功能,重复的业务逻辑,这违背了良好的软件工程中的很多原则。

    • 分布式系统的复杂性
      微服务通过REST API或消息来将不同的服务联系起来,这在之前可能只是一个简单的远程过程调用。分布式系统也就意味着开发者需要考虑网络延迟、容错、消息序列化、不可靠的网络、异步、版本控制、负载等,而面对如此多的微服务都需要分布式时,整个产品需要有一整套完整的机制来保证各个服务可以正常运转。

    • 事务、异步、测试面临挑战
      跨进程之间的事务、大量的异步处理、多个微服务之间的整体测试都需要有一整套的解决方案,而现在看起来,这些技术并没有特别成熟。

SpringCloud介绍

  • springcloud是微服务架构的集大成者,将一系列优秀的组件进行了整合。基于springboot构建,对我们熟悉spring的程序员来说,上手比较容易。

  • 通过一些简单的注解,我们就可以快速的在应用中配置一下常用模块并构建庞大的分布式系统。

  • SpringCloud的组件相当繁杂,拥有诸多子项目。重点关注Netflix

下面简单介绍下经常用的5个:

服务发现——Netflix Eureka

客服端负载均衡——Netflix Ribbon(重点掌握Netflix Feign)

断路器——Netflix Hystrix

服务网关——Netflix Zuul

分布式配置——Spring Cloud Config

Eureka

作用:实现服务治理(服务注册与发现)

简介:Spring Cloud Eureka是Spring Cloud Netflix[停止更新]项目下的服务治理模块。

由两个组件组成:Eureka服务端和Eureka客户端。

Eureka服务端用作服务注册中心。支持集群部署。

Eureka客户端是一个java客户端,用来处理服务注册与发现。

在应用启动时,Eureka客户端向服务端注册自己的服务信息,同时将服务端的服务信息缓存到本地。客户端会和服务端周期性的进行心跳交互,以更新服务租约和服务信息。

Ribbon

作用:Ribbon,主要提供客户侧的软件负载均衡算法。

简介:Spring Cloud Ribbon是一个基于HTTP和TCP的客户端负载均衡工具,它基于Netflix Ribbon实现。通过Spring Cloud的封装,可以让我们轻松地将面向服务的REST模版请求自动转换成客户端负载均衡的服务调用。

注意看上图,关键点就是将外界的rest调用,根据负载均衡策略转换为微服务调用。Ribbon有比较多的负载均衡策略,以后专门讲解。

Hystrix

作用:断路器,保护系统,控制故障范围。

简介:为了保证其高可用,单个服务通常会集群部署。由于网络原因或者自身的原因,服务并不能保证100%可用,如果单个服务出现问题,调用这个服务就会出现线程阻塞,此时若有大量的请求涌入,Servlet容器的线程资源会被消耗完毕,导致服务瘫痪。服务与服务之间的依赖性,故障会传播,会对整个微服务系统造成灾难性的严重后果,这就是服务故障的“雪崩”效应。

Zuul

作用:api网关,路由,负载均衡等多种作用

简介:类似nginx,反向代理的功能,不过netflix自己增加了一些配合其他组件的特性。

在微服务架构中,后端服务往往不直接开放给调用端,而是通过一个API网关根据请求的url,路由到相应的服务。当添加API网关后,在第三方调用端和服务提供方之间就创建了一面墙,这面墙直接与调用方通信进行权限控制,后将请求均衡分发给后台服务端。

config

作用:配置管理

简介:SpringCloud Config提供服务器端和客户端。服务器存储后端的默认实现使用git,因此它轻松支持标签版本的配置环境,以及可以访问用于管理内容的各种工具。

这个还是静态的,得配合Spring Cloud Bus实现动态的配置更新。

相关组件架构图

从上图可以看出Spring Cloud各个组件相互配合,合作支持了一套完整的微服务架构。

  • 其中Eureka负责服务的注册与发现,很好将各服务连接起来
  • Hystrix 负责监控服务之间的调用情况,连续多次失败进行熔断保护。
  • Hystrix dashboard,Turbine 负责监控 Hystrix的熔断情况,并给予图形化的展示
  • Spring Cloud Config 提供了统一的配置中心服务
  • 当配置文件发生变化的时候,Spring Cloud Bus 负责通知各服务去获取最新的配置信息
  • 所有对外的请求和服务,我们都通过Zuul来进行转发,起到API网关的作用
  • 最后我们使用Sleuth+Zipkin将所有的请求数据记录下来,方便我们进行后续分析

为什么要使用springcloud

Spring Cloud从设计之初就考虑了绝大多数互联网公司架构演化所需的功能,如服务发现注册、配置中心、消息总线、负载均衡、断路器、数据监控等。这些功能都是以插拔的形式提供出来,方便我们系统架构演进的过程中,可以合理的选择需要的组件进行集成,从而在架构演进的过程中会更加平滑、顺利。

微服务架构是一种趋势,Spring Cloud提供了标准化的、全站式的技术方案,意义可能会堪比当前Servlet规范的诞生,有效推进服务端软件系统技术水平的进步。

Eureka服务与注册

为什么需要服务中心

过去,每个应用都是一个CPU,一个主机上的单一系统。然而今天,随着大数据和云计算时代的到来,任何独立的程序都可以运行在多个计算机上。并且随着业务的发展,访问用户量的增加,开发人员或小组的增加,系统会被拆分成多个功能模块。拆分后每个功能模块可以作为一个独立的子系统提供其职责范围内的功能。而多个子系统中,由于职责不同并且会存在相互调用,同时可能每个子系统还需要多个实例部署在多台服务器或者镜像中,导致了子系统间的相互调用形成了一个错综复杂的网状结构

对于微服务之间错综复杂的调用关系,通过eureka来管理,可以让每个服务之间不用关心如何调用的问题,专注于自己的业务功能实现。

Eureka的管理

  • 服务需要有一个统一的名称(或服务ID)并且是唯一标识,以便于接口调用时各个接口的区分。并且需要将其注册到Eureka Server中,其他服务调用该接口时,也是根据这个唯一标识来获取。
  • 服务下有多个实例,每个实例也有一个自己的唯一实例ID。因为它们各自有自己的基础信息如:不同的IP。所以它们的信息也需要注册到Eureka Server中,其他服务调用它们的服务接口时,可以查看到多个该服务的实例信息,根据负载策略提供某个实例的调用信息后,调用者根据信息直接调用该实例。

Eureka如何管理服务调用

  • 在Eureka Client启动的时候,将自身的服务的信息发送到Eureka Server。然后进行2调用当前服务器节点中的其他服务信息,保存到Eureka Client中。当服务间相互调用其它服务时,在Eureka Client中获取服务信息(如服务地址,端口等)后,进行第3步,根据信息直接调用服务。(注:服务的调用通过http(s)调用)
  • 当某个服务仅需要调用其他服务,自身不提供服务调用时。在Eureka Client启动后会拉取Eureka Server的其他服务信息,需要调用时,在Eureka Client的本地缓存中获取信息,调用服务。
  • Eureka Client通过向Eureka Serve发送心跳(默认每30秒)来续约服务的。 如果客户端持续不能续约,那么,它将在大约90秒内从服务器注册表中删除。 注册信息和续订被复制到集群中的Eureka Serve所有节点。 以此来确保当前服务还“活着”,可以被调用。
  • 来自任何区域的Eureka Client都可以查找注册表信息(每30秒发生一次),以此来确保调用到的服务是“活的”。并且当某个服务被更新或者新加进来,也可以调用到新的服务。

Eureka Server和Eureka Client

  • Eureka Server

    • 提供服务注册:各个微服务启动时,会通过Eureka Client向Eureka Server进行注册自己的信息(例如服务信息和网络信息),Eureka Server会存储该服务的信息。

    • 提供服务信息提供:服务消费者在调用服务时,本地Eureka Client没有的情况下,会到Eureka Server拉取信息。

    • 提供服务管理:通过Eureka Client的Cancel、心跳监控、renew等方式来维护该服务提供的信息以确保该服务可用以及服务的更新。

    • 信息同步:每个Eureka Server同时也是Eureka Client,多个Eureka Server之间通过P2P复制的方式完成服务注册表的同步

  • Eureka Client

    • Eureka Client是一个Java客户端,用于简化与Eureka Server的交互。并且管理当前微服务,同时为当前的微服务提供服务提供者信息。

    • Eureka Client会拉取、更新和缓存Eureka Server中的信息。即使所有的Eureka Server节点都宕掉,服务消费者依然可以使用缓存中的信息找到服务提供者。

    • Eureka Client在微服务启动后,会周期性地向Eureka Server发送心跳(默认周期为30秒)以续约自己的信息。如果Eureka Server在一定时间内没有接收到某个微服务节点的心跳,Eureka Server将会注销该微服务节点(默认90秒)。

服务续约、下线、剔除

  • 服务续约

    Application Service内的Eureka Client后台启动一个定时任务,跟Eureka Server保持一个心跳续约任务,每隔一段时间(默认30S)向Eureka Server发送一次renew请求,进行续约,告诉Eureka Server我还活着,防止被Eureka Server的Evict任务剔除。

  • 服务下线

    Application Service应用停止后,向Eureka Server发送一个cancel请求,告诉注册中心我已经退出了,Eureka Server接收到之后会将其移出注册列表,后面再有获取注册服务列表的时候就获取不到了,防止消费端消费不可用的服务。

  • 服务剔除

    Eureka Server启动后在后台启动一个Evict任务,对一定时间内没有续约的服务进行剔除。

  • 服务通讯方式

    服务间使用标准的REST方式通讯,所以Eureka服务注册中心并不仅适用于Java平台,其他平台也可以纳入到服务治理平台里面。

  • 自我保护

    本地调试Eureka的程序时,会出现: 

    该警告是触发了Eureka Server的自我保护机制。

    Eureka Server在运行期间,会统计心跳失败的比例在15分钟之内是否低于85%,如果低于,就会将当前实例注册信息保护起来,让实例不会过期,尽可能保护这些注册信息。

    但是如果在保护期间,实例出现问题,那么客户端很容易拿到实际已经不存在的服务实例,会出现调用失败。这个时候客户端的容错机制就很重要了。(重新请求,断路器)

    保护机制,可能会导致服务实例不能够被正确剔除。

    在本地开发时,可使用:eureka.server.enable-self-preservation=false关闭保护机制,使不可用实例能够正常下线。

Eureka和Zookeeper区别

Eureka:可以在发生因网络问题导致的各节点失去联系也不会暂停服务,但是最新的数据可能不统一。

Zookeeper:如果发生网络问题导致的Master和其他节点失去联系,就会使得其他的节点推选出新的Master,但是推选的时间内无法提供服务,但是可以保证任何时候的数据都是统一的。

基于idea多模块搭建eureka注册中心

  • 首先创建父工程parent-demo,父工程pom.xml配置如下:

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
        <modules>
            <module>eureka-demo</module>
            <module>user-demo</module>
            <module>order-demo</module>
            <module>zuul-demo</module>
            <module>config-server-demo</module>
            <module>config-client-demo</module>
        </modules>
        <parent>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>2.1.3.RELEASE</version>
            <relativePath/> <!-- lookup parent from repository -->
        </parent>
        <groupId>tech.aistar</groupId>
        <artifactId>parent-demo</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <name>parent-demo</name>
        <description>Demo project for Spring Boot</description>
    
        <properties>
            <java.version>1.8</java.version>
            <spring-cloud.version>Greenwich.SR2</spring-cloud.version>
        </properties>
    
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter</artifactId>
            </dependency>
    
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
                <scope>test</scope>
            </dependency>
        </dependencies>
    
        <dependencyManagement>
            <dependencies>
                <dependency>
                    <groupId>org.springframework.cloud</groupId>
                    <artifactId>spring-cloud-dependencies</artifactId>
                    <version>${spring-cloud.version}</version>
                    <type>pom</type>
                    <scope>import</scope>
                </dependency>
            </dependencies>
        </dependencyManagement>
    
        <build>
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                </plugin>
            </plugins>
        </build>
    </project>
    
  • 创建注册中心模块moudle

    eureka-demo模块,pom.xml文件如下:

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
        <parent>
            <groupId>tech.aistar</groupId>
            <artifactId>parent-demo</artifactId>
            <version>0.0.1-SNAPSHOT</version>
            <relativePath/> <!-- lookup parent from repository -->
        </parent>
        <groupId>tech.aistar</groupId>
        <artifactId>eureka-demo</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <name>eureka-demo</name>
        <description>Demo project for Spring Boot</description>
    
        <properties>
            <java.version>1.8</java.version>
        </properties>
    
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
            </dependency>
        </dependencies>
    </project>
    

    eureka-demo的application.yml文件配置如下:

    spring:
      application:
        name: eureka-demo
        
    server:
      port: 8888
    
    eureka:
      instance:
        # 注册到eurekaip地址
        hostname: localhost
      client:
        # 因为自己是注册中心,不需要自己注册自己
        register-with-eureka: false
        # 因为自己是注册中心,不需要检索服务
        fetch-registry: false
        service-url:
            # 服务注册中心的配置内容,指定服务注册中心的位置
            defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
            preferIpAddress: true
    

    添加启动类:

    @SpringBootApplication
    //开启注册中心
    @EnableEurekaServer
    public class EurekaDemoApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(EurekaDemoApplication.class, args);
        }
    }
    

    访问网页,查看EurekaServer

生产者和消费者注册及调用实战

生产者 - user-demo

  • 创建用户user-demo用户服务,pom.xml文件如下:

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    
  • user-demo的application.yml文件配置如下:

    spring:
      application:
        name: user-demo
    
    server:
      port: 8885
    
    eureka:
      instance:
        # 注册到eurekaip地址
        hostname: localhost
      client:
        register-with-eureka: true
        fetch-registry: true
        service-url:
            # 服务注册中心的配置内容,指定服务注册中心的位置
            defaultZone: http://localhost:8888/eureka/
            preferIpAddress: true
    
  • user-demo的启动类

    @SpringBootApplication
    //开启eureka的客户端注解
    @EnableEurekaClient
    @EnableDiscoveryClient
    public class UserDemoApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(UserDemoApplication.class, args);
        }
    }
    

消费者 - order-demo

  • 创建moudle - order-demo订单服务,pom.xml文件如下:

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    
  • order-demo的application.yml

    spring:
      application:
        name: order-demo
    
    server:
      port: 8882
    
    eureka:
      instance:
        # 注册到eurekaip地址
        hostname: localhost
      client:
        register-with-eureka: true
        fetch-registry: true
        service-url:
            # 服务注册中心的配置内容,指定服务注册中心的位置
            defaultZone: http://localhost:8888/eureka/
            preferIpAddress: true
    
  • order-demo的启动类

    @SpringBootApplication
    @EnableEurekaClient
    public class OrderDemoApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(OrderDemoApplication.class, args);
        }
    
    }
    

最后,一次启动服务注册中心eureka-demo,user-demo和order-demo

Feign介绍

Feign是一个声明式的伪Http客户端,它使得写Http客户端变得更简单。使用Feign,只需要创建一个接口并注解。Feign默认集成了Ribbon,并和Eureka结合,默认实现了负载均衡的效果

简而言之:

Feign 采用的是基于接口的注解

RestTemplate和feign区别

使用RestTemplate时,URL参数是以编程方式构造的,数据被发送到其他服务。

Feign是Spring Cloud Netflix库,用于在基于REST的服务调用上提供更高级别的抽象。Spring Cloud Feign在声明性原则上工作。使用Feign时,我们在客户端编写声明式REST服务接口,并使用这些接口来编写客户端程序。开发人员不用担心这个接口的实现。

实战

需求:在order-demo服务中调用user-demo服务的程序.

  • 在user-demo服务中添加controller - UserController.java

    @RestController
    public class UserController {
    
        @GetMapping("/user/{id}")
        public String get(@PathVariable("id") Integer id){
            if(id == 1){
                return "min";
            }else{
                return "驰星";
            }
        }
    }
    
  • 修改order-demo服务pom.xml文件,添加如下配置:

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
    
  • 修改order-demo服务,添加FeignService.java

    @FeignClient(value = "user-demo")
    public interface FeignService {
    
        @GetMapping("/user/{id}")
        public String get(@PathVariable("id") Integer id);
    }
    
  • 修改order-demo服务,添加controller - OrderController.java

    @RestController
    public class OrderController {
    
        @Autowired
        private FeignService feignService;
    
        @GetMapping("/order")
        public String getOrder(Integer id,String name){
            //调用user-demo的信息
            String result = feignService.get(id);
    
            return "商品名称:"+name+",生成订单:"+result;
        }
    
  • 修改order-demo的启动类

    @SpringBootApplication
    @EnableEurekaClient
    @EnableFeignClients
    public class OrderDemoApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(OrderDemoApplication.class, args);
        }
    
    }
    
  • 测试

    分别启动eureka-demo,user-demo,order-demo

Feign负载均衡效果测试

  • 启动eureka-demo

  • 启动user-demo

  • 再次启动第二个实例user-demo,在启动之前,修改user-demo的application.yml

    和UserController.java

    # 修改端口号
    server:
      port: 8886
    

    UserController.java

    为了体现俩个服务实例的区别,把”驰星”修改成”驰星1”

    @RestController
    public class UserController {
    
        @GetMapping("/user/{id}")
        public String get(@PathVariable("id") Integer id){
            if(id == 1){
                return "min";
            }else{
                return "驰星1";
            }
        }
    }
    
  • 启动order-demo

  • 测试1 - http://localhost:8882/order?id=2&name=tom

  • 测试2 - http://localhost:8882/order?id=2&name=tom

    采用”轮询”的方式进行调用!

  • 测试3 - 断掉其中一个user-demo的服务实例,再次输入上面的地址进行测试

Eureka会将服务端的服务信息缓存到本地测试

将eureka-demo服务停止,测试order-demo是否能够调用user-demo

结论:是可以正常调用的!

hystrix熔断器

为什么要使用熔断器?

在微服务架构中,根据业务来拆分成一个个的服务,服务与服务之间可以相互调用,在Spring Cloud可以用RestTemplate+Ribbon和Feign来调用。为了保证其高可用,单个服务通常会集群部署。由于网络原因或者自身的原因,服务并不能保证100%可用,如果单个服务出现问题,调用这个服务就会出现线程阻塞,此时若有大量的请求涌入,Servlet容器的线程资源会被消耗完毕,导致服务瘫痪。服务与服务之间的依赖性,故障会传播,会对整个微服务系统造成灾难性的严重后果,这就是服务故障的“雪崩”效应。

为了解决这个问题,业界提出了断路器模型。

熔断器简介

Netflix开源了Hystrix组件,实现了断路器模式,SpringCloud对这一组件进行了整合。 在微服务架构中,一个请求需要调用多个服务是非常常见的,如下图:

较底层的服务如果出现故障,会导致连锁故障。当对特定的服务的调用的不可用达到一个阀值(Hystric 是5秒20次) 断路器将会被打开。

断路打开后,可用避免连锁故障,fallback方法可以直接返回一个固定值。

hystrix特性

  • 请求熔断: 当Hystrix Command请求后端服务失败数量超过一定比例(默认50%), 断路器会切换到开路状态(Open). 这时所有请求会直接失败而不会发送到后端服务. 断路器保持在开路状态一段时间后(默认5秒), 自动切换到半开路状态(HALF-OPEN).

    这时会判断下一次请求的返回情况, 如果请求成功, 断路器切回闭路状态(CLOSED), 否则重新切换到开路状态(OPEN). Hystrix的断路器就像我们家庭电路中的保险丝, 一旦后端服务不可用, 断路器会直接切断请求链, 避免发送大量无效请求影响系统吞吐量, 并且断路器有自我检测并恢复的能力.

  • 服务降级:Fallback相当于是降级操作. 对于查询操作, 我们可以实现一个fallback方法, 当请求后端服务出现异常的时候, 可以使用fallback方法返回的值. fallback方法的返回值一般是设置的默认值或者来自缓存.告知后面的请求服务不可用了,不要再来了。

  • 依赖隔离(采用舱壁模式,Docker就是舱壁模式的一种):在Hystrix中, 主要通过线程池来实现资源隔离。通常在使用的时候我们会根据调用的远程服务划分出多个线程池。比如说,一个服务调用两外两个服务,你如果调用两个服务都用一个线程池,那么如果一个服务卡在哪里,资源没被释放,后面的请求又来了,导致后面的请求都卡在哪里等待,导致你依赖的A服务把你卡在哪里,耗尽了资源,也导致了你另外一个B服务也不可用了。这时如果依赖隔离,某一个服务调用A B两个服务,如果这时我有100个线程可用,我给A服务分配50个,给B服务分配50个,这样就算A服务挂了,我的B服务依然可以用。

  • 请求缓存:比如一个请求过来请求我userId=1的数据,你后面的请求也过来请求同样的数据,这时我不会继续走原来的那条请求链路了,而是把第一次请求缓存过了,把第一次的请求结果返回给后面的请求。

    请求缓存是在同一请求多次访问中保证只调用一次这个服务提供者的接口,在这同一次请求第一次的结果会被缓存,保证同一请求中同样的多次访问返回结果相同。

  • 请求合并:我依赖于某一个服务,我要调用N次,比如说查数据库的时候,我发了N条请求发了N条SQL然后拿到一堆结果,这时候我们可以把多个请求合并成一个请求,发送一个查询多条数据的SQL的请求,这样我们只需查询一次数据库,提升了效率。

hystrix流程结构解析

流程说明:

1:每次调用创建一个新的HystrixCommand,把依赖调用封装在run()方法中.
2:执行execute()/queue做同步或异步调用.
3:判断熔断器(circuit-breaker)是否打开,如果打开跳到步骤8,进行降级策略,如果关闭进入步骤.
4:判断线程池/队列/信号量是否跑满,如果跑满进入降级步骤8,否则继续后续步骤.
5:调用HystrixCommand的run方法.运行依赖逻辑
5a:依赖逻辑调用超时,进入步骤8.
6:判断逻辑是否调用成功
6a:返回成功调用结果
6b:调用出错,进入步骤8.
7:计算熔断器状态,所有的运行状态(成功, 失败, 拒绝,超时)上报给熔断器,用于统计从而判断熔断器状态.
8:getFallback()降级逻辑.
  以下四种情况将触发getFallback调用:
 (1):run()方法抛出非HystrixBadRequestException异常。
 (2):run()方法调用超时
 (3):熔断器开启拦截调用
 (4):线程池/队列/信号量是否跑满
8a:没有实现getFallback的Command将直接抛出异常
8b:fallback降级逻辑调用成功直接返回
8c:降级逻辑调用失败抛出异常
9:返回执行成功结果

服务降级实战

  • 修改order-demo的pom.xml,增加

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
    </dependency>
    
  • order-demo,启动类开启

    @EnableHystrix //启用熔断器
    
  • order-demo的application.yml开启hystrix功能

    # yml开启hystrix功能
    Feign:
      hystrix:
        enabled: true
    
  • order-demo服务中添加fallback类

    /**
     * @author success
     * @version 1.0
     * @description:本类用来演示:调用user服务的时候,如果出现错误,则调用此处的代码
     */
    @Component
    public class MyFallback implements FeignService{
        @Override
        public String get(Integer id) {
            return "error get(Integer id)";
        }
    }
    
  • 修改order-demo的FeignService.java

    在@FeignClient注解中添加fallback=MyFallback.class

    @FeignClient(value = "user-demo",fallback = MyFallback.class)
    public interface FeignService {
    
        @GetMapping("/user/{id}")
        public String get(@PathVariable("id") Integer id);
    }
    
  • 测试 - 将user-demo服务停止,然后输入

    http://localhost:8882/order?id=2&name=tom

    出现MyFallback类中的实现语句:

    商品名称:tom,生成订单:error get(Integer id)
    

依赖隔离

  • 添加OrderCommand.java

    package tech.aistar.service.pool;
    
    import com.netflix.hystrix.*;
    
    /**
     * @author success
     * @version 1.0
     * @description:本类用来演示:依赖隔离
     * @date 2019/8/26 0026
     */
    public class OrderCommand extends HystrixCommand<String>{
        private String value;
    
        public OrderCommand(String value) {
            super(Setter.withGroupKey(
                    //服务分组
                    HystrixCommandGroupKey.Factory.asKey("OrderGroup"))
                    //线程分组
                    .andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("OrderPool"))
                    //线程池配置
                    .andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties.Setter()
                            .withCoreSize(10)
                            .withKeepAliveTimeMinutes(5)
                            .withMaxQueueSize(10)
                            .withQueueSizeRejectionThreshold(10000))
                    .andCommandPropertiesDefaults(
                            HystrixCommandProperties.Setter()
                                    .withExecutionIsolationStrategy(HystrixCommandProperties.ExecutionIsolationStrategy.THREAD)));
            this.value = value;
        }
    
        @Override
        protected String run() throws Exception {
            String threadName = Thread.currentThread().getName();
            return threadName + " || " + value;
        }
    }
    
  • 添加UserCommand.java

    package tech.aistar.service.pool;
    
    import com.netflix.hystrix.*;
    
    /**
     * @author success
     * @version 1.0
     * @description:本类用来演示:
     * @date 2019/8/26 0026
     */
    public class UserCommand extends HystrixCommand<String>{
        private String value;
    
        public UserCommand(String value) {
            super(Setter.withGroupKey(
                    //服务分组
                    HystrixCommandGroupKey.Factory.asKey("UserGroup"))
                    //线程分组
                    .andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("UserPool"))
                    //线程池配置
                    .andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties.Setter()
                            .withCoreSize(10)
                            .withKeepAliveTimeMinutes(5)
                            .withMaxQueueSize(10)
                            .withQueueSizeRejectionThreshold(10000))
                    .andCommandPropertiesDefaults(
                            HystrixCommandProperties.Setter()
                                    .withExecutionIsolationStrategy(HystrixCommandProperties.ExecutionIsolationStrategy.THREAD)));
            this.value = value;
        }
    
        @Override
        protected String run() throws Exception {
            String threadName = Thread.currentThread().getName();
            return threadName + " || " + value;
        }
    }
    
  • 添加OrderService.java

    @Service
    public class OrderService {
    
        @Autowired
        private FeignService feignService;
    
        // 测试依赖隔离
        public String testPool() {
    
            UserCommand userCommand = new UserCommand("库里");
    
            OrderCommand orderCommand1 = new OrderCommand("篮球");
            OrderCommand orderCommand2 = new OrderCommand("足球");
    
            // 同步调用
            String val1 = userCommand.execute();
            String val2 = orderCommand1.execute();
            String val3 = orderCommand2.execute();
    
            // 异步调用
    //        Future<String> f1 = userCommand.queue();
    //        Future<String> f2 = userCommand.queue();
    //        Future<String> f3 = userCommand.queue();
    
              return "val1=" + val1 + "val2=" + val2 + "val3=" + val3;
          //  return "f1=" + f1.get() + "f2=" + f2.get() + "f3=" + f3.get();
        }
    }
    
  • order-demo控制层添加

    @GetMapping("/testpool")
    public String testPool(){
        return orderService.testPool();
    }
    
  • 测试:http://localhost:8882/testpool

    val1=hystrix-UserPool-1 || 库里val2=hystrix-OrderPool-1 || 篮球val3=hystrix-OrderPool-2 || 足球
    

请求合并

如图,多个客户端发送请求调用(消费者)项目中的findOne方法,这时候在这个项目中的线程池中会发申请与请求数量相同的线程数,对EurekaServiceProvider(服务提供者)的getUserById方法发起调用,每个线程都要调用一次,在高并发的场景下,这样势必会对服务提供者项目产生巨大的压力。

请求合并就是将单个请求合并成一个请求,去调用服务提供者,从而降低服务提供者负载的,一种应对高并发的解决办法

请求合并的原理

NetFlix在Hystrix为我们提供了应对高并发的解决方案—-请求合并,如下图

通过请求合并器设置延迟时间,将时间内的,多个请求单个的对象的方法中的参数(id)取出来,拼成符合服务提供者的多个对象返回接口(getUsersByIds方法)的参数,指定调用这个接口(getUsersByIds方法),返回的对象List再通过一个方法(mapResponseToRequests方法),按照请求的次序将结果对象对应的装到Request对应的Response中返回结果。

请求合并适用场景

在服务提供者提供了返回单个对象和多个对象的查询接口,并且单个对象的查询并发数很高,服务提供者负载较高的时候,我们就可以使用请求合并来降低服务提供者的负载

请求合并带来的问题

  1. 我们为这个请求人为的设置了延迟时间,这样在并发不高的接口上使用请求缓存,会降低响应速度
  2. 实现请求合并比较复杂

使用注解配置

  • OrderService.java中添加

     /**
         * 演示请求合并
         * @param id
         * @return
         */
        @HystrixCollapser(batchMethod = "findAll",collapserProperties = {
                @HystrixProperty(name = "timerDelayInMilliseconds",value = "300")
        })
        public Future<String> findOne(Integer id){
            System.out.println("被合并的请求!");
            return null;
        }
    
        @HystrixCommand
        public List<String> findAll(List<Integer> ids){
            System.out.println("合并的请求");
            List<String> results = new ArrayList<>();
            for(Integer id:ids){
                results.add(feignService.get(id));
            }
            return results;
        }
    
  • OrderController.java中添加

    /**
         * 请求合并
         * @return
         */
    @GetMapping("/getMerge")
    public String getMerge() throws ExecutionException, InterruptedException {
        HystrixRequestContext context = HystrixRequestContext.initializeContext();
    
        //必须是异步
        Future<String> r1 = orderService.findOne(1);
        Future<String> r2 = orderService.findOne(2);
        // Thread.sleep(1000);
        Future<String> r3 = orderService.findOne(1);
    
        //context.close();
    
        return "u1:"+r1.get()+",u2:"+r2.get()+"r3:"+r3.get();
    }
    
  • 测试

    http://localhost:8882/getMerge
    

    观察控制台 - order-demo控制台只会出现一次”合并的请求”

    合并的请求
    

请求缓存

  • OrderService.java

     /**
         * 请求缓存,如果俩次请求的cacheKey是一样的,则此处的代码仅仅会执行一次
         * 此处是采用注解的配置方式
         * @param id
         * @param cacheKey
         * @return
         */
        @CacheResult
        @HystrixCommand(commandKey = "cache-user")
        public String getUser(Integer id,@CacheKey Long cacheKey){
            System.out.println("请求缓存,如果cacheKey是一样的,则不会再执行!");
            return feignService.get(id);
        }
    
  • 控制层

     /**
         * 请求缓存
         * @return
         */
        @GetMapping("/getCache")
        public String getCache(){
            HystrixRequestContext context = HystrixRequestContext.initializeContext();
    
            Long key = 9999L;
            String u1 = orderService.getUser(1,key);
            String u2 = orderService.getUser(2,9998L);
    
            context.close();
    
            return "u1:"+u1+",u2:"+u2;
        }
    

路由网关zuul

为什么需要服务网关

在分布式系统系统中,有商品、订单、用户、广告、支付等等一大批的服务,前端怎么调用呢?和每个服务一个个打交道?这显然是不可能的,这就需要有一个角色充当所有请求的入口,这个角色就是服务网关(API gateway)

客户端直接与微服务通讯的问题

  1. 客户端会多次请求不同的微服务,增加了客户端的复杂性。
  2. 存在跨域请求,在一定场景下处理相对复杂。
  3. 认证复杂,每个服务都需要独立认证。
  4. 难以重构,随着项目的迭代,可能需要重新划分微服务。例如,可能将多个服务合并成一个或者将一个服务拆分成多个。如果客户端直接与微服务通讯,那么重构将会很难实施。

网关的优点

  1. 易于监控。可在微服务网关收集监控数据并将其推送到外部系统进行分析。
  2. 易于认证。可在微服务网关上进行认证。然后再将请求转发到后端的微服务,而无须在每个微服务中进行认证。
  3. 减少了客户端与各个微服务之间的交互次数。

什么是网关?

服务网关是微服务架构中一个不可或缺的部分。通过服务网关统一向外系统提供REST API的过程中,除了具备服务路由、均衡负载功能之外,它还具备了权限控制等功能。Spring Cloud Netflix 中的 Zuul 就担任了这样的一个角色,为微服务架构提供了前门保护的作用,同时将权限控制这些较重的非业务逻辑内容迁移到服务路由层面,使得服务集群主体能够具备更高的可复用性和可测试性。

使用zuul

  • 新建zuul-demo模块

  • pom.xml文件

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
            </dependency>
    
  • application.xml

    spring:
      application:
        name: zuul-demo
    
    server:
      port: 9000
    
    eureka:
      instance:
        hostname: localhost
      client:
        register-with-eureka: true
        fetch-registry: true
        service-url:
            defaultZone: http://localhost:8888/eureka/
            preferIpAddress: true
    zuul:
      routes:
        # 配置统一前缀访问
        api-order:
          path: /api-order/**
          serviceId: order-demo
        api-user:
          path: /api-user/**
          serviceId: user-demo
    
  • 启动类

    @SpringBootApplication
    @EnableEurekaClient
    @EnableDiscoveryClient
    // 开启zuul功能
    @EnableZuulProxy
    public class ZuulDemoApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(ZuulDemoApplication.class, args);
        }
    }
    
  • 测试路由访问

    # 测试1:http://localhost:9000/api-order/testpool
    # 测试2:http://localhost:9000/api-user/user/1
    # 测试3:http://localhost:9000/user-demo/user/1
    # 测试4:http://localhost:9000/order-demo/testpool
    

    都是允许访问的

  • 配置统一前缀访问

    zuul:
      routes:
        # 配置统一前缀访问
        api-order:
          path: /api-order/**
          serviceId: order-demo
        api-user:
          path: /api-user/**
          serviceId: user-demo
        #前缀访问
          prefix: /parent
    

    测试路由访问:

    # http://localhost:9000/parent/api-order/testpool
    # http://localhost:9000/parent/order-demo/testpool
    
  • 忽略服务名serviceId访问

    zuul:
      routes:
        # 配置统一前缀访问
        api-order:
          path: /api-order/**
          serviceId: order-demo
        api-user:
          path: /api-user/**
          serviceId: user-demo
        # 前缀访问
      prefix: /parent
        # 忽略服务名serviceId访问
        ignored-services: "*"
    

    测试路由访问

    #测试 http://localhost:9000/parent/api-order/testpool ->ok
    #测试http//localhost:9000/parent/order-demo/testpool ->error
    
  • 配url绑定映射

    zuul:
      routes:
        testurl:
          # url: http://www.iduoan.com
          url: http://localhost:8885/
          path: /testurl/**
    

    测试路由访问:

    # 测试 http://localhost:9000/testurl/user/1
    
  • 配置URL映射负载

    ribbon:
      eureka:
        enabled: false
    
    #Ribbon请求的微服务serviceId
    success-user:
      ribbon:
        listOfServers: http://www.huya.com,http://www.douyu.com
    
    zuul:
      routes:
        testurl:
          serviceId: success-user
          path: /testurl/**
    

zuul过滤器

Zuul本身是一系列过滤器的集成,那么他当然也就提供了自定义过滤器的功能,zuul提供了四种过滤器:前置过滤器,路由过滤器,错误过滤器,简单过滤器,实现起来也十分简单,只需要编写一个类去实现zuul提供的接口。

添加过滤器类

package tech.aistar.filter;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.exception.ZuulException;
import com.sun.scenario.effect.FilterContext;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.stereotype.Component;

/**
 * @author success
 * @version 1.0
 * @description:本类用来演示:
 * Zuul本身是一系列过滤器的集成,那么他当然也就提供了自定义过滤器的功能,
 * zuul提供了四种过滤器:前置过滤器,路由过滤器,错误过滤器,
 * 简单过滤器,实现起来也十分简单,只需要编写一个类去实现zuul提供的接口。
 */
@Component
public class MyFilter01 extends ZuulFilter{
    /**
     * 类型包含 pre post route error
     * pre 代表在路由代理之前执行
     * route 代表代理的时候执行
     * error 代表出现错的时候执行
     * post 代表在route 或者是 error 执行完成后执行
     */
    @Override
    public String filterType() {
        return FilterConstants.PRE_TYPE;
    }

    @Override
    public int filterOrder() {
        // 优先级,数字越大,优先级越低
        return 1;
    }

    @Override
    public boolean shouldFilter() {

        // 是否执行该过滤器,true代表需要过滤
        return true;
    }

    @Override
    public Object run() throws ZuulException {
        System.out.println("1111111111111111111");
        return null;
    }
}

高可用分布式配置中心

在分布式系统中,由于服务数量巨多,为了方便服务配置文件统一管理,所以需要分布式配置中心组件。在Spring Cloud中,有分布式配置中心组件spring cloud config ,它支持配置服务放在配置服务的内存中(即本地),也支持放在远程Git仓库中。在spring cloud config 组件中,分两个角色,一是config server,二是config client。

没有使用统一配置中心时,所存在的问题

  • 配置文件分散在各个项目里,不方便维护
  • 配置内容安全与权限,实际开发中,开发人员是不知道线上环境的配置的
  • 更新配置后,项目需要重启

有哪些开源配置中心

spring-cloud/spring-cloud-config
https://github.com/spring-cloud/spring-cloud-config
spring出品,可以和spring cloud无缝配合
diamond
https://github.com/takeseem/diamond
disconf
https://github.com/knightliao/disconf
ctrip apollo
https://github.com/ctripcorp/apollo/
Apollo(阿波罗)是携程框架部门研发的开源配置管理中心,具备规范的权限、流程治理等特性。

快速入门config-server

  • 创建config-server-demo工程

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-config-server</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    
  • 启动类

    @SpringBootApplication
    @EnableConfigServer
    public class ConfigServerDemoApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(ConfigServerDemoApplication.class, args);
        }
    }
    
  • 配置文件

    server:
      port: 9100
    spring:
      application:
        name: config-server-demo
      cloud:
        config:
          server:
            git:
              uri: https://gitee.com/guancg/config-server-demo.git
    
  • 创建码云仓库

    https://gitee.com/guancg/config-server-demo.git

  • 创建测试的配置文件

    • 文件名-环境名.后缀

  • config支持我们使用的请求的参数规则为:

快速入门config-client

  • 创建config-client-demo模块

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-config</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    
  • 启动类

    @SpringBootApplication
    public class ConfigClientDemoApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(ConfigClientDemoApplication.class, args);
        }
    }
    
  • 测试controller

    @RestController
    //刷新消息组件
    @RefreshScope
    public class ConfigClientController {
    
        @Value("${name}")
        private String name;
    
        @GetMapping("/value")
        public String getName(){
            return name;
        }
    }
    
  • bootstrap.yml

    server:
      port: 9201
    spring:
      application:
        name: config-client-demo           # 指定了配置文件的应用名
      cloud:
        config:
          uri: http://localhost:9100/      #Config server\的uri
          profile: dev                       #指定的环境
          label: master                      #指定的分支    
    

    http://localhost:9201/value

Config+Bus : 实现动态刷新

  • config-client-demo模块添加依赖

    <!--消息总件组件-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-bus-amqp</artifactId>
    </dependency>
    
  • 添加注解

    //刷新消息组件
    @RefreshScope
    public class ConfigClientController {
    }
    
  • 启动rabbitmq消息队列

  • 修改配置文件

    server:
      port: 9201
    spring:
      application:
        name: config-client-demo           # 指定了配置文件的应用名
      cloud:
        config:
          uri: http://localhost:9100/      #Config server\的uri
          profile: dev                       #指定的环境
          label: master                      #指定的分支
      rabbitmq:
          host: 192.168.2.49
          port: 5672
          username: guest
          password: guest
    management: # 暴露总线消息地址`
      endpoints:
        web:
          exposure:
            include: "bus-refresh"
          cors:
            allowed-origins: "*"
            allowed-methods: "*"
      #测试 - http://localhost:9201/actuator/bus-refresh
    
  • 本次测试地址:POST请求

    http://localhost:9201/actuator/bus-refresh
    

文章作者: 码农耕地人
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 码农耕地人 !
  目录