SpringCloud

SpringCloud

1. 微服务基础知识

1. 系统架构的演变

随着互联网的发展,网站应用的规模不断扩大,常规的应用架构已无法应对,分布式服务架构以及微服务架构势在必行,亟需一个治理系统确保架构有条不紊的演进。

1. 单体应用架构

Web应用程序发展的早期,大部分web工程(包含前端页面,web层代码,service层代码,dao层代码)是将所有的功能模块,打包到一起并放在一个web容器中运行。
在这里插入图片描述

比如搭建一个电商系统:客户下订单,商品展示,用户管理。这种将所有功能都部署在一个web容器中运行的系统就叫做单体架构。

  • 优点:
    1. 所有的功能集成在一个项目工程中
    2. 项目架构简单,前期开发成本低,周期短,小型项目的首选。
  • 缺点:
    1. 全部功能集成在一个工程中,对于大型项目不易开发、扩展及维护。
    2. 系统性能扩展只能通过扩展集群结点,成本高、有瓶颈。
    3. 技术栈受限。
2. 垂直应用架构

当访问量逐渐增大,单一应用增加机器带来的加速度越来越小,将应用拆成互不相干的几个应用,以提升效率
在这里插入图片描述

  • 优点:
    1. 项目架构简单,前期开发成本低,周期短,小型项目的首选。
    2. 通过垂直拆分,原来的单体项目不至于无限扩大
    3. 不同的项目可采用不同的技术。
  • 缺点:
    1. 全部功能集成在一个工程中,对于大型项目不易开发、扩展及维护。
    2. 系统性能扩展只能通过扩展集群结点,成本高、有瓶颈。
3. 分布式SOA架构
1. 什么是SOA

SOA 全称为 Service-Oriented Architecture,即面向服务的架构。它可以根据需求通过网络对松散耦合的粗粒度应用组件(服务)进行分布式部署、组合和使用。一个服务通常以独立的形式存在于操作系统进程中。

站在功能的角度,把业务逻辑抽象成可复用、可组装的服务,通过服务的编排实现业务的快速再生,目的:把原先固有的业务功能转变为通用的业务服务,实现业务逻辑的快速复用。

通过上面的描述可以发现 SOA 有如下几个特点:分布式、可重用、扩展灵活、松耦合

2. SOA架构

当垂直应用越来越多,应用之间交互不可避免,将核心业务抽取出来,作为独立的服务,逐渐形成稳定的服务中心,使前端应用能更快速的响应多变的市场需求
在这里插入图片描述

  • 优点:
    1. 抽取公共的功能为服务,提高开发效率
    2. 对不同的服务进行集群化部署解决系统压力
    3. 基于ESB/DUBBO减少系统耦合
  • 缺点:
    1. 抽取服务的粒度较大
    2. 服务提供方与调用方接口耦合度较高
4. 微服务架构

在这里插入图片描述

  • 优点:
    1. 通过服务的原子化拆分,以及微服务的独立打包、部署和升级,小团队的交付周期将缩短,运维成
      本也将大幅度下降
    2. 微服务遵循单一原则。微服务之间采用Restful等轻量协议传输。
  • 缺点:
    1. 微服务过多,服务治理成本高,不利于系统维护。
    2. 分布式系统开发的技术成本高(容错、分布式事务等)。
5. SOA与微服务的关系

SOA( Service Oriented Architecture )面向服务的架构:他是一种设计方法,其中包含多个服务, 服务之间通过相互依赖最终提供一系列的功能。一个服务 通常以独立的形式存在与操作系统进程中。各个服务之间 通过网络调用。

微服务架构:其实和 SOA 架构类似,微服务是在 SOA 上做的升华,微服务架构强调的一个重点是“业务需要彻底的组件化和服务化”,原有的单个业务系统会拆分为多个可以独立开发、设计、运行的小应用。这些小应用之间通过服务完成交互和集成。
在这里插入图片描述

2. 分布式核心知识

1. 分布式中的远程调用

在微服务架构中,通常存在多个服务之间的远程调用的需求。远程调用通常包含两个部分:序列化和通信协议。常见的序列化协议包括json、xml、hession、protobuf、thrift、text、bytes等,目前主流的远程调用技术有基于HTTP的RESTful接口以及基于TCP的RPC协议。
在这里插入图片描述

1. RESTful接口

REST,即Representational State Transfer的缩写,如果一个架构符合REST原则,就称它为RESTful架构。

1.资源(Resources)

所谓"资源",就是网络上的一个实体,或者说是网络上的一个具体信息。它可以是一段文本、一张图片、一首歌曲、一种服务,总之就是一个具体的实在。你可以用一个URI(统一资源定位符)指向它,每种资源对应一个特定的URI。要获取这个资源,访问它的URI就可以,因此URI就成了每一个资源的地址或独一无二的识别符。REST的名称"表现层状态转化"中,省略了主语。“表现层"其实指的是"资源”(Resources)的"表现层"。

2. 表现层(Representation)

“资源"是一种信息实体,它可以有多种外在表现形式。我们把"资源"具体呈现出来的形式,叫做它的"表现层”(Representation)。比如,文本可以用txt格式表现,也可以用HTML格式、XML格式、JSON格式表现,甚至可以采用二进制格式;图片可以用JPG格式表现,也可以用PNG格式表现。URI只代表资源的实体,不代表它的形式。严格地说,有些网址最后的".html"后缀名是不必要的,因为这个后缀名表示格式,属于"表现层"范畴,而URI应该只代表"资源"的位置。

3. 状态转化(State Transfer)

访问一个网站,就代表了客户端和服务器的一个互动过程。在这个过程中,势必涉及到数据和状态的变化。互联网通信协议HTTP协议,是一个无状态协议。这意味着,所有的状态都保存在服务器端。因此,如果客户端想要操作服务器,必须通过某种手段,让服务器端发生"状态转化"(State Transfer)。客户端用到的手段,只能是HTTP协议。具体来说,就是HTTP协议里面,四个表示操作方式的动词:GET、POST、PUT、DELETE。它们分别对应四种基本操作:GET用来获取资源,POST用来新建资源(也可以用于更新资源),PUT用来更新资源,DELETE用来删除资源。

4. 总结什么是RESTful架构
  • 每一个URI代表一种资源;
  • 客户端和服务器之间,传递这种资源的某种表现层;
  • 客户端通过四个HTTP动词,对服务器端资源进行操作,实现"表现层状态转化"。
2. RPC协议

RPC(Remote Procedure Call ) 一种进程间通信方式。允许像调用本地服务一样调用远程服务。RPC框架的主要目标就是让远程服务调用更简单、透明。RPC框架负责屏蔽底层的传输方式(TCP或者UDP)、序列化方式(XML/JSON/二进制)和通信细节。开发人员在使用的时候只需要了解谁在什么位置提供了什么样的远程服务接口即可,并不需要关心底层通信细节和调用过程。
在这里插入图片描述

3. 区别与联系

在这里插入图片描述

  1. HTTP相对更规范,更标准,更通用,无论哪种语言都支持http协议。如果你是对外开放API,例如开放平台,外部的编程语言多种多样,你无法拒绝对每种语言的支持,现在开源中间件,基本最先支持的几个协议都包含RESTful。
  2. RPC 框架作为架构微服务化的基础组件,它能大大降低架构微服务化的成本,提高调用方与服务提供方的研发效率,屏蔽跨进程调用函数(服务)的各类复杂细节。让调用方感觉就像调用本地函数一样调用远端函数、让服务提供方感觉就像实现一个本地函数一样来实现服务。
2. 分布式中的CAP原理

现如今,对于多数大型互联网应用,分布式系统(distributed system)正变得越来越重要。分布式系统的最大难点,就是各个节点的状态如何同步。CAP 定理是这方面的基本定理,也是理解分布式系统的起点。

CAP理论由 Eric Brewer 在ACM研讨会上提出,而后CAP被奉为分布式领域的重要理论。分布式系统的CAP理论,首先把分布式系统中的三个特性进行了如下归纳:
在这里插入图片描述

Consistency(一致性):数据一致更新,所有数据的变化都是同步的

Availability(可用性):在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求

Partition tolerance(分区容忍性):某个节点的故障,并不影响整个系统的运行

任何分布式系统只可同时满足二点,没法三者兼顾,既然一个分布式系统无法同时满足一致性、可用性、分区容错性三个特点,所以我们就需要抛弃一样:
在这里插入图片描述

在这里插入图片描述

需要明确一点的是,在一个分布式系统当中,分区容忍性和可用性是最基本的需求,所以在分布是系统中,我们的系统最当关注的就是A(可用性)P(容忍性),通过补偿的机制寻求数据的一致性

3. 常见微服务框架

1. SpringCloud

Spring Cloud是一系列框架的有序集合。它利用Spring Boot的开发便利性巧妙地简化了分布式系统基础设施的开发,如服务发现注册、配置中心、消息总线、负载均衡、断路器、数据监控等,都可以用Spring Boot的开发风格做到一键启动和部署。Spring Cloud并没有重复制造轮子,它只是将目前各家公司开发的比较成熟、经得起实际考验的服务框架组合起来,通过Spring Boot风格进行再封装屏蔽掉了复杂的配置和实现原理,最终给开发者留出了一套简单易懂、易部署和易维护的分布式系统开发工具包。

2. ServiceComb

Apache ServiceComb 是业界第一个Apache微服务顶级项目, 是一个开源微服务解决方案,致力于帮助企业、用户和开发者将企业应用轻松微服务化上云,并实现对微服务应用的高效运维管理。其提供一站式开源微服务解决方案,融合SDK框架级、0侵入ServiceMesh场景并支持多语言。

3. ZeroC ICE

ZeroC IceGrid 是ZeroC公司的杰作,继承了CORBA的血统,是新一代的面向对象的分布式系统中间件。作为一种微服务架构,它基于RPC框架发展而来,具有良好的性能与分布式能力。

2. SpringCloud概述

1. 微服务中的相关概念

1. 服务注册与发现

服务注册:服务实例将自身服务信息注册到注册中心。这部分服务信息包括服务所在主机IP和提供服务的Port,以及暴露服务自身状态以及访问协议等信息。

服务发现:服务实例请求注册中心获取所依赖服务信息。服务实例通过注册中心,获取到注册到其中的服务实例的信息,通过这些信息去请求它们提供的服务。
在这里插入图片描述

2. 负载均衡

负载均衡是高可用网络基础架构的关键组件,通常用于将工作负载分布到多个服务器来提高网站、应用、数据库或其他服务的性能和可靠性。
在这里插入图片描述

3. 熔断

熔断这一概念来源于电子工程中的断路器(Circuit Breaker)。在互联网系统中,当下游服务因访问压力过大而响应变慢或失败,上游服务为了保护系统整体的可用性,可以暂时切断对下游服务的调用。这种牺牲局部,保全整体的措施就叫做熔断。
在这里插入图片描述

4. 链路追踪

随着微服务架构的流行,服务按照不同的维度进行拆分,一次请求往往需要涉及到多个服务。互联网应用构建在不同的软件模块集上,这些软件模块,有可能是由不同的团队开发、可能使用不同的编程语言来实现、有可能布在了几千台服务器,横跨多个不同的数据中心。因此,就需要对一次请求涉及的多个服务链路进行日志记录,性能监控即链路追踪
在这里插入图片描述

5. API网关

随着微服务的不断增多,不同的微服务一般会有不同的网络地址,而外部客户端可能需要调用多个服务的接口才能完成一个业务需求,如果让客户端直接与各个微服务通信可能出现:

  • 客户端需要调用不同的url地址,增加难度
  • 再一定的场景下,存在跨域请求的问题
  • 每个微服务都需要进行单独的身份认证

API网关字面意思是将所有API调用统一接入到API网关层,由网关层统一接入和输出。一个网关的基本功能有:统一接入、安全防护、协议适配、流量管控、长短链接支持、容错能力。有了网关之后,各个API服务提供团队可以专注于自己的的业务逻辑处理,而API网关更专注于安全、流量、路由等问题。
在这里插入图片描述

2. SpringCloud的介绍

Spring Cloud是一系列框架的有序集合。它利用Spring Boot的开发便利性巧妙地简化了分布式系统基础设施的开发,如服务发现注册、配置中心、消息总线、负载均衡、断路器、数据监控等,都可以用Spring Boot的开发风格做到一键启动和部署。Spring Cloud并没有重复制造轮子,它只是将目前各家公司开发的比较成熟、经得起实际考验的服务框架组合起来,通过Spring Boot风格进行再封装屏蔽掉了复杂的配置和实现原理,最终给开发者留出了一套简单易懂、易部署和易维护的分布式系统开发工具包。

3. SpringCloud的架构

1. SpringCloud中的核心组件

Spring Cloud的本质是在 Spring Boot 的基础上,增加了一堆微服务相关的规范,并对应用上下文(Application Context)进行了功能增强。既然 Spring Cloud 是规范,那么就需要去实现,目前Spring Cloud 规范已有 Spring官方,Spring Cloud Netflflix,Spring Cloud Alibaba等实现。通过组件化的方式,Spring Cloud将这些实现整合到一起构成全家桶式的微服务技术栈。

Spring Cloud Netflflix组件
在这里插入图片描述

Spring Cloud Alibaba组件
在这里插入图片描述

Spring Cloud原生及其他组件
在这里插入图片描述

2. SpringCloud的体系结构

在这里插入图片描述

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

  • 注册中心负责服务的注册与发现,很好将各服务连接起来
  • 断路器负责监控服务之间的调用情况,连续多次失败进行熔断保护。
  • API网关负责转发所有对外的请求和服务**
  • 配置中心提供了统一的配置信息管理服务,可以实时的通知各个服务获取最新的配置信息链路追踪技术可以将所有的请求数据记录下来,方便我们进行后续分析
  • 各个组件又提供了功能完善的dashboard监控平台,可以方便的监控各组件的运行状况

3. 服务调用

1. RestTemplate介绍

Spring框架提供的RestTemplate类可用于在应用中调用rest服务,它简化了与http服务的通信方式,统一了RESTful的标准,封装了http链接, 我们只需要传入url及返回值类型即可。相较于之前常用的HttpClient,RestTemplate是一种更优雅的调用RESTful服务的方式。

在Spring应用程序中访问第三方REST服务与使用Spring RestTemplate类有关。RestTemplate类的设计原则与许多其他Spring 模板类(例如JdbcTemplate、JmsTemplate)相同,为执行复杂任务提供了一种具有默认行为的简化方法。

RestTemplate默认依赖JDK提供http连接的能力(HttpURLConnection),如果有需要的话也可以通过setRequestFactory方法替换为例如 Apache HttpComponents、Netty或OkHttp等其它HTTP library。

考虑到RestTemplate类是为调用REST服务而设计的,因此它的主要方法与REST的基础紧密相连就不足为奇了,后者是HTTP协议的方法:HEAD、GET、POST、PUT、DELETE和OPTIONS。例如,RestTemplate类具有headForHeaders()、getForObject()、postForObject()、put()和delete()等方法。

2. RestTemplate方法介绍

在这里插入图片描述

3. 通过RestTemplate调用微服务

  1. 在工程启动类中配置RestTemplate
//配置RestTemplate交给spring管理
 @Bean
 public RestTemplate getRestTemplate() {
     return new RestTemplate();
 }
  1. 编写方法
@PostMapping("/{id}")
 public String order(Integer num) {
    //通过restTemplate调用商品微服务
    Product object = restTemplate.getForObject("http://127.0.0.1:9002/product/1",                           Product.class);
    System.out.println(object);
    return "操作成功";
 }

4. 硬编码存在的问题

至此已经可以通过RestTemplate调用商品微服务的RESTFul API接口。但是我们把提供者的网络地址(ip,端口)等硬编码到了代码中,这种做法存在许多问题:

  • 应用场景有局限
  • 无法动态调整

4. 服务注册Eureka基础

1. 微服务的注册中心

注册中心可以说是微服务架构中的”通讯录“,它记录了服务和服务地址的映射关系。在分布式架构中,服务会注册到这里,当服务需要调用其它服务时,就这里找到服务的地址,进行调用。
在这里插入图片描述

1. 注册中心的主要作用

服务注册中心(下称注册中心)是微服务架构非常重要的一个组件,在微服务架构里主要起到了协调者的一个作用。注册中心一般包含如下几个功能:

  1. 服务发现:
  • 服务注册/反注册:保存服务提供者和服务调用者的信息

  • 服务订阅/取消订阅:服务调用者订阅服务提供者的信息,最好有实时推送的功能

  • 服务路由(可选):具有筛选整合服务提供者的能力。

  1. 服务配置:
  • 配置订阅:服务提供者和服务调用者订阅微服务相关的配置

  • 配置下发:主动将配置推送给服务提供者和服务调用者

  1. 服务健康检测
  • 检测服务提供者的健康情况
2. 常见的注册中心

Zookeeper

zookeeper它是一个分布式服务框架,是Apache Hadoop 的一个子项目,它主要是用来解决分布式应用中经常遇到的一些数据管理问题,如:统一命名服务、状态同步服务、集群管理、分布式应用配置项的管理等。简单来说zookeeper=文件系统+监听通知机制。

Eureka

Eureka是在Java语言上,基于Restful Api开发的服务注册与发现组件,Springcloud Netflflix中的重要组件

Consul

Consul是由HashiCorp基于Go语言开发的支持多数据中心分布式高可用的服务发布和注册服务软件,采用Raft算法保证服务的一致性,且支持健康检查。

Nacos

Nacos是一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。简单来说 Nacos 就是注册中心 + 配置中心的组合,提供简单易用的特性集,帮助我们解决微服务开发必会涉及到的服务注册与发现,服务配置,服务管理等问题。Nacos 还是 Spring Cloud Alibaba 组件之一,负责服务注册与发现
在这里插入图片描述

2. Eureka的概述

1. Eureka的基础知识

Eureka是Netflflix开发的服务发现框架,SpringCloud将它集成在自己的子项目spring-cloud-netflflix中,实现SpringCloud的服务发现功能。
在这里插入图片描述

上图简要描述了Eureka的基本架构,由3个角色组成:

1、Eureka Server

  • 提供服务注册和发现

2、Service Provider

  • 服务提供方

  • 将自身服务注册到Eureka,从而使服务消费方能够找到

3、Service Consumer

  • 服务消费方

  • 从Eureka获取注册服务列表,从而能够消费服务

2. Eureka的交互流程与原理

在这里插入图片描述

  • Application Service 相当于本书中的服务提供者,Application Client相当于服务消费者;
  • Make Remote Call,可以简单理解为调用RESTful API;
  • us-east-1c、us-east-1d等都是zone,它们都属于us-east-1这个region;

由图可知,Eureka包含两个组件:Eureka Server和Eureka Client,它们的作用如下:

  • Eureka Client是一个Java客户端,用于简化与Eureka Server的交互;

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

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

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

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

综上,Eureka通过心跳检测、健康检查和客户端缓存等机制,提高了系统的灵活性、可伸缩性和可用性。

3. 搭建Eureka注册中心

1. 搭建Eureka服务中心
  1. 创建模块

  2. 引入maven坐标

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
    </dependency>
    
  3. 配置application.yml

spring:
  application:
    name: eureka-server
server:
  port: 9000 #端口
#配置eureka server
eureka:
  client:
    register-with-eureka: false #是否将自己注册到注册中心
    fetch-registry: false #是否从eureka中获取注册信息
    service-url: #配置暴露给Eureka Client的请求地址
      defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
  1. 配置启动类
@SpringBootApplication
@EnableEurekaServer  //激活eurekaserver
public class EurekaServerApplication {
   public static void main(String[] args) {
      SpringApplication.run(EurekaServerApplication.class, args);
   }
}
2. 服务注册中心管理后台

打开浏览器访问http://localhost8761即可进入EurekaServer内置的管理控制台,显示效果如下
在这里插入图片描述

4. 服务注册到Eureka注册中心

  1. 引入EurekaClient 坐标
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
  1. 修改application.yml添加EurekaServer的信息
#配置Eureka
eureka:
  client: 
    service-url:  # eureka server的路径
      defaultZone: http://localhost:9000/eureka/
  instance:
    prefer-ip-address: true #使用ip地址注册
  1. 修改启动类添加服务注册注解
@SpringBootApplication
//@EnableDiscoveryClient
//@EnableEurekaClient
public class UserApplication {
   public static void main(String[] args) {
     SpringApplication.run(UserApplication.class, args);
   }
}

从Spring Cloud Edgware版本开始, @EnableDiscoveryClient 或 @EnableEurekaClient 可省略。只需加上相关依赖,并进行相应配置,即可将微服务注册到服务发现组件上。

  1. 服务消费者通过注册中心获取服务列表,并调用

Eureka中的元数据:服务的主机名、ip、等信息,可以通过eurekaserver进行获取,用于服务之间的调用

//注入restTemplate对象
@Autowired
private RestTemplate restTemplate;
 
/**
 * 注入DiscoveryClient :
 *  springcloud提供的获取原数组的工具类
 *      调用方法获取服务的元数据信息
 *
 */
@Autowired
private DiscoveryClient discoveryClient;
 
/**
 * 参数:商品id
 *  通过订单系统,调用商品服务根据id查询商品信息
 *      1.需要配置商品对象
 *      2.需要调用商品服务
 *  使用java中的urlconnection,httpclient,okhttp
 */
@RequestMapping(value = "/buy/{id}",method = RequestMethod.GET)
public Product findById(@PathVariable Long id) {
   // 调用discoveryClient方法
   //已调用服务名称获取所有的元数据
   List<ServiceInstance> instances = discoveryClient.getInstances("service-product");
   //获取唯一的一个元数据
   ServiceInstance instance = instances.get(0);
   //根据元数据中的主机地址和端口号拼接请求微服务的URL
   Product product = null;
   //如何调用商品服务?
   product = restTemplate.getForObject("http://"+instance.getHost()+":"+instance.getPort()+"/product/1",Product.class);
   return product;
}

5. Eureka中的自我保护

微服务第一次注册成功之后,每30秒会发送一次心跳将服务的实例信息注册到注册中心。通知 Eureka Server 该实例仍然存在。如果超过90秒没有发送更新,则服务器将从注册信息中将此服务移除。Eureka Server在运行期间,会统计心跳失败的比例在15分钟之内是否低于85%,如果出现低于的情况(在单机调试的时候很容易满足,实际在生产环境上通常是由于网络不稳定导致),Eureka Server会将当前的实例注册信息保护起来,同时提示这个警告。保护模式主要用于一组客户端和Eureka Server之间存在网络分区场景下的保护。一旦进入保护模式,Eureka Server将会尝试保护其服务注册表中的信息,不再删除服务注册表中的数据(也就是不会注销任何微服务)验证完自我保护机制开启后,并不会马上呈现到web上,而是默认需等待 5 分钟(可以通过eureka.server.wait-time-in-ms-when-sync-empty 配置),即 5 分钟后你会看到下面的提示信息:
在这里插入图片描述

如果关闭自我保护

通过设置 eureka.enableSelfPreservation=false 来关闭自我保护功能。

6. Eureka中的元数据

Eureka的元数据有两种:标准元数据和自定义元数据。

  • 标准元数据:主机名、IP地址、端口号、状态页和健康检查等信息,这些信息都会被发布在服务注册表中,用于服务之间的调用。
  • 自定义元数据:可以使用eureka.instance.metadata-map配置,符合KEY/VALUE的存储格式。这些元数据可以在远程客户端中访问。

在程序中可以使用DiscoveryClient 获取指定微服务的所有元数据信息

@SpringBootTest
@RunWith(SpringJUnit4ClassRunner.class)
public class RestTemplateTest {
 
   @Autowired
   private DiscoveryClient discoveryClient;
    
   @Test
   public void test() {
      //根据微服务名称从注册中心获取相关的元数据信息
      List<ServiceInstance> instances = discoveryClient.getInstances("shop-service-                                            product");
      for (ServiceInstance instance : instances) {
          System.out.println(instance);
       }
   }
}

5. 服务注册Eureka高级

1. Eureka Server 高可用集群

Eureka Server可以通过运行多个实例并相互注册的方式实现高可用部署,Eureka Server实例会彼此增量地同步信息,从而确保所有节点数据一致。事实上,节点之间相互注册是Eureka Server的默认行为。
在这里插入图片描述

1. 搭建Eureka Server高可用集群
  1. 准备2个EurekaServer,相互注册

​ 1号server:9000

#模拟两个EurekaServer
#端口9000 , 8000
#两个server需要相互注册
spring:
  application:
    name: eureka-server
server:
  port: 9000 #端口
#配置eureka server
eureka:
  client:
#    register-with-eureka: false #是否将自己注册到注册中心
#    fetch-registry: false #是否从eureka中获取注册信息
    service-url: #配置暴露给Eureka Client的请求地址
      defaultZone: http://127.0.0.1:8000/eureka/

2号server:8000

#模拟两个EurekaServer
#端口9000 , 8000
#两个server需要相互注册
spring:
  application:
    name: eureka-server
server:
  port: 8000 #端口
#配置eureka server
eureka:
  client:
#    register-with-eureka: false #是否将自己注册到注册中心
#    fetch-registry: false #是否从eureka中获取注册信息
    service-url: #配置暴露给Eureka Client的请求地址
      defaultZone: http://127.0.0.1:9000/eureka/
2. 需要将微服务注册到两个EurekaServer上

如果需要将微服务注册到Eureka Server集群只需要修改yml配置文件即可

eureka:
 client:
   serviceUrl:
     defaultZone: http://eureka1:8000/eureka/,http://eureka1:9000/eureka/

2. Eureka中的常见问题

1. 服务注册慢

默认情况下,服务注册到Eureka Server的过程较慢。

服务的注册涉及到心跳,默认心跳间隔为30s。在实例、服务器、客户端都在本地缓存中具有相同的元数据之前,服务不可用于客户端发现(所以可能需要3次心跳)。可以通过配置eureka.instance.leaseRenewalIntervalInSeconds (心跳频率)加快客户端连接到其他服务的过程。在生产中,最好坚持使用默认值,因为在服务器内部有一些计算,他们对续约做出假设

2. 服务节点剔除问题

默认情况下,由于Eureka Server剔除失效服务间隔时间为90s且存在自我保护的机制。所以不能有效而迅速的剔除失效节点,这对开发或测试会造成困扰。解决方案如下:

Eureka Server

配置关闭自我保护,设置剔除无效节点的时间间隔

eureka:
 instance:
   hostname: eureka1
 client:
   service-url:
     defaultZone: http://eureka2:8762/eureka
 server:
   enable-self-preservation: false  #关闭自我保护
   eviction-interval-timer-in-ms: 4000 #剔除时间间隔,单位:毫秒

Eureka Client

配置开启健康检查,并设置续约时间

eureka:
 client:
   healthcheck: true #开启健康检查(依赖spring-boot-actuator)
   serviceUrl:
     defaultZone: http://eureka1:8000/eureka/,http://eureka1:9000/eureka/
 instance:
   preferIpAddress: true
   lease-expiration-duration-in-seconds: 10 #eureka client发送心跳给server端后,续
约到期时间(默认90秒)
   lease-renewal-interval-in-seconds: 5 #发送心跳续约间隔
3. 监控页面显示ip

在Eureka Server的管控台中,显示的服务实例名称默认情况下是微服务定义的名称和端口。为了更好的对所有服务进行定位,微服务注册到Eureka Server的时候可以手动配置示例ID。配置方式如下

eureka:
 instance:
   instance-id: ${spring.cloud.client.ip-address}:${server.port}
  #spring.cloud.client.ip-address:获取ip地址

在这里插入图片描述

3. Eureka源码解析

1. SpringBoot中的自动装载

1.ImportSelector

ImportSelector接口是Spring导入外部配置的核心接口,在SpringBoot的自动化配置和@EnableXXX(功能性注解)中起到了决定性的作用。当在@Confifiguration标注的Class上使用@Import引入了一个ImportSelector实现类后,会把实现类中返回的Class名称都定义为bean。

public interface ImportSelector {
    String[] selectImports(AnnotationMetadata var1);
}

DeferredImportSelector接口继承ImportSelector,他和ImportSelector的区别在于装载bean的时机上,DeferredImportSelector需要等所有的@Confifiguration都执行完毕后才会进行装载

public interface DeferredImportSelector extends ImportSelector {
 //...省略
}

例子,看下ImportSelector的用法

  1. 定义Bean对象
public class User {
 private String username;
 private Integer age;
 //省略..
}
  1. 定义配置类Confifiguration
//定义一个configuration ,注意这里并没有使用spring注解,spring扫描的时候并不会装载该类
public class UserConfiguration {
 @Bean
 public User getUser() {
 return new User("张三",18);
 }
}
  1. 定义ImportSelector
public class UserImportSelector implements ImportSelector {
 @Override
 public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        //获取配置类名称
 return new String[]{UserConfiguration.class.getName()};
 }
}
  1. 定义EnableXXX注解
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Target(ElementType.TYPE)
@Import(UserImportSelector.class)
public @interface EnableUserBean {
}
  1. 测试
/**
* 通过在类上声明@EnableUserBean,会自动的加载所有对象
*/
@EnableUserBean
public class TestApplication {
   public static void main(String[] args) {
     AnnotationConfigApplicationContext applicationContext = new
                         AnnotationConfigApplicationContext(TestApplication.class);
     User user = applicationContext.getBean(User.class);
     System.out.println(user);
   }
}

由此可见,HelloWorldConfifiguration对象并没有使用Spring的对象对象创建注解声明(@Controller,@Service,@Repostiroty),而是使用编程的方式动态的载入bean。

这个接口在哪里调用呢?我们可以来看一下ConfifigurationClassParser这个类的processImports方法

