《Spring Cloud学习笔记:Eureka & Nacos Discovery & Ribbon》

1. 服务拆分和远程调用

  • 任何分布式架构都离不开服务的拆分,微服务也一样。
  • 服务拆分:一个单体架构按照功能模块进行拆分,变成多个服务。

微服务需要根据业务模块拆分,做到单一职责,不要重复开发相同业务。 

1.1 服务拆分原则 或 服务拆分注意事项:

微服务拆分时粒度要小,模块内高内聚,模块间低耦合: 

  1. 不同的微服务,要做到单一职责(微服务拆分的目的就是单一职责),不要重复开发相同业务
  2. 微服务要做到数据独立,不同微服务都应该有自己独立的数据库,每个微服务都会有自己的数据库,不要访问其它微服务的数据库,做到了数据解耦。
  3. 微服务要对外暴露RESTful的业务接口,供其它微服务调用,并且一定要保证微服务对外接口的稳定性(即:尽量保证接口外观不变)!

怎么拆

之前我们说过,微服务拆分时粒度要小,这其实是拆分的目标。具体可以从两个角度来分析:

  • 高内聚:每个微服务的职责要尽量单一,包含的业务相互关联度高、完整度高。

  • 耦合:每个微服务的功能要相对独立,尽量减少对其它微服务的依赖,或者依赖接口的稳定性要强。

高内聚首先是单一职责,但不能说一个微服务就一个接口,而是要保证微服务内部业务的完整性为前提。目标是当我们要修改某个业务时,最好就只修改当前微服务,这样变更的成本更低。

一旦微服务做到了高内聚,那么服务之间的耦合度自然就降低了。

当然,微服务之间不可避免的会有或多或少的业务交互,比如下单时需要查询商品数据。这个时候我们不能在订单服务直接查询商品数据库,否则就导致了数据耦合。而应该由商品服务对应暴露接口,并且一定要保证微服务对外接口的稳定性(即:尽量保证接口外观不变)。虽然出现了服务间调用,但此时无论你如何在商品服务做内部修改,都不会影响到订单微服务,服务间的耦合度就降低了。

拆分方式 - 服务拆分时的两种方式:

  • 纵向拆分:就是按照项目的功能模块来拆分,这种拆分模式可以尽可能提高服务的内聚性。
  • 横向拆分:是看各个功能模块之间有没有公共的业务部分,如果有将其抽取出来作为通用服务,这样可以提高业务的复用性,避免重复开发,同时通用业务一般接口稳定性较强,也不会使服务之间过分耦合。

一般微服务项目会有两种不同的工程结构: 

1. 完全解耦:每一个微服务都创建为一个独立的工程,甚至可以使用不同的开发语言来开发,项                          目完全解耦。

  • 优点:服务之间耦合度低
  • 缺点:每个项目都有自己的独立仓库,管理起来比较麻烦

2. Maven聚合:整个项目为一个Project,然后每个微服务是其中的一个Module

  • 优点:项目代码集中,管理和运维方便(授课也方便)
  • 缺点:服务之间耦合,编译时间较长

注意:在企业开发的生产环境中,每一个微服务都应该有自己的独立数据库服务,而不仅仅是                   database。 

1.2 实现微服务远程调用案例:

  • 需求:根据订单ID查询订单的同时,把订单所属的用户信息一起返回。

  • 要想实现跨服务远程调用,就要把 / 将原来的本地方法调用,改造成跨微服务的远程过程调用RPC,即Remoet Produce Call),微服务之间的远程调用被称为RPC,即远程过程调用!

服务拆分之后,不可避免的会出现跨微服务的业务,此时微服务之间就需要进行远程调用。

微服务之间的远程调用被称为RPC,即远程过程调用。

RPC的实现方式有很多,比如:

  • 基于HTTP协议
  • 基于Dubbo协议

要想实现跨服务远程调用,其实就是发送一次HTTP的请求~! 

Http方式,这种方式不关心服务提供者的具体技术实现,只要对外暴露Http接口即可,更符合微服务的需要。 

1.3 服务间远程调用方式分析

微服务的调用方式: 

  • 基于RestTemplate发起的HTTP请求实现远程调用!
  • HTTP请求做远程调用是与语言无关的调用,这种方式不关心服务提供者的具体技术实现,只要对外暴露HTTP接口即可,只要知道对方的IP、端口、请求路径、请求参数即可,更符合微服务的需要。
转变为,如何在Java代码当中发送HTTP请求?

  • 利用Spring提供的RestTemplate来发送HTTP请求! 
    • RestTemplate其中提供了大量的方法,方便我们发生HTTP的请求,常见的Get、Post、Put、Delete请求都支持,如果请求参数比较复杂,还可以使用exchange方法来构造请求。

注册RestTemplate到Spring容器,调用RestTemplate的API发送HTTP请求,常见的方法有:

  • getForObject:发送Get请求并返回指定类型对象
  • postForObject:发送Post请求并返回指定类型对象
  • put:发生PUT请求
  • delete:发生Delete请求
  • exchange:发送任意类型请求,返回ResponseEntity

因此,我们只能在order-service向user-service发起一个远程调用,发起一个HTTP的请求,调用根据ID查询用户的这个接口,远程调用的大概的步骤是这样的:

  1. 在需要发送远程调用模块的启动类当中注册一个RestTemplate的实例到Spring容器
  2. 修改业务代码(在业务代码当中注入RestTemplate ),使用RestTemplate提供的API来发起HTTP请求:修改order-service服务中的OrderService类中的queryOrderById方法,根据Order对象中的userId查询User
  3. 将查询的User填充到Order对象,一起返回

1. 我们知道,Bean的注入只能放在配置类里面,而启动类就是一个配置类:

2.  在业务代码当中注入RestTemplate,使用RestTemplate提供的API来发起HTTP请求

