一:软件定制开发供应商负载均衡的二种实现
1.1)软件定制开发供应商服务端的负载均衡(Nginx)
①:软件定制开发供应商我们用户服务发送请求软件定制开发供应商首先打到上,然后Ng软件定制开发供应商根据负载均衡算法进行软件定制开发供应商选择一个服务调 用,而我们的Ng软件定制开发供应商部署在服务器上的,所以Ng软件定制开发供应商又称为服务端的负载均衡(软件定制开发供应商具体调用哪个服务, 由Ng所了算)
1.2)软件定制开发供应商客户端负载均衡(
spring cloud ribbon是 基于NetFilix ribbon 软件定制开发供应商实现的一套客户端的负载 均衡工具,Ribbon软件定制开发供应商客户端组件提供一系列软件定制开发供应商的完善的配置,如超时,重试 等。
通过Load Balancer(LB)获取到服务提供的所有机器实例,Ribbon 会自动基于某种规则(轮询,随机)去调用这些服务。Ribbon也可以实 现 我们自己的。
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> T doExecute(URI url, @Nullable HttpMethod method, @Nullab
- le RequestCallback requestCallback,
- @Nullable ResponseExtractor<T> 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<ServiceInstance> 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的依赖)
- <!‐‐加入nocas‐client‐‐>
- <dependency>
- <groupId>com.alibaba.cloud</groupId>
- <artifactId>spring‐cloud‐alibaba‐nacos‐discovery</artifactId>
- </dependency>
-
- <!‐‐加入ribbon‐‐>
- <dependency>
- <groupId>org.springframework.cloud</groupId>
- <artifactId>spring‐cloud‐starter‐netflix‐ribbon</artifactId>
- </dependency>
第二步:写注解: 在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,然后修改下图,那么就可以同一 个工程启动二个实例
1.5)Ribbon的内置的负载均衡算法
①: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详细配置:
1.8)Ribbon 自定义负载均衡策略
我们发现,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<Instance> allInstance = namingService.getAllInstances(invokedSeriv
- ceName);
-
- List<Instance> 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<Instance> 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<Instance> 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<Instance>
- * @exception: NacosException
- * @date:2019/11/21 16:41
- */
- private List<Instance> getTheSameClusterAndTheSameVersionInstances(Naco
- sDiscoveryProperties discoveryProperties) throws NacosException {
-
- //当前的集群的名称
- String currentClusterName = discoveryProperties.getClusterName();
-
- String currentVersion = discoveryProperties.getMetadata().get("current‐
- version");
-
- //获取所有实例的信息(包括不同集群的)
- List<Instance> allInstance = getAllInstances(discoveryProperties);
-
- List<Instance> 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<Instance>
- * @exception: NacosException
- * @date:2019/11/21 16:42
- */
- private List<Instance> 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<Instance> allInstance = namingService.getAllInstances(invokedSeriv
- ceName);
-
- return allInstance;
- }
-
- /**
- * 方法实现说明:跨集群环境下 相同版本的
- * @author:smlz
- * @param discoveryProperties
- * @return: List<Instance>
- * @exception: NacosException
- * @date:2019/11/21 17:11
- */
- private List<Instance> getCrossClusterAndTheSameVersionInstList(NacosDi
- scoveryProperties discoveryProperties) throws NacosException {
-
- //版本号
- String currentVersion = discoveryProperties.getMetadata().get("current‐
- version");
-
- //被调用的所有实例
- List<Instance> allInstance = getAllInstances(discoveryProperties);
-
- List<Instance> 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<Instance> 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