【深入理解SpringCloud微服务】深入理解Ribbon原理并手写一个微服务负载均衡器

news2024/11/22 19:49:36

深入理解Ribbon原理并手写一个微服务负载均衡器

  • 负载均衡器
  • 理解Ribbon原理
  • 手写一个微服务负载均衡器
    • 总体设计
    • LoadBalanceClientHttpRequestFactory
    • SimpleLoadBalanceClient
    • SimpleLoadBalancer
    • LoadBalanceRule
    • spring.factories与LoadBalanceConfig

负载均衡器

在微服务架构里面,我们的服务消费者请求服务提供者,通常使用RestTemplate发起http请求。

在这里插入图片描述

我们可以写死服务提供者的ip地址和端口号,然后通过RestTemplate发起http请求时指定该服务提供者的ip地址和端口号。我们可以写死服务提供者的ip地址端口号,但是一个服务通常有好几个服务提供者节点组成一个集群,这时候服务消费者就要记录所有服务提供者的ip地址端口号,并且要自行决定请求哪一个节点,这是非常不便于维护的。即使只有一个服务提供者,它的ip地址和端口好也是有可能会变的。

在这里插入图片描述

在微服务的世界里,负载均衡器是一个重要组成部分。而负载均衡器可以使得服务消费者可以按照某种负载均衡策略请求微服务集群中的不同服务提供者节点。

在这里插入图片描述

由于有了负载均衡器,服务消费者请求服务提供者不再需要通过ip地址加端口号的方式,而是可以以服务名作为域名,负载均衡器会通过一定的负载均衡策略,选择服务名对应的微服务集群中的其中一个服务提供者节点,将请求地址中的服务名替换为该节点的ip地址端口号。

在这里插入图片描述

理解Ribbon原理

Ribbon是一个经典的微服务负载均衡器,它是微服务客户端的负载均衡器。通过引入Ribbon,我们的服务消费者可以通过Ribbon的负载均衡机制,选择服务提供者集群中的某个节点发起请求。

在这里插入图片描述

Ribbon通过在RestTemplate中加入拦截器的方式,扩展了RestTemplate的能力,使得它具备客户端负载均衡的能力。Ribbon会在RestTemplate的拦截器链interceptors中加入一个自己的拦截器LoadBalancerInterceptor,这个LoadBalancerInterceptor会为RestTemplate提供负载均衡的能力。

在这里插入图片描述

LoadBalancerInterceptor被添加到RestTemplate之后,每个通过RestTemplate发起的http请求都会经过LoadBalancerInterceptor的处理。LoadBalancerInterceptor会调用LoadBalancerClient负载均衡客户端进行处理,LoadBalancerClient会通过Ribbon的负载均衡器ILoadBalancer根据负载均衡策略从服务提供者列表中选出一个节点,然后LoadBalancerClient根据选取到的负载均衡节点的ip地址和端口号重写请求的url。

在这里插入图片描述

这样,RestTemplate拿到重写后的url,就可以请求对应的服务提供者节点了。

那么还剩下一个问题,LoadBalancerInterceptor是什么时候又是如何被添加到RestTemplate的拦截器链的呢?

其实Ribbon利用了Spring的SmartInitializingSingleton这个扩展点,Spring会在完成所有非懒加载单例bean的初始化后,触发SmartInitializingSingleton的调用。Ribbon扩展了Spring的这个SmartInitializingSingleton接口并往Spring容器中注册。

在这里插入图片描述

Spring在完成所有非懒加载单例bean的初始化后触发该SmartInitializingSingleton的调用,往RestTemplate的拦截器链中添加LoadBalancerInterceptor。

在这里插入图片描述

手写一个微服务负载均衡器

了解了微服务负载均衡器的作用,又理解了Ribbon的原理之后,我们就可以参照Ribbon动手写一个自己的微服务负载均衡器了。

我们大体上还是参照Ribbon增强RestTemplate的方式,但是我们不像Ribbon那样往RestTemplate的拦截器链上加入自己的拦截器,而是使用另外一个接口ClientHttpRequestFactory。

