微服务之负载均衡组件Ribbon

发布时间:2023-05-25 10:30

一:负载均衡的二种实现

1.1)服务端的负载均衡(Nginx)

微服务之负载均衡组件Ribbon_第1张图片

①:我们用户服务发送请求首先打到Ng上,然后Ng根据负载均衡算法进行选择一个服务调 用,而我们的Ng部署在服务器上的,所以Ng又称为服务端的负载均衡(具体调用哪个服务, 由Ng所了算)

1.2)客户端负载均衡(ribbon

spring cloud ribbon是 基于NetFilix ribbon 实现的一套客户端的负载 均衡工具,Ribbon客户端组件提供一系列的完善的配置,如超时,重试 等。

通过Load Balancer(LB)获取到服务提供的所有机器实例,Ribbon 会自动基于某种规则(轮询,随机)去调用这些服务。Ribbon也可以实 现 我们自己的负载均衡算法。

微服务之负载均衡组件Ribbon_第2张图片

 1.3)自定义的负载均衡算法(随机)

可以通过DiscoveryClient组件来去我们的Nacos服务端 拉取给名称的微服务列表。我们可以通过这个特性来改写我们的RestTemplate 组件.

①:经过阅读源码RestTemplate组件得知,不管是post,get请求最终是会调 用我们的doExecute()方法,所以我们写一个MyRestTemplate类继承 RestTemplate,从写doExucute()方法。

@Slf4j
public class MyRestTemplate extends RestTemplate {

private DiscoveryClient discoveryClient;

public MyRestTemplate (DiscoveryClient discoveryClient) {
this.discoveryClient = discoveryClient;
}

 protected  T doExecute(URI url, @Nullable HttpMethod method, @Nullab
le RequestCallback requestCallback,
@Nullable ResponseExtractor responseExtractor) throws RestClientExce
ption {

Assert.notNull(url, "URI is required");
Assert.notNull(method, "HttpMethod is required");
ClientHttpResponse response = null;
try {
//判断url的拦截路径,然后去redis(作为注册中心)获取地址随机选取一个
log.info("请求的url路径为:{}",url);
url = replaceUrl(url);
log.info("替换后的路径:{}",url);
ClientHttpRequest request = createRequest(url, method);
if (requestCallback != null) {
requestCallback.doWithRequest(request);
}
response = request.execute();
handleResponse(url, method, response);
return (responseExtractor != null ? responseExtractor.extractData(respo
nse) : null);
}
catch (IOException ex) {
String resource = url.toString();
String query = url.getRawQuery();
resource = (query != null ? resource.substring(0,
resource.indexOf('?')) : resource);
 throw new ResourceAccessException("I/O error on " + method.name() +
 " request for \"" + resource + "\": " + ex.getMessage(), ex);
 } finally {
 if (response != null) {
 response.close();
 }
 }
 }


 /**
 * 把服务实例名称替换为ip:端口
 * @param url
 * @return
 */
 private URI replaceUrl(URI url){
 //解析我们的微服务的名称
 String sourceUrl = url.toString();
 String [] httpUrl = sourceUrl.split("//");
 int index = httpUrl[1].replaceFirst("/","@").indexOf("@");
 String serviceName = httpUrl[1].substring(0,index);

 //通过微服务的名称去nacos服务端获取 对应的实例列表
 List serviceInstanceList = discoveryClient.getInstance
s(serviceName);
 if(serviceInstanceList.isEmpty()) {
 throw new RuntimeException("没有可用的微服务实例列表:"+serviceName);
 }

 //采取随机的获取一个
 Random random = new Random();
 Integer randomIndex = random.nextInt(serviceInstanceList.size());
 log.info("随机下标:{}",randomIndex);
 String serviceIp = serviceInstanceList.get(randomIndex).getUri().toStri
ng();
log.info("随机选举的服务IP:{}",serviceIp);
String targetSource = httpUrl[1].replace(serviceName,serviceIp);
try {
return new URI(targetSource);
} catch (URISyntaxException e) {
e.printStackTrace();
}
return url;
}

}

