负载均衡Ribbon
上一章讲述了服务注册和发现组件Eureka(https://www.jianshu.com/p/5d29348a3435)
,同时跟踪了Eureka的部分源码深入讲解了Eureka的机制,其中还构建了高可用的Eureka Server。本章讲解如何使用RestTemplate和Ribbon相结合作为服务消费者去消费服务,同时从源码角度深入理解Ribbon。
RestTemplate简介
RestTemplate是用来消费REST服务的,它的主要方法都与REST的HTTP协议的一些方法紧密相连,列如REST中HEAD、GET、POST、PUT、DELETE、OPTIONS等方法对应为RestTemplate中headForHeaders()、getForObject()、postForObject()、put()和delete()等方法。RestTemplate支持XML、JSON数据格式,默认实现了序列化,可以自动将JSON字符串转换为实体。
//表示将请求返回的JSON字符串转换成一个User对象
User user = restTemplate.getForObject("https://www.xxx.com/",User.class)
Ribbon简介
负载均衡是指将服务分摊到多个执行单元上,常见的有两种。
- 服务端负载均衡:独立的进程单元,通过负载均衡策略,请求转发到不同的执行器上,如Nenix。
- 客户端负载均衡:将负载均衡的逻辑封装到服务消费者的客户端上,客户端维护服务提供者的信息列表,通过服务列表结合策略分摊到不同的服务提供者。而Ribbon就是属于这一种。
使用负载均衡带来的好处很明显:
- 当集群里的1台或者多台服务器down的时候,剩余的没有down的服务器可以保证服务的继续使用。
- 使用了更多的机器保证了机器的良性使用,不会由于某一高峰时刻导致系统cpu急剧上升。
Ribbon的子模块如下,很多子模块在开发环境中不一定用到。
- ribbon-loadbanlance:可以独立使用或与其他模块一起使用的负载均衡器API。
- ribbon-eureka:结合Eureka客户端API,为负载均衡提供动态服务注册列表信息。
- ribbon-core:Ribbon的核心API。
RestTemplate和Ribbon结合消费服务
我们在上一章项目的基础上进行改造。
- 首先我们在eureka-client Module中加一个controller如下
@RestController
public class HiController {
@Value("${server.port}")
private String port;
@GetMapping("/hi")
public String hi(String name) {
return "hi " + name + ",I am from " + port;
}
}
这么写的目的是需要以不同的端口启动两个eureka-client,访问接口时通过打印不同的端口来判断Ribbon是否实现了负载均衡的功能。
-
启动eureka-server,启动两个eureka-client实例(启动方式在上一章有说明)
-
新建一个spring-boot Module工程eureka-ribbon-client作为服务消费者。
pom依赖如下
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-ribbon</artifactId>
</dependency>
在工程配置文件application.yml中做程序的相关配置如下
spring:
application:
name: eureka-ribbon-client
server:
port: 8765
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
management:
security:
enabled: false
在启动类上添加注解@EnableEurekaClient开启 Eureka Client功能。
@SpringBootApplication
@EnableEurekaClient
public class EurekaRibbonClientApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaRibbonClientApplication.class, args);
}
}
在程序的IOC容器中注入一个restTemplate的Bean,并在这个Bean上加上@LoadBalanced注解,此时RestTemplate就结合了Ribbon开启了负载均衡的功能。
@Configuration
public class RibbonConfig {
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
写一个service类,其中运用restTemplate调用之前启动的eureka-client的API接口。由于Ribbon自身维护有服务信息列表,所以在请求的Uri上不需要使用硬编码(如IP地址),只需要写服务名即可。
@Service
public class RibbonService {
@Autowired
private RestTemplate restTemplate;
public String hiFromClient() {
return restTemplate.getForObject("http://eureka-client/hi?name=dzy", String.class);
}
}
最后写一个Controller类,来调用service中的方法。
@RestController
public class RibbonController {
@Autowired
private RibbonService ribbonService;
@GetMapping("/hi")
public String hi() {
return ribbonService.hiFromClient();
}
}
启动项目,确定在注册中心中已经注册。然后多次访问http://localhost:8765/hi,浏览器会交替出现两个服务提供者实例的信息,说明实现了负载均衡。
LoadBalanceClient简介
负载均衡的核心类为LoadBalanceClient,用LoadBalanceClient可以获取服务提供者的服务信息,多次访问以下代码接口,会轮流得到两个服务提供者的实例信息。
@RestController
public class RibbonController {
@Autowired
private LoadBalancerClient loadBalancerClient;
@GetMapping("/load-balancer")
public String load() {
ServiceInstance instance = loadBalancerClient.choose("eureka-client");
return instance.getHost() + ":" + instance.getPort();
}
}
LoadBalanceClient是从Eureka Client获取服务的注册列表信息,并将服务注册列表信息缓存了一份。当调用choose方法时,根据负载均衡策略选择一个服务实例的信息进行负载均衡。LoadBalanceClient也可以不从Eureka Client获取注册列表信息,这时需要自己维护一份服务注册列表信息,具体配置如下。
stores:
ribbon:
listOfServers: client1.com,client2.com
ribbon:
eureka:
enable: false
通过配置ribbon.eureka.enable为false来禁止调用从Eureka Client获取注册信息。通过stores.ribbon.listOfServices来配置服务实例的Url,此时在调用choose方法时会从配置的实例中来进行负载均衡。
Ribbon源码解析
在使用restTemplate时,只需要添加@LoadBalanced注解即可,非常的简单方便,现在我们看看它底层的原理。
LoadBalancerAutoConfiguration.java为实现客户端负载均衡器的自动化配置类。在这个类中实现了很多的功能,如下:
Ribbon要实现负载均衡自动化配置需要满足如下两个条件:
- @ConditionalOnClass(RestTemplate.class):RestTemplate类必须存在于当前工程的环境中。
- @ConditionalOnBean(LoadBalancerClient.class):在spring的Bean工程中必须有LoadBalancerClient.class的实现Bean。
@Configuration
@ConditionalOnClass(RestTemplate.class)
@ConditionalOnBean(LoadBalancerClient.class)
@EnableConfigurationProperties(LoadBalancerRetryProperties.class)
public class LoadBalancerAutoConfiguration {}
在自动化配置中主要做三件事:
- 创建一个LoadBalancerInterceptor的Bean,用于实现对客户端发起请求时进行拦截,以实现客户端负载均衡。
@Bean
public LoadBalancerInterceptor ribbonInterceptor(
LoadBalancerClient loadBalancerClient,
LoadBalancerRequestFactory requestFactory) {
return new LoadBalancerInterceptor(loadBalancerClient, requestFactory);
}
- 创建一个RestTemplateCustomizer的Bean,用于给RestTemplate增加LoadBalancerInterceptor拦截器。
@Bean
@ConditionalOnMissingBean
public RestTemplateCustomizer restTemplateCustomizer(
final LoadBalancerInterceptor loadBalancerInterceptor) {
return new RestTemplateCustomizer() {
@Override
public void customize(RestTemplate restTemplate) {
List<ClientHttpRequestInterceptor> list = new ArrayList<>(
restTemplate.getInterceptors());
list.add(loadBalancerInterceptor);
restTemplate.setInterceptors(list);
}
};
}
- 维护了一个被@LoadBalanced注解修饰的RestTemplate对象列表,并在这里进行初始化,通过调用RestTemplateCustomizer的实例来给需要客户端负载均衡的RestTemplate增加LoadBalancerInterceptor拦截器。
@LoadBalanced
@Autowired(required = false)
private List<RestTemplate> restTemplates = Collections.emptyList();
接下来,我们看看LoadBalancerInterceptor拦截器是如何将一个普通的RestTemplate变成客户度负载均衡的。在拦截器中执行的是loadBalancer的execute方法。
@Override
public ClientHttpResponse intercept(final HttpRequest request, final byte[] body,
final ClientHttpRequestExecution execution) throws IOException {
final URI originalUri = request.getURI();
String serviceName = originalUri.getHost();
Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri);
return this.loadBalancer.execute(serviceName, requestFactory.createRequest(request, body, execution));
}
在execute主要调用了getServer方法。
@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);
}
protected Server getServer(ILoadBalancer loadBalancer) {
if (loadBalancer == null) {
return null;
}
return loadBalancer.chooseServer("default"); // TODO: better handling of key
}
在getServer函数中,我们可以看到获取具体服务实例的时候,使用了Netflix Ribbon自身的ILoadBalancer接口中定义的chooseServer函数。该接口代码如下
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();
}
在BaseLoadBalancer类实现了基础的负载均衡,其中chooseServer代码如下。
public Server chooseServer(Object key) {
if (counter == null) {
counter = createCounter();
}
counter.increment();
if (rule == null) {
return null;
} else {
try {
return rule.choose(key);
} catch (Exception e) {
logger.warn("LoadBalancer [{}]: Error choosing server for key {}", name, key, e);
return null;
}
}
}
在其中主要代码为rule.choose(key),调用的是IRule接口的choose方法。
public interface IRule{
public Server choose(Object key);
public void setLoadBalancer(ILoadBalancer lb);
public ILoadBalancer getLoadBalancer();
}
choose方法的实现类如图

其中常用的实现类为:
RandomRule:随机选择一个UP的服务。
RoundRobinRule:轮询获取服务。
BestAvailableRule:跳过熔断的服务,获取请求数最少的服务.通常与ServerListSubsetFilter一起使用。
RetryRule:在RoundRobinRule的基础上,增加了重试的机制。
BestAvailableRule:表示请求数最少策略。
综上可以得出用@LoadBalanced标注的restTemplate在请求之前增加了拦截机制,在拦截器中是调用了LoadBalancerClient的execute方法,在其中根据负载均衡的策略进行服务实例的访问。
总结
在这一章节中,对RestTemplate,Ribbon进行了介绍,并实现了两者的结合来实现负载均衡消费服务。最后对Ribbon部分源码进行了简单解析。在下一章介绍声明式调用feign的使用。
PS:项目github地址:https://github.com/dzydzydzy/spring-cloud-example.git