在RestTemplate发起http请求时,会调用ClientHttpRequestFactory的createRequest(URI uri, HttpMethod httpMethod)方法构建一个ClientHttpRequest对象,里面包含了请求的url地址。然后再调用这个request对象的execute()方法发起http请求,返回一个response对象。这一切的逻辑就在RestTemplate的doExecute()方法中。

RestTemplate#doExecute

	protected <T> T doExecute(URI url, HttpMethod method, ...) throws RestClientException {

		...
		ClientHttpResponse response = null;
		try {
			// 调用ClientHttpRequestFactory的createRequest()方法方法构造ClientHttpRequest
			ClientHttpRequest request = createRequest(url, method);
			...
			// 调用ClientHttpRequest的execute()方法发起http请求,返回response
			response = request.execute();
			...
		}
		catch (...) {...}
		...
	}

在这里插入图片描述

总体设计

于是我们的大体设计就是实现一个自己的ClientHttpRequestFactory,在ClientHttpRequestFactory的createRequest方法里面进行负载均衡和重构url的操作。而我们的ClientHttpRequestFactory对象也是通过Spring的扩展点SmartInitializingSingleton接口放入到RestTemplate中。

在这里插入图片描述

我们的框架设计大概就是下面那样:

在这里插入图片描述

除了ClientHttpRequestFactory以外,我们还要实现LoadBalanceClient负载均衡客户端,ClientHttpRequestFactory会调用LoadBalanceClient。然后LoadBalanceClient里面是一个loadBalancerMap(负载均衡器map),key是服务名,value是对应的LoadBalancer负载均衡器。

那么整体流程如下:

  1. ClientHttpRequestFactory调用LoadBalanceClient
  2. LoadBalanceClient从url中取出serviceName,以serviceName为key从loadBalancerMap中取出对应的LoadBalancer
  3. LoadBalancer进行负载均衡选取一个节点
  4. LoadBalanceClient获取LoadBalancer返回的节点,根据节点的ip地址和port端口重写url
  5. ClientHttpRequestFactory利用重写的url构建ClientHttpRequest对象

在这里插入图片描述

上图除开灰色部分,其余的部分都是我们要实现的逻辑。

其中LoadBalancer里面还有一个RegistryCenterClient对象和LoadBalanceRule对象。RegistryCenterClient是注册中心客户端,用于从注册中心中根据服务名serviceName查询服务提供者列表的。而LoadBalanceRule则是负载均衡规则。

在这里插入图片描述

总体设计就讲述完毕,下面我们就去看一下代码。

LoadBalanceClientHttpRequestFactory

我们实现的ClientHttpRequestFactory名字叫做LoadBalanceClientHttpRequestFactory,它实现了ClientHttpRequestFactory接口。在LoadBalanceClientHttpRequestFactory中调用我们自己实现的负载均衡客户端LoadBalanceClient进行负载均衡和重构url的操作,然后使用重构后的url构建一个Request对象返回。

在这里插入图片描述

public class LoadBalanceClientHttpRequestFactory implements ClientHttpRequestFactory {
	
	private ClientHttpRequestFactory parent;
	
	private LoadBalanceClient loadBalanceClient;

	...

	@Override
	public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException {
		uri = URI.create(reconstructUri(uri.toString()));
		// 使用重构后的url构建request对象
		return parent.createRequest(uri, httpMethod);
	}
	
	private String reconstructUri(String uri) {
		String serviceName = null;
		boolean startsWithHttps = uri.startsWith("https");
		String temp = uri.replace(startsWithHttps ? "https://" : "http://", "");
		serviceName = temp.contains("/") ? temp.substring(0, temp.indexOf("/")) : temp;
		// 调用我们自己实现的负载均衡客户端LoadBalanceClient进行负载均衡和重构url的操作
		uri = loadBalanceClient.reconstructUrl(serviceName, uri);
		return uri;
	}

可以看到构建Request对象时,我们调用的是parent.createRequest(uri, httpMethod),这个parent也是一个ClientHttpRequestFactory,它是Spring提供的SimpleClientHttpRequestFactory,通过它就可以使用给定的url构建一个Request对象,无需我们重复造轮子。

在这里插入图片描述

SimpleLoadBalanceClient

LoadBalanceClientHttpRequestFactory会调用LoadBalanceClient接口的reconstructUrl(serviceName, uri)方法,LoadBalanceClient是我们定义的负载均衡客户端接口,具体实现类是SimpleLoadBalanceClient。

public class SimpleLoadBalanceClient implements LoadBalanceClient {
	
