10-微服务架构体系

一、微服务架构体系

(一)微服务架构基本概念

​    1、微服务三大要素

​    微服务不是一个纯技术概念,微服务架构三大要素:业务建模、技术体系、研发过程

​    (1)业务建模:业务架构 + 功能边界

​        对于一个复杂的业务而言,要想把它构建成微服务系统,就需要将系统进行拆分,拆分的前提就是要进行业务建模,换句话说,我们如何把控业务架构以及各个功能的边界,就需要业务建模;

​        如下图所示,要对业务进行建模,分拆成不同的子域,例如核心子域、通用子域、支撑子域等。其中就诊、处方、药品等属于核心子域,账户、就诊卡、用户等属于通用子域,而搜索、支付已经不属于业务方面的东西了,更偏重于技术组件,可以算作支撑子域。这样一划分,业务边界还是很清晰的,业务边界清晰之后,业务架构就会很灵活,可以帮助我们更好大的拆分服务,例如我们可以将系统简单的分为核心服务、通用服务、支撑服务这三个服务,当然也可以在核心服务中把控其粒度并做更细的拆分,例如拆分为就诊服务、药品服务等。

​        微服务架构能够实现的原因是可以把边界划分的很清楚,各个边界之间的继承关系通过一些技术手段进行集成就可以了。但是本质上它得有边界,而且边界是很清晰的。

​        

​    (2)技术体系

​        主要有八个点,具体如下图所示,后面会进行展开。

​        

​    (3)研发过程

​        Function Team是职能团队,例如Java团队、测试团队、前端团队等,跨职能团队是Feature Team,是围绕业务组建的团队,而不是围绕技术来组建的团队。

​        

​    2、微服务的扩展性

​        对于系统的扩展性有一个专门的概念叫 AFK扩展立方体,其中X轴表示水平复制和负载均衡,代表了水平扩展,Y轴表示按业务功能的拆分,代表了垂直扩展,Z轴表示数据分区,考虑的是数据分扩展性,例如数据的分区和分库分表等;总的来说,Y轴是业务功能驱动的,X轴和Z轴是技术驱动的,如果一个系统同时满足了XYZ,则这个系统可以无限扩展。

​        

​        示例: 比如用户预约挂号应用,一个集群撑不住时,分了多个集群(X轴),数据做了分区处理(Z轴),经过分析发现是用户和医生访问量很大,就将预约挂号应用拆成了患者服务、医生服务、支付服务等三个服务(Y 轴)。三个服务的业务特点各不相同,独立维护,各自都可以再次按需扩展。

​    3、微服务的业务边界:领域驱动设计(DDD)

​        要做业务拆分就需要确定业务边界,这里业界有很多方法,这里比较推荐 DDD(领域驱动设计),下图是对DDD概念的整合,例如边界在哪,就需要先界定上下文内容和结构,然后和不同的上下文进行交互,交互完成后,内核就形成了一个子域,子域中最重要的就是聚合,其中聚合根代表一个事务的最小操作单元,也代表当前业务主题下所有的操作入口。

​        

​    4、微服务的数据管理

​        微服务架构首先要保证微服务和数据要分离,不能放在同一个服务器上。

​        

​    5、微服务数据管理策略

​      XQRS模式是指查询和命令分离的模式,对于查来说是没有任何副作用的,但是增删改都是有副作用的,都需要进行业务处理和校验,在满足数据完整性的前提下才能进行处理;而且在分布式场景下,还会涉及分布式事务的问题。

​      因此建议把数据操作分成两大类,一类是查询,叫做Query Model,一类是增删改操作,叫做Command Model,不严谨的讲就是不要在查询中更改数据,这样查询性能就会很高,同时可以保证系统的安全性。

​        

​    6、微服务事务管理策略

​        主要是当需要跨越多个服务时该如何处理事务,这个一般有两种解决思路,分别是强一致性和弱一致性(最终一致性)。

​            强一致性:数据的一致性是实时的,每一时刻都保持一致

​            弱一致性:某个时刻数据可能非一致,但是到达某个时间点以后总能保持一致,也即我们所说的最终一致性

​    7、微服务与遗留系统

​        对于现有架构如何演进为微服务架构,这里主要介绍两种架构模式:绞杀者模式和修缮者模式。

​      (1)绞杀者模式

​        这里主要得用到微服务网关,刚开始,以老服务为主,当有新需求进入,则使用新服务,随着时间的运行,新服务的比重超过老服务后,就可以决定是否要下掉老服务。简单的讲就是使用新老服务共存的方式,让新服务不断壮大,等壮大到一定阶段后,就可以下线老服务,但是由于网关的存在,对于客户端是无感的。

​        现有架构进行修改的遗留系统,推荐采用绞杀者模式

​        

​      (2)修缮者模式

​         修缮者模式本质上是一种重构策略,我们认为有某些代码是不好的,那么就先做抽象,然后对于抽象在做新的实现。

​          

​        

​        总的来说,绞杀者模式是从1到1的完全替换,修缮者模式只是针对部分内容做优化。

(二)微服务架构核心技术组件

​    1、网络通信:RPC VS HTTP

​        对于网络通信,之前提到过,包含网络连接模式(长链接、短连接)、IO模型(BIO、NIO、AIO)、服务调用方式(RPC、HTTP)

​        那么对于微服务而言,是用RPC还是HTTP,目前来说,在微服务架构中,HTTP是主流,其主要是从开发、维护、性能等维度做了综合考虑,包括像Spring Cloud都是使用的HTTP的调用方式。

​        

​    2、服务治理

​        服务治理实际上就是注册中心,主要是做服务的注册与发现,同时间接的做服务的调用,从而保证整个链路的健壮性,以及在安全性和性能方面的考虑。

​        

​    3、服务路由

​        服务路由一般是与注册中心结合起来,在做服务调用时,并不是直接调用,而是使用负载均衡器做路由。

​        

​    4、服务容错

​        服务容错是一个非常大的话题,从技术的角度包括:集群容错策略、服务隔离机制、服务限流机制、服务熔断机制

​    5、服务配置

​        配置中心用来做项目的统一配置管理,从而解决一大堆的各类配置项,各种不定时的修改需求的问题。

​    6、服务网关

​        服务网关可以做请求监控、安全管理、路由规则、日志记录、访问控制、服务适配等。

​        

​    7、服务安全

​        在微服务中,服务间的访问需要有安全认证,例如基于Token机制的服务安全架构,在调用服务时传递Token,然后服务提供者需要调用授权服务器验证Token。

​        

​        

​    8、服务监控

​        在微服务架构中,监控是一个非常重要的内容,例如数据埋点、指标采集、调用关系、性能分析等,最终都要使用可视化工具做展示。

​        

​    9、Spring Cloud Alibaba技术组件

​        在上述的微服务组件中,Spring Cloud提供了一整套的解决方案,同时也提供了一些具体落地的框架,例如:

​            服务路由:Spring Cloud LoadBalancer

​            服务事件:Spring Cloud Steam

​            链路跟踪:Spring Cloud Sleuth

​            服务安全:Spring Cloud Security

​            服务网关:Spring Cloud Gateway

​        而Spring Cloud Alibaba作为Spring Cloud 的延伸,也提供了一些框架,例如:

​            服务治理:Nacos

​            服务配置:Nacos

​            服务容错:Sentinel

(三)客服系统案例演进

​    在之前的客服系统2.0架构(分布式架构)中,服务拆分为客服服务、IM服务、消息服务、集成服务、搜索服务、以及其他模块,而集成服务又集成了外包的客服服务。

​    该架构是个分布式服务,使用到了分布式通信(Netty)、分布式数据库(分库分表)、分布式搜索(ES)、分布式消息(MQ)、分布式缓存(Redis)等,具体如下图所示:

​        

