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

深入理解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()方法方法构造ClientHttpRequestClientHttpRequest request = createRequest(url, method);...// 调用ClientHttpRequest的execute()方法发起http请求,返回responseresponse = 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;...@Overridepublic 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;@Autowiredprivate RegistryCenterClient registryCenterClient;@Autowiredprivate LoadBalanceProperties loadBalanceProperties;...public SimpleLoadBalanceClient() {loadBalancerMap = new ConcurrentHashMap<>();}@Overridepublic String reconstructUrl(String serviceName, String url) {// 根据服务名serviceName获取负载均衡器LoadBalancerLoadBalancer loadBalancer = loadBalancerMap.get(serviceName);// 如果loadBalancerMap中没有对应的LoadBalancer,则创建LoadBalancerif (loadBalancer == null) {// 利用Java的SPI机制加载所有的负载均衡策略类LoadBalanceRuleServiceLoader<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对象,实现类是SimpleLoadBalancerloadBalancer = 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;...@Overridepublic 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<>();@Overridepublic 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();}@Beanpublic LoadBalanceClientHttpRequestFactory loadBalanceClientHttpRequestFactory(LoadBalanceClient loadBalanceClient) {return new LoadBalanceClientHttpRequestFactory(loadBalanceClient);}@Bean@ConditionalOnMissingBeanpublic RestTemplateCustomizer restTemplateCustomizer(LoadBalanceClientHttpRequestFactory loadBalanceClientHttpRequestFactory) {return (restTemplate) -> restTemplate.setRequestFactory(loadBalanceClientHttpRequestFactory);}@Beanpublic 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.mzph.cn/news/876341.shtml

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

相关文章

ElasticSearch(七)— 相关性检索和组合查询

一、 相关性评分 全文检索与数据库查询的一个显著区别&#xff0c; 就是它并不一定会根据查询条件 做完全精确的匹配。除了模糊查询以外&#xff0c;全文检索还会根据查询条件给文档的相关性打分并排序&#xff0c;将那些与查询条件相关性高的文档排在最前面。 相关性( Relev…

kubernetes service详解

一、service的类型 clusterip&#xff1a;集群内部访问externalname&#xff1a;调用外部API时使用&#xff0c;域名解析&#xff0c;让应用不用关心实际的IP地址nodeport&#xff1a;集群外部访问&#xff0c;暴漏节点上的端口&#xff0c;转发到pod内loadbalancer&#xff1…

【科大讯飞笔试题汇总】2024-07-27-科大讯飞秋招提前批(研发岗)-三语言题解(Cpp/Java/Python)

&#x1f36d; 大家好这里是清隆学长 &#xff0c;一枚热爱算法的程序员 ✨ 本系列打算持续跟新 秋招笔试题 &#x1f44f; 感谢大家的订阅➕ 和 喜欢&#x1f497; &#x1f4e7; 清隆这边最近正在收集近一年半互联网笔试题汇总&#xff0c;有需要的小伙伴可以关注 文末 公主号…

如何通过压缩提示降低GPT-4的成本

如果使用得当&#xff0c;LLMLingua可以降低使用高级LLM的成本&#xff0c;并使更广泛的用户和应用程序可以使用LLM。 像GPT-4和Claude这样的大型语言模型(LLM)可以通过良好的提示工程学习新任务。然而&#xff0c;较长的提示会增加使用这些模型的成本&#xff0c;并且还会减慢…

使用大型语言模型进行文档解析(附带代码)

动机 多年来&#xff0c;正则表达式一直是我解析文档的首选工具&#xff0c;我相信对于许多其他技术人员和行业来说也是如此。 尽管正则表达式在某些情况下功能强大且成功&#xff0c;但它们常常难以应对现实世界文档的复杂性和多变性。 另一方面&#xff0c;大型语言模型提供了…

vue3使用vue-i18n,调用t时指定语言获取翻译后的文本

今天碰到一个问题。就是使用i18n在调用t(‘key’)时&#xff0c;一般是直接返回当前语言文本。 比如我现在是简体。直接调用 t(‘commonBar.close’) 的话会返回简体 ‘关闭’。 但我现在这个地方返回其他语言&#xff0c;比如繁体。要怎么处理呢 查了文档&#xff0c;发现t函数…

writing classes ... [xxx of xxxx] 执行时间太长

一、问题展示 二、解决方法 打开设置【File - Settings…】修改堆大小

【C++】选择结构-多条件if语句

