Spring Cloud-Ribbon负载均衡
简述
Spring Cloud Netflix Ribbon是Spring Cloud Netflix子项目的核心组件之一,主要给服务间调用及API网关转发提供负载均衡的功能。
Ribbon简介
在微服务架构中,每个服务都会部署多个节点,当有服务调用该服务时,如何保证负载均衡是个不得不考虑的问题。负载均衡可以增加系统的可用性和扩展性,当我们使用RestTemplate来调用其他服务时,Ribbon可以很方便的实现负载均衡的功能。
RestTemplate的使用
RestTemplate是一个HTTP客户端,使用它我们可以方便的调用HTTP接口,支持GET、POST、PUT、DELETE等方法。
GET请求方法
public <T> T getForObject(String url, Class<T> responseType, Object... uriVariables)
public <T> T getForObject(String url, Class<T> responseType, Map<String, ?> uriVariables)
public <T> T getForObject(URI url, Class<T> responseType)
public <T> ResponseEntity<T> getForEntity(String url, Class<T> responseType, Object... uriVariables)
public <T> ResponseEntity<T> getForEntity(String url, Class<T> responseType, Map<String, ?> uriVariables)
public <T> ResponseEntity<T> getForEntity(URI url, Class<T> responseType)
getForObject方法
返回对象为响应体封装对象。
示例如下:
@GetMapping("/{id}")
public Result getUser(@PathVariable Long id) {
return restTemplate.getForObject(userServiceUrl + "/user/{1}", Result.class, id);
}
getForEntity方法
返回对象为ResponseEntity对象,包含响应头、响应状态码、响应体等。
示例如下:
@GetMapping("/getByUsername")
public Result getUserByUsername(@RequestParam String username) {
ResponseEntity<Result> responseEntity = restTemplate.getForEntity(userServiceUrl + "/user/getByUsername?username={1}", Result.class, username);
if (responseEntity.getStatusCode().is2xxSuccessful()) {
return responseEntity.getBody();
} else {
return new Result("操作失败", 500);
}
}
POST请求方法
public <T> T postForObject(String url, @Nullable Object request, Class<T> responseType, Object... uriVariables)
public <T> T postForObject(String url, @Nullable Object request, Class<T> responseType, Map<String, ?> uriVariables)
public <T> T postForObject(URI url, @Nullable Object request, Class<T> responseType)
public <T> ResponseEntity<T> postForEntity(String url, @Nullable Object request, Class<T> responseType, Object... uriVariables)
public <T> ResponseEntity<T> postForEntity(String url, @Nullable Object request, Class<T> responseType, Map<String, ?> uriVariables)
public <T> ResponseEntity<T> postForEntity(URI url, @Nullable Object request, Class<T> responseType)
方法与GET请求方法类似。
postForObject方法示例:
@PostMapping("/insert")
public Result insert(@RequestBody User user) {
return restTemplate.postForObject(userServiceUrl + "/user/insert", user, Result.class);
}
postForEntity方法示例:
@PostMapping("/insert")
public Result insert(@RequestBody User user) {
return restTemplate.postForEntity(userServiceUrl + "/user/insert", user, Result.class).getBody();
}
PUT请求方法
public void put(String url, @Nullable Object request, Object... uriVariables)
public void put(String url, @Nullable Object request, Map<String, ?> uriVariables)
public void put(URI url, @Nullable Object request)
PUT方法示例:
@PutMapping("/update")
public Result update(@RequestBody User user) {
restTemplate.put(userServiceUrl + "/user/update", user);
return Result.isSuccess();
}
DELETE请求方法
public void delete(String url, Object... uriVariables)
public void delete(String url, Map<String, ?> uriVariables)
public void delete(URI url)
DELETE方法示例:
@DeleteMapping("/delete/{id}")
public Result delete(@PathVariable Long id) {
restTemplate.delete(userServiceUrl + "/user/delete/{id}", id);
return Result.isSuccess();
}
创建user-service模块(被调用方)
创建user-service模块,用于给ribbon服务提供调用。
pom.xml添加对应依赖
如下所示:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
application.yml配置端口及注册中心地址
如下所示:
# 指定服务端口
server:
port: 8201
# 指定服务名称
spring:
application:
name: user-service
# 指定注册地址
eureka:
client:
# 指定是否从注册中心获取服务
fetch-registry: true
# 指定是否将服务注册到注册中心
register-with-eureka: true
service-url:
# 配置注册中心地址
defaultZone: http://localhost:8001/eureka/
创建基类及接口类
User类:
public class User {
private Long id;
private String username;
private String password;
// setter和getter方法省略
}
Result类:
public class Result<T> {
private String message;
private int code;
private T data;
public Result(){}
public Result(String message, int code) {
this.message = message;
this.code = code;
}
public Result(String message, int code, T data) {
this.message = message;
this.code = code;
this.data = data;
}
public static Result isSuccess() {
return new Result("操作成功", 200);
}
public Result isSuccess(T data) {
return new Result("操作成功", 200, data);
}
//setter和getter方法省略
}
UserController类:
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@PostMapping("/insert")
public Result insert(@RequestBody User user) {
userService.insert(user);
return Result.isSuccess();
}
@GetMapping("/get/{id}")
public Result getUser(@PathVariable Long id) {
User user = userService.getUser(id);
return new Result().isSuccess(user);
}
@PostMapping("/update")
public Result update(@RequestBody User user) {
userService.update(user);
return Result.isSuccess();
}
@DeleteMapping("/delete/{id}")
public Result delete(@PathVariable Long id) {
userService.delete(id);
return Result.isSuccess();
}
}
UserService接口类:
public interface UserService {
public int insert(User user);
public User getUser(Long id);
public List<User> listUser(User user);
public int update(User user);
public int delete(Long id);
}
UserServiceImpl实现类:
@Service
public class UserServiceImpl implements UserService {
private List<User> userList = new ArrayList<>();
Logger logger = LoggerFactory.getLogger(UserServiceImpl.class);
@Override
public int insert(User user) {
logger.info("新增成功...");
userList.add(user);
return 1;
}
@Override
public User getUser(Long id) {
logger.info("查询用户...");
for (User user : userList) {
if (user.getId().longValue() == id) {
return user;
}
}
return null;
}
@Override
public List<User> listUser(User user) {
logger.info("查询用户列表...");
return userList;
}
@Override
public int update(User user) {
for (User dbUser : userList) {
if (user.getId().longValue() == dbUser.getId()) {
userList.remove(dbUser);
userList.add(user);
break;
}
}
logger.info("更新成功...");
return 1;
}
@Override
public int delete(Long id) {
for (User user : userList) {
if (user.getId().longValue() == id) {
userList.remove(user);
}
}
logger.info("删除成功...");
return 1;
}
}
创建ribbon-service模块(调用方)
pom.xml添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
注意:由于eureka-client3.0版本默认集成了ribbon,故无需单独引入ribbon依赖;若eureka-client版本为3.0以下,则还需引入netflix-ribbon依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
<version>2.2.5.RELEASE</version>
</dependency>
application.yml配置端口及注册中心地址
如下所示:
# 指定服务端口
server:
port: 8301
# 指定服务名称
spring:
application:
name: ribbon-service
# 指定主机地址
eureka:
client:
# 指定是否从注册中心获取服务
fetch-registry: true
# 指定是否将服务注册到注册中心
register-with-eureka: true
service-url:
# 配置注册中心地址
defaultZone: http://localhost:8001/eureka/
service-url:
user-service: http://user-service
新增RibbonConfig配置类
使用@LoadBalanced注解赋予RestTemplate负载均衡的能力,如下所示:
@Configuration
public class RibbonConfig {
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
可以看到,使用Ribbon的负载均衡功能非常简单,和直接使用RestTemplate没什么两样,只需给RestTemplate添加@LoadBalanced注解即可。
添加接口类:
@RestController
@RequestMapping("/user/ribbon")
public class UserRibbonController {
@Autowired
private RestTemplate restTemplate;
@Value("${service-url.user-service}")
private String userServiceUrl;
@PostMapping("/insert")
public Result insert(@RequestBody User user) {
return restTemplate.postForObject(userServiceUrl + "/user/insert", user, Result.class);
}
@GetMapping("/{id}")
public Result getUser(@PathVariable Long id) {
return restTemplate.getForObject(userServiceUrl + "/user/{1}", Result.class, id);
}
@GetMapping("/list")
public List<Result> listUser() {
return restTemplate.getForObject(userServiceUrl + "/user/list", List.class);
}
@GetMapping("/getByUsername")
public Result getUserByUsername(@RequestParam String username) {
ResponseEntity<Result> responseEntity = restTemplate.getForEntity(userServiceUrl + "/user/getByUsername?username={1}", Result.class, username);
if (responseEntity.getStatusCode().is2xxSuccessful()) {
return responseEntity.getBody();
} else {
return new Result("操作失败", 500);
}
}
@PutMapping("/update")
public Result update(@RequestBody User user) {
restTemplate.put(userServiceUrl + "/user/update", user);
return Result.isSuccess();
}
@DeleteMapping("/delete/{id}")
public Result delete(@PathVariable Long id) {
restTemplate.delete(userServiceUrl + "/user/delete/{id}", id);
return Result.isSuccess();
}
}
负载功能测试
启动eureka-server于8001端口;
启动user-service于8201端口;
启动另外一个user-service为8202端口,复制启动类配置,添加server.port=8202启动即可;
启动完之后,刷新注册中心可查询启动的服务,如下图示:
uaa-service接口测试:
使用postman测试CRUD相关接口;
示例-获取用户详情(uaa-service直接访问):
http://localhost:8201/user/100
响应:
{
"message": "操作成功",
"code": 200,
"data": {
"id": 100,
"username": "admin",
"password": "123456"
}
}
ribbon-service接口测试:
使用postman测试CRUD相关接口;
示例-获取用户详情(ribbon-service调用user-service访问):
http://localhost:8301/user/ribbon/100
响应:
{
"message": "操作成功",
"code": 200,
"data": {
"id": 100,
"username": "admin",
"password": "123456"
}
}
多次通过ribbon请求上述接口,发现user-service两个控制台,日志交替打印如下所示:
UserServiceApplication控制台日志:
2022-12-07 22:09:40.069 INFO 91332 --- [nio-8201-exec-9] c.w.s.userservice.UserServiceImpl : 查询用户...
2022-12-07 22:09:43.797 INFO 91332 --- [io-8201-exec-10] c.w.s.userservice.UserServiceImpl : 查询用户...
2022-12-07 22:09:46.608 INFO 91332 --- [nio-8201-exec-7] c.w.s.userservice.UserServiceImpl : 查询用户...
UserServiceApplication(1)控制台日志:
2022-12-07 22:09:33.864 INFO 59524 --- [nio-8202-exec-2] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet'
2022-12-07 22:09:33.864 INFO 59524 --- [nio-8202-exec-2] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
2022-12-07 22:09:33.864 INFO 59524 --- [nio-8202-exec-2] o.s.web.servlet.DispatcherServlet : Completed initialization in 0 ms
2022-12-07 22:09:33.881 INFO 59524 --- [nio-8202-exec-2] c.w.s.userservice.UserServiceImpl : 查询用户...
2022-12-07 22:09:42.296 INFO 59524 --- [nio-8202-exec-4] c.w.s.userservice.UserServiceImpl : 查询用户...
2022-12-07 22:09:45.333 INFO 59524 --- [nio-8202-exec-3] c.w.s.userservice.UserServiceImpl : 查询用户...
Ribbon常用配置
全局配置
ribbon:
ConnectTimeout: 1000 #服务请求连接超时时间(毫秒)
ReadTimeout: 3000 #服务请求处理超时时间(毫秒)
OkToRetryOnAllOperations: true #对超时请求启用重试机制
MaxAutoRetriesNextServer: 1 #切换重试实例的最大个数
MaxAutoRetries: 1 # 切换实例后重试最大次数
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule #修改负载均衡算法
对指定服务配置
与全局配置的区别是:ribbon节点挂载服务名称下面,如下是ribbon-service调用user-service时的单独配置。
user-service:
ribbon:
ConnectTimeout: 1000 #服务请求连接超时时间(毫秒)
ReadTimeout: 3000 #服务请求处理超时时间(毫秒)
OkToRetryOnAllOperations: true #对超时请求启用重试机制
MaxAutoRetriesNextServer: 1 #切换重试实例的最大个数
MaxAutoRetries: 1 # 切换实例后重试最大次数
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule #修改负载均衡算法
Ribbon的负载均衡策略
所谓的负载均衡策略,就是当A服务调用B服务时,此时B服务有多个实例,这时A服务以何种方式来选择调用的B实例,ribbon可以选择以下几种负载均衡策略。
- com.netflix.loadbalancer.RandomRule:随机策略
- com.netflix.loadbalancer.RoundRobinRule:轮询策略
- com.netflix.loadbalancer.RetryRule:在轮询策略的基础上添加重试机制;
- com.netflix.loadbalancer.WeightedResponseTimeRule:对轮询策略的扩展,响应速度越快的示例,选择权重越大,越容易被选择;
- com.netflix.loadbalancer.BestAvailableRule:选择并发较小的实例;
- com.netflix.loadbalancer.AvailabilityFilteringRule:先过滤掉故障实例,再选择并发较小的实例;
- com.netflix.loadbalancer.ZoneAwareLoadBalanver:采用双重过滤,同时过滤不是同一区域的实例和故障实例,再选择并发较小的实例;
(完)