​    对于客服系统的微服务化升级,这里采用中台化策略进行升级,中台化策略是指将常变内容作为前台业务,不变的内容作为中台业务,这样前台可以支持业务的快速发展,中台可以为所有的前台业务做支持。

​    例如下图是阿里的大中台小前台架构示意图。

​        

​    中台架构已经已经被阿里锁抛弃,因为其存在很多问题,这里不做过多阐述,但是对于其思想还是可以参考的,那么参考其思想,对于客服系统来说,也可以将其拆分为前台系统和中台系统。

​    中台服务:中台服务主要做一些通用服务,例如下沉客服、IM、搜索等服务,底层为集成服务

​        

​    前台服务:前台服务为工单服务、聊天服务

​        

​    基于上述拆分,客服系统3.0架构(微服务架构)的最终效果如下图所示,分别是前台服务层的业务服务,包含工单服务和聊天服务;中台服务层的通用基础服务,包含客服服务、IM服务、消息服务、搜索服务等;底层为集成服务,用来集成外包客服服务;同时对于微服务架构,还要有各个微服务组件来做支持,例如服务容错、安全控制等。

​        

二、使用新一代注册中心Nacos

(一)Nacos整体架构

​    1、注册中心原理

​    注册中心基本模型包括三大角色、两大操作和一个关键技术,其中三大角色指的是注册中心、服务提供者、服务消费者;两大操作是指服务的注册和订阅;一个关键技术指的是通知;无论是哪个注册中心,其都是这个架构与思路,具体的实现差异主要在如何通知上。

​        

​    对于服务变更通知机制,主要有服务轮询机制和服务监听机制。

​        服务轮询机制:服务轮询机制主要是服务消费者定时从注册中心拉取最新的服务信息,然后更新本地的数据,典型的代表就是Eureka。

​        

​        服务监听机制:服务监听机制主要是服务消费者在注册中心上注册一个监听器,当服务发生变化时,监听器主动调用服务消费者进行处理,典型的代表就是Zookeeper。

​        

​        除了服务轮询和服务监听外,还可以将二者进行结合,典型的代表就是Nacos。    

​    另外一般情况下注册中心和服务路由都是绑定在一起的,客户端通过注册中心获取服务端实例,并基于负载均衡算法实现服务路由。

​        

​    2、Nacos概述

​      Nacos几乎支持所有的主流语言,同时有多语言生态集成方案,是阿里云微服务DNS的最佳实践。

​      Nacos有很多优点:

​        首先架构本身具有易用(使用方式简单)、稳定(稳定性高)、实时(实时性很强,变更的信息可以很快的推送到订阅端)、规模(在阿里云大规模、海量规模的应用场景);

​        对于开发者而言,其提供了简单的数据模型和标准的API,带来了很好的用户体验,开发友好度很不错;

​        第三个优点就是高可用,Nacos具有99.9%高可用

​    3、Nacos 架构模型

​      Nacos也是使用了分层架构,包括用户层、业务层、内核层、插件层。其中用户层包括了命令行、SDK等;业务层主要是服务管理(注册中心)、配置管理(配置中心)、元数据管理,业务层表示其提供的功能;内核层主要是其用到的技术,例如插件机制、事件机制、日志模块等,同时还包括一致性协议、存储等;插件层使用 SPI 机制可以做命名服务、用户管理、角色管理等。

​        

​    4、Nacos部署

​      Nacos部署方式有单机部署模式和集群部署模式两种,这里就不再说明,可以参考我之前的文章:SpringCloudAlibaba--Nacos应用

​      Nacos集群部署时存在一些点需要注意,在上述的文章中都有提及,另外配置集群地址时,上面使用的是无代理模式,即直接配置了nacos集群的所有ip与端口(server-addr: 127.0.0.1:8848,127.0.0.1:8850,127.0.0.1:8852),但是这样如果节点发生变更,就需要调整每个实例的配置,会比较麻烦,因此推荐使用代理的方式,例如使用域名(域名指向具体的多个实例)。

(二)Nacos注册中心功能特性

​    1、Nacos分级模型    

​      Nacos采用了三级分级模型,第一级是服务,如:test-service;第二级是集群,A、B、C三个集群;第三级是实例,集群中的多个实例。

​        

​      服务和实例都比较好理解,对于集群来说,在Nacos中,每一个实例都对应一个集群,默认的集群名称是DEFAULT,同时也可以进行配置,例如 cluster-name:hangzhou 就将集群名称配置为hangzhou;使用集群主要是为了通过负载均衡算法实现同集群服务优先调用,减少网络开销;同时也可以通过元数据实现定制化控制,例如在实例上添加标签,访问时使用标签访问指定的实例。

​    2、Nacos资源隔离

​      Nacos资源使用两级隔离机制(Namespace、Group),资源的合理管理,不同资源之间不能直接访问。

​      (1)命名空间:

​          命名空间默认为空,如果是全局性的公共命名空间就叫public

​          典型应用:不同的环境可以指定不同的命名空间;不同命名空间下的服务互不可见;dev、test、prod

​          设置命名空间:每个命名空间都有唯一id,服务设置命名空间时要写id而不是名称;例如:namespace:d73a49df-ceb9-425f-b7a5-87d33a110dfd

​          使用命名空间机制是逻辑隔离的一种方式,但是实际我们比较推荐物理隔离的方式,即在每个环境都搭建一个nacos集群,使用不同的Nacos环境地址来进行隔离。

​      (2)分组

​          默认分组名称为DEFAULT_GROUP,其典型的应用:同一个环境内,不同业务场景可以指定不同的分组,例如支付分组、物流分组;如果自己要设置分组,可以进行设置,例如:group:MY_GROUP

​    3、Nacos分级模型总结

​        Nacos完整的分级模型如下图所示,首先用Namepace、Group进行隔离,然后使用Service、Cluster、Instance进行分级。

​        

​    4、Nacos服务路由

​      注册中心一般会和服务路由、负载均衡配置配合使用,Nacos也是这样做的。Nacos在服务路由和负载均衡上提供了保护阈值、权重、就近访问等功能来组合服务路由和负载均衡。

​    (1)保护阈值

​        Nacos控制台可以在服务级别配置保护阈值,阈值在0~1之间, 这个阈值对应的是健康实例比例值(当前服务健康实例数/当前服务总实例数),当比例值 < 保护阈值时,Nacos会把该服务所有的实例信息(健康+不健康)全部提供给消费者,尽管会有失败响应,但能避免雪崩效应。

​        保护阈值一般情况下设置0即可,表示该怎么请求就怎么请求;但是如果真的要进行设置,就需要考虑好,因为设置小了没什么用,设置的大了,容易触发容错机制。

​    (2)权重

​        Nacos控制台可以在实例级别设置实例的权重值,权重值在0~1之间,同集群内的多个实例,权重越高被访问的频率越高,权重设置为0则完全不会被访问,其主要可以确保性能好的机器承担更多的用户请求。

​    (3)就近访问

​        优先选择同集群服务实例列表,本地集群找不到服务实例才会去其它集群寻找,并且会报警告;当确定了可用实例列表后,再采用随机负载均衡挑选实例

​        如果想要实现统计群调用,可以使用NacosRule进行配置: com.alibaba.cloud.nacos.ribbon.NacosRule

(三)客服系统案例演进

​    1、pom依赖

<dependency>
   <groupId>com.alibaba.cloud</groupId>
   <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

​  2、配置文件

​    必须要设置服务名称,然后可以使用profiles配置环境,然后在cloud中配置注册中心地址,对于配置值,可以使用双@进行引入

spring:
  application:
    name: intergation-service
  profiles:
    active: @spring.profiles.active@
  cloud:
    nacos:
      discovery:
        server-addr: @spring.cloud.nacos.discovery.server-addr@
        enabled: true
        cluster-name: beijing
        group: LCL_GALAXY_GROUP
        namespace:  @spring.cloud.nacos.discovery.namespace@

