源码分析及实践测试OpenFeign负载均衡

1 缘起

补课吧。
之前一直着急往前赶进度,
只梳理了微服务架构以及如何使用这些架构中的组件,
然而,并不了解运作原理,
我依旧还是太弱了,经不起大风大浪,
所以,想使自己强壮一些,继续研究一下源码。
还有另外一个原因,最近看了K8S,并实践了K8S部署SpringBoot服务,
发现,可以直接使用K8S进行负载均衡,
于是,又想到,Spring自己也有负载均衡,是如何实现的?
所以,有了这篇文章。

2 源码分析

2.1 如何找到入口

我们知道,
当开发同学使用SpringBoot进行开发时,
没有配置某些Bean,但是,可以直接使用这些Bean,
就说明,SpringBoot在启动时自动装配了这些Bean,
OpenFeign的负载均衡是同样的的道理,
开发同学没有配置负载均衡策略,但是,使用OpenFeign可以自动进行均衡,
因此,OpenFeign的负载均衡是通过自动装配自动完成的,
所以, 找负载均衡的自动装配类,
进入OpenFeign的源码,找到的LoadBalancer自动装配类,源码如下图所示。
位置:org.springframework.cloud.openfeign.loadbalancer.FeignLoadBalancerAutoConfiguration
该类即一种自动装配,实例化基于LoadBalancerClient的Client实现,
LoadBalancerClient是接口,即实现LoadBalancerClient。
在这里插入图片描述

2.2 寻找负载均衡策略

通过负载均衡入口类FeignLoadBalancerAutoConfiguration,我们看到标识了很多个注解,
需要关注的是LoadBalancerClientFactory负载均衡工厂,
我是怎么知道的?
挨个点进去看看呗。
在这里插入图片描述

2.2.1 负载均衡客户端工厂

这个工厂是干啥的?
从厂名猜测一下,应该是装配负载均衡客户端的。
下面,穿上工作服,进入负载均衡工厂吧!
首先看看工厂里都有啥?
源码如下图所示,由注释可知,该工厂就是生产客户端、负载均衡和客户端配置实例的。
他为每个客户端名称创建一个Spring应用上下文,并且根据需要提取。
位置:org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory
在这里插入图片描述

2.2.2 负载均衡客户端配置

通过上图,仔细阅读,不难发现,
工厂的构造器LoadBalancerClientFactory()
通过继承NamedContextFactory<LoadBalancerClientSpecification>进行实例化,
而LoadBalancerClientSpecification就是指定负载均衡客户端,
所以,可以看下使用的类:LoadBalancerClientConfiguration.class,
猜想,这个类就是指定负载均衡策略的。(其实,是先点进去才知道,马后炮)
位置:org.springframework.cloud.loadbalancer.annotation.LoadBalancerClientConfiguration
该类源码如下图所示,由源码可知,
创建的Bean:reactorServiceInstanceLoadBalancer使用的负载均衡策略是“轮询”方式,
RoundRobinLoadBalancer。
在这里插入图片描述

2.3 “轮询式”负载均衡策略

老规矩,进入源码查看实现,
看看这个“轮询”的负载均衡策略是如何实现的?
位置:org.springframework.cloud.loadbalancer.core.RoundRobinLoadBalancer#RoundRobinLoadBalancer(org.springframework.beans.factory.ObjectProvider<org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier>, java.lang.String)
第一层的方法如下图所示,
这里关注第三个参数:new Random().nextInt(1000)
随机生成值域为[1, 1000]的种子,用于均衡客户端,而不是按顺序轮询。
在这里插入图片描述
实例化的参数如下图所示,
position作为种子。
在这里插入图片描述

接下来,看看是如何使用这个种子进行均衡的,
方法调用路径:choose-》processInstanceResponse-》getInstanceResponse,
因此,最终的均衡逻辑在:getInstanceResponse
位置:org.springframework.cloud.loadbalancer.core.RoundRobinLoadBalancer#getInstanceResponse
均衡逻辑中,有一个TODO: enforce order?是否强制均衡顺序?
这里使用的均衡,并不是顺序均衡,
而是通过随机种子与服务数量取余(pos%servcie.size),来决定将请求均衡到哪个服务上。

在这里插入图片描述

到这里,完成OpenFeign负载均衡探索。
下面验证下OpenFeign的负载均衡。


3 实践

本次实验的服务架构如下图所示。
共有三个模块:注册中心(Eureka)、消费者(tutorial)和生产者(spring-boot-template,三个)。

序号模块描述
1注册中心这里绘制的是Eureka集群,但是,实际实验过程,只用了一个Eureka服务
2消费者通过集成OpenFeign和LoadBalancer调用生产者,验证LoadBalancer的负载均衡功能
3生产者对外提供接口,注册到Eureka,其他服务可以通过OpenFeign调用

在这里插入图片描述

3.1 依赖

SpingCloud版本:2020.0.3
因此,集成Eureka后,需要将默认的Ribbon移除,
添加SpringCloud的负载均衡。

<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
	<exclusions>
		<exclusion>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
		</exclusion>
	</exclusions>
	<version>2.2.3.RELEASE</version>
</dependency>
<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-loadbalancer</artifactId>
</dependency>
<dependencyManagement>
	<dependencies>
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-dependencies</artifactId>
			<version>2020.0.3</version>
			<type>pom</type>
			<scope>import</scope>
		</dependency>
	</dependencies>
</dependencyManagement>

3.2 配置消费者(Consumer)FeignClient:服务A

服务A需要调用同一注册中心(Eureka实验用)的服务B,
因此,需要在服务A(tutorial)中配置服务B的Feigin客户端,以调用服务B(spring-boot-template),
服务A(tutorial)中配置的FeiginClient如下所示,
通过@FeignClient(value = “spring-boot-template”)指定生产者(Provider)。

