发布时间:2023-05-25 10:30
①:我们用户服务发送请求首先打到Ng上,然后Ng根据负载均衡算法进行选择一个服务调 用,而我们的Ng部署在服务器上的,所以Ng又称为服务端的负载均衡(具体调用哪个服务, 由Ng所了算)
spring cloud ribbon是 基于NetFilix ribbon 实现的一套客户端的负载 均衡工具,Ribbon客户端组件提供一系列的完善的配置,如超时,重试 等。
通过Load Balancer(LB)获取到服务提供的所有机器实例,Ribbon 会自动基于某种规则(轮询,随机)去调用这些服务。Ribbon也可以实 现 我们自己的负载均衡算法。
可以通过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;
}
}
①:创建整合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,然后修改下图,那么就可以同一 个工程启动二个实例
①: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的情况下类是 轮询。
场景:我订单中心需要采用随机算法调用 库存中心 而采用轮询算法调用其他中心微服务。 基于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
开启饥饿加载
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详细配置:http://c.biancheng.net/view/5356.html
我们发现,nacos server上的页面发现 注册的微服务有一个权重的概 念。 取值为0-1之间
权重选择的概念: 假设我们一个微服务部署了三台服务器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领域模型中有一个集群的概念 同集群优先权重负载均衡算法
业务场景:现在我们有二个微服务order-center, product-center二个微服 务。我们在南京机房部署一套order-center,product-center。为了容灾处 理,我们在北京同样部署一套order-center,product-center
@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版本。
代码实现思路:通过我们实例的元数据控制
/**
* 同一个集群,同已版本号 优先调用策略
* 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
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
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
测试说明: 从我们的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下线,那们就会发生跨集群调用相同的版本:
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