3、多环境配置

​    在pom文件中使用profiles配置多环境。

  <profiles>
    <profile>
      <id>local</id>
      <properties>
        <spring.cloud.nacos.discovery.server-addr>192.168.249.130:8848</spring.cloud.nacos.discovery.server-addr>
        <spring.profiles.active>local</spring.profiles.active>
        <spring.cloud.nacos.discovery.namespace>dae2f8c4-a44a-4143-afc5-1f8aaa84c72c</spring.cloud.nacos.discovery.namespace>
      </properties>
      <activation>
        <activeByDefault>true</activeByDefault>
      </activation>
    </profile>
    <profile>
      <id>dev</id>
      <properties>
        <spring.cloud.nacos.discovery.server-addr>192.168.249.130:8848</spring.cloud.nacos.discovery.server-addr>
        <spring.profiles.active>dev</spring.profiles.active>
        <spring.cloud.nacos.discovery.namespace>b5b0791d-acb0-462e-9513-facb051a505f</spring.cloud.nacos.discovery.namespace>
      </properties>
    </profile>
    <profile>
      <id>test</id>
      <properties>
        <spring.cloud.nacos.discovery.server-addr>192.168.249.130:8848</spring.cloud.nacos.discovery.server-addr>
        <spring.profiles.active>local</spring.profiles.active>
        <spring.cloud.nacos.discovery.namespace>734d2b15-9c71-4b61-bd52-67eec39e2774</spring.cloud.nacos.discovery.namespace>
      </properties>
    </profile>
    <profile>
      <id>prod</id>
      <properties>
        <spring.cloud.nacos.discovery.server-addr>192.168.249.130:8848</spring.cloud.nacos.discovery.server-addr>
        <spring.profiles.active>local</spring.profiles.active>
        <spring.cloud.nacos.discovery.namespace>9b03f190-07c9-4fd7-bdc5-14f8f61bffb2</spring.cloud.nacos.discovery.namespace>
      </properties>
    </profile>
  </profiles>

三、使用OpenFeign重构远程调用过程

(一)OpenFeign基本应用

​    Feign是一种透明化远程调用机制,已停更。Feign是在Ribbon的基础上进行了一次改进,是一个使用起来更加方便的HTTP客户端。 采用接口的方式,只需要将需要调用的其他服务的方法定义成抽象方法即可,不需要自己构建HTTP请求。使用效果上就像是调用自身工程的方法调用,而感觉不到是调用远程方法,使得编写客户端变得非常容易。概念上类似于MyBatis的@Mapper注解。

​    OpenFeign是Feign的升级版,Spring Cloud在Feign的基础上支持了SpringMVC的注解。 OpenFeign可以解析SpringMVC的@RequestMapping等注解下的接口,并通过动态代理的方式产生实现类。

​    使用OpenFeign开发时,首先需要在启动类上添加@EnableFeignClients注解:告诉系统扫描所有使用注解@FeignClient定义的Feign客户端

@SpringBootApplication(scanBasePackages = "com.lcl.galaxy.cs.frontend.business.*")
@EnableFeignClients
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

​    然后在接口上添加@FeignClient注解:通过指定目标服务的名称或地址来发起远程调用,这里需要注意,@FeignClient注解中的name必须与服务提供者在注册中心中注册的名字一致。

@Component
@FeignClient(name = "ticket-service")
public interface TicketClient {
    @RequestMapping(value = "/customerTickets/try", method = RequestMethod.POST)
    Result<Boolean> ticketTry(@RequestBody TccRequest<AddTicketReqVO> addTicketReqVO);

    @RequestMapping(value = "/customerTickets/confirm", method = RequestMethod.POST)
    Result<Boolean> ticketConfirm(@RequestBody TccRequest<String> ticketNo);

    @RequestMapping(value = "/customerTickets/cancel", method = RequestMethod.POST)
    Result<Boolean> ticketCancel(@RequestBody TccRequest<String> ticketNo);
}

​    但是上面这种开发模式有一个弊端,如果服务消费者很多,服务提供者一旦发生变化,客户端就都需要进行变更,同时每个客户端都要写很多相同的代码。

​    另外一种开发模式是服务端统一做封装,然后提供一个SDK给客户端,客户端只需要引入SDK即可。

​    但总体来说,两种方式的原理是一样的,只不过谁做得多谁做的少而已。

(二)OpenFeign高级特性

​    OpenFeign有四大高级特性:自动降级、超时配置、日志控制、错误解码

​    1、自动降级

​        可以在@FeignClient注解中设置fallbackFactory工厂,同时该工厂需要实现@FeignClient注解所在的接口,这样当调用失败后,会执行降级工厂中对应的方法。

@FeignClient(name = ApiConstants.SERVICE_NAME, path = ApiConstants.PREFIX + "/decryptionAuditRecords", fallbackFactory = DecryptionAuditRecordApiFallback.class)
public interface DecryptionAuditRecordApi {
    @RequestMapping(value = "/", method = RequestMethod.POST)
    void addDecryptionAuditRecord(@RequestBody @Validated AddDecryptionAuditRecordReq addDecryptionAuditRecordReq);
}

public class DecryptionAuditRecordApiFallback implements DecryptionAuditRecordApi {
    @Override
    public void addDecryptionAuditRecord(AddDecryptionAuditRecordReq addDecryptionAuditRecordReq) {
    }
}

​    2、超时配置

​        可以对全局进行超时配置,也可以针对特定服务进行超时配置,如果同时配置了全局超时规则和针对某个特定服务的超时规则,那么特性的配置会覆盖全局配置,并且优先生效。

feign:
  client:
    config:
      # 全局超时配置
      default:
        # 网络连接阶段1秒超时
        connectTimeout: 1000
        # 服务请求响应阶段5秒超时
        readTimeout: 5000
      # 针对某个特定服务的超时配置
      provider-service:
        connectTimeout: 1000
        readTimeout: 2000

​    3、日志控制

​      OpenFeign日志级别包括NONE、BASIC、HEADERS、FULL:

​        NONE:不记录任何信息,这是OpenFeign默认的日志级别;

​        BASIC:只记录服务请求的URL、HTTP Method、响应状态码(如 200、404 等)和服务调用的执行时间;

​        HEADERS:在BASIC的基础上,还记录了请求和响应中的HTTP Headers;

​        FULL:在HEADERS级别的基础上,还记录了服务请求和服务响应中的Body和metadata,FULL级别记录了最完整的调用信息

​      在调试阶段建议使用FULL级别,这样可以保证查看到完整的调用信息,在生产阶段建议使用BASIC级别,因为输出的内容太多会影响性能,输出的太少会不导致信息太少,问题不好定位。

​      设置OpenFeign的日志等级一般在其配置类中直接进行设置,样例代码如下所示:

@Configuration
@EnableFeignClients()
public class FeignConfiguration {
    @Bean
    Logger.Level feignLoggerlevel() {
        return Logger.Level.FULL;
    }
}

​    4、错误解码

​      错误解码主要是针对错误信息的二次封装,比如调用失败后,可以对错误信息做信息解码、打印日志、抛出异常等操作。

​      错误代码一般在配置文件中进行设置,样例代码如下所示:

@Configuration
@EnableFeignClients()
public class FeignConfiguration {
    @Bean
    FeignErrorDecoder errorDecoder() {
        return new FeignErrorDecoder();
    }
}

public class FeignErrorDecoder extends ErrorDecoder.Default {
    private static final Logger logger = LoggerFactory.getLogger(FeignErrorDecoder.class);

    @Override
    public Exception decode(String methodKey, Response response) {
        Exception exception = super.decode(methodKey, response);
        logger.error(exception.getMessage(), exception);
        return exception;
    }
}

(三)客服系统案例演进

​    在原有的结构中,

​        定时任务(microservice-middleground-task)