private void processImports(ConfigurationClass configClass, SourceClass
currentSourceClass,
            Collection<SourceClass> importCandidates, boolean
checkForCircularImports) {
        if (importCandidates.isEmpty()) {
            return;
       }
        if (checkForCircularImports && isChainedImportOnStack(configClass)) {
            this.problemReporter.error(new CircularImportProblem(configClass, 
this.importStack));
       }
        else {
            this.importStack.push(configClass);
            try {
                for (SourceClass candidate : importCandidates) 
{ //对ImportSelector的处理
                    if (candidate.isAssignable(ImportSelector.class)) {
                        // Candidate class is an ImportSelector -> delegate to 
it to determine imports
                        Class<?> candidateClass = candidate.loadClass();
                        ImportSelector selector =
BeanUtils.instantiateClass(candidateClass, ImportSelector.class);
                        ParserStrategyUtils.invokeAwareMethods(
                                selector, this.environment, this.resourceLoader, 
this.registry);
                        if (this.deferredImportSelectors != null && selector
instanceof DeferredImportSelector) { //如果为延迟导入处理
则加入集合当中
                            this.deferredImportSelectors.add(
                                    new
DeferredImportSelectorHolder(configClass, (DeferredImportSelector) selector));
                       }
                        else { //根据ImportSelector方法
的返回值来进行递归操作
                            String[] importClassNames =
selector.selectImports(currentSourceClass.getMetadata());
                            Collection<SourceClass> importSourceClasses =
asSourceClasses(importClassNames);
                            processImports(configClass, currentSourceClass, 
importSourceClasses, false);
                       }
                   }
                    else if
(candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) {
                        // Candidate class is an ImportBeanDefinitionRegistrar -
>
                        // delegate to it to register additional bean 
definitions
                        Class<?> candidateClass = candidate.loadClass();
                        ImportBeanDefinitionRegistrar registrar =
                                BeanUtils.instantiateClass(candidateClass, 
ImportBeanDefinitionRegistrar.class);
                        ParserStrategyUtils.invokeAwareMethods(
                                registrar, this.environment, 
this.resourceLoader, this.registry);
                        configClass.addImportBeanDefinitionRegistrar(registrar, 
currentSourceClass.getMetadata());
                   }
                    else { // 如果当前的类既不是
ImportSelector也不是ImportBeanDefinitionRegistar就进行@Configuration的解析处理
                        // Candidate class not an ImportSelector or 
ImportBeanDefinitionRegistrar ->
                        // process it as an @Configuration class
                        this.importStack.registerImport(
                                currentSourceClass.getMetadata(), 
candidate.getMetadata().getClassName());
                       
processConfigurationClass(candidate.asConfigClass(configClass));
                   }
               }
           }
            catch (BeanDefinitionStoreException ex) {
                throw ex;
           }
            catch (Throwable ex) {
                throw new BeanDefinitionStoreException(
                        "Failed to process import candidates for configuration 
class [" +
                        configClass.getMetadata().getClassName() + "]", ex);
           }
            finally {
                this.importStack.pop();
           }
       }
   }

在这里我们可以看到ImportSelector接口的返回值会递归进行解析,把解析到的类全名按照@Confifiguration进行处理

2. springBoot自动装载

SpringBoot开箱即用的特点,很大程度上归功于ImportSelector。接下来我们看下springBoot是如何在spring的基础上做扩展的。在SpringBoot中最重要的一个注解SpringBootApplication

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
    excludeFilters = {@Filter(
    type = FilterType.CUSTOM,
    classes = {TypeExcludeFilter.class}
), @Filter(
    type = FilterType.CUSTOM,
    classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication {
 //...
}

在SpringBootApplication注解中声明了一个 @EnableAutoConfiguration

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {
    String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
    Class<?>[] exclude() default {};
    String[] excludeName() default {};
}

在EnableAutoConfifiguration中通过Import引入了SpringBoot定义的AutoConfigurationImportSelector

这个类内容比较多,我们只需看下最主要的逻辑代码即可

public class AutoConfigurationImportSelector
 implements DeferredImportSelector, BeanClassLoaderAware, 
ResourceLoaderAware,
 BeanFactoryAware, EnvironmentAware, Ordered {
 @Override
 public String[] selectImports(AnnotationMetadata annotationMetadata) {
 if (!isEnabled(annotationMetadata)) {
 return NO_IMPORTS;
 }
 AutoConfigurationMetadata autoConfigurationMetadata =
AutoConfigurationMetadataLoader
 .loadMetadata(this.beanClassLoader);
 //主要逻辑在getAutoConfigurationEntry这个方法
 AutoConfigurationEntry autoConfigurationEntry =
getAutoConfigurationEntry(
 autoConfigurationMetadata, annotationMetadata);
 return
StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
 }
 protected AutoConfigurationEntry getAutoConfigurationEntry(
 AutoConfigurationMetadata autoConfigurationMetadata,
 AnnotationMetadata annotationMetadata) {
 if (!isEnabled(annotationMetadata)) {
 return EMPTY_ENTRY;
 }
 AnnotationAttributes attributes = getAttributes(annotationMetadata);
 //通过getCandidateConfigurations方法获取所有需要加载的bean
 List<String> configurations =
getCandidateConfigurations(annotationMetadata,
 attributes);
 //去重处理
 configurations = removeDuplicates(configurations);
 //获取不需要加载的bean,这里我们可以通过spring.autoconfigure.exclude人为配置
 Set<String> exclusions = getExclusions(annotationMetadata, attributes);
 checkExcludedClasses(configurations, exclusions);
 configurations.removeAll(exclusions);
 configurations = filter(configurations, autoConfigurationMetadata);
 //发送事件,通知所有的AutoConfigurationImportListener进行监听
 fireAutoConfigurationImportEvents(configurations, exclusions);
 return new AutoConfigurationEntry(configurations, exclusions);
 }
 //这里是获取bean渠道的地方,重点看SpringFactoriesLoader#loadFactoryNames
 protected List<String> getCandidateConfigurations(AnnotationMetadata
metadata,
 AnnotationAttributes attributes) {
 //这里的getSpringFactoriesLoaderFactoryClass()最终返回
EnableAutoConfiguration.class
 List<String> configurations = SpringFactoriesLoader.loadFactoryNames(
 getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader());
 Assert.notEmpty(configurations,
 "No auto configuration classes found in METAINF/spring.factories. If you "
 + "are using a custom packaging, make sure that file is 
correct.");
 return configurations;
 }
}

从上面的逻辑可以看出,最终获取bean的渠道在SpringFactoriesLoader.loadFactoryNames

public final class SpringFactoriesLoader {
    public static final String FACTORIES_RESOURCE_LOCATION = "METAINF/spring.factories";
    private static final Log logger =
LogFactory.getLog(SpringFactoriesLoader.class);
    private static final Map<ClassLoader, MultiValueMap<String, String>> cache =
new ConcurrentReferenceHashMap();
    public static List<String> loadFactoryNames(Class<?> factoryClass, @Nullable
ClassLoader classLoader) {
        String factoryClassName = factoryClass.getName();
        //通过factoryClassName获取相应的bean全称
 //上面传入的factoryClass是EnableAutoConfiguration.class
        return
(List)loadSpringFactories(classLoader).getOrDefault(factoryClassName, 
Collections.emptyList());
   }
    private static Map<String, List<String>> loadSpringFactories(@Nullable
ClassLoader classLoader) {
        MultiValueMap<String, String> result = (MultiValueMap)cache.get(classLoader);
        if (result != null) {
            return result;
       } else {
            try {
                //获取工程中所有META-INF/spring.factories文件,将其中的键值组合成Map
                Enumeration<URL> urls = classLoader != null ?
classLoader.getResources("META-INF/spring.factories") : 
ClassLoader.getSystemResources("META-INF/spring.factories");
                LinkedMultiValueMap result = new LinkedMultiValueMap();
                while(urls.hasMoreElements()) {
                    URL url = (URL)urls.nextElement();
每个jar都可以定义自己的META-INF/spring.factories ,jar被加载的同时 spring.factories里面定义的
bean就可以自动被加载
4.3.2 Eureka服务注册核心源码解析
4.3.2.1 EnableEurekaServer注解作用
通过 @EnableEurekaServer 激活EurekaServer
                    UrlResource resource = new UrlResource(url);
                    Properties properties =
PropertiesLoaderUtils.loadProperties(resource);
                    Iterator var6 = properties.entrySet().iterator();
                    while(var6.hasNext()) {
                        Entry<?, ?> entry = (Entry)var6.next();
                        String factoryClassName =
((String)entry.getKey()).trim();
                        String[] var9 =
StringUtils.commaDelimitedListToStringArray((String)entry.getValue());
                        int var10 = var9.length;
                        for(int var11 = 0; var11 < var10; ++var11) {
                            String factoryName = var9[var11];
                            result.add(factoryClassName, factoryName.trim());
                       }
                   }
               }
                cache.put(classLoader, result);
                return result;
           } catch (IOException var13) {
                throw new IllegalArgumentException("Unable to load factories 
from location [META-INF/spring.factories]", var13);
           }
       }
   }
    private static <T> T instantiateFactory(String instanceClassName, Class<T>
factoryClass, ClassLoader classLoader) {
        try {
            Class<?> instanceClass = ClassUtils.forName(instanceClassName, 
classLoader);
            if (!factoryClass.isAssignableFrom(instanceClass)) {
                throw new IllegalArgumentException("Class [" + instanceClassName
+ "] is not assignable to [" + factoryClass.getName() + "]");
           } else {
                return ReflectionUtils.accessibleConstructor(instanceClass, new
Class[0]).newInstance();
           }
       } catch (Throwable var4) {
            throw new IllegalArgumentException("Unable to instantiate factory 
class: " + factoryClass.getName(), var4);
       }
   }
}

每个jar都可以定义自己的META-INF/spring.factories ,jar被加载的同时 spring.factories里面定义的bean就可以自动被加载

3. Eureka服务注册核心源码解析
1. EnableEurekaServer注解作用

通过 @EnableEurekaServer 激活EurekaServer

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import({EurekaServerMarkerConfiguration.class})
public @interface EnableEurekaServer {
}

此类有一个重要作用:导入EurekaServerMarkerConfifiguration配置类实例化了一个Marker的bean对象,此对象是实例化核心配置类的前提条件

@Configuration
public class EurekaServerMarkerConfiguration {
    public EurekaServerMarkerConfiguration() {
   }
    @Bean
    public EurekaServerMarkerConfiguration.Marker eurekaServerMarkerBean() {
        return new EurekaServerMarkerConfiguration.Marker();
   }
    class Marker {
        Marker() {
       }
   }
}
2. 自动装载核心配置类

SpringCloud对EurekaServer的封装使得发布一个EurekaServer无比简单,根据自动装载原则可以在spring-cloud-netflix-eureka-server-2.1.0.RELEASE.jar 下找到 spring.factories
在这里插入图片描述

EurekaServerAutoConfiguration 是Eureka服务端的自动配置类

@Configuration
@Import({EurekaServerInitializerConfiguration.class})
@ConditionalOnBean({Marker.class})
@EnableConfigurationProperties({EurekaDashboardProperties.class, 
InstanceRegistryProperties.class})
@PropertySource({"classpath:/eureka/server.properties"})
public class EurekaServerAutoConfiguration extends WebMvcConfigurerAdapter {
    //...代码省略
}

现在我们展开来说这个Eureka服务端的自动配置类;

  1. 这个配置类实例化的前提条件是上下文中存在 EurekaServerMarkerConfifiguration.Marker 这个bean,解释了上面的问题

  2. 通过@EnableConfifigurationProperties({ EurekaDashboardProperties.class, InstanceRegistryProperties.class })导入了两个配置类

    1. EurekaDashboardProperties : 配置 EurekaServer的管控台
    2. InstanceRegistryProperties : 配置期望续约数量和默认的通信数量
  3. 通过@Import({EurekaServerInitializerConfifiguration.class})引入启动配置类

3. EurekaServerInitializerConfifiguration
@Configuration
public class EurekaServerInitializerConfiguration implements
ServletContextAware, SmartLifecycle, Ordered {
    public void start() {
       (new Thread(new Runnable() {
            public void run() {
                try {
                   
EurekaServerInitializerConfiguration.this.eurekaServerBootstrap.contextInitiali
zed(EurekaServerInitializerConfiguration.this.servletContext);
                    EurekaServerInitializerConfiguration.log.info("Started 
Eureka Server");
                    EurekaServerInitializerConfiguration.this.publish(new
EurekaRegistryAvailableEvent(EurekaServerInitializerConfiguration.this.getEureka
ServerConfig()));
                    EurekaServerInitializerConfiguration.this.running = true;
                    EurekaServerInitializerConfiguration.this.publish(new
EurekaServerStartedEvent(EurekaServerInitializerConfiguration.this.getEurekaServ
erConfig()));
               } catch (Exception var2) {
                    EurekaServerInitializerConfiguration.log.error("Could not 
initialize Eureka servlet context", var2);
               }
           }
       })).start();
   }

可以看到EurekaServerInitializerConfifiguration实现了SmartLifecycle,也就意味着Spring容器启动时会去执行start()方法。加载所有的EurekaServer的配置

4. EurekaServerAutoConfifiguration

实例化了EurekaServer的管控台的Controller类 EurekaController

@Bean
@ConditionalOnProperty(
  prefix = "eureka.dashboard",
  name = {"enabled"},
   matchIfMissing = true
)
public EurekaController eurekaController() {
  return new EurekaController(this.applicationInfoManager);
}

实例化EurekaServerBootstrap类

public EurekaServerBootstrap eurekaServerBootstrap(PeerAwareInstanceRegistry
registry, EurekaServerContext serverContext) {
        return new EurekaServerBootstrap(this.applicationInfoManager, 
this.eurekaClientConfig, this.eurekaServerConfig, registry, serverContext);
   }

实例化jersey相关配置类

@Bean
    public FilterRegistrationBean jerseyFilterRegistration(Application
eurekaJerseyApp) {
        FilterRegistrationBean bean = new FilterRegistrationBean();
        bean.setFilter(new ServletContainer(eurekaJerseyApp));
        bean.setOrder(2147483647);
        bean.setUrlPatterns(Collections.singletonList("/eureka/*"));
        return bean;
   }
    @Bean
    public Application jerseyApplication(Environment environment, ResourceLoader
resourceLoader) {
        ClassPathScanningCandidateComponentProvider provider = new
ClassPathScanningCandidateComponentProvider(false, environment);
        provider.addIncludeFilter(new AnnotationTypeFilter(Path.class));
        provider.addIncludeFilter(new AnnotationTypeFilter(Provider.class));
        Set<Class<?>> classes = new HashSet();
        String[] var5 = EUREKA_PACKAGES;
        int var6 = var5.length;
        for(int var7 = 0; var7 < var6; ++var7) {
            String basePackage = var5[var7];
            Set<BeanDefinition> beans =
provider.findCandidateComponents(basePackage);
            Iterator var10 = beans.iterator();
            while(var10.hasNext()) {
                BeanDefinition bd = (BeanDefinition)var10.next();
                Class<?> cls =
ClassUtils.resolveClassName(bd.getBeanClassName(), 
resourceLoader.getClassLoader());
                classes.add(cls);
           }
       }
        Map<String, Object> propsAndFeatures = new HashMap();
       
propsAndFeatures.put("com.sun.jersey.config.property.WebPageContentRegex", 
"/eureka/(fonts|images|css|js)/.*");
        DefaultResourceConfig rc = new DefaultResourceConfig(classes);
        rc.setPropertiesAndFeatures(propsAndFeatures);
        return rc;
   }

jerseyApplication 方法,在容器中存放了一个jerseyApplication对象,jerseyApplication()方法里的东西和Spring源码里扫描@Component逻辑类似,扫描@Path和@Provider标签,然后封装成beandefifinition,封装到Application的set容器里。通过fifilter过滤器来过滤url进行映射到对象的Controller

5. 暴露的服务端接口

由于集成了Jersey,我们可以找到在EurekaServer的依赖包中的 eureka-core-1.9.8.jar ,可以看到一些列的XXXResource
在这里插入图片描述

这些类都是通过Jersey发布的供客户端调用的服务接口。

服务端接受客户端的注册

在ApplicationResource.addInstance()方法中可以看到 this.registry.register(info, “true”.equals(isReplication));

 public void register(InstanceInfo info, boolean isReplication) {
         //默认有效时长90m
        int leaseDuration = 90;
        if (info.getLeaseInfo() != null &&
info.getLeaseInfo().getDurationInSecs() > 0) {
            leaseDuration = info.getLeaseInfo().getDurationInSecs();
       }
 //注册实例
        super.register(info, leaseDuration, isReplication);
        //同步到其他EurekaServer服务
        this.replicateToPeers(PeerAwareInstanceRegistryImpl.Action.Register, 
info.getAppName(), info.getId(), info, (InstanceStatus)null, isReplication);
   }

继续找到父类的register方法可以看到整个注册的过程

//线程安全的map,存放所有注册的示例对象
 private final ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>>
registry = new ConcurrentHashMap();    
 public void register(InstanceInfo registrant, int leaseDuration, boolean
isReplication) {
        try {
            this.read.lock();
            Map<String, Lease<InstanceInfo>> gMap = (Map)this.registry.get(registrant.getAppName());
            EurekaMonitors.REGISTER.increment(isReplication);
            //如果第一个实例注册会给registryput进去一个空的
            if (gMap == null) {
                ConcurrentHashMap<String, Lease<InstanceInfo>> gNewMap = new
ConcurrentHashMap();
                gMap = (Map)this.registry.putIfAbsent(registrant.getAppName(), 
gNewMap);
                if (gMap == null) {
                    gMap = gNewMap;
               }
           }
 //根据注册的示例对象id,获取已存在的Lease
            Lease<InstanceInfo> existingLease = (Lease)
((Map)gMap).get(registrant.getId());
            
            if (existingLease != null && existingLease.getHolder() != null) {
                Long existingLastDirtyTimestamp =
((InstanceInfo)existingLease.getHolder()).getLastDirtyTimestamp();
                Long registrationLastDirtyTimestamp =
registrant.getLastDirtyTimestamp();
                logger.debug("Existing lease found (existing={}, provided={}", 
existingLastDirtyTimestamp, registrationLastDirtyTimestamp);
                if (existingLastDirtyTimestamp > registrationLastDirtyTimestamp) 
{
                    logger.warn("There is an existing lease and the existing 
lease's dirty timestamp {} is greater than the one that is being registered {}", 
existingLastDirtyTimestamp, registrationLastDirtyTimestamp);
                    logger.warn("Using the existing instanceInfo instead of the 
new instanceInfo as the registrant");
                    registrant = (InstanceInfo)existingLease.getHolder();
               }
           } else {
                Object var6 = this.lock;
                synchronized(this.lock) {
                    if (this.expectedNumberOfClientsSendingRenews > 0) {
                        ++this.expectedNumberOfClientsSendingRenews;
                        this.updateRenewsPerMinThreshold();
                   }
               }
                logger.debug("No previous lease information found; it is new 
registration");
           }
            Lease<InstanceInfo> lease = new Lease(registrant, leaseDuration);
            if (existingLease != null) {
               
lease.setServiceUpTimestamp(existingLease.getServiceUpTimestamp());
           }
 //将lease存入gMap
           ((Map)gMap).put(registrant.getId(), lease);
            AbstractInstanceRegistry.CircularQueue var20 =
this.recentRegisteredQueue;
            synchronized(this.recentRegisteredQueue) {
                this.recentRegisteredQueue.add(new
Pair(System.currentTimeMillis(), registrant.getAppName() + "(" +
registrant.getId() + ")"));
           }
            if
(!InstanceStatus.UNKNOWN.equals(registrant.getOverriddenStatus())) {
                logger.debug("Found overridden status {} for instance {}. 
Checking to see if needs to be add to the overrides", 
registrant.getOverriddenStatus(), registrant.getId());
                if
(!this.overriddenInstanceStatusMap.containsKey(registrant.getId())) {
                    logger.info("Not found overridden id {} and hence adding 
it", registrant.getId());
                    this.overriddenInstanceStatusMap.put(registrant.getId(), 
registrant.getOverriddenStatus());
               }
           }
            InstanceStatus overriddenStatusFromMap = (InstanceStatus)this.overriddenInstanceStatusMap.get(registrant.getId());
            if (overriddenStatusFromMap != null) {
                logger.info("Storing overridden status {} from map", 
overriddenStatusFromMap);
                registrant.setOverriddenStatus(overriddenStatusFromMap);
           }
            InstanceStatus overriddenInstanceStatus =
this.getOverriddenInstanceStatus(registrant, existingLease, isReplication);
            registrant.setStatusWithoutDirty(overriddenInstanceStatus);
            if (InstanceStatus.UP.equals(registrant.getStatus())) {
                lease.serviceUp();
           }
            registrant.setActionType(ActionType.ADDED);
            this.recentlyChangedQueue.add(new
AbstractInstanceRegistry.RecentlyChangedItem(lease));
            registrant.setLastUpdatedTimestamp();
            this.invalidateCache(registrant.getAppName(), 
registrant.getVIPAddress(), registrant.getSecureVipAddress());
            logger.info("Registered instance {}/{} with status {} (replication=
{})", new Object[]{registrant.getAppName(), registrant.getId(), 
registrant.getStatus(), isReplication});
       } finally {
            this.read.unlock();
       }
   }

服务端接受客户端的续约

在InstanceResource的renewLease方法中完成客户端的心跳(续约)处理,其中最关键的方法就是this.registry.renew(this.app.getName(), this.id, isFromReplicaNode)

public boolean renew(String appName, String id, boolean isReplication) {
        //客户端续约
        if (super.renew(appName, id, isReplication)) { 
            //同步到其他的EurekaServer服务
           
this.replicateToPeers(PeerAwareInstanceRegistryImpl.Action.Heartbeat, appName, 
id, (InstanceInfo)null, (InstanceStatus)null, isReplication);
            return true;
       } else {
            return false;
       }
   }

继续找到父类的renew方法可以看到整个续约的过程

public boolean renew(String appName, String id, boolean isReplication) {
        EurekaMonitors.RENEW.increment(isReplication);
        Map<String, Lease<InstanceInfo>> gMap = (Map)this.registry.get(appName);
        //从内存map中根据id获取示例对象的Lease对象
        Lease<InstanceInfo> leaseToRenew = null;
        if (gMap != null) {
            leaseToRenew = (Lease)gMap.get(id);
       }
        if (leaseToRenew == null) {
            EurekaMonitors.RENEW_NOT_FOUND.increment(isReplication);
            logger.warn("DS: Registry: lease doesn't exist, registering 
resource: {} - {}", appName, id);
            return false;
       } else {
            //获取示例对象
            InstanceInfo instanceInfo = (InstanceInfo)leaseToRenew.getHolder();
            if (instanceInfo != null) {
                InstanceStatus overriddenInstanceStatus =
this.getOverriddenInstanceStatus(instanceInfo, leaseToRenew, isReplication);
                if (overriddenInstanceStatus == InstanceStatus.UNKNOWN) {
                    logger.info("Instance status UNKNOWN possibly due to deleted 
override for instance {}; re-register required", instanceInfo.getId());
                    EurekaMonitors.RENEW_NOT_FOUND.increment(isReplication);
                    return false;
               }
                if (!instanceInfo.getStatus().equals(overriddenInstanceStatus)) 
{
                    logger.info("The instance status {} is different from 
overridden instance status {} for instance {}. Hence setting the status to 
overridden status", new Object[]{instanceInfo.getStatus().name(), 
instanceInfo.getOverriddenStatus().name(), instanceInfo.getId()});
                    //设置示例状态
                   
instanceInfo.setStatusWithoutDirty(overriddenInstanceStatus);
               }
}
 //设置续约次数
            this.renewsLastMin.increment();
            leaseToRenew.renew();
            return true;
       }
   }
6. 服务剔除

在AbstractInstanceRegistry.postInit()方法,在此方法里开启了一个每60秒调用一次EvictionTask.evict()的定时器。

 public void evict(long additionalLeaseMs) {
        logger.debug("Running the evict task");
        if (!this.isLeaseExpirationEnabled()) {
            logger.debug("DS: lease expiration is currently disabled.");
       } else {
            List<Lease<InstanceInfo>> expiredLeases = new ArrayList();
            Iterator var4 = this.registry.entrySet().iterator();
            while(true) {
                Map leaseMap;
                do {
                    if (!var4.hasNext()) {
                        int registrySize = (int)this.getLocalRegistrySize();
                        int registrySizeThreshold = (int)((double)registrySize *
this.serverConfig.getRenewalPercentThreshold());
                        int evictionLimit = registrySize -
registrySizeThreshold;
                        int toEvict = Math.min(expiredLeases.size(), 
evictionLimit);
                        if (toEvict > 0) {
                            logger.info("Evicting {} items (expired={}, 
evictionLimit={})", new Object[]{toEvict, expiredLeases.size(), evictionLimit});
                            Random random = new
Random(System.currentTimeMillis());
                            for(int i = 0; i < toEvict; ++i) {
                                int next = i +
random.nextInt(expiredLeases.size() - i);
                                Collections.swap(expiredLeases, i, next);
                                Lease<InstanceInfo> lease = (Lease)expiredLeases.get(i);
                                String appName =
((InstanceInfo)lease.getHolder()).getAppName();
                                String id =
((InstanceInfo)lease.getHolder()).getId();
                                EurekaMonitors.EXPIRED.increment();
                                logger.warn("DS: Registry: expired lease for 
{}/{}", appName, id);
                                this.internalCancel(appName, id, false);
                           }
                       }
                        return;
                   }
4.3.3 Eureka服务发现核心源码解析
4.3.3.1 自动装载
在服务消费者导入的坐标中有 spring-cloud-netflix-eureka-client-2.1.0.RELEASE.jar 找到其中
的 spring.factories 可以看到所有自动装载的配置类
4.3.3.2 服务注册
                    Entry<String, Map<String, Lease<InstanceInfo>>> groupEntry = (Entry)var4.next();
                    leaseMap = (Map)groupEntry.getValue();
               } while(leaseMap == null);
                Iterator var7 = leaseMap.entrySet().iterator();
                while(var7.hasNext()) {
                    Entry<String, Lease<InstanceInfo>> leaseEntry = (Entry)var7.next();
                    Lease<InstanceInfo> lease = (Lease)leaseEntry.getValue();
                    if (lease.isExpired(additionalLeaseMs) && lease.getHolder() 
!= null) {
                        expiredLeases.add(lease);
                   }
               }
           }
       }
   }
4. Eureka服务发现核心源码解析
1. 自动装载

在服务消费者导入的坐标中有 spring-cloud-netflix-eureka-client-2.1.0.RELEASE.jar 找到其中的 spring.factories 可以看到所有自动装载的配置类
在这里插入图片描述

2. 服务注册
 boolean register() throws Throwable {
        logger.info("DiscoveryClient_{}: registering service...", 
this.appPathIdentifier);
        EurekaHttpResponse httpResponse;
        try {
            httpResponse =
this.eurekaTransport.registrationClient.register(this.instanceInfo);
       } catch (Exception var3) {
            logger.warn("DiscoveryClient_{} - registration failed {}", new
Object[]{this.appPathIdentifier, var3.getMessage(), var3});
            throw var3;
       }
        if (logger.isInfoEnabled()) {
            logger.info("DiscoveryClient_{} - registration status: {}", 
this.appPathIdentifier, httpResponse.getStatusCode());
       }
        return httpResponse.getStatusCode() ==
Status.NO_CONTENT.getStatusCode();
}
3. 服务下架
@PreDestroy
    public synchronized void shutdown() {
        if (this.isShutdown.compareAndSet(false, true)) {
            logger.info("Shutting down DiscoveryClient ...");
            if (this.statusChangeListener != null && this.applicationInfoManager
!= null) {
               
this.applicationInfoManager.unregisterStatusChangeListener(this.statusChangeLis
tener.getId());
           }
            this.cancelScheduledTasks();
            if (this.applicationInfoManager != null &&
this.clientConfig.shouldRegisterWithEureka() &&
this.clientConfig.shouldUnregisterOnShutdown()) {
               
this.applicationInfoManager.setInstanceStatus(InstanceStatus.DOWN);
                this.unregister();
           }
            if (this.eurekaTransport != null) {
                this.eurekaTransport.shutdown();
           }
            this.heartbeatStalenessMonitor.shutdown();
            this.registryStalenessMonitor.shutdown();
            logger.info("Completed shut down of DiscoveryClient");
       }
   }
4. 心跳续约

在com.netflflix.discovery.DiscoveryClient.HeartbeatThread中定义了续约的操作,我们查看renew()方 法;

 boolean renew() {
        try {
            EurekaHttpResponse<InstanceInfo> httpResponse =
this.eurekaTransport.registrationClient.sendHeartBeat(this.instanceInfo.getAppNa
me(), this.instanceInfo.getId(), this.instanceInfo, (InstanceStatus)null);
            logger.debug("DiscoveryClient_{} - Heartbeat status: {}", 
this.appPathIdentifier, httpResponse.getStatusCode());
            if (httpResponse.getStatusCode() ==
Status.NOT_FOUND.getStatusCode()) {
                this.REREGISTER_COUNTER.increment();
                logger.info("DiscoveryClient_{} - Re-registering apps/{}", 
this.appPathIdentifier, this.instanceInfo.getAppName());
                long timestamp = this.instanceInfo.setIsDirtyWithTime();
                boolean success = this.register();
                if (success) {
                    this.instanceInfo.unsetIsDirty(timestamp);
               }
                 return success;
           } else {
                return httpResponse.getStatusCode() ==
Status.OK.getStatusCode();
           }
       } catch (Throwable var5) {
            logger.error("DiscoveryClient_{} - was unable to send heartbeat!", 
this.appPathIdentifier, var5);
            return false;
       }
   }

在renew()这个方法中,首先向注册中心执行了心跳续约的请求,StatusCode为200成功,若为404则执行register()重新注册操作;

最后总结一下eureka客户端做的事情;

  1. 根据配置文件初始化bean,创建客户端实例信息 InstanceInfo

  2. 第一次全量拉取注册中心服务列表(url=/apps),初始化周期任务:

    1. CacheRefreshThread 定时刷新本地缓存服务列表,若是客户端第一次拉取,则会全量拉取,后面则增量拉取.若增量拉取失败则全量拉取,配置属性为eureka.client.registryFetchIntervalSeconds=30默认拉取一次;
    2. HeartbeatThread 通过renew()续约任务,维持于注册中心的心跳(url=/apps/ {id}),若返回状态码为404则说明该服务实例没有在注册中心注册,执行register()向注册中心注册实例信息;
    3. ApplicationInfoManager.StatusChangeListener 注册实例状态监听类,监听服务实例状态变化,向注册中心同步实例状态;
    4. InstanceInfoReplicator 定时刷新实例状态,并向注册中心同步,默认eureka.client.instanceInfoReplicationIntervalSeconds=30执行一次.若实例状态有变更,则重新执行注册;

6. Eureka替换方案Consul

1. 什么是consul

1. consul概述

Consul 是 HashiCorp 公司推出的开源工具,用于实现分布式系统的服务发现与配置。与其它分布式服务注册与发现的方案,Consul 的方案更“一站式”,内置了服务注册与发现框 架、分布一致性协议实现、健康检查、Key/Value 存储、多数据中心方案,不再需要依赖其它工具(比如 ZooKeeper 等)。使用起来也较 为简单。Consul 使用 Go 语言编写,因此具有天然可移植性(支持Linux、windows和Mac OS X);安装包仅包含一个可执行文件,方便部署,与 Docker 等轻量级容器可无缝配合。

Consul 的优势:

  • 使用 Raft 算法来保证一致性, 比复杂的 Paxos 算法更直接. 相比较而言, zookeeper 采用的是Paxos, 而 etcd 使用的则是 Raft。
  • 支持多数据中心,内外网的服务采用不同的端口进行监听。 多数据中心集群可以避免单数据中心的单点故障,而其部署则需要考虑网络延迟, 分片等情况等。 zookeeper 和 etcd 均不提供多数据中心功能的支持。
  • 支持健康检查。 etcd 不提供此功能。
  • 支持 http 和 dns 协议接口。 zookeeper 的集成较为复杂, etcd 只支持 http 协议。
  • 官方提供 web 管理界面, etcd 无此功能。
  • 综合比较, Consul 作为服务注册和配置管理的新星, 比较值得关注和研究。

特性:

  • 服务发现
  • 健康检查
  • Key/Value 存储
  • 多数据中心
2. consul与Eureka的区别

一致性

Consul强一致性(CP)

  • 服务注册相比Eureka会稍慢一些。因为Consul的raft协议要求必须过半数的节点都写入成功才认为注册成功

  • Leader挂掉时,重新选举期间整个consul不可用。保证了强一致性但牺牲了可用性。

Eureka保证高可用和最终一致性(AP)

  • 服务注册相对要快,因为不需要等注册信息replicate到其他节点,也不保证注册信息是否replicate成功

  • 当数据出现不一致时,虽然A, B上的注册信息不完全相同,但每个Eureka节点依然能够正常对外提供服务,这会出现查询服务信息时如果请求A查不到,但请求B就能查到。如此保证了可用性但牺牲了一致性。

开发语言和使用

eureka就是个servlet程序,跑在servlet容器中

Consul则是go编写而成,安装启动即可

3. consul的下载与安装

Consul 不同于 Eureka 需要单独安装,访问Consul 官网下载 Consul 的最新版本,我这里是consul1.5x。根据不同的系统类型选择不同的安装包,从下图也可以看出 Consul 支持所有主流系统。

在linux虚拟中下载consul服务

## 从官网下载最新版本的Consul服务
wget https://releases.hashicorp.com/consul/1.5.3/consul_1.5.3_linux_amd64.zip
##使用unzip命令解压
unzip consul_1.5.3_linux_amd64.zip
##将解压好的consul可执行命令拷贝到/usr/local/bin目录下
cp consul /usr/local/bin
##测试一下
consul

启动consul服务

##已开发者模式快速启动,-client指定客户端可以访问的ip地址
[root@node01 ~]# consul agent -dev -client=0.0.0.0
==> Starting Consul agent...
           Version: 'v1.5.3'
           Node ID: '49ed9aa0-380b-3772-a0b6-b0c6ad561dc5'
         Node name: 'node01'
       Datacenter: 'dc1' (Segment: '<all>')
           Server: true (Bootstrap: false)
       Client Addr: [127.0.0.1] (HTTP: 8500, HTTPS: -1, gRPC: 8502, DNS: 8600)
     Cluster Addr: 127.0.0.1 (LAN: 8301, WAN: 8302)
           Encrypt: Gossip: false, TLS-Outgoing: false, TLS-Incoming: false, 
Auto-Encrypt-TLS: false

启动成功之后访问: http://IP:8500 ,可以看到 Consul 的管理界面
在这里插入图片描述

2. consul的基本使用

Consul 支持健康检查,并提供了 HTTP 和 DNS 调用的API接口完成服务注册,服务发现,以及K/V存储这些功能。接下来通过发送HTTP请求的形式来了解一下Consul

1. 服务注册与发现

注册服务

通过postman发送put请求到http://192.168.74.101:8500/v1/catalog/register地址可以完成服务注册

{
 "Datacenter": "dc1", 
 "Node": "node01", 
 "Address": "192.168.74.102",
 "Service": {
 "ID":"mysql-01",
 "Service": "mysql", 
 "tags": ["master","v1"], 
 "Address": "192.168.74.102",
 "Port": 3306
 }
}

服务查询

通过postman发送get请求到http://192.168.74.101:8500/v1/catalog/services查看所有的服务列表
在这里插入图片描述

通过postman发送get请求到http://192.168.74.101:8500/v1/catalog/service/服务名查看具体的服务详情
在这里插入图片描述

服务删除

通过postman发送put请求到http://192.168.74.101:8500/v1/catalog/deregister删除服务

{
  "Datacenter": "dc1",
  "Node": "node01",
  "ServiceID": "mysql-01"
}
2. Consul的KV存储

可以参照Consul提供的KV存储的API完成基于Consul的数据存储
在这里插入图片描述

  • key值中可以带/, 可以看做是不同的目录结构。
  • value的值经过了base64_encode,获取到数据后base64_decode才能获取到原始值。数据不能大于512Kb
  • 不同数据中心的kv存储系统是独立的,使用dc=?参数指定。

3. 基于consul的服务注册

  1. 引入依赖
  <!--SpringCloud提供的基于Consul的服务发现-->
 <dependency>
     <groupId>org.springframework.cloud</groupId>
     <artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
 <!--actuator用于心跳检查-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

其中 spring-cloud-starter-consul-discovery 是SpringCloud提供的对consul支持的相关依赖。spring-boot-starter-actuator 适用于完成心跳检测响应的相关依赖。

  1. 配置服务注册

修改每个微服务的application.yml配置文件,添加consul服务发现的相关配置信息

spring:
 ...省略
 cloud:
   consul: #consul相关配置
     host: 192.168.74.101 #ConsulServer请求地址
     port: 8500 #ConsulServer端口
     discovery:
        #是否注册
       register: true
        #实例ID
       instance-id: ${spring.application.name}-1
        #服务实例名称
       service-name: ${spring.application.name}
        #服务实例端口
       port: ${server.port}
        #健康检查路径
       healthCheckPath: /actuator/health
        #健康检查时间间隔
       healthCheckInterval: 15s
        #开启ip地址注册
       prefer-ip-address: true
        #实例的请求ip
       ip-address: ${spring.cloud.client.ip-address}

其中 spring.cloud.consul 中添加consul的相关配置

  • host:表示Consul的Server的请求地址
  • port:表示Consul的Server的端口
  • discovery:服务注册与发现的相关配置
    • ​ instance-id :
    • 实例的唯一id(推荐必填),spring cloud官网文档的推荐,为了保证生成一个唯一的id ,也可以换成 s p r i n g . a p p l i c a t i o n . n a m e : {spring.application.name}: spring.application.name:{spring.cloud.client.ipAddress}
    • prefer-ip-address:开启ip地址注册
    • ip-address:当前微服务的请求ip
  1. 在控制台中查看服务列表
    在这里插入图片描述

4. 基于consul的服务发现

由于SpringCloud对Consul进行了封装。对于在消费者端获取服务提供者信息和Eureka是一致的。同样使用 DiscoveryClient 完成调用获取微服务实例信息

5. consul高可用集群

在这里插入图片描述

此图是官网提供的一个事例系统图,图中的Server是consul服务端高可用集群,Client是consul客户端。consul客户端不保存数据,客户端将接收到的请求转发给响应的Server端。Server之间通过局域网或广域网通信实现数据一致性。每个Server或Client都是一个consul agent。Consul集群间使用了GOSSIP协议通信和raft一致性算法。上面这张图涉及到了很多术语:

  • Agent——agent是一直运行在Consul集群中每个成员上的守护进程。通过运行 consul agent来启动。agent可以运行在client或者server模式。指定节点作为client或者server是非常简单的,除非有其他agent实例。所有的agent都能运行DNS或者HTTP接口,并负责运行时检查和保持服务同步。
  • Client——一个Client是一个转发所有RPC到server的代理。这个client是相对无状态的。client唯一执行的后台活动是加入LAN
  • gossip池。这有一个最低的资源开销并且仅消耗少量的网络带宽。
  • Server——一个server是一个有一组扩展功能的代理,这些功能包括参与Raft选举,维护集群状态,响应RPC查询,与其他数据中心交互WANgossip和转发查询给leader或者远程数据中心。
  • DataCenter——虽然数据中心的定义是显而易见的,但是有一些细微的细节必须考虑。例如,在EC2中,多个可用区域被认为组成一个数据中心?我们定义数据中心为一个私有的,低延迟和高带宽的一个网络环境。这不包括访问公共网络,但是对于我们而言,同一个EC2中的多个可用区域可以被认为是一个数据中心的一部分。
  • Consensus——在我们的文档中,我们使用Consensus来表明就leader选举和事务的顺序达成一致。由于这些事务都被应用到有限状态机上,Consensus暗示复制状态机的一致性。
  • Gossip——Consul建立在Serf的基础之上,它提供了一个用于多播目的的完整的gossip协议。
  • Serf提供成员关系,故障检测和事件广播。更多的信息在gossip文档中描述。这足以知道gossip使用基于UDP的随机的点到点通信。
  • LAN Gossip——它包含所有位于同一个局域网或者数据中心的所有节点。 WAN
  • Gossip——它只包含Server。这些server主要分布在不同的数据中心并且通常通过因特网或者广域网通信。

在每个数据中心,client和server是混合的。一般建议有3-5台server。这是基于有故障情况下的可用性和性能之间的权衡结果,因为越多的机器加入达成共识越慢。然而,并不限制client的数量,它们可以很容易的扩展到数千或者数万台。

同一个数据中心的所有节点都必须加入gossip协议。这意味着gossip协议包含一个给定数据中心的所有节点。这服务于几个目的:第一,不需要在client上配置server地址。发现都是自动完成的。第二,检测节点故障的工作不是放在server上,而是分布式的。这是的故障检测相比心跳机制有更高的可扩展性。第三:它用来作为一个消息层来通知事件,比如leader选举发生时。每个数据中心的server都是Raft节点集合的一部分。这意味着它们一起工作并选出一个leader,一个有额外工作的server。leader负责处理所有的查询和事务。作为一致性协议的一部分,事务也必须被复制到所有其他的节点。因为这一要求,当一个非leader得server收到一个RPC请求时,它将请求转发给集群leader。

server节点也作为WAN gossip Pool的一部分。这个Pool不同于LAN Pool,因为它是为了优化互联网更高的延迟,并且它只包含其他Consul server节点。这个Pool的目的是为了允许数据中心能够以lowtouch的方式发现彼此。这使得一个新的数据中心可以很容易的加入现存的WAN gossip。因为server都运行在这个pool中,它也支持跨数据中心请求。当一个server收到来自另一个数据中心的请求时,它随即转发给正确数据中想一个server。该server再转发给本地leader。这使得数据中心之间只有一个很低的耦合,但是由于故障检测,连接缓存和复用,跨数据中心的请求都是相对快速和可靠的。

1. Consul的核心知识

Gossip协议

传统的监控,如ceilometer,由于每个节点都会向server报告状态,随着节点数量的增加server的压力随之增大。在所有的Agent之间(包括服务器模式和普通模式)运行着Gossip协议。服务器节点和普通Agent都会加入这个Gossip集群,收发Gossip消息。每隔一段时间,每个节点都会随机选择几个节点发送Gossip消息,其他节点会再次随机选择其他几个节点接力发送消息。这样一段时间过后,整个集群都能收到这条消息。示意图如下。
在这里插入图片描述

RAFT一致性算法
在这里插入图片描述

为了实现集群中多个ConsulServer中的数据保持一致性,consul使用了基于强一致性的RAFT算法。

在Raft中,任何时候一个服务器可以扮演下面角色之一:

  1. Leader: 处理所有客户端交互,日志复制等,一般一次只有一个Leader.

  2. Follower: 类似选民,完全被动

  3. Candidate(候选人): 可以被选为一个新的领导人。

Leader全权负责所有客户端的请求,以及将数据同步到Follower中(同一时刻系统中只存在一个Leader)。Follower被动响应请求RPC,从不主动发起请求RPC。Candidate由Follower向Leader转换的中间状态

关于RAFT一致性算法有一个经典的动画http://thesecretlivesofdata.com/raft/,其中详细介绍了选举,数据同步的步骤。

2. Consul集群搭建

在这里插入图片描述

首先需要有一个正常的Consul集群,有Server,有Leader。这里在服务器Server1、Server2、Server3上分别部署了Consul Server。(这些服务器上最好只部署Consul程序,以尽量维护Consul Server的稳定)

服务器Server4和Server5上通过Consul Client分别注册Service A、B、C,这里每个Service分别部署在了两个服务器上,这样可以避免Service的单点问题。(一般微服务和Client绑定)

在服务器Server6中Program D需要访问Service B,这时候Program D首先访问本机Consul Client提供的HTTP API,本机Client会将请求转发到Consul Server,Consul Server查询到Service B当前的信息返回

1. 准备环境
在这里插入图片描述

  • Agent 以 client 模式启动的节点。在该模式下,该节点会采集相关信息,通过 RPC 的方式向server 发送。Client模式节点有无数个,官方建议搭配微服务配置
  • Agent 以 server 模式启动的节点。一个数据中心中至少包含 1 个 server 节点。不过官方建议使用3 或 5 个 server 节点组建成集群,以保证高可用且不失效率。server 节点参与 Raft、维护会员信息、注册服务、健康检查等功能。

2. 安装consul并启动

  • 在每个consul节点上安装consul服务,下载安装过程和单节点一致
##从官网下载最新版本的Consul服务
wget https://releases.hashicorp.com/consul/1.5.3/consul_1.5.3_linux_amd64.zip
##使用unzip命令解压
unzip consul_1.5.3_linux_amd64.zip
##将解压好的consul可执行命令拷贝到/usr/local/bin目录下
cp consul /usr/local/bin
##测试一下
consul
  • 启动每个consul server节点
##登录s1虚拟机,以server形式运行
consul agent -server -bootstrap-expect 3 -data-dir /etc/consul.d -node=server-1 
-bind=192.168.74.101 -ui -client 0.0.0.0 &
##登录s2 虚拟机,以server形式运行
consul agent -server -bootstrap-expect 2 -data-dir /etc/consul.d -node=server-2 
-bind=192.168.74.102 -ui -client 0.0.0.0 & 
##登录s3 虚拟机,以server形式运行
consul agent -server -bootstrap-expect 2 -data-dir /etc/consul.d -node=server-3 
-bind=192.168.74.103 -ui -client 0.0.0.0 &

-server: 以server身份启动。

-bootstrap-expect:集群要求的最少server数量,当低于这个数量,集群即失效。

-data-dir:data存放的目录,更多信息请参阅consul数据同步机制

-node:节点id,在同一集群不能重复。

-bind:监听的ip地址。

-client:客户端的ip地址(0.0.0.0表示不限制)

& :在后台运行,此为linux脚本语法

至此三个Consul Server模式服务全部启动成功

##在本地电脑中使用client形式启动consul
consul agent -client=0.0.0.0  -data-dir /etc/consul.d -node=client-1

3. 每个节点加入集群

在s2,s3,s4 服务其上通过consul join 命令加入 s1中的consul集群中

##加入consul集群
consul join 192.168.74.101

4. 测试

在任意一台服务器中输入 consul members查看集群中的所有节点信息

##查看consul集群节点信息
consul members

在这里插入图片描述

3. Consul常见问题
1. 节点和服务注销

当服务或者节点失效,Consul不会对注册的信息进行剔除处理,仅仅标记已状态进行标记(并且不可使用)。如果担心失效节点和失效服务过多影响监控。可以通过调用HTTP API的形式进行处理

节点和服务的注销可以使用HTTP API:

  • 注销任意节点和服务:/catalog/deregister
  • 注销当前节点的服务:/agent/service/deregister/:service_id

如果某个节点不继续使用了,也可以在本机使用consul leave命令,或者在其它节点使用consul forceleave 节点Id。

2. 健康检查与故障转移

在集群环境下,健康检查是由服务注册到的Agent来处理的,那么如果这个Agent挂掉了,那么此节点的健康检查就处于无人管理的状态。

从实际应用看,节点上的服务可能既要被发现,又要发现别的服务,如果节点挂掉了,仅提供被发现的功能实际上服务还是不可用的。当然发现别的服务也可以不使用本机节点,可以通过访问一个Nginx实现的若干Consul节点的负载均衡来实现。

7. 服务调用Ribbon入门

1. Ribbon概述

1. 什么是Ribbon

是 Netflflixfa 发布的一个负载均衡器,有助于控制 HTTP 和 TCP客户端行为。在 SpringCloud 中,Eureka一般配合Ribbon进行使用,Ribbon提供了客户端负载均衡的功能,Ribbon利用从Eureka中读取到的服务信息,在调用服务节点提供的服务时,会合理的进行负载。

在SpringCloud中可以将注册中心和Ribbon配合使用,Ribbon自动的从注册中心中获取服务提供者的列表信息,并基于内置的负载均衡算法,请求服务

2. Ribbon的主要作用

服务调用

基于Ribbon实现服务调用, 是通过拉取到的所有服务列表组成(服务名-请求路径的)映射关系。借助RestTemplate 最终进行调用

负载均衡

当有多个服务提供者时,Ribbon可以根据负载均衡的算法自动的选择需要调用的服务地址

2. 基于Ribbon实现调用服务

  1. 引入依赖

eureka内部继承了ribbon,在springcloud提供的服务发现的jar中以及包含了Ribbon的依赖。所以这里不需要导入任何额外的坐标

  1. 修改启动类
/**
* 基于Ribbon的服务调用与负载均衡
*/
@LoadBalanced
@Bean
public RestTemplate getRestTemplate() {
    return new RestTemplate();
}

8. 服务调用Ribbon高级

1. 负载均衡概述

1. 什么是负载均衡

在搭建网站时,如果单节点的 web服务性能和可靠性都无法达到要求;或者是在使用外网服务时,经常担心被人攻破,一不小心就会有打开外网端口的情况,通常这个时候加入负载均衡就能有效解决服务问题。

负载均衡是一种基础的网络服务,其原理是通过运行在前面的负载均衡服务,按照指定的负载均衡算法,将流量分配到后端服务集群上,从而为系统提供并行扩展的能力。

负载均衡的应用场景包括流量包、转发规则以及后端服务,由于该服务有内外网个例、健康检查等功能,能够有效提供系统的安全性和可用性。
在这里插入图片描述

2. 客户端负载均衡与服务端负载均衡

服务端负载均衡

先发送请求到负载均衡服务器或者软件,然后通过负载均衡算法,在多个服务器之间选择一个进行访问;即在服务器端再进行负载均衡算法分配

客户端负载均衡

客户端会有一个服务器地址列表,在发送请求前通过负载均衡算法选择一个服务器,然后进行访问,这是客户端负载均衡;即在客户端就进行负载均衡算法分配

2. 基于Ribbon实现负载均衡

1. 负载均衡策略

Ribbon内置了多种负载均衡策略,内部负责复杂均衡的顶级接口为com.netflix.loadbalancer.IRule ,实现方式如下
在这里插入图片描述

  • com.netflix.loadbalancer.RoundRobinRule :以轮询的方式进行负载均衡。
  • com.netflix.loadbalancer.RandomRule :随机策略
  • com.netflix.loadbalancer.RetryRule :重试策略。
  • com.netflix.loadbalancer.WeightedResponseTimeRule :权重策略。会计算每个服务的权重,越高的被调用的可能性越大。
  • com.netflix.loadbalancer.BestAvailableRule :最佳策略。遍历所有的服务实例,过滤掉故障实例,并返回请求数最小的实例返回。
  • com.netflix.loadbalancer.AvailabilityFilteringRule :可用过滤策略。过滤掉故障和请求数超过阈值的服务实例,再从剩下的实力中轮询调用。

在服务消费者的application.yml配置文件中修改负载均衡策略

##需要调用的微服务名称
shop-service-product:
 ribbon:
   NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule

策略选择:

1、如果每个机器配置一样,则建议不修改策略 (推荐)

2、如果部分机器配置强,则可以改为 WeightedResponseTimeRule

3. Ribbon中负载均衡的源码解析

1. Ribbon中的关键组件

在这里插入图片描述

  • ServerList:可以响应客户端的特定服务的服务器列表。
  • ServerListFilter:可以动态获得的具有所需特征的候选服务器列表的过滤器。
  • ServerListUpdater:用于执行动态服务器列表更新。
  • Rule:负载均衡策略,用于确定从服务器列表返回哪个服务器。
  • Ping:客户端用于快速检查服务器当时是否处于活动状态。
  • LoadBalancer:负载均衡器,负责负载均衡调度的管理。
2. @LoadBalanced注解

使用Ribbon完成客户端负载均衡往往是从一个注解开始的

/**
* 基于Ribbon的服务调用与负载均衡
*/
@LoadBalanced
@Bean
public RestTemplate getRestTemplate() {
    return new RestTemplate();
}

这个注解的主要作用是什么呢,查看源码

/**
* Annotation to mark a RestTemplate bean to be configured to use a 
LoadBalancerClient
* @author Spencer Gibb
*/
@Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Qualifier
public @interface LoadBalanced {
}

通过注释可以知道@LoadBalanced注解是用来给RestTemplate做标记,方便我们对RestTemplate添加一个LoadBalancerClient,以实现客户端负载均衡。

3. 自动装配

根据SpringBoot中的自动装配规则可以在 spring-cloud-netflix-ribbon-2.1.0.RELEASE.jar 中可以找到 spring.factories

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\org.springframew
ork.cloud.netflix.ribbon.RibbonAutoConfiguration

找到自动装配的类RibbonAutoConfifiguration

@Configuration
@Conditional({RibbonAutoConfiguration.RibbonClassesConditions.class})
@RibbonClients
@AutoConfigureAfter(
    name = {"org.springframework.cloud.netflix.eureka.EurekaClientAutoConfiguration"} )
@AutoConfigureBefore({LoadBalancerAutoConfiguration.class, 
AsyncLoadBalancerAutoConfiguration.class})
@EnableConfigurationProperties({RibbonEagerLoadProperties.class, 
ServerIntrospectorProperties.class})
public class RibbonAutoConfiguration {
 @Bean
    public SpringClientFactory springClientFactory() {
        SpringClientFactory factory = new SpringClientFactory();
        factory.setConfigurations(this.configurations);
        return factory;
   }
    
    @Bean
    @ConditionalOnMissingBean({LoadBalancerClient.class})
    public LoadBalancerClient loadBalancerClient() {
        return new RibbonLoadBalancerClient(this.springClientFactory());
   }
    //省略
}

通过 RibbonAutoConfiguration 引入了 LoadBalancerAutoConfiguration 配置类

4. 负载均衡调用
@Configuration
@ConditionalOnClass({RestTemplate.class})
@ConditionalOnBean({LoadBalancerClient.class})
@EnableConfigurationProperties({LoadBalancerRetryProperties.class})
public class LoadBalancerAutoConfiguration {
    @LoadBalanced
    @Autowired(
        required = false
   )
    private List<RestTemplate> restTemplates = Collections.emptyList();
    @Autowired(
        required = false
   )
    private List<LoadBalancerRequestTransformer> transformers =
Collections.emptyList();
 public LoadBalancerAutoConfiguration() {
   }
    @Bean
    public SmartInitializingSingleton
loadBalancedRestTemplateInitializerDeprecated(ObjectProvider<List<RestTemplateCu
stomizer>> restTemplateCustomizers) {
        return () -> {
            restTemplateCustomizers.ifAvailable((customizers) -> {
                Iterator var2 = this.restTemplates.iterator();
                while(var2.hasNext()) {
                    RestTemplate restTemplate = (RestTemplate)var2.next();
                    Iterator var4 = customizers.iterator();
                    while(var4.hasNext()) {
                        RestTemplateCustomizer customizer = (RestTemplateCustomizer)var4.next();
                        customizer.customize(restTemplate);
                   }
               }
           });
       };
   }
    @Bean
    @ConditionalOnMissingBean
    public LoadBalancerRequestFactory
loadBalancerRequestFactory(LoadBalancerClient loadBalancerClient) {
        return new LoadBalancerRequestFactory(loadBalancerClient, 
this.transformers);
   }
    @Configuration
    @ConditionalOnClass({RetryTemplate.class})
    public static class RetryInterceptorAutoConfiguration {
        public RetryInterceptorAutoConfiguration() {
       }
        @Bean
        @ConditionalOnMissingBean
        public RetryLoadBalancerInterceptor ribbonInterceptor(LoadBalancerClient
loadBalancerClient, LoadBalancerRetryProperties properties, 
LoadBalancerRequestFactory requestFactory, LoadBalancedRetryFactory
loadBalancedRetryFactory) {
            return new RetryLoadBalancerInterceptor(loadBalancerClient, 
properties, requestFactory, loadBalancedRetryFactory);
       }
        @Bean
        @ConditionalOnMissingBean
        public RestTemplateCustomizer
restTemplateCustomizer(RetryLoadBalancerInterceptor loadBalancerInterceptor) {
            return (restTemplate) -> {
                List<ClientHttpRequestInterceptor> list = new
ArrayList(restTemplate.getInterceptors());
                list.add(loadBalancerInterceptor);
                restTemplate.setInterceptors(list);
           };
       }
   }
    @Configuration
    @ConditionalOnClass({RetryTemplate.class})
    public static class RetryAutoConfiguration {
        public RetryAutoConfiguration() {
       }
        @Bean
        @ConditionalOnMissingBean
        public LoadBalancedRetryFactory loadBalancedRetryFactory() {
            return new LoadBalancedRetryFactory() {
           };
       }
   }
    @Configuration
   
@ConditionalOnMissingClass({"org.springframework.retry.support.RetryTemplate"})
    static class LoadBalancerInterceptorConfig {
        LoadBalancerInterceptorConfig() {
       }
        @Bean
        public LoadBalancerInterceptor ribbonInterceptor(LoadBalancerClient
loadBalancerClient, LoadBalancerRequestFactory requestFactory) {
            return new LoadBalancerInterceptor(loadBalancerClient, 
requestFactory);
       }
        @Bean
        @ConditionalOnMissingBean
        public RestTemplateCustomizer
restTemplateCustomizer(LoadBalancerInterceptor loadBalancerInterceptor) {
            return (restTemplate) -> {
                List<ClientHttpRequestInterceptor> list = new
ArrayList(restTemplate.getInterceptors());
                list.add(loadBalancerInterceptor);
                restTemplate.setInterceptors(list);
           };
       }
   }
}

在该自动化配置类中,主要做了下面三件事:

  • 创建了一个 LoadBalancerInterceptor 的Bean,用于实现对客户端发起请求时进行拦截,以实现客户端负载均衡。
  • 创建了一个 RestTemplateCustomizer 的Bean,用于给 RestTemplate 增加LoadBalancerInterceptor 拦截器。
  • 维护了一个被 @LoadBalanced 注解修饰的 RestTemplate 对象列表,并在这里进行初始化,通过调用 RestTemplateCustomizer 的实例来给需要客户端负载均衡的 RestTemplate 增加LoadBalancerInterceptor 拦截器。
public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor {
    private LoadBalancerClient loadBalancer;
    private LoadBalancerRequestFactory requestFactory;
    public LoadBalancerInterceptor(LoadBalancerClient loadBalancer, 
LoadBalancerRequestFactory requestFactory) {
        this.loadBalancer = loadBalancer;
        this.requestFactory = requestFactory;
   }
    public LoadBalancerInterceptor(LoadBalancerClient loadBalancer) {
        this(loadBalancer, new LoadBalancerRequestFactory(loadBalancer));
   }
    public ClientHttpResponse intercept(HttpRequest request, byte[] body, 
ClientHttpRequestExecution execution) throws IOException {
        URI originalUri = request.getURI();
        String serviceName = originalUri.getHost();
        Assert.state(serviceName != null, "Request URI does not contain a valid 
hostname: " + originalUri);
        return (ClientHttpResponse)this.loadBalancer.execute(serviceName, 
this.requestFactory.createRequest(request, body, execution));
   }
}

通过源码以及之前的自动化配置类,我们可以看到在拦截器中注入了 LoadBalancerClient 的实现。当一个被 @LoadBalanced 注解修饰的 RestTemplate 对象向外发起HTTP请求时,会被LoadBalancerInterceptor 类的 intercept 函数所拦截。由于我们在使用RestTemplate时候采用了服务名作为host,所以直接从 HttpRequest 的URI对象中通过getHost()就可以拿到服务名,然后调用execute 函数去根据服务名来选择实例并发起实际的请求。

分析到这里, LoadBalancerClient 还只是一个抽象的负载均衡器接口,所以我们还需要找到它的具体实现类来进一步分析。通过查看ribbon的源码,我们可以很容易的在org.springframework.cloud.netflix.ribbon 包下找到对应的实现类:RibbonLoadBalancerClient 。

public class RibbonLoadBalancerClient implements LoadBalancerClient {
    public ServiceInstance choose(String serviceId) {
        Server server = this.getServer(serviceId);
        return server == null ? null : new
RibbonLoadBalancerClient.RibbonServer(serviceId, server, this.isSecure(server, 
serviceId), this.serverIntrospector(serviceId).getMetadata(server));
   }
    
    public <T> T execute(String serviceId, LoadBalancerRequest<T> request) 
throws IOException {
        ILoadBalancer loadBalancer = this.getLoadBalancer(serviceId);
        Server server = this.getServer(loadBalancer);
        if (server == null) {
            throw new IllegalStateException("No instances available for " +
serviceId);
       } else {
            RibbonLoadBalancerClient.RibbonServer ribbonServer = new
RibbonLoadBalancerClient.RibbonServer(serviceId, server, this.isSecure(server, 
serviceId), this.serverIntrospector(serviceId).getMetadata(server));
            return this.execute(serviceId, ribbonServer, request);
       }
   }
    
    protected Server getServer(String serviceId) {
        return this.getServer(this.getLoadBalancer(serviceId));
   }
    
    protected Server getServer(ILoadBalancer loadBalancer) {
        return loadBalancer == null ? null : 
loadBalancer.chooseServer("default");
   }
    
    protected ILoadBalancer getLoadBalancer(String serviceId) {
        return this.clientFactory.getLoadBalancer(serviceId);
   }
    
    //省略...
}
  • ServiceInstance choose(String serviceId):根据传入的服务id,从负载均衡器中为指定的服务选择一个服务实例。
  • T execute(String serviceId, LoadBalancerRequest request):根据传入的服务id,指定的负载均衡器中的服务实例执行请求。
  • T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest request):根据传入的服务实例,执行请求。

从 RibbonLoadBalancerClient 代码可以看出,实际负载均衡的是通过 ILoadBalancer 来实现的

@Bean
@ConditionalOnMissingBean
public ILoadBalancer ribbonLoadBalancer(IClientConfig config, ServerList<Server>
serverList, ServerListFilter<Server> serverListFilter, IRule rule, IPing ping, 
ServerListUpdater serverListUpdater) {
    return (ILoadBalancer)(this.propertiesFactory
   .isSet(ILoadBalancer.class, this.name) ? (ILoadBalancer)this.propertiesFactory
   .get(ILoadBalancer.class, config, this.name) : new
ZoneAwareLoadBalancer(config, rule, ping, serverList, serverListFilter, 
serverListUpdater));
}

9. 服务调用Feign入门

之前使用的RestTemplate实现REST API调用,代码大致如下:

@GetMapping("/buy/{id}")
public Product order() {
    Product product = restTemplate.getForObject("http://shop-service-product/product/1", Product.class);
    return product; }

由代码可知,我们是使用拼接字符串的方式构造URL的,该URL只有一个参数。但是,在现实中,URL中往往含有多个参数。

1. Feign简介

Feign是Netflflix开发的声明式,模板化的HTTP客户端,其灵感来自Retrofifit,JAXRS-2.0以及WebSocket.

  • Feign可帮助我们更加便捷,优雅的调用HTTP API。
  • 在SpringCloud中,使用Feign非常简单——创建一个接口,并在接口上添加一些注解,代码就完成了。
  • Feign支持多种注解,例如Feign自带的注解或者JAX-RS注解等。
  • SpringCloud对Feign进行了增强,使Feign支持了SpringMVC注解,并整合了Ribbon和Eureka,从而让Feign的使用更加方便。

2. 基于Feign的服务调用

  1. 依赖导入
<!--springcloud整合的openFeign-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
  1. 启动类添加Feign的支持
@SpringBootApplication(scanBasePackages="cn.itcast.order")
@EntityScan("cn.itcast.entity")
@EnableFeignClients
public class OrderApplication {
 public static void main(String[] args) {
 SpringApplication.run(OrderApplication.class, args);
 }
}

通过@EnableFeignClients注解开启Spring Cloud Feign的支持功能

  1. 启动类激活FeignClient

创建一个Feign接口,此接口是在Feign中调用微服务的核心接口

在服务消费者 shop_service_order 添加一个 ProductFeginClient 接口

//指定需要调用的微服务名称 @FeignClient(name="shop-service-product")
public interface ProductFeginClient { 
    //调用的请求路径 
    @RequestMapping(value = "/product/{id}",method = RequestMethod.GET) 
    public Product findById(@PathVariable("id") Long id); 
}
  • 定义各参数绑定时,@PathVariable、@RequestParam、@RequestHeader等可以指定参数属性,在Feign中绑定参数必须通过value属性来指明具体的参数名,不然会抛出异常
  • @FeignClient:注解通过name指定需要调用的微服务的名称,用于创建Ribbon的负载均衡器。所以Ribbon会把 shop-service-product 解析为注册中心的服务。
  1. 配置请求提供者的调用接口
@RestController
@RequestMapping("/order")
public class OrderController {
 
    @Autowired
    private ProductFeignClient productFeignClient;
 
    @RequestMapping(value = "/buy/{id}",method = RequestMethod.GET)
    public Product findById(@PathVariable Long id) {
        Product product = null;
        product = productFeignClient.findById(id);
        return product;
    }
}

3. Feign和Ribbon的联系

Ribbon是一个基于 HTTP 和 TCP 客户端 的负载均衡的工具。它可以 在客户端 配置RibbonServerList(服务端列表),使用 HttpClient 或 RestTemplate 模拟http请求,步骤相当繁琐。

Feign 是在 Ribbon的基础上进行了一次改进,是一个使用起来更加方便的 HTTP 客户端。采用接口的方式, 只需要创建一个接口,然后在上面添加注解即可 ,将需要调用的其他服务的方法定义成抽象方法即可, 不需要自己构建http请求。然后就像是调用自身工程的方法调用,而感觉不到是调用远程方法,使得编写客户端变得非常容易

4. 负载均衡

Feign中本身已经集成了Ribbon依赖和自动配置,因此我们不需要额外引入依赖,也不需要再注册RestTemplate 对象。另外,我们可以像之前那样去配置Ribbon,可以通过 ribbon.xx 来进行全局配置。也可以通过 服务名.ribbon.xx 来对指定服务配置

10. 服务调用Feign高级

1. Feign的配置

从Spring Cloud Edgware开始,Feign支持使用属性自定义Feign。对于一个指定名称的FeignClient(例如该Feign Client的名称为 feignName ),Feign支持如下配置项

feign:
    client:
        config:
            feignName: ##定义FeginClient的名称
                connectTimeout: 5000 # 相当于Request.Options
                readTimeout: 5000 # 相当于Request.Options
                # 配置Feign的日志级别,相当于代码配置方式中的Logger
                loggerLevel: full
                # Feign的错误解码器,相当于代码配置方式中的ErrorDecoder
                errorDecoder: com.example.SimpleErrorDecoder
                # 配置重试,相当于代码配置方式中的Retryer
                retryer: com.example.SimpleRetryer
                # 配置拦截器,相当于代码配置方式中的RequestInterceptor
                requestInterceptors:
                    - com.example.FooRequestInterceptor
                    - com.example.BarRequestInterceptor
                decode404: false
  • feignName:FeginClient的名称
  • connectTimeout : 建立链接的超时时长
  • readTimeout : 读取超时时长
  • loggerLevel: Fegin的日志级别
  • errorDecoder :Feign的错误解码器
  • retryer : 配置重试
  • requestInterceptors : 添加请求拦截器
  • decode404 : 配置熔断不处理404异常

2. 请求压缩

Spring Cloud Feign 支持对请求和响应进行GZIP压缩,以减少通信过程中的性能损耗。通过下面的参数即可开启请求与响应的压缩功能:

feign: 
  compression: 
    request: 
      enabled: true # 开启请求压缩 
    response: 
      enabled: true # 开启响应压缩

同时,我们也可以对请求的数据类型,以及触发压缩的大小下限进行设置:

feign: 
  compression: 
    request: 
      enabled: true # 开启请求压缩 
      mime-types: text/html,application/xml,application/json # 设置压缩的数据类型 
      min-request-size: 2048 # 设置触发压缩的大小下限

注:上面的数据类型、压缩大小下限均为默认值。

3. 日志级别

在开发或者运行阶段往往希望看到Feign请求过程的日志记录,默认情况下Feign的日志是没有开启的。要想用属性配置方式来达到日志效果,只需在 application.yml 中添加如下内容即可:

feign:
  client:
    config:
      service-product:
        loggerLevel: FULL
logging:
  level:
    cn.itcast.order.fegin.ProductFeginClient: debug
  • logging.level.xx : debug : Feign日志只会对日志级别为debug的做出响应

  • feign.client.config.shop-service-product.loggerLevel : 配置Feign的日志Feign有四种

    日志级别:

    • NONE【性能最佳,适用于生产】:不记录任何日志(默认值)
    • BASIC【适用于生产环境追踪问题】:仅记录请求方法、URL、响应状态代码以及执行时间
    • HEADERS:记录BASIC级别的基础上,记录请求和响应的header。
    • FULL【比较适用于开发及测试环境定位问题】:记录请求和响应的header、body和元数据。
      在这里插入图片描述

4. 源码分析

通过上面的使用过程,@EnableFeignClients和@FeignClient两个注解就实现了Feign的功能,那我们从@EnableFeignClients注解开始分析Feign的源码

  1. EnableFeignClients注解
@Retention(RetentionPolicy.RUNTIME) 
@Target({ElementType.TYPE}) 
@Documented 
@Import({FeignClientsRegistrar.class}) 
public @interface EnableFeignClients { 
    //略 
}

通过 @EnableFeignClients 引入了FeignClientsRegistrar客户端注册类

  1. FeignClientsRegistrar注册类
class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware { 
    public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { 
        this.registerDefaultConfiguration(metadata, registry);                                 this.registerFeignClients(metadata, registry); 
    } 
}

通过其类结构可知,由于实现了ImportBeanDefifinitionRegistrar接口,那么在registerBeanDefifinitions()中就会解析和注册BeanDefifinition,主要注册的对象类型有两种:

  • 注册缺省配置的配置信息
  • 注册那些添加了@FeignClient的类或接口 : 这也是重点
public void registerFeignClients(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { 
    ClassPathScanningCandidateComponentProvider scanner = this.getScanner();               scanner.setResourceLoader(this.resourceLoader); 
    Map<String, Object> attrs = metadata.getAnnotationAttributes(EnableFeignClients.class.getName());                     AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(FeignClient.class); 
    Class<?>[] clients = attrs == null ? null : (Class[]) ((Class[])attrs.get("clients")); 
    Object basePackages; 
    if (clients != null && clients.length != 0) { 
        final Set<String> clientClasses = new HashSet(); 
        basePackages = new HashSet(); Class[] var9 = clients; 
        int var10 = clients.length; 
        for(int var11 = 0; var11 < var10; ++var11) { 
            Class<?> clazz = var9[var11];                                                         ((Set)basePackages).add(ClassUtils.getPackageName(clazz));                           clientClasses.add(clazz.getCanonicalName()); 
        }
        AbstractClassTestingTypeFilter filter = new AbstractClassTestingTypeFilter() { 
            protected boolean match(ClassMetadata metadata) {
            String cleaned = metadata.getClassName().replaceAll("\\$", "."); 
                return clientClasses.contains(cleaned); 
            } 
        };
        scanner.addIncludeFilter(new FeignClientsRegistrar.AllTypeFilter(Arrays.asList(filter, annotationTypeFilter))); 
    } else { 
        scanner.addIncludeFilter(annotationTypeFilter); 
        basePackages = this.getBasePackages(metadata); 
    }
    Iterator var17 = ((Set)basePackages).iterator(); 
    while(var17.hasNext()) { 
        String basePackage = (String)var17.next(); 
        Set<BeanDefinition> candidateComponents = scanner.findCandidateComponents(basePackage); 
        Iterator var21 = candidateComponents.iterator(); 
        while(var21.hasNext()) { 
            BeanDefinition candidateComponent = (BeanDefinition)var21.next(); 
            if (candidateComponent instanceof AnnotatedBeanDefinition) {                               AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition)candidateComponent;                                                           AnnotationMetadata annotationMetadata = beanDefinition.getMetadata(); Assert.isTrue(annotationMetadata.isInterface(), "@FeignClient can only be specified on an interface"); 
                 Map<String, Object> attributes = annotationMetadata.getAnnotationAttributes(FeignClient.class.getCanonicalName()) ;                    String name = this.getClientName(attributes);                                        this.registerClientConfiguration(registry, name, attributes.get("configuration"));                                                                      this.registerFeignClient(registry, annotationMetadata, attributes);    	      } 
        } 
    } 
}

该方法主要是扫描类路径,对所有的FeignClient生成对应的 BeanDefinitio 。同时又调用了registerClientConfiguration 注册配置的方法,这里是第二处调用。这里主要是将扫描的目录下,每个项目的配置类加载的容器当中。调用 registerFeignClient 注册对象

  1. 注册FeignClient对象
private void registerFeignClient(BeanDefinitionRegistry registry, AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
        // 1.获取类名称,也就是本例中的FeignService接口
        String className = annotationMetadata.getClassName();
        // 2.BeanDefinitionBuilder的主要作用就是构建一个AbstractBeanDefinition
        // AbstractBeanDefinition类最终被构建成一个BeanDefinitionHolder
        // 然后注册到Spring中
        // 注意:beanDefinition类为FeignClientFactoryBean,故在Spring获取类的时候实际返回 的是
        // FeignClientFactoryBean类
        BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(FeignClientFactoryBean.class);
        validate(attributes);
        // 3.添加FeignClientFactoryBean的属性,
        // 这些属性也都是我们在@FeignClient中定义的属性
        definition.addPropertyValue("url", getUrl(attributes));
        definition.addPropertyValue("path", getPath(attributes));
        String name = getName(attributes); definition.addPropertyValue("name", name);
        definition.addPropertyValue("type", className);
        definition.addPropertyValue("decode404", attributes.get("decode404"));
        definition.addPropertyValue("fallback", attributes.get("fallback"));
        definition.addPropertyValue("fallbackFactory", attributes.get("fallbackFactory"));
        definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
        // 4.设置别名 name就是我们在@FeignClient中定义的name属性
        String alias = name + "FeignClient";
        AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();
        boolean primary = (Boolean)attributes.get("primary");// has a default, won't be null
        beanDefinition.setPrimary(primary);//
        String qualifier = getQualifier(attributes);
        if (StringUtils.hasText(qualifier)) {
            alias = qualifier;
        }
        // 5.定义BeanDefinitionHolder,
        // 在本例中 名称为FeignService,类为FeignClientFactoryBean
        BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className, new String[] { alias });
        BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry); 
    }

通过分析可知:我们最终是向Spring中注册了一个bean,bean的名称就是类或接口的名称(也就是本例中的FeignService),bean的实现类是FeignClientFactoryBean,其属性设置就是我们在@FeignClient中定义的属性。那么下面我们在Controller中对FeignService的的引入,实际就是引入了FeignClientFactoryBean

  1. FeignClientFactoryBean类

对@EnableFeignClients注解的源码进行了分析,了解到其主要作用就是把带有@FeignClient注解的类或接口用FeignClientFactoryBean类注册到Spring中。

class FeignClientFactoryBean implements FactoryBean<Object>, InitializingBean, ApplicationContextAware {
        public Object getObject() throws Exception {
            return this.getTarget();
        }//略 .
    }

通过 FeignClientFactoryBean 类结构可以发现其实现了FactoryBean类,那么当从ApplicationContext中获取该bean的时候,实际调用的是其getObject()方法。返回调用getTarget()方法

<T> T getTarget() {
        FeignContext context = (FeignContext) this.applicationContext.getBean(FeignContext.class);
        Builder builder = this.feign(context);
        if (!StringUtils.hasText(this.url)) {
            if (!this.name.startsWith("http")) {
                this.url = "http://" + this.name;
            } else {
                this.url = this.name;
            }
            this.url = this.url + this.cleanPath();
            return this.loadBalance(builder, context, new HardCodedTarget(this.type, this.name, this.url));
        } else {
            if (StringUtils.hasText(this.url) && !this.url.startsWith("http")) {
                this.url = "http://" + this.url;
            }
            String url = this.url + this.cleanPath();
            Client client = (Client) this.getOptional(context, Client.class);
            if (client != null) {
                if (client instanceof LoadBalancerFeignClient) {
                    client = ((LoadBalancerFeignClient) client).getDelegate();
                }
                builder.client(client);
            }
            Targeter targeter = (Targeter) this.get(context, Targeter.class);
            return targeter.target(this, builder, context, new HardCodedTarget(this.type, this.name, url));
        }
    }
  • FeignClientFactoryBean实现了FactoryBean的getObject、getObjectType、isSingleton方法;实现了InitializingBean的afterPropertiesSet方法;实现了ApplicationContextAware的setApplicationContext方法

  • getObject调用的是getTarget方法,它从applicationContext取出FeignContext,然后构造Feign.Builder并设置了logger、encoder、decoder、contract,之后通过confifigureFeign根据FeignClientProperties来进一步配置Feign.Builder的retryer、errorDecoder、 request.Options、requestInterceptors、queryMapEncoder、decode404

  • 初步配置完Feign.Builder之后再判断是否需要loadBalance,如果需要则通过loadBalance方法来设置,不需要则在Client是LoadBalancerFeignClient的时候进行unwrap

  1. 发送请求

由上可知,FeignClientFactoryBean.getObject()具体返回的是一个代理类,具体为FeignInvocationHandler

static class FeignInvocationHandler implements InvocationHandler {
        private final Target target;
        private final Map<Method, MethodHandler> dispatch;

        FeignInvocationHandler(Target target, Map<Method, MethodHandler> dispatch) {
            this.target = (Target) Util.checkNotNull(target, "target", new Object[0]);
            this.dispatch = (Map) Util.checkNotNull(dispatch, "dispatch for %s", new Object[]{target});
        }

        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            if (!"equals".equals(method.getName())) {
                if ("hashCode".equals(method.getName())) {
                    return this.hashCode();
                } else {
                    return "toString".equals(method.getName()) ? this.toString() : ((MethodHandler) this.dispatch.get(method)).invoke(args);
                }
            } else {
                try {
                    Object otherHandler = args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null;
                    return this.equals(otherHandler);
                } catch (IllegalArgumentException var5) {
                    return false;
                }
            }
        }

        public boolean equals(Object obj) {
            if (obj instanceof ReflectiveFeign.FeignInvocationHandler) {
                ReflectiveFeign.FeignInvocationHandler other = (ReflectiveFeign.FeignInvocationHandler) obj;
                return this.target.equals(other.target);
            } else {
                return false;
            }
        }

        public int hashCode() {
            return this.target.hashCode();
        }

        public String toString() {
            return this.target.toString();
        }
    }
  • FeignInvocationHandler实现了InvocationHandler,是动态代理的代理类。
  • 当执行非Object方法时进入到this.dispatch.get(method)).invoke(args)
  • dispatch是一个map集合,根据方法名称获取MethodHandler。具体实现类为SynchronousMethodHandler
final class SynchronousMethodHandler implements MethodHandler {
        public Object invoke(Object[] argv) throws Throwable {
            RequestTemplate template = this.buildTemplateFromArgs.create(argv);
            Retryer retryer = this.retryer.clone();
            while (true) {
                try {
                    return this.executeAndDecode(template);
                } catch (RetryableException var8) { //略 
                }
            }
        }

        Object executeAndDecode(RequestTemplate template) throws Throwable {
            Request request = this.targetRequest(template);
            if (this.logLevel != Level.NONE) {
                this.logger.logRequest(this.metadata.configKey(), this.logLevel, request);
            }
            long start = System.nanoTime();
            Response response;
            try {
                response = this.client.execute(request, this.options);
            } catch (IOException var15) { //略 
            }
        }
    }
  • SynchronousMethodHandler内部创建了一个RequestTemplate对象,是Feign中的请求模板对象。内部封装了一次请求的所有元数据。
  • retryer中定义了用户的重试策略。
  • 调用executeAndDecode方法通过client完成请求处理,client的实现类是LoadBalancerFeignClient

11. 微服务架构的高并发问题

通过注册中心已经实现了微服务的服务注册和服务发现,并且通过Ribbon实现了负载均衡,已经借助Feign可以优雅的进行微服务调用。那么我们编写的微服务的性能怎么样呢,是否存在问题呢?

1. 性能工具Jmetter

Apache JMeter是Apache组织开发的基于Java的压力测试工具。用于对软件做压力测试,它最初被设计用于Web应用测试,但后来扩展到其他测试领域。 它可以用于测试静态和动态资源,例如静态文件、Java 小服务程序、CGI 脚本、Java 对象、数据库、FTP 服务器, 等等。JMeter 可以用于对服务器、网络或对象模拟巨大的负载,来自不同压力类别下测试它们的强度和分析整体性能。另外JMeter能够对应用程序做功能/回归测试,通过创建带有断言的脚本来验证你的程序返回了你期望的结果。为了最大限度的灵活性,JMeter允许使用正则表达式创建断言。

1. 安装Jmetter

Jmetter安装十分简单,使用资料中的 apache-jmeter-2.13.zip 完整压缩包,解压找到安装目录下bin/jmeter.bat 以管理员身份启动即可
在这里插入图片描述

2. 配置Jmetter
  1. 创建新的测试计划
    在这里插入图片描述

  2. 测试计划下创建发起请求的线程组
    在这里插入图片描述

  • 可以配置请求的线程数
  • 以及每个请求发送的请求次数
  1. 创建http请求模板

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jsmvc5ri-1641129771299)(C:\Users\14125\AppData\Roaming\Typora\typora-user-images\image-20211225214329006.png)]

  1. 配置测试的接口信息
    在这里插入图片描述

2. 系统负载过高存在的问题

1. 问题分析

在微服务架构中,我们将业务拆分成一个个的服务,服务与服务之间可以相互调用,由于网络原因或者自身的原因,服务并不能保证服务的100%可用,如果单个服务出现问题,调用这个服务就会出现网络延迟,此时若有大量的网络涌入,会形成任务累计,导致服务瘫痪。

在SpringBoot程序中,默认使用内置tomcat作为web服务器。单tomcat支持最大的并发请求是有限的,如果某一接口阻塞,待执行的任务积压越来越多,那么势必会影响其他接口的调用。

2. 线程池的形式实现服务隔离
  1. 配置坐标

为了方便实现线以线程池的形式完成资源隔离,需要引入如下依赖

<dependency> 
    <groupId>com.netflix.hystrix</groupId> 
    <artifactId>hystrix-metrics-event-stream</artifactId> 
    <version>1.5.12</version> 
</dependency> 
<dependency> 
    <groupId>com.netflix.hystrix</groupId> 
    <artifactId>hystrix-javanica</artifactId> 
    <version>1.5.12</version> 
</dependency>
  1. 配置线程池
public class OrderCommand extends HystrixCommand<String> {
        private RestTemplate restTemplate;
        private Long id;

        public OrderCommand(RestTemplate restTemplate, Long id) {
            super(setter());
            this.restTemplate = restTemplate;
            this.id = id;
        }

        private static Setter setter() {
            // 服务分组
            HystrixCommandGroupKey groupKey = HystrixCommandGroupKey.Factory.asKey("order_product");
            // 服务标识
            HystrixCommandKey commandKey = HystrixCommandKey.Factory.asKey("product");
            // 线程池名称
            HystrixThreadPoolKey threadPoolKey = HystrixThreadPoolKey.Factory.asKey("order_product_pool");
            /** 线程池配置
             * withCoreSize : 线程池大小为10
             * withKeepAliveTimeMinutes: 线程存活时间15秒
             * withQueueSizeRejectionThreshold :队列等待的阈值为100,超过100执行拒绝 策略
             */
            HystrixThreadPoolProperties.Setter threadPoolProperties = HystrixThreadPoolProperties.Setter().withCoreSize(50).withKeepAliveTimeMinutes(15).withQueueSizeRejectionThreshold(100);
            // 命令属性配置Hystrix 开启超时
            HystrixCommandProperties.Setter commandProperties = HystrixCommandProperties.Setter()
                    // 采用线程池方式实现服务隔离
                    .withExecutionIsolationStrategy(HystrixCommandProperties.ExecutionIsolationStrat egy.THREAD)
                    // 禁止
                    .withExecutionTimeoutEnabled(false);
            return HystrixCommand.Setter.withGroupKey(groupKey).andCommandKey(commandKey).andThreadPoolKey(threadPoolKey).andThreadPoolPropertiesDefaults(threadPoolProperties).andCommandPropertiesDefaults(commandProperties);
        }

        @Override
        protected String run() throws Exception {
            return restTemplate.getForObject("http://shop-service- product/product/" + id, String.class);
        }

        @Override
        protected String getFallback() {
            return "熔断降级";
        }
    }
  1. 配置调用
@Autowired
    private RestTemplate restTemplate;

    @GetMapping("/buy/{id}")
    public String order(@PathVariable Long id) throws ExecutionException, InterruptedException, TimeoutException {
        return new OrderCommand(restTemplate, id).execute();
    }

12. 服务熔断Hystrix入门

1. 服务容错的核心知识

1. 雪崩效应

在微服务架构中,一个请求需要调用多个服务是非常常见的。如客户端访问A服务,而A服务需要调用B服务,B服务需要调用C服务,由于网络原因或者自身的原因,如果B服务或者C服务不能及时响应,A服务将处于阻塞状态,直到B服务C服务响应。此时若有大量的请求涌入,容器的线程资源会被消耗完毕,导致服务瘫痪。服务与服务之间的依赖性,故障会传播,造成连锁反应,会对整个微服务系统造成灾难性的严重后果,这就是服务故障的“雪崩”效应。
在这里插入图片描述

雪崩是系统中的蝴蝶效应导致其发生的原因多种多样,有不合理的容量设计,或者是高并发下某一个方法响应变慢,亦或是某台机器的资源耗尽。从源头上我们无法完全杜绝雪崩源头的发生,但是雪崩的根本原因来源于服务之间的强依赖,所以我们可以提前评估,做好熔断,隔离,限流。

2. 服务隔离

顾名思义,它是指将系统按照一定的原则划分为若干个服务模块,各个模块之间相对独立,无强依赖。当有故障发生时,能将问题和影响隔离在某个模块内部,而不扩散风险,不波及其它模块,不影响整体的系统服务。

3. 熔断降级

熔断这一概念来源于电子工程中的断路器(Circuit Breaker)。在互联网系统中,当下游服务因访问压力过大而响应变慢或失败,上游服务为了保护系统整体的可用性,可以暂时切断对下游服务的调用。这种牺牲局部,保全整体的措施就叫做熔断。
在这里插入图片描述

所谓降级,就是当某个服务熔断之后,服务器将不再被调用,此时客户端可以自己准备一个本地的fallback回调,返回一个缺省值。 也可以理解为兜底

4. 服务限流

限流可以认为服务降级的一种,限流就是限制系统的输入和输出流量已达到保护系统的目的。一般来说系统的吞吐量是可以被测算的,为了保证系统的稳固运行,一旦达到的需要限制的阈值,就需要限制流量并采取少量措施以完成限制流量的目的。比方:推迟解决,拒绝解决,或者者部分拒绝解决等等。

2. Hystrix介绍

在这里插入图片描述

Hystrix是由Netflflix开源的一个延迟和容错库,用于隔离访问远程系统、服务或者第三方库,防止级联失败,从而提升系统的可用性与容错性。Hystrix主要通过以下几点实现延迟和容错。

  • 包裹请求:使用HystrixCommand包裹对依赖的调用逻辑,每个命令在独立线程中执行。这使用了设计模式中的“命令模式”。
  • 跳闸机制:当某服务的错误率超过一定的阈值时,Hystrix可以自动或手动跳闸,停止请求该服务一段时间。
  • 资源隔离:Hystrix为每个依赖都维护了一个小型的线程池(或者信号量)。如果该线程池已满,发往该依赖的请求就被立即拒绝,而不是排队等待,从而加速失败判定。
  • 监控:Hystrix可以近乎实时地监控运行指标和配置的变化,例如成功、失败、超时、以及被拒绝的请求等。
  • 回退机制:当请求失败、超时、被拒绝,或当断路器打开时,执行回退逻辑。回退逻辑由开发人员自行提供,例如返回一个缺省值。
  • 自我修复:断路器打开一段时间后,会自动进入“半开”状态。

3. Rest实现服务熔断

  1. 配置依赖

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
    </dependency>
    
  2. 开启熔断

    @EntityScan("cn.itcast.entity") 
    //@EnableCircuitBreaker //开启熔断器 
    //@SpringBootApplication
    @SpringCloudApplication public class OrderApplication {
        //创建RestTemplate对象 
        @Bean 
        @LoadBalanced 
        public RestTemplate restTemplate() { 
            return new RestTemplate(); 
        }
        public static void main(String[] args) { SpringApplication.run(OrderApplication.class, args);} 
    }
    

    可以看到,我们类上的注解越来越多,在微服务中,经常会引入上面的三个注解,于是Spring就提供了一个组合注解:@SpringCloudApplication

  3. 配置熔断降级业务逻辑

    /**
     * 降级方法
     *  和需要收到保护的方法的返回值一致
     *  方法参数一致
     */
    public Product orderFallBack(Long id) {
       Product product = new Product();
       product.setProductName("触发降级方法");
       return product;
    }
    

    4.在需要保护的接口上使用@HystrixCommand配置

    /**
     * 使用注解配置熔断保护
     *     fallbackmethod : 配置熔断之后的降级方法
     */
    @HystrixCommand(fallbackMethod = "orderFallBack")
    @RequestMapping(value = "/buy/{id}",method = RequestMethod.GET)
    public Product findById(@PathVariable Long id) {
       return restTemplate.getForObject("http://service-product/product/1",Product.class);
    }
    

    有代码可知,为 findProduct 方法编写一个回退方法fifindProductFallBack,该方法与 findProduct 方法具有相同的参数与返回值类型,该方法返回一个默认的错误信息。在 Product 方法上,使用注解@HystrixCommand的fallbackMethod属性,指定熔断触发的降级方法是 findProductFallBack

我们刚才把fallback写在了某个业务方法上,如果这样的方法很多,那岂不是要写很多。所以我们可以把Fallback配置加在类上,实现默认fallback:
在这里插入图片描述

超时设置
hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 3000 #默认的连接超时时间1秒,若1秒没有返回数据,自动的触发降级逻辑

4. Feign实现服务熔断

SpringCloud Fegin默认已为Feign整合了hystrix,所以添加Feign依赖后就不用在添加hystrix

  1. 修改application.yml在Fegin中开启hystrix

在Feign中已经内置了hystrix,但是默认是关闭的需要在工程的 application.yml 中开启对hystrix的支持

#HEADERS : 在BASIC的基础上,记录请求和响应头信息   FULL : 记录所有
feign:
  #开启对hystrix的支持
  hystrix:
    enabled: true
  1. 配置FeignClient接口的实现类

基于Feign实现熔断降级,那么降级方法需要配置到FeignClient接口的实现类中

//实现自定义的ProductFeginClient接口 在接口实现类中编写熔断降级方法
@Component
public class ProductFeignClientCallBack implements ProductFeignClient {
 
   /**
    * 熔断降级的方法
    */
   public Product findById(Long id) {
      Product product = new Product();
      product.setProductName("feign调用触发熔断降级方法");
      return product;
   }
}
  1. 修改feignClient接口添加降级方法的支持
/**
 * 声明需要调用的微服务名称
 *  @FeignClient
 *      * name : 服务提供者的名称
 *      * fallback : 配置熔断发生降级方法
 *                  实现类
 */
@FeignClient(name="service-product",fallback = ProductFeignClientCallBack.class)
public interface ProductFeignClient {
 
   /**
    * 配置需要调用的微服务接口
    */
   @RequestMapping(value="/product/{id}",method = RequestMethod.GET)
   public Product findById(@PathVariable("id") Long id);
}

13. 服务熔断Hystrix高级

1. Hystrix的监控平台

除了实现容错功能,Hystrix还提供了近乎实时的监控,HystrixCommand和HystrixObservableCommand在执行时,会生成执行结果和运行指标。比如每秒的请求数量,成功数量等。这些状态会暴露在Actuator提供的/health端点中。只需为项目添加 spring-boot-actuator 依赖,重启项目,访问http://localhost:9001/actuator/hystrix.stream ,即可看到实时的监控数据。

1. 搭建Hystrix DashBoard监控

刚刚讨论了Hystrix的监控,但访问/hystrix.stream接口获取的都是已文字形式展示的信息。很难通过文字直观的展示系统的运行状态,所以Hystrix官方还提供了基于图形化的DashBoard(仪表板)监控平台。Hystrix仪表板可以显示每个断路器(被@HystrixCommand注解的方法)的状态。

  1. 导入依赖
<!--引入hystrix的监控信息-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>
  1. 在启动类使用@EnableHystrixDashboard注解激活仪表盘项目
//激活hystrix
@EnableCircuitBreaker
public class FeignOrderApplication {}
  1. 访问测试
    在这里插入图片描述

输入监控断点展示监控的详细数据
在这里插入图片描述

2. 断路器聚合监控Turbine

在微服务架构体系中,每个服务都需要配置Hystrix DashBoard监控。如果每次只能查看单个实例的监控数据,就需要不断切换监控地址,这显然很不方便。要想看这个系统的Hystrix Dashboard数据就需要用到Hystrix Turbine。Turbine是一个聚合Hystrix 监控数据的工具,他可以将所有相关微服务的Hystrix 监控数据聚合到一起,方便使用。引入Turbine后,整个监控系统架构如下:
在这里插入图片描述

  1. 搭建TurbineServer

    创建工程 shop_hystrix_turbine 引入相关坐标

    <dependency> 
        <groupId>org.springframework.cloud</groupId> 
        <artifactId>spring-cloud-starter-netflix-turbine</artifactId> 
    </dependency> 
    <dependency> 
        <groupId>org.springframework.cloud</groupId> 
        <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> 
    </dependency> 
    <dependency> 
        <groupId>org.springframework.cloud</groupId> 
        <artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId> </dependency>
    
  2. 配置多个微服务的hystrix监控

    在application.yml的配置文件中开启turbine并进行相关配置

    server: 
      	port: 8031 
    spring: 
      	application:
        	name: microservice-hystrix-turbine 
    eureka: 
        client: 
        service-url: 
        defaultZone: http://localhost:8761/eureka/ 
        instance: 
        prefer-ip-address: true 
    turbine: 
        # 要监控的微服务列表,多个用,分隔 
        appConfig: shop-service-order 
        clusterNameExpression: "'default'"
    
    • eureka相关配置 : 指定注册中心地址
    • turbine相关配置:指定需要监控的微服务列表

turbine会自动的从注册中心中获取需要监控的微服务,并聚合所有微服务中的 /hystrix.stream 数据

  1. 配置启动类
@SpringBootApplication 
@EnableTurbine 
@EnableHystrixDashboard 
public class TurbineServerApplication { 
    public static void main(String[] args) { SpringApplication.run(TurbineServerApplication.class, args); 
                                           } 
}

作为一个独立的监控项目,需要配置启动类,开启HystrixDashboard监控平台,并激活Turbine

  1. 测试

    浏览器访问 http://localhost:8031/hystrix 展示HystrixDashboard。并在url位置输入 http://localhost:8031/turbine.stream,动态根据turbine.stream数据展示多个微服务的监控数据
    在这里插入图片描述

2. 熔断器的状态

熔断器有三个状态 CLOSED 、 OPEN 、 HALF_OPEN 熔断器默认关闭状态,当触发熔断后状态变更为OPEN ,在等待到指定的时间,Hystrix会放请求检测服务是否开启,这期间熔断器会变为 HALF_OPEN 半开启状态,熔断探测服务可用则继续变更为 CLOSED 关闭熔断器。
在这里插入图片描述

  • Closed:关闭状态(断路器关闭),所有请求都正常访问。代理类维护了最近调用失败的次数,如果某次调用失败,则使失败次数加1。如果最近失败次数超过了在给定时间内允许失败的阈值,则代理类切换到断开(Open)状态。此时代理开启了一个超时时钟,当该时钟超过了该时间,则切换到半断开(Half-Open)状态。该超时时间的设定是给了系统一次机会来修正导致调用失败的错误。
  • Open:打开状态(断路器打开),所有请求都会被降级。Hystix会对请求情况计数,当一定时间内失败请求百分比达到阈值,则触发熔断,断路器会完全关闭。默认失败比例的阈值是50%,请求次数最少不低于20次。
  • Half Open:半开状态,open状态不是永久的,打开后会进入休眠时间(默认是5S)。随后断路器会自动进入半开状态。此时会释放1次请求通过,若这个请求是健康的,则会关闭断路器,否则继续保持打开,再次进行5秒休眠计时。

熔断器的默认触发阈值是20次请求,不好触发。休眠时间时5秒,时间太短,不易观察,为了测试方便,我们可以通过配置修改熔断策略:

circuitBreaker.requestVolumeThreshold=5 
circuitBreaker.sleepWindowInMilliseconds=10000 circuitBreaker.errorThresholdPercentage=50

解读:

  • requestVolumeThreshold:触发熔断的最小请求次数,默认20
  • errorThresholdPercentage:触发熔断的失败请求最小占比,默认50%
  • sleepWindowInMilliseconds:熔断多少秒后去尝试请求

3. 熔断器的隔离策略

微服务使用Hystrix熔断器实现了服务的自动降级,让微服务具备自我保护的能力,提升了系统的稳定性,也较好的解决雪崩效应。其使用方式目前支持两种策略:

  • **线程池隔离策略:**使用一个线程池来存储当前的请求,线程池对请求作处理,设置任务返回处理超时时间,堆积的请求堆积入线程池队列。这种方式需要为每个依赖的服务申请线程池,有一定的资源消耗,好处是可以应对突发流量(流量洪峰来临时,处理不完可将数据存储到线程池队里慢慢处理)
  • **信号量隔离策略:**使用一个原子计数器(或信号量)来记录当前有多少个线程在运行,请求来先判断计数器的数值,若超过设置的最大线程个数则丢弃改类型的新请求,若不超过则执行计数操作请求来计数器+1,请求返回计数器-1。这种方式是严格的控制线程且立即返回模式,无法应对突发流量(流量洪峰来临时,处理的线程超过数量,其他的请求会直接返回,不继续去请求依赖的服务)

线程池和型号量两种策略功能支持对比如下:
在这里插入图片描述

  • hystrix.command.default.execution.isolation.strategy : 配置隔离策略

    • ExecutionIsolationStrategy.SEMAPHORE 信号量隔离
    • ExecutionIsolationStrategy.THREAD 线程池隔离
  • hystrix.command.default.execution.isolation.maxConcurrentRequests : 最大信号量上限

4. Hystrix的核心源码

Hystrix 底层基于 RxJava,RxJava 是响应式编程开发库,因此Hystrix的整个实现策略简单说即:把一个HystrixCommand封装成一个Observable(待观察者),针对自身要实现的核心功能,对Observable进行各种装饰,并在订阅各步装饰的Observable,以便在指定事件到达时,添加自己的业务。
在这里插入图片描述

Hystrix主要有4种调用方式:

  • toObservable() 方法 :未做订阅,只是返回一个Observable 。
  • observe() 方法 :调用 #toObservable() 方法,并向 Observable 注册 rx.subjects.ReplaySubject发起订阅。
  • queue() 方法 :调用 #toObservable() 方法的基础上,调用:Observable#toBlocking() 和BlockingObservable#toFuture() 返回 Future 对象execute() 方法 :调用 #queue() 方法的基础上,调用 Future#get() 方法,同步返回 #run() 的执行结果

主要的执行逻辑:

  1. 每次调用创建一个新的HystrixCommand,把依赖调用封装在run()方法中.

  2. 执行execute()/queue做同步或异步调用.

  3. 判断熔断器(circuit-breaker)是否打开,如果打开跳到步骤8,进行降级策略,如果关闭进入步骤.

  4. 判断线程池/队列/信号量是否跑满,如果跑满进入降级步骤8,否则继续后续步骤.

  5. 调用HystrixCommand的run方法.运行依赖逻辑,依赖逻辑调用超时,进入步骤8.

  6. 判断逻辑是否调用成功。返回成功调用结果;调用出错,进入步骤8.

  7. 计算熔断器状态,所有的运行状态(成功, 失败, 拒绝,超时)上报给熔断器,用于统计从而判断熔断器状态.

  8. getFallback()降级逻辑。以下四种情况将触发getFallback调用:

    1. run()方法抛出非HystrixBadRequestException异常。
    2. run()方法调用超时
    3. 熔断器开启拦截调用
    4. 线程池/队列/信号量是否跑满
    5. 没有实现getFallback的Command将直接抛出异常,fallback降级逻辑调用成功直接返回,降级逻辑调用失败抛出异常.
  9. 返回执行成功结果

1. HystrixCommand注解

在实际应用过程通过@HystrixCommand注解能够更加简单快速的实现Hystrix的应用,那么我们就直接从@HystrixCommand注解入手,其中包含了诸多参数配置,如执行隔离策略,线程池定义等,这些参数就不一一说明了,我们来看看其是如何实现服务降级的。

public @interface HystrixCommand {
        String groupKey() default "";

        String commandKey() default "";

        String threadPoolKey() default "";

        String fallbackMethod() default "";

        HystrixProperty[] commandProperties() default {};

        HystrixProperty[] threadPoolProperties() default {};

        Class<? extends Throwable>[] ignoreExceptions() default {};

        ObservableExecutionMode observableExecutionMode() default ObservableExecutionMode.EAGER;

        HystrixException[] raiseHystrixExceptions() default {};

        String defaultFallback() default "";
    }

其定义了fallbackMethod方法名,正如其名,其提供了一个定义回退方法映射,在异常触发时此方法名对应的method将被触发执行,从而实现服务的降级。那么@HystrixCommand注解又是如何被执行的呢,我们找到 HystrixCommandAspect.java ,其切点定义如下

@Aspect
    public class HystrixCommandAspect {
        private static final Map<HystrixCommandAspect.HystrixPointcutType, HystrixCommandAspect.MetaHolderFactory> META_HOLDER_FACTORY_MAP;

        public HystrixCommandAspect() {
        }

        @Pointcut("@annotation(com.netflix.hystrix.contrib.javanica.annotation.HystrixC ommand)")
        public void hystrixCommandAnnotationPointcut() {
        }

        @Pointcut("@annotation(com.netflix.hystrix.contrib.javanica.annotation.HystrixC ollapser)")
        public void hystrixCollapserAnnotationPointcut() {
        }

        @Around("hystrixCommandAnnotationPointcut() || hystrixCollapserAnnotationPointcut()")
        public Object methodsAnnotatedWithHystrixCommand(ProceedingJoinPoint joinPoint) throws Throwable {
            //略 
        }
    }

可以看到被 @HystrixCommand 注解的方法将会执行切面处理。

2. 环绕通知增强

在HystrixCommandAspect的methodsAnnotatedWithHystrixCommand方法中我们可以看到如下

@Around("hystrixCommandAnnotationPointcut() || hystrixCollapserAnnotationPointcut()")
    public Object methodsAnnotatedWithHystrixCommand(ProceedingJoinPoint joinPoint) throws Throwable {
        Method method = AopUtils.getMethodFromTarget(joinPoint);
        Validate.notNull(method, "failed to get method from joinPoint: %s", new Object[]{joinPoint});
        if (method.isAnnotationPresent(HystrixCommand.class) && method.isAnnotationPresent(HystrixCollapser.class)) {
            throw new IllegalStateException("method cannot be annotated with HystrixCommand and HystrixCollapser annotations at the same time");
        } else {
            HystrixCommandAspect.MetaHolderFactory metaHolderFactory = (HystrixCommandAspect.MetaHolderFactory) META_HOLDER_FACTORY_MAP.get(HystrixComma ndAspect.HystrixPointcutType.of(method));
            MetaHolder metaHolder = metaHolderFactory.create(joinPoint);
            HystrixInvokable invokable = HystrixCommandFactory.getInstance().create(metaHolder);
            ExecutionType executionType = metaHolder.isCollapserAnnotationPresent() ? metaHolder.getCollapserExecutionType() : metaHolder.getExecutionType();
            try {
                Object result;
                if (!metaHolder.isObservable()) {
                    result = CommandExecutor.execute(invokable, executionType, metaHolder);
                } else {
                    result = this.executeObservable(invokable, executionType, metaHolder);
                }
            } return result;
        } catch(HystrixBadRequestException var9){
            throw var9.getCause();
        } catch(HystrixRuntimeException var10){
            throw this.hystrixRuntimeExceptionToThrowable(metaHolder, var10);
        }
    }

此方法通过环绕通知的形式对目标方法进行增强,主要作用如下:

  • HystrixInvokable:定义了后续真正执行HystrixCommand的GenericCommand实例
  • 定义metaHolder,包含了当前被注解方法的所有相关有效信息
  • 执行方法: 在进入执行体前,其有一个判断条件,判断其是否是一个Observable模式(在Hystrix中,其实现大量依赖RXJAVA,会无处不在的看到Observable,其是一种观察者模式的实现,具体可以到RxJava项目官方做更多了解)

14. Sentinel

1. Sentinel概述

1. Sentinel简介

随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel 以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。

Sentinel 具有以下特征:

  • 丰富的应用场景:Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景,例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应用等。
  • 完备的实时监控:Sentinel 同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机器秒级数据,甚至 500 台以下规模的集群的汇总运行情况。
  • 广泛的开源生态:Sentinel 提供开箱即用的与其它开源框架/库的整合模块,例如与 SpringCloud、Dubbo、gRPC 的整合。您只需要引入相应的依赖并进行简单的配置即可快速地接入Sentinel。
  • 完善的 SPI 扩展点:Sentinel 提供简单易用、完善的 SPI 扩展接口。您可以通过实现扩展接口来快速地定制逻辑。例如定制规则管理、适配动态数据源等。

Sentinel 的主要特性:
在这里插入图片描述

2. Sentinel与Hystrix的区别

在这里插入图片描述

3. 迁移方案

Sentinel官方提供了详细的由Hystrix 迁移到Sentinel 的方法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AYwA45o9-1641129771304)(C:\Users\14125\AppData\Roaming\Typora\typora-user-images\image-20211225222559162.png)]

4. 名词解释

Sentinel 可以简单的分为 Sentinel 核心库和 Dashboard。核心库不依赖 Dashboard,但是结合Dashboard 可以取得最好的效果。

使用 Sentinel 来进行熔断保护,主要分为几个步骤:

  1. 定义资源

  2. 定义规则

  3. 检验规则是否生效

资源:可以是任何东西,一个服务,服务里的方法,甚至是一段代码。

规则:Sentinel 支持以下几种规则:流量控制规则、熔断降级规则、系统保护规则、来源访问控制规则和 热点参数规则。Sentinel 的所有规则都可以在内存态中动态地查询及修改,修改之后立即生效

先把可能需要保护的资源定义好,之后再配置规则。也可以理解为,只要有了资源,我们就可以在任何时候灵活地定义各种流量控制规则。在编码的时候,只需要考虑这个代码是否需要保护,如果需要保护,就将之定义为一个资源。

2. Sentinel中的管理控制台

1. 下载启动控制台
  1. 获取 Sentinel 控制台

    您可以从官方网站中下载最新版本的控制台 jar 包,下载地址如下:

    https://github.com/alibaba/Sentinel/releases/download/1.6.3/sentinel-dashboard-1.6.3.jar

  2. 启动

    java -Dserver.port=8080 -Dcsp.sentinel.dashboard.server=localhost:8080 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard.jar
    

    其中 -Dserver.port=8080 用于指定 Sentinel 控制台端口为 8080 。 从 Sentinel 1.6.0 起,Sentinel 控制台引入基本的登录功能,默认用户名和密码都是 sentinel 。可以参考 鉴权模块文档 配置用户名和密码。

启动 Sentinel 控制台需要 JDK 版本为 1.8 及以上版本。

2. 客户端接入控制台
  1. 引入JAR包
<dependency> 
	<groupId>com.alibaba.csp</groupId> 
    <artifactId>sentinel-transport-simple-http</artifactId> 
</dependency>
  1. 配置启动参数
spring:
  cloud:
    sentinel:
      transport:
        dashboard: localhost:8080   #sentinel控制台的请求地址

这里的 spring.cloud.sentinel.transport.dashboard 配置控制台的请求路径。

3. 查看机器列表以及健康情况

默认情况下Sentinel 会在客户端首次调用的时候进行初始化,开始向控制台发送心跳包。也可以配置sentinel.eager=true ,取消Sentinel控制台懒加载。

打开浏览器即可展示Sentinel的管理控制台
在这里插入图片描述

3. 基于Sentinel的服务保护

1. 通用资源保护
  1. 在客户端(需要管理微服务上)引入坐标
    在这里插入图片描述

父工程引入

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>Greenwich.RELEASE</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-alibaba-dependencies</artifactId>
            <version>2.1.0.RELEASE</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

子工程中引入sentinel

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
  1. 在客户端配置启动参数
spring:
  cloud:
    sentinel:
      transport:
        dashboard: localhost:8080   #sentinel控制台的请求地址
  1. 配置熔断降级方法
/**
 * @SentinelResource
 *      blockHandler : 声明熔断时调用的降级方法
 *      fallback : 抛出异常执行的降级方法
 *      value : 自定义的资源名称
 *          * 不设置:当前全类名.方法名
 */
@SentinelResource(value="orderFindById",blockHandler = "orderBlockHandler",fallback = "orderFallback")
@RequestMapping(value = "/buy/{id}",method = RequestMethod.GET)
public Product findById(@PathVariable Long id) {
   if(id != 1) {
      throw new RuntimeException("错误");
   }
   return restTemplate.getForObject("http://service-product/product/1",Product.class);
}
 
 
/**
 * 定义降级逻辑
 *  hystrix和sentinel
 *      熔断执行的降级方法
 *      抛出异常执行的降级方法
 */
public Product orderBlockHandler(Long id) {
   Product product = new Product();
   product.setProductName("触发熔断的降级方法");
   return product;
}
 
public Product orderFallback(Long id) {
   Product product = new Product();
   product.setProductName("抛出异常执行的降级方法");
   return product;
}

在需要被保护的方法上使用@SentinelResource注解进行熔断配置。与Hystrix不同的是,Sentinel对抛出异常和熔断降级做了更加细致的区分,通过 blockHandler 指定熔断降级方法,通过 fallback 指定触发异常执行的降级方法。对于@SentinelResource的其他配置如下表:
在这里插入图片描述
在这里插入图片描述

注:1.6.0 之前的版本 fallback 函数只针对降级异常( DegradeException )进行处理,不能针对业务异常进行处理。

特别地,若 blockHandler 和 fallback 都进行了配置,则被限流降级而抛出 BlockException 时只会进入 blockHandler 处理逻辑。若未配置 blockHandler 、 fallback 和 defaultFallback ,则被限流降级时会将 BlockException 直接抛出

2. Rest实现熔断

Spring Cloud Alibaba Sentinel 支持对 RestTemplate 的服务调用使用 Sentinel 进行保护,在构造RestTemplate bean的时候需要加上 @SentinelRestTemplate 注解。

@SpringBootApplication
@EntityScan("cn.itcast.order.entity")
public class RestOrderApplication {
 
    /**
     * sentinel支持对restTemplate的服务调用使用sentinel方法.在构造
     *  RestTemplate对象的时候,只需要加载@SentinelRestTemplate即可
     *
     *  资源名:
     *       httpmethod:schema://host:port/path :协议、主机、端口和路径
     *       httpmethod:schema://host:port :协议、主机和端口
     *
     *  @SentinelRestTemplate:
     *    异常降级
     *      fallback      : 降级方法
     *      fallbackClass : 降级配置类
     *    限流熔断
     *      blockHandler
     *      blockHandlerClass
     */
 
    @LoadBalanced
    @Bean
    @SentinelRestTemplate(fallbackClass = ExceptionUtils.class,fallback = "handleFallback",
        blockHandler = "handleBlock" ,blockHandlerClass = ExceptionUtils.class
    )
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
 
    public static void main(String[] args) {
        SpringApplication.run(RestOrderApplication.class,args);
    }
}
  • @SentinelRestTemplate 注解的属性支持限流( blockHandler , blockHandlerClass )和降级( fallback , fallbackClass )的处理。
  • 其中 blockHandler 或 fallback 属性对应的方法必须是对应 blockHandlerClass 或 fallbackClass 属性中的静态方法。
  • 该方法的参数跟返回值跟org.springframework.http.client.ClientHttpRequestInterceptor#interceptor 方法一致,其中参数多出了一个 BlockException 参数用于获取 Sentinel 捕获的异常。
3. Feign实现熔断

Sentinel 适配了 Feign 组件。如果想使用,除了引入 sentinel-starter 的依赖外还需要 2 个步骤:

  • 配置文件打开 sentinel 对 feign 的支持: feign.sentinel.enabled=true
  • 加入 openfeign starter 依赖使 sentinel starter 中的自动化配置类生效:
  1. 引入依赖

    <dependency> 
        <groupId>com.alibaba.cloud</groupId> 
        <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> 
    </dependency> 
    <dependency> 
        <groupId>org.springframework.cloud</groupId> 
        <artifactId>spring-cloud-starter-openfeign</artifactId> 
    </dependency>
    
  2. 开启sentinel支持

在工程的application.yml中添加sentinel 对 feign 的支持

feign: 
	sentinel: 
		enabled: true
  1. 配置FeignClient

和使用Hystrix的方式基本一致,需要配置FeignClient接口以及通过 fallback 指定熔断降级方法

//指定需要调用的微服务名称
    @FeignClient(name = "shop-service-product", fallback = ProductFeginClientCallBack.class)
    public interface ProductFeginClient {
        //调用的请求路径 
        @RequestMapping(value = "/product/{id}", method = RequestMethod.GET)
        public Product findById(@PathVariable("id") Long id);
    }
  1. 配置熔断方法
/*** 实现自定义的ProductFeginClient接口 * 在接口实现类中编写熔断降级方法 */
    @Component
    public class ProductFeginClientCallBack implements ProductFeginClient {
        /*** 降级方法 */
        public Product findById(Long id) {
            Product product = new Product();
            product.setId(-1l);
            product.setProductName("熔断:触发降级方法");
            return product;
        }
    }

Feign 对应的接口中的资源名策略定义:httpmethod:protocol://requesturl。 @FeignClient 注解中的所有属性,Sentinel 都做了兼容。ProductFeginClient 接口中方法 fifindById 对应的资源名为 GET:http://shop-serviceproduct/product/{str}。

15. 微服务网关Zuul

1. Zuul简介

ZUUL是Netflflix开源的微服务网关,它可以和Eureka、Ribbon、Hystrix等组件配合使用,Zuul组件的核心是一系列的过滤器,这些过滤器可以完成以下功能:

  • 动态路由:动态将请求路由到不同后端集群
  • 压力测试:逐渐增加指向集群的流量,以了解性能
  • 负载分配:为每一种负载类型分配对应容量,并弃用超出限定值的请求
  • 静态响应处理:边缘位置进行响应,避免转发到内部集群
  • 身份认证和安全: 识别每一个资源的验证要求,并拒绝那些不符的请求。Spring Cloud对Zuul进行了整合和增强。

Spring Cloud对Zuul进行了整合和增强

2. 搭建Zuul网关服务器

  1. 创建工程导入依赖

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    
  2. 配置启动类,开启网关服务器功能
    @SpringBootApplication
    //开启zuul网关功能
    @EnableZuulProxy
    //eureka的服务发现
    @EnableDiscoveryClient
    public class ZuulServerApplication {}
    
  3. 编写配置文件

    server:
      port: 8080 #端口
    spring:
      application:
        name: api-zuul-server #服务名称
    

3. Zuul中的路由转发

最直观的理解:“路由”是指根据请求URL,将请求分配到对应的处理程序。在微服务体系中,Zuul负责接收所有的请求。根据不同的URL匹配规则,将不同的请求转发到不同的微服务处理。

##路由配置
zuul:
  routes:
    product-service: #路由id,随便写
      path: /product-service/** #映射路径  #localhost:8080/product-service/sxxssds
      url: http://127.0.0.1:9001 #映射路径对应的实际微服务url地址
      sensitiveHeaders: #默认zuul会屏蔽cookie,cookie不会传到下游服务,这里设置为空则取 消默认的黑名单,如果设置了具体的头信息则不会传到下游服务

只需要在application.yml文件中配置路由规则即可:

  • product-service:配置路由id,可以随意取名
  • url:映射路径对应的实际url地址
  • path:配置映射路径,这里将所有请求前缀为/product-service/的请求,转发到http://127.0.0.1:9002处理
1. 面向服务的路由

微服务一般是由几十、上百个服务组成,对于一个URL请求,最终会确认一个服务实例进行处理。如果对每个服务实例手动指定一个唯一访问地址,然后根据URL去手动实现请求匹配,这样做显然就不合理。

Zuul支持与Eureka整合开发,根据ServiceID自动的从注册中心中获取服务地址并转发请求,这样做的好处不仅可以通过单个端点来访问应用的所有服务,而且在添加或移除服务实例的时候不用修改Zuul的路由配置。

  1. 添加Eureka客户端依赖

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    
  2. 开启eureka的客户端发现
    @SpringBootApplication
    //开启zuul网关功能
    @EnableZuulProxy
    //eureka的服务发现
    @EnableDiscoveryClient
    public class ZuulServerApplication {
     
       public static void main(String[] args) {
          SpringApplication.run(ZuulServerApplication.class,args);
       }
    }
    
  3. 在zuul网关服务中配置eureka的注册中心相关信息
    #配置Eureka
    eureka:
      client:
        service-url:
          defaultZone: http://localhost:9000/eureka/
      instance:
        prefer-ip-address: true #使用ip地址注册
    
  4. 修改路由中的映射配置

    因为已经有了Eureka客户端,我们可以从Eureka获取服务的地址信息,因此映射时无需指定IP地址,而是通过服务名称来访问,而且Zuul已经集成了Ribbon的负载均衡功能。

    ##路由配置
    zuul:
      routes:
        #已商品微服务
        product-service: #路由id,随便写
          path: /product-service/** #映射路径  #localhost:8080/product-service/sxxssds
          #url: http://127.0.0.1:9001 #映射路径对应的实际微服务url地址
          serviceId: service-product #配置转发的微服务的服务名称
    
2. 简化的路由配置

在刚才的配置中,我们的规则是这样的:

  • zuul.routes.< route>.path=/xxx/** : 来指定映射路径。 < route> 是自定义的路由名
  • zuul.routes.< route>.serviceId=/product-service :来指定服务名。

而大多数情况下,我们的 路由名称往往和服务名会写成一样的。因此Zuul就提供了一种简化的配置语法: zuul.routes.=

上面的配置可以简化为一条:

##路由配置
zuul:
  routes:
    #已商品微服务
    #product-service: #路由id,随便写
     # path: /product-service/** #映射路径  #localhost:8080/product-service/sxxssds
      #url: http://127.0.0.1:9001 #映射路径对应的实际微服务url地址
     # serviceId: service-product #配置转发的微服务的服务名称
     #如果路由id 和 对应的微服务的serviceId一致的话
    service-product: /product-service/**
     #zuul中的默认路由配置
     #如果当前的微服务名称 service-product , 默认的请求映射路径 /service-product/**
     #  /service-order/
3. 默认的路由规则

在使用Zuul的过程中,上面讲述的规则已经大大的简化了配置项。但是当服务较多时,配置也是比较繁琐的。因此Zuul就指定了默认的路由规则:

  • 默认情况下,一切服务的映射路径就是服务名本身。
    • 例如服务名为: shop-service-product ,则默认的映射路径就是: /shop-service-product/**
4. Zuul加入后的架构

在这里插入图片描述

4. Zuul中的过滤器

Zuul它包含了两个核心功能:对请求的路由过滤。其中路由功能负责将外部请求转发到具体的微服务实例上,是实现外部访问统一入口的基础;而过滤器功能则负责对请求的处理过程进行干预,是实现请求校验、服务聚合等功能的基础。其实,路由功能在真正运行时,它的路由映射和请求转发同样也由几个不同的过滤器完成的。所以,过滤器可以说是Zuul实现API网关功能最为核心的部件,每一个进入Zuul的HTTP请求都会经过一系列的过滤器处理链得到请求响应并返回给客户端.

1. ZuulFilter简介

Zuul 中的过滤器跟我们之前使用的 javax.servlet.Filter 不一样,javax.servlet.Filter 只有一种类型,可以通过配置 urlPatterns 来拦截对应的请求。而 Zuul 中的过滤器总共有 4 种类型,且每种类型都有对应的使用场景。

  1. PRE:这种过滤器在请求被路由之前调用。我们可利用这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等。

  2. ROUTING:这种过滤器将请求路由到微服务。这种过滤器用于构建发送给微服务的请求,并使用Apache HttpClient或Netfifilx Ribbon请求微服务。

  3. POST:这种过滤器在路由到微服务以后执行。这种过滤器可用来为响应添加标准的HTTPHeader、收集统计信息和指标、将响应从微服务发送给客户端等。

  4. ERROR:在其他阶段发生错误时执行该过滤器

Zuul提供了自定义过滤器的功能实现起来也十分简单,只需要编写一个类去实现zuul提供的接口

public abstract ZuulFilter implements IZuulFilter {
        abstract public String filterType ();
        abstract public int filterOrder ();
        boolean shouldFilter ();// 来自IZuulFilter
        Object run () throws ZuulException;// IZuulFilter 
    }
  • ZuulFilter是过滤器的顶级父类。在这里我们看一下其中定义的4个最重要的方法

  • shouldFilter :返回一个 Boolean 值,判断该过滤器是否需要执行。返回true执行,返回false不执行。

  • run :过滤器的具体业务逻辑。

  • filterType :返回字符串,代表过滤器的类型。包含以下4种:

    • pre :请求在被路由之前执行
    • routing :在路由请求时调用
    • post :在routing和errror过滤器之后调用
    • error :处理请求时发生错误调用
  • filterOrder :通过返回的int值来定义过滤器的执行顺序,数字越小优先级越高。

2. 生命周期

在这里插入图片描述

  • 正常流程:

    • 请求到达首先会经过pre类型过滤器,而后到达routing类型,进行路由,请求就到达真正的服务提供者,执行请求,返回结果后,会到达post过滤器。而后返回响应。
  • 异常流程:

    • 整个过程中,pre或者routing过滤器出现异常,都会直接进入error过滤器,再error处理完毕后,会将请求交给POST过滤器,最后返回给用户。
    • 如果是error过滤器自己出现异常,最终也会进入POST过滤器,而后返回。
    • 如果是POST过滤器出现异常,会跳转到error过滤器,但是与pre和routing不同的时,请求不会再到达POST过滤器了。
  • 不同过滤器的场景:

    • 请求鉴权:一般放在pre类型,如果发现没有访问权限,直接就拦截了
    • 异常处理:一般会在error类型和post类型过滤器中结合来处理。
    • 服务调用时长统计:pre和post结合使用。

所有内置过滤器列表:
在这里插入图片描述

3. 自定义过滤器

接下来我们来自定义一个过滤器,模拟一个登录的校验。基本逻辑:如果请求中有access-token参数,则认为请求有效,放行

/**
 * 自定义的zuul过滤器
 *  继承抽象父类
 */
@Component
    public class LoginFilter extends ZuulFilter {
        /**
        * 定义过滤器类型
        *  pre
        *  routing
        *  post
        *  error
        */
        @Override
        public String filterType() {
            // 登录校验,肯定是在前置拦截
            return "pre";
        }

         /**
            *  指定过滤器的执行顺序
            *      返回值越小,执行顺序越高
            */
        @Override
        public int filterOrder() {
            // 顺序设置为1
            return 1;
        }

       /**
        * 当前过滤器是否生效
        *  true : 使用此过滤器
        *  flase : 不使用此过滤器
        */
        @Override
        public boolean shouldFilter() {
            // 返回true,代表过滤器生效。
            return true;
        }

       /**
        * 指定过滤器中的业务逻辑
        *  身份认证:
        *      1.所有的请求需要携带一个参数 : access-token
        *      2.获取request请求
        *      3.通过request获取参数access-token
        *      4.判断token是否为空
        *      4.1 token==null : 身份验证失败
        *      4.2 token!=null : 执行后续操作
        *  在zuul网关中,通过RequestContext的上下问对象,可以获取对象request对象
        */
        @Override
        public Object run() throws ZuulException {
            // 登录校验逻辑。
            // 1)获取Zuul提供的请求上下文对象
            RequestContext ctx = RequestContext.getCurrentContext();
            // 2) 从上下文中获取request对象
            HttpServletRequest req = ctx.getRequest();
            // 3) 从请求中获取token
            String token = req.getParameter("access-token");
            // 4) 判断
            if (token == null || "".equals(token.trim())) {
                // 没有token,登录校验失败,拦截
                ctx.setSendZuulResponse(false);
                // 返回401状态码。也可以考虑重定向到登录页。
                ctx.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
            }
            // 校验通过,可以考虑把用户信息放入上下文,继续向后执行 
            return null;
        }
    }

**RequestContext:**用于在过滤器之间传递消息。它的数据保存在每个请求的ThreadLocal中。它用于存储请求路由到哪里、错误、HttpServletRequest、HttpServletResponse都存储在RequestContext中。RequestContext扩展了ConcurrentHashMap,所以,任何数据都可以存储在上下文中

5. 服务网关Zuul的核心源码解析

在这里插入图片描述

在Zuul中, 整个请求的过程是这样的,首先将请求给zuulservlet处理,zuulservlet中有一个zuulRunner对象,该对象中初始化了RequestContext:作为存储整个请求的一些数据,并被所有的zuulfifilter共享。zuulRunner中还有 FilterProcessor,FilterProcessor作为执行所有的zuulfifilter的管理器。FilterProcessor从fifilterloader 中获取zuulfifilter,而zuulfifilter是被fifilterFileManager所加载,并支持groovy热加载,采用了轮询的方式热加载。有了这些fifilter之后,zuulservelet首先执行的Pre类型的过滤器,再执行route类型的过滤器,最后执行的是post 类型的过滤器,如果在执行这些过滤器有错误的时候则会执行error类型的过滤器。执行完这些过滤器,最终将请求的结果返回给客户端。

  1. 初始化

SpringCloud对Zuul的封装使得发布一个ZuulServer无比简单,根据自动装载原则可以在 spring- cloud-netflix-zuul-2.1.0.RELEASE.jar 下找到 spring.factories
在这里插入图片描述

@Configuration
@Import({RestClientRibbonConfiguration.class, OkHttpRibbonConfiguration.class, HttpClientRibbonConfiguration.class, HttpClientConfiguration.class})
@ConditionalOnBean({Marker.class})
public class ZuulProxyAutoConfiguration extends ZuulServerAutoConfiguration {
    //省略 
}
@Configuration
@EnableConfigurationProperties({ZuulProperties.class})
@ConditionalOnClass({ZuulServlet.class, ZuulServletFilter.class})
@ConditionalOnBean({Marker.class})
public class ZuulServerAutoConfiguration {
    @Bean
    @Primary
    public CompositeRouteLocator primaryRouteLocator(Collection<RouteLocator> routeLocators) {
        return new CompositeRouteLocator(routeLocators);
    }

    @Bean
    @ConditionalOnMissingBean({SimpleRouteLocator.class})
    public SimpleRouteLocator simpleRouteLocator() {
        return new SimpleRouteLocator(this.server.getServlet().getContextPath(), this.zuulProperties);
    }

    @Bean
    public ZuulController zuulController() {
        return new ZuulController();
    }

    @Configuration
    protected static class ZuulFilterConfiguration {
        @Autowired
        private Map<String, ZuulFilter> filters;

        protected ZuulFilterConfiguration() {
        }

        @Bean
        public ZuulFilterInitializer zuulFilterInitializer(CounterFactory counterFactory, TracerFactory tracerFactory) {
            FilterLoader filterLoader = FilterLoader.getInstance();
            FilterRegistry filterRegistry = FilterRegistry.instance();
            return new ZuulFilterInitializer(this.filters, counterFactory, tracerFactory, filterLoader, filterRegistry);
        }
    }//其他省略
}
  • CompositeRouteLocator:组合路由定位器,看入参就知道应该是会保存好多个RouteLocator,构造过程中其实仅包括一个DiscoveryClientRouteLocator。
  • SimpleRouteLocator:默认的路由定位器,主要负责维护配置文件中的路由配置。
  • ZuulController:Zuul创建的一个Controller,用于将请求交由ZuulServlet处理。
  • ZuulHandlerMapping:这个会添加到SpringMvc的HandlerMapping链中,只有选择ZuulHandlerMapping的请求才能出发到Zuul的后续流程。
  • 注册ZuulFilterInitializer,通过FilterLoader加载应用中所有的过滤器并将过滤器注册到FilterRegistry,那我们接下来一起看下过滤器是如何被加载到应用中的
public class ZuulFilterInitializer {
        private static final Log log = LogFactory.getLog(ZuulFilterInitializer.class);
        private final Map<String, ZuulFilter> filters;
        private final CounterFactory counterFactory;
        private final TracerFactory tracerFactory;
        private final FilterLoader filterLoader;
        private final FilterRegistry filterRegistry;

        public ZuulFilterInitializer(Map<String, ZuulFilter> filters, CounterFactory counterFactory, TracerFactory tracerFactory, FilterLoader filterLoader, FilterRegistry filterRegistry) {
            this.filters = filters;
            this.counterFactory = counterFactory;
            this.tracerFactory = tracerFactory;
            this.filterLoader = filterLoader;
            this.filterRegistry = filterRegistry;
        }

        @PostConstruct
        public void contextInitialized() {
            log.info("Starting filter initializer");
            TracerFactory.initialize(this.tracerFactory);
            CounterFactory.initialize(this.counterFactory);
            Iterator var1 = this.filters.entrySet().iterator();
            while (var1.hasNext()) {
                Entry<String, ZuulFilter> entry = (Entry) var1.next();
                this.filterRegistry.put((String) entry.getKey(), (ZuulFilter) entry.getValue());
            }
        }
    }
  1. 请求转发

在Zuul的自动配置中我们看到了 ZuulHandlerMapping ,为SpringMVC中 HandlerMapping 的拓展实现,会自动的添加到HandlerMapping链中。

public class ZuulHandlerMapping extends AbstractUrlHandlerMapping {
    private final RouteLocator routeLocator;
    private final ZuulController zuul;
    private ErrorController errorController;
    private PathMatcher pathMatcher = new AntPathMatcher();
    private volatile boolean dirty = true;

    public ZuulHandlerMapping(RouteLocator routeLocator, ZuulController zuul) {
        this.routeLocator = routeLocator;
        this.zuul = zuul;
        this.setOrder(-200);
    }

    private void registerHandlers() {
        Collection<Route> routes = this.routeLocator.getRoutes();
        if (routes.isEmpty()) {
            this.logger.warn("No routes found from RouteLocator");
        } else {
            Iterator var2 = routes.iterator();
            while (var2.hasNext()) {
                Route route = (Route) var2.next();
                this.registerHandler(route.getFullPath(), this.zuul);
            }
        }
    }
}

其主要目的就是把所有路径的请求导入到ZuulController上.另外的功效是当觉察RouteLocator路由表变更,则更新自己dirty状态,重新注册所有Route到ZuulController。

public class ZuulController extends ServletWrappingController {
    public ZuulController() {
        //在这里已经设置了ZuulServlet
        this.setServletClass(ZuulServlet.class);
        this.setServletName("zuul");
        this.setSupportedMethods((String[]) null);
    }

    public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
        ModelAndView var3;
        try {
            //在这里面会调用ZuulServlet的service方法
            var3 = super.handleRequestInternal(request, response);
        } finally {
            RequestContext.getCurrentContext().unset();
        }
        return var3;
    }
}

在 ZuulController 中的 handleRequest 方法,会调用已经注册的 ZuulServlet 完成业务请求,我们进入 ZuulServlet 看下内部是如何处理的

public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
    try {
        this.init((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse);
        RequestContext context = RequestContext.getCurrentContext();
        context.setZuulEngineRan();
        try {
            this.preRoute();
        } catch (ZuulException var13) {
            this.error(var13);
            this.postRoute();
            return;
        }
        try {
            this.route();
        } catch (ZuulException var12) {
            this.error(var12);
            this.postRoute();
            return;
        }
        try {
            this.postRoute();
        } catch (ZuulException var11) {
            this.error(var11);
        }
    } catch (Throwable var14) {
        this.error(new ZuulException(var14, 500, "UNHANDLED_EXCEPTION_" + var14.getClass().getName()));
    } finally {
        RequestContext.getCurrentContext().unset();
    }
}
  1. 过滤器

Zuul默认注入的过滤器可以在 spring-cloud-netflix-core.jar 中找到
在这里插入图片描述

6. Zuul网关存在的问题

  • 性能问题

    • Zuul1x版本本质上就是一个同步Servlet,采用多线程阻塞模型进行请求转发。简单讲,每来一个请求,Servlet容器要为该请求分配一个线程专门负责处理这个请求,直到响应返回客户端这个线程才会被释放返回容器线程池。如果后台服务调用比较耗时,那么这个线程就会被阻塞,阻塞期间线程资源被占用,不能干其它事情。我们知道Servlet容器线程池的大小是有限制的,当前端请求量大,而后台慢服务比较多时,很容易耗尽容器线程池内的线程,造成容器无法接受新的请求。
  • 不支持任何长连接,如websocket

16. 微服务网关Gateway

1. Gateway简介

1. 简介

Spring Cloud Gateway 是 Spring 官方基于 Spring 5.0,Spring Boot 2.0 和 Project Reactor 等技术开发的网关,旨在为微服务架构提供一种简单而有效的统一的 API 路由管理方式,统一访问接口。SpringCloud Gateway 作为 Spring Cloud 生态系中的网关,目标是替代 Netflflix ZUUL,其不仅提供统一的路由方式,并且基于 Filter 链的方式提供了网关基本的功能,例如:安全,监控/埋点,和限流等。它是基于Nttey的响应式开发模式。

2. 核心概念在这里插入图片描述
  1. 路由(route)

路由是网关最基础的部分,路由信息由一个ID、一个目的URL、一组断言工厂和一

组Filter组成。如果断言为真,则说明请求URL和配置的路由匹配。

  1. 断言(predicates) Java8中的断言函数,Spring Cloud Gateway中的断言函数输入类型是

Spring5.0框架中的ServerWebExchange。Spring Cloud Gateway中的断言函数允许开发者去定

义匹配来自Http Request中的任何信息,比如请求头和参数等。

  1. 过滤器(fifilter)

一个标准的Spring webFilter,Spring Cloud Gateway中的Filter分为两种类型,

分别是Gateway Filter和Global Filter。过滤器Filter可以对请求和响应进行处理。

2. 服务搭建

  1. 创建工程导入坐标

    <!--
        springcloudgateway的内部是通过netty + webflux 实现
        webflux实现和springmvc存在冲突
     -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-gateway</artifactId>
    </dependency>
    

    注意:SpringCloud Gateway使用的web框架为webflflux,和SpringMVC不兼容。引入的限流组件是hystrix。redis底层不再使用jedis,而是lettuce。

  2. 配置启动类

    @SpringBootApplication
    public class GatewayServerApplication {
     
       public static void main(String[] args) {
          SpringApplication.run(GatewayServerApplication.class,args);
       }
    }
    
  3. 编写配置文件

    spring:
      application:
        name: api-gateway-server #服务名称
    # 配置springcloudgateway-server 
      cloud: #配置SpringCloudGateway的路由
        gateway:
          routes:
          # 配置路由:路由ud,路由到微服务的uri,断言(判断条件)
          - id: product-service         # 保持唯一
            uri: http://127.0.0.1:9001  # 目标微服务请求地址
            predicates:                 
            - Path=/product/**          # 路由条件 path:路由匹配条件
    

3. 路由转发

1. 路由规则

Spring Cloud Gateway 的功能很强大,前面我们只是使用了 predicates 进行了简单的条件匹配,其实Spring Cloud Gataway 帮我们内置了很多 Predicates 功能。在 Spring Cloud Gateway 中 Spring 利用Predicate 的特性实现了各种路由匹配规则,有通过 Header、请求参数等不同的条件来进行作为条件匹配到对应的路由。
在这里插入图片描述

示例

#路由断言之后匹配 
spring: 
	cloud: 
		gateway: 
			routes: 
			- id: after_route 
				uri: https://xxxx.com 
				#路由断言之前匹配 
				predicates: 
				- After=xxxxx 
#路由断言之前匹配 
spring: 
	cloud: 
		gateway: 
			routes: 
			- id:before_route 
                uri: https://xxxxxx.com 
                predicates: 
                - Before=xxxxxxx 
#路由断言之间 
spring: 
	cloud: 
		gateway: 
			routes: 
			- id: between_route 
                uri: https://xxxx.com 
                predicates: 
                - Between=xxxx,xxxx 
#路由断言Cookie匹配,此predicate匹配给定名称(chocolate)和正则表达式(ch.p) 
spring: 
	cloud: 
		gateway: 
			routes: 
			- id: cookie_route 
                uri: https://xxxx.com 
                predicates: 
                - Cookie=chocolate, ch.p 
#路由断言Header匹配,header名称匹配X-Request-Id,且正则表达式匹配\d+
spring: 
	cloud: 
		gateway: 
			routes: 
			- id: header_route 
                uri: https://xxxx.com 
                predicates: 
                - Header=X-Request-Id, \d+
#路由断言匹配Host匹配,匹配下面Host主机列表,**代表可变参数
spring: 
	cloud: 
		gateway: 
			routes: 
			- id: host_route 
                uri: https://xxxx.com 
                predicates:
                - Host=**.somehost.org,**.anotherhost.org
#路由断言Method匹配,匹配的是请求的HTTP方法
spring: 
	cloud: 
		gateway: 
			routes: 
				- id: method_route 
                    uri: https://xxxx.com 
                    predicates: 
                    - Method=GET
#路由断言匹配,{segment}为可变参数
spring: 
	cloud: 
		gateway: 
			routes: 
			- id: host_route 
				uri: https://xxxx.com 
				predicates: 
				- Path=/foo/{segment},/bar/{segment}
#路由断言Query匹配,将请求的参数param(baz)进行匹配,也可以进行regexp正则表达式匹配 (参数包含 foo,并且foo的值匹配ba.)
spring: 
	cloud: 
		gateway: 
			routes: 
			- id: query_route 
				uri: https://xxxx.com 
				predicates:
				- Query=baz 或 Query=foo,ba.
#路由断言RemoteAddr匹配,将匹配192.168.1.1~192.168.1.254之间的ip地址,其中24为子网掩码位 数即255.255.255.0
spring: 
	cloud: 
		gateway: 
			routes: 
			- id: remoteaddr_route 
				uri: https://example.org 
				predicates:
                - RemoteAddr=192.168.1.1/24
2. 动态路由
  1. 添加注册中心依赖
<dependency> 
    <groupId>org.springframework.cloud</groupId> 
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> 
</dependency>
  1. 配置动态路由

修改 application.yml 配置文件,添加eureka注册中心的相关配置,并修改访问映射的URL为服务名称

server: 
	port: 8080 #服务端口
spring: 
	application:
    	name: api-gateway #指定服务名
    cloud: 
    	gateway: 
    		routes: 
    		- id: product-service 
    			uri: lb://shop-service-product 
    			predicates: 
    			- Path=/product/**
eureka: 
	client: 
		serviceUrl: 
			defaultZone: http://127.0.0.1:8761/eureka/ 
			registry-fetch-interval-seconds: 5 # 获取服务列表的周期:5s
	instance: 
		preferIpAddress: true ip-address: 127.0.0.1

uri : uri以 lb: //开头(lb代表轮询),后面接的就是你需要转发到的服务名称

3. 重写转发路径

在SpringCloud Gateway中,路由转发是直接将匹配的路由path直接拼接到映射路径(URI)之后,那么在微服务开发中往往没有那么便利。这里就可以通过RewritePath机制来进行路径重写。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-T6ZXKo73-1641129771308)(C:\Users\14125\AppData\Roaming\Typora\typora-user-images\image-20211229221841951.png)]

重新启动网关,我们在浏览器访问http://127.0.0.1:8080/product-service/product/1,会抛出404。这是由于路由转发规则默认转发到商品微服务( http://127.0.0.1:9002/product- service/product/1 )路径上,而商品微服务又没有 product-service 对应的映射配置。

  1. 添加RewritePath重写转发路径

修改 application.yml ,添加重写规则。

spring: 
	application: 
		name: api-gateway #指定服务名
	cloud: 
		gateway: 
			routes: 
			- id: product-service 
				uri: lb://shop-service-product 
				predicates: 
				- Path=/product-service/** 
				filters: 
				- RewritePath=/product-service/(?<segment>.*), /$\{segment}

通过RewritePath配置重写转发的url,将/product-service/(?.*),重写为{segment},然后转发到订单微服务。比如在网页上请求http://localhost:8080/product-service/product,此时会将请求转发到http://127.0.0.1:9002/product/1( 值得注意的是在yml文档中 $ 要写成 $\ )

4. 过滤器

1. 过滤器基础
1. 过滤器的生命周期

Spring Cloud Gateway 的 Filter 的生命周期不像 Zuul 的那么丰富,它只有两个:“pre” 和 “post”。

  • PRE: 这种过滤器在请求被路由之前调用。我们可利用这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等。

  • POST:这种过滤器在路由到微服务以后执行。这种过滤器可用来为响应添加标准的 HTTPHeader、收集统计信息和指标、将响应从微服务发送给客户端等。

2. 过滤器类型

Spring Cloud Gateway 的 Filter 从作用范围可分为另外两种GatewayFilter 与 GlobalFilter。

  • GatewayFilter:应用到单个路由或者一个分组的路由上。
  • GlobalFilter:应用到所有的路由上。
2. 局部过滤器

局部过滤器(GatewayFilter),是针对单个路由的过滤器。可以对访问的URL过滤,进行切面处理。在Spring Cloud Gateway中通过GatewayFilter的形式内置了很多不同类型的局部过滤器。这里简单将Spring Cloud Gateway内置的所有过滤器工厂整理成了一张表格,虽然不是很详细,但能作为速览使用。如下:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

每个过滤器工厂都对应一个实现类,并且这些类的名称必须以 GatewayFilterFactory 结尾,这是Spring Cloud Gateway的一个约定,例如 AddRequestHeader 对应的实现类为AddRequestHeaderGatewayFilterFactory 。对于这些过滤器的使用方式可以参考官方文档

3. 全局过滤器

全局过滤器(GlobalFilter)作用于所有路由,Spring Cloud Gateway 定义了Global Filter接口,用户可以自定义实现自己的Global Filter。通过全局过滤器可以实现对权限的统一校验,安全性验证等功能,并且全局过滤器也是程序员使用比较多的过滤器。

Spring Cloud Gateway内部也是通过一系列的内置全局过滤器对整个路由转发进行处理如下:
在这里插入图片描述

5. 统一鉴权

内置的过滤器已经可以完成大部分的功能,但是对于企业开发的一些业务功能处理,还是需要我们自己编写过滤器来实现的,那么我们一起通过代码的形式自定义一个过滤器,去完成统一的权限校验。

1. 鉴权逻辑

开发中的鉴权逻辑:

  • 当客户端第一次请求服务时,服务端对用户进行信息认证(登录)
  • 认证通过,将用户信息进行加密形成token,返回给客户端,作为登录凭证
  • 以后每次请求,客户端都携带认证的token
  • 服务端对token进行解密,判断是否有效。
    在这里插入图片描述

如上图,对于验证用户是否已经登录鉴权的过程可以在网关层统一检验。检验的标准就是请求中是否携带token凭证以及token的正确性。

2. 代码实现

下面的我们自定义一个GlobalFilter,去校验所有请求的请求参数中是否包含“token”,如何不包含请求参数“token”则不转发路由,否则执行正常的逻辑。

/**
 * 自定义一个全局过滤器
 *      实现 globalfilter , ordered接口
 */
@Component
public class LoginFilter implements GlobalFilter,Ordered {
 
   /**
    * 执行过滤器中的业务逻辑
    *     对请求参数中的access-token进行判断
    *      如果存在此参数:代表已经认证成功
    *      如果不存在此参数 : 认证失败.
    *  ServerWebExchange : 相当于请求和响应的上下文(zuul中的RequestContext)
    */
   public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
      System.out.println("执行了自定义的全局过滤器");
      //1.获取请求参数access-token
      String token = exchange.getRequest().getQueryParams().getFirst("access-token");
      //2.判断是否存在
      if(token == null) {
         //3.如果不存在 : 认证失败
         System.out.println("没有登录");
         exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
         return exchange.getResponse().setComplete(); //请求结束
      }
      //4.如果存在,继续执行
      return chain.filter(exchange); //继续向下执行
   }
 
   /**
    * 指定过滤器的执行顺序 , 返回值越小,执行优先级越高
    */
   public int getOrder() {
      return 0;
   }
}
  • 自定义全局过滤器需要实现GlobalFilter和Ordered接口。
  • 在fifilter方法中完成过滤器的逻辑判断处理
  • 在getOrder方法指定此过滤器的优先级,返回值越大级别越低
  • ServerWebExchange 就相当于当前请求和响应的上下文,存放着重要的请求-响应属性、请求实例和响应实例等等。一个请求中的request,response都可以通过 ServerWebExchange 获取
  • 调用 chain.filter 继续向下游执行

6. 网关限流

1. 常见的限流算法
1. 计数器

计数器限流算法是最简单的一种限流实现方式。其本质是通过维护一个单位时间内的计数器,每次请求计数器加1,当单位时间内计数器累加到大于设定的阈值,则之后的请求都被拒绝,直到单位时间已经过去,再将计数器重置为零
在这里插入图片描述

2. 漏桶算法

漏桶算法可以很好地限制容量池的大小,从而防止流量暴增。漏桶可以看作是一个带有常量服务时间的单服务器队列,如果漏桶(包缓存)溢出,那么数据包会被丢弃。 在网络中,漏桶算法可以控制端口的流量输出速率,平滑网络上的突发流量,实现流量整形,从而为网络提供一个稳定的流量。
在这里插入图片描述

为了更好的控制流量,漏桶算法需要通过两个变量进行控制:一个是桶的大小,支持流量突发增多时可以存多少的水(burst),另一个是水桶漏洞的大小(rate)。

3. 令牌桶算法

令牌桶算法是对漏桶算法的一种改进,桶算法能够限制请求调用的速率,而令牌桶算法能够在限制调用的平均速率的同时还允许一定程度的突发调用。在令牌桶算法中,存在一个桶,用来存放固定数量的令牌。算法中存在一种机制,以一定的速率往桶中放令牌。每次请求调用需要先获取令牌,只有拿到令牌,才有机会继续执行,否则选择选择等待可用的令牌、或者直接拒绝。放令牌这个动作是持续不断的进行,如果桶中令牌数达到上限,就丢弃令牌,所以就存在这种情况,桶中一直有大量的可用令牌,这时进来的请求就可以直接拿到令牌执行,比如设置qps为100,那么限流器初始化完成一秒后,桶中就已经有100个令牌了,这时服务还没完全启动好,等启动完成对外提供服务时,该限流器可以抵挡瞬时的100个请求。所以,只有桶中没有令牌时,请求才会进行等待,最后相当于以一定的速率执行。在这里插入图片描述

2. 基于Filter的限流

SpringCloudGateway官方就提供了基于令牌桶的限流支持。基于其内置的过滤器工厂RequestRateLimiterGatewayFilterFactory 实现。在过滤器工厂中是通过Redis和lua脚本结合的方式进行流量控制。

  1. 环境搭建

首先在工程的pom文件中引入gateway的起步依赖和redis的reactive依赖

<dependency> 
    <groupId>org.springframework.cloud</groupId> 
    <artifactId>spring-cloud-starter-gateway</artifactId> 
</dependency> 
<dependency> 
    <groupId>org.springframework.boot</groupId> 
    <artifatId>spring-boot-starter-data-redis-reactive</artifactId> 
</dependency>
  1. 修改application.yml配置文件
spring:
  application:
    name: api-gateway-server #服务名称
  redis:
    host: localhost
    pool: 6379
    database: 0
  cloud: #配置SpringCloudGateway的路由
    gateway:
      routes:
      # 配置路由:路由ud,路由到微服务的uri,断言(判断条件)
      - id: order-service
        uri: lb://service-order
        predicates:
        - Path=/order-service/**
        filters:
        - name: RequestRateLimiter
          args:
            # 使用SpEL从容器中获取对象
            key-resolver: '#{@pathKeyResolver}'
            # 令牌桶每秒填充平均速率
            redis-rate-limiter.replenishRate: 1
            # 令牌桶的上限
            redis-rate-limiter.burstCapacity: 3
        - RewritePath=/product-service/(?<segment>.*), /$\{segment}

在 application.yml 中添加了redis的信息,并配置了RequestRateLimiter的限流过滤器:

  • burstCapacity,令牌桶总容量。
  • replenishRate,令牌桶每秒填充平均速率。
  • key-resolver,用于限流的键的解析器的 Bean 对象的名字。它使用 SpEL 表达式根据#
  • {@beanName}从 Spring 容器中获取 Bean 对象。
  1. 配置redis中key解析器keySesolver
@Configuration
public class KeyResolverConfiguration {
 
    /**
     * 编写基于请求路径的限流规则
     * //abc
     * //基于请求ip 127.0.0.1
     * //基于参数
     */
    //@Bean
    public KeyResolver pathKeyResolver() {
        //自定义的KeyResolver
        return new KeyResolver() {
            /**
             * ServerWebExchange :
             *      上下文参数
             */
            public Mono<String> resolve(ServerWebExchange exchange) {
                return Mono.just(exchange.getRequest().getPath().toString());
            }
        };
    }
 
    /**
     * 基于请求参数的限流
     * <p>
     * 请求 abc ? userId=1
     */
    @Bean
    public KeyResolver userKeyResolver() {
        return exchange -> Mono.just(
                exchange.getRequest().getQueryParams().getFirst("userId")
                //exchange.getRequest().getHeaders().getFirst("X-Forwarded-For") 基于请求ip的限流
        );
    }
}
3. 基于Sentinel的限流

Sentinel 支持对 Spring Cloud Gateway、Zuul 等主流的 API Gateway 进行限流

  1. 依赖导入
<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-spring-cloud-gateway-adapter</artifactId>
    <version>x.y.z</version>
</dependency>
  1. 编写配置类
/**
 * sentinel限流的配置
 */
// @Configuration
public class GatewayConfiguration {
 
   private final List<ViewResolver> viewResolvers;
 
   private final ServerCodecConfigurer serverCodecConfigurer;
 
   public GatewayConfiguration(ObjectProvider<List<ViewResolver>> viewResolversProvider,
                               ServerCodecConfigurer serverCodecConfigurer) {
      this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
      this.serverCodecConfigurer = serverCodecConfigurer;
   }
 
   /**
    * 配置限流的异常处理器:SentinelGatewayBlockExceptionHandler
    */
   @Bean
   @Order(Ordered.HIGHEST_PRECEDENCE)
   public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
      return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer);
   }
 
   /**
    * 配置限流过滤器
    */
   @Bean
   @Order(Ordered.HIGHEST_PRECEDENCE)
   public GlobalFilter sentinelGatewayFilter() {
      return new SentinelGatewayFilter();
   }
 
   /**
    * 配置初始化的限流参数
    *  用于指定资源的限流规则.
    *      1.资源名称 (路由id)
    *      2.配置统计时间
    *      3.配置限流阈值
    */
   @PostConstruct
   public void initGatewayRules() {
      Set<GatewayFlowRule> rules = new HashSet<>();
      // rules.add(new GatewayFlowRule("product-service")
      //        .setCount(1)
      //        .setIntervalSec(1)
      // );
      rules.add(new GatewayFlowRule("product_api")
         .setCount(1).setIntervalSec(1)
      );
      GatewayRuleManager.loadRules(rules);
   }
}
  • 基于Sentinel 的Gateway限流是通过其提供的Filter来完成的,使用时只需注入对应的SentinelGatewayFilter 实例以及 SentinelGatewayBlockExceptionHandler 实例即可。
  • @PostConstruct定义初始化的加载方法,用于指定资源的限流规则。这里资源的名称为 order- service ,统计时间是1秒内,限流阈值是1。表示每秒只能访问一个请求。
  1. 网关配置
