负载均衡分类
服务端负载均衡
常见的服务端负载均衡有Nginx,客户端将请求发送给Nginx,Nginx根据负载均衡算法选择一个服务器调用,具体调用哪个服务器由Nginx说了算,客户端是不知道服务器的真实IP的。
客户端负载均衡
Spring Cloud Ribbon是基于NetFilix Ribbon实现的一套客户端负载均衡,Ribbon客户端组件提供了一系列的完善的配置,例如超时,重试等。客户端从注册中心获取到服务器的列表,由客户端自己根据负载均衡算法选择将流量分发给哪个服务器,客户端是知道服务器的真实IP的。
重写RestTemplate的doExecute()方法实现负载均衡
通过阅读RestTemplate源码得知,不管是POST,GET请求,最终都会调用doExecute()方法,因此我们可以通过继承RestTemplate类并重写doExecute()方法来实现负载均衡算法。
package com.tuling.config; import lombok.extern.slf4j.Slf4j; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.http.HttpMethod; import org.springframework.http.client.ClientHttpRequest; import org.springframework.http.client.ClientHttpResponse; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.web.client.*; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.util.List; import java.util.Random; /** * 根据RestTemplate特性自己改造 */ @Slf4j public class TulingRestTemplate extends RestTemplate { private DiscoveryClient discoveryClient; public TulingRestTemplate (DiscoveryClient discoveryClient) { this.discoveryClient = discoveryClient; } protected <T> T doExecute(URI url, @Nullable HttpMethod method, @Nullable RequestCallback requestCallback, @Nullable ResponseExtractor<T> responseExtractor) throws RestClientException { Assert.notNull(url, "URI is required"); Assert.notNull(method, "HttpMethod is required"); ClientHttpResponse response = null; try { log.info("请求的url路径为:{}",url); //把服务名 替换成我们的IP 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(response) : 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进行替换 * http://product-center/selectProductInfoById/1 * @param url:请求的url * @return: 替换后的url * @exception: * @date:2020/2/6 13:11 */ private URI replaceUrl(URI url){ //1:从URI中解析调用的调用的serviceName=product-center String serviceName = url.getHost(); log.info("调用微服务的名称:{}",serviceName); //2:解析我们的请求路径 reqPath= /selectProductInfoById/1 String reqPath = url.getPath(); log.info("请求path:{}",reqPath); //通过微服务的名称去nacos服务端获取 对应的实例列表 List<ServiceInstance> serviceInstanceList = discoveryClient.getInstances(serviceName); if(serviceInstanceList.isEmpty()) { throw new RuntimeException("没有可用的微服务实例列表:"+serviceName); } String serviceIp = chooseTargetIp(serviceInstanceList); String source = serviceIp+reqPath; try { return new URI(source); } catch (URISyntaxException e) { log.error("根据source:{}构建URI异常",source); } return url; } /** * 方法实现说明:从服务列表中 随机选举一个ip * @param serviceInstanceList 服务列表 * @return: 调用的ip * @exception: * @date:2020/2/6 13:15 */ private String chooseTargetIp(List<ServiceInstance> serviceInstanceList) { //采取随机的获取一个 Random random = new Random(); Integer randomIndex = random.nextInt(serviceInstanceList.size()); String serviceIp = serviceInstanceList.get(randomIndex).getUri().toString(); log.info("随机选举的服务IP:{}",serviceIp); return serviceIp; } }
order服务的Controller:
package com.tuling.controller; import com.tuling.entity.OrderInfo; import com.tuling.entity.ProductInfo; import com.tuling.mapper.OrderInfoMapper; import com.tuling.vo.OrderVo; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestTemplate; /** * 自定义负载均衡的服务消费者 */ @RestController public class OrderInfoController { @Autowired private RestTemplate restTemplate; @Autowired private OrderInfoMapper orderInfoMapper; @RequestMapping("/selectOrderInfoById/{orderNo}") public Object selectOrderInfoById(@PathVariable("orderNo") String orderNo) { OrderInfo orderInfo = orderInfoMapper.selectOrderInfoById(orderNo); if(null == orderInfo) { return "根据orderNo:"+orderNo+"查询没有该订单"; } ResponseEntity<ProductInfo> responseEntity = restTemplate.getForEntity("http://product-center/selectProductInfoById/"+orderInfo.getProductNo(), ProductInfo.class); ProductInfo productInfo = responseEntity.getBody(); if(productInfo == null) { return "没有对应的商品"; } OrderVo orderVo = new OrderVo(); orderVo.setOrderNo(orderInfo.getOrderNo()); orderVo.setUserName(orderInfo.getUserName()); orderVo.setProductName(productInfo.getProductName()); orderVo.setProductNum(orderInfo.getProductCount()); return orderVo; } }
在后端并行启动两个product实例:我们用 8081启动一个服务后,然后修改端口为8082,如下图所示勾选Allow parallel run,就可以同一个工程启动两个实例。
spring: datasource: druid: username: root password: 123456 jdbcUrl: jdbc:mysql://192.168.1.14:3306/tuling-ms-alibaba?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=UTF-8&useSSL=false driverClassName: com.mysql.jdbc.Driver initialSize: 5 minIdle: 5 maxActive: 20 maxWait: 60000 timeBetweenEvictionRunsMillis: 60000 minEvictableIdleTimeMillis: 300000 validationQuery: SELECT 1 FROM DUAL testWhileIdle: true testOnBorrow: false testOnReturn: false poolPreparedStatements: true filters: stat,wall #\u914D\u7F6E\u8FC7\u6EE4\u5668 maxPoolPreparedStatementPerConnectionSize: 20 useGlobalDataSourceStat: true connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500 application: name: product-center cloud: nacos: discovery: server-addr: 192.168.1.5:8848 server: port: 8081 #port: 8082
查看日志可以看到我们重写的负载均衡方法先从注册中心获取到product服务的列表,并且随机选择一个IP替换url。
2021-03-02 22:26:14.098 INFO 8449 --- [nio-8080-exec-9] com.tuling.config.TulingRestTemplate : 请求的url路径为:http://product-center/selectProductInfoById/1 2021-03-02 22:26:14.098 INFO 8449 --- [nio-8080-exec-9] com.tuling.config.TulingRestTemplate : 调用微服务的名称:product-center 2021-03-02 22:26:14.098 INFO 8449 --- [nio-8080-exec-9] com.tuling.config.TulingRestTemplate : 请求path:/selectProductInfoById/1 2021-03-02 22:26:14.098 INFO 8449 --- [nio-8080-exec-9] com.tuling.config.TulingRestTemplate : 随机选举的服务IP:http://192.168.1.136:8081 2021-03-02 22:26:14.098 INFO 8449 --- [nio-8080-exec-9] com.tuling.config.TulingRestTemplate : 替换后的路径:http://192.168.1.136:8081/selectProductInfoById/1 2021-03-02 22:26:18.477 INFO 8449 --- [io-8080-exec-10] com.tuling.config.TulingRestTemplate : 请求的url路径为:http://product-center/selectProductInfoById/1 2021-03-02 22:26:18.477 INFO 8449 --- [io-8080-exec-10] com.tuling.config.TulingRestTemplate : 调用微服务的名称:product-center 2021-03-02 22:26:18.477 INFO 8449 --- [io-8080-exec-10] com.tuling.config.TulingRestTemplate : 请求path:/selectProductInfoById/1 2021-03-02 22:26:18.478 INFO 8449 --- [io-8080-exec-10] com.tuling.config.TulingRestTemplate : 随机选举的服务IP:http://192.168.1.136:8082 2021-03-02 22:26:18.478 INFO 8449 --- [io-8080-exec-10] com.tuling.config.TulingRestTemplate : 替换后的路径:http://192.168.1.136:8082/selectProductInfoById/1
Ribbon组件使用
第一步:加入依赖(nacos-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注册
package com.tuling.config; import org.springframework.cloud.client.loadbalancer.LoadBalanced; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.client.RestTemplate; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class WebConfig implements WebMvcConfigurer { @LoadBalanced @Bean public RestTemplate restTemplate( ) { return new RestTemplate(); } }
第三步:写配置文件(现在暂时用默认的ribbon负载均衡方法,配置文件只写nacos的就行)
spring: datasource: druid: username: root password: 123456 jdbcUrl: jdbc:mysql://192.168.1.14:3306/tuling-ms-alibaba?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=UTF-8&useSSL=false driverClassName: com.mysql.jdbc.Driver initialSize: 5 minIdle: 5 maxActive: 20 maxWait: 60000 timeBetweenEvictionRunsMillis: 60000 minEvictableIdleTimeMillis: 300000 validationQuery: SELECT 1 FROM DUAL testWhileIdle: true testOnBorrow: false testOnReturn: false poolPreparedStatements: true filters: stat,wall #\u914D\u7F6E\u8FC7\u6EE4\u5668 maxPoolPreparedStatementPerConnectionSize: 20 useGlobalDataSourceStat: true connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500 cloud: nacos: discovery: server-addr: 192.168.1.5:8848 application: name: order-center server: port: 8080
第四步:写Controller
package com.tuling.controller; import com.tuling.entity.OrderInfo; import com.tuling.entity.ProductInfo; import com.tuling.mapper.OrderInfoMapper; import com.tuling.vo.OrderVo; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestTemplate; /** * 基于ribbon负载均衡 */ @RestController public class OrderInfoController { @Autowired private RestTemplate restTemplate; @Autowired private OrderInfoMapper orderInfoMapper; @RequestMapping("/selectOrderInfoById/{orderNo}") public Object selectOrderInfoById(@PathVariable("orderNo") String orderNo) { OrderInfo orderInfo = orderInfoMapper.selectOrderInfoById(orderNo); if(null == orderInfo) { return "根据orderNo:"+orderNo+"查询没有该订单"; } ResponseEntity<ProductInfo> responseEntity = null; try { responseEntity = restTemplate.getForEntity("http://product-center/selectProductInfoById/"+orderInfo.getProductNo(), ProductInfo.class); }catch (Exception e) { System.out.println(e.getStackTrace()); } ProductInfo productInfo = responseEntity.getBody(); if(productInfo == null) { return "没有对应的商品"; } OrderVo orderVo = new OrderVo(); orderVo.setOrderNo(orderInfo.getOrderNo()); orderVo.setUserName(orderInfo.getUserName()); orderVo.setProductName(productInfo.getProductName()); orderVo.setProductNum(orderInfo.getProductCount()); return orderVo; } }
和前面一样并行起2个product服务:
Ribbon细粒度自定义配置
场景:order服务需要采用随机算法调用product服务,使用轮询算法调用pay服务,其他服务使用随机算法调用。
在order服务的配置文件中指定即可:
spring: datasource: druid: username: root password: 123456 jdbcUrl: jdbc:mysql://192.168.1.14:3306/tuling-ms-alibaba?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=UTF-8&useSSL=false driverClassName: com.mysql.jdbc.Driver initialSize: 5 minIdle: 5 maxActive: 20 maxWait: 60000 timeBetweenEvictionRunsMillis: 60000 minEvictableIdleTimeMillis: 300000 validationQuery: SELECT 1 FROM DUAL testWhileIdle: true testOnBorrow: false testOnReturn: false poolPreparedStatements: true filters: stat,wall #\u914D\u7F6E\u8FC7\u6EE4\u5668 maxPoolPreparedStatementPerConnectionSize: 20 useGlobalDataSourceStat: true connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500 cloud: nacos: discovery: server-addr: 192.168.1.5:8848 #自定义负载均衡时使用 cluster-name: NJ-CLUSTER metadata: current-version: V1 application: name: order-center #开启ribbon饥饿加载,解决微服务调用第一次很慢的情况下 ribbon: NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule eager-load: enabled: true #可以指定多个微服务用逗号分隔 clients: product-center,pay-center # ##自定义Ribbon的细粒度配置 product-center: ribbon: NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule pay-center: ribbon: NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RoundRobinRule server: port: 8080
自定义负载均衡策略
order服务有V1一个版本,在NJ-Cluster(1个实例);product服务有V1,V2两个版本分别在BJ-Cluster和NJ-Cluster(4个实例)。
要求order服务按照以下优先级调用product服务,跨版本不允许调用:
- 同集群同版本。
- 同版本不同集群。
package com.ribbonconfig; import com.netflix.loadbalancer.IRule; import com.tuling.myrule.TheSameClusterPriorityWithVersionRule; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class GlobalRibbonConfig { @Bean public IRule theSameClusterPriorityRule() { return new TheSameClusterPriorityWithVersionRule(); //自定义负载均衡策略 } }
编写自定义负载均衡策略TheSameClusterPriorityWithVersionRule,继承AbstractLoadBalancerRule并重写choose()方法:
package com.tuling.myrule; import com.alibaba.cloud.nacos.NacosDiscoveryProperties; import com.alibaba.cloud.nacos.ribbon.NacosServer; import com.alibaba.nacos.api.exception.NacosException; import com.alibaba.nacos.api.naming.NamingService; import com.alibaba.nacos.api.naming.pojo.Instance; import com.netflix.client.config.IClientConfig; import com.netflix.loadbalancer.AbstractLoadBalancerRule; import com.netflix.loadbalancer.BaseLoadBalancer; import com.netflix.loadbalancer.Server; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.util.StringUtils; import java.util.ArrayList; import java.util.List; /** * 自定义负载均衡,同集群同版本优先调用,然后是跨集群同版本 */ @Slf4j public class TheSameClusterPriorityWithVersionRule extends AbstractLoadBalancerRule { @Autowired private NacosDiscoveryProperties discoveryProperties; @Override public void initWithNiwsConfig(IClientConfig clientConfig) { } /** * 重写choose方法,自定义负载均衡策略 * @param key * @return */ @Override public Server choose(Object key) { try { //获取当前的集群名称 String currentClusterName = discoveryProperties.getClusterName(); //获取和当前集群 相同集群下,相同版本的 所有实例 List<Instance> theSameClusterNameAndTheSameVersionInstList = getTheSameClusterAndTheSameVersionInstances(discoveryProperties); //声明被调用的实例 Instance toBeChooseInstance; //判断同集群同版本号的微服务实例是否为空 if(theSameClusterNameAndTheSameVersionInstList.isEmpty()) { //如果没有同集群同版本的服务,跨集群调用相同的版本 toBeChooseInstance = crossClusterAndTheSameVersionInovke(discoveryProperties); }else { toBeChooseInstance = TulingWeightedBalancer.chooseInstanceByRandomWeight(theSameClusterNameAndTheSameVersionInstList); log.info("同集群同版本调用--->当前微服务所在集群:{},被调用微服务所在集群:{},当前微服务的版本:{},被调用微服务版本:{},Host:{},Port:{}", currentClusterName,toBeChooseInstance.getClusterName(),discoveryProperties.getMetadata().get("current-version"), toBeChooseInstance.getMetadata().get("current-version"),toBeChooseInstance.getIp(),toBeChooseInstance.getPort()); } return new NacosServer(toBeChooseInstance); } catch (NacosException e) { log.error("同集群优先权重负载均衡算法选择异常:{}",e); return null; } } /** * 方法实现说明:获取相同集群下,相同版本的 所有实例 * @param discoveryProperties nacos的配置 * @return: List<Instance> * @exception: NacosException */ private List<Instance> getTheSameClusterAndTheSameVersionInstances(NacosDiscoveryProperties discoveryProperties) throws NacosException { //当前的集群的名称 String currentClusterName = discoveryProperties.getClusterName(); String currentVersion = discoveryProperties.getMetadata().get("current-version"); //获取所有实例的信息(包括不同集群的) List<Instance> allInstance = getAllInstances(discoveryProperties); List<Instance> theSameClusterNameAndTheSameVersionInstList = new ArrayList<>(); //过滤相同集群的 for(Instance instance : allInstance) { if(StringUtils.endsWithIgnoreCase(instance.getClusterName(),currentClusterName)&& StringUtils.endsWithIgnoreCase(instance.getMetadata().get("current-version"),currentVersion)) { theSameClusterNameAndTheSameVersionInstList.add(instance); } } return theSameClusterNameAndTheSameVersionInstList; } /** * 方法实现说明:获取被调用服务的所有实例 * @param discoveryProperties nacos的配置 * @return: List<Instance> * @exception: NacosException */ private List<Instance> getAllInstances(NacosDiscoveryProperties discoveryProperties) 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(invokedSerivceName); return allInstance; } /** * 方法实现说明:跨集群环境下 相同版本的 * @param discoveryProperties * @return: List<Instance> * @exception: NacosException */ private List<Instance> getCrossClusterAndTheSameVersionInstList(NacosDiscoveryProperties 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-version"),currentVersion)) { crossClusterAndTheSameVersionInstList.add(instance); } } return crossClusterAndTheSameVersionInstList; } private Instance crossClusterAndTheSameVersionInovke(NacosDiscoveryProperties discoveryProperties) throws NacosException { //获取所有集群下相同版本的实例信息 List<Instance> crossClusterAndTheSameVersionInstList = getCrossClusterAndTheSameVersionInstList(discoveryProperties); //当前微服务的版本号 String currentVersion = discoveryProperties.getMetadata().get("current-version"); //当前微服务的集群名称 String currentClusterName = discoveryProperties.getClusterName(); //声明被调用的实例 Instance toBeChooseInstance = null ; //没有对应相同版本的实例 if(crossClusterAndTheSameVersionInstList.isEmpty()) { log.info("跨集群调用找不到对应合适的版本当前版本为:currentVersion:{}",currentVersion); throw new RuntimeException("找不到相同版本的微服务实例"); }else { toBeChooseInstance = TulingWeightedBalancer.chooseInstanceByRandomWeight(crossClusterAndTheSameVersionInstList); log.info("跨集群同版本调用--->当前微服务所在集群:{},被调用微服务所在集群:{},当前微服务的版本:{},被调用微服务版本:{},Host:{},Port:{}", currentClusterName,toBeChooseInstance.getClusterName(),discoveryProperties.getMetadata().get("current-version"), toBeChooseInstance.getMetadata().get("current-version"),toBeChooseInstance.getIp(),toBeChooseInstance.getPort()); } return toBeChooseInstance; } }
RestTemplate添加@LoadBalanced注解:
package com.tuling.config; import org.springframework.cloud.client.loadbalancer.LoadBalanced; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.client.RestTemplate; @Configuration public class WebConfig { @LoadBalanced @Bean public RestTemplate restTemplate( ) { return new RestTemplate(); } }
order服务Controller方法和之前一样。
连续在浏览器请求order服务,在所有product服务都好的情况下,会优先调用同集群同版本的:
2021-03-04 23:42:20.492 INFO 9478 --- [nio-8080-exec-2] .m.TheSameClusterPriorityWithVersionRule : 同集群同版本调用--->当前微服务所在集群:NJ-CLUSTER,被调用微服务所在集群:NJ-CLUSTER,当前微服务的版本:V1,被调用微服务版本:V1,Host:192.168.1.136,Port:8081 2021-03-04 23:42:32.408 INFO 9478 --- [nio-8080-exec-4] .m.TheSameClusterPriorityWithVersionRule : 同集群同版本调用--->当前微服务所在集群:NJ-CLUSTER,被调用微服务所在集群:NJ-CLUSTER,当前微服务的版本:V1,被调用微服务版本:V1,Host:192.168.1.136,Port:8081 2021-03-04 23:42:33.029 INFO 9478 --- [nio-8080-exec-5] .m.TheSameClusterPriorityWithVersionRule : 同集群同版本调用--->当前微服务所在集群:NJ-CLUSTER,被调用微服务所在集群:NJ-CLUSTER,当前微服务的版本:V1,被调用微服务版本:V1,Host:192.168.1.136,Port:8081
将NJ-Cluster,V1版本的product服务下线,此时会去调BJ-Cluster的V1版本的服务:
2021-03-04 23:47:10.242 INFO 9478 --- [nio-8080-exec-8] .m.TheSameClusterPriorityWithVersionRule : 跨集群同版本调用--->当前微服务所在集群:NJ-CLUSTER,被调用微服务所在集群:BJ-CLUSTER,当前微服务的版本:V1,被调用微服务版本:V1,Host:192.168.1.136,Port:8083 2021-03-04 23:47:11.627 INFO 9478 --- [nio-8080-exec-9] .m.TheSameClusterPriorityWithVersionRule : 跨集群同版本调用--->当前微服务所在集群:NJ-CLUSTER,被调用微服务所在集群:BJ-CLUSTER,当前微服务的版本:V1,被调用微服务版本:V1,Host:192.168.1.136,Port:8083 2021-03-04 23:47:12.380 INFO 9478 --- [io-8080-exec-10] .m.TheSameClusterPriorityWithVersionRule : 跨集群同版本调用--->当前微服务所在集群:NJ-CLUSTER,被调用微服务所在集群:BJ-CLUSTER,当前微服务的版本:V1,被调用微服务版本:V1,Host:192.168.1.136,Port:8083
如果将NJ-Cluster,V1版本的product服务也下线,此时由于剩下的只有V2版本的product服务,将无法调用: