第五章 使用Ribbon实现客户端负载均衡
- 在实际的生产环境中,各个微服务都会部署多个实例。那么服务消费者要如何将请求分摊到多个服务提供者实例上呢?这就需要使用Ribbon。
Ribbon概述
-
Ribbon是Netflix的负载均衡器,它有助于控制HTTP和TCP客户端的行为。为Ribbon配置服务者地址列表后,Ribbon就可以基于某种负载均衡算法,自动地帮助服务消费者去请求。Ribbon默认为我们提供了很多的负载均衡算法,如:轮询,随机等。我们也可以为其实现自定义的负载均衡算法。
-
当Ribbon与Eureka配合使用时,Ribbon可自动从Eureka Server获取服务提供者地址列表,并基于负载均衡算法,请求其中一个服务提供者实例。
为服务消费者整合Ribbon
- 创建Maven项目
-
复制项目microservice-consumer-movie,将ArtifactId修改为microservice-consumer-movie-ribbon。并添加maven依赖如下。
-
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-ribbon</artifactId> </dependency>
-
由于之前的电影微服务添加了spring-cloud-starter-netflix-eureka-client,该依赖已经包含了spring-cloud-starter-netflix-ribbon,所以此处也可以不引入。
-
-
修改启动类添加注解**@LoadBalanced**
-
@Bean @LoadBalanced //此注解可为RestTemplate整合Ribbin,使其具备负载均衡能力。 public RestTemplate restTemplate() { return new RestTemplate(); }
-
-
修改MovieController代码。
-
@RestController public class MovieController{ private static final Logger LOGGER = LoggerFactory.getLogger(MovieController.class); @Autowired private RestTemplate restTemplate; @Autowired private LoadBalancerClient loadBalancerClient; @GetMapping("/user/{id}") public User findById(@PathVariable Long id) { return this.restTemplate.getForObject("http://microservice-provider-user/"+id, User.class); } @GetMapping("/log-user-instance") public void logUserInstance() { ServiceInstance serviceInstance = this.loadBalancerClient.choose("microservice-provider-user"); // 打印当前选择的是哪个节点 MovieController.LOGGER.info("{}:{}:{}", serviceInstance.getServiceId(), serviceInstance.getHost(), serviceInstance.getPort()); } }
-
上面将请求地址修改为了http://microservice-provider-user/。此地址是用户微服务的虚拟主机名,当Ribbon和Eureka配合使用时,会自动将虚拟主机名映射成微服务的网络地址。在新添加的log-user-instance()中可使用LoadBalancerClient的API更加直观地获取当前选择的用户微服务节点。
-
-
测试
-
启动microservice-discovery-eureka
-
启动两个或更多的microservice-provider-user实例
-
启动microservice-movie-ribbon
-
访问http://localhost:8761,如下。
-
多次访问http://localhost:8010/user/1,返回如下
-
同时注意启动的多个用户微服务下,有类似信息
-
Hibernate: select user0_.id as id1_0_0_, user0_.age as age2_0_0_, user0_.balance as balance3_0_0_, user0_.name as name4_0_0_, user0_.username as username5_0_0_ from user user0_ where user0_.id=?
-
-
多次访问http://localhost:8010/log-user-instance,控制台输出如下。
-
可以看到,此时请求会均匀分布到两个用户微服务节点上,说明已经实现了负载均衡。
-
-
注:
-
在默认情况下,虚拟主机名和服务名称是一致的,我们也可以使用配置属性
eureka: instance: virtual-host-name: xxxxxxx #或使用下面配置指定虚拟主机名 eureka: instance: secure-virtual-host-name: xxxxx
-
不能将restTemplate.getForObject(…)与loadBalancerClient.choose(…)写在同一个方法中,两者之间会有冲突。因为此时代码中的restTemplate实际上是一个Ribbon客户端,本身已经包含了"choose"的行为。
-
虚拟主机名不能包含"__"之类的字符,否则Ribbon在调用时会报异常。
-
Ribbon配置自定义
- Spring Cloud Edgware允许使用Java代码或属性自定义Ribbon的配置。
使用Java代码自定义Ribbon配置
配置指定名称的Ribbon Client
-
在Spring Cloud中,Ribbon默认的配置如下(格式为BeanType beanName: ClassName)
- IClientConfig ribbonClientConfig: DefaultClientConfigImpl
- IRule ribbonRule: ZoneAvoidanceRule
- IPing ribbonPing: DummyPing
- ServerList ribbonServerList: ConfigurationBasedServerList
- ServerListFilter ribbonServerListFilter: ZonePreferenceServerListFilter
- ILoadBalancer ribbonLoadBalancer: ZoneAwareLoadBalancer
- ServerListUpdater ribbonServerListUpdater: PollingServerListUpdater
-
如,下面代码
-
//截取自org.springcframework.cloud.netflix.ribbon.RibbonClientConfiguration @Configuration @EnableConfigurationProperties @Import({HttpClientConfiguration.class, OkHttpRibbonConfiguration.class, RestClientRibbonConfiguration.class, HttpClientRibbonConfiguration.class}) public class RibbonClientConfiguration { //...... @Bean @ConditionalOnMissingBean public IRule ribbonRule(IClientConfig config) { if (this.propertiesFactory.isSet(IRule.class, this.name)) { return (IRule)this.propertiesFactory.get(IRule.class, config, this.name); } else { ZoneAvoidanceRule rule = new ZoneAvoidanceRule(); rule.initWithNiwsConfig(config); return rule; } } //...... }
-
BeanType是IRule,beanName是ribbonRule,ClassName是ZoneAvoidanceRule,这是一种根据服务提供者所在Zone的性能以及服务提供者可用性综合计算,选择提供者节点的负载均衡规则。
-
在Spring cloud中,Ribbon默认的配置类是RibbonClientConfiguration。也可以使用一个POJO自定义的Ribbon配置(自定义配置会覆盖默认的配置)。这种配置是细粒度的,不同名称的Ribbon客户端可使用不同配置。
-
-
创建Maven项目,使用Ribbon客户端自定义配置。
-
创建Ribbon的配置类
-
/** * 该类为Ribbon的配置类, * 注意该类不应该出现在应用程序上下文的@ComponentScan所扫描的包中。 */ @Configuration public class RibbonConfiguration { @Bean public IRule ribbonRule() { // 将负载均衡规则改为随机 return new RandomRule(); } }
-
创建空类,如下
-
/** * 使用@RibbonClient,为特定name的Ribbon Client自定义配置。 * 使用@RibbonClient的configuration属性,指定Ribbon的配置类 */ @Configuration @RibbonClient(name = "microservice-provider-user" ,configuration = RibbonConfiguration.class) public class TestConfiguration { }
-
-
测试
- 启动microservice-discovery-eureka。
- 启动多个microservice-provider-user。
- 启动microservice-consumer-movie-ribbon-customizing。
- 多次访问http://localhost:8010/log-user-instance,注意查看日志。如下。可以看到请求随机的分配到了用户微服务上。
-
注:
- 上面RibbonConfiguration类不能存放主应用程序上下文的**@ComponentScan所扫描的包中,否则该类中的配置信息将被所有的@RibbonClient**共享。
- 如果只想自定义某一个Ribbon客户端的配置,必须防止**@Configuration注解的类所在的包与@ComponentScan扫描的包重叠,或应显式指定@ComponentScan不扫描@Configuration**类所在的包。
使用属性自定义Ribbon配置
-
从Spring Cloud Netflix 1.2.0开始,Ribbon支持使用属性自定义。这种方式比使用Java代码配置的方式更加方便。
-
支持属性如下,配置的前缀是.ribbon。是Ribbon Client的名称,如果省略不写,则表示全局配置。
- NFLoadBalancerClassName: 配置ILoadBalancer的实现类。
- NFLoadBalancerRuleClassName: 配置IRule的实现类。
- NFLoadBalancerPingClassName: 配置IPing的实现类。
- NIWSServerListClassName: 配置ServerList的实现类。
- NIWSServerListFilterClassName: 配置ServerListFilter的实现类。
-
创建Maven项目
-
修改application.yml文件
-
server: port: 8010 spring: application: name: microservice-consumer-movie eureka: client: serviceUrl: defaultZone: http://localhost:8761/eureka/ instance: prefer-ip-address: true # 下面将microservice-provider-user的Ribbon Client的负载均衡设置为随机 microservice-provider-user: ribbon: NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule #如果配置为下面的形式表示对所有Ribbon Client都使用RandomRule #ribbon: # NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
-
-
测试
- 启动microservice-discovery-eureka。
- 启动多个microservice-provider-user实例。
- 启动microservice-consumer-movie-ribbon-customizing-properties。
- 多次访问http://localhost:8010/log-user-instance,查看控制台,可以看到请求随机的分配到了两个用户微服务上
-
注:属性配置的方式比Java代码配置的方式优先级更高。
脱离Eureka使用Ribbon
- 在一些遗留的微服务中,它们可能并没有注册到Eureka Server上,甚至根本不是使用Spring Cloud开发的,此时要想使用Ribbon实现负载均衡,就要脱离Eureka。
- Spring Cloud支持脱离Eureka使用,架构如下。
- 创建Maven项目
- 去除spring-cloud-starter-netflix-eureka-client依赖,并添加spring-cloud-starter-netflix-ribbon依赖。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
-
去除启动类上的**@EnableDiscoveryClient**注解
-
@SpringBootApplication public class ConsumerMovieApplication { @Bean @LoadBalanced public RestTemplate restTemplate() { return new RestTemplate(); } public static void main(String[] args) { SpringApplication.run(ConsumerMovieApplication.class,args); } }
-
-
修改application.yml
-
server: port: 8010 spring: application: name: microservice-consumer-movie microservice-provider-user: ribbon: listOfServers: localhost:8000,localhost:8001 # microservice-provider-user.ribbon.listOfServers # 用于为名为microservice-provider-user的Ribbon客户端设置请求的地址列表
-
-
测试
-
对microservice-simple-provider-user进行打包。并启动多个实例,命令如下:
-
java -jar microservice-simple-provider-user-0.0.1-SNAPSHOT.jar
-
java -jar microservice-simple-provider-user-0.0.1-SNAPSHOT.jar --server.port=8001
-
-
启动microservice-consumer-movie-without-eureka。
-
访问http://localhost:8010/log-user-instance,观察控制台打印信息。尽管微服务没有注册到Eureka上,Ribbon仍可正常工作,请求依旧会分摊到两个用户微服务节点上。
-
饥饿加载
-
Spring Cloud会为每个名称的Ribbon Client维护一个子应用程序上下文,指定名称的Ribbon Client第一次请求时,对应的上下文才会被加载,因此,首次请求往往会比较慢。从Spring Cloud Dalston开始,可以在配置中配置饥饿加载,如:
-
ribbon: eager-load: enabled: true clients: client1, client2
-
-
这样对于名为client1,client2的Ribbon Client,将在启动时就加载对应的子应用程序上下文,从而提高首次请求的访问速度。