在微服务架构中,各个微服务虽然是独立运行,独立部署,但是各个微服务间也是存在相互调用通信的情况。而在SpringCloud中通常使用Http Rest的调用方式来实现服务间的通信。
Spring框架提供的RestTemplate类可用于在应用中调用rest服务,它简化了与http服务的通信方式,统一了RESTFUL的标准,封装了http链接, 我们只需要传入url及返回值类型即可。相较于之前常用的HttpClient,RestTemplate是一种更优雅的调用RESTFUL服务的方式。所以我们也先来使用RestTemplate实现服务间的通信。注意的是,以下使用的服务注册中心是Eureka。
User 代表用户服务 端口号为8999
Product 代表商品服务 端口号为9000
<!--springboot web依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--eureka client 依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
</dependencies>
#User服务配置
server:
port: 8999
spring:
application:
name: USER
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka #指定服务注册中心的地址
-----------------------------------------------------------------
#Product服务配置
server:
port: 9000
spring:
application:
name: PRODUCT
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka #指定服务注册中心的地址
@SpringBootApplication
@EnableEurekaClient //开启eureka客户端
public class ProductApplication {
public static void main(String[] args){
SpringApplication.run(ProductApplication.class, args);
}
}
-----------------------------------------------------------------
@SpringBootApplication
@EnableEurekaClient //开启eureka客户端
public class UserApplication {
public static void main(String[] args){
SpringApplication.run(UserApplication.class, args);
}
}
在Product服务中编写一段简单的测试业务代码:
@RestController
@Slf4j
public class ProductController {
@Value("${server.port}")
private String port;
@GetMapping("/product/find")
public String find(){
log.info("商品服务调用成功,端口为{}", port);
return "服务调用成功,服务提供端口: " + port;
}
}
@RestController
@Slf4j
public class UserController {
@GetMapping("/user")
public String user(){
log.info("用户服务调用成功...");
//使用RestTemplate调用product服务
RestTemplate restTemplate = new RestTemplate();
//使用RestTemplate向商品服务发送Http请求调用
String forObject = restTemplate.getForObject("http://localhost:9000/product/find", String.class);
return forObject;
}
}
完成上述操作后,先启动服务注册中心服务,然后启动User和Product服务。在浏览器进入http://localhost:8761,可以发现这两个服务已经注册到Eureka服务注册中心,
同时,在浏览器进入http://localhost:8999/user,可以查看服务调用结果,用户服务已经成功调用了商品服务中的方法。
RestTemplate是直接基于服务地址调用,并没有在服务注册中心获取服务,也没有办法完成服务的负载均衡,如果需要实现服务的负载均衡需要自己书写服务负载均衡策略,或者使用Ribbon组件。
负载均衡,关于它的定义解释网络上有很多的博客帖子介绍,比如这篇文章:负载均衡。其实通俗的来讲,负载均衡就是尽力将网络流量平均分发到多个服务器上,以提高系统整体的响应速度和可用性,不会让一个服务器处理无限制地处理任务。对于上面使用RestTemplate进行服务间的通信,它并不能实现服务间的负载均衡,所以我们可以自己定义负载均衡策略。
为了更能体现负载均衡的策略,我们可以将Product服务拷贝两份,并分配不同的端口并启动。首先选中需要拷贝的服务ProductApplication,然后右键单击选中Copy Configuration:
拷贝的服务需要给定服务名,同时也需要给定端口,但是端口注意要不相同:
拷贝完服务之后,依次启动商品服务和用户服务,将用户服务运行多次,即可发现调用的商品服务并不是都是一样的,而是随机分配的。
当然以上的做法只是提供一个负载均衡的参考,实际的开发过程中,我们这考虑的问题还不是很周全的。比如负载均衡的策略过于单一,仅仅是采取一种随机数的策略,大多数场景中这并不适用,同时我们也无法对服务进行健康检查,不能确保服务是否可以调用。
官方地址:https://github.com/Netflix/ribbon
Spring Cloud Ribbon是一个基于HTTP和TCP的客户端负载均衡工具,它基于Netflix Ribbon实现。通过Spring Cloud的封装,可以让我们轻松地将面向服务的REST模版请求自动转换成客户端负载均衡的服务调用。
Ribbon实现负载均衡,其主要原理就是根据调用服务的id去服务注册中心拉取服务id的服务列表,并将服务列表拉取到本地进行缓存,然后在本地通过默认的轮询的负载均衡策略在现有的列表中选择一个可用的节点提供服务。当然默认是轮询的策略,这是可以根据需求改变的。
如果使用的是Eureka服务注册中心,那么无须引入依赖,因为在eureka中默认集成了Ribbon组件:
当然如果使用的client中没有Ribbon依赖,那么就需要显示地引入依赖:
<!--引入ribbon依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
使用LoadBalanceClient形式调用
@Autowired
private LoadBalancerClient loadBalancerClient;
@GetMapping("/user")
public String user(){
log.info("用户服务调用成功....");
//根据负载均衡策略选取一个服务调用
ServiceInstance product = loadBalancerClient.choose("PRODUCT");
log.info("服务端口:[{}]",product.getPort());
log.info("服务地址:[{}]",product.getUri());
//使用RestTemplate调用product服务
RestTemplate restTemplate = new RestTemplate();
String forObject = restTemplate.getForObject("http://" + product.getHost() + ":" + product.getPort() + "/product/find",
String.class);
return forObject;
}
启动服务注册中心,并依次启动用户服务以及商品服务,浏览器进入http://localhost:8761可以查看相应服务已经注册到服务中心,
同时在控制台也能观察到User服务对Product服务进行调用的结果,从结果来看确实实现了负载均衡,但是它也是存在缺点的,每次的调用都需要先获取一个负载均衡机器再通过RestTemplate调用服务。
使用@LoadBalanced调用
在User服务中创建一个配置类,使用工厂模式管理RestTemplate,避免每次调用都需要重新创建一个RestTemplate对象,
@Configuration
public class RestFactory {
@Bean
@LoadBalanced //使对象具有Ribbon的负载均衡的特性
public RestTemplate getRestTemplate(){
return new RestTemplate();
}
}
//将RestTemplate注入
@Autowired
private RestTemplate restTemplate;
@GetMapping("/user")
public String user(){
log.info("用户服务调用成功....");
//使用RestTemplate调用product服务,使用服务ID获取
String forObject = restTemplate.getForObject("http://PRODUCT/product/find", String.class);
return forObject;
}
启动程序运行服务同样可以实现调用负载均衡。对于@LoadBalanced实现负载均衡,其实主要的工作原理还是对添加该注解的RestTemplate会被内部LoadBalancerInterceptor拦截器拦截处理,将请求的服务名转换为具体的访问地址,再发起请求,具体的可以看看这篇文章:@LoadBalanced。
对于Ribbon的默认负载均衡策略,我们可以通过对用LoadBalanceClient形式调用的代码进行debug源码追踪。在LoadBalanceClient的代码当中,我们使用loadBalancerClient.choose去获取对应服务id的其中一个服务,所以进入choose的方法当中,对于choose的实现方法,最终会进入到RibbonLoadBalancerClient的实现类,
进入choose的实现方法中,我们再进入this.getServer的方法,查看其获取服务的过程,之后再次
进入chooseServer的方法中,对于chooseServer有三个实现类,采用debug确定进入的是
ZoneAwareLoadBalancer这个实现类,
最终在return中进入到 BaseLoadBalancer.class中的chooseServer方法中,
继续往下调试,我们也不难发现,key的值是default,在最后的choose中确定了负载均衡策略,而且是默认的策略rule,我们在代码定义中可以发现rule变量,其中就包括了默认的负载均衡策略,
就是RoundRobinaRule,即轮询策略。
Ribbon除了默认的轮询策略,还有其他几种策略一样可以配置使用。对于其他的负载均衡策略,我们同样可以在代码中寻找到,有了上面的源码探索,我们可以知道负载策略的最高层级的接口是IRule,对于其实现类在这使用的是AbstractLoadBalancerRule.class,因此我们可以将它们的逻辑图展示出来,
在这对这七种策略进行简单的介绍:
RandomRule | 随机策略 会随机选择服务 |
RoundRobinRule | 轮询策略 按顺序循环选择服务 |
RetryRule | 重试策略 先按照RoundRobinRule的策略获取服务,如果获取失败则在指定时间内进行重试,获取可用的服务 |
PredicateBasedRule | 它先通过内部定义的一个过滤器过滤出一部分服务实例清单,然后再采用线性轮询的方式从过滤出来的结果中选取一个服务实例 |
BestAviableRule | 最低并发策略 会先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,然后选择一个并发量最小的服务 |
WeightedResponseTimeRule | 响应时间加权策略 根据平均响应的时间计算所有服务的权重,响应时间越快服务权重越大被选中的概率越高,刚启动时如果统计信息不足,则使用RoundRobinRule策略,等统计信息足够会切换回来 |
AvailabilityFilteringRule | 可用过滤策略 会先过滤由于多次访问故障而处于断路器跳闸状态的服务,还有并发的连接数量超过阈值的服务,然后对剩余的服务列表按照轮询策略进行访问 |
ZoneAvoidanceRule | ZoneAvoidanceRule中的过滤条件是以ZoneAvoidancePredicate为主过滤条件和以AvailabilityPredicate为次过滤条件组成的一个叫做CompositePredicate的组合过滤条件,过滤成功之后,继续采用线性轮询的方式从过滤结果中选择一个出来 |
Ribbon虽然定义了默认的负载均衡策略,但是我们同样可以去修改它的策略配置实现自己的需求。同时需要注意,以下提到的方法只是对局部有效,并不作用于全局。
#修改服务默认的负载均衡策略样例
PRODUCT:
ribbon:
#修改为随机策略
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
------------------------------------------------------------------------------------
serverName: #代表要调用的服务名
ribbon:
#xxxx代表需要替换的上述提到的七种策略的其他策略名
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.xxxx
服务间的调用我们已经可以使用RestTemplate及Ribbon完成,但是对于它们的使用还是存在一些不足:
- 1.每次调用服务都需要写类似代码,存在大量的代码冗余
- 2.服务地址如果修改,维护成本增高
- 3.使用时不够灵活
所以OpenFeign组件的引入可以使得服务间的调用更高效。
官网地址:OpenFeign
Feign是一个声明式的伪Http客户端,它使得写Http客户端变得更简单。使用Feign,只需要创建一个接口并注解。它具有可插拔的注解特性(可以使用springmvc的注解),可使用Feign 注解和JAX-RS注解。Feign支持可插拔的编码器和解码器。Feign默认集成了Ribbon,默认实现了负载均衡的效果并且SpringCloud为Feign添加了SpringMVC注解的支持。
在服务的调用方加入OpenFeign的依赖,本例中则是在User服务中添加该依赖。
<!--Open Feign依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
@SpringBootApplication
@EnableEurekaClient //开启eureka客户端
@EnableFeignClients //开启Feign支持
public class UserApplication {
public static void main(String[] args){
SpringApplication.run(UserApplication.class, args);
}
}
在User服务中创建一个包存专门放Feign调用接口,同时在其中创建调用接口:
//value属性用来指定:调用服务名称
@FeignClient(value = "PRODUCT")
public interface ProductClient {
@GetMapping("/product/find") 书写服务调用路径
public String find();
}
@RestController
@Slf4j
public class UserController {
@Autowired
private ProductClient productClient;
@GetMapping("/user")
public String user(){
log.info("用户服务调用成功....");
String msg = productClient.find();
return msg;
}
}
默认情况下,OpenFiegn在进行服务调用时,要求服务提供方处理业务逻辑时间必须在1S内返回,如果超过1S没有返回则OpenFeign会直接报错,不会等待服务执行,但是往往在处理复杂业务逻辑是可能会超过1S,因此需要修改OpenFeign的默认服务调用超时时间。
#xxxx代表被调用的服务名
feign.client.config.xxxx.connectTimeout=5000 #配置指定服务连接超时
feign.client.config.xxxx.readTimeout=5000 #配置指定服务等待超时
feign.client.config.default.connectTimeout=5000 #配置所有服务连接超时
feign.client.config.default.readTimeout=5000 #配置所有服务等待超时