1.4)通过Ribbon组件来实习负载均衡(默认的负载均衡算法是 轮询)

①:创建整合Ribbon的工程:

服务消费者(order) 服务提供者(product)****服务提供者不需 要Ribbon的依赖

第一步:加入依赖(加入nocas-client和ribbon的依赖)



com.alibaba.cloud
spring‐cloud‐alibaba‐nacos‐discovery




org.springframework.cloud
 spring‐cloud‐starter‐netflix‐ribbon
 

第二步:写注解: 在RestTemplate上加入@LoadBalanced注解

@Configuration
public class WebConfig {

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

第三步:写配置文件(这里是写Nacos 的配置文件,暂时没有配置Ribbon的配置)

spring:
    cloud:
        nacos:
            discovery:
                server‐addr: localhost:8848
application:
name: order‐center

启动技巧: 我们启动服务提供者的技巧(并行启动)

1)我们的product的端口是8081,我们用 8081启动一个服务后,然后修改端口为8082,然后修改下图,那么就可以同一 个工程启动二个实例

微服务之负载均衡组件Ribbon_第3张图片

1.5)Ribbon的内置的负载均衡算法

 微服务之负载均衡组件Ribbon_第4张图片

 ①:RandomRule(随机选择一个Server)

②:RetryRule 对选定的负载均衡策略(轮询)基础上重试机制,在一个配置时间段内当选择Server不成功, 则一直尝试使用subRule的方式选择一个可用的server.

③:RoundRobinRule 轮询选择, 轮询index,选择index对应位置的Server ④:AvailabilityFilteringRule 过滤掉一直连接失败的被标记为circuit tripped的后端Server,并过滤掉那些高并发的后端 Server或者使用一个AvailabilityPredicate来包含过滤server的逻辑,其实就就是检查 status里记录的各个Server的运行状态

⑤:BestAvailableRule 选择一个最小的并发请求的Server,逐个考察Server,如果Server被tripped了,则跳过。

⑥:WeightedResponseTimeRule 根据响应时间加权,响应时间越长,权重越小,被选中的可能性越低;

⑦:ZoneAvoidanceRule(默认是这个) 复合判断Server所在Zone的性能和Server的可用性选择Server,在没有Zone的情况下类是 轮询。

1.6)Ribbon的细粒度自定义配置

场景:我订单中心需要采用随机算法调用 库存中心 而采用轮询算法调用其他中心微服务。 基于java代码细粒度配置

**注意点**

我们针对调用具体微服务的具体配置类 ProductCenterRibbonConfig,OtherCenterRibbonConfig不能被放在我们主启动类所 在包以及子包下,不然就起不到细粒度配置

@Configuration
@RibbonClients(value = {
@RibbonClient(name = "product‐center",configuration = ProductCenterRibbonConfig.class),
@RibbonClient(name = "pay‐center",configuration =
PayCenterRibbonConfig.class)
})
public class CustomRibbonConfig {

}

 @Configuration
 public class ProductCenterRibbonConfig {

 @Bean
 public IRule randomRule() {
 return new RandomRule();
 }
 }

 @Configuration
 public class PayCenterRibbonConfig {

 public IRule roundRobinRule() {
 return new RoundRobinRule();
 }
 }

基于yml配置:(我们可以在order-center的yml中进行配置) 配置格式的语法如下

serviceName:

        ribbon:

                NFLoadBalancerRuleClassName: 负载均衡的对应class的全类名

配置案例: 我们的order-center调用我们的product-center

#自定义Ribbon的细粒度配置
product‐center:
	ribbon:
		NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
pay‐center:
	ribbon:
		NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RoundRobinRule

1.7)解决Ribbon 第一次调用耗时高的配置

开启饥饿加载

ribbon:
	eager‐load:
		enabled: true
		clients: product‐center #可以指定多个微服务用逗号分隔

常用参数讲解:

每一台服务器重试的次数,不包含首次调用的那一次
ribbon.MaxAutoRetries=1