​          使用Dubbo---->>> 客户服务(microservice-middleground-customer-service)

​            使用Dubbo---->>> 集成服务(microservice-intergation-esb)

​              使用RestTemplate---->>> 三方服务(outsouring-system-microservice-hangzhou)

​    这里改造定时任务调用客户服务以及客户服务调用集成服务,使用Openfeign进行调用。

​    1、工具类项目

​        首先修改工具类项目,增加Openfeign配置,其他项目依赖工具类项目时不需要再做配置,这样避免了多个项目的重复配置。

​    (1)添加Openfeign依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

​    (2)添加Openfeign配置

​        主要配置可以使用Openfeign,同时指定扫描@FeignClients包路径,然后设置Openfeign的日志登记和错误解码处理。

@Slf4j
public class FeignErrorDecoder extends ErrorDecoder.Default {

    @Override
    public Exception decode(String methodKey, Response response) {
        Exception exception = super.decode(methodKey, response);
        // 添加异常日志,对原有异常的返回结果进行定制化处理
        log.info(exception.getMessage(), exception);
        return exception;
    }
}
@Configuration
@EnableFeignClients(basePackages = {"com.lcl.galaxy.microservice.*"})
public class FeignConfiguration {

    @Bean
    public Logger.Level feignLoggerLevel(){
        return Logger.Level.FULL;
    }


    @Bean
    public FeignErrorDecoder errorDecoder(){
        return new FeignErrorDecoder();
    }
}

​    2、改造客户服务调用集成服务(使用SDK集成)

​      这里使用SDK集成的方式来实现。

​    (1)集成服务原有接口

@RestController
@RequestMapping("/api/integration/staffs")
public class CustomerStaffIntegrationServiceApiImpl {

    @Autowired
    private CustomerStaffEndpoint customerStaffEndpoint;

    @RequestMapping(value = "/", method = RequestMethod.POST)
    public List<PlatformCustomerStaff> fetchCustomerStaffs(@RequestBody OutsourcingSystemDTO outsourcingSystem){
        return customerStaffEndpoint.fetchCustomerStaffs(outsourcingSystem);
    }
}

​    (2)创建一个SDK项目

​        创建SDK项目microservice-intergation-api,用以封装Openfeign调用。

​        引入依赖

    <dependency>
      <groupId>com.lcl.galaxy</groupId>
      <artifactId>microservice-infrastructure-utility</artifactId>
      <version>0.0.1-SNAPSHOT</version>
      <scope>compile</scope>
    </dependency>

​        Openfeign客户端

public class ApiConstants {

    public static final String SERVICE_NAME = "intergation-service";
    public static final String PREFIX = "/api/integration";
    public static final String VERSION = "1.0.0";
}
@FeignClient(name = ApiConstants.SERVICE_NAME, path = ApiConstants.PREFIX+"/staffs",
        fallbackFactory = CustomerStaffIntegrationServiceApiFallback.class,
        configuration = FeignConfiguration.class)
public interface CustomerStaffIntegrationServiceApi {

    @RequestMapping(value = "/", method = RequestMethod.POST)
    List<PlatformCustomerStaff> fetchCustomerStaffs(@RequestBody OutsourcingSystemDTO outsourcingSystemDTO);
}

​    (3)在客户端集成SDK

​        引入SDK

<dependency>
   <groupId>com.lcl.galaxy</groupId>
   <artifactId>microservice-intergation-api</artifactId>
   <version>0.0.1-SNAPSHOT</version>
</dependency>

​        注入SDK客户端

@Component
public class CustomerStaffIntergationClient {

    @Autowired
    private CustomerStaffIntegrationServiceApi customerStaffIntegrationService;

    public List<CustomerStaff> getCustomerStaffs(OutsourcingSystem outsourcingSystem){
        OutsourcingSystemDTO outsourcingSystemDTO = CustomerStaffIntegegrationConverter.INSTANCE.convertOutsourcingSystemDTO(outsourcingSystem);
        List<PlatformCustomerStaff> platformCustomerStaffs = customerStaffIntegrationService.fetchCustomerStaffs(outsourcingSystemDTO);
        return CustomerStaffIntegegrationConverter.INSTANCE.convertOutsourcingSystems(platformCustomerStaffs);
    }

}

​    3、改造定时任务服务调用客户服务(服务调用方自己写FeignClilent)

​    (1)引入依赖

<dependency>
    <groupId>com.lcl.galaxy</groupId>
    <artifactId>microservice-infrastructure-utility</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <scope>compile</scope>
</dependency>

​    (2)Openfeign客户端

@FeignClient(value = ApiConstants.SERVICE_NAME, path = ApiConstants.PREFIX + "/sync",
                fallbackFactory = CustomerStaffSyncClientFallback.class,
                configuration = FeignConfiguration.class)
public interface CustomerStaffSyncClient {

    @RequestMapping(value = "/{systemId}", method = RequestMethod.GET)
    void syncOutsourcingCustomerStaffsBySystemId(@PathVariable("systemId") Long systemId);
}

​        这里需要注意一点,如果使用工具类配置Openfeign客户端的配置类,需要保证工具类和当前应用的包路径是一致的,否则就需要在启动类上添加Spring注解的扫描路径,保证配置类可以被扫描到。

@SpringBootApplication(scanBasePackages = {"com.lcl.galaxy.microservice"})
public class MiddleGroundTaskApplication {

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

    @Bean
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }
}

四、使用Spring Cloud LoadBalancer重构负载均衡

(一)Spring Cloud LoadBalancer用法

​    1、基于DiscoveryClient工具类实现负载均衡

​    在做远程调用负载均衡时,首先要明确目标服务有哪些可用的实例,然后如何从这些服务实例中选择一个服务进行调用,最后谁来完成对服务实例调用的分发操作。

​    Spring Cloud也提供了DiscoveryClient工具类,这个工具类是对远程调用过程的抽象,是带有服务发现机制的客户端组件。可以通过discoveryClient.getInstances来获取了目标服务的所有可用实例,然后选择了第一个实例或随机获取一个实例,最终使用restTemplate.exchange进行远程调用。

@Autowired
RestTemplate restTemplate;
@Autowired
private DiscoveryClient discoveryClient;

public User getUserByUserName(String userName) {
    // 获取服务列表
    List<ServiceInstance> instances = discoveryClient.getInstances("userservice");
    if (instances.size() == 0) return null;
    String userserviceUri = String.format("%s/users/%s", instances.get(0).getUri().toString(), userName);
    ResponseEntity<User> user = restTemplate.exchange(userserviceUri, HttpMethod.GET, null, User.class, userName);
    return result.getBody();
}

​    2、负载均衡分类

​    通过以上代码,基于DiscoveryClient来实现负载均衡并不复杂,但是在真实环境中并没有这么简单,因为考虑的点很多,例如设置一些定制化规则、黑白名单等,这样DiscoveryClient就不一定满足,因此就要引入客户端负载均衡组件来更好的扩展负载均衡能力。

​    负载均衡的类型可以分为服务端的负载均衡和客户端的负载均衡,两者主要的区别是负载均衡的位置,客户端负载均衡是在客户端,例如LoadBalancer、Dubbo中的负载均衡等,服务端负载均衡在服务端,例如F5、Nginx等。

​    客户端负载均衡由于在客户端本身,不会存在单点、性能瓶颈等问题,缺点是需要知道所有的服务实例信息;

​    服务端负载均衡的优缺点与客户端服务负载均衡刚好相反,优点是客户端不需要知道所有的服务实例,只需要知道负载均衡器的实例信息即可,但是由于服务端负载均衡是独立部署的,存在单点和性能问题。

​        
​    3、负载均衡算法

​    负载均衡算法有很多,例如最常见的随机和轮询,在随机和轮询的基础上还优化出了加权随机和加权轮询;除了随机和轮询之外,还有Hash、一致性Hash、最小连接数等。

