Ribbon使用手册
如何实现负载均衡?
如何实现负载均衡?
- 服务器端负载均衡
- Nginx
- 客户端侧负载均衡
- 客户端编写负载均衡算法,动态选择微服务实例
- 手写一个客户端侧负载均衡器
Ribbon 与 Spring Cloud Loadbalancer 现状
Ribbon
- 成熟、流行
- 被 Spring Cloud 列入维护模式
Spring Cloud Loadbalancer
- 下一代客户端侧负载均衡器
- 过于简陋
- 暂时没有找到成功案例
- 目前默认的负载均衡器依然是Ribbon
使用 Ribbon
加依赖

spring-cloud-starter-consul-discovery
中已经有了 spring-cloud-starter-netflix-ribbon
的依赖。
加注解 @LoadBalanced
@EnableCaching
@EnableScheduling
@SpringBootApplication
public class ZhibeiApplication {
@PostConstruct
void started() {
// 设置时区为东八区
TimeZone.setDefault(TimeZone.getTimeZone("Asia/Shanghai"));
// TimeZone.setDefault(TimeZone.getTimeZone("GMT+8"));
}
public static void main(String[] args) {
SpringApplication.run(ZhibeiApplication.class, args);
}
@Bean
@LoadBalanced
public RestTemplate createRestTemplate() {
return new RestTempalte();
}
}
第三步,写代码
获取全部实例,并筛选
@RestController
public class TestController {
@Autowired
private DiscoveryClient discoveryClient;
@GetMapping("/test")
public List<ServiceInstance> test() {
List<ServiceInstance> instances = discoveryClient.getInstances("micro-user");
return instances.stream().filter(instance -> {
Map<String, String> metadata = instance.getMetadata();
String engineRoom = metadata.get("engineRoom");
return "本地".equals(engineRoom);
}).collect(Collectors.toList());
}
}
使用 Ribbon 实现负载均衡
本质上就是再一个List中选择一个来调用。
// micro-class
@GetMapping("/test/{userId}")
public UserDTO test(@PathVariable(value = "userId") String userId) {
return restTemplate.getForObject(
"http://micro-user/users/{userId}",
UserDTO.class,
userId
);
}
// micro-user
@GetMapping("/users/{id}")
public User findById(@PathVariable Integer id) {
try {
return userService.findByUserId(id);
} catch (IllegalAccessException e) {
e.printStackTrace();
return null;
}
}
Ribbon 核心组件
接口 | 作用 | 默认值 |
---|---|---|
IClientConfig |
读取配置 | DefaultClientConfigImpl |
IRule |
负债均衡规则,选择实例 | ZoneAvoidanceRule |
IPing |
筛选掉ping不通的实例 | DummyPing |
ServerList<Server> |
交给 Ribbon 的实例列表 | Ribbon:ConfigurationBasedServerList Spring Cloud Consul Discovery:ConsulServerList |
ServerListFilter<Server> |
过滤掉不符合条件的实例 | ZonePerferenceServerListFilter |
ILoadBalancer |
Ribbon 的入口 | ZoneAwareLoadBalancer |
ServerListUpdater |
更新交给 Ribbon 的 List 的策略 | PollingServerListUpdater |
Ribbon 内置负载均衡规则
规则名称 | 特点 |
---|---|
AvailabilityFilter | 过滤掉一直连接失败的被标记为 circuit tripped 的后端 Server ,并过滤掉那些高并发的后端 Server 或者使用一个 AvailabilityPredicate 来过滤 server 的逻辑,其实就是检查 status 里记录的各个 server 的运行状态 |
BestAvailableRule | 选择一个最小的并发请求的 Server,逐个考察 Server,如果 Server 被 tripped 了,则跳过 |
RandomRule | 随机选择一个 Server |
RetryRule | 对选定的负载均衡策略机制上重试机制,在一个配置时间段内当选择 Server 不成功,则一直尝试使用 subRule 的方式选择一个可用的 server |
RoundRobinRule | 轮询选择 |
WeightedResponseTimeRule | 根据响应时间加权,响应时间越长,权重越小,被选中的可能性就越低 |
ZoneAvoidanceRule | 复合判断 Server 所在的 Zone 的性能和 Server 的可用性选择 Server,在没有 Zone 的环境下,类似于轮询(RoundRobinRule) |
细粒度配置自定义
Java 代码方式