	...
	
	private Map<String, LoadBalancer> loadBalancerMap;

	@Autowired
	private RegistryCenterClient registryCenterClient;
	
	@Autowired
	private LoadBalanceProperties loadBalanceProperties;
	
	...
	
	public SimpleLoadBalanceClient() {
		loadBalancerMap = new ConcurrentHashMap<>();
	}

	@Override
	public String reconstructUrl(String serviceName, String url) {
		// 根据服务名serviceName获取负载均衡器LoadBalancer
		LoadBalancer loadBalancer = loadBalancerMap.get(serviceName);
		// 如果loadBalancerMap中没有对应的LoadBalancer,则创建LoadBalancer
		if (loadBalancer == null) {
			// 利用Java的SPI机制加载所有的负载均衡策略类LoadBalanceRule
			ServiceLoader<LoadBalanceRule> serviceLoader = ServiceLoader.load(LoadBalanceRule.class);
			for (LoadBalanceRule loadBalanceRule: serviceLoader) {
				// 读取LoadBalanceRule实现类上的@Rule注解
				Rule rule = loadBalanceRule.getClass().getAnnotation(Rule.class);
				// 判断@Rule注解是否与配置文件指定的负载均衡类型匹配
				if (StringUtils.equals(rule.value(), loadBalanceProperties.getType())) {
					// 创建LoadBalancer对象,实现类是SimpleLoadBalancer
					loadBalancer = new SimpleLoadBalancer(serviceName, registryCenterClient, loadBalanceRule);
					// LoadBalancer对象缓存到map中
					loadBalancerMap.put(serviceName, loadBalancer);
					break;
				}
			}
		}
		...
		// 根据负载均衡策略选取一个节点
		MicroService microService = loadBalancer.chooseMicroService();
		if (microService != null) {
			// 把url中的服务名替换成选取节点的ip地址和端口号
			url = url.replace(serviceName, microService.getIp() + ":" + microService.getPort());
		}
		return url;
	}

}

SimpleLoadBalanceClient根据serviceName从loadBalancerMap取出LoadBalancer,然后调用LoadBalancer的chooseMicroService()方法根据负载均衡策略选取一个服务提供者节点MicroService,然后把url中的服务名替换成选取出的节点的ip地址和端口号。

如果SimpleLoadBalanceClient取不到LoadBalancer,就会创建一个LoadBalancer。创建LoadBalancer前首先通过Java的SPI机制加载所有的负载均衡策略类LoadBalanceRule,再通过@Rule注解与配置文件的配置进行匹配,匹配出一个LoadBalanceRule。再以匹配到的LoadBalanceRule对象以及注册中心客户端RegistryCenterClient 为构造方法参数,创建SimpleLoadBalancer对象,缓存到loadBalancerMap中。

在这里插入图片描述

SimpleLoadBalancer

再来看一下SimpleLoadBalancer的代码,

public class SimpleLoadBalancer implements LoadBalancer {
	
	private String serviceName;
	
	private RegistryCenterClient registryCenterClient;
	
	private LoadBalanceRule loadBalanceRule;
	
	...