​    (1)随机(Random)算法:随机算法非常简单,就是获取一个随机数,然后根据随机数获取实例集合的下标对应的实例。

//普通随机 
java.util.Random random = new java.util.Random();
int randomPosition = random.nextInt(serverList.size());
return serverList.get(randomPosition);

​    (2)加权随机:加权随机和随机区别在于根据权重向实例集合中放置指定数量的实例,而随机是每一个实例只会在集合中存在的一个,例如在加权随机实现中,服务的A实例与B实例的权重比例是2:1,那么集合中就会存在三个对象,即两个A实例和一个B实例。

//加权随机 
Set<String> keySet = serverWeightMap.keySet();
Iterator<String> iterator = keySet.iterator();
List<String> serverList = new ArrayList<String>();
while (iterator.hasNext()) {
    String server = iterator.next();
    int weight = serverWeightMap.get(server);
    for (int i = 0; i < weight; i++) {
        serverList.add(server);
    }
}

​    (3)轮询(Round Robin)算法:轮询算法也比较简单,就是按照顺序从集合中获取实例,不过还需要保证获取顺序的并发问题;而对于加权轮询和加权随机一样,只是根据权重添加实例。

String server = null;
synchronized (position) {
    if (position > serverList.size()) {
        position = 0;
    }
    server = serverList.get(position);
    position++;
}
return server; 

//类似加权随机算法,我们也可以实现加权轮循算法

​    (4)源IP哈希(Source IP Hash)算法:Hash算法是根据某个信息计算hash值,然后使用hash值与实例数取余获取实例集合的下标,而源IP哈希算法就是根据IP进行Hash计算。

String remoteIp = getRemoteIp();
int hashCode = remoteIp.hashCode();
int serverListSize = serverList.size();
int serverPos = hashCode % serverListSize;
return serverList.get(serverPos);

​    (5)一致性Hash(Consistent Hash)算法:

​          简单Hash算法的问题有一个很大的问题,即一旦发生实例变化,则绝大多数的取余结果会发生变化,例如有4个实例,而IP的hash值是0-99,当实例从4个变为3个计算下标后仍然不变的只有27%,x%4=x%3。

​          因此引入了一致性Hash算法,其使用了一个hash环,并将4个实例放入hash环中,当一个IP计算hash后,选择与其临近的实例,如果实例发生变动,则只会影响很小的一部分数据。

​        

​      

​    (6)所谓最少连接数(Least Connection)算法:

​        根据当前服务器的连接数量来决定目标服务器。在系统运行过程中,连接数显然是一个不断在变化的参数,我们可以选择那些连接数较少的服务来接收新的请求。因此,当执行分发策略时,我们会根据在某一个特定的时间点下服务实例的最新连接数来判断是否执行客户端请求。

​        而在下一个时间点时,服务实例的连接数一般都会发生相应的变化,对应的请求处理也会做相应的调整。

​    (7)服务调用时延算法:

​        针对每一台服务器,我们都可以计算一段时间内所有请求的服务调用时延。有了这个参数之后,就可以执行各种计算策略进一步决定选择那一台服务器来对请求做出响应。

​    针对上述分析:

​        负载均衡类型可以分为:客户端负载均衡、服务端负载均衡

​        负载均衡算法可以分为:

​            静态负载均衡:随机(加权随机)、轮询(加权轮询)

​            动态负载均衡:最少连接数、调用时延、hash、一致性hash

​        

​    4、Spring Cloud LoadBalancer:

​    Spring Cloud LoadBalancer提供了一个能够实现负载均衡策略的@Loadbalanced注解,其使用方式非常简单,只需要在RestTemplate上加上该注解,远程调用就具备了负载均衡的能力。

//自动具备负载均衡机制 
@LoadBalanced
@Bean
public RestTemplate getRestTemplate() {
    return new RestTemplate();
}

​    当RestTemplate使用了@LoadBalancer注解后,就可以使用下面的方式进行调用,不过这种方式不常用。

ResponseEntity<UserMapper> restExchange = restTemplate.exchange("http://userservice/users/{userName}", HttpMethod.GET, null, UserMapper.class, userName);

(二)定制化路由策略

​    1、定制化路由策略

​    做定制化路由策略就需要先了解ReactiveLoadBalancer接口,其有两个分支,一个分支是ReactorLoadbalancer,这是提供了不同的负载均衡算法实现,另一个分支是Factory(LoadBalancerClientFactory),这个一个工厂类,用于生成LoadBalancerClient客户端,该客户端使用指定的负载均衡算法。

​        

​    在负载均衡定制化可以使用配置类来进行指定负载均衡器,如下代码所示:

​        重定义了ReactorServiceInstanceLoadBalancer接口,将其注入到Spring容器中

​        使用environment.getProperty从配置文件中获取服务名称

​        使用负载均衡客户端工厂类loadBalancerClientFactory的getLazyProvider方法获取provider,即ServiceInstanceListSupplier,它是一个提供了服务端实例列表的实现类

​        最终封装成一个随机负载均衡算法实现类RandomLoadBalancer。

@Configuration
public class MyLoadBalancerConfig {
    
    // 重定义ReactorServiceInstanceLoadBalancer接口
    @Bean
    public ReactorServiceInstanceLoadBalancer reactorServiceInstanceLoadBalancer(Environment environment, LoadBalancerClientFactory loadBalancerClientFactory) {
        String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
        //返回随机轮询负载均衡方式
        return new RandomLoadBalancer(loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class), name);
    }
}

​    总体来说就是根据服务名称获取所有的服务实例,然后将其封装成自己想要的负载均衡算法。

​    由此可见,定制化负载均衡策略主要分三个步骤:

​        实现ReactorServiceInstanceLoadBalancer接口

​        初始化自定义MyLoadBalancerConfig配置

​        通过@LoadBalancerClient注解指定自定义配置

​    2、定制化路由代码实现--参照与复制

​    (1)负载均衡配置类

@Configuration
public class RandomLoadBalancerConfig {
    // 重定义ReactorServiceInstanceLoadBalancer接口
    @Bean
    public ReactorServiceInstanceLoadBalancer reactorServiceInstanceLoadBalancer(Environment environment, LoadBalancerClientFactory loadBalancerClientFactory) {
        String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
        //返回随机轮询负载均衡方式
        return new RandomLoadBalancer(loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class), name);
    }
}

​    (2)将服务名与负载均衡器绑定

@SpringBootApplication(scanBasePackages = {"com.lcl.galaxy.microservice"})
@MapperScan("com.lcl.galaxy.microservice.middleground.customer.mapper")
@LoadBalancerClient(name = ApiConstants.SERVICE_NAME, configuration = RandomLoadBalancerConfig.class)
public class MiddlegroundCustomerServiceApplication {

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

​    3、定制化路由代码实现--完全自定义

​    (1)实现自定义负载均衡算法

​        将ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider注入到自定义的负载均衡算法中,在choose方法中编写自己需要的负载均衡逻辑。

@Slf4j
public class CustomerRandomLoadBalancer implements ReactorServiceInstanceLoadBalancer {

    private ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;

    public CustomerRandomLoadBalancer(ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider) {
        this.serviceInstanceListSupplierProvider = serviceInstanceListSupplierProvider;
    }

    @Override
    public Mono<Response<ServiceInstance>> choose(Request request) {
        ServiceInstanceListSupplier supplier = (ServiceInstanceListSupplier)this.serviceInstanceListSupplierProvider.getIfAvailable(NoopServiceInstanceListSupplier::new);
        return supplier.get(request).next().map((serviceInstances) -> {
            return this.getInstanceResponse(serviceInstances);
        });
    }