spring:
  application:
    name: api-gateway-server #服务名称
  redis:
    host: localhost
    pool: 6379
    database: 0
  cloud: #配置SpringCloudGateway的路由
    gateway:
      routes:
      # 配置路由:路由ud,路由到微服务的uri,断言(判断条件)
      - id: product-service
        uri: lb://service-product
        predicates:
        - Path=/product-service/**
        filters:
        - RewritePath=/product-service/(?<segment>.*), /$\{segment}
  1. 自定义异常提示
/**
	 * 自定义限流处理器
	 */
	@PostConstruct
	public void initBlockHandlers() {
		BlockRequestHandler blockHandler = new BlockRequestHandler() {
			public Mono<ServerResponse> handleRequest(ServerWebExchange serverWebExchange, Throwable throwable) {
				Map map = new HashMap();
				map.put("code",001);
				map.put("message","不好意思,限流啦");
				return ServerResponse.status(HttpStatus.OK)
						.contentType(MediaType.APPLICATION_JSON_UTF8)
						.body(BodyInserters.fromObject(map));
			}
		};
		GatewayCallbackManager.setBlockHandler(blockHandler);
	}
  1. 参数限流
rules.add(new GatewayFlowRule("order-service")
    .setCount(1)
    .setIntervalSec(1)
    .setParamItem(new GatewayParamFlowItem()
    .setParseStrategy(SentinelGatewayConstants.PARAM_PARSE_STRATEGY_URL_PARAM)
    .setFi eldName("id") ));