注册中心原理 

服务调用 

  • 在微服务架构中,最常见的场景就是微服务之间的相互调用。 

举例:

商品微服务:

  • 对外提供查询商品列表的接口
  • 对外提供查询查询某个商品的信息接口

订单微服务:

  • 对外提供创建订单接口 

以下单为例子,客户向订单系统发起一个下单的请求,系统在进行保存订单之前需要调用商品系统查询商品的信息:

一般把调用方称为服务消费者,把被调用方称为服务提供者。 

根据一个或多个系统中,不同服务之间形成的一个完整的调用链,来形成服务的上下游关系:

  • 上游服务:向外提供服务,属于被调用方,多数是服务提供者
  • 下游服务:消费上游服务,属于调用方,多数是服务消费者 

下游服务调用上游服务~!

1.4 提供者与消费者

在服务调用关系中,在微服务远程调用的过程中,会有两个不同的角色:
  • 服务提供者(被调用方):一次业务中,被其它微服务调用的服务(提供或暴露接口给其它微服务调用)
  • 服务消费者(调用方):一次业务中调用其它微服务的服务(调用其它微服务提供的接口) 
  • 服务提供者与服务消费者的角色是相对的,相对于具体的业务,业务不同,角色是会变化的!
服务消费者  调用  服务提供者
注意:
  • 服务提供者与服务消费者的角色并不是绝对的,而是相对于业务而言,一个服务既可以是提供者,也可以是消费者! 
  • 技术要跟业务相关联!
思考:服务A调用服务B,服务B调用服务C,那么服务B是什么角色?
  • 对于服务A调用服务B的业务而言,A是服务消费者,B是服务提供者
  • 对于服务B调用服务C的业务而言,B是服务消费者,C是服务提供者

因此,服务B既可以是服务提供者,也可以是服务消费者! 

总结:一个服务既可以是服务提供者,又可以是服务消费者! 

服务治理中的三个角色 

在大型微服务项目中,服务提供者的数量会非常多,为了管理这些服务就引入了注册中心的概念。注册中心、服务提供者、服务消费者三者间关系如下:

在服务治理框架中,都会构建一个注册中心(管理IP、端口的地方) :

  • 注册中心:记录并监控微服务各实例状态,推送服务变更信息 
  • 注册中心的功能:动态的实现服务治理

什么是服务治理? 

  • 服务治理是微服务架构中最核心最基本的模块,用于实现各个微服务的自动化注册与发现。
  • 大白话:各个微服务IP、端口的统一管理(CRUD) 

流程如下:

  • 服务启动时就会注册自己的服务信息(服务名、IP、端口)到注册中心

  • 服务调用者可以从注册中心订阅想要的服务,获取服务对应的实例列表(1个服务可能多实例部署)

  • 服务调用者自己对实例列表负载均衡,挑选一个实例

  • 服务调用者向该实例发起远程调用

当服务提供者的实例宕机或者启动新实例时,调用者如何得知呢?

  • 服务提供者会定期向注册中心发送请求,报告自己的健康状态(心跳请求)

  • 当注册中心长时间收不到提供者的心跳时,会认为该实例宕机,将其从服务的实例列表中剔除

  • 当服务有新实例启动时,会发送注册服务请求,其信息会被记录在注册中心的服务实例列表

  • 当注册中心服务列表变更时,会主动通知微服务,更新本地服务列表

服务消费者如何知道服务提供者的地址? 

  • 服务提供者会在启动时注册自己的信息到注册中心,服务消费者可以从注册中心订阅和拉取服务信息。

服务消费者如何得知服务状态变更? 

  • 服务提供者通过心跳机制向注册中心报告自己的健康状态,当心跳异常时注册中心会将异常服务剔除,并通知了订阅该服务的服务消费者。

当服务提供者有多个实例时,服务消费者该选择哪一个?

  • 服务消费者可以通过负载均衡算法,从多个实例中选择一个。 

2. Eureka注册中心

2.1  远程调用的问题

