上一章讲述了服务注册和发现组件Eureka,同时追踪源码深入讲解了Eureka的机制,并通过案例讲解了如何构建高可用的EurekaServer。本章讲解如何使用 RestTemplate和Ribbon相结合作为服务消费者去消费服务,同时从源码的角度来深入讲解 Ribbon。
一、RestTemplate简介
RestTemplate是Spring Resources中一个访问第三方RESTful API接口的网络请求框架。RestTemplate的设计原则和其他SpringTemplate (例如 JdbcTemplate、 JmsTemplate)类似,都是为执行复杂任务提供了一个具有默认行为的简单方法。
RestTemplate是用来消费REST服务的,所以RestTemplate 的主要方法都与REST的Http协议的一些方法紧密相连,例如HEAD、GET、POST、PUT、DELETE和OPTIONS等方法, 这些方法在RestTemplate类对应的方法为headForHeaders()、getForObject()、postForObject()、put()和delete()等。
二、Ribbon简介
负载均衡是指将负载分摊到多个执行单元上,常见的负载均衡有两种方式。一种是独立进程单元,通过负载均衡策略,将请求转发到不同的执行单元上,例如Ngnix。另一种是将负载均衡逻辑以代码的形式封装到服务消费者的客户端上,服务消费者客户端维护了一份服务提供者的信息列表,有了信息列表,通过负载均衡策略将请求分摊给多个服务提供者,从而达到负载均衡的目的。
Ribbon是Netflix公司开源的一个负载均衡的组件,它属于上述的第二种方式,是将负载均衡逻辑封装在客户端中,并且运行在客户端的进程里。Ribbon是一个经过了云端测试的IPC库,可以很好地控制HTTP和TCP 客户端的负载均衡行为。
在SpringCloud构建的微服务系统中, Ribbon作为服务消费者的负载均衡器,有两种使用方式,一种是和RestTemplate相结合,另一种是和Feign相结合。Feign已经默认集成了Ribbon, 关于Feign的内容将会在下一章进行详细讲解。
Ribbon有很多子模块,但很多模块没有用于生产环境 ,目前Netflix公司用于生产环境的Ribbon子模块如下:
- ribbon-loadbalancer: 可以独立使用或与其他模块一起使用的负载均衡器API。
- ribbon-eureka: Ribbon结合Eureka客户端的API,为负载均衡器提供动态服务注册列表信息。
- ribbon-core: Ribbon的核心API
三、使用RestTemplate和Ribbo和Ribbon来消费服务
本案例是上一节案例的基础上进行改造的,先回顾一下上一节中的代码结构,它包括一个服务注册中心eureka-server、一个服务提供者eureka-client。eureka-client向eureka-server注册服务,并且eureka-client提供了一个“ /hi"API接口,用于提供服务。
启动eureka-server, 端口为8671。启动两个eureka-client实例,端口分别为 8762和8763。 启动完成后,在浏览器上访问 http://localhost:8671/,浏览器显示 eureka-client 的两个实例已经成功向服务注册中心注册,它们的端口分别为8672和8673,如下所示。
创建完成 eureka-ribbon-client 的 Module 工程之后 , 在其pom文件中引入相关的依赖,包括继承了主Maven工程的pom文件,引入了EurekaClient的起步依赖spring-cloud-starter eureka、Ribbon的起步依赖spring-cloud-starter-ribbon,以及Web的起步依赖spring-boot-starter-web, 代码如下:
<parent>
<groupId>com.hand</groupId>
<artifactId>macro-service</artifactId>
<version>1.0-SNAPSHOT</version>
<relativePath/>
</parent>
<dependencies>
<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>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
macro-service为主模块的配置,内容如下:
<groupId>com.hand</groupId>
<artifactId>macro-service</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.10.RELEASE</version>
<relativePath/>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring.cloud.vension>Dalston.SR1</spring.cloud.vension>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring.cloud.vension}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
在工程的配置文件appIication.yml做程序的相关配置,包括指定程序名为 eureka-ribbon-client,程序的端口号为8674,服务的注册地址http://localhost:8761/eureka/,代码如下:
spring:
application:
name: eureka-ribbon-client
server:
port: 8674
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8671/eureka/
另外,作为EurekaClient需要在程序的入口类加上注解@EnableEurekaClient开启EurekaClient功能,代码如下:
@SpringBootApplication
@EnableEurekaClient
public class EurekaRibbonClientApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaRibbonClientApplication.class, args);
}
}
写一个RESTful API接口,在该API接口内部需要调用eureka-client的API接口"/hi”, 即服务消费。由于eureka-client为两个实例,它们的端口为8672和8673。在调用eureka-client的API接口“/hi”时希望做到轮流访问这两个实例,这时就需要将RestTemplate和Ribbon相结合,进行负载均衡。
首先需要在程序的IoC容器中注入一个 restTemplate的Bean,并在这个Bean上加上@LoadBalanced注解,此时RestTemplate就结合了Ribbon开启了负载均衡功能 ,代码如下:
@Configuration
public class RibbonConfig {
@Bean
@LoadBalanced
RestTemplate restTemplate(){
return new RestTemplate();
}
}
写一个RibbonService类,在该类的 hi()方法用 restTemplate 调用eureka-client的API接口,此时Uri上不需要使用硬编码(例如IP地址),只需要写服务名eureka-client 即可,代码如下:
@Service
public class RibbonService {
@Autowired
private RestTemplate restTemplate;
public String hi(String name) {
return restTemplate.getForObject("http://eureka-client/hi?name=" + name, String.class);
}
}
写一个RibbonController类,为该类加上@RestController注解,开启RestController的功能, 写一个“/hi” Get方法的接口,调用RibbonService类的hi()方法,代码如下:
@RestController
public class RibbonController {
@Autowired
private RibbonService ribbonService;
@GetMapping(value = "/hi")
public String hi(@RequestParam String name){
return ribbonService.hi(name);
}
}
启动eureka-ribbon-client工程,在浏览器上访问http://localhost:8671,显示的EurekaServer 的主界面如下图所示。在主界面上发现有两个服务被注册,分别为eureka-client和eureka-ribbon-client,其中eureka-client有两个实例,端口为8672和8673,而eureka-ribbon-client的端口为8674
在浏览器上多次访问 http://localhost:8674/hi?name=ben,浏览器会轮流显示如下内容:
hi ben, i am from port:8672
hi ben, i am from port:8673
四、LoadBalancerClient简介
负载均衡器的核心类为LoadBalancerClient, LoadBalancerCiient可以获取负载均衡的服务提供者的实例信息。为了演示,在RibbonController重新写一个接口“/testRibbon”,通过LoadBalancerCIient去选择一个eureka-client的服务实例的信息,并将该信息返回,继续在eureka-ribbon-client工程上修改,代码如下:
@RestController
public class RibbonController {
...//省略代码
@Autowired
private LoadBalancerClient loadBalancer;
@GetMapping ("/testRibbon")
public String testRibbon() {
ServiceInstance instance = loadBalancer.choose("eureka-client");
return instance.getHost() + ":" + instance.getPort();
}
}
重新启动工程,在浏览器上多次访问http://localhost:8764/testRibbon,浏览器会轮流显示如下内容 :
localhost:8672
localhost:8673
可见,LoadBalancerClient 的 choose(”eureka-client'’)方法可以轮流得到 eureka-client 的两个 服务实例的信息。
负载均衡器LoadBalancerClient是从EurekaClient获取服务注册列表信息的,并将服务注册列表信息缓存了一份。在LoadBalancerCJient 调用choose()方法时,根据负载均衡策略选择一个服务实例的信息,从而进行了负载均衡。 LoadBalancerClient也可以不从EurekaClient获取注册列表信息, 这时需要自己维护一份服务注册列表信息。需要修改application.xml的配置信息,通过stores.ribbon.listOfServers来配置这些服务实例的Uri。
#禁止Ribbon从Eureka获取注册列表信息
ribbon:
eureka:
enabled: false
#手动配置服务列表
stores:
ribbon:
listOfServers: example1.com,example2.com
@RestController
public class RibbonController {
@Autowired
private LoadBalancerClient loadBalancer;
@GetMapping ("/testSelfConfigRibbon")
public String testSelfConfigRibbon() {
ServiceInstance instance = loadBalancer.choose("stores");
return instance.getHost() + ":" + instance.getPort();
}
启动工程 ,在浏览器上多次访问内容:http://localhost:8769/testRibbon,浏览器会交替出现以下内容:
example1.com:80
example2.com:80
由此,我们知道在Ribbon中的负载均衡客户端为LoadBalancerClient。在SpringCloud项目中,负载均衡器Ribbon会默认从EurekaClient的服务注册列表中获取服务的信息,并缓存一份。根据缓存的服务注册列表信息,可以通过LoadBalancerClient来选择不同的服务实例, 从而实现负载均衡。如果禁止Ribbon从Eureka获取注册列表信息,则需要自己去维护一份服务注册列表信息。根据自己维护服务注册列表的信息,Ribbon也可以实现负载均衡。
五、源码解析Ribbon
为了深入理解Ribbon,通过查看源码来分析Ribbon如何和RestTemplate相结合来做负载均衡。开启负载均衡的关键在@LoadBalanced这个注解上,首先从这个注解入手。
@Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Qualifier
public @interface LoadBalanced {
}
上面是@LoadBalanced的定义,这就是一个普通的标记注解,作用就是修饰RestTemplate让其拥有负载均衡的能力,全局搜索发现在LoadBalancerAutoConfiguration.java这个类里用到了。
@Configuration
@ConditionalOnClass(RestTemplate.class)
@ConditionalOnBean(LoadBalancerClient.class)
@EnableConfigurationProperties(LoadBalancerRetryProperties.class)
public class LoadBalancerAutoConfiguration {
@LoadBalanced
@Autowired(required = false)
private List<RestTemplate> restTemplates = Collections.emptyList();
@Bean
public SmartInitializingSingleton loadBalancedRestTemplateInitializer(
final List<RestTemplateCustomizer> customizers) {
return new SmartInitializingSingleton() {
@Override
public void afterSingletonsInstantiated() {
for (RestTemplate restTemplate : LoadBalancerAutoConfiguration.this.restTemplates) {
for (RestTemplateCustomizer customizer : customizers) {
customizer.customize(restTemplate);
}
}
}
};
}
//这里的restTemplates是所有的被@LoadBalanced注解的集合,这就是标记注解的作用(Autowired是可以集合注入的)
@Autowired(required = false)
private List<LoadBalancerRequestTransformer> transformers = Collections.emptyList();
@Bean
@ConditionalOnMissingBean
public LoadBalancerRequestFactory loadBalancerRequestFactory(
LoadBalancerClient loadBalancerClient) {
return new LoadBalancerRequestFactory(loadBalancerClient, transformers);
}
//生成一个LoadBalancerInterceptor的Bean
@Configuration
@ConditionalOnMissingClass("org.springframework.retry.support.RetryTemplate")
static class LoadBalancerInterceptorConfig {
@Bean
public LoadBalancerInterceptor ribbonInterceptor(
LoadBalancerClient loadBalancerClient,
LoadBalancerRequestFactory requestFactory) {
return new LoadBalancerInterceptor(loadBalancerClient, requestFactory);
}
//给注解了@LoadBalanced的RestTemplate加上拦截器
@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的作用,当服务启动时,标记了的RestTemplate对象里面就会被自动加入LoadBalancerInterceptor拦截器,这样当RestTemplate像外面发起http请求时,会被LoadBalancerInterceptor的intercept函数拦截,而intercept里面又调用了LoadBalancerClient接口实现类execute方法,我们接着往下看;
public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor {
...//省略部分代码
@Override
public ClientHttpResponse intercept(final HttpRequest request, final byte[] body,
final ClientHttpRequestExecution execution) throws IOException {
//这是以服务名为地址的原始请求:例:http://HI-SERVICE/hi
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));
}
}
这里的LoadBalancerClient的实现是RibbonLoadBalancerClient,调用的是RibbonLoadBalancerClient.execute()方法。在execute内首先执行getLoadBalancer(serviceId)获取ILoadBalancer的实现者,然后调用getServer(loadBalancer)方法通过负载均衡策略获取服务。
@Override
public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException {
//通过serviceId找到ILoadBalancer的实现者
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);
}
继续看getServer(loadBalancer)方法,发现是调用ILoadBalancer实现类对象的chooseServer()方法。
protected Server getServer(ILoadBalancer loadBalancer) {
if (loadBalancer == null) {
return null;
}
return loadBalancer.chooseServer("default"); // TODO: better handling of key
}
这里ILoadBalancer接口有三个实现类,通过查看源码发现,BaseLoadBalancer和ZoneAwareLoadBalancer类里都有具体的实现方法,到底调用的是哪个类的方法呢?
查看RibbonClientConfiguration.java类发现如下代码:
@Bean
@ConditionalOnMissingBean
public ILoadBalancer ribbonLoadBalancer(IClientConfig config,
ServerList<Server> serverList, ServerListFilter<Server> serverListFilter,
IRule rule, IPing ping) {
if (this.propertiesFactory.isSet(ILoadBalancer.class, name)) {
return this.propertiesFactory.get(ILoadBalancer.class, config, name);
}
ZoneAwareLoadBalancer<Server> balancer = LoadBalancerBuilder.newBuilder()
.withClientConfig(config).withRule(rule).withPing(ping)
.withServerListFilter(serverListFilter).withDynamicServerList(serverList)
.buildDynamicServerListLoadBalancer();
return balancer;
}
由此可知,拦截器里默认调用的是ZoneAwareLoadBalancer.chooseServer()方法。
public Server chooseServer(Object key) {
//ENABLED默认值为true
if (!ENABLED.get() || getLoadBalancerStats().getAvailableZones().size() <= 1) {
logger.debug("Zone aware logic disabled or there is only one zone");
return super.chooseServer(key);
}
Server server = null;
try {
LoadBalancerStats lbStats = getLoadBalancerStats();
Map<String, ZoneSnapshot> zoneSnapshot = ZoneAvoidanceRule.createSnapshot(lbStats);
logger.debug("Zone snapshots: {}", zoneSnapshot);
if (triggeringLoad == null) {
triggeringLoad = DynamicPropertyFactory.getInstance().getDoubleProperty(
"ZoneAwareNIWSDiscoveryLoadBalancer." + this.getName() + ".triggeringLoadPerServerThreshold", 0.2d);
}
if (triggeringBlackoutPercentage == null) {
triggeringBlackoutPercentage = DynamicPropertyFactory.getInstance().getDoubleProperty(
"ZoneAwareNIWSDiscoveryLoadBalancer." + this.getName() + ".avoidZoneWithBlackoutPercetage", 0.99999d);
}
Set<String> availableZones = ZoneAvoidanceRule.getAvailableZones(zoneSnapshot, triggeringLoad.get(), triggeringBlackoutPercentage.get());
logger.debug("Available zones: {}", availableZones);
if (availableZones != null && availableZones.size() < zoneSnapshot.keySet().size()) {
String zone = ZoneAvoidanceRule.randomChooseZone(zoneSnapshot, availableZones);
logger.debug("Zone chosen: {}", zone);
if (zone != null) {
BaseLoadBalancer zoneLoadBalancer = getLoadBalancer(zone);
server = zoneLoadBalancer.chooseServer(key);
}
}
} catch (Exception e) {
logger.error("Error choosing server using zone aware logic for load balancer={}", name, e);
}
if (server != null) {
return server;
} else {
logger.debug("Zone avoidance logic is not invoked.");
return super.chooseServer(key);
}
}
但是由于我是模拟单服务器测试的,所以是单区域,通过调试可以看到空间数为1,如下图。所以这里会去调用ZoneAwareLoadBalancer父类的chooseServer()方法,也就是BaseLoadBalancer的chooseServer()方法。
//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的实现类如下:
IRule的默认实现类有以下7种。在大多数情况下,这些默认的实现类是可以满足需求的,如果有特殊的谛求,可以自己实现。
- BestAvailableRule: 选择最小请求数。
- ClientConfigEnabledRoundRobinRule:轮询。
- RandornRule: 随机选择一个server。
- RoundRobinRule: 轮询选择server。
- RetryRule: 根据轮询的方式重试。
- ZoneAvoidanceRule:根据server的zone区域和可用性来轮询选择。
- WeightedResponseTirneRule: 根据响应时间去分配一个weight,weight越低,被选择的可能性就越低。
综上所述,Ribbon的负载均衡,主要通过LoadBalancerClient来实现的,而LoadBalancerClient具体交给了ILoadBalancer来处理,ILoadBalancer通过配置IRule等信息,并向EurekaClient获取注册列表的信息,得到注册列表后,ILoadBalancer根据IRule的策略进行负载均衡。
RestTemplate 被@LoadBalance注解后,能使用负载均衡,主要是维护了一个被@LoadBalance注解的RestTemplate列表,并给列表中的RestTemplate添加拦截器,进而交给负载均衡器去处理。