	@Override
	public MicroService chooseMicroService() {
		// 通过注册中心客户端根据服务名拉取服务实例列表
		List<MicroService> microServiceList = registryCenterClient.getMicroServiceList(serviceName);
		...
		// 根据负载均衡规则选出一个节点
		return loadBalanceRule.chooseMicroService(serviceName, microServiceList);
	}

}

在这里插入图片描述

SimpleLoadBalancer的逻辑很简单,就是调用负载均衡客户端RegistryCenterClient的getMicroServiceList(serviceName)方法根据服务名serviceName从注册中心拉取服务实例列表。RegistryCenterClient里面是有本地缓存的,如果本地已经缓存了服务名对应的服务实例列表,就不会请求注册中心,因此SimpleLoadBalancer里面我就没有再做一次缓存了。当SimpleLoadBalancer通过RegistryCenterClient获取到实例列表后,调用负载均衡规则LoadBalanceRule的chooseMicroService(serviceName, microServiceList)方法根据负载均衡规则从列表中选取一个节点。

LoadBalanceRule

LoadBalanceRule是负载均衡规则的接口,类似与Ribbon的IRule。我们看一个轮询策略的实现类RoundRobinLoadBalanceRule。

@Rule("roundrobin")
public class RoundRobinLoadBalanceRule implements LoadBalanceRule {
	
	// key->服务名,value->下标
	private Map<String, AtomicLong> indexMap = new ConcurrentHashMap<>();

	@Override
	public MicroService chooseMicroService(String serviceName, List<MicroService> microServices) {
		AtomicLong index = indexMap.putIfAbsent(serviceName, new AtomicLong());
		long num = index.getAndIncrement();
		return microServices.get((int) (num % microServices.size()));
	}
	

}

代码一看就懂,indexMap是服务名serviceName与AtomicLong计数器的映射,chooseMicroService方法通过通过serviceName拿到计算器,然后调用AtomicLong的getAndIncrement()进行原子自增操作,然后模上服务列表的size。

我们注意到RoundRobinLoadBalanceRule类上有一个@Rule(“roundrobin”),我们规定每个LoadBalanceRule实现类都必须被@Rule注解修饰,然后@Rule的属性是负载均衡规则名称,用于与配置文件的“loadbalance.rule.type”配置进行匹配的。

spring.factories与LoadBalanceConfig

我们使用SpringBoot的自动装配机制,在spring.factories文件中定义好我们的自动配置类LoadBalanceConfig
spring.factories

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.huangjunyi1993.simple.microservice.loadbalance.config.LoadBalanceConfig

LoadBalanceConfig就是我们的自动配置类,会往Spring中注册LoadBalanceClientHttpRequestFactory、LoadBalanceClient等核心组件,并通过SmartInitializingSingleton扩展点把LoadBalanceClientHttpRequestFactory设置到RestTemplate中。

在这里插入图片描述

@Configuration
@EnableConfigurationProperties({LoadBalanceProperties.class})
public class LoadBalanceConfig {

	@Autowired(required = false)
	private List<RestTemplate> restTemplates = Collections.emptyList();

	@Bean
	@ConditionalOnMissingBean(LoadBalanceClient.class)
	public LoadBalanceClient loadBalanceClient() {
		return new SimpleLoadBalanceClient();
	}

	@Bean
	public LoadBalanceClientHttpRequestFactory loadBalanceClientHttpRequestFactory(LoadBalanceClient loadBalanceClient) {
		return new LoadBalanceClientHttpRequestFactory(loadBalanceClient);
	}

	@Bean
	@ConditionalOnMissingBean
	public RestTemplateCustomizer restTemplateCustomizer(LoadBalanceClientHttpRequestFactory loadBalanceClientHttpRequestFactory) {
		return (restTemplate) -> restTemplate.setRequestFactory(loadBalanceClientHttpRequestFactory);
	}