# 重试的服务器的个数,不包含首次调用的那一台实例
ribbon.MaxAutoRetriesNextServer=2

# 是否对所有的操作进行重试(True 的话 会对post put操作进行重试,存在服务幂等问
题)
ribbon.OkToRetryOnAllOperations=true

 # 建立连接超时
 ribbon.ConnectTimeout=3000

 # 读取数据超时
 ribbon.ReadTimeout=3000

 举列子: 上面会进行几次重试
 MaxAutoRetries
 +
 MaxAutoRetriesNextServer
 +
 (MaxAutoRetries *MaxAutoRetriesNextServer)

微服务之负载均衡组件Ribbon_第5张图片

 Ribbon详细配置:http://c.biancheng.net/view/5356.html

1.8)Ribbon 自定义负载均衡策略

我们发现,nacos server上的页面发现 注册的微服务有一个权重的概 念。 取值为0-1之间

微服务之负载均衡组件Ribbon_第6张图片

 权重选择的概念: 假设我们一个微服务部署了三台服务器A,B,C 其中A,B,C三台服务的性能不一,A的性能最牛逼,B次之,C最差. 那么我们设置权重比例 为5 : 3:2 那就说明 10次请求到A上理论是5次,B 服务上理论是3次,B服务理论是2次.

①:但是Ribbon 所提供的负载均衡算法中没有基于权重的负载均衡算 法。我们自己实现一个.

@Slf4j
public class TulingWeightedRule extends AbstractLoadBalancerRule {

@Autowired
private NacosDiscoveryProperties discoveryProperties;

@Override
public void initWithNiwsConfig(IClientConfig clientConfig) {
//读取配置文件并且初始化,ribbon内部的 几乎用不上
 }


 @Override
 public Server choose(Object key) {
 try {
 log.info("key:{}",key);
 BaseLoadBalancer baseLoadBalancer = (BaseLoadBalancer) this.getLoadBala
ncer();
log.info("baseLoadBalancer‐‐‐>:{}",baseLoadBalancer);

//获取微服务的名称
String serviceName = baseLoadBalancer.getName();

//获取Nocas服务发现的相关组件API
NamingService namingService =
discoveryProperties.namingServiceInstance();

//获取 一个基于nacos client 实现权重的负载均衡算法
Instance instance =
namingService.selectOneHealthyInstance(serviceName);
//返回一个server
return new NacosServer(instance);
} catch (NacosException e) {
log.error("自定义负载均衡算法错误");
}
return null;
}
}

进阶版本1:我们发现Nacos领域模型中有一个集群的概念 同集群优先权重负载均衡算法

微服务之负载均衡组件Ribbon_第7张图片

业务场景:现在我们有二个微服务order-center, product-center二个微服 务。我们在南京机房部署一套order-center,product-center。为了容灾处 理,我们在北京同样部署一套order-center,product-center

微服务之负载均衡组件Ribbon_第8张图片 

@Slf4j
public class TheSameClusterPriorityRule extends AbstractLoadBalancerRule
{

@Autowired
private NacosDiscoveryProperties discoveryProperties;

@Override
public void initWithNiwsConfig(IClientConfig clientConfig) {

 }