    private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances) {
        log.info("进入自定义负载均衡算法");
        if (instances.isEmpty()) {
            return new EmptyResponse();
        }
        log.info("执行自定义负载均衡算法");
        int index = ThreadLocalRandom.current().nextInt(instances.size());
        ServiceInstance instance = (ServiceInstance)instances.get(index);
        return new DefaultResponse(instance);
    }
}

​    (2)负载均衡配置类

​        将负载均衡算法实现注入Spring 容器中

@Configuration
public class CustomerRandomLoadBalancerConfig {
    // 重定义ReactorServiceInstanceLoadBalancer接口
    @Bean
    public ReactorServiceInstanceLoadBalancer customerLoadBalancer(ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider) {
        //返回随机轮询负载均衡方式
        return new CustomerRandomLoadBalancer(serviceInstanceListSupplierProvider);
    }
}

​    (3)将服务名称与负载均衡绑定

@SpringBootApplication(scanBasePackages = {"com.lcl.galaxy.microservice"})
@MapperScan("com.lcl.galaxy.microservice.middleground.customer.mapper")
@LoadBalancerClient(name = ApiConstants.SERVICE_NAME, configuration = CustomerRandomLoadBalancerConfig.class)
public class MiddlegroundCustomerServiceApplication {

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

​    基于以上流程,就可以完全定制一个我们自己想要的负载均衡策略。

​    4、OpenFeign处理机制

​    Feign是一种基于接口的声明式HTTP客户端,可以通过@FeignClient注解定义接口并自动生成REST API的客户端实现,而RequestInterceptor是Feign提供的一种机制,可以用于在发送请求前或响应后对请求进行自定义处理。

​    首先需要自定义一个RequestInterceptor,并对RequestTemplate做修改

public class MyFeignRequestInterceptor implements RequestInterceptor {
    @Override
    public void apply(RequestTemplate requestTemplate) { 
        // Feign请求过程中,对于传输数据的各种定制化处理机制扩展
        ...
    }
}

​    然后将其作为RequestInterceptor注入到Spring容器中

@Configuration
public class MyConfiguration {
    @Bean
    public RequestInterceptor requestInterceptor() {
        return new MyFeignRequestInterceptor();
    }
}

​    在设置Openfeign客户端时指定配置类

@FeignClient(name = ApiConstants.SERVICE_NAME, configuration = MyConfiguration.class)
public interface ClientService {
}

(三)客服系统案例演进

​    基于OpenFeign处理机制可以做很多定制化的路由方式,下面演示基于标签的定制化路由机制。

​    定义Tag工具类,可以从ServiceInstance和HttpHeaders中获取tag的值,也可以在requestTemplate的header中设置tag的值

public class TagUtils {
    private static final String TAG_NAME = "tag";

    public static String getTag(ServiceInstance serviceInstance){
        // 通过服务定义的元数据来获取标签信息
        return serviceInstance.getMetadata().get(TAG_NAME);
    }

    public static String getTag(HttpHeaders headers){
        return headers.getFirst(TAG_NAME);
    }

    public static void setTagName(RequestTemplate requestTemplate, String tag){
        requestTemplate.header(TAG_NAME, tag);
    }
}

​    在配置文件设置tag的值

tag:
  cs

​    自定义负载均衡算法:这里是关键点,在choose方法中,首先获取到所有的实例,然后判断哪些实例元数据中tag的值和当前服务要访问的tag一致,如果存在一致的服务实例,就返回满足条件的服务实例集合,如果没有,就返回全部实例集合(这里可以根据自己的要求设计返回全部实例还是返回空),最后用Nacos提供的加权随机算法NacosBalancer.getHostByRandomWeight3选择一个实例。

@Slf4j
public class TagLoadBalancer implements ReactorServiceInstanceLoadBalancer {

    @Value("${tag}")
    private String tagValue;

    private ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;

    public TagLoadBalancer(ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider) {
        this.serviceInstanceListSupplierProvider = serviceInstanceListSupplierProvider;
    }

    @Override
    public Mono<Response<ServiceInstance>> choose(Request request) {
        ServiceInstanceListSupplier supplier = (ServiceInstanceListSupplier)this.serviceInstanceListSupplierProvider.getIfAvailable(NoopServiceInstanceListSupplier::new);
        return supplier.get(request).next().map((serviceInstances) -> this.getInstanceResponse(serviceInstances, tagValue));
    }

    private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances, String tagValue) {
        log.info("进入自定义负载均衡算法");
        if (instances.isEmpty()) {
            return new EmptyResponse();
        }

        List<ServiceInstance> chooseInstances = filterList(instances, instance -> tagValue.equals(TagUtils.getTag(instance)));

        if(CollUtil.isEmpty(chooseInstances)){
            log.info("没有找到满足需求的实例");
            chooseInstances = instances;
        }

        // 使用 Nacos 提供的加权随机负载均衡
        return new DefaultResponse(NacosBalancer.getHostByRandomWeight3(chooseInstances));
    }

    public static <T> List<T> filterList(Collection<T> from, Predicate<T> predicate) {
        if(CollUtil.isEmpty(from)){
            return new ArrayList<>();
        }
        return from.stream().filter(predicate).collect(Collectors.toList());
    }
}

​    实例的元数据在Nacos控制台可以进行设置:

​        ![image-20230715181631144](/Users/conglongli/Documents/sumUp/客服系统/image/10/nacos实例元数据设置.png" width="30%" height="30%"/>

​    这种方式可以用来做很多扩展,例如将tag换为version,就可以来做灰度发布。

五、Spring Cloud LoadBalancer负载均衡架构解析

(一)LoadBalancerClient

​    1、LoadBalancerClient接口

​    在Spring Cloud LoadBalancer负载均衡中最重要的接口是LoadBalanceerClient,这个接口是对于负载均衡的抽象,其有两个支线,一个是面对RestTemplate工具类和WebClient的@Loadbalancerd注解,一个是面向FeignClient的@FeignClient注解。

​        

​    LoadBalancerClient接口继承了ServiceInstanceChooser接口,在ServiceInstanceChooser接口中,提供了根据服务ID获取服务实例的choose方法,在LoadBalancerClient中扩展了执行调用的方法execute和组装调用对象的reconstructURI方法。

​    这样串下来是比较好理解的,当服务调用时,使用choose选择一个实例,然后执行reconstructURI方法根据实例的IP和端口拼装URI,然后执行execute方法进行调用。

public interface ServiceInstanceChooser {
    //根据特定服务获取服务实例 
    ServiceInstance choose(String serviceId);

    //根据特定服务和请求获取服务实例 
    <T> ServiceInstance choose(String serviceId, Request<T> request);
}

public interface LoadBalancerClient extends ServiceInstanceChooser {
    //执行服务调用,使用从负载均衡器中挑选出的服务实例来执行请求内容 
    <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException;

    <T> T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest<T> request) throws IOException;

    //根据所选ServiceInstance的host和port再加上服务的端点路径来构造一个真正可供访问的服务 
    URI reconstructURI(ServiceInstance instance, URI original);
}

​    2、实现类BlockingLoadBalancerClient

​    在Spring Cloud Alibaba中有很多LoadBalancerClient的实现类,例如BlockingLoadBalancerClient,这是一个阻塞式的负载均衡实现类,在其choose方法中,使用loadBalancerClientFactory根据服务ID获取了一个负载均衡器实例,然后调用负载均衡器的choose方法获取了负载均衡结果。

​    由于在负载均衡器中使用的都是响应式编程,在该实现中使用了Mono.from(loadBalancer.choose(request)).block()将响应式转为阻塞式,这也是该实现类名称的由来。

public class BlockingLoadBalancerClient implements LoadBalancerClient {
    @Override
    public <T> ServiceInstance choose(String serviceId, Request<T> request) {
        // 通过LoadBalancerClientFactory 获取负载均衡器实例
        ReactiveLoadBalancer<ServiceInstance> loadBalancer = loadBalancerClientFactory.getInstance(serviceId);
        if (loadBalancer == null) {
            return null;
        }
        // 通过block方法阻塞获取负载均衡执行结果
        Response<ServiceInstance> loadBalancerResponse = Mono.from(loadBalancer.choose(request)).block();
        if (loadBalancerResponse == null) {
            return null;
        }
        return loadBalancerResponse.getServer();
    }
}

