Ribbon
概述在微服务架构中,业务都会被拆分成一个独立的服务,服务与服务的通讯是基于http restful
的。Spring Cloud
有两种服务调用方式,一种是Ribbon+restTemplate
,另一种是Feign
,本文介绍使用Spring Cloud Ribbon
在客户端负载均衡的调用服务。
Ribbon
是一个客户端负载均衡器,可以简单的理解成类似于 nginx
的负载均衡模块的功能,Feign
默认集成了Ribbon
。
主流的LB
方案可分成两类:
LB
,即在服务的消费方和提供方之间使用独立的LB
设施(可以是硬件,如F5
, 也可以是软件,如nginx),由该设施负责把访问请求通过某种策略转发至服务的提供方;LB
,将LB
逻辑集成到消费方,消费方从服务注册中心获知有哪些地址可用,然后自己再从这些地址中选择出一个合适的服务器。Ribbon
就属于后者,它只是一个类库,集成于消费方进程,消费方通过它来获取到服务提供方的地址。Ribbon
的架构图如下所示:
在这里给普及一下有哪些负载均衡算法:
1、简单轮询负载均衡(RoundRobin)
以轮询的方式依次将请求调度不同的服务器,即每次调度执行i = (i + 1) mod n
,并选出第i
台服务器。
2、随机负载均衡 (Random)
随机选择状态为UP
的Server
3、加权响应时间负载均衡(WeightedResponseTime)
根据相应时间分配一个weight
,相应时间越长,weight
越小,被选中的可能性越低。
4、区域感知轮询负载均衡(ZoneAvoidanceRule)
复合判断server
所在区域的性能和server
的可用性选择server
Ribbon
自带负载均衡策略比较策略名 | 策略声明 | 策略描述 | 实现说明 |
---|---|---|---|
BestAvailableRule | public class BestAvailableRule extends ClientConfigEnabledRoundRobinRule | 选择一个最小的并发请求的server | 逐个考察Server,如果Server被tripped了,则忽略,在选择其中ActiveRequestsCount最小的server |
AvailabilityFilteringRule | public class AvailabilityFilteringRule extends PredicateBasedRule | 过滤掉那些因为一直连接失败的被标记为circuit tripped的后端server,并过滤掉那些高并发的的后端server(active connections 超过配置的阈值) | 使用一个AvailabilityPredicate来包含过滤server的逻辑,其实就就是检查status里记录的各个server的运行状态 |
WeightedResponseTimeRule | public class WeightedResponseTimeRule extends RoundRobinRule | 根据相应时间分配一个weight,相应时间越长,weight越小,被选中的可能性越低。 | 一个后台线程定期的从status里面读取评价响应时间,为每个server计算一个weight。Weight的计算也比较简单responsetime 减去每个server自己平均的responsetime是server的权重。当刚开始运行,没有形成statas时,使用roubine策略选择server。 |
RetryRule | public class RetryRule extends AbstractLoadBalancerRule | 对选定的负载均衡策略机上重试机制。 | 在一个配置时间段内当选择server不成功,则一直尝试使用subRule的方式选择一个可用的server |
RoundRobinRule | public class RoundRobinRule extends AbstractLoadBalancerRule | roundRobin方式轮询选择server | 轮询index,选择index对应位置的server |
RandomRule | public class RandomRule extends AbstractLoadBalancerRule | 随机选择一个server | 在index上随机,选择index对应位置的server |
ZoneAvoidanceRule | public class ZoneAvoidanceRule extends PredicateBasedRule | 复合判断server所在区域的性能和server的可用性选择server | 使用ZoneAvoidancePredicate和AvailabilityPredicate来判断是否选择某个server,前一个判断判定一个zone的运行性能是否可用,剔除不可用的zone(的所有server),AvailabilityPredicate用于过滤掉连接数过多的Server。 |
建一个服务消费者,重新新建一个spring-boot
工程,取名为:service-ribbon
;
在它的pom.xml
继承了父pom
文件,并引入了以下依赖:
<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.0modelVersion>
<groupId>com.forezpgroupId>
<artifactId>service-ribbonartifactId>
<version>0.0.1-SNAPSHOTversion>
<packaging>jarpackaging>
<name>service-ribbonname>
<description>Demo project for Spring Bootdescription>
<parent>
<groupId>com.forezpgroupId>
<artifactId>sc-f-chapter2artifactId>
<version>0.0.1-SNAPSHOTversion>
parent>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-ribbonartifactId>
dependency>
dependencies>
project>
在工程的配置文件指定服务的注册中心地址为http://localhost:8761/eureka/
,程序名称为 service-ribbon
,程序端口为8764
。配置文件application.yml
如下:
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
server:
port: 8764
spring:
application:
name: service-ribbon
在工程的启动类中,通过@EnableDiscoveryClient
向服务中心注册;并且向程序的ioc
注入一个bean: restTemplate
;并通过@LoadBalanced
注解表明这个restRemplate
开启负载均衡的功能。
@SpringBootApplication
@EnableEurekaClient
@EnableDiscoveryClient
public class ServiceRibbonApplication {
public static void main(String[] args) {
SpringApplication.run( ServiceRibbonApplication.class, args );
}
@Bean
@LoadBalanced
RestTemplate restTemplate() {
return new RestTemplate();
}
}
写一个测试类HelloService
,通过之前注入ioc
容器的restTemplate
来消费service-hi
服务的“/hi”
接口,在这里我们直接用的程序名替代了具体的url
地址,在ribbon
中它会根据服务名来选择具体的服务实例,根据服务实例在请求的时候会用具体的url
替换掉服务名,代码如下:
@Service
public class HelloService {
@Autowired
RestTemplate restTemplate;
public String hiService(String name) {
//String url = "http://localhost:8990/drce/hi?name="+name,String.class)";
return restTemplate.getForObject("http://SERVICE-HI/hi?name="+name,String.class);
}
}
写一个controller
,在controller
中用调用HelloService
的方法,代码如下:
@RestController
public class HelloControler {
@Autowired
HelloService helloService;
@GetMapping(value = "/hi")
public String hi(@RequestParam String name) {
return helloService.hiService( name );
}
}
在浏览器上多次访问http://localhost:8764/hi?name=forezp
,浏览器交替显示:
hi forezp,i am from port:8762
hi forezp,i am from port:8763
这说明当我们通过调用restTemplate.getForObject(“http://SERVICE-HI/hi?name=”+name,String.class)
方法时,已经做了负载均衡,访问了不同的端口的服务实例。
eureka server
,端口为8761service-hi
工程跑了两个实例,端口分别为8762,8763
,分别向服务注册中心注册sercvice-ribbon
端口为8764
,向服务注册中心注册sercvice-ribbon
通过restTemplate
调用service-hi
的hi
接口时,因为用ribbon
进行了负载均衡,会轮流的调用service-hi
:8762
和8763
两个端口的hi
接口;RestTemplate
用法详解RestTemplate
简介RestTemplate
是从 Spring3.0
开始支持的一个 HTTP
请求工具,它提供了常见的REST
请求方案的模版,例如 GET
请求、POST
请求、PUT
请求、DELETE
请求以及一些通用的请求执行方法exchange
以及execute
。RestTemplate
继承自 InterceptingHttpAccessor
并且实现了 RestOperations
接口,其中RestOperations
接口定义了基本的 RESTful
操作,这些操作在 RestTemplate
中都得到了实现。接下来我们就来看看这些操作方法的使用。
Spring
中如何使用Rest
资源借助 RestTemplate
,Spring
应用能够方便地使用REST
资源,Spring
的 RestTemplate
访问使用了模版方法的设计模式。
模版方法将过程中与特定实现相关的部分委托给接口,而这个接口的不同实现定义了接口的不同行为。
RestTemplate
定义了36
个与REST
资源交互的方法,其中的大多数都对应于HTTP
的方法。其实,这里面只有11
个独立的方法,其中有十个有三种重载形式,而第十一个则重载了六次,这样一共形成了36
个方法。
delete()
在特定的URL
上对资源执行HTTP DELETE
操作
exchange()
在URL
上执行特定的HTTP
方法,返回包含对象的ResponseEntity
,这个对象是从响应体中映射得到的
execute()
在URL
上执行特定的HTTP
方法,返回一个从响应体映射得到的对象
getForEntity()
发送一个HTTP GET
请求,返回的ResponseEntity
包含了响应体所映射成的对象
getForObject()
发送一个HTTP GET
请求,返回的请求体将映射为一个对象
postForEntity()
POST
数据到一个URL
,返回包含一个对象的ResponseEntity
,这个对象是从响应体中映射得到的
postForObject()
POST
数据到一个URL
,返回根据响应体匹配形成的对象
headForHeaders()
发送HTTP HEAD
请求,返回包含特定资源URL
的HTTP
头
optionsForAllow()
发送HTTP OPTIONS
请求,返回对特定URL
的Allow
头信息
postForLocation()
POST
数据到一个URL
,返回新创建资源的URL
put()
PUT
资源到特定的URL
实际上,由于Post
操作的非幂等性,它几乎可以代替其他的CRUD
操作
Get
请求RestTemplate
的get
方法有以上几个,可以分为两类: getForEntity()
和 getForObject()
首先看 getForEntity()
的返回值类型ResponseEntity
ResponseEntity getForEntity()
看一下 ResponseEntity
的文档描述:
它继承了HttpEntity
,封装了返回的响应信息,包括响应状态、响应头和响应体.
在测试之前我们首先 创建一个Rest
服务,模拟提供Rest
数据,这里给出Controller
层代码,具体可以查看源码,文章最后会给出:
@RestController
public class UserController {
@Autowired
private UserService userService;
@RequestMapping(value = "getAll")
public List<UserEntity> getUser() {
List<UserEntity> list = userService.getAll();
return list;
}
@RequestMapping("get/{id}")
public UserEntity getById(@PathVariable(name = "id") String id) {
return userService.getById(id);
}
@RequestMapping(value = "save")
public String save(UserEntity userEntity) {
return "保存成功";
}
@RequestMapping(value = "saveByType/{type}")
public String saveByType(UserEntity userEntity,@PathVariable("type")String type) {
return "保存成功,type="+type;
}
}
getForEntity
1、无参数的 getForEntity
方法
@RequestMapping("getForEntity")
public List<UserEntity> getAll2() {
ResponseEntity<List> responseEntity = restTemplate.getForEntity("http://localhost/getAll", List.class);
HttpHeaders headers = responseEntity.getHeaders();
HttpStatus statusCode = responseEntity.getStatusCode();
int code = statusCode.value();
List<UserEntity> list = responseEntity.getBody();
System.out.println(list.toString());
return list;
}
2、有参数的 getForEntity
请求,参数列表,可以使用{}
进行url
路径占位符
//有参数的 getForEntity 请求,参数列表
@RequestMapping("getForEntity/{id}")
public UserEntity getById2(@PathVariable(name = "id") String id) {
ResponseEntity<UserEntity> responseEntity = restTemplate.getForEntity("http://localhost/get/{id}", UserEntity.class, id);
UserEntity userEntity = responseEntity.getBody();
return userEntity;
}
3、有参数的 get
请求,使用map
封装参数
//有参数的 get 请求,使用map封装参数
@RequestMapping("getForEntity/{id}")
public UserEntity getById4(@PathVariable(name = "id") String id) {
HashMap<String, String> map = new HashMap<>();
map.put("id",id);
ResponseEntity<UserEntity> responseEntity = restTemplate.getForEntity("http://localhost/get/{id}", UserEntity.class, map);
UserEntity userEntity = responseEntity.getBody();
return userEntity;
}
通过断点调试我们看下 返回的 responseEntity
的信息如图:
因此我们可以获取Http
请求的全部信息。
但是,通常情况下我们并不想要Http
请求的全部信息,只需要相应体即可.对于这种情况,RestTemplate
提供了 getForObject()
方法用来只获取 响应体信息.
getForObject
和getForEntity
用法几乎相同,指示返回值返回的是 响应体,省去了我们 再去getBody()
。
getForObject
1、无参数的 getForObject
请求
//无参数的 getForObject 请求
@RequestMapping("getAll2")
public List<UserEntity> getAll() {
List<UserEntity> list = restTemplate.getForObject("http://localhost/getAll", List.class);
System.out.println(list.toString());
return list;
}
2、有参数的getForObject
请求,使用参数列表
//有参数的 getForObject 请求
@RequestMapping("get2/{id}")
public UserEntity getById(@PathVariable(name = "id") String id) {
UserEntity userEntity = restTemplate.getForObject("http://localhost/get/{id}", UserEntity.class, id);
return userEntity;
}
3、有参数的 get
请求,使用map
封装请求参数
//有参数的 get 请求,使用map封装请求参数
@RequestMapping("get3/{id}")
public UserEntity getById3(@PathVariable(name = "id") String id) {
HashMap<String, String> map = new HashMap<>();
map.put("id",id);
UserEntity userEntity = restTemplate.getForObject("http://localhost/get/{id}", UserEntity.class, map);
return userEntity;
}
Post
请求了解了get
请求后,Post
请求就变得很简单了,我们可以看到post
有如下方法:
postForEntity
1、post
请求,保存 UserEntity
对像
//post 请求,提交 UserEntity 对像
@RequestMapping("saveUser")
public String save(UserEntity userEntity) {
ResponseEntity<String> responseEntity = restTemplate.postForEntity("http://localhost/save", userEntity, String.class);
String body = responseEntity.getBody();
return body;
}
浏览器访问:http://localhost/saveUser?username=itguang&password=123456&age=20&email=123@123.com
我们再次断点调试,查看 responseEntity
中的信息:
2、有参数的postForEntity
请求
// 有参数的 postForEntity 请求
@RequestMapping("saveUserByType/{type}")
public String save2(UserEntity userEntity,@PathVariable("type")String type) {
ResponseEntity<String> responseEntity = restTemplate.postForEntity("http://localhost/saveByType/{type}", userEntity, String.class, type);
String body = responseEntity.getBody();
return body;
}
// 有参数的 postForEntity 请求,使用map封装
@RequestMapping("saveUserByType2/{type}")
public String save3(UserEntity userEntity,@PathVariable("type")String type) {
HashMap<String, String> map = new HashMap<>();
map.put("type", type);
ResponseEntity<String> responseEntity = restTemplate.postForEntity("http://localhost/saveByType/{type}", userEntity, String.class,map);
String body = responseEntity.getBody();
return body;
}
我们浏览器访问:localhost/saveUserByType/[email protected]
就会返回:保存成功,type=120
对与其它请求方式,由于不常使用,所以这里就不再讲述。