 @Override
 public Server choose(Object key) {
 try {
 //第一步:获取当前服务所在的集群
 String currentClusterName = discoveryProperties.getClusterName();

 //第二步:获取一个负载均衡对象
 BaseLoadBalancer baseLoadBalancer = (BaseLoadBalancer)
getLoadBalancer();

//第三步:获取当前调用的微服务的名称
String invokedSerivceName = baseLoadBalancer.getName();

//第四步:获取nacos clinet的服务注册发现组件的api
NamingService namingService =
discoveryProperties.namingServiceInstance();

//第五步:获取所有的服务实例
List allInstance = namingService.getAllInstances(invokedSeriv
ceName);

List theSameClusterNameInstList = new ArrayList<>();

//第六步:过滤筛选同集群下的所有实例
for(Instance instance : allInstance) {
if(StringUtils.endsWithIgnoreCase(instance.getClusterName(),currentClus
terName)) {
theSameClusterNameInstList.add(instance);
}
}

Instance toBeChooseInstance ;

//第七步:选择合适的一个实例调用
if(theSameClusterNameInstList.isEmpty()) {

toBeChooseInstance = TulingWeightedBalancer.chooseInstanceByRandomWeigh
t(allInstance);

log.info("发生跨集群调用‐‐‐>当前微服务所在集群:{},被调用微服务所在集群:{},Ho
st:{},Port:{}",
currentClusterName,toBeChooseInstance.getClusterName(),toBeChooseInstan
ce.getIp(),toBeChooseInstance.getPort());
}else {
toBeChooseInstance = TulingWeightedBalancer.chooseInstanceByRandomWeigh
t(theSameClusterNameInstList);

log.info("同集群调用‐‐‐>当前微服务所在集群:{},被调用微服务所在集群:{},Host:
{},Port:{}",
currentClusterName,toBeChooseInstance.getClusterName(),toBeChooseInstan
ce.getIp(),toBeChooseInstance.getPort());
 }

 return new NacosServer(toBeChooseInstance);

 } catch (NacosException e) {
 log.error("同集群优先权重负载均衡算法选择异常:{}",e);
 }
 return null;
 }
 }

 /**
 * 根据权重选择随机选择一个
 * Created by smlz on 2019/11/21.
*/
public class TulingWeightedBalancer extends Balancer {

public static Instance chooseInstanceByRandomWeight(List host
s) {
return getHostByRandomWeight(hosts);
}
}

 配置文件

spring
	cloud:
		nacos:
		discovery:
			server-addr: localhost:8848
			#配置集群名称
			cluster-name: NJ-CLUSTER
			#namespace: bc7613d2-2e22-4292-a748-48b78170f14c #指定namespace的id
application:
 name: order-center

进阶版本2: 根据进阶版本1,我们现在需要解决生产环境金丝雀发布问题 比如 order-center 存在二个版本 V1(老版本) V2(新版 本),product-center也存在二个版本V1(老版本) V2新版本 现在需要 做到的是

order-center(V1)---->product-center(v1),

order-center(V2)--- ->product-center(v2)。

记住v2版本是小面积部署的,用来测试用 户对新版本功能的。若用户完全接受了v2。我们就可以把V1版本卸载 完全部署V2版本。

微服务之负载均衡组件Ribbon_第9张图片

 代码实现思路:通过我们实例的元数据控制