​    在BlockingLoadBalancerClient的execute方法中,首先基于Hint机制实现强制负载均衡,使用LoadBalancerRequestAdapter来获取请求,然后基于choose方法获取目标服务实例,最后执行request.apply方法来做远程调用。

public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException {
    // 基于Hint机制实现强制负载均衡
    String hint = getHint(serviceId);
    LoadBalancerRequestAdapter<T, DefaultRequestContext> lbRequest = new LoadBalancerRequestAdapter<>(request, new DefaultRequestContext(request, hint));
    // 基于choose方法获取目标服务实例
    ServiceInstance serviceInstance = choose(serviceId, lbRequest);
    return execute(serviceId, serviceInstance, lbRequest);
}

public <T> T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest<T> request) throws IOException {
    try {
        // 执行远程调用
        T response = request.apply(serviceInstance);
        Object clientResponse = getClientResponse(response);
        return response;
    }
    ...
    return null;
}

​    3、ReactiveLoadBalancer

​    下图是负载均衡中最基本的组件,最上层的是ReactiveLoadBalancer接口,子接口有ReactorLoadBalancer和ReactorServiceInstanceLoadBalancer,对应的实现类有轮询负载均衡器RoundRobinLoadBalancer、随机负载均衡器RandomLoadBalancer、基于Nacos的负载均衡器NacosLoadBalancer,以及上面自己创建的CustomerRandomLoadBalancer和TagLoadBalancer。

​    作为最顶层的ReactiveLoadBalancer接口,提供了根据请求来获取实例的choose方法。

public interface ReactiveLoadBalancer<T> {
    Publisher<Response<T>> choose(Request request);
}

​        

​    以系统自带的RandomLoadBalancer为例,源代码如下所示,和前面自己写的基本上一模一样,首先根据serviceInstanceListSupplierProvider获取所有状态正常的实例,然后调用getInstanceResponse使用具体的负载均衡算法选择一个实例,这里的负载均衡算法是随机,在实现时使用了线程安全的随机获取方式。

public class RandomLoadBalancer implements ReactorServiceInstanceLoadBalancer {
    private final String serviceId;
    private ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;

    public Mono<Response<ServiceInstance>> choose(Request request) {
        ServiceInstanceListSupplier supplier = serviceInstanceListSupplierProvider.getIfAvailable(NoopServiceInstanceListSupplier::new);
        return supplier.get(request).next().map(serviceInstances -> processInstanceResponse(supplier, serviceInstances));
    }
    ......

    // 实现具体负载均衡算法
    private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances) {
        if (instances.isEmpty()) {
            return new EmptyResponse();
        }

        // 基于ThreadLocalRandom实现随机数
        int index = ThreadLocalRandom.current().nextInt(instances.size());
        ServiceInstance instance = instances.get(index);
        return new DefaultResponse(instance);
    }
}

​    4、LoadBalancerClientFactory

​    有了负载均衡器之后,如果获取这个负载均衡器是下一个需要考虑的点,在SpringCloud中是使用LoadBalancerClientFactory来获取对应的负载均衡器的。

​    LoadBalancerClientFactory继承了NamedContextFactory,这是Spring内置的负载均衡器,在NamedContextFactory中,提供了getInstance方法,该方法可以使用服务ID和ReactorServiceInstanceLoadBalancer类获取到Spring容器中的负载均衡器实例。

​    在LoadBalancerClientFactory中提供了根据服务ID获取负载均衡器的方法getInstance,最终调用的是NamedContextFactory中的getInstance方法。

// 根据serviceId获取 ReactorServiceInstanceLoadBalancer实现类
public class LoadBalancerClientFactory extends NamedContextFactory<LoadBalancerClientSpecification> implements ReactiveLoadBalancer.Factory<ServiceInstance> {
    @Override
    public ReactiveLoadBalancer<ServiceInstance> getInstance(String serviceId) {
        return getInstance(serviceId, ReactorServiceInstanceLoadBalancer.class);
    }
}

public abstract class NamedContextFactory<C extends NamedContextFactory.Specification> implements DisposableBean, ApplicationContextAware {
    public <T> T getInstance(String name, Class<T> type) {
        AnnotationConfigApplicationContext context = getContext(name);
        try {
            return context.getBean(type);
        } 
        return null;
    }
}

​    5、LoadBalancerRequestFactory

​    还有一个发起请求处理的工厂类LoadBalancerRequestFactory,其提供了创建请求的方法createRequest,在该方法中,首先创建了一个请求包装类ServiceRequestWrapper,然后对该类进行包装,例如嵌入Cookie等,最后调用execution.execute来实现真正的HTTP远程调用。

public class LoadBalancerRequestFactory {
    private LoadBalancerClient loadBalancer;
    private List<LoadBalancerRequestTransformer> transformers;

    public LoadBalancerRequest<ClientHttpResponse> createRequest(final HttpRequest request, final byte[] body, final ClientHttpRequestExecution execution) {
        return instance -> {
            // 包装请求,构建集成了负载均衡的URI
            HttpRequest serviceRequest = new ServiceRequestWrapper(request, instance, this.loadBalancer);
            if (this.transformers != null) {
                for (LoadBalancerRequestTransformer transformer : this.transformers) {
                    // 转换请求,嵌入Cookie等
                    serviceRequest = transformer.transformRequest(serviceRequest, instance);
                }
            }
            // 执行请求,真正实现HTTP远程调用
            return execution.execute(serviceRequest, body);
        };
    }
}

(二)RestTemplate与负载均衡

​    上面描述了负载均衡底层LoadBalancerClient接口的抽象,但是对于开发人员来说,并不使用这么底层的实现,而是使用@LoadBalanced注解和@FeignClient注解。

​    1、@LoadBalanced注解

​    @LoadBalanced注解与其他的注解没什么大的区别,主要是增加了一个@Qualifier注解。

    //自动具备负载均衡机制 
    @LoadBalanced
    @Bean
    public RestTemplate getRestTemplate() {
        return new RestTemplate();
    }

@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Qualifier
public @interface LoadBalanced {
}

​    2、LoadBalancerAutoConfiguration

​    在负载均衡器自动装配类LoadBalancerAutoConfiguration中,如果RestTemplate和LoadBalancerClient存在时,则进行自动注入,在RestTemplate上使用@LoadBalanced注解。

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RestTemplate.class)
@ConditionalOnBean(LoadBalancerClient.class) // 自动配置生效条件
@EnableConfigurationProperties(LoadBalancerClientsProperties.class)
public class LoadBalancerAutoConfiguration {
    @LoadBalanced
    @Autowired(required = false)
    private List<RestTemplate> restTemplates = Collections.emptyList();
    @Autowired(required = false)
    private List<LoadBalancerRequestTransformer> transformers = Collections.emptyList();

    // 创建LoadBalancerRequestFactory
    @Bean
    @ConditionalOnMissingBean
    public LoadBalancerRequestFactory loadBalancerRequestFactory(LoadBalancerClient loadBalancerClient) {
        return new LoadBalancerRequestFactory(loadBalancerClient, this.transformers);
    }
}

​    3、LoadBalancerInterceptorConfig:

​    在自动装配类中,还有一个拦截器自动装配类LoadBalancerInterceptorConfig,在该类中,创建了一个LoadBalancerInterceptor拦截器,并将该拦截器放入RestTemplate的拦截器集合中。