	@Bean
	public SmartInitializingSingleton loadBalancedRestTemplateInitializer(List<RestTemplateCustomizer> customizers) {
		return () -> restTemplates.forEach(restTemplate -> customizers.forEach(customizer -> customizer.customize(restTemplate)));
	}

}

大源码图:
在这里插入图片描述

代码仓库地址:https://gitee.com/huang_junyi/simple-microservice/tree/master/simple-microservice-loadbalance
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1953674.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

应用层_计算机网络

文章目录 应用层HTTP用户与服务器的交互&#xff1a;cookieWeb缓存HTTP/2 SMTPDNS&#xff1a;因特网的目录服务P2P文件分发BitTorrentCDN内容分发网 应用层 应用层协议定义了运行在不同端系统上的应用程序进程如何相互传递报文。应用层协议定义了以下内容&#xff1a; 交换的…

结构性设计模式-外观模式

一、外观模式 有些人可能炒过股票&#xff0c;但其实大部分人都不太懂&#xff0c;这种没有足够了解证券知识的情况下做股票是很容易亏钱的&#xff0c;刚开始炒股肯定都会想&#xff0c;如果有个懂行的帮帮手就好&#xff0c;其实基金就是个好帮手&#xff0c;支付宝里就有许…

算力共享:如何理解、标识与调控多层次算力资源的异构性和复杂性,实现智能算力网生态诸要素有效互操作?

目录 鹏程云主机和NPU计算服务器关系 NPU计算服务器 两者关系 结论 两种不同类型的处理器或计算单元 FPGA MLU NS3(Network Simulator version 3) 一、基本属性 二、主要功能与特点 三、应用与前景 对象存储和HDD存储 一、定义与特点 二、应用场景 三、总结 对…

培养前端工程化思维,不要让一行代码毁了整个程序

看《阿丽亚娜 5 号&#xff08;Ariane 5&#xff09;火箭爆炸》有感。 1、动手写项目之前&#xff0c;先进行全局性代码逻辑思考&#xff0c;将该做的事情&#xff0c;一些细节&#xff0c;统一建立标准&#xff0c;避免为以后埋雷。 2、避免使用不必要或无意义的代码、注释。…

把 网页代码 嵌入到 单片机程序中 2 日志2024/7/26

之前不是说把 网页代码 嵌入到 单片机程序中 嘛! 目录 之前不是说把 网页代码 嵌入到 单片机程序中 嘛! 修改vs的tasks.json配置 然后 测试 结果是正常的,可以编译了 但是:当我把我都html代码都写上去之后 还是会报错!!! 内部被检测到了,没辙,只有手动更新了小工具代码 …

低功耗单声道音频编解码器ES8311中文规格书介绍

特征 具有ADC和DAC的低功耗单声道音频编解码器ES8311。 ES8311 QFN20封装的外形和丝印 系统 • 高性能、低功耗多位 delta-sigma 音频 ADC 和 DAC • I2S/PCM 主站或从站串行数据端口 • 256/384Fs、USB 12/24 MHz 和其他非标准音频系统时钟 • I2C 接口 模数转换器 • 24…

28 列表创建与删除

使用 “” 直接将一个列表赋值给变量即可创建列表对象。 my_list [a, #, 128, [12], [], {2, }, {a: 1, b: 2}] print(my_list) print(type(my_list)) print(id(my_list[0]), id(my_list[-1]))可以使用 list() 函数把元组、range对象、字符串、字典、集合或其他可迭代对象转换…

PDF管理器和查看器PdfDing

什么是 PdfDing &#xff1f; PdfDing 是一款自托管 PDF 管理器和查看器&#xff0c;可在多种设备上提供无缝用户体验。它设计精简、速度快&#xff0c;并且易于通过 Docker 设置。 功能特点 在多种设备上无缝基于浏览器的 PDF 查看使用标签整理 PDF干净且响应迅速的用户界面暗…

photoshop学习笔记——选区3

从窗口面板可以打开历史记录面板&#xff0c;历史记录面板保存了所有的操作 可以点击历史记录中某一条&#xff0c;回到当时的操作状态&#xff0c;也可以通过编辑中的 还原、重做、切换到最终状态逐步调整或直接跳到最终状态 回退之后&#xff0c;如果有新的操作&#xff0c;历…

GEE:设置ui.Map.Layer上交互矢量边界填充颜色为空,只显示边界

一、目标 最近在GEE的交互功能鼓捣一些事情&#xff0c;在利用buffer功能实现了通过选点建立一个矩形后&#xff0c;需要将该矩形填充颜色设为空&#xff0c;只留边界。 然而通过正常设置layer的可视化参数并不能实现这一目的。因此只能另辟蹊径&#xff0c;改为定义矢量边界…

项目开发实战案例 —— Spring Boot + MyBatis + Hibernate + Spring Cloud

作者简介 我是本书的作者&#xff0c;拥有多年Java Web开发经验&#xff0c;致力于帮助更多开发者快速掌握并运用Java Web技术栈中的关键框架和技术。本书旨在通过实战案例的方式&#xff0c;带领读者深入理解并实践Spring Boot、MyBatis、Hibernate以及Spring Cloud等热门技术…

reshape函数介绍及应用

reshape 函数在 MATLAB 中是一个非常有用的函数&#xff0c;通过重新排列现有元素来重构数组。它允许你重新调整数组&#xff08;或矩阵&#xff09;的尺寸&#xff0c;而不改变其数据。这个函数特别适用于当你需要将一个矩阵或数组从一种结构转换为另一种结构时&#xff0c;只…

【计算机网络】TCP负载均衡实验

一&#xff1a;实验目的 1&#xff1a;了解TCP负载均衡的配置。 2&#xff1a;学会使用NAT技术处理和外部网络的连接。 二&#xff1a;实验仪器设备及软件 硬件&#xff1a;RCMS交换机、网线、内网网卡接口、Windows 2019操作系统的计算机等。具体为&#xff1a;二层交换机1…

Redis:RDB持久化

1. 简介 实现类似照片记录效果的方式&#xff0c;就是把某一时刻的数据和状态以文件的形式写到磁盘上&#xff0c;也就是 快照。这样一来即使故障宕机&#xff0c;快照文件也不会丢失&#xff0c;数据的可靠性也就得到了保证。 这个快照文件就称为RDB文件(dump.rdb)&#xff0c…

黑马头条Day10-定时计算热点文章、xxl-job

一、今日内容 1. 需求分析 目前实现的思路&#xff1a;从数据库直接按照发布时间倒序查询 问题&#xff1a; 如果访问量比较大&#xff0c;直接查询数据库&#xff0c;压力较大新发布的文章会展示在前面&#xff0c;并不是热点文章 2. 实现思路 解决方案&#xff1a;把热点…

Android 列表或网格形式展示大量数据:RecyclerView(二):缓存复用

一、缓存复用 为什么要了解这个呢&#xff1f;当我们rv出现卡顿&#xff0c;出现闪烁的时候&#xff0c;你应该如何优化呢&#xff1f; 为什么有时候onCreateViewHolder会被调用&#xff1f;onBindVilewHolder会被调用呢&#xff1f; visiable的使用&#xff0c;会导致重新绘制…

《“王栎鑫变张艺兴”?娱乐圈乌龙背后梦幻联动与未来合作遐想》

在这个充满惊喜与欢笑的娱乐圈里&#xff0c;每一个不经意的瞬间都可能成为网友热议的焦点&#xff0c;而《快乐老友记》的最新花絮&#xff0c;无疑为这个多彩的世界又添上了一抹亮丽的色彩。当“王栎鑫被路人认成张艺兴”这一话题如春风般拂过网络&#xff0c;不仅让两位才华…

【初阶数据结构】复杂度算法题篇

旋转数组 力扣原题 方案一 循环K次将数组所有元素向后移动⼀位&#xff08;代码不通过) 时间复杂度O(n2) 空间复杂度O(1) void rotate(int* nums, int numsSize, int k) {while (k--) {int end nums[numsSize - 1];for (int i numsSize - 1; i > 0; i--) {nums[i] num…

JAVAWeb实战(前端篇)

项目实战一 0.项目结构 1.创建vue3项目&#xff0c;并导入所需的依赖 npm install vue-router npm install axios npm install pinia npm install vue 2.定义路由&#xff0c;axios&#xff0c;pinia相关的对象 文件&#xff08;.js&#xff09; 2.1路由(.js) import {cre…

【数据结构】详解二叉树及其操作

无论你觉得自己多么的了不起&#xff0c;也永远有人比你更强。&#x1f493;&#x1f493;&#x1f493; 目录 ✨说在前面 &#x1f34b;知识点一&#xff1a;二叉树的遍历 • &#x1f330;1.创建一棵二叉树 • &#x1f330;2.二叉树的遍历 •&#x1f525;前序遍历 •&a…