spring cloud Ribbon运用与源码分析
简介
关于负载均衡,大部分互联网从业者都有了解,本文就简单描述一下,不进行深挖。常见的负载均衡有两种,一种就是独立进程单元,将请求通过负载均衡策略分摊到其他执行者,比如典型的Nginx。另一种将负载逻辑以代码形式封装到服务消费者的客户端上,客户端维护了一份服务提供者的信息,用过负责均衡策略来分摊达到期望的效果。今天本文所介绍的Ribbon就是基于第二种。
项目搭建
既然本文介绍的是ribbon的负载均衡,所以就需要多个实例。建立一个eureka 的注册中心,以及两个相同的eureka client端。一个client端端口为2101.一个为2102.每个从client端都有一个相同的Controller。
@RestController
@RequestMapping("/hello")
public class DiscoveryController {
@Value("${server.port}")
String port;
@GetMapping("/")
public String hello(@RequestParam String name) {
return "Hello, " + name + ", port : " + port;
}
}
然后启动eureka 的server端以及两个client端。
可以清楚的发现有eureka-client服务注册到上去了,并且此服务拥有两个实例。
搭建ribbon端
新建一个模块,引入ribbon,eureka client的相关依赖。
pom文件引入的相关依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
application.properties的配置
spring.application.name=ribbon
server.port=8088
eureka.client.serviceUrl.defaultZone=http://localhost:1101/eureka/
没有什么好说的,只是作为一个正常的eureka client端正常配置。
关于code的编写
Controller层的编写
作为一个独立的web微服务,自然就需要Controller层。
@RequestMapping("/hello")
@RestController
public class RibbonController {
@Autowired
RibbonService ribbonService;
@GetMapping("/")
public String hello(@RequestParam String name) {
return ribbonService.hello(name);
}
service层的编写
有了Controller层,自然要调用service层,本文为了从简演示,没有去写接口,直接写实现类。
@Service
public class RibbonService {
@Autowired
RestTemplate restTemplate;
// 定向到访问的服务地址以及穿书参数,地址以服务名方式访问,不需要用编码写成IP端口形式。
public String hello(String name) {
return restTemplate.getForObject("http://eureka-client/hello/?name=" + name, String.class);
}
}
config的编写
因为借助于spring 的restTemplate去访问接口,所以根据官网文档,我们还需要创建一个bean,来给restTemplate开启负载均衡功能。
@Configuration
public class RibbonConfig {
@Bean
@LoadBalanced
RestTemplate restTemplate(){
return new RestTemplate();
}
这时候就可以启动ribbon这个模块了。启动完成后在注册中心可以发现ribbon已经注册上去了。
这时候我们访问http://localhost:8088/hello/?name=aa这个地址可以清楚的获取到预期的结果。
LoadBalanceClient 简介与演示
loadBalanceClient是Ribbon的核心类,loadBalanceClient可以获得负载均衡提供者的实例信息。接下里演示一下loadBalanceClient的功能。我们需要重写一下RibbonController,添加了一个方法。
@Autowired
LoadBalancerClient loadBalancerClient;
@GetMapping("/testRibbon")
public String testRibbon(){
ServiceInstance serviceInstance = loadBalancerClient.choose("eureka-client");
return serviceInstance.getHost() + ":" + serviceInstance.getPort();
}
代码中可知,注入一个loadBalancerClient的实例,通过它来获取eureka-client这个服务的具体地址和端口。运行一下,访问http://localhost:8088/hello/testRibbon。
可观察它已轮流获取两个eureka-client的实例信息。那它是如何获取到服务的实例信息的呢,熟悉eureka的同学应该了解。ribbon本身作为eureka的一个子服务启动时会自动从注册中心拉取到一份服务信息缓存到本地。
ribbon其实也可以不依赖服务中心拉取服务信息,也可以在本地进行维护,需要修改一下application.properties的配置文件即可。
# 关掉ribbon从服务中心获取服务信息
ribbon.eureka.enabled=false
# 配置eureka-client实例的具体地址
eureka-client.ribbon.listOfServers= http://localhost:2101,http://localhost:2102
这样就可以做到不依赖服务中心,直接本地维护达到负载效果。
源码分析
前面曾提到过LoadBalancerClient为Ribbon的核心类,接下来就分析这个类的作用。
public interface LoadBalancerClient extends ServiceInstanceChooser {
<T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException;
<T> T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest<T> request) throws IOException;
URI reconstructURI(ServiceInstance instance, URI original);
}
从代码中可以看到,LoadBalancerClient是一个接口继承了ServiceInstanceChooser这个接口。名字也直达的表明ServiceInstanceChooser这个接口是选择服务实例的功能。LoadBalancerClient的实现类为RibbonLoadBalancerClient。
继续分析RibbonLoadBalancerClient的代码
/**
* New: Select a server using a 'key'.
* @param serviceId of the service to choose an instance for
* @param hint to specify the service instance
* @return the selected {@link ServiceInstance}
*/
public ServiceInstance choose(String serviceId, Object hint) {
Server server = getServer(getLoadBalancer(serviceId), hint);
if (server == null) {
return null;
}
return new RibbonServer(serviceId, server, isSecure(server, serviceId),
serverIntrospector(serviceId).getMetadata(server));
}
代码太长就贴了部分,choose()这个方法是选择一个实例,会调用getServer()方法来获取一个实例,如果为空就实例化一个内部类RibbonServer,返回一个新实例。接下我们继续追踪getServer()方法。
protected Server getServer(ILoadBalancer loadBalancer, Object hint) {
if (loadBalancer == null) {
return null;
}
// Use 'default' on a null hint, or just pass it on?
return loadBalancer.chooseServer(hint != null ? hint : "default");
}
从代码分析,loadBalancer这个类是干啥的,查看一下。
public interface ILoadBalancer {
void addServers(List<Server> var1);
Server chooseServer(Object var1);
void markServerDown(Server var1);
/** @deprecated */
@Deprecated
List<Server> getServerList(boolean var1);
List<Server> getReachableServers();
List<Server> getAllServers();
}
可知,ILoadBalancer就是一个接口,这个接口包含了获取servers的以及其它方法。接下里继续查看这个接口的实现状况。
进入到BaseLoadBalancer类里查看一下
观察一下类结构,发现有三个内部类以及一些构造方法。简单介绍一下内部类做了啥,pingtask建立一个定时任务定时进行心跳。心跳的同时检测服务中心的服务列表是否与本地缓存的一致,如不一致就进行更新。
在诸多构造方法中,自然会注意到IPing,IRule这些类是干啥。追踪观察一下
public interface IRule {
Server choose(Object var1);
void setLoadBalancer(ILoadBalancer var1);
ILoadBalancer getLoadBalancer();
}
IRule这个接口是负载均衡的负载策略接口
有诸多实现类,简单的介绍一下各个类实现了哪些负载策略。
- BestAvailableRule:选择最小请求数
- ClientConfigEnabledRoundRobinRule:轮询
- RandomRule:随机选择
- WeightedResponseTimeRule:按权重进行分配
- ZoneAvoidanceRule: 根据server 和zone的区域进行轮询
看下IPing
public interface IPing {
boolean isAlive(Server var1);
}
接口很简洁,就是判断server是否还存活。看下实现类
这几个类从名字去判断也可了解就是去判断server上的服务是否响应。
- DummyPing:直接返回true,并新增了一个initWithNiwsConfig方法。
- NoOpPing: 直接返回true。
- PingConstant: 固定返回某服务是否可用,默认为true。
DynamicServerListLoadBalancer为BaseLoadBalancer具体实现类,详细介绍了各个功能实现。
public DynamicServerListLoadBalancer(IClientConfig clientConfig, IRule rule, IPing ping, ServerList<T> serverList, ServerListFilter<T> filter, ServerListUpdater serverListUpdater) {
super(clientConfig, rule, ping);
this.isSecure = false;
this.useTunnel = false;
this.serverListUpdateInProgress = new AtomicBoolean(false);
this.updateAction = new NamelessClass_1();
this.serverListImpl = serverList;
this.filter = filter;
this.serverListUpdater = serverListUpdater;
if (filter instanceof AbstractServerListFilter) {
((AbstractServerListFilter)filter).setLoadBalancerStats(this.getLoadBalancerStats());
}
this.restOfInit(clientConfig);
}
构造方法里加载了之前提到的相关配置,最后并进行初始化。关于IClientConfig这个接口的配置
public interface IClientConfig {
String getClientName();
String getNameSpace();
void loadProperties(String var1);
void loadDefaultValues();
Map<String, Object> getProperties();
就是获得一些client端的基础信息,实现类为DefaultClientConfigImpl,这个类就是一个配置类,定义了大多配置常量。
由此可知,ribbon启动时先从服务中心拉取服务列表,并且定时心跳检测,然后通过IRule的负载策略来达到负载均衡。
不过,不知道读者是否发现之前分析了那么久的LoadBalancerClient,和@LoadBalanced注解有什么关系呢。
接下来,在项目里搜索@LoadBalanced,看下有哪些类使用过这注解。在LoadBalancerAutoConfiguration这个类里发现了这个注解。
@Configuration
@ConditionalOnClass({RestTemplate.class})
@ConditionalOnBean({LoadBalancerClient.class})
@EnableConfigurationProperties({LoadBalancerRetryProperties.class})
public class LoadBalancerAutoConfiguration {
@LoadBalanced
@Autowired(
required = false
)
private List<RestTemplate> restTemplates = Collections.emptyList();
@Autowired(
required = false
)
不仅发现了使用@LoadBalanced注解,还注入了LoadBalancerClient,RestTemplate这两个类。那这个类做什么呢,发现restTemplates这个list。
@Bean
public SmartInitializingSingleton loadBalancedRestTemplateInitializerDeprecated(final ObjectProvider<List<RestTemplateCustomizer>> restTemplateCustomizers) {
return () -> {
restTemplateCustomizers.ifAvailable((customizers) -> {
Iterator var2 = this.restTemplates.iterator();
while(var2.hasNext()) {
RestTemplate restTemplate = (RestTemplate)var2.next();
Iterator var4 = customizers.iterator();
while(var4.hasNext()) {
RestTemplateCustomizer customizer = (RestTemplateCustomizer)var4.next();
customizer.customize(restTemplate);
}
}
});
};
}
在这个初始化方法里,运用到customizer.customize(restTemplate)这个列表里的对象。这个类里还维护了一个拦截器的内部类,初始化的会自动为其添加LoadBalancerInterceptor拦截器。
@Configuration
@ConditionalOnMissingClass({"org.springframework.retry.support.RetryTemplate"})
static class LoadBalancerInterceptorConfig {
LoadBalancerInterceptorConfig() {
}
// 实例化一个拦截器
@Bean
public LoadBalancerInterceptor ribbonInterceptor(LoadBalancerClient loadBalancerClient, LoadBalancerRequestFactory requestFactory) {
return new LoadBalancerInterceptor(loadBalancerClient, requestFactory);
}
@Bean
@ConditionalOnMissingBean
public RestTemplateCustomizer restTemplateCustomizer(final LoadBalancerInterceptor loadBalancerInterceptor) {
return (restTemplate) -> {
List<ClientHttpRequestInterceptor> list = new ArrayList(restTemplate.getInterceptors());
list.add(loadBalancerInterceptor);
restTemplate.setInterceptors(list);
};
所以这就是为什么给RestTemplate增加一个@LoadBalanced注解就可以开启负载均衡,因为维护了一个@LoadBalanced注解的restTemplate列表,在初始化时,会对每个对象增加一个拦截器,而拦截器的方法就是之前提到的ribbon 核心接口LoadBalancerClient去完成。