@Configuration(proxyBeanMethods = false)
@Conditional(RetryMissingOrDisabledCondition.class)
static class LoadBalancerInterceptorConfig {
    @Bean
    public LoadBalancerInterceptor loadBalancerInterceptor(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增加拦截器 LoadBalancerInterceptor
            restTemplate.setInterceptors(list);
        };
    }
}

​    4、LoadBalancerInterceptor:

​    拦截器的拦截实现就比较简单了,首先获取请求的URI和serviceName,然后调用负载均衡器的execute方法执行远程调用,具体的执行流程上面已经提到,就是具体的LoadbalancerClient处理流程。

public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor {
    private LoadBalancerClient loadBalancer;
    private LoadBalancerRequestFactory requestFactory;

    @Override
    public ClientHttpResponse intercept(final HttpRequest request, final byte[] body, final ClientHttpRequestExecution execution) throws IOException {
        final URI originalUri = request.getURI();
        String serviceName = originalUri.getHost();
        // 通过LoadBalancerClient执行负载均衡
        return this.loadBalancer.execute(serviceName, this.requestFactory.createRequest(request, body, execution));
    }
}

​    5、总结

​    @LoadBalanced注解整体集成流程是通过LoadBalancerAutoConfiguration将@LoadBalanced注解注入到RestTemplate上,同时也通过LoadBalancerInterceptorConfig将LoadBalancerInterceptor放入RestTemplate的拦截器集合中;然后在使用RestTemplate做远程调用时,会执行LoadBalancerInterceptor的拦截方法,在该方法中使用了底层的LoadBalancerClient接口处理。

​        

​    6、扩展 - 实现自定义负载均衡机制

​    根据上面的分析,如果要自己实现一个自定义负载均衡机制,主要需要四步:

​        定义@MyLoadBalanced注解

​        实现MyLoadBalancerAutoConfiguration配置类

​        实现MyLoadBalancerInterceptor

​        在RestTemplate上使用@MyLoadBalanced注解

​    7、BlockingLoadBalancerClientAutoConfiguration

​    上面主要是分析了为什么在RestTemplate上使用@LoadBalanced注解就可以做负载均衡,但是实际的负载均衡处理是在LoadBalancerClient,其对应的自动装配类是BlockingLoadBalancerClientAutoConfiguration,在该自动装配类中,创建并注入了LoadBalancerClient和LoadBalancerServiceInstanceCookieTransformer。

Configuration(proxyBeanMethods =false)

@LoadBalancerClients
@AutoConfigureAfter(LoadBalancerAutoConfiguration.class) // 自动配置生效条件
@AutoConfigureBefore({org.springframework.cloud.client.loadbalancer.LoadBalancerAutoConfiguration.class, AsyncLoadBalancerAutoConfiguration.class})
@ConditionalOnClass(RestTemplate.class)
public class BlockingLoadBalancerClientAutoConfiguration {

    // 创建LoadBalancerClient
    @Bean
    @ConditionalOnBean(LoadBalancerClientFactory.class)
    @ConditionalOnMissingBean
    public LoadBalancerClient blockingLoadBalancerClient(LoadBalancerClientFactory loadBalancerClientFactory) { 
        return new BlockingLoadBalancerClient(loadBalancerClientFactory);
    }

    // 创建LoadBalancerRequestTransformer
    @Bean
    @ConditionalOnBean(LoadBalancerClientFactory.class)
    @ConditionalOnMissingBean(LoadBalancerServiceInstanceCookieTransformer.class)
    public LoadBalancerServiceInstanceCookieTransformer loadBalancerServiceInstanceCookieTransformer(LoadBalancerClientFactory loadBalancerClientFactory) {
        return new LoadBalancerServiceInstanceCookieTransformer(loadBalancerClientFactory);
    }
    ...
}

(三)Feign与负载均衡

​    与RestTemplate实现负载均衡的逻辑一样,Feign的自动装配类是FeignLoadBalancerAutoConfiguration,在该装配类中,引入了多种远程调用的实现,例如HttpClient、OkHttp、HttpClient5等,每一个实现都有具体的负载均衡配置类,默认用的是DefaultFeignLoadBalancerConfiguration。

@ConditionalOnClass(Feign.class) // 自动配置生效条件
@ConditionalOnBean({LoadBalancerClient.class, LoadBalancerClientFactory.class})
@AutoConfigureBefore(FeignAutoConfiguration.class)
@AutoConfigureAfter({BlockingLoadBalancerClientAutoConfiguration.class, LoadBalancerAutoConfiguration.class})
@EnableConfigurationProperties(FeignHttpClientProperties.class)
@Configuration(proxyBeanMethods = false)
@Import({HttpClientFeignLoadBalancerConfiguration.class,
        OkHttpFeignLoadBalancerConfiguration.class, 
        HttpClient5FeignLoadBalancerConfiguration.class, 
        DefaultFeignLoadBalancerConfiguration.class // 默认配置 
        })
public class FeignLoadBalancerAutoConfiguration {
}

​    在DefaultFeignLoadBalancerConfiguration中,实际上创建了一个FeignBlockingLoadBalancerClient对象。

@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(LoadBalancerClientsProperties.class)
class DefaultFeignLoadBalancerConfiguration {
    @Bean
    @ConditionalOnMissingBean
    @Conditional(OnRetryNotEnabledCondition.class)
    public Client feignClient(LoadBalancerClient loadBalancerClient, LoadBalancerClientFactory loadBalancerClientFactory) {
        // 创建FeignBlockingLoadBalancerClient
        return new FeignBlockingLoadBalancerClient(new Client.Default(null, null), loadBalancerClient, loadBalancerClientFactory);
    }
}

​    在FeignBlockingLoadBalancerClient的execute方法中,调用loadBalancerClient的choose方法选择了一个实例,然后基于LoadBalancerClient构建URI并穿件新的请求对象,最后调用executeWithLoadBalancerLifecycleProcessing方法做远程调用。

public Response execute(Request request, Request.Options options) throws IOException {
    final URI originalUri = URI.create(request.url());
    String serviceId = originalUri.getHost();
    String hint = getHint(serviceId);
    // 基于LoadBalancerClient获取服务实例
    ServiceInstance instance = loadBalancerClient.choose(serviceId, lbRequest);
    if (instance == null) {
        return Response.builder().request(request).status(HttpStatus.SERVICE_UNAVAILABLE.value()).body(message, StandardCharsets.UTF_8).build();
    }
    // 基于LoadBalancerClient构建URI
    String reconstructedUrl = loadBalancerClient.reconstructURI(instance, originalUri).toString();
    Request newRequest = buildRequest(request, reconstructedUrl);
    return executeWithLoadBalancerLifecycleProcessing(delegate, options, newRequest, lbRequest, lbResponse, supportedLifecycleProcessors);
}

​    远程调用最终会执行SynchronousMethodHandler的invoke方法,在该方法中,使用FeignBlockingLoadBalancerClient 执行远程调用,在调用过程中可以构建RequestTemplate,实现对请求的定制化处理。

final class SynchronousMethodHandler implements MethodHandler {
    @Override
    public Object invoke(Object[] argv) throws Throwable { 
      ...
      return executeAndDecode(template, options);
    }

    Object executeAndDecode(RequestTemplate template, Options options) throws Throwable {
        ...
        Response response;
        try {
            // 使用FeignBlockingLoadBalancerClient 执行远程调用
            response = client.execute(request, options);
            // 构建RequestTemplate,实现对请求的定制化处理
            response = response.toBuilder().request(request).requestTemplate(template).build();
        }
    }
}

​    最终使用FeignInvocationHandler做动态代理

// 实现JDK动态代理
static class FeignInvocationHandler implements InvocationHandler {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { 
        ...
        // 集成SynchronousMethodHandler
        return dispatch.get(method).invoke(args);
    }
}

​    对于Feign整体集成流程如下图所示:

​        

posted @ 2023-07-25 16:38  李聪龙  阅读(910)  评论(0编辑  收藏  举报