Spring Cloud 服务消费者(rest+ribbon)
一、Ribbon
在微服务架构中,业务都会被拆分成一个独立的服务,服务与服务的通讯是基于http restful的。Spring Cloud有两种微服务调用方式:①ribbon+restTemplate ②feign
本篇主要是将ribbon+restTemplate
ribben这个东西比较复杂,我们先简单说一下:
Spring Cloud Ribbon 是一个基于Http和TCP的客服端负载均衡工具,它是基于Netflix Ribbon实现的。它不像服务注册中心、配置中心、API网关那样独立部署,但是它几乎存在于每个微服务的基础设施中。包括前面的提供的声明式服务调用也是基于该Ribbon实现的。
RestTemplate是Spring自己提供的对象,不是新的内容。读者不知道RestTemplate可以查看相关的文档。
二、简单实例
1.新建MAVEN项目,加入依赖
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>test</groupId>
<artifactId>java-maven-user</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>service-ribbon</name>
<description>Demo project for Spring Boot</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.2.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>
</properties>
<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>
<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>Dalston.RC1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
</project>
2.配置文件
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
server:
port: 8764
spring:
application:
name: service-ribbon
在工程的启动类中,通过@EnableDiscoveryClient向服务中心注册;并且向程序的ioc注入一个bean: restTemplate;并通过@LoadBalanced注解表明这个restRemplate开启负载均衡的功能。package test;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
@SpringBootApplication
@EnableDiscoveryClient
public class ServiceRibbonApplication {
public static void main(String[] args) {
SpringApplication.run(ServiceRibbonApplication.class, args);
}
@Bean
@LoadBalanced
RestTemplate restTemplate() {
return new RestTemplate();
}
}
3.测试类
写一个测试类HelloService,通过之前注入ioc容器的restTemplate来消费service-hi服务的“/hi”接口,在这里我们直接用的程序名替代了具体的url地址,在ribbon中它会根据服务名来选择具体的服务实例,根据服务实例在请求的时候会用具体的url替换掉服务名,代码如下:
package test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
@Service
public class HelloService {
@Autowired
RestTemplate restTemplate;
public String hiService(String name) {
return restTemplate.getForObject("http://SERVICE-HI/hi?name="+name,String.class);
}
}
写一个controller,在controller中用调用HelloService 的方法,代码如下:package test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloControler {
@Autowired
HelloService helloService;
@RequestMapping(value = "/hi")
public String hi(@RequestParam String name){
return helloService.hiService(name);
}
}
4.发送请求
http://localhost:8764/hi?name=heidou,可以看到页面:
hi heidou,i am from port:8763
hi heidou,i am from port:8762 交替出现
三、原理分析
最基本原理;
普通使用RestTemplate请求其他服务时,内部使用的就是常规的http请求实例发送请求。
为RestTemplate增加了@LoanBalanced 注解后,实际上通过LoadBalancerInterceptor对HTTP请求进行拦截,并利用负载均衡器LoadBalancerClient将以逻辑用户名为host的URI转换为具体的服务实例地址。
再用Ribbon实现负载均衡器的时候,实际上使用的是Ribbon中定义的ILoadBalancer接口的实现,默认是ZoneAwareLoadBalancer。
1.负载均衡分类:
⑴服务端负债均衡
①硬件负债均衡,F5
②软件负债均衡,Nginx
⑵客户端负载均衡
所有客户端结点都维护者自己要访问的服务端清单。
在微服务使用客户端负载均衡很简单,只需要:
①服务提供者注册到注册中心
②服务消费者直接调用@LoadBalanced注解修饰过的RestTemplate来实现面向服务的接口调用。
2.RestTemplate
该对象会使用Ribbon的自动化配置,同时通过配置@LoadBalanced开启客户端负载均衡
几种主要的请求类型:
⑴GET请求
⑵POST请求
⑶PUT请求
⑷DELETE请求
3.源码分析
⑴LoadBalancerClient接口
从这个接口我们看到负载均衡器所需要具备的能力:
①根据传入的服务名serviceId,从负载均衡器中挑选一个对应服务的实例。
②使用挑选出的实例执行请求内容
③为系统构建一个合适的host:port形式的URI
⑵LoadBalancerAutoConfiguration客户端负载均衡器的自动化配置类
先贴源码:
根据类头的注解我们可以看出,自动化配置需要满足两个条件:
①RestTemplate类必须存在于当前工程中
②在Spring的Bean工程中必须有LoadBalancerClient的实现Bean
在这个类中,主要做了三件事:
①创建LoadBalancerInterceptor的Bean来实现对客户端发起请时进行拦截,以实现客户端的负载均衡。
②创建RestTemplateCustomizer的Bean来给RestTemplate增加LoadBalancerInterceptor拦截器
③维护一个被@LoadBalanced注解修饰的对象列表,并在这里进行初始化,通过调用RestTemplateCustomizer的实例来给需要客户端负载均衡的RestTemplate增加
LoadBalancerInterceptor拦截器
⑶LoadBalancerInterceptor
我们来看看LoadBalancerInterceptor拦截器如何将一个普通RestTemplate变成客户端负载均衡的
从代码中可知,我们注入了LoadBalancerClient的实现
当一个被@LoadBalanced注解修饰的RestTemplate对象向外发起HTTP请求时,会被intercept函数拦截,可以通过getHost()拿到服务名,
然后通过execute发起实际请求
然后我们找到LoadBalancerClient的实现Ribbon-LoadBalancerClient
步骤:
①可以看到第一步就是给句serviceId获得具体的服务实例
这里使用了Netfilx Ribbon自身的ILoadBalancer接口中的chooseServer函数(通过某种策略,从负载均衡器中挑选出一个具体的服务实例)
SpringCloud默认采用ZoneAwareLoadBalancer来实现负载均衡器
②获取到Server之后,增加ServerId等内容对它进行包装
③调用LoadBalancerRequest的apply方法,向具体服务实例发送请求。这样就实现了从host的URI请求到host:post形式的实例访问地址的转换。
我们看看apply方法传入的参数:
我们传入的是RibbonService:
它除了server对象之外还存储了服务名,是否HTTPS标识以及一个MAP类型的元数据集合。
我们具体分析下apply这个函数:
先看第一句:
重新构建了URI
再看第二句:
这里有个request.getURI()方法,会调用之前的ServiceRequestWrapper对象中重写的getURI函数。它会使用
RibbonLoadBalancerClient中实现的reconstructURI来组织具体请求的服务实例地址。
看最后一句话,这个方法从Server对象中获取HOST和PORT信息,然后和URI对象进行整合,最终形成要访问的实例地址。
4.负载均衡器
这里我们主要是分析ILoadBalancer接口的具体实现类
⑴AbstractLoadBalancer
这个是ILoadBalancer的抽象实现。
①这里边有个分组的枚举:
ALL:所有服务实例
STATUS_UP:正常服务实例
STATUS_NOT_UP:挂掉的服务实例
②gerServerList():根据分组类型获取不同的服务实例列表。
③getLoadBalancerStats():定义了获取 LoadBalancerStats的方法
④LoadBalancerStats是负载均衡器存储各个实例当前的属性和统计信息。
⑵BaseLoadBalancer
是Ribbon负载均衡器的基础实现类,定义了很多关于负载均衡器的基本内容。
①定义了两个服务列表,一个是所有的,一个是正常的
②BaseLoadBalancer中定义了一个IPingStrategy,用来描述服务检查策略,IPingStrategy默认实现采用了SerialPingStrategy实现,SerialPingStrategy中的pingServers方法就是遍历所有的服务实例,一个一个发送请求,查看这些服务实例是否还有效,如果网络环境不好的话,这种检查策略效率会很低,如果我们想自定义检查策略的话,可以重写SerialPingStrategy的pingServers方法。
③在BaseLoadBalancer的chooseServer方法中(负载均衡的核心方法),我们发现最终调用了IRule中的choose方法来找到一个具体的服务实例,IRule是一个接口,在BaseLoadBalancer它的默认实现是RoundRobinRule类,RoundRobinRule类中采用了最常用的线性负载均衡规则,也就是所有有效的服务端轮流调用。
④在BaseLoadBalancer的构造方法中会启动一个PingTask,这个PingTask用来检查Server是否有效,PingTask的默认执行时间间隔为10秒。
⑤markServerDown方法用来标记一个服务是否有效,标记方式为调用Server对象的setAlive方法设置isAliveFlag属性为false。
⑥getReachableServers方法用来获取所有有效的服务实例列表。
⑦getAllServers方法用来获取所有服务的实例列表。
⑧addServers方法表示向负载均衡器中添加一个新的服务实例列表。
⑶DynamicServerListLoadBalancer
它继承了BaseLoadBalancer,实现了服务清单在运行期的动态更新能力。它还具备了对服务实例清单的过滤功能。也就是说我们可以通过过滤器来选择性的获取一批实例服务清单。
①首先DynamicServerListLoadBalancer类一开始就声明了一个变量serverListImpl,serverListImpl变量的类型是一个ServerList<T extends Server>,这里的泛型得是Server的子类,ServerList是一个接口,里边定义了两个方法:一个getInitialListOfServers用来获取初始化的服务实例清单;另一个getUpdatedListOfServers用于获取更新的服务实例清单。
②ServerList接口有很多实现类,DynamicServerListLoadBalancer默认使用了DomainExtractingServerList类作为ServerList的实现,但是在DomainExtractingServerList的构造方法中又传入了DiscoveryEnabledNIWSServerList对象,查看源码发现最终两个清单的获取方式是由DiscoveryEnabledNIWSServerList类来提供的。
③DomainExtractingServerList类中的obtainServersViaDiscovery方法是用来发现服务实例并获取的,obtainServersViaDiscovery方法的主要逻辑是这样:首先依靠EurekaClient从服务注册中心获取到具体的服务实例InstanceInfo列表,然后对这个列表进行遍历,将状态为UP的实例转换成DiscoveryEnabledServer对象并放到一个集合中,最后将这个集合返回。
④DynamicServerListLoadBalancer中还定义了一个ServerListUpdater.UpdateAction类型的服务更新器,Spring Cloud提供了两种服务更新策略:一种是PollingServerListUpdater,表示定时更新;另一种是EurekaNotificationServerListUpdater表示由Eureka的事件监听来驱动服务列表的更新操作,默认的实现策略是第一种,即定时更新,定时的方式很简单,创建Runnable,调用DynamicServerListLoadBalancer中updateAction对象的doUpdate方法,Runnable延迟启动时间为1秒,重复周期为30秒。
⑤在更新服务清单的时候,调用了我们在第一点提到的getUpdatedListOfServers方法,拿到实例清单之后,又调用了一个过滤器中的方法进行过滤。过滤器的类型有好几种,默认是DefaultNIWSServerListFilter,这是一个继承自ZoneAffinityServerListFilter的过滤器,具有区域感知功能。即它会对服务提供者所处的Zone和服务消费者所处的Zone进行比较,过滤掉哪些不是同一个区域的实例。
⑷ZoneAwareLoadBalancer
ZoneAwareLoadBalancer是DynamicServerListLoadBalancer的子类,ZoneAwareLoadBalancer的出现主要是为了弥补DynamicServerListLoadBalancer的不足。由于DynamicServerListLoadBalancer中并没有重写chooseServer方法,所以DynamicServerListLoadBalancer中负责均衡的策略依然是我们在BaseLoadBalancer中分析出来的线性轮询策略,这种策略不具备区域感知功能,这样当需要跨区域调用时,可能会产生高延迟。ZoneAwareLoadBalancer重写了setServerListForZones方法,该方法在其父类中的功能主要是根据区域Zone分组的实例列表,为负载均衡器中的LoadBalancerStats对象创建ZoneStats并存入集合中,ZoneStats是一个用来存储每个Zone的状态和统计信息。重写之后的setServerListForZones方法主要做了两件事:一件是调用getLoadBalancer方法来创建负载均衡器,同时创建服务选择策略;另一件是对Zone区域中的实例清单进行检查,如果对应的Zone下已经没有实例了,则将Zone区域的实例列表清空,防止节点选择时出现异常。
5.Spring Cloud中的负载均衡策略
IRule
这是所有负载均衡策略的父接口,里边的核心方法就是choose方法,用来选择一个服务实例。
AbstractLoadBalancerRule
AbstractLoadBalancerRule是一个抽象类,里边主要定义了一个ILoadBalancer,就是我们上文所说的负载均衡器,负载均衡器的功能我们在上文已经说的很详细了,这里就不再赘述,这里定义它的目的主要是辅助负责均衡策略选取合适的服务端实例。
RandomRule
看名字就知道,这种负载均衡策略就是随机选择一个服务实例,看源码我们知道,在RandomRule的无参构造方法中初始化了一个Random对象,然后在它重写的choose方法又调用了choose(ILoadBalancer lb, Object key)这个重载的choose方法,在这个重载的choose方法中,每次利用random对象生成一个不大于服务实例总数的随机数,并将该数作为下标所以获取一个服务实例。
RoundRobinRule
RoundRobinRule这种负载均衡策略叫做线性负载均衡策略,也就是我们在上文所说的BaseLoadBalancer负载均衡器中默认采用的负载均衡策略。这个类的choose(ILoadBalancer lb, Object key)函数整体逻辑是这样的:开启一个计数器count,在while循环中遍历服务清单,获取清单之前先通过incrementAndGetModulo方法获取一个下标,这个下标是一个不断自增长的数先加1然后和服务清单总数取模之后获取到的(所以这个下标从来不会越界),拿着下标再去服务清单列表中取服务,每次循环计数器都会加1,如果连续10次都没有取到服务,则会报一个警告No available alive servers after 10 tries from load balancer: XXXX。
RetryRule
看名字就知道这种负载均衡策略带有重试功能。首先RetryRule中又定义了一个subRule,它的实现类是RoundRobinRule,然后在RetryRule的choose(ILoadBalancer lb, Object key)方法中,每次还是采用RoundRobinRule中的choose规则来选择一个服务实例,如果选到的实例正常就返回,如果选择的服务实例为null或者已经失效,则在失效时间deadline之前不断的进行重试(重试时获取服务的策略还是RoundRobinRule中定义的策略),如果超过了deadline还是没取到则会返回一个null。
WeightedResponseTimeRule
WeightedResponseTimeRule是RoundRobinRule的一个子类,在WeightedResponseTimeRule中对RoundRobinRule的功能进行了扩展,WeightedResponseTimeRule中会根据每一个实例的运行情况来给计算出该实例的一个权重,然后在挑选实例的时候则根据权重进行挑选,这样能够实现更优的实例调用。WeightedResponseTimeRule中有一个名叫DynamicServerWeightTask的定时任务,默认情况下每隔30秒会计算一次各个服务实例的权重,权重的计算规则也很简单,如果一个服务的平均响应时间越短则权重越大,那么该服务实例被选中执行任务的概率也就越大。
ClientConfigEnabledRoundRobinRule
ClientConfigEnabledRoundRobinRule选择策略的实现很简单,内部定义了RoundRobinRule,choose方法还是采用了RoundRobinRule的choose方法,所以它的选择策略和RoundRobinRule的选择策略一致,不赘述。
BestAvailableRule
BestAvailableRule继承自ClientConfigEnabledRoundRobinRule,它在ClientConfigEnabledRoundRobinRule的基础上主要增加了根据loadBalancerStats中保存的服务实例的状态信息来过滤掉失效的服务实例的功能,然后顺便找出并发请求最小的服务实例来使用。然而loadBalancerStats有可能为null,如果loadBalancerStats为null,则BestAvailableRule将采用它的父类即ClientConfigEnabledRoundRobinRule的服务选取策略(线性轮询)。
PredicateBasedRule
PredicateBasedRule是ClientConfigEnabledRoundRobinRule的一个子类,它先通过内部定义的一个过滤器过滤出一部分服务实例清单,然后再采用线性轮询的方式从过滤出来的结果中选取一个服务实例。
ZoneAvoidanceRule
ZoneAvoidanceRule是PredicateBasedRule的一个实现类,只不过这里多一个过滤条件,ZoneAvoidanceRule中的过滤条件是以ZoneAvoidancePredicate为主过滤条件和以AvailabilityPredicate为次过滤条件组成的一个叫做CompositePredicate的组合过滤条件,过滤成功之后,继续采用线性轮询的方式从过滤结果中选择一个出来。
推荐阅读
-
使用Spring Cloud Feign作为HTTP客户端调用远程HTTP服务的方法(推荐)
-
Spring Cloud Gateway 之请求坑位[微服务IP不同请求会失败]
-
详解Spring Cloud Consul 实现服务注册和发现
-
Spring-cloud 注册服务提供者搭建方法
-
Spring-cloud 服务发现与消费(以ribbon为例)
-
Spring Cloud开发人员如何解决服务冲突和实例乱窜?(IP实现方案)
-
详解Spring Cloud Gateway基于服务发现的默认路由规则
-
SpringCloud之服务注册与发现Spring Cloud Eureka实例代码
-
Spring Cloud Alibaba | Sentinel: 服务限流高级篇
-
Spring Cloud第七篇 | 声明式服务调用Feign