Spring cloud ribbon是一个基于HTTP和TCP的客户端负载均衡工具,基于Netflix Ribbon实现。通过spring cloud的封装,可以让我们轻松地将面向服务的REST模板请求自动转换成客户端负载均衡的服务调用。
微服务的调用,API网关的请求转发等内容,实际上都是通过Ribbon实现的,Feign也是基于Ribbon实现的工具,所以对于spring cloud ribbon的理解和使用,对于使用spring cloud来构建微服务非常重要。
负载均衡是对系统高可用、网络压力的缓解和处理能力扩容的重要手段之一。我们通常说的负载均衡指的是服务端负载均衡,软件是通过在服务器上安装一些具有均衡负载功能或模块的软件来完成请求分发工作。会维护一个下挂可用的服务端清单,通过心跳检测来剔除故障的服务端节点保证清单中都是可以正常访问的服务端节点。当客户端发送请求到负载均衡设备的时候,该设备以某种算法(比如线性轮询,按权重负载、按流量负载等)从维护的可用服务端清单中取出一台服务端地址,进行转发。
客户端负载均衡和服务端负载均衡最大不同点在于服务清单所存储的位置。客户端负载均衡中,所有客户端节点维护者自己要访问的服务端清单,这些服务端清单来自于服务注册中心。同服务端负载均衡架构类似,在客户端负载均衡中也需要心跳取维护服务端清单的健康性,只是这个步骤需要与服务注册中心配合完成。在spring cloud实现的服务治理框架中,默认会创建针对各个服务治理框架的Ribbon自动化整合配置。
通过spring cloud ribbon的封装,在微服务架构中使用客户端负载均衡调用非常简单,只需要如下两步:
1. 服务提供者只需要启动多个服务实例并注册到一个注册中心或是多个相关联的注册中心
2. 服务消费者直接通过调用被@LoadBalanced注解修饰过的RestTemplate来实现面向服务的接口调用
这样就可以将服务提供者的高可用以及服务消费者的负载均衡一起实现了。
(此实例可以看之前的ribbon-consumer)
该对象会使用Ribbon的自动化配置,同时通过配置@LoadBalanced还能够开启客户端负载均衡。以下演示针对几种不同请求类型和参数类型的服务调用实现。
RestTemplate中,对GET请求可以通过如下两个方法进行调用实现(源码中的返回用了HttpMethod.GET)
返回ResponseEntity,该对象是spring 对HTTP请求相应的封装,主要存储了HTTP的几个重要元素,比如HTTP请求状态码的枚举对象HttpStatus,它的父类HttpEntity中还存储着HTTP请求的头信息对象HttpHeaders以及泛型类型的请求体对象
public ResponseEntity getForEntity(String url, Class responseType, Object... uriVariables)throws RestClientException;
在服务提供者中加一个请求:
@RequestMapping("/findUser")
public User findUser(){
User user = userService.findOneUser(1);
return user;
}
启动注册中心eureka-server,启动服务提供者client-service
在服务消费者ribbon-consumer中增加请求(主类参考之前的服务消费者实例中,这里不重复):
@RestController
public class ConsumerController {
@Autowired
RestTemplate restTemplate;
//当然你需要吧服务提供者的User类考到消费者项目下
@RequestMapping(value="/ribbon-consumer1",method=RequestMethod.GET)
public User findUser() {
ResponseEntity responseEntity = restTemplate.getForEntity("http://HELLO-SERVICE/user/findUser", User.class);
return responseEntity.getBody();
}
}
启动服务消费者,访问http://cc-pc:9001/ribbon-consumer1
{“id”:1,”userName”:”cc”,”password”:”123456”,”mobilePhone”:”18800000000”,”address”:”北京”,”role”:1,”note”:”我天,我们有了第一个用户”}
可以传递参数
我们改造一下服务提供者中请求:
@RequestMapping("/findUser")
public User findUser(Integer id){
User user = userService.findOneUser(id);
return user;
}
再改造一下服务消费者请求:
@RequestMapping(value="/ribbon-consumer1",method=RequestMethod.GET)
public User findUser(Integer id) {
ResponseEntity responseEntity = restTemplate.getForEntity("http://HELLO-SERVICE/user/findUser?id={1}", User.class, id);
return responseEntity.getBody();
}
重新编译启动服务提供者,消费者,访问http://cc-pc:9001/ribbon-consumer1?id=1
{“id”:1,”userName”:”cc”,”password”:”123456”,”mobilePhone”:”18800000000”,”address”:”北京”,”role”:1,”note”:”我天,我们有了第一个用户”}
这个函数还提供了重载的方法1
@Override
public ResponseEntity getForEntity(String url, Class responseType, Map<String, ?> uriVariables)
throws RestClientException;
ribbon-consumer添加请求:
@RequestMapping(value="/ribbon-consumer2",method=RequestMethod.GET)
public User findUser2(Integer id) {
Map<String,Integer> params = new HashMap<String,Integer>();
params.put("id", 1);
ResponseEntity<User> responseEntity = restTemplate.getForEntity("http://HELLO-SERVICE/user/findUser?id={id}", User.class, params);
return responseEntity.getBody();
}
访问http://cc-pc:9001/ribbon-consumer2?id=1
{“id”:1,”userName”:”cc”,”password”:”123456”,”mobilePhone”:”18800000000”,”address”:”北京”,”role”:1,”note”:”我天,我们有了第一个用户”}
这个函数还提供了重载的方法2
@Override
public ResponseEntity getForEntity(URI url, Class responseType) throws RestClientException;
ribbon-consumer添加请求:
@RequestMapping(value="/ribbon-consumer3",method=RequestMethod.GET)
public User findUser3(Integer id) {
UriComponents uriComponents = UriComponentsBuilder.fromUriString("http://HELLO-SERVICE/user/findUser?id={id}").build()
.expand(1).encode();
URI uri = uriComponents.toUri();
ResponseEntity responseEntity = restTemplate.getForEntity(uri, User.class);
return responseEntity.getBody();
}
http://cc-pc:9001/ribbon-consumer3?id=1
{“id”:1,”userName”:”cc”,”password”:”123456”,”mobilePhone”:”18800000000”,”address”:”北京”,”role”:1,”note”:”我天,我们有了第一个用户”}
这个函数就不用再找body来找对象了,直接就返回了。
和上面一样有三个重载的方法:
@RequestMapping(value="/ribbon-consumer11",method=RequestMethod.GET)
public User findUser11(Integer id) {
User user = restTemplate.getForObject("http://HELLO-SERVICE/user/findUser?id={1}", User.class, id);
return user;
}
@RequestMapping(value="/ribbon-consumer12",method=RequestMethod.GET)
public User findUser12(Integer id) {
Map<String,Integer> params = new HashMap<String,Integer>();
params.put("id", 1);
User user = restTemplate.getForObject("http://HELLO-SERVICE/user/findUser?id={id}", User.class, params);
return user;
}
@RequestMapping(value="/ribbon-consumer13",method=RequestMethod.GET)
public User findUser13(Integer id) {
UriComponents uriComponents = UriComponentsBuilder.fromUriString("http://HELLO-SERVICE/user/findUser?id={id}").build()
.expand(1).encode();
URI uri = uriComponents.toUri();
User user= restTemplate.getForObject(uri, User.class);
return user;
}
访问http://cc-pc:9001/ribbon-consumer11?id=1等,均返回了。
与GET请求的getForEntity类似,下面这个例子提交User对象,返回String
服务提供者增加:
@RequestMapping("/findUserName")
public String findUserName(User user) {
return "cc -->"+user.toString();//为了看参数
}
服务消费者增加:
@RequestMapping(value="/ribbon-consumer21",method=RequestMethod.POST)
public String findUserName21(String name) {
User user = new User();
user.setId(1);
user.setUserName("cc");
user.setPassword("123456");
ResponseEntity responseEntity = restTemplate.postForEntity("http://HELLO-SERVICE/user/findUserName?id={1}&userName={2}&password={3}",
null,String.class, user.getId(),user.getUserName(),user.getPassword());
return responseEntity.getBody();
}
因为这里是post方式请求,浏览器url写路径是行不通了,我们这里简单写一个静态页面,就放在static下面了,如果写动态页面参照之前的springboot模板引擎。
Static文件夹下创建cc.html:
<html>
<head>
<title>cc练习页面title>
head>
<body>
<form action="/ribbon-consumer21" method="post">
<input type="submit" value="Post请求练习21"/>
form>
body>
html>
分别启动,访问http://cc-pc:9001/cc.html
点击后显示:
类似GET方法,有类似的重载方法:
@Override
public ResponseEntity postForEntity(String url, Object request, Class responseType, Map uriVariables)
throws RestClientException ;
@Override
public ResponseEntity postForEntity(URI url, Object request, Class responseType) throws RestClientException ;
不再一一列举。
与getForObject类似,也是直接返回对象使用不用再处理,也有重载的方法类似。
public T postForObject(String url, Object request, Class responseType, Object... uriVariables)
throws RestClientException;
@RequestMapping(value="/ribbon-consumer31",method=RequestMethod.POST)
public String findUserName31() {
User user = new User();
user.setId(1);
user.setUserName("cc");
user.setPassword("123456");
String str = restTemplate.postForObject("http://HELLO-SERVICE/user/findUserName?id={1}&userName={2}&password={3}",
null,String.class, user.getId(),user.getUserName(),user.getPassword());
return str;
}
cc–>User [id=1, userName=cc, password=123456]
post请求提交资源,返回资源的URI,因为该URI就相当于制定了返回类型,所以此方法实现的POST请求不需要再指定responseType,也有类似的重载方法
@Override
public URI postForLocation(String url, Object request, Object... uriVariables) throws RestClientException;
这个方法我老是执行不通。。。可以用下别的方法= =
PUT请求和DELETE请求省略,可自行去观看。。。
源码中还可以看到exchange和execute,他们是普通写法,可以使用,方法类似,具体可参考spring官网。
ribbon.<key>=<value>
格式进行配置,例如
ribbon.ConnnectTimeout=250
全局配置可以作为默认值理解,当客户端配置了相应key值,将覆盖全局配置
<client>.ribbon.<key>=<value>
例如我们之前的restTemplate.getForObject(“http://HELLO-SERVICE/user/findUser?id={id}”, User.class, params)方法中的实例名称即第一个client
HELLO-SERVICE.ribbon.listOfServers=localhost:8001,localhost:8002
Key和value可以通过查看com.netflix.client.config.CommonClientConfigKey来获得
(可参考以下网址http://javadox.com/com.netflix.ribbon/ribbon-core/2.0-RC4/com/netflix/client/config/CommonClientConfigKey.html)
此时,spring cloud会触发Eurek中实现的对Ribbon的自动化配置。我们的配置将会变得非常简单,Eureka会为我们维护所有服务的实例清单,不用去通过指定ribbon的参数来指定具体的服务实例清单。当然如果你想使用ribbon的配置来维护清单,可以配置禁用eureka的清单维护:ribbon.eureka.enabled=false.
对于ribbon的参数配置,依然可采用之前介绍的两种配置方式来实现。
Spring cloud ribbon默认实现了区域亲和策略,所以,我们可以通过Eureka实例的元数据配置来实现区域化的实例配置方案。比如,可以将处于不同机房的实例配置成不同的区域值,作为跨区域的容错机制实现。只需在服务实例的元数据中增加zone参数来指定自己所在的区域
Eureka.instance.metadataMap.zone=shanghai
通过简单的配置,原来那些通过RestTemplate实现的服务访问就会自动根据配置来实现重试策略。
# open the retry function
spring.cloud.loadbalancer.retry.enabled=true
# hystrix'time must be more larger than the ribbon's,or the retry is not useful
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=10000
# connect timeout
hello-service.ribbon.ConnectTimeout=250
# handle timeout
hello-service.ribbon.ReadTimeout=1000
# is it for all the operations to retry
hello-service.ribbon.OkToRetryOnAllOperation=true
# change time for alter service
hello-service.ribbon.MaxAutoRetriesNextServer=2
# the time for now service to change time
hello-service.ribbon.MaxAutoRetries=1
以上配置后,当访问故障请求的时候,会再次尝试访问一次当前实例,如果不行,就换一个实例进行访问,如果不行,再换一个,还不行,返回失败信息。