通过指定PARAM_PARSE_STRATEGY_URL_PARAM表示从url中获取参数,setFieldName指定参数名称

  1. 自定义API分组
@PostConstruct
private void initCustomizedApis() {
    Set<ApiDefinition> definitions = new HashSet<>();
    ApiDefinition api1 = new ApiDefinition("product_api").setPredicateItems(new HashSet<ApiPredicateItem>() {
        {
            //以/product-service/product 开头的请求
            add(new ApiPathPredicateItem().setPattern("/product- service/product/**").setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX));
        }
    });
    ApiDefinition api2 = new ApiDefinition("order_api").setPredicateItems(new HashSet<ApiPredicateItem>() {
        {
            //order-service/order 完成的url路径匹配
            add(new ApiPathPredicateItem().setPattern("/order-service/order"));
        }
    });
    definitions.add(api1);
    definitions.add(api2);
    GatewayApiDefinitionManager.loadApiDefinitions(definitions);
}

7. 网关高可用

高可用HA(High Availability)是分布式系统架构设计中必须考虑的因素之一,它通常是指,通过设计减少系统不能提供服务的时间。我们都知道,单点是系统高可用的大敌,单点往往是系统高可用最大的风险和敌人,应该尽量在系统设计的过程中避免单点。方法论上,高可用保证的原则是“集群化”,或者叫“冗余”:只有一个单点,挂了服务会受影响;如果有冗余备份,挂了还有其他backup能够顶上。
在这里插入图片描述