/**
* 同一个集群,同已版本号 优先调用策略
* Created by smlz on 2019/11/21.
*/
@Slf4j
public class TheSameClusterPriorityWithVersionRule extends AbstractLoadBa
lancerRule {

@Autowired
private NacosDiscoveryProperties discoveryProperties;

 @Override
 public void initWithNiwsConfig(IClientConfig clientConfig) {

 }

 @Override
 public Server choose(Object key) {

 try {

 String currentClusterName = discoveryProperties.getClusterName();

 List theSameClusterNameAndTheSameVersionInstList = getTheSame
ClusterAndTheSameVersionInstances(discoveryProperties);

//声明被调用的实例
Instance toBeChooseInstance;

//判断同集群同版本号的微服务实例是否为空
if(theSameClusterNameAndTheSameVersionInstList.isEmpty()) {
//跨集群调用相同的版本
toBeChooseInstance = crossClusterAndTheSameVersionInovke(discoveryPrope
rties);
}else {
toBeChooseInstance = TulingWeightedBalancer.chooseInstanceByRandomWeigh
t(theSameClusterNameAndTheSameVersionInstList);
log.info("同集群同版本调用‐‐‐>当前微服务所在集群:{},被调用微服务所在集群:{},
当前微服务的版本:{},被调用微服务版本:{},Host:{},Port:{}",
currentClusterName,toBeChooseInstance.getClusterName(),discoveryPropert
ies.getMetadata().get("current‐version"),
 toBeChooseInstance.getMetadata().get("current‐version"),toBeChooseInsta
nce.getIp(),toBeChooseInstance.getPort());
}

return new NacosServer(toBeChooseInstance);

} catch (NacosException e) {
log.error("同集群优先权重负载均衡算法选择异常:{}",e);
return null;
}
}



/**
* 方法实现说明:获取相同集群下,相同版本的 所有实例
* @author:smlz
* @param discoveryProperties nacos的配置
* @return: List
* @exception: NacosException
* @date:2019/11/21 16:41
*/
private List getTheSameClusterAndTheSameVersionInstances(Naco
sDiscoveryProperties discoveryProperties) throws NacosException {

//当前的集群的名称
String currentClusterName = discoveryProperties.getClusterName();

String currentVersion = discoveryProperties.getMetadata().get("current‐
version");

//获取所有实例的信息(包括不同集群的)
List allInstance = getAllInstances(discoveryProperties);

List theSameClusterNameAndTheSameVersionInstList = new ArrayL
ist<>();

//过滤相同集群的
for(Instance instance : allInstance) {
if(StringUtils.endsWithIgnoreCase(instance.getClusterName(),currentClus
terName)&&
StringUtils.endsWithIgnoreCase(instance.getMetadata().get("current‐vers
ion"),currentVersion)) {

theSameClusterNameAndTheSameVersionInstList.add(instance);
}
}

return theSameClusterNameAndTheSameVersionInstList;
}

/**
* 方法实现说明:获取被调用服务的所有实例
* @author:smlz
* @param discoveryProperties nacos的配置
* @return: List
* @exception: NacosException
* @date:2019/11/21 16:42
*/
private List getAllInstances(NacosDiscoveryProperties discove
ryProperties) throws NacosException {

//第1步:获取一个负载均衡对象
BaseLoadBalancer baseLoadBalancer = (BaseLoadBalancer)
getLoadBalancer();

//第2步:获取当前调用的微服务的名称
String invokedSerivceName = baseLoadBalancer.getName();

//第3步:获取nacos clinet的服务注册发现组件的api
NamingService namingService =
discoveryProperties.namingServiceInstance();

//第4步:获取所有的服务实例
List allInstance = namingService.getAllInstances(invokedSeriv
ceName);

return allInstance;
}

/**
* 方法实现说明:跨集群环境下 相同版本的
* @author:smlz
* @param discoveryProperties
* @return: List
* @exception: NacosException
* @date:2019/11/21 17:11
*/
private List getCrossClusterAndTheSameVersionInstList(NacosDi
scoveryProperties discoveryProperties) throws NacosException {

//版本号
String currentVersion = discoveryProperties.getMetadata().get("current‐
version");

//被调用的所有实例
List allInstance = getAllInstances(discoveryProperties);

List crossClusterAndTheSameVersionInstList = new ArrayList<>
();

//过滤相同版本
for(Instance instance : allInstance) {
if(StringUtils.endsWithIgnoreCase(instance.getMetadata().get("current‐v
ersion"),currentVersion)) {

 crossClusterAndTheSameVersionInstList.add(instance);
 }
 }

 return crossClusterAndTheSameVersionInstList;
 }

 private Instance crossClusterAndTheSameVersionInovke(NacosDiscoveryProp
erties discoveryProperties) throws NacosException {

//获取所有集群下相同版本的实例信息
List crossClusterAndTheSameVersionInstList = getCrossClusterA
ndTheSameVersionInstList(discoveryProperties);
//当前微服务的版本号
String currentVersion = discoveryProperties.getMetadata().get("current‐
version");
//当前微服务的集群名称
String currentClusterName = discoveryProperties.getClusterName();

//声明被调用的实例
Instance toBeChooseInstance = null ;

//没有对应相同版本的实例
if(crossClusterAndTheSameVersionInstList.isEmpty()) {
log.info("跨集群调用找不到对应合适的版本当前版本为:currentVersion:{}",curr
entVersion);
throw new RuntimeException("找不到相同版本的微服务实例");
}else {
toBeChooseInstance = TulingWeightedBalancer.chooseInstanceByRandomWeigh
t(crossClusterAndTheSameVersionInstList);

log.info("跨集群同版本调用‐‐‐>当前微服务所在集群:{},被调用微服务所在集群:
{},当前微服务的版本:{},被调用微服务版本:{},Host:{},Port:{}",
currentClusterName,toBeChooseInstance.getClusterName(),discoveryPropert
ies.getMetadata().get("current‐version"),
toBeChooseInstance.getMetadata().get("current‐version"),toBeChooseInsta
nce.getIp(),toBeChooseInstance.getPort());
}

return toBeChooseInstance;
}
}

配置文件说明

order-center的yml的配置

spring:
	cloud:
		nacos:
			discovery:
				server-addr: localhost:8848
				#所在集群
				cluster-name: NJ-CLUSTER
				metadata:
					#版本号
					 current-version: V1
				 #namespace: bc7613d2-2e22-4292-a748-48b78170f14c #指定namespace的id
 application:
	name: order-center

微服务之负载均衡组件Ribbon_第10张图片

product-center的yml配置说明:

NJ-CLUSTER下的V1版本 

spring:
application:
name: product‐center
	cloud:
		nacos:
		discovery:
			server‐addr: localhost:8848
			cluster‐name: NJ‐CLUSTER
			metadata:
				current‐version: V1
			 #namespace: 20989a73‐cdb3‐41b8‐85c0‐e9a3530e28a6
 server:
 port: 8084

NJ-CLUSTER下的V2版本

spring:
	application:
		name: product‐center
	cloud:
	nacos:
		discovery:
		server‐addr: localhost:8848
		cluster‐name: NJ‐CLUSTER
		metadata:
		 current‐version: V2
		 #namespace: 20989a73‐cdb3‐41b8‐85c0‐e9a3530e28a6
server:
  port: 8083

微服务之负载均衡组件Ribbon_第11张图片

BJ-CLUSTER下的V1版本

spring:
	application:
		name: product‐center
	cloud:
		nacos:
		discovery:
		server‐addr: localhost:8848
		cluster‐name: BJ‐CLUSTER
		metadata:
		 current‐version: V1
		 #namespace: 20989a73‐cdb3‐41b8‐85c0‐e9a3530e28a6
 server:
 port: 8082

 BJ-CLUSTER下的V2版本

spring:
	application:
		name: product‐center
	cloud:
		nacos:
		discovery:
		server‐addr: localhost:8848
		cluster‐name: BJ‐CLUSTER
		metadata:
		 current‐version: V2
		 #namespace: 20989a73‐cdb3‐41b8‐85c0‐e9a3530e28a6
 server:
 port: 8081

微服务之负载均衡组件Ribbon_第12张图片

测试说明: 从我们的order-center调用product-center的时候 优先会调用同集群同版本 的 2022-2-09 22:42:42.317 INFO 45980 --- [nio-8080-exec-3] .m.TheSameClusterPriorityWithVersionRule : 同集群同版本调用--->当前微服务所在 集群:NJ-CLUSTER,被调用微服务所在集群:NJ-CLUSTER,当前微服务的版本:V1,被调用微 服务版本:V1,Host:192.168.0.120,Port:8081

若我们把同集群的 同版本的product-center下线,那们就会发生跨集群调用相同的版本:

微服务之负载均衡组件Ribbon_第13张图片 

2022-2-09 22:44:48.723 INFO 45980 --- [nio-8080-exec-6] .m.TheSameClusterPriorityWithVersionRule : 跨集群同版本调用--->当前微服务所在 集群:NJ-CLUSTER,被调用微服务所在集群:BJ-CLUSTER,当前微服务的版本:V1,被调用微 服务版本:V1,Host:192.168.0.120,Port:8083 

ItVuer - 免责声明 - 关于我们 - 联系我们

本网站信息来源于互联网,如有侵权请联系:561261067@qq.com

桂ICP备16001015号