回顾:
  • 订单服务(服务消费者需要远程调用我们的用户服务(服务提供者),通过HTTP请求实现了跨微服务的远程调用但是这种手动发送HTTP请求的方式存在一些问题之前定义的的URL路径中的IP、端口、请求路径等都是硬编码,写死的,不够灵活。
假如我们的服务提供者被调用较多,为了应对更高的并发,进行了多实例部署,部署了多个实例,如图:
思考:
  1. 服务提供者这么多实例,服务消费者在发起远程调用的时候,该如何得知每一个服务提供者实例的IP地址和端口?
  2. HTTP请求要写URL地址,有多个服务提供者实例地址,服务消费者调用时该如何选择,到底该调用哪个实例呢?
  3. 服务消费者如何得知服务提供者的健康状态?服务消费者如何得知某个服务提供者实例是否依然健康,是不是已经宕机? 如果在运行过程中,某一个服务提供者实例突然宕机,服务消费者依然在调用该怎么办?
  4. 如果并发太高,服务提供者临时多部署了N台实例,服务消费者如何知道新实例的IP地址?
这些问题都需要利用Spring Cloud中的注册中心来解决其中最广为人知的注册中心就是Eureka。

目前开源的注册中心框架有很多,国内比较常见的有: 

  • Zookeeper:Zookeeper是一个分布式服务框架,是Apache Hadoop的一个子项目,它主要是用来解决分布式应用中经常遇到的一些数据管理问题,如:统一命名服务、状态同步服务、集群管理、分布式应用配置项的管理等。
  • Eureka:Netflix公司出品,目前被集成在Spring Cloud Netflix当中,一般用于Java应用,但是现在已经闭源,早期版本的Spring Cloud都是使用Eureka作为注册中心
  • Nacos:Aliabab公司出品,目前被集成在Spring Cloud Alibaba中,Nacos是一个更易于构建云原生应用的服务动态发现、配置管理和服务管理平台,一般用于Java应用
  • Consul:Consul是基于Go语言开发的开源工具,主要面向分布式,服务化的系统提供服务注册、服务发现和配置管理的功能,HashiCorp公司出品,目前集成在Spring Cloud中,不限制微服务语言,Consul本身只是一个二进制的可执行文件,所以安装和部署都非常简单,只需要从官网下载后,再执行对应的启动脚本即可 

Eureka和Nacos注册中心的starter中提供的功能都遵循Spring Cloud中的API规范,都是基于Spring Cloud Common规范,因此在业务开发使用上没有太太差异由于Nacos是国内产品,中文文档比较丰富,而且同时具备配置管理功能,因此在国内使用较多! 

2.2 Eureka注册中心原理 - Eureka的结构和作用

Eureka的结构如下:

服务注册与发现模型
在Eureka的结构或架构当中,有两类角色(微服务角色有两类): 
  1. 角色一:Eureka-Server注册中心(Eureka服务端):记录和管理这些微服务(记录服务信息、心跳监控)!
  2. 角色二:服务提供者和服务消费者,不管是服务提供者还是服务消费者,都是微服务,所以统称为Eureka的客户端 - Eureka Client端。

在Spring Cloud的生态中,采用服务注册与发现模型,来实现微服务之间的互相发现发现与调用: 

服务注册与发现模型

如上图所示,通过在微服务系统中引入一个叫注册中心的组件,来作为协调者。

其最简化的过程是,所有的微服务应用在启动过程中会将自己的服务信息,包含服务名称、主机IP地址和端口号等信息发送到注册中心中,这个叫服务注册,然后上游的微服务(服务消费者)在处理请求过程中,根据服务名称到注册中心中查找对应服务的所有实例IP地址和端口号,因为一个服务可能多实例部署(服务发现或服务拉取),然后服务消费者,也就是调用者对实例列表做负载均衡,挑选一个实例冰箱该实例发起远程调用,从而让分散的微服务系统之间能像一个整体一样对外提供请求处理能力。 

回答之前的三个问题:

问题一:服务消费者在发起远程调用的时候,如何得知每一个服务提供者实例的IP地址? 

获取地址信息的流程如下:

  • 所有的微服务应用的实例在启动过程中会将自身包含服务名称、主机IP地址和端口号等信息发送或注册到Eureka-Server注册中心(Eureka服务端)中,这个叫服务注册
  • Eureka-Server注册中心(Eureka服务端)保存服务名称到服务实例地址列表的映射关系;
  • 服务消费者根据服务名称,拉取服务列表,拉取对应服务的所有实例IP地址和端口号列表,这个叫服务发现或服务拉取。
问题二:服务消费者如何从多个服务提供者实例中选择具体的实例?
  • 服务消费者从实例列表中利用负载均衡算法选中一个实例地址(选中一个微服务),接着向该实例地址(微服务)发起远程调用! 
问题三:服务消费者如何得知某个服务提供者实例是否依然健康,是不是已经宕机?

心跳监控

  • 服务提供者(所有的微服务应用)会每隔一段时间(默认30秒)向Eureka-Server注册中心(Eureka服务端)发起或发送一次心跳请求,确认自己的健康状态,称为心跳请求;
  • Eureka会更新服务记录列表信息:当服务提供者超过一定时间没有发送心跳时,Eureka-Server注册中心(Eureka服务端)会认为该微服务实例故障或宕机,会将该实例从服务的实例列表中剔除(心跳不正常的会被剔除);
  • 这样,服务消费者在拉取服务时,就能将故障实例排除了。

问题四:如果并发太高,服务提供者临时多部署了N台实例,服务消费者如何知道新实例的IP地址? 

  • 当服务有新实例启动时,会发送注册服务请求,其信息会被记录在注册中心的服务实例列表!
  • 当注册中心的服务实例列表变更时,会主动通知微服务,更新本地服务列表! 
注意:
  • 一个微服务,既可以是服务提供者,又可以是服务发现者,因此Eureka将服务注册、服务发现等功能统一封装到了Eureka-Client(Eureka客户端)!

因此,接下来我们动手实践的步骤包括: 

2.3 搭建EurekaServer

Eureka-Server - 注册中心服务端的搭建,必须创建一个独立的微服务! 

在cloud-demo父工程下,创建一个子模块:

填写模块信息:  

然后填写服务信息:

搭建Eureka - Server服务的步骤如下:

1. 创建项目,引入eureka-server依赖,引入Spring Cloud为eureka提供的starter依赖:spring-cloud-starter-netflix-eureka-server

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>

2. 编写启动类,添加@EnableEurekaServer注解(EurekaServer自动装配的开关)

package cn.itcast.eureka;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

/**
 * 手动编写启动类
 * 给Eureka-Server服务编写一个启动类
 * 注意:一定要添加@EnableEurekaServer注解,开启Eureka的注册中心功能
 */
@SpringBootApplication
@EnableEurekaServer
public class EurekaApplication {
    public static void main(String[] args) {
        SpringApplication.run(EurekaApplication.class,args);
    }
}

3. 添加application.yml文件,在application.yml中配置eureka地址 或 编写下面的配置:

  • Eureka自己也是一个微服务,所以Eureka在启动的时候会将自己也注册到Eureka注册中心中,这是为了将来Eureka集群之间通信去用的。
  • 是为了做服务的注册采取配置的Eureka的服务名称以及Eureka的地址信息!
注意等级关系,否则配置会失效!!!
server:
  port: 10086  #服务的端口,随便给定一个端口号
spring:
  application:
    name: eureka-server #服务的名称(指定Eureka的服务名称)
eureka:
  client:
    service-url:  #Eureka的地址信息(如果是集群,则地址之间用逗号隔开,现在是单机)
      defaultZone: http://127.0.0.1:10086/eureka
启动服务:
  • 启动微服务,然后在浏览器访问:http://127.0.0.1:10086 

看到下面Eureka的管理页面就算是成功了:  

  • 实例:一个服务每部署一份儿就是一个实例! 
  • Eureka会记录一个服务的所有实例!

2.4 服务注册

  • 服务注册:保存服务提供者和服务调用者的信息 

下面,我们将服务提供者注册到eureka-server注册中心中去,服务注册需要经过两个步骤:

1. 在服务提供者的项目中引入eureka-client依赖,引入spring-cloud-starter-netfix-eureka-            client的依赖

<!-- eureka-client(Eureka客户端依赖)-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>

2. 在application.yml文件中,添加服务名称以及配置eureka地址,编写下面的配置:

spring
  application:
    name: user-service #服务的名称
  eureka:
    client:
      service-url: #Eureka的地址信息(如果是集群,则地址之间用逗号隔开,现在是单机)
        defaultZone: http://127.0.0.1:10086/eureka

将服务消费者的信息也注册到EurekaServer中去,服务列表验证: 

一个服务启动多个实例的操作

  • 我们可以将一个服务多次启动,模拟多实例部署,但为了避免端口冲突,需要修改端口设置 

首先,复制原来的服务启动配置 - 复制配置:

然后,在弹出的窗口中,填写信息,配置启动项,注意重命名配置新的端口号,避免端口冲突(重新配置一个端口以及修改服务名称):

  • -D代表就是参数了! 

添加成功:

查看Erueka-Server的管理页面:

总结:
  • 凡是引Eureka客户端依赖 + 配置Eureka地址就是在做服务的注册! 
  • 无论是服务消费者还是服务提供者,只要引入eureka-client依赖、配置eureka地址后,都可以完成服务注册!

2.5 服务发现 /  服务拉取

  • 服务消费者要去注册中心订阅服务,这个过程就是服务发现或服务拉取。
  • 服务拉取是基于服务名称获取服务列表,然后在对服务列表做负载均衡。 

下面,我们将服务消费者的逻辑修改:向eureka-server注册中心拉取服务提供者的信息,实现服务发现或服务拉取。 

服务消费者完成服务拉取的步骤: 

1. 在服务消费者的pom文件中,引入eureka-client依赖

  • 之前说过,服务发现或服务拉取、服务注册统一都封装在eureka-client依赖,因此这一步与服务注册时一致。 
<!-- eureka-client(Eureka客户端依赖)-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>

2. 配置文件

  • 服务发现或服务拉取也需要知道Eureka地址,因此第二步与服务注册一样,都是配置Eureka信息,在服务消费者的application.yml文件中,添加服务名称以及eureka地址:
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/cloud_order?useSSL=false
    username: root
    password: 123456
    driver-class-name: com.mysql.jdbc.Driver
  application:
    name: order-service #服务的名称
eureka:
  client:
    service-url:  #Eureka的地址信息(如果是集群,则地址之间用逗号隔开,现在是单机)
      defaultZone: http://127.0.0.1:10086/eureka

3. 服务的拉取和负载均衡

服务调用者去eureka-server注册中心中拉取服务提供者的实例列表,并且实现负载均衡,虽然服务提供者有多个实例,但真正发起调用时只需要知道一个实例的地址,因此服务调用者必须利用负载均衡算法,从多个实例中挑选一个去访问。

什么是负载均衡? 

  • 负载均衡可以将流量分散到多个服务器上,避免了单一服务器的过载问题,同时可以提高系统的整体吞吐量和可用性。

常见的负载均衡算法有:

  • 随机负载均衡(Round):随机选择一台服务器来处理每个请求。
  • 轮询负载均衡算法(Round Robin):依次将请求分发给每台服务器,循环往复,适用于负载相对均衡的环境。
  • 权重轮询(Weighted Round Robin):为每个服务器分配不同的权重值,按照权重比例分发请求,高权重的服务器将接收到更多的请求,适用于服务器性能不均衡的情况。
  • IP的Hash散列算法(IP_Hash => IP绑定):即相同IP的请求转发到同一台服务器,基于客户端的IP地址进行哈希运算,然后将请求发送给对应的服务器,这样每个访客固定访问一个后端服务器,可以解决分布式Session问题
  • 最少连接算法(Least Connection):将请求分发给当前连接数最少的服务器。
  • fair - 最短响应时间算法:按后端服务器的响应事件来分配请求,将请求分发给响应时间最短的服务器。
  • url_hash:按访问URL的hash结果来分配请求,使每个url定向到同一个后端服务器。
  • 自适应负载均衡(Adaptive Load Balancing):基于实时监测服务器的负载和性能指标,动态调整请求的分发策略。

3.1 修改服务消费者的业务代码,修改访问的URL路径,用服务提供者的服务名称代替IP以及端口        从而实现远程调用: 

3.2 在服务消费者项目的启动类中,给RestTemplate这个Bean添加负载均衡注解 -                              @LoadBalanced注解: 

  • LoadBalanced:负载均衡 

重启服务消费者,清空服务提供者实例的控制台日志,看看服务消费者到底访问的是哪一个服务提供者的实例? 

  • 发现发起两次请求,有时候会远程调用同一个服务实例,有时候会两个服务实例都被远程调用! 

Spring会自动帮助我们从eureka-server端,根据服务名称来获取服务实例列表,而后完成负载均衡! 

  • 负载均衡的目的:避免请求堆积在同一个服务器上!

3. Ribbon负载均衡

  • 负载均衡器 - Ribbon - 远程调用负载均衡Ribbon

背景铺垫

  • 微服务项目有一个重要的功能,就是可以很容易的弹性扩容或弹性伸缩(动态扩容或动态缩容) 

抛开容器技术(Docker,K8s这些),传统的扩缩容简单理解就是服务集群:

  • 扩容:特定时期(比如某天公司要促销,单一的订单服务、商品服务不一定能撑得住,此时就得扩容),一个微服务可能容易挂掉(撑不住或宕机),那么多开几个就行,就为扩容,说白了扩容的常用手段就是搞集群。
  • 缩容:特定时期过后,多开的微服务可以适当关掉多余的,就是缩容

如图所示:

集群存在一个很大的问题,就是客户端发起的请求让哪个微服务来处理?

  • 解决方案:负载均衡服务器

负载均衡(Load Balance)分类

  • 通俗的讲,负载均衡就是将负载(工作任务,访问请求)进行分摊到多个操作单元(服务器、组件)上进行执行。  

根据负载均衡发生位置的不同,一般分为服务端负载均衡和客户端负载均衡:

服务端负载均衡:指的是发生在服务提供者的一方,比如常见的Nginx负载均衡:

客户端负载均衡:指的是发生在服务请求即服务消费者的一方,也就是说在服务发送请求之前已经选好了由哪个实例来进行处理该请求,比如Ribbon:

我们在微服务调用关系中一般会选择客户端负载均衡,也就是在服务调用的一方来决定服务由哪个提供者执行,Ribbon就是在客户端进行了负载均衡。

----------------------------------------------------------------------------------- 

在刚才,我们在RestTemplate的Bean上面添加了@LoadBalanced注解,即可实现负载均衡功能,这是什么原理呢?

3.1 负载均衡原理

Spring Cloud底层其实是利用了一个名为Ribbon的组件,来实现负载均衡功能的。

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

负载均衡流程

Ribbon拦截我们的RestTemplate请求并获取到请求所指定的对应的服务名称(serviceId)之后,它要根据服务名称去找到Eureka-Server注册中心去进行服务拉取或服务发现,然后在对拉取到的服务列表(ServiceList)做负载均衡。

源码跟踪

为什么我们在代码当中定义的URL地址输入的是service服务名称,而不是真实的IP和端口,但却能远程调用,访问成功呢?

  • @LoadBalanced注解相当于是一个标记,标记RestTemplate发起的请求将来要被Ribbon去拦截和处理了,只不过拦截的动作是由LoadBalancerInterceptor去完成的。
  • 显然有人帮我们根据service服务名称,获取到了服务实例真实的IP和端口号它就是LoadBalancerInterceptor:这个类会对在RestTemplate的请求进行拦截,然后从Eureka-Server注册中心根据服务名称获取服务列表,随后利用负载均衡算法得到真实的服务地址信息,从而替换服务名称。
我们进行源码跟踪:

ClientHttpRequestInterceptor:客户端HTTP请求的拦截器  

(1)LoadBalancerIntercepor - 负载均衡拦截器

可以看到这里的interceprt()方法,拦截了用户或客户端的HttpRequest请求,然后做了几件事:

  • request.getURI():获取请求URI,本例中就是 http://user-service/user/8
  • originalUri.getHost():获取URI路径的主机名,说白了就是获取服务名称serviceName - ServiceId
  • this.loadBalancer.execute():根据服务名称去进行服务拉取,然后在对拉取到的服务列表 - ServiceList做负载均衡,负载均衡的规则:IRule rule

(2)LoadBalancerClient:客户端负载平衡器 

  • 这里的execute()是重写的LoadBalancerClient接口的方法:

RibbonLoadBalancerClient

  • RibbonLoadBalancerClient实现了LoadBalancerClient接口,重写了execute()方法! 

(3)负载均衡策略 - IRule 

继续跟入RibbonLoadBalancerClient对execute()方法的具体实现:

在刚才的代码中,可以看到获取服务是通过一个getServer()方法来做的负载均衡,因此继续跟入getServer()方法的详情,还是在RibbonLoadBalancerClient当中:

继续跟踪源码chooseServer()方法:

  • chooseServer()方法是定义在ILoadBalancer接口当中的一个抽象方法。

我们来看它的重写或看它的实现方法,选择BaseLoadBalancer: 

我们来看看这个rule是谁,继续跟踪源码:

点击看看IRule的源码,I应该指的就是Interface - 接口:

IRule接口决定了负载均衡的策略
按Ctrl + H,看看IRule接口的实现类:

3.2 负载均衡策略

Ribbon的负载均衡策略/规则是由一个叫做IRule的接口来定义的,IRule接口就定义了负载均衡的策略(IRule接口决定了负载均衡的策略),并且IRule接口有很多不同的实现类,它的每一个实现类都是一种规则:

IRule接口的继承关系图

IRule接口默认的实现或默认的负载均衡规则是ZoneAvoidancerRule,是一种轮询方案 - Round Robin!

不同规则的含义如下:
内置负载均衡规则类规则描述
RoundRobinRule简单轮询服务列表来选择服务器。它是Ribbon默认的负载均衡规则
AvailabilityFilteringRule对以下两种服务器进行忽略: (1)在默认情况下,这台服务器如果3次连接失败,这台服务器就会被设置为“短路”状态。短路状态将持续30秒,如果再次连接失败,短路的持续时间就会几何级地增加。 (2)并发数过高的服务器。如果一个服务器的并发连接数过高,配置了AvailabilityFilteringRule规则的客户端也会将其忽略。并发连接数的上限,可以由客户端的<clientName>.<clientConfigNameSpace>.ActiveConnectionsLimit属性进行配置。
WeightedResponseTimeRule为每一个服务器赋予一个权重值。服务器响应时间越长,这个服务器的权重就越小。这个规则会随机选择服务器,这个权重值会影响服务器的选择。
ZoneAvoidanceRule以区域可用的服务器为基础进行服务器的选择。使用Zone对服务器进行分类,这个Zone可以理解为一个机房、一个机架等。而后再对Zone内的多个服务做轮询。
BestAvailableRule忽略那些短路的服务器,并选择并发数较低的服务器。
RandomRule随机选择一个可用的服务器。
RetryRule重试机制的选择逻辑

注意:一般情况下使用默认的负载均衡策略或方案就可以了,不需要做修改。 

修改负载均衡策略或自定义负载均衡策略

  • 通过定义IRule实现可以修改或自定义负载均衡策略,自定义负载均衡有两种方式:

1. 代码方式:在服务(消费者)的启动类当中,定义一个新的IRule的Bean注入到Spring容器:

 

2. 配置方式:在application.yml配置文件当中,来添加新的配置也可以修改负载均衡的规则:

# 给某个微服务配置负载均衡规则
serviceName:  #服务提供者的服务名称
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.IRule接口的具体实现类的全类名 # 负载均衡规则

 Eg:

两种负载均衡的配置方式的区别 - 作用范围不同:
  • 第一种代码方式是针对于全局或全体,可以做到全局配置,针对于服务调用者远程调用任意一个服务提供者都是这个策略;缺点:修改时需要重新打包发布;
  • 而第二种配置方式:缺点:配置文件修改只是针对于某一个具体的服务提供者而言的,无法做全局配置;优点是直观、方便,无需重新打包发布。 
注意:
  • 在Spring框架中,Bean的优先级会高于application.yml配置文件的优先级:因为当Spring容器初始化时,会首先创建Bean并进行依赖注入,然后再加载配置文件中的属性与值,因此,如果Bean中定义了某个属性,并且在配置文件当中也存在相同名称的属性,那么Bean重点额属性值会覆盖配置文件中额度值。 

3.3 懒加载 & 饥饿加载

Ribbon默认是采用懒加载,即第一次访问时才会去创建LoadBalanceClient - 客户端负载平衡器,请求时间会很长。

  • 验证:第一次远程调用访问耗时550ms,而第二次远程调用访问耗时只有27ms! 

而饥饿加载则会在项目启动时创建,降低第一次访问的耗时,通过下面的配置开启饥饿加载: 

ribbon:
  eager-load:
    enabled: true  #开启饥饿加载
    clients: serviceName #指定饥饿加载的服务名称,指定对该服务开启饥饿加载
ribbon:
  eager-load:
    enabled: true  #开启饥饿加载
    clients:  #如果有多个服务,则需要换行指定饥饿加载的服务名称
      - serviceName1 
      - serviceName2
      - serviceName3

懒加载 & 饥饿加载 

  • 懒加载和饥饿加载是两个相对的概念,它们在计算机领域中有着不同的含义!
懒加载(Lazy Loading)
  • 懒加载指的是在需要的时候才进行加载或初始化操作
  • 懒加载的优点是可以节省资源,在实际需要时再进行加载,避免不必要的性能开销;缺点是第一次访问时,请求时间会很长。

饥饿加载(Eager Loading)

  • 饥饿加载指的是系统在应用程序启动或对象创建时即进行加载或初始化所有需要的资源或对象。
  • 饥饿加载的优点是可以减少后续操作的等待时间;缺点是可能会占用较多的初始资源。

因此,懒加载和饥饿加载时两种加载策略!

补充:SpringMVC的容器  - dispactherServlet的初始化也是懒加载!

4.  Nacos注册中心

Nacos是目前国内企业中占比最多的注册中心组件,它是阿里巴巴的产品。 

4.1 认识和安装Nacos

  • 国内一般都推崇阿里巴巴的技术,比如注册中心,Spring Cloud Alibaba也推出了一个名为Nacos的注册中心。
  • Nacos是阿里巴巴的产品,现在是Spring Cloud中的一个组件,相比Eureka功能更加丰富,在国内受欢迎程序较高。
  • Nacos:一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。
  • 注意:Eureka有两个版本,其中2.x版本是已经停止维护了,1.4.x版本并没有停止维护!

Nacos是基于Java语言去实现的,Nacos的默认端口是8848!

Nacos服务搭建:

  1. 下载安装包
  2. 解压
  3. 在bin目录下运行cmd窗口执行指令:startup.cmd -m standalone 

4.2 Nacos快速入门

服务注册到Nacos

Nacos是Spring Cloud Aliaba的组件,而Spring Cloud Alibaba也遵循Spring Cloud中定义的服务注册、服务发现规范,因此使用Nacos和使用Eureka对于微服务来说,并没有太大区别,主要差异在于:

  • 依赖不同
  • 服务地址不同

服务注册到Nacos的步骤

  • 引入依赖
  • 配置Nacos地址
  • 重启微服务

1. 引入依赖

  • 在父工程的pom文件中的<dependencyManagement>中引入Spring Cloud Alibaba的管理依赖
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-alibaba-dependencies</artifactId>
    <version>2.2.5.RELEASE</version>
    <type>pom</type>
    <scope>import</scope>
</dependency>

  • 在服务的pom文件中添加或引入Nacos的客户端依赖包:nacos-discovery依赖 
<!-- nacos客户端依赖包 -->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

  • 注意:不要忘了注释掉服务中原有的erueka的客户端依赖! 

2. 配置Nacos地址:spring.cloud.nacos.server-addr

在服务的application.yml配置文件当中添加Nacos地址:

spring:
  cloud:
    nacos:
      server-addr: localhost:8848    # nacos服务端地址
  • 注意:不要忘了注释掉erueka的地址! 

3. 重启微服务,登录Nacos管理页面,在浏览器输入地址:http://127.0.0.1:8848/nacos即可,可以看到微服务信息: 

点击详情,还可以查看到服务的详细信息! 

记得启动数据库,打开浏览器刷新: 

4.3 Nacos服务分级存储模型

一个服务可以包含多个实例,也就是一个服务可以多实例部署,但是如果将多个实例部署在同一个机房,则会存在单点故障问题,就好比把多个鸡蛋放在一个篮子里,为了解决单点故障问题,我们会将一个服务的多个实例部署到多个不同地域机房或服务器  => 也就是搞集群,这样就可以解决单点故障问题(容灾),而Nacos就是将同一机房或服务器内的多个实例划分为一个集群,也就是说,同一个服务可以包含多个集群(集群按地域划分),每个集群下可以有多个实例,形成服务分级存储模型,所以在Nacos的服务分级存储模型中,一级是服务 => 二级是集群 => 三级是实例:将同一个服务的多个实例分成多个集群,每个集群下又包含多个实例,集群就是一个地域的划分,如图:

服务跨集群调用问题:

微服务相互访问时的两种方式: 
  1. 本地局域网内访问(推荐)     
  2. 访问其它集群内的实例

由于本地局域网内的访问,它的距离比较短,所以速度比较快,延迟比较低,而跨集群的访问调用,延迟是非常高的!

因此微服务互相访问调用时,应该尽可能选择本地集群的服务(服务调用尽可能选择本地集群的服务,跨集群调用延迟较高),尽可能访问同一个集群的实例,当跟本地集群不可用或不可访问时,再去访问其它集群,例如:

为什么Nacos要引入服务分级存储模型呢,为什么要加地域划分?
  • 为了防止跨集群调用!

任何一个微服务的实例在注册到Nacos时,都会生成以下几个信息,用来确认当前实例的身份,从外到内依次是:

  • namespace:命名空间

  • group:分组

  • service:服务名

  • cluster:集群

  • instance:实例,包含ip和端口

这就是nacos中的服务分级模型。

在Nacos内部会有一个服务实例的注册表,是基于Map实现的,其结构与分级模型的对应关系如下:

给服务配置集群属性:如何设置实例的集群属性

修改服务的application.yml文件,添加集群配置:

spring:
  cloud:
    nacos:
      server-addr: localhost:8848 # Nacos服务端地址
      discovery:
        cluster-name: HZ # 配置集群名称,也就是机房位置,例如:HZ代指杭州

添加完集群配置后,重启该服务即可! 

我们在Nacos控制台看到下面结果:

也可以再次复制一个服务启动配置(即一个服务启动多个实例),添加属性:

添加属性(配置端口号以及实例的集群属性): 

-Dserver.port=8083 -Dspring.cloud.nacos.discovery.cluster-name=SH 

然后启动该项目即可! 

同集群优先的负载均衡配置:NacosRule负载均衡策略

  • 刚才我们学习了如何配置集群属性,但是我们最终想要实现的是服务在进行远程调用时优先选择本地集群,由于IRule默认的ZoneAvoidanceRule采用的是轮询方案,并不能实现根据同集群优先来实现负载均衡,因此我们也需要给服务调用者也配置一个集群属性。
  • 在Nacos中提供了一个NacosRule的实现,可以优先从同集群中挑选实例。

1. 给服务调用者配置集群信息

  • 修改服务调用者的application.yml文件,添加集群配置:
spring:
  cloud:
    nacos:
      server-addr: localhost:8848  # Nacos服务端地址
      discovery:
        cluster-name: SH # 配置集群名称,也就是机房位置,例如:SH代指上海
2. 修改服务调用者的负载均衡规则
  • 修改服务调用者的application.yml文件,修改负载均衡规则,设置负载均衡的IRule为NacosRule,这个规则会寻找与自己同集群的服务实例列表,会优先选择本地集群,在本地集群内的多个服务当中,它再采用随机方式进行负载均衡来挑选实例;如果本地没有,它才会跨集群访问,而跨集群访问时它会报警告,来去提醒我们的运维人员:
user-service: # 给某个微服务配置负载均衡规则,这里是user-service服务
  ribbon:               # NacosRule的负载均衡规则优先会寻找与自己同集群的服务
    NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule 

接着重启服务调用者,去Nacos控制台看看: 

发起远程调用,看一看会不会调用与自己同集群的服务:  

跨集群访问时控制台会有警告,来提醒我们的运维人员:  

服务实例的权重配置:根据权重负载均衡

实际部署中会出现这样的场景:

  • 服务器设备之间的性能有差异,部分实例所在机器性能较好,而部分较差,我们当然希望性能好的机器来承担更多的用户请求,但默认情况下NacosRule的负载均衡规则是同集群内随机挑选,不会考虑机器的性能问题,因此,Nacos提供了权重配置来控制访问频率,通过修改服务实例的权重,可以控制访问频率,权重越大则访问到的频率就越高,那我们就可以把性能好的机器权重设的大一些,性能差的机器权重设的小一些!

在Nacos控制台,找到服务的实例列表,点击编辑,即可设置或修改服务实例的权重,注意权重值一般只配 0 到 1 之间的:

在弹出的编辑窗口,即可修改服务实例的权重: 

  • 将权重设置为0.1,那该服务实例被访问到的频率就大大降低了~! 

注意:如果权重修改为0,则该实例永远不会被访问! 

这有什么作用呢? 

  • 联想到在对服务做版本的升级:我有多个服务器,先将其中的一个服务实例的权重调成0,此时该服务实例就不会承担用户请求了,这个时候对它做停机重启,去做一些版本的升级,用户是没有感知的,升级完成后再重启,重启完成之后,再将它的权重调小一点儿,比如调成0.01,这个时候我们放出少数用户进来做个测试看看有没有什么问题,如果没有什么问题,就可以逐渐扩大比例,依次升级,这个时候用户是无感知的,这样就可以做到平滑升级。

4.4 Nacos环境隔离 - namespace

Nacos它首先是一个注册中心,但是它其实还是一个数据中心所以在Nacos里面,它为了去做数据和服务的管理,它会有一个隔离的这么一个概念,环境隔离的概念,环境隔离里面,有这么几个东西,首当其冲的就是我们的namespace命名空间:

  • Nacos中服务存储和数据存储的最外层都是一个名为namespace的东西,用来做最外层隔离

Nacos中提供了基于namspace来实现环境隔离的功能:

  • Nacos中可以配置多个namespace,相互之间完全隔离,默认的namespace名为public
  • ​​​namespace下还可以继续分组,也就是在namespace的内部可以有group用来分组、service服务、data数据等,默认的group是DEFAULT_GROUP,group之下就是服务和配置了
  • 不同namespace之间相互隔离,例如不同namespace的服务互相不可见

namespace说白了就是一个隔离的空间,环境隔离说白了其实就是在对服务或数据去做隔离~!

服务划分、实例划分是基于业务或者是地域去做的划分,企业实际开发中,往往会搭建多个运行环境,比如我们还会有开发环境、测试环境、预发布环境、生产环境的变化,所以我们会基于这种环境变化去做隔离,namespace就是来去做这件事情的。

Group是用来做分组的,我们可以把业务相关度较高的服务放到一个组。

创建namespace

默认情况下(也就是在没设置命名空间的情况下),所有service、data、group都在同一个namespace,名为public:

1. 在Nacos控制台可以创建namespace,用来隔离不同环境

我们可以点击页面新增按钮,添加一个namespace:

 2. 然后,填写表单,填写一个新的命名空间信息

  • 命名空间ID如果不填则会用UUID自动生成! 

3. 保存后会在或就能在控制台的页面看到一个新的namespace命名空间的id:

给微服务配置namespace

  • 默认情况下,所有的微服务注册发现、配置管理都是走public这个命名空间。如果要指定命名空间则需要修改application.yml文件。
  • 给微服务配置namespace只能通过修改配置来实现

此时访问order-service,因为namespace不同,会导致找不到userservice,控制台会报错:

总结:不同namespace之间是相互隔离的,不可访问,要想让服务可以访问,必须得               放到相同的环境下。 

Nacos注册中心的临时实例和非临时实例 

Nacos的服务实例(服务提供者)分为两种类型,它们的心跳健康检测机制是不一样的:

  • 临时实例 - 采用的是心跳检测模式(干儿子):如果实例宕机超过一定时间,会从服务列表剔除,默认的类型(默认情况下所有的实例都是临时实例)。

  • 非临时实例 - 不会进行心跳检测,而是由Nacos主动询问(亲儿子)=> 非临时实例采用主动检测模式:如果实例宕机,Nacos不会从服务列表剔除,仅仅是标记该服务不健康了,它会等着该服务恢复健康,也可以叫永久实例。

注意:服务拉取不是每一次都要做的,如果我每次发请求时都去做一次拉取,那这样一来,对Eureka来讲,压力是有点儿太大了,所以作为消费者,它在做服务拉取时,它会将拉取到的服务信息缓存到一个列表当中。  

Nacos和Eureka的对比  

Nacos与Eureka的共同点:

  • Eureka和Nacos都能起到注册中心的作用用法基本类似微服务启动时注册信息到Eureka,这点与Nacos一致,也就是说都支持服务注册,也都支持服务拉取
  • 都有基于心跳的健康检测机制:都支持服务提供者心跳方式做健康检测
  • Nacos和Eureka都支持集群

Nacos与Eureka的区别,还是有一些区别的,例如:

  • Eureka仅有注册中心功能,而Nacos同时支持注册中心和配置管理。

而且服务注册发现上也有区别,Nacos和Eureka的服务提供者的健康检测机制不同,服务消费者的服务拉取机制也不同:

  • 实例类型:Nacos的实例有永久和临时实例之分,而Eureka只支持临时实例
  • 健康检测:Nacos对临时实例采用心跳模式检测,对永久实例采用主动请求来检测;Eureka只支持心跳模式
  • 微服务每隔30秒向Eureka发送心跳请求,报告自己的健康状态,Nacos中默认是5秒一次。
  • Eureka如果90秒未收到心跳,则认为服务疑似故障,可能被剔除,而Nacos中则是15秒超时,30秒剔除:Nacos Server会开启一个定时任务来检测注册服务实例的健康情况,对于超时15s没有收到客户端(Nacos Client - 服务消费者)心跳的实例会将它的healthy属性设置为false,如果某个实例超过30s还没有收到心跳,则直接剔除该实例(被剔除的实例如果恢复发送心跳则会重新注册)
  • Eureka如果发现超过85%比例的服务都心跳异常,会认为是自己的网络异常,暂停剔除服务的功能
  • Eureka每隔60秒执行一次服务检测和清理任务,Nacos是每隔5秒执行一次
  • Eureka只支持定时拉取模式:Eureka只能等微服务自己每隔30秒更新一次服务列表(pull),而Nacos支持定时拉取和订阅推送两种模式:Nacos中的微服务既有定时更新(会在Nacos Client本地开启一个定时任务拉取最新的注册表信息更新到本地缓存),也有在服务变更时的UDP广播推送{推送给所有的订阅者}(pull + push)
  • 当Eureka发现服务宕机并从服务列表剔除以后,并不会将服务列表的变更消息推送给所有的微服务,而是等待微服务自己来拉取时发现服务列表的变化,而微服务每隔30秒才会去Eureka更新一次服务列表,这进一步推迟了服务宕机时被发现的时间;而Nacos中的微服务除了自己定时去Nacos中拉取服务列表以外,Nacos还会在服务列表变更时主动推送最新的服务列表给所有的订阅者(Nacos发现如果有服务挂了,会立即推送给服务消费者)
  • Nacos和Eureka的集群间的数据同步默认采用AP模式(会强调服务的可用性),即最全高可用性;当Nacos集群中采用非临时实例时,采用CP模式(强调数据的可靠性和一致性)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Surpass余sheng军

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值