我们实际使用 Spring Cloud Gateway 的方式如上图,不同的客户端使用不同的负载将请求分发到后端的 Gateway,Gateway 再通过HTTP调用后端服务,最后对外输出。因此为了保证 Gateway 的高可用性,前端可以同时启动多个 Gateway 实例进行负载,在 Gateway 的前端使用 Nginx 或者 F5 进行负载转发以达到高可用性。

  1. 准备多个GateWay工程
  2. 配置ngnix
#集群配置
upstream gateway {
	server 127.0.0.1:8081;
	server 127.0.0.1:8080;
}
server {
    listen       80;
    server_name  localhost;
 
    #charset koi8-r;
 
    #access_log  logs/host.access.log  main;
 
	#127.0.0.1
	location  / {
		proxy_pass http://gateway;
	}
}

8. 执行流程分析在这里插入图片描述

Spring Cloud Gateway 核心处理流程如上图所示,Gateway的客户端向 Spring Cloud Gateway 发送请求,请求首先被 HttpWebHandlerAdapter 进行提取组装成网关上下文,然后网关的上下文会传递到 DispatcherHandler 。 DispatcherHandler 是所有请求的分发处理器, DispatcherHandler 主要负责分发请求对应的处理器。比如请求分发到对应的 RoutePredicateHandlerMapping (路由断言处理映射器)。路由断言处理映射器主要作用用于路由查找,以及找到路由后返回对应的FilterWebHandler 。 FilterWebHandler 主要负责组装Filter链并调用Filter执行一系列的Filter处理,然后再把请求转到后端对应的代理服务处理,处理完毕之后将Response返回到Gateway客户端。

17. 链路追踪

1. 微服务架构下的问题

在大型系统的微服务化构建中,一个系统会被拆分成许多模块。这些模块负责不同的功能,组合成系统,最终可以提供丰富的功能。在这种架构中,一次请求往往需要涉及到多个服务。互联网应用构建在不同的软件模块集上,这些软件模块,有可能是由不同的团队开发、可能使用不同的编程语言来实现、有可能布在了几千台服务器,横跨多个不同的数据中心,也就意味着这种架构形式也会存在一些问题:

  • 如何快速发现问题?
  • 如何判断故障影响范围?
  • 如何梳理服务依赖以及依赖的合理性?
  • 如何分析链路性能问题以及实时容量规划?

分布式链路追踪(Distributed Tracing),就是将一次分布式请求还原成调用链路,进行日志记录,性能监控并将 一次分布式请求的调用情况集中展示。比如各个服务节点上的耗时、请求具体到达哪台机器上、每个服务节点的请求状态等等。

目前业界比较流行的链路追踪系统如:Twitter的Zipkin,阿里的鹰眼,美团的Mtrace,大众点评的cat等,大部分都是基于google发表的Dapper。Dapper阐述了分布式系统,特别是微服务架构中链路追踪的概念、数据表示、埋点、传递、收集、存储与展示等技术细节。
在这里插入图片描述

