Ribbon 是什么
Ribbon 是一个客户端负载均衡器,它赋予了应用一些支配HTTP 与 TCP 行为的能力,这里的负载均衡是客户端的负载均衡,也有人称为后端负载均衡是进程内负载均衡的一种。
- 前面分析了 Eureka 的使用,作为服务注册中心,Eureka 分为 Server 端和 Client 端,Client 端作为服务的提供者,将自己注册到 Server 端,Client 端高可用的方式是使用多机部署然后注册到 Server,Server 端为了保证服务的高可用,也可以使用多机部署的方式。
- 前面简单搭建了Eureka Client 和 Server,然后将Client成功注册到 Server,本节我们将来看看如何调用Eureka 服务,在多机部署情况下如何保证负载均衡。Spring Cloud 提供了一个组件:Ribbon。
- Ribbon是客户端负载均衡器,可以对 HTTP 和 TCP 客户端的行为进行大量控制。Ribbon中的中心概念是指定客户端的概念。
Ribbon API
服务端负载均衡
- 服务端负载均衡。将多个服务注册到一个公共的注册中心,服务调用者访问注册中心,由注册中心提供服务的负载均衡。
客户端负载均衡:
服务端负载均衡的代表性例子就是nginx,LVS。那么客户端的负载均衡就是我们要说的Ribbon。Ribbon主要提供客户端负载平衡算法,除此之外,Ribbon还提供:
- 服务发现集成 : 功能区负载平衡器在动态环境(如云)中提供服务发现。功能区库中包含与Eureka和Netflix服务发现组件的集成;
- 容错 : Ribbon API 可以动态确定服务器是否已在实时环境中启动并运行,并且可以 检测到那些已关闭的服务器;
- 可配置的负载平衡规则 : Ribbon支持开箱即用的 RoundRobinRule,AvailabilityFilteringRule,WeightedResponseTimeRule,还支持定义自定义规则。
Ribbon API
Rule
:定义负载均衡策略;Ping
: 定义如何 ping 目标服务实例来判断是否存活, ribbon 使用单独的线程每隔一段时间(默认10s)对本地缓存的ServerList
做一次检查;ServerList
:定义如何获取服务实例列表. 两种实现基于配置的ConfigurationBasedServerList
和基于Eureka 服务发现的DiscoveryEnabledNIWSServerList
;ServerListFilter
: 用来使用期望的特征过滤静态配置,动态获得的候选服务实例列表. 若未提供, 默认使用ZoneAffinityServerListFilter;ILoadBalancer
: 定义了软负载均衡器的操作的接口。一个典型的负载均衡器至少需要一组用来做负载均衡的服务实例;ServerListUpdater
: DynamicServerListLoadBalancer用来更新实例列表的策略(推EurekaNotificationServerListUpdater/拉PollingServerListUpdater, 默认是拉)
Ribbon 的搭建
由于客户端负载均衡需要从注册中心获取服务列表,所以需要集成注册中心。
创建注册中心
-
创建父级工程 cloud-ribbon-practice
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.2.3.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> <spring.cloud-version>Hoxton.SR3</spring.cloud-version> </properties> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring.cloud-version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>
-
创建注册中心 cloud-eureka-server
<dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId> </dependency> </dependencies>
-
配置文件 application.yaml
server: port: 8761 eureka: instance: hostname: localhost client: registerWithEureka: false fetchRegistry: false serviceUrl: defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
-
启动类
@SpringBootApplication @EnableEurekaServer public class EurekaServerApplication { public static void main(String[] args) { SpringApplication.run(EurekaServerApplication.class, args); } }
创建源服务的提供者 cloud-ribbon-hello-b1、cloud-ribbon-hello-b2、cloud-ribbon-hello-b3
-
每个工程都引入 Eureka
<dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> </dependencies>
-
配置文件 application.xml
b1spring: application: name: ribbon-service-b server: port: 7777 eureka: client: serviceUrl: defaultZone: http://localhost:8761/eureka/ instance: prefer-ip-address: true
b2
spring: application: name: ribbon-service-b server: port: 7778 eureka: client: serviceUrl: defaultZone: http://localhost:8761/eureka/ instance: prefer-ip-address: true
b3
spring: application: name: ribbon-service-b server: port: 7779 eureka: client: serviceUrl: defaultZone: http://localhost:8761/eureka/ instance: prefer-ip-address: true
-
b1、b2、b3的controller
@RestController public class OrderController { @Value("${server.port}") private Integer port; @Value("${spring.application.name}") private String name; @GetMapping("/test") public String add() { return "this service name is " + name + " and port is " + port; } }
-
b1、b2、b3的启动类
@SpringBootApplication @EnableDiscoveryClient public class ServiceB1Application { public static void main(String[] args) { SpringApplication.run(ServiceB1Application.class, args); } } @SpringBootApplication @EnableDiscoveryClient public class ServiceB2Application { public static void main(String[] args) { SpringApplication.run(ServiceB2Application.class, args); } } @SpringBootApplication @EnableDiscoveryClient public class ServiceB3Application { public static void main(String[] args) { SpringApplication.run(ServiceB3Application.class, args); } }
创建服务的调用者 cloud-ribbon-hello-a
-
spring-cloud-starter-netflix-eureka-client
已经集成了 ribbon。不需要额外引入,直接使用即可。<dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> </dependencies>
-
配置文件 application.yml
spring: application: name: ribbon-hello-a server: port: 7776 eureka: client: serviceUrl: defaultZone: http://localhost:8761/eureka/ instance: prefer-ip-address: true
启动类,RestTemplate 使用了
@LoadBalanced
,这样 RestTemplate 就开启了 ribbon 的负载均衡了。@SpringBootApplication @EnableDiscoveryClient public class ServiceAApplication { public static void main(String[] args) { SpringApplication.run(ServiceAApplication.class, args); } @LoadBalanced @Bean public RestTemplate restTemplate() { return new RestTemplate(); } } controller @RestController @RequestMapping("ribbon") public class TestController { @Autowired private RestTemplate restTemplate; @GetMapping("/test") public String test(){ String body = restTemplate.getForEntity("http://RIBBON-SERVICE-B/test", String.class).getBody(); return body; } }
测试
- 测试,使用postman访问几次:
http://localhost:7776/ribbon/...
Ribbon 的服务策略配置
策略配置参数
从上一节可以看到,开启负载均衡只需通过@LoadBalanced
注解即可。
- 定义一个负载均衡的 RestTemplate,使用了
@LoadBalanced
注解,该注解配合覆盖均衡策略一起使用RestTemplate
发出的请求才能生效。
可以根据实际的业务场景选择最合适的策略:
策略类 | 命名 | 描述 |
---|---|---|
RandomRule | 随机策略 | 随机选择Server |
RoundRobinRule | 轮询策略 | 按顺序选择Server |
RetryRule | 重试策略 | 在一个配置时间段内当选择 Server 不成功,则一直尝试选择一个可用的Server |
BestAvailableRule | 最低并发策略 | 先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,然后选择一个并发量最小的服务。 |
AvailabilityFilteringRule | 可用过滤策略 | 过滤一直连接失败并标记为circuit tripped的Server,过滤掉那些高并发连接的Server(active connections超过配置的阈值) |
ResponseTimeWeightedRule | 响应时间加权策略 | 已经被弃用,作用同WeightedResponseTimeRule |
WeightedResponseTimeRule | 响应时间加权策略 | 根据Server的响应时间分配权重,响应时间越长,权重越低,被选中的概率就越低。响应时间越短,权重越高,被选择到的概率越高 |
ZoneAvoidanceRule | 区域权衡策略 | 综合判断 Server 所在区域的性能和Server 的可用性轮询选择 Server,并且判断一个 AWS Zone 的运行性能是否可用,剔除不可用的 Zone 中 的所有 Server |
配置方式
全局配置
-
使用 Ribbon 时配置全局的负载均衡策略,需要加一个配置类。该配置类需要被
@ComponentScan
扫描到才能全局生效。@Configuration public class GlobalRuleConfig { @Bean public IRule ribbonRule() { return new RandomRule(); } }
上面配置了随机的策略,多次访问
http://localhost:7776/ribbon/...
基于 @RibbonClient
或@RibbonClients
注解的配置
注意:编写自定义配置类,需要特别注意的是官方文档明确给出了警告:这个自定义配置类不能放在
@ComponentScan
所扫描的包以及其子包下(即不能放在主启动类所在的包及其子包下,因此我们需要新建一个包来放该配置类),否则我们自定义的这个配置类就会被所有的Ribbon客户端所共享,也就达不到特殊化定制的目的了
-
策略配置
@Configuration public class AnnoRuleConfig { @Bean public IRule ribbonRule() { return new RandomRule(); } }
-
启动类的配置
@SpringBootApplication @EnableDiscoveryClient @RibbonClient(name = "ribbon-service-b", configuration = AnnoRuleConfig.class) public class ServiceAApplication { public static void main(String[] args) { SpringApplication.run(ServiceAApplication.class, args); } @LoadBalanced @Bean public RestTemplate restTemplate() { return new RestTemplate(); } }
- 通过
@RibbonClient
指定某个服务的负载均衡策略,其他没有被指定的,就是用默认的负载均衡策略。
- 通过
-
@RibbonClient
注解可以把其他的配置类作为另外一个IOC容器导入到应用中,相当于加载了两个完全不相干的 Spring 的 beans 配置文件,此时应用中会有两个IOC容器。@RibbonClient(name = "RIBBON-SERVICE-B", configuration = AnnoRuleConfig.class)
-
也可以使用
RibbonClients
指定多个服务的负载均衡策略@RibbonClients(value = { @RibbonClient(name = "RIBBON-SERVICE-B", configuration = AnnoRuleConfig.class), @RibbonClient(name = "RIBBON-SERVICE-C", configuration = AnnoRuleConfig.class) })
基于配置文件
-
下面对服务
ribbon-service-b
的负载均衡策略使用RIBBON-SERVICE-B: ribbon: NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
Ribbon 超时与重试
使用 HTTP 发起请求难免会发生问题,在F版开始 Ribbon 的重试机制默认是开启的,需要添加对超时时间与重试策略的配置。
-
列如下面
ribbon-service-b
服务的配置RIBBON-SERVICE-B: ribbon: NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule ConnectTimeout: 3000 ReadTimeout: 60000 MaxAutoRetries: 3 #对第一次请求的服务的重试次数 MaxAutoRetriesNextServer: 1 #要重试的下一个服务的最大数量(不包括第一个服务) OkToRetryOnAllOperations: true
-
也可以全局配置
ribbon: ConnectTimeout: 3000 ReadTimeout: 60000 MaxAutoRetries: 3 #对第一次请求的服务的重试次数 MaxAutoRetriesNextServer: 1 #要重试的下一个服务的最大数量(不包括第一个服务) OkToRetryOnAllOperations: true
-
一般 Ribbon 都是搭配 OpenFeign 这类 Http 客户端或者其他RPC 使用。因为这样去调用远程服务会更加优雅和方便。而OpenFeign默认是继承了Ribbon,对于Ribbon的超时时间配置也是很简单。
-
对于网络抖动这些可以使用
spring-retry
,spring-retry是spring提供的一个基于spring的重试框架,非常好用。
Ribbon 饥饿加载
Ribbon 在进行客户端负载均衡的时候,并不是启动时就加载上下文,而是在实际请求的时候采取创建。
-
因为要加载上下文的原因,在第一次调用时可能会很慢,甚至导致超时。
-
所以我们可以指定 Ribbon 客户端开启立即加载(饥饿加载),在应用启动的时候就立即加载所有配置项的应用程序上下文。
ribbon: eager-load: clients: ribbon-service-b, ribbon-service-order enabled: true
Eureka 在 Ribbon 的作用
下面看一下整合 Eureka 和 Ribbon 如何实现服务调用 和 负载均衡。有了 Eureka 之后,服务调用就无需关注服务提供者的IP。