package com.monkey.tutorial.common.rpc;

import com.monkey.tutorial.common.constant.MicroServiceApiConstant;
import com.monkey.tutorial.common.constant.MicroServiceNameConstant;
import com.monkey.tutorial.common.response.Response;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

/**
 * FeignTemplate服务调用.
 *
 * @author xindaqi
 * @date 2021-12-22 10:01
 */
@FeignClient(value = "spring-boot-template")
public interface IFeignTemplateService {

    /**
     * 测试接口.
     *
     * @return 测试结果
     */
    @RequestMapping(MicroServiceApiConstant.API_GET_TEST)
    Response<String> feign1Test(@RequestParam("msg") String msg);

    @RequestMapping(MicroServiceApiConstant.API_FEIGN_TEST)
    String feign2Test();
}

3.3 启用FeignClient

package com.monkey.tutorial;

import com.monkey.tutorial.common.constant.MicroServiceNameConstant;
import io.micrometer.core.instrument.MeterRegistry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.actuate.autoconfigure.metrics.MeterRegistryCustomizer;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClient;
import org.springframework.cloud.loadbalancer.core.RandomLoadBalancer;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;

@SpringBootApplication
@EnableCaching
@EnableFeignClients
@ServletComponentScan
public class TutorialApplication {

	private static final Logger logger = LoggerFactory.getLogger(TutorialApplication.class);

	public static void main(String[] args) {
		SpringApplication.run(TutorialApplication.class, args);
		logger.info("Tutorial 成功启动");
	}
}

3.4 配置生产者(Provider):服务B

这里,在单台机器上开多个生产者的服务,
使用3个生产者:p1,p2和p3

3.4.1 配置文件

  • application.yml
spring:
  main:
    allow-bean-definition-overriding: true
  application:
    name: spring-boot-template
  profiles:
    active: dev
  web:
    resources:
      static-locations: classpath:/resources/
mybatis:
  config-location: classpath:/config/mybatis-config.xml
  mapper-locations: classpath:mapper/*.xml
logging:
  config: classpath:config/logback.xml

每个生产者中这里精简了许多配置,
只给出了必要的配置:端口(用于区分服务)、Eureka(注册到Eureka)

  • application-p1.yml
server:
  port: 9321
  servlet:
    session:
      timeout: PT10S
eureka:
  client:
    fetch-registry: true
    register-with-eureka: true
    service-url:
      defaultZone: http://localhost:8001/eureka/eureka
  • application-p2.yml
server:
  port: 9322
  servlet:
    session:
      timeout: PT10S
eureka:
  client:
    fetch-registry: true
    register-with-eureka: true
    service-url:
      defaultZone: http://localhost:8001/eureka/eureka
  • application-p3.yml
server:
  port: 9323
  servlet:
    session:
      timeout: PT10S
eureka:
  client:
    fetch-registry: true
    register-with-eureka: true
    service-url:
      defaultZone: http://localhost:8001/eureka/eureka

3.4.2 生产者接口

package com.monkey.springboottemplate.api;

import com.monkey.springboottemplate.common.enms.BizExceptionResponseCodeEnums;
import com.monkey.springboottemplate.common.exception.BizException;
import com.monkey.springboottemplate.common.response.Response;
import org.apache.ibatis.annotations.Param;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;

import static com.monkey.springboottemplate.common.constant.DigitalConstant.ONE;

/**
 * 测试接口.
 *
 * @author xindaqi
 * @date 2021-04-30 18:01
 */
@RestController
@RequestMapping("/api/v1")
public class TestApi {

    @GetMapping("/get/test")
    public Response<String> getTest(@RequestParam("msg") String msg, HttpServletRequest httpServletRequest) {
        int localPort = httpServletRequest.getLocalPort();
        StringBuilder sb = new StringBuilder();
        sb.append(msg).append(",LocalPort:").append(localPort);

        return Response.success(sb.toString());
    }

    @GetMapping("/feign")
    public String feignTest() {
        return "feign";
    }
}

4 IDEA配置

IDEA指定配置文件,分别启动生产者。
在Configuration中配置需要启动的主函数,如下图所示。
在这里插入图片描述
这里使用三个生产者(Provider),因此新建三个Application,
分别为Provider-1、Provider-2和Provider-3,
添加运行参数,指定运行时激活的配置文件,如下图所示。

--spring.profiles.active

在这里插入图片描述
分别启动三个生产者,启动成功后,在IDEA中开启了三个Provider,
如下图所示。
在这里插入图片描述

5 注册中心

启动注册中心Eureka,
之后启动消费者和3个生产者,
总共有4个服务注册到Eureka,
登录Eureka,如下图所示,可以看到。
在这里插入图片描述

6 测试

通过消费者接口调用消费者,
每次调用都会均衡到不同的消费者。
下面测试了三次,结果如下图所示,
通过不同的接口判定请求的是哪个消费者。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

7 小结

(1)SpringCloud:2020.0.3,已经弃用Netflix的Ribbon,
使用自有组件spring-cloud-loadbalancer做负载均衡,因此,集成Eureka时需要移除ribbon;
(2)spring-cloud-loadbalancer使用默认的负载均衡策略为:随机“轮询”方式,通过随机种子与服务数量取余,选择均衡的服务;
(3)消费者无需选择负载均衡策略,OpenFeign启动时,自动装配负载均衡器;
(4)spring-cloud-loadbalancer负载均衡有两种:纯随机和随机轮询。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

天然玩家

坚持才能做到极致

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

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

打赏作者

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

抵扣说明:

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

余额充值