2. Sleuth概述

1. 简介

Spring Cloud Sleuth 主要功能就是在分布式系统中提供追踪解决方案,并且兼容支持了 zipkin,你只需要在pom文件中引入相应的依赖即可。

2. 相关概念

Spring Cloud Sleuth 为Spring Cloud提供了分布式根据的解决方案。它大量借用了Google Dapper的设计。先来了解一下Sleuth中的术语和相关概念。

Spring Cloud Sleuth采用的是Google的开源项目Dapper的专业术语。

  • Span:基本工作单元,例如,在一个新建的span中发送一个RPC等同于发送一个回应请求给RPC,span通过一个64位ID唯一标识,trace以另一个64位ID表示,span还有其他数据信息,比如摘要、时间戳事件、关键值注释(tags)、span的ID、以及进度ID(通常是IP地址) span在不断的启动和停止,同时记录了时间信息,当你创建了一个span,你必须在未来的某个时刻停止它。

  • Trace:一系列spans组成的一个树状结构,例如,如果你正在跑一个分布式大数据工程,你可能需要创建一个trace。

  • Annotation:用来及时记录一个事件的存在,一些核心annotations用来定义一个请求的开始和结束

    • cs - Client Sent -客户端发起一个请求,这个annotion描述了这个span的开始
    • sr - Server Received -服务端获得请求并准备开始处理它,如果将其sr减去cs时间戳便可得到网络延迟
    • ss - Server Sent -注解表明请求处理的完成(当请求返回客户端),如果ss减去sr时间戳便可得到服务端需要的处理请求时间
    • cr - Client Received -表明span的结束,客户端成功接收到服务端的回复,如果cr减去cs时间戳便可得到客户端从服务端获取回复的所有所需时间

3. 链路追踪Sleuth入门

  1. 依赖导入

    <!--sleuth链路追踪-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-sleuth</artifactId>
    </dependency>
    
  2. 修改配置文件

    logging:
      level:
        root: info
        org.springframework.web.servlet.DispatcherServlet: DEBUG
        org.springframework.cloud.sleuth: DEBUG
    

4. Zipkin的概述

Zipkin 是 Twitter 的一个开源项目,它基于 Google Dapper 实现,它致力于收集服务的定时数据,以解决微服务架构中的延迟问题,包括数据的收集、存储、查找和展现。 我们可以使用它来收集各个服务器上请求链路的跟踪数据,并通过它提供的 REST API 接口来辅助我们查询跟踪数据以实现对分布式系统的监控程序,从而及时地发现系统中出现的延迟升高问题并找出系统性能瓶颈的根源。除了面向开发的 API 接口之外,它也提供了方便的 UI 组件来帮助我们直观的搜索跟踪信息和分析请求链路明细,比如:可以查询某段时间内各用户请求的处理时间等。 Zipkin 提供了可插拔数据存储方式:In-Memory、MySql、Cassandra 以及 Elasticsearch。
在这里插入图片描述

上图展示了 Zipkin 的基础架构,它主要由 4 个核心组件构成:

  • Collector:收集器组件,它主要用于处理从外部系统发送过来的跟踪信息,将这些信息转换为
  • Zipkin 内部处理的 Span 格式,以支持后续的存储、分析、展示等功能。
  • Storage:存储组件,它主要对处理收集器接收到的跟踪信息,默认会将这些信息存储在内存中,我们也可以修改此存储策略,通过使用其他存储组件将跟踪信息存储到数据库中。
  • RESTful API:API 组件,它主要用来提供外部访问接口。比如给客户端展示跟踪信息,或是外接系统访问以实现监控等。
  • Web UI:UI 组件,基于 API 组件实现的上层应用。通过 UI 组件用户可以方便而有直观地查询和分析跟踪信息。

Zipkin 分为两端,一个是 Zipkin 服务端,一个是 Zipkin 客户端,客户端也就是微服务的应用。客户端会配置服务端的 URL 地址,一旦发生服务间的调用的时候,会被配置在微服务里面的 Sleuth 的监听器监听,并生成相应的 Trace 和 Span 信息发送给服务端。发送的方式主要有两种,一种是 HTTP 报文的方式,还有一种是消息总线的方式如 RabbitMQ。

不论哪种方式,我们都需要:

  • 一个 Eureka 服务注册中心,这里我们就用之前的 eureka 项目来当注册中心。
  • 一个 Zipkin 服务端。
  • 多个微服务,这些微服务中配置Zipkin 客户端。

5. Zipkin Server的部署和配置

  1. Zipkin Server下载

从spring boot 2.0开始,官方就不再支持使用自建Zipkin Server的方式进行服务链路追踪,而是直接提供了编译好的 jar 包来给我们使用。可以从官方网站下载先下载Zipkin的web UI,我们这里下载的是zipkin-server-2.12.9-exec.jar

  1. 启动

在命令行输入 java -jar zipkin-server-2.12.9-exec.jar 启动 Zipkin Server
在这里插入图片描述

  • 默认Zipkin Server的请求端口为 9411
  • Zipkin Server的启动参数可以通过官方提供的yml配置文件查找
  • 在浏览器输入 http://127.0.0.1:9411即可进入到Zipkin Server的管理后台

6. 客户端Zipkin+Sleuth整合

通过查看日志分析微服务的调用链路并不是一个很直观的方案,结合zipkin可以很直观地显示微服务之间的调用关系。

1.客户端添加依赖(客户端指的是需要被追踪的微服务)

<!--zipkin依赖-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
  1. 修改客户端配置文件
zipkin: 
	base-url: http://127.0.0.1:9411/ #zipkin server的请求地址 
	sender: 
		type: web #请求方式,默认以http的方式向zipkin server发送追踪数据 
sleuth: 
	sampler: 
		probability: 1.0 #采样的百分比

指定了zipkin server的地址,下面制定需采样的百分比,默认为0.1,即10%,这里配置1,是记录全部的sleuth信息,是为了收集到更多的数据(仅供测试用)。在分布式系统中,过于频繁的采样会影响系统性能,所以这里配置需要采用一个合适的值。

  1. 测试

    以此启动每个微服务,启动Zipkin Service。通过浏览器发送一次微服务请求。打开 Zipkin Service控制台,我们可以根据条件追踪每次请求调用过程
    在这里插入图片描述

单击该trace可以看到请求的细节
在这里插入图片描述

7. 基于消息中间件收集数据

在默认情况下,Zipkin客户端和Server之间是使用HTTP请求的方式进行通信(即同步的请求方式),在网络波动,Server端异常等情况下可能存在信息收集不及时的问题。Zipkin支持与rabbitMQ整合完成异步消息传输。

加了MQ之后,通信过程如下图所示:
在这里插入图片描述

1. RabbitMQ的安装与启动
2. 服务端启动
java -jar zipkin-server-2.12.9-exec.jar --RABBIT_ADDRESSES=127.0.0.1:5672
  • RABBIT_ADDRESSES : 指定RabbitMQ地址
  • RABBIT_USER: 用户名(默认guest)
  • RABBIT_PASSWORD : 密码(默认guest)

启动Zipkin Server之后,我们打开RabbitMQ的控制台可以看到多了一个Queue
在这里插入图片描述

其中 zipkin 就是为我们自动创建的Queue队列

3. 客户端配置
  1. 配置依赖
<dependency>
	<groupId>org.springframework.cloud</groupId>
  	<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
<dependency>
	<groupId>org.springframework.cloud</groupId>
  	<artifactId>spring-cloud-sleuth-zipkin</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.amqp</groupId>
    <artifactId>spring-rabbit</artifactId>
</dependency>

导入 spring-rabbit 依赖,是Spring提供的对rabbit的封装,客户端会根据配置自动的生产消息并发送到目标队列中

  1. 配置消息中间件rabbit mq地址等信息
zipkin:
  #base-url: http://127.0.0.1:9411/  #server的请求地址
  sender:
    #type: web #数据的传输方式 , 已http的形式向server端发送数据
    type: rabbit #向rabbitmq中发送消息
sleuth:
  sampler:
    probability: 1 #采样比
rabbitmq:
  host: localhost
  port: 5672
  username: guest
  password: guest
  listener: # 这里配置了重试策略
    direct:
      retry:
        enabled: true
    simple:
      retry:
        enabled: true
  • 修改消息的投递方式,改为rabbit即可。
  • 添加rabbitmq的相关配置
  1. 测试

关闭Zipkin Server,并随意请求连接。打开rabbitmq管理后台可以看到,消息已经推送到rabbitmq。 当Zipkin Server启动时,会自动的从rabbitmq获取消息并消费,展示追踪数据

可以看到如下效果:

  • 请求的耗时时间不会出现突然耗时特长的情况
  • 当ZipkinServer不可用时(比如关闭、网络不通等),追踪信息不会丢失,因为这些信息会保存在Rabbitmq服务器上,直到Zipkin服务器可用时,再从Rabbitmq中取出这段时间的信息
    在这里插入图片描述

8. 存储跟踪数据

Zipkin Server默认时间追踪数据信息保存到内存,这种方式不适合生产环境。因为一旦Service关闭重启或者服务崩溃,就会导致历史数据消失。Zipkin支持将追踪数据持久化到mysql数据库或者存储到elasticsearch中

1. 准备数据库

可以从官网找到Zipkin Server持久mysql的数据库脚本

CREATE TABLE IF NOT EXISTS zipkin_spans ( `trace_id_high` BIGINT NOT NULL DEFAULT 0 COMMENT 'If non zero, this means the trace uses 128 bit traceIds instead of 64 bit', `trace_id` BIGINT NOT NULL, `id` BIGINT NOT NULL, `name` VARCHAR(255) NOT NULL, `parent_id` BIGINT, `debug` BIT(1), `start_ts` BIGINT COMMENT 'Span.timestamp(): epoch micros used for endTs query and to implement TTL', `duration` BIGINT COMMENT 'Span.duration(): micros used for minDuration and maxDuration query' ) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8 COLLATE utf8_general_ci; ALTER TABLE zipkin_spans ADD UNIQUE KEY(`trace_id_high`, `trace_id`, `id`) COMMENT 'ignore insert on duplicate';
 ALTER TABLE zipkin_spans ADD INDEX(`trace_id_high`, `trace_id`, `id`) 
COMMENT 'for joining with zipkin_annotations';  
  ALTER TABLE zipkin_spans ADD INDEX(`trace_id_high`, `trace_id`) COMMENT 'for 
getTracesByIds';  
  ALTER TABLE zipkin_spans ADD INDEX(`name`) COMMENT 'for getTraces and 
getSpanNames';  
  ALTER TABLE zipkin_spans ADD INDEX(`start_ts`) COMMENT 'for getTraces 
ordering and range';  
CREATE TABLE IF NOT EXISTS zipkin_annotations ( 
 `trace_id_high` BIGINT NOT NULL DEFAULT 0 COMMENT 'If non zero, this means 
the trace uses 128 bit traceIds instead of 64 bit',  
 `trace_id` BIGINT NOT NULL COMMENT 'coincides with zipkin_spans.trace_id', 
 `span_id` BIGINT NOT NULL COMMENT 'coincides with zipkin_spans.id', 
 `a_key` VARCHAR(255) NOT NULL COMMENT 'BinaryAnnotation.key or 
Annotation.value if type == -1',  
 `a_value` BLOB COMMENT 'BinaryAnnotation.value(), which must be smaller 
than 64KB', 
 `a_type` INT NOT NULL COMMENT 'BinaryAnnotation.type() or -1 if 
Annotation',  
 `a_timestamp` BIGINT COMMENT 'Used to implement TTL; Annotation.timestamp 
or zipkin_spans.timestamp',  
 `endpoint_ipv4` INT COMMENT 'Null when Binary/Annotation.endpoint is 
null', 
 `endpoint_ipv6` BINARY(16) COMMENT 'Null when Binary/Annotation.endpoint 
is null, or no IPv6 address',  
 `endpoint_port` SMALLINT COMMENT 'Null when Binary/Annotation.endpoint is 
null', 
 `endpoint_service_name` VARCHAR(255) COMMENT 'Null when 
Binary/Annotation.endpoint is null'  
 ) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8 COLLATE 
utf8_general_ci;  
  ALTER TABLE zipkin_annotations ADD UNIQUE KEY(`trace_id_high`, `trace_id`, 
`span_id`, `a_key`, `a_timestamp`) COMMENT 'Ignore insert on duplicate';  
  ALTER TABLE zipkin_annotations ADD INDEX(`trace_id_high`, `trace_id`, 
`span_id`) COMMENT 'for joining with zipkin_spans';  
  ALTER TABLE zipkin_annotations ADD INDEX(`trace_id_high`, `trace_id`) 
COMMENT 'for getTraces/ByIds';  
  ALTER TABLE zipkin_annotations ADD INDEX(`endpoint_service_name`) COMMENT 
'for getTraces and getServiceNames';  
ALTER TABLE zipkin_annotations ADD INDEX(`a_type`) COMMENT 'for getTraces'; 
ALTER TABLE zipkin_annotations ADD INDEX(`a_key`) COMMENT 'for getTraces'; 
  ALTER TABLE zipkin_annotations ADD INDEX(`trace_id`, `span_id`, `a_key`) 
COMMENT 'for dependencies job';  
CREATE TABLE IF NOT EXISTS zipkin_dependencies ( 
 `day` DATE NOT NULL, 
 `parent` VARCHAR(255) NOT NULL, 
 `child` VARCHAR(255) NOT NULL,  
 `call_count` BIGINT  
 ) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8 COLLATE 
utf8_general_ci;  
ALTER TABLE zipkin_dependencies ADD UNIQUE KEY(`day`, `parent`, `child`);
2. 配置启动服务端
java -jar zipkin-server-2.12.9-exec.jar --STORAGE_TYPE=mysql --
MYSQL_HOST=127.0.0.1 --MYSQL_TCP_PORT=3306 --MYSQL_DB=zipkin --MYSQL_USER=root -
-MYSQL_PASS=111111
  • STORAGE_TYPE : 存储类型
  • MYSQL_HOST: mysql主机地址
  • MYSQL_TCP_PORT:mysql端口
  • MYSQL_DB: mysql数据库名称
  • MYSQL_USER:mysql用户名
  • MYSQL_PASS :mysql密码

配置好服务端之后,可以在浏览器请求几次。回到数据库查看会发现数据已经持久化到mysql中
在这里插入图片描述

18. SpringCloud Stream

在实际的企业开发中,消息中间件是至关重要的组件之一。消息中间件主要解决应用解耦,异步消息,流量削锋等问题,实现高性能,高可用,可伸缩和最终一致性架构。不同的中间件其实现方式,内部结构是不一样的。如常见的RabbitMQ和Kafka,由于这两个消息中间件的架构上的不同,像RabbitMQ有exchange,kafka有Topic,partitions分区,这些中间件的差异性导致我们实际项目开发给我们造成了一定的困扰,我们如果用了两个消息队列的其中一种,后面的业务需求,我想往另外一种消息队列进行迁移,这时候无疑就是一个灾难性的,一大堆东西都要重新推倒重新做,因为它跟我们的系统耦合了,这时候 springcloud Stream 给我们提供了一种解耦合的方式。

1. 概述

Spring Cloud Stream由一个中间件中立的核组成。应用通过Spring Cloud Stream插入的input(相当于消费者consumer,它是从队列中接收消息的)和output(相当于生产者producer,它是从队列中发送消息的。)通道与外界交流。通道通过指定中间件的Binder实现与外部代理连接。业务开发者不再关注具体消息中间件,只需关注Binder对应用程序提供的抽象概念来使用消息中间件实现业务即可。
在这里插入图片描述

**说明:**最底层是消息服务,中间层是绑定层,绑定层和底层的消息服务进行绑定,顶层是消息生产者和消息消费者,顶层可以向绑定层生产消息和和获取消息消费

2. 核心概念

绑定器

Binder 绑定器是Spring Cloud Stream中一个非常重要的概念。在没有绑定器这个概念的情况下,我们的Spring Boot应用要直接与消息中间件进行信息交互的时候,由于各消息中间件构建的初衷不同,它们的实现细节上会有较大的差异性,这使得我们实现的消息交互逻辑就会非常笨重,因为对具体的中间件实现细节有太重的依赖,当中间件有较大的变动升级、或是更换中间件的时候,我们就需要付出非常大的代价来实施。

通过定义绑定器作为中间层,实现了应用程序与消息中间件(Middleware)细节之间的隔离。通过向应用程序暴露统一的Channel通过,使得应用程序不需要再考虑各种不同的消息中间件的实现。当需要升级消息中间件,或者是更换其他消息中间件产品时,我们需要做的就是更换对应的Binder绑定器而不需要修改任何应用逻辑 。甚至可以任意的改变中间件的类型而不需要修改一行代码。

发布/订阅模型

在Spring Cloud Stream中的消息通信方式遵循了发布-订阅模式,当一条消息被投递到消息中间件之后,它会通过共享的 Topic 主题进行广播,消息消费者在订阅的主题中收到它并触发自身的业务逻辑处理。这里所提到的 Topic 主题是Spring Cloud Stream中的一个抽象概念,用来代表发布共享消息给消费者的地方。在不同的消息中间件中, Topic 可能对应着不同的概念,比如:在RabbitMQ中的它对应了Exchange、而在Kakfa中则对应了Kafka中的Topic。
在这里插入图片描述

3. 入门案例

消息生产者:
  1. 导入依赖

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-stream-rabbit</artifactId>
    </dependency>
    
  2. 定义bingding

    public interface Source {
        String OUTPUT = "output";
     
        @Output("output")
        MessageChannel output();
    }
    

    这就接口声明了一个 binding 命名为 “output”。这个binding 声明了一个消息输出流,也就是消息的生 产者。

  3. 修改配置文件

    spring:
     cloud:
       stream:
         bindings:
           output:
             destination: itcast-default
             contentType: text/plain
    
    • contentType:用于指定消息的类型。具体可以参考 spring cloud stream docs
    • destination:指定了消息发送的目的地,对应 RabbitMQ,会发送到 exchange 是 itcast-default 的所有消息队列中。
  4. 测试发送消息

    /**
     * 入门案例:
     *      1.引入依赖
     *      2.配置application.yml文件
     *      3.发送消息的话,定义一个通道接口,通过接口中内置的messagechannel
     *              springcloudstream中内置接口  Source
     *      4.@EnableBinding : 绑定对应通道
     *      5.发送消息的话,通过MessageChannel发送消息
     *          * 如果需要MessageChannel --> 通过绑定的内置接口获取
     */
    @SpringBootApplication
    @EnableBinding(Source.class)
    public class Application implements CommandLineRunner {
       @Autowired
       @Qualifier("output")
       MessageChannel output;
       @Override
       public void run(String... strings) throws Exception {
    //发送MQ消息
          output.send(MessageBuilder.withPayload("hello world").build());
       }
       public static void main(String[] args) {
          SpringApplication.run(Application.class);
       }
    }
    
消息消费者:
  1. 创建工程引入依赖

    <dependency>
    	<groupId>org.springframework.cloud</groupId>
    	<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
    </dependency>
    
  2. 定义bingding

    同发送消息一致,在Spring Cloud Stream中接受消息,需要定义一个接口,如下是内置的一个接口。

    public interface Sink {
        String INPUT = "input";
        @Input("input")
        SubscribableChannel input();
    }
    

    注释 @Input 对应的方法,需要返回 SubscribableChannel ,并且参入一个参数值。

    这就接口声明了一个 binding 命名为 “input” 。

  3. 配置application.yml

    spring:
     cloud:
      stream:
       bindings:
        input:
         destination: itcast-default
    

    destination:指定了消息获取的目的地,对应于MQ就是 exchange,这里的exchange就是 itcast-default

  4. 测试

    @SpringBootApplication
    @EnableBinding(Sink.class)
    public class Application {
    // 监听 binding 为 Sink.INPUT 的消息
    @StreamListener(Sink.INPUT)
    	public void input(Message<String> message) {
    		System.out.println("监听收到:" + message.getPayload());
    	}
    	public static void main(String[] args) {
    		SpringApplication.run(Application.class);
    	}
    }
    
    • 定义一个 class (这里直接在启动类),并且添加注解**@EnableBinding(Sink.class)** ,其中Sink 就是上述的接口。同时定义一个方法(此处是 input)标明注解为**@StreamListener(Processor.INPUT)**,方法参数为 Message 。
    • 所有发送 exchange 为“itcast-default ” 的MQ消息都会被投递到这个临时队列,并且触发上述的方法。

