学习笔记
作用:Ribbon主要提供客户端的软件负载均衡算法
简介:Spring Cloud Ribbon是一个基于HTTP和TCP的客户端负载均衡工具,它基于Netflix Ribbon实现。通过Spring Cloud的封装,可以让我们轻松地将面向服务的REST模板请求自动转换成客户端负载均衡的服务调用。
通过Spring Cloud Ribbon的封装,我们在微服务架构中使用客户端负载均衡调用非常简单,只需要如下两步:
1.服务提供者只需要启动多个服务实例并注册到一个注册中心或是多个相关联的服务注册中心。
2.服务消费者直接通过调用被@LoadBalanced注解修饰过的RestTemplate来实现面向服务的接口调用。
@Bean //把RestTemplate注册到SpringBoot容器中,如果使用rest方式以别名方式进行调用,依赖ribbon负载均衡器@LoadBalanced @LoadBalanced //@LoadBalanced开启以别名方式去Eureka读取注册信息,然后本地实现rpc远程调用 RestTemplate restTemplate() { return new RestTemplate(); }
修改负载均衡策略配置
服务提供者别名:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RoundRobinRule
内置负载均衡规则类 | 规则描述 |
RoundRobinRule | 简单轮询服务列表来选择服务器。它是Ribbon默认的负载均衡规则。 |
AvailabilityFilteringRule | 对以下两种服务器进行忽略: (1)在默认情况下,这台服务器如果3次连接失败,这台服务器就会被设置为“短路”状态。短路状态将持续30秒,如果再次连接失败,短路的持续时间就会几何级地增加。 (2)并发数过高的服务器。如果一个服务器的并发连接数过高,配置了AvailabilityFilteringRule规则的客户端也会将其忽略。并发连接数的上线,可以由客户端的进行配置。 |
WeightedResponseTimeRule | 为每一个服务器赋予一个权重值。服务器响应时间越长,这个服务器的权重就越小。这个规则会随机选择服务器,这个权重值会影响服务器的选择。 |
ZoneAvoidanceRule | 以区域可用的服务器为基础进行服务器的选择。使用Zone对服务器进行分类,这个Zone可以理解为一个机房、一个机架等。 |
BestAvailableRule | 忽略那些短路的服务器,并选择并发数较低的服务器。 |
RandomRule | 随机选择一个可用的服务器。 |
Retry | 重试机制的选择逻辑 |
Ribbon源码分析
LoadBalancerAutoConfiguration.java为实现客户端负载均衡器的自动化配置类。
@Bean @ConditionalOnMissingBean public RestTemplateCustomizer restTemplateCustomizer( final LoadBalancerInterceptor loadBalancerInterceptor) { return restTemplate -> { List<ClientHttpRequestInterceptor> list = new ArrayList<>( restTemplate.getInterceptors()); list.add(loadBalancerInterceptor); restTemplate.setInterceptors(list); }; }
在自动化配置中主要做三件事:
- 创建一个LoadBalancerInterceptor的Bean,用于实现对客户端发起请求时进行拦截,以实现客户端负载均衡。
- 创建一个RestTemplateCustomizer的Bean,用于给RestTemplate增加LoadBalancerInterceptor拦截器。
- 维护了一个被@LoadBalanced注解修饰的RestTemplate对象列表,并在这里进行初始化,通过调用RestTemplateCustomizer的实例来给需要客户端负载均衡的RestTemplate增加LoadBalancerInterceptor拦截器。
从@LoadBalanced注解码的注释中,可以知道该注解用来给RestTemplate标记,以使用负载均衡的客户端(LoadBalancerClient)来配置它。
LoadBalancerClient
public interface LoadBalancerClient extends ServiceInstanceChooser { <T> T execute(String var1, LoadBalancerRequest<T> var2) throws IOException; // 从负载均衡器中挑选出的服务实例来执行请求内容。 <T> T execute(String var1, ServiceInstance var2, LoadBalancerRequest<T> var3) throws IOException; // 为了给一些系统使用,创建一个带有真实host和port的URI。 // 一些系统使用带有原服务名代替host的URI,比如http://myservice/path/to/service。 // 该方法会从服务实例中取出host:port来替换这个服务名。 URI reconstructURI(ServiceInstance var1, URI var2); }
父接口ServiceInstanceChooser
public interface ServiceInstanceChooser { /** * Choose a ServiceInstance from the LoadBalancer for the specified service * @param serviceId the service id to look up the LoadBalancer * @return a ServiceInstance that matches the serviceId */ // 根据传入的服务名serviceId,从负载均衡器中挑选一个对应服务的实例。 ServiceInstance choose(String serviceId); }
RibbonLoadBalancerClient 实现类代码
@Override public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException { // 获取负载均衡策略 ILoadBalancer loadBalancer = getLoadBalancer(serviceId); // 根据负载均衡策略,获取一个服务器 Server server = getServer(loadBalancer); if (server == null) { throw new IllegalStateException("No instances available for " + serviceId); } RibbonServer ribbonServer = new RibbonServer(serviceId, server, isSecure(server, serviceId), serverIntrospector(serviceId).getMetadata(server)); return execute(serviceId, ribbonServer, request); }
ILoadBalancer接口
public interface ILoadBalancer { //向负载均衡器的实例列表中增加实例 public void addServers(List<Server> newServers); //通过某种策略,从负载均衡器中选择一个具体的实例 public Server chooseServer(Object key); //用来通知和标识负载均衡器中某个具体实例已经停止服务,不然负载均衡器在下一次获取服务实例清单前都会认为服务实例均是正常服务的。 public void markServerDown(Server server); //获取正常服务列表 public List<Server> getReachableServers(); //所有已知实例列表 public List<Server> getAllServers(); }
LoadBalancerContext类中实现
// 转换host:port形式的请求地址。 public URI reconstructURIWithServer(Server server, URI original) { String host = server.getHost(); int port = server.getPort(); String scheme = server.getScheme(); if (host.equals(original.getHost()) && port == original.getPort() && scheme == original.getScheme()) { return original; } if (scheme == null) { scheme = original.getScheme(); } if (scheme == null) { scheme = deriveSchemeAndPortFromPartialUri(original).first(); } try { StringBuilder sb = new StringBuilder(); sb.append(scheme).append("://"); if (!Strings.isNullOrEmpty(original.getRawUserInfo())) { sb.append(original.getRawUserInfo()).append("@"); } sb.append(host); if (port >= 0) { sb.append(":").append(port); } sb.append(original.getRawPath()); if (!Strings.isNullOrEmpty(original.getRawQuery())) { sb.append("?").append(original.getRawQuery()); } if (!Strings.isNullOrEmpty(original.getRawFragment())) { sb.append("#").append(original.getRawFragment()); } URI newURI = new URI(sb.toString()); return newURI; } catch (URISyntaxException e) { throw new RuntimeException(e); } }
模仿Ribbon本地负载均衡效果
注册中心
pom.xml
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.1.RELEASE</version> </parent> <!-- 管理依赖 --> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Finchley.M7</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <!-- SpringCloud eureka-server --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId> </dependency> </dependencies> <!-- 注意:这里必须要添加,否则各种依赖有问题 --> <repositories> <repository> <id>spring-milestones</id> <name>Spring Milestones</name> <url>https://repo.spring.io/libs-milestone</url> <snapshots> <enabled>false</enabled> </snapshots> </repository> </repositories>
application.yml
###服务端口号
server:
port: 8100
eureka:
instance:
###注册中心ip地址
hostname: 127.0.0.1
client:
service-url:
###注册中心地址
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
###因为自己是注册中心,是否需要将自己注册给自己的注册中心(集群的时候是需要是为true)
register-with-eureka: false
###因为自己是注册中心,不需要去检索服务信息
fetch-registry: false
server:
###测试时关闭自我保护机制,保证不可用服务及时踢出
enable-self-preservation: false
###剔除失效服务间隔
eviction-interval-timer-in-ms: 2000
主类
@SpringBootApplication @EnableEurekaServer //表示开启EurekaServer服务,开启注册中心 public class AppEureka { public static void main(String[] args) { SpringApplication.run(AppEureka.class, args); } }
服务提供者
pom.xml
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.1.RELEASE</version> </parent> <!-- 管理依赖 --> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Finchley.M7</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <!-- SpringBoot整合Web组件 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- SpringBoot整合eureka客户端 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> </dependencies> <!-- 注意:这里必须要添加,否则各种依赖有问题 --> <repositories> <repository> <id>spring-milestones</id> <name>Spring Milestones</name> <url>https://repo.spring.io/libs-milestone</url> <snapshots> <enabled>false</enabled> </snapshots> </repository> </repositories>
application.yml
###会员项目的端口号
server:
port: 8040
###服务别名(服务注册到注册中心名称)
spring:
application:
name: app-hclz-member
eureka:
client:
service-url:
###当前会员服务注册到eureka服务地址
defaultZone: http://localhost:8100/eureka
###需要将我的服务注册到eureka上
register-with-eureka: true
###需要检索服务
fetch-registry: true
主类
@SpringBootApplication @EnableEurekaClient //将当前服务注册到eureka上 public class AppMember { public static void main(String[] args) { SpringApplication.run(AppMember.class, args); } }
控制类
@RestController public class MemberApiController { @Value("${server.port}") private String serverPort; @RequestMapping("/getMember") public String getMember() { return "this is member,我是会员服务,springcloud2.0版本! 会员项目端口号:"+serverPort; } }
服务消费者
pom.xml(与服务提供者一致)
application.yml
###订单服务的端口号
server:
port: 8001
###服务别名(服务注册到注册中心名称)
spring:
application:
name: app-hclz-order
eureka:
client:
service-url:
###当前会员服务注册到eureka服务地址
defaultZone: http://localhost:8100/eureka
###需要将我的服务注册到eureka上
register-with-eureka: true
###需要检索服务
fetch-registry: true
主类
@SpringBootApplication @EnableEurekaClient public class AppOrder { public static void main(String[] args) { SpringApplication.run(AppOrder.class, args); } @Bean //把RestTemplate注册到SpringBoot容器中,如果使用rest方式以别名方式进行调用,依赖ribbon负载均衡器@LoadBalanced //@LoadBalanced //模仿Ribbon本地负载均衡效果将此注解注释掉 RestTemplate restTemplate() { return new RestTemplate(); } }
//模仿Ribbon本地负载均衡效果 @RestController public class MockRibbonController { //可以获取注册中心上的服务列表 @Autowired private DiscoveryClient discoveryClient; @Autowired private RestTemplate restTemplate; //初始化接口的请求总数 private int reqCount = 1; @RequestMapping("/ribbonMember") public String ribbonMember() { //1.获取对应服务器远程调用地址 String instancesUrl = getInstances() + "/getMember"; System.out.println("************************instancesUrl:" + instancesUrl); //2.可以直接使用httpClient技术实现远程调用 String result = restTemplate.getForObject(instancesUrl,String.class); System.out.println("-----------------------"+result); return result; } private String getInstances() { List<ServiceInstance> instances = discoveryClient.getInstances("app-hclz-member"); if(instances == null || instances.size() <= 0) { return null; } //获取服务器集群个数 int instanceSize = instances.size(); //请求总数 % 服务器集群数量 得到下标 int serviceIndex = reqCount % instanceSize; //将请求总数增加1 reqCount++; return instances.get(serviceIndex).getUri().toString(); } }
分别启动注册中心,多个端口的服务提供者,服务消费者
Ribbon与Nginx区别
Ribbon本地负载均衡,原理:在调用接口的时候,会在eureka注册中心上获取注册信息服务列表,获取到之后,缓存在jvm本地,使用本地实现rpc远程调用技术进行调用。即客户端实现负载均衡。
Nginx服务器负载均衡,客户端所有请求都会交给nginx,然后再由nginx实现转发请求。即服务端实现负载均衡。
应用场景:
Ribbon本地负载均衡器适合在微服务rpc远程调用,比如Dubbo、SpringCloud
Nginx服务负载均衡器适合针对服务器端,比如Tomcat、Jetty