多条件if语句格式为 if(第一个条件) else if(若第一个条件未满足&#xff0c;执行此条件) {第二个条件满足执行此操作} else if(若第二个条件未满足&#xff0c;执行此条件) {第三个条件满足执行此操作} ...... else{若所有条件都不满足执行此操作} 下面是一个实例 #inc…

Qt基础 | 自定义界面组件 | 提升法 | 为UI设计器设计自定义界面组件的Widget插件 | MSVC2019编译器中文乱码问题

文章目录 一、自定义 Widget 组件1.自定义 Widget 子类2.自定义 Widget 组件的使用 二、自定义 Qt Designer 插件1.创建 Qt Designer Widget 插件项目2.插件项目各文件的功能实现3.插件的编译与安装4.使用自定义插件5.使用 MSVC 编译器输出中文的问题 一、自定义 Widget 组件 当…

Vue2从基础到实战(指令篇)

Vue中的常用指令&#xff01; 概念&#xff1a;指令&#xff08;Directives&#xff09;是 Vue 提供的带有 v- 前缀 的 特殊 标签属性。 vue 中的指令按照不同的用途可以分为如下 6 大类&#xff1a; 内容渲染指令&#xff08;v-html、v-text&#xff09; 条件渲染指令&…

计科录取75人!常州大学计算机考研考情分析!

常州大学&#xff08;Changzhou University&#xff09;&#xff0c;简称“常大”&#xff0c;位于江苏省常州市&#xff0c;是江苏省人民政府与中国石油天然气集团有限公司、中国石油化工集团有限公司及中国海洋石油集团有限公司共建的省属全日制本科院校&#xff0c;为全国深…

C++~~string模拟实现(3)

目录 1.传统写法和现代写法 2.对于流提取的优化 3.简单机制了解 4.string类的几个构造函数总结 4.1基本用法 4.2两个赋值方式 4.3拷贝构造 4.4获取字符 4.5一个容易混淆的对比 4.6创建对象 1.传统写法和现代写法 &#xff08;1&#xff09;上面的代码里面的左边部分是…

51单片机-第五节-串口通信

1.什么是串口&#xff1f; 串口是通讯接口&#xff0c;实现两个设备的互相通信。 单片机自带UART&#xff0c;其中引脚有TXD发送端&#xff0c;RXD接收端。且电平标准为TTL&#xff08;5V为1,0V为0&#xff09;。 2.常见电平标准&#xff1a; &#xff08;1&#xff09;TTL电…

pycharm+pytorch+gpu开发环境搭建

一、安装anacoda 1、下载Anaconda安装包 官网下载地址 https://www.anaconda.com/distribution/ 清华镜像 Index of /anaconda/archive/ | 清华大学开源软件镜像站 | Tsinghua Open Source Mirror 下载python3.8对应的版本Anaconda3-2021.04-Windows-x86_64.exe 下载完成…

Vue中常用指令简介

一. V-html 作用&#xff1a;更新元素的innerHTML&#xff0c;拥有响应式的特点&#xff0c;即数据驱动视图&#xff0c;解析标签&#xff0c;作用类似于js中获取dom对象&#xff0c;然后再进行innerHTML赋值。 展示了一下v-html解析标签的特点&#xff0c;这算是和插值表达式…

【网络安全的神秘世界】文件包含漏洞

&#x1f31d;博客主页&#xff1a;泥菩萨 &#x1f496;专栏&#xff1a;Linux探索之旅 | 网络安全的神秘世界 | 专接本 | 每天学会一个渗透测试工具 一、概述 文件包含&#xff1a;重复使用的函数写在文件里&#xff0c;需要使用某个函数时直接调用此文件&#xff0c;而无需再…

【Linux】生产者消费者模型 + 线程池的介绍和代码实现

前言 上节我们学习了线程的同步与互斥&#xff0c;学习了互斥锁和条件变量的使用。本章我们将学习编程的一个重要模型&#xff0c;生产者消费者模型&#xff0c;并且运用之前学的线程同步和互斥的相关接口来实现阻塞队列和环形队列&#xff0c;最后再来实现一个简易的线程池。 …

国科大作业考试资料《人工智能原理与算法》2024新编-第十三次作业整理

1、假设我们从决策树生成了一个训练集&#xff0c;然后将决策树学习应用于该训练集。当训练集的大小趋于无穷时&#xff0c;学习算法将最终返回正确的决策树吗&#xff1f;为什么是或不是&#xff1f; 本次有两个参考&#xff1a; 参考一&#xff1a; 当训练集的大小趋于无穷…

普中51单片机:蜂鸣器的简单使用(十一)

文章目录 引言蜂鸣器的分类工作原理无源蜂鸣器压电式蜂鸣器&#xff1a;电磁式蜂鸣器&#xff1a; 电路符号及应用代码演示——无源蜂鸣器 引言 蜂鸣器是一种常见的电子音响器件&#xff0c;广泛应用于各种电子产品中。它们能够发出不同频率的声音&#xff0c;用于警报、提醒、…

整数二分详解【附带PPT】

#include<bits/stdc.h> using namespace std; int n,a[1001],k;int b_search1(int l,int r,int k){while(l<r){int mlr1>>1;//检查是否满足橙色性质 if(a[m]<k) lm;else rm-1;}//循环结束l和r同时指向边界 return l; }int b_search2(int l,int r,int k){whil…