4. 自定义消息通道

Spring Cloud Stream 内置了两种接口,分别定义了 binding 为 “input” 的输入流,和 “output” 的输出 流,而在我们实际使用中,往往是需要定义各种输入输出流。使用方法也很简单

/**
 * 自定义的消息通道
 */
public interface MyProcessor {
 
   /**
    * 消息生产者的配置
    */
   String MYOUTPUT = "myoutput";
 
   @Output("myoutput")
   MessageChannel myoutput();
 
   /**
    * 消息消费者的配置
    */
   String MYINPUT = "myinput";
 
   @Input("myinput")
   SubscribableChannel myinput();
}
  • 一个接口中,可以定义无数个输入输出流,可以根据实际业务情况划分。上述的接口,定义了一个订单输入,和订单输出两个 binding。
  • 使用时,需要在 @EnableBinding 注解中,添加自定义的接口。
  • 使用 @StreamListener 做监听的时候,需要指定 OrderProcessor.INPUT_ORDER

修改配置

spring:
  cloud:
    stream:
      bindings:
      #指定消息发送的目的地,在rabbitmq中,发送到一个itcast-default的exchange中
        output:
          destination: itcast-default  
        myoutput:
          destination: itcast-custom-output

在这里插入图片描述

5. 消息分组

通常在生产环境,我们的每个服务都不会以单节点的方式运行在生产环境,当同一个服务启动多个实例 的时候,这些实例都会绑定到同一个消息通道的目标主题(Topic)上。默认情况下,当生产者发出一 条消息到绑定通道上,这条消息会产生多个副本被每个消费者实例接收和处理,但是有些业务场景之 下,我们希望生产者产生的消息只被其中一个实例消费,这个时候我们需要为这些消费者设置消费组来 实现这样的功能。

server:
  port: 7003 #服务端口
spring:
  application:
    name: rabbitmq-consumer #指定服务名
  rabbitmq:
    addresses: 127.0.0.1
    username: guest
    password: guest
  cloud:
    stream:
      instanceCount: 2  #消费者总数
      instanceIndex: 1  #当前消费者的索引
      bindings:
        input: #内置的获取消息的通道 , 从itcast-default中获取消息
          destination: itcast-default
        myinput:
          destination: itcast-custom-output
          group: group1
          consumer:
            partitioned: true  #开启分区支持
      binders:
        defaultRabbit:
          type: rabbit

在同一个group中的多个消费者只有一个可以获取到消息并消费

6. 消息分区

有一些场景需要满足, 同一个特征的数据被同一个实例消费, 比如同一个id的传感器监测数据必须被同一个实例统计计算分析, 否则可能无法获取全部的数据。又比如部分异步任务,首次请求启动task,二次请求取消task,此场景就必须保证两次请求至同一实例
在这里插入图片描述

消息消费者配置

cloud:
   stream:
     instance-count: 2
     instance-index: 0
     bindings:
       input:
         destination: itcast-default
       inputOrder:
         destination: testChannel
         group: group-2
         consumer:
           partitioned: true
     binders:
       defaultRabbit:
         type: rabbit

从上面的配置中,我们可以看到增加了这三个参数:

  1. spring.cloud.stream.bindings.input.consumer.partitioned :通过该参数开启消费者分区功能;

  2. spring.cloud.stream.instanceCount :该参数指定了当前消费者的总实例数量;

  3. spring.cloud.stream.instanceIndex :该参数设置当前实例的索引号,从0开始,最大值为spring.cloud.stream.instanceCount 参数 - 1。我们试验的时候需要启动多个实例,可以通过运行参数来为不同实例设置不同的索引值。

消息生产者配置

spring:
 application:
   name: rabbitmq-producer #指定服务名
 rabbitmq:
   addresses: 127.0.0.1
   username: itcast
   password: itcast
   virtual-host: myhost
 cloud:
   stream:
     bindings:
       input:
         destination: itcast-default
          producer:
           partition-key-expression: payload
           partition-count: 2
     binders:
       defaultRabbit:
         type: rabbit

从上面的配置中,我们可以看到增加了这两个参数:

  1. pring.cloud.stream.bindings.output.producer.partitionKeyExpression :通过该参数指定了分区键的表达式规则,我们可以根据实际的输出消息规则来配置SpEL来生成合适的分区键;

  2. spring.cloud.stream.bindings.output.producer.partitionCount :该参数指定了消息分区的数量。

到这里消息分区配置就完成了,我们可以再次启动这两个应用,同时消费者启动多个,但需要注意的是要为消费者指定不同的实例索引号,这样当同一个消息被发给消费组时,我们可以发现只有一个消费实例在接收和处理这些相同的消息。

19. SpringCloud Config

1. 什么是配置中心

1. 配置中心概述

对于传统的单体应用而言,常使用配置文件来管理所有配置,比如SpringBoot的application.yml文件, 但是在微服务架构中全部手动修改的话很麻烦而且不易维护。微服务的配置管理一般有以下需求:

  • 集中配置管理,一个微服务架构中可能有成百上千个微服务,所以集中配置管理是很重要的。
  • 不同环境不同配置,比如数据源配置在不同环境(开发,生产,测试)中是不同的。
  • 运行期间可动态调整。例如,可根据各个微服务的负载情况,动态调整数据源连接池大小等
  • 配置修改后可自动更新。如配置内容发生变化,微服务可以自动更新配置

综上所述对于微服务架构而言,一套统一的,通用的管理配置机制是不可缺少的总要组成部分。常见的 做法就是通过配置服务器进行管理。

2. 常见配置中心

Spring Cloud Config为分布式系统中的外部配置提供服务器和客户端支持。

**Apollo(阿波罗)**是携程框架部门研发的分布式配置中心,能够集中化管理应用不同环境、不同集群的 配置,配置修改后能够实时推送到应用端,并且具备规范的权限、流程治理等特性,适用于微服务配置 管理场景

Disconf 专注于各种「分布式系统配置管理」的「通用组件」和「通用平台」, 提供统一的「配置管理服务」包括 百度、滴滴出行、银联、网易、拉勾网、苏宁易购、顺丰科技 等知名互联网公司正在使用! 「disconf」在「2015 年度新增开源软件排名 TOP 100(OSC开源中国提供)」中排名第16强。Disconf的功能特点描述图:
在这里插入图片描述

2. Spring Cloud Config简介

Spring Cloud Confifig项目是一个解决分布式系统的配置管理方案。它包含了Client和Server两个部分,server提供配置文件的存储、以接口的形式将配置文件的内容提供出去,client通过接口获取数据、并依据此数据初始化自己的应用。
在这里插入图片描述

Spring Cloud Config为分布式系统中的外部配置提供服务器和客户端支持。使用Config Server,您可 以为所有环境中的应用程序管理其外部属性。它非常适合spring应用,也可以使用在其他语言的应用 上。随着应用程序通过从开发到测试和生产的部署流程,您可以管理这些环境之间的配置,并确定应用 程序具有迁移时需要运行的一切。服务器存储后端的默认实现使用git,因此它轻松支持标签版本的配置 环境,以及可以访问用于管理内容的各种工具。

Spring Cloud Config服务端特性:

  • HTTP,为外部配置提供基于资源的API(键值对,或者等价的YAML内容)
  • 属性值的加密和解密(对称加密和非对称加密)
  • 通过使用@EnableConfigServer在Spring boot应用中非常简单的嵌入。
  • Config客户端的特性(特指Spring应用)
  • 绑定Config服务端,并使用远程的属性源初始化Spring环境。
  • 属性值的加密和解密(对称加密和非对称加密)

3. Spring Cloud Config入门

搭建服务端程序
  1. 准备工作

    码云上传配置文件
    在这里插入图片描述

  2. 导入依赖

    <dependency>
    	<groupId>org.springframework.cloud</groupId>
    	<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
    </dependency>
    
  3. 配置启动类

    @SpringBootApplication
    @EnableConfigServer //开启配置中心服务端功能
    public class ConfigServerApplication {
     
       public static void main(String[] args) {
          SpringApplication.run(ConfigServerApplication.class,args);
       }
    }
    
  4. 修改配置文件

    server:
      port: 10000 #服务端口
    spring:
      application:
        name: config-server #指定服务名
      cloud:
        config:
          server:
            git:
              uri: https://gitee.com/ppl520/config-repostory.git
    
    • 通过 spring.cloud.config.server.git.uri : 配置git服务地址
    • 通过spring.cloud.confifig.server.git.username: 配置git用户名
    • 通过spring.cloud.confifig.server.git.password: 配置git密码
修改客户端程序
  1. 导入依赖

    <dependency>
    	<groupId>org.springframework.cloud</groupId>
    	<artifactId>spring-cloud-starter-config</artifactId>
    </dependency>
    
  2. 删除application.yml

    springboot的应用配置文件,需要通过Config-server获取,这里不再需要。

  3. 添加bootstrap.yml

    spring:
      cloud:
        config:
          name: product #应用名称,需要对应git中配置文件名称的前半部分
          profile: dev #开发环境
          label: master #git中的分支
          uri: http://localhost:10000 #config-server的请求地址
    
手动刷新

我们已经在客户端取到了配置中心的值,但当我们修改gitee上面的值时,服务端(Config Server) 能实时获取最新的值,但客户端(Config Client)读的是缓存,无法实时获取最新值。SpringCloud已 经为我们解决了这个问题,那就是客户端使用post去触发refresh,获取最新数据,需要依赖spring-boot-starter-actuator

  1. 导入依赖

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    
  2. 添加标签

    对应的controller类加上@RefreshScope

@RestController
@RequestMapping("/product")
@RefreshScope //开启动态刷新
public class ProductController {

}
  1. 配置文件中暴露端点

    management:
      endpoints:
        web:
          exposure:
            include: bus-refresh
    

    在postman中访问http://localhost:9002/actuator/bus-refresh,使用post提交,查看数据已经发生了变化

4. 配置中心的高可用

在之前的代码中,客户端都是直接调用配置中心的server端来获取配置文件信息。这样就存在了一个问 题,客户端和服务端的耦合性太高,如果server端要做集群,客户端只能通过原始的方式来路由, server端改变IP地址的时候,客户端也需要修改配置,不符合springcloud服务治理的理念。 springcloud提供了这样的解决方案,我们只需要将server端当做一个服务注册到eureka中,client端去 eureka中去获取配置中心server端的服务既可。

服务端修改
  1. 依赖导入
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-config-server</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
  1. 配置文件

    eureka:
      client:
        serviceUrl:
          defaultZone: http://127.0.0.1:9000/eureka/
    
服务端改造
  1. 添加依赖

    <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>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-eureka</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    
  2. 配置文件

    spring:
      cloud:
        config:
          name: product #应用名称,需要对应git中配置文件名称的前半部分
          profile: dev #开发环境
          label: master #git中的分支
          uri: http://localhost:10000 #config-server的请求地址
          #通过注册中心获取config-server配置
          discovery:
            enabled: true #开启服务发现
            service-id: config-server
    
高可用

为了模拟生产集群环境,我们改动server端的端口为1000,再启动一个server端来做服务的负载,提供高可用的server端支持。
在这里插入图片描述

如上图就可发现会有两个server端同时提供配置中心的服务,防止某一台down掉之后影响整个系统的使用。

我们先单独测试服务端,分别访问: http://localhost:10000/product-pro.yml 、http://localhost:10001/product-pro.yml 返回信息:

eureka:
  client:
    serviceUrl:
      defaultZone: http://127.0.0.1:8761/eureka/
  instance:
    instance-id: ${spring.cloud.client.ip-address}:9002
    preferIpAddress: true
productValue: 200
server:
  port: 9002
spring:
  application:
    name: shop-service-product
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    password: 111111
    url: jdbc:mysql://localhost:3306/shop?useUnicode=true&characterEncoding=utf8
    username: root
  jpa:
    database: MySQL
    open-in-view: true
    show-sql: true

5. 消息总线bus

在微服务架构中,通常会使用轻量级的消息代理来构建一个共用的消息主题来连接各个微服务实例,它 广播的消息会被所有在注册中心的微服务实例监听和消费,也称消息总线。 SpringCloud中也有对应的解决方案,SpringCloud Bus 将分布式的节点用轻量的消息代理连接起来, 可以很容易搭建消息总线,配合SpringCloud config 实现微服务应用配置信息的动态更新。
在这里插入图片描述

根据此图我们可以看出利用Spring Cloud Bus做配置更新的步骤:

  1. 提交代码触发post请求给bus/refresh
  2. server端接收到请求并发送给Spring Cloud Bus
  3. Spring Cloud bus接到消息并通知给其它客户端
  4. 其它客户端接收到通知,请求Server端获取最新配置
  5. 全部客户端均获取到最新的配置

6. 消息总线整合配置中心

依赖导入
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--消息总线bus-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-bus</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-stream-binder-rabbit</artifactId>
</dependency>
服务端配置
server:
  port: 10000 #服务端口
spring:
  application:
    name: config-server #指定服务名
  cloud:
    config:
      server:
        git:
          uri: https://gitee.com/ppl520/config-repostory.git
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: guest
    password: guest
management:
  endpoints:
    web:
      exposure:
        include: bus-refresh
eureka:
  client:
    serviceUrl:
      defaultZone: http://127.0.0.1:9000/eureka/
  instance:
    preferIpAddress: true
    instance-id: ${spring.cloud.client.ip-address}:${server.port} #spring.cloud.client.ip-address:获取ip地址
客户端配置
spring:
  cloud:
    config:
      name: product #应用名称,需要对应git中配置文件名称的前半部分
      profile: dev #开发环境
      label: master #git中的分支
      #通过注册中心获取config-server配置
      discovery:
        enabled: true #开启服务发现
        service-id: config-server

需要在码云对应的配置文件中添加rabbitmq的配置信息

rabbitmq:
  host: 127.0.0.1
  port: 5672
  username: guest
  password: guest

重新启动对应的eureka-server , config-server , product-service。配置信息刷新后,只需要向配置 中心发送对应的请求,即可刷新每个客户端的配置

20. 配置中心Apollo

在这里插入图片描述

Apollo(阿波罗)是携程框架部门研发的分布式配置中心,能够集中化管理应用不同环境、不同集群的配置,配置修改后能够实时推送到应用端,并且具备规范的权限、流程治理等特性,适用于微服务配置管理场景。服务端基于Spring Boot和Spring Cloud开发,打包后可以直接运行,不需要额外安装Tomcat等应用容器。

1. Apollo概述

pollo(阿波罗)是携程框架部门研发的开源配置管理中心,能够集中化管理应用不同环境、不同集群 的配置,配置修改后能够实时推送到应用端,并且具备规范的权限、流程治理等特性。 正是基于配置的特殊性,所以Apollo从设计之初就立志于成为一个有治理能力的配置发布平台,目前提 供了以下的特性:

  • 统一管理不同环境、不同集群的配置
    • Apollo提供了一个统一界面集中式管理不同环境(environment)、不同集群(cluster)、 不同命名空间(namespace)的配置。
    • 同一份代码部署在不同的集群,可以有不同的配置,比如zookeeper的地址等
    • 通过命名空间(namespace)可以很方便地支持多个不同应用共享同一份配置,同时还允许 应用对共享的配置进行覆盖
  • 版本发布管
    • 所有的配置发布都有版本概念,从而可以方便地支持配置的回滚
  • 灰度发布
    • 支持配置的灰度发布,比如点了发布后,只对部分应用实例生效,等观察一段时间没问题后 再推给所有应用实例
  • 权限管理、发布审核、操作审计
    • 应用和配置的管理都有完善的权限管理机制,对配置的管理还分为了编辑和发布两个环节, 从而减少人为的错误。
    • 所有的操作都有审计日志,可以方便地追踪问题
  • 客户端配置信息监控
    • 可以在界面上方便地看到配置在被哪些实例使用
  • 提供Java和.Net原生客户端
    • 提供了Java和.Net的原生客户端,方便应用集成
    • 支持Spring Placeholder, Annotation和Spring Boot的ConfigurationProperties,方便应用 使用(需要Spring 3.1.1+)
    • 同时提供了Http接口,非Java和.Net应用也可以方便地使用
  • 部署简单
    • 配置中心作为基础服务,可用性要求非常高,这就要求Apollo对外部依赖尽可能地少
    • 目前唯一的外部依赖是MySQL,所以部署非常简单,只要安装好Java和MySQL就可以让
    • Apollo跑起来 Apollo还提供了打包脚本,一键就可以生成所有需要的安装包,并且支持自定义运行时参数

2. Apollo的实现方式

在这里插入图片描述

Apollo客户端的实现原理:

  1. 客户端和服务端保持了一个长连接,从而能第一时间获得配置更新的推送。
  2. 客户端还会定时从Apollo配置中心服务端拉取应用的最新配置。
    1. 这是一个fallback机制,为了防止推送机制失效导致配置不更新
    2. 客户端定时拉取会上报本地版本,所以一般情况下,对于定时拉取的操作,服务端都会返回 304 – Not Modified
    3. 定时频率默认为每5分钟拉取一次,客户端也可以通过在运行时指定System Property: apollo.refreshInterval 来覆盖,单位为分钟。
  3. 客户端从Apollo配置中心服务端获取到应用的最新配置后,会保存在内存中
  4. 客户端会把从服务端获取到的配置在本地文件系统缓存一份 在遇到服务不可用,或网络不通的时候,依然能从本地恢复配置
  5. 应用程序从Apollo客户端获取最新的配置、订阅配置更新通知

3. 搭建Apollo服务端

1. 环境要求

java:

  • Apollo服务端:1.8+
  • Apollo客户端:1.7+

由于需要同时运行服务端和客户端,所以建议安装Java 1.8+。

MySQL:

  • 版本要求:5.6.5+

Apollo的表结构对 timestamp 使用了多个default声明,所以需要5.6.5以上版本

2. 环境搭建

(1) 下载Apollo

通过官网提供的下载连接https://github.com/nobodyiam/apollo-build-scripts下载安装包

(2) 配置数据库

Apollo服务端共需要两个数据库: ApolloPortalDB 和 ApolloConfigDB ,我们把数据库、表的创建和 样例数据都分别准备了sql文件,只需要导入数据库即可。

(3) 配置数据库连接

Apollo服务端需要知道如何连接到你前面创建的数据库,所以需要编辑demo.sh,修改ApolloPortalDB 和ApolloConfigDB相关的数据库连接串信息。

#apollo config db info
apollo_config_db_url=jdbc:mysql://localhost:3306/ApolloConfigDB?
characterEncoding=utf
apollo_config_db_username=用户名
apollo_config_db_password=密码(如果没有密码,留空即可)

(4)启动

./demo.sh start

当看到如下输出后,就说明启动成功了!

==== starting service ====
Service logging file is ./service/apollo-service.log
Started [10768]
Waiting for config service startup.......
Config service started. You may visit http://localhost:8080 for service status 
now!
Waiting for admin service startup....
Admin service started
==== starting portal ====
Portal logging file is ./portal/apollo-portal.log
Started [10846]
Waiting for portal startup......
Portal started. You can visit http://localhost:8070 now!

(5)测试

通过浏览器打开 http://ip:8070 即可访问Apollo配置中心的前端页面
在这里插入图片描述

输入默认用户名密码apollo/admin即可登录到应用中
在这里插入图片描述

4. 客户端集成

Apollo的客户端jar包已经上传到中央仓库,应用在实际使用时只需要按照如下方式引入即可。

<dependency>
    <groupId>com.ctrip.framework.apollo</groupId>
    <artifactId>apollo-client</artifactId>
    <version>1.1.0</version>
</dependency>
1. Spring Boot集成

Spring Boot支持通过application.properties/bootstrap.properties来配置,该方式能使配置在更早的阶段注入,比如使用 @ConditionalOnProperty 的场景或者是有一些spring-boot-starter在启动阶段就需要读取配置做一些事情(如dubbo-spring-boot-project),所以对于Spring Boot环境建议通过以下方式来接入Apollo(需要0.10.0及以上版本)。

使用方式很简单,只需要在application.properties/bootstrap.properties中按照如下样例配置即可。

  1. 注入默认 application namespace的配置示例

    # will inject 'application' namespace in bootstrap phase
    apollo.bootstrap.enabled = true
    
  2. 注入非默认 application namespace或多个namespace的配置示例

     apollo.bootstrap.enabled = true
     apollo.bootstrap.namespaces = application,FX.apollo,application.yml
    
  3. 将Apollo配置加载提到初始化日志系统之前(1.2.0+)

    从1.2.0版本开始,如果希望把日志相关的配置(如 logging.level.root=info 或 logback-spring.xml 中的参数)也放在Apollo管理,那么可以额外配置apollo.bootstrap.eagerLoad.enabled=true 来使Apollo的加载顺序放到日志系统加载之前,不过这会导致Apollo的启动过程无法通过日志的方式输出(因为执行Apollo加载的时候,日志系统压根没有准备好呢!所以在Apollo代码中使用Slf4j的日志输出便没有任何内容),更多信息可以参考PR 1614。参考配置示例如下:

    # will inject 'application' namespace in bootstrap phase
    apollo.bootstrap.enabled = true
    # put apollo initialization before logging system initialization
    apollo.bootstrap.eagerLoad.enabled=true
    

cloud-bus


org.springframework.cloud
spring-cloud-stream-binder-rabbit


#### 服务端配置

```yml
server:
  port: 10000 #服务端口
spring:
  application:
    name: config-server #指定服务名
  cloud:
    config:
      server:
        git:
          uri: https://gitee.com/ppl520/config-repostory.git
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: guest
    password: guest
management:
  endpoints:
    web:
      exposure:
        include: bus-refresh
eureka:
  client:
    serviceUrl:
      defaultZone: http://127.0.0.1:9000/eureka/
  instance:
    preferIpAddress: true
    instance-id: ${spring.cloud.client.ip-address}:${server.port} #spring.cloud.client.ip-address:获取ip地址
客户端配置
spring:
  cloud:
    config:
      name: product #应用名称,需要对应git中配置文件名称的前半部分
      profile: dev #开发环境
      label: master #git中的分支
      #通过注册中心获取config-server配置
      discovery:
        enabled: true #开启服务发现
        service-id: config-server

需要在码云对应的配置文件中添加rabbitmq的配置信息

rabbitmq:
  host: 127.0.0.1
  port: 5672
  username: guest
  password: guest

重新启动对应的eureka-server , config-server , product-service。配置信息刷新后,只需要向配置 中心发送对应的请求,即可刷新每个客户端的配置

20. 配置中心Apollo

[外链图片转存中…(img-zTPJXuk4-1641129771316)]

Apollo(阿波罗)是携程框架部门研发的分布式配置中心,能够集中化管理应用不同环境、不同集群的配置,配置修改后能够实时推送到应用端,并且具备规范的权限、流程治理等特性,适用于微服务配置管理场景。服务端基于Spring Boot和Spring Cloud开发,打包后可以直接运行,不需要额外安装Tomcat等应用容器。

1. Apollo概述

pollo(阿波罗)是携程框架部门研发的开源配置管理中心,能够集中化管理应用不同环境、不同集群 的配置,配置修改后能够实时推送到应用端,并且具备规范的权限、流程治理等特性。 正是基于配置的特殊性,所以Apollo从设计之初就立志于成为一个有治理能力的配置发布平台,目前提 供了以下的特性:

  • 统一管理不同环境、不同集群的配置
    • Apollo提供了一个统一界面集中式管理不同环境(environment)、不同集群(cluster)、 不同命名空间(namespace)的配置。
    • 同一份代码部署在不同的集群,可以有不同的配置,比如zookeeper的地址等
    • 通过命名空间(namespace)可以很方便地支持多个不同应用共享同一份配置,同时还允许 应用对共享的配置进行覆盖
  • 版本发布管
    • 所有的配置发布都有版本概念,从而可以方便地支持配置的回滚
  • 灰度发布
    • 支持配置的灰度发布,比如点了发布后,只对部分应用实例生效,等观察一段时间没问题后 再推给所有应用实例
  • 权限管理、发布审核、操作审计
    • 应用和配置的管理都有完善的权限管理机制,对配置的管理还分为了编辑和发布两个环节, 从而减少人为的错误。
    • 所有的操作都有审计日志,可以方便地追踪问题
  • 客户端配置信息监控
    • 可以在界面上方便地看到配置在被哪些实例使用
  • 提供Java和.Net原生客户端
    • 提供了Java和.Net的原生客户端,方便应用集成
    • 支持Spring Placeholder, Annotation和Spring Boot的ConfigurationProperties,方便应用 使用(需要Spring 3.1.1+)
    • 同时提供了Http接口,非Java和.Net应用也可以方便地使用
  • 部署简单
    • 配置中心作为基础服务,可用性要求非常高,这就要求Apollo对外部依赖尽可能地少
    • 目前唯一的外部依赖是MySQL,所以部署非常简单,只要安装好Java和MySQL就可以让
    • Apollo跑起来 Apollo还提供了打包脚本,一键就可以生成所有需要的安装包,并且支持自定义运行时参数

2. Apollo的实现方式

[外链图片转存中…(img-sOv9W2GY-1641129771317)]

Apollo客户端的实现原理:

  1. 客户端和服务端保持了一个长连接,从而能第一时间获得配置更新的推送。
  2. 客户端还会定时从Apollo配置中心服务端拉取应用的最新配置。
    1. 这是一个fallback机制,为了防止推送机制失效导致配置不更新
    2. 客户端定时拉取会上报本地版本,所以一般情况下,对于定时拉取的操作,服务端都会返回 304 – Not Modified
    3. 定时频率默认为每5分钟拉取一次,客户端也可以通过在运行时指定System Property: apollo.refreshInterval 来覆盖,单位为分钟。
  3. 客户端从Apollo配置中心服务端获取到应用的最新配置后,会保存在内存中
  4. 客户端会把从服务端获取到的配置在本地文件系统缓存一份 在遇到服务不可用,或网络不通的时候,依然能从本地恢复配置
  5. 应用程序从Apollo客户端获取最新的配置、订阅配置更新通知

3. 搭建Apollo服务端

1. 环境要求

java:

  • Apollo服务端:1.8+
  • Apollo客户端:1.7+

由于需要同时运行服务端和客户端,所以建议安装Java 1.8+。

MySQL:

  • 版本要求:5.6.5+

Apollo的表结构对 timestamp 使用了多个default声明,所以需要5.6.5以上版本

2. 环境搭建

(1) 下载Apollo

通过官网提供的下载连接https://github.com/nobodyiam/apollo-build-scripts下载安装包

(2) 配置数据库

Apollo服务端共需要两个数据库: ApolloPortalDB 和 ApolloConfigDB ,我们把数据库、表的创建和 样例数据都分别准备了sql文件,只需要导入数据库即可。

(3) 配置数据库连接

Apollo服务端需要知道如何连接到你前面创建的数据库,所以需要编辑demo.sh,修改ApolloPortalDB 和ApolloConfigDB相关的数据库连接串信息。

#apollo config db info
apollo_config_db_url=jdbc:mysql://localhost:3306/ApolloConfigDB?
characterEncoding=utf
apollo_config_db_username=用户名
apollo_config_db_password=密码(如果没有密码,留空即可)

(4)启动

./demo.sh start

当看到如下输出后,就说明启动成功了!

==== starting service ====
Service logging file is ./service/apollo-service.log
Started [10768]
Waiting for config service startup.......
Config service started. You may visit http://localhost:8080 for service status 
now!
Waiting for admin service startup....
Admin service started
==== starting portal ====
Portal logging file is ./portal/apollo-portal.log
Started [10846]
Waiting for portal startup......
Portal started. You can visit http://localhost:8070 now!

(5)测试

通过浏览器打开 http://ip:8070 即可访问Apollo配置中心的前端页面

[外链图片转存中…(img-a4T5G3Uh-1641129771317)]

输入默认用户名密码apollo/admin即可登录到应用中

[外链图片转存中…(img-MpDUmtOx-1641129771317)]

4. 客户端集成

Apollo的客户端jar包已经上传到中央仓库,应用在实际使用时只需要按照如下方式引入即可。

<dependency>
    <groupId>com.ctrip.framework.apollo</groupId>
    <artifactId>apollo-client</artifactId>
    <version>1.1.0</version>
</dependency>
1. Spring Boot集成

Spring Boot支持通过application.properties/bootstrap.properties来配置,该方式能使配置在更早的阶段注入,比如使用 @ConditionalOnProperty 的场景或者是有一些spring-boot-starter在启动阶段就需要读取配置做一些事情(如dubbo-spring-boot-project),所以对于Spring Boot环境建议通过以下方式来接入Apollo(需要0.10.0及以上版本)。

使用方式很简单,只需要在application.properties/bootstrap.properties中按照如下样例配置即可。

  1. 注入默认 application namespace的配置示例

    # will inject 'application' namespace in bootstrap phase
    apollo.bootstrap.enabled = true
    
  2. 注入非默认 application namespace或多个namespace的配置示例

     apollo.bootstrap.enabled = true
     apollo.bootstrap.namespaces = application,FX.apollo,application.yml
    
  3. 将Apollo配置加载提到初始化日志系统之前(1.2.0+)

    从1.2.0版本开始,如果希望把日志相关的配置(如 logging.level.root=info 或 logback-spring.xml 中的参数)也放在Apollo管理,那么可以额外配置apollo.bootstrap.eagerLoad.enabled=true 来使Apollo的加载顺序放到日志系统加载之前,不过这会导致Apollo的启动过程无法通过日志的方式输出(因为执行Apollo加载的时候,日志系统压根没有准备好呢!所以在Apollo代码中使用Slf4j的日志输出便没有任何内容),更多信息可以参考PR 1614。参考配置示例如下:

    # will inject 'application' namespace in bootstrap phase
    apollo.bootstrap.enabled = true
    # put apollo initialization before logging system initialization
    apollo.bootstrap.eagerLoad.enabled=true
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值