package cn.sbx0.micro.classes.configuration;
import org.springframework.cloud.netflix.ribbon.RibbonClient;
import org.springframework.context.annotation.Configuration;
import ribbon.configurantion.RibbonConfiguration;
@Configuration
@RibbonClient(name = "micro-user", configuration = RibbonConfiguration.class)
public class MicroUserConfiguration {
}
package ribbon.configurantion;
import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.RandomRule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RibbonConfiguration {
@Bean
public IRule rule() {
return new RandomRule();
}
}
父子上下文的坑
注意 RibbonConfiguration
所在的包 ribbon.configurantion
和 MicroClassApplication
所在的包 cn.sbx0.micro.classes
是同级的。如果将 RibbonConfiguration
放在 MicroClassApplication
可以扫描到的包路径,该 RibbonConfiguration
将变成全局配置,被所有 Ribbon
所共享,也就无法达到细粒度配置了。
配置属性方式
<clientName>.ribbon.NFLoadBalancerRuleClassName=负载规制的全路径
micro-user:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
Java 代码方式 vs 配置属性方式
配置方式 | 优点 | 缺点 |
---|---|---|
代码方式 | 基于代码,更加灵活 | 有小坑(父子上下文) 线上修改得重新打包、发布 |
配置属性 | 易上手 配置更加直观 线上修改无需重新打包、发布 优先级更高 |
极端情况下没有代码配置方式灵活 |
- 尽量使用属性配置,属性配置方式实现不了得情况下再考虑使用代码配置方式
- 在同一个微服务内尽量保持单一性
- 比如统一使用属性配置,不建议两种方式混用,增加定位代码的复杂性
全局配置
有”聪明“的小伙伴已经猜到可以使用之前提到的 父子上下文
这一小坑来实现全局配置。但是,并不推荐使用这种方式,因为很可能导致整个项目启动都启动不起来。正确的全局配置方式是什么样的呢?
正确的全局配置
package cn.sbx0.micro.classes.configuration;
import org.springframework.cloud.netflix.ribbon.RibbonClients;
import org.springframework.context.annotation.Configuration;
import ribbon.configurantion.RibbonConfiguration;
@Configuration
@RibbonClients(defaultConfiguration = RibbonConfiguration.class)
public class GlobalRibbonConfiguration {
}
目前,全局配置只能使用 Java 代码来实现,不能使用配置属性的方式。
Ribbon 支持的配置项
Java 代码方式
package ribbon.configurantion;
import com.netflix.loadbalancer.IPing;
import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.PingUrl;
import com.netflix.loadbalancer.RandomRule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RibbonConfiguration {
@Bean
public IRule rule() {
return new RandomRule();
}
@Bean
public IPing ping() {
return new PingUrl();
}
}
配置属性方式
<clientName>.ribbon
如下属性:
NFLoadBalancerClassName
:ILoadBalancer
实现类NFLoadBalancerRuleClassName
:IRule
实现类NFLoadBalancerPingClassName
:IPing
实现类NIWSServerListClassName
:ServerList
实现类NIWSServerListFilterClassName
:ServerListFilter
实现类
饥饿加载
Ribbon 默认是懒加载,如何将它设置为饥饿加载呢?
ribbon:
eager-load:
enabled: true
clients: micro-uesr,micro-class
优先调用同机房实例
package cn.sbx0.micro.classes.ribbon;
import com.netflix.client.config.IClientConfig;
import com.netflix.loadbalancer.AbstractLoadBalancerRule;
import com.netflix.loadbalancer.ILoadBalancer;
import com.netflix.loadbalancer.Server;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ThreadLocalRandom;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.consul.discovery.ConsulDiscoveryProperties;
import org.springframework.cloud.consul.discovery.ConsulServer;
import org.springframework.util.CollectionUtils;
public class ShortestRule extends AbstractLoadBalancerRule {
private final String TAG = "JF";
@Autowired
private ConsulDiscoveryProperties consulDiscoveryProperties;
@Override
public void initWithNiwsConfig(IClientConfig iClientConfig) {
// 读取配置
}
@Override
public Server choose(Object o) {
// 1. 获取想要调用微服务的实例列表
ILoadBalancer loadBalancer = this.getLoadBalancer();
List<Server> reachableServers = loadBalancer.getReachableServers();
// 2. 筛选出机房相同的实例列表
// 课程微服务所配置的tags
// spring.cloud.consul.discovery.tags: engineRoom=local
Map<String, String> metadata = consulDiscoveryProperties.getMetadata();
List<Server> matchServer = reachableServers.stream().filter(server -> {
ConsulServer consulServer = (ConsulServer) server;
Map<String, String> consulServerMetadata = consulServer.getMetadata();
return Objects.equals(metadata.get(TAG), consulServerMetadata.get(TAG));
}).collect(Collectors.toList());
// 3. 随机返回一个实例
if (CollectionUtils.isEmpty(matchServer)) {
return this.randomChoose(reachableServers);
}
return this.randomChoose(matchServer);
}
public Server randomChoose(List<Server> servers) {
int i = ThreadLocalRandom.current().nextInt(servers.size());
return servers.get(i);
}
}
package ribbon.configurantion;
import cn.sbx0.micro.classes.ribbon.ShortestRule;
import com.netflix.loadbalancer.IPing;
import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.PingUrl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RibbonConfiguration {
@Bean
public IRule rule() {
return new ShortestRule();
}
@Bean
public IPing ping() {
return new PingUrl();
}
}
元数据还可以做很多事情,比如控制微服务的版本,灰度发布。
在 yml 中的某个配置上 按
Ctrl
+单击
可以看到这个属性是在哪个类中定义的。![]()
如何解决当某个微服务 down 机,导致调用失败的问题
- 将 PolingServerListUpdater 更新 ServerList 定时任务周期缩短
# 五秒
<clientName>.ribbon.ServerListRefreshInterval = 5000
并不建议更改这个属性,因为可能会导致性能问题。
- 配置 IPing ,将不可用的实例筛选掉
并不是每次调用之前都 ping 一下,而是定时去 ping 。
- 直接使用 Consul API 直接从 Consul 查询
package cn.sbx0.micro.classes.ribbon; import com.ecwid.consul.v1.ConsulClient; import com.ecwid.consul.v1.QueryParams; import com.ecwid.consul.v1.Response; import com.ecwid.consul.v1.health.HealthServicesRequest; import com.ecwid.consul.v1.health.model.HealthService; import com.netflix.client.config.IClientConfig; import com.netflix.loadbalancer.AbstractLoadBalancerRule; import com.netflix.loadbalancer.ILoadBalancer; import com.netflix.loadbalancer.Server; import com.netflix.loadbalancer.ZoneAwareLoadBalancer; import java.util.List; import java.util.concurrent.ThreadLocalRandom; import java.util.stream.Collectors; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.consul.discovery.ConsulDiscoveryProperties; import org.springframework.cloud.consul.discovery.ConsulServer; import org.springframework.util.CollectionUtils; public class EveryTimeRule extends AbstractLoadBalancerRule { private final String TAG = "JF"; @Autowired private ConsulDiscoveryProperties consulDiscoveryProperties; @Autowired private ConsulClient consulClient; @Override public void initWithNiwsConfig(IClientConfig iClientConfig) { // 读取配置 } @Override public Server choose(Object o) { // 1. 获取想要调用微服务的实例列表 ILoadBalancer loadBalancer = this.getLoadBalancer(); ZoneAwareLoadBalancer zoneAwareLoadBalancer = (ZoneAwareLoadBalancer) loadBalancer; // 想要调用的微服务的名称 micro-user String name = zoneAwareLoadBalancer.getName(); List<String> tags = consulDiscoveryProperties.getTags(); String tagName = tags.stream().filter(tag -> tag.startsWith(TAG)).findFirst().orElse(null); // 2. 筛选出机房相同的实例列表 Response<List<HealthService>> serviceResponses = this.consulClient.getHealthServices(name, HealthServicesRequest.newBuilder().setTag(tagName).setPassing(true) .setQueryParams(QueryParams.DEFAULT).build()); // 当前健康的微服务实例 List<HealthService> healthServiceList = serviceResponses.getValue(); // 3. 随机返回一个实例 if (CollectionUtils.isEmpty(healthServiceList)) { healthServiceList = this.consulClient.getHealthServices(name, HealthServicesRequest.newBuilder().setTag(null).setPassing(true) .setQueryParams(QueryParams.DEFAULT).build()).getValue(); } List<ConsulServer> matchServer = healthServiceList.stream().map(ConsulServer::new) .collect(Collectors.toList()); return this.randomChoose(matchServer); } public Server randomChoose(List<ConsulServer> servers) { int i = ThreadLocalRandom.current().nextInt(servers.size()); return servers.get(i); } }
如果 Consul 挂了,这个就挂了。每次请求都要调用 Consul ,增加 Consul 的负载。
没有绝对完美的方案,只能根据实际项目来进行取舍。