客户端的负载均衡 spring cloud ribbon
Spring Cloud Ribbon 是一个基于HTTP 和 TCP客户端的负载均衡工具。服务端的调用 api网关的请求转发都是基于ribbon来实现的。 Feign也是基于ribbon实现的。
客户端的负载均衡:
负载均衡是一个对系统的高可用,网络压力的缓解和处理能力扩容的重要手段之一,我们通常所说的负载均衡是服务端的负载均衡,其中包括硬件的负载均衡和软件的负载均衡,硬件是在服务器节点之间安装专门用于负载均衡的设备例如F5,软件的负载均衡是在服务器上安装一些具有负载均衡功能或者模块的工具来完成请求分发工作,例如:nginx。
负载均衡的设备在下方挂一个可用的服务器清单,通过心跳检测和剔除故障的服务端节点,通过某一种算法(线性轮询、按权重分配、按流量负载等)从服务器清单列表中选择一台服务器进行请求的转发。
与服务器端的负载均衡不同,客户端的负载均衡服务器的清单存储在客户端,所有的客户端节点都保存着服务器清单,而这些清单来源于注册中心,同时客户端也需要通过心跳机制来检测服务器的可用性,只是需要与注册中心配合完成。
在SpringCloud中该如何使用?
通过Spring Cloud Ribbon 的封装,我们只需要:
- 客户端服务提供者启动多个实例,并注册到注册中心。
- 服务消费者通过@LoadBalanced注解修饰过的RestTemplate来实现向服务端的调用。
实际使用简单例子:
idea是一个很好的开发工具,我们可以在创建项目的时候就选择好,例如在新建项目的时候
这样可以避免出现Spring Cloud中子项目的版本和Spring Boot版本出现不一致的状况。
在当前的小例子中我使用的Spring Boot的版本是 2.0.8 对应出现的Spring Cloud版本为 Finchley SR2。
在这里我们模仿使用一个服务的注册中心,一个服务的提供者,一个服务的消费者。
下面为测试的实际案例:
注册中心:
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<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>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.8.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.wc.study.eureka</groupId>
<artifactId>eureka_server01</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>eureka_server01</name>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Finchley.SR2</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</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>${spring-cloud.version}</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>
</project>
主类:
package com.wc.study.eureka;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@SpringBootApplication
@EnableEurekaServer //启动Eureka注册中心
public class EurekaServer01Application {
public static void main(String[] args) {
SpringApplication.run(EurekaServer01Application.class, args);
}
}
配置文件 : application.yml
server:
port: 8080
eureka:
instance:
hostname: localhost
client:
#关闭向注册中心注册自己
register-with-eureka: false
#由于自己就是注册中心 自己就维护着服务实例 不需要去检索服务
fetch-registry: false
serviceUrl:
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
服务的提供者:
pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<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>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.8.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.wc.study.eureka</groupId>
<artifactId>eureka_client02</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>eureka_client02</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Finchley.SR2</spring-cloud.version>
</properties>
<dependencies>
<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>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.18</version>
</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>${spring-cloud.version}</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>
</project>
配置文件 application.yml
server:
port: 8091
spring:
application:
name: hello-service
eureka:
instance:
hostname: localhost
instance-id: provider-${random.int[8000,9000]}
client.serviceUrl.defaultZone: http://localhost:8080/eureka/
主类:
package com.wc.study.eureka;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@EnableDiscoveryClient
public class EurekaClient02Application {
public static void main(String[] args) {
SpringApplication.run(EurekaClient02Application.class, args);
}
}
测试的一个实体类:
package com.wc.study.eureka.bean;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
/**
* Created by wangchen on 2019/1/22.
*/
@Getter
@Setter
@ToString
@AllArgsConstructor
public class Student {
private String name;
private int age;
}
提供服务的controller:
package com.wc.study.eureka.controller;
import com.wc.study.eureka.bean.Student;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.*;
/**
* Created by wangchen on 2019/1/22.
* 服务提供者controller
*/
@RestController
public class ProviderController {
private static final Logger LOGGER = LoggerFactory.getLogger(ProviderController.class);
@GetMapping("/hello")
public String sayHello(@RequestParam String name){
LOGGER.info("传入的参数信息 {}",name);
return "hello" + name;
}
@PostMapping("/save")
public Student getStudent(@RequestBody Student student){
LOGGER.info("传入的参数信息 {}",student);
return student;
}
}
服务的消费者:
pom.xml 文件:
<?xml version="1.0" encoding="UTF-8"?>
<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>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.8.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.wc.study.ribbon</groupId>
<artifactId>ribbon_demo01</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>ribbon_demo01</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Finchley.SR2</spring-cloud.version>
</properties>
<dependencies>
<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>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.18</version>
</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>${spring-cloud.version}</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>
</project>
配置文件 application.yml
server:
port: 8092
spring:
application:
name: consumer
eureka:
instance:
hostname: localhost
instance-id: consumer-${random.int[8000,9000]}
client.serviceUrl.defaultZone: http://localhost:8080/eureka/
主类:
package com.wc.study.ribbon;
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 RibbonDemo02Application {
@Bean
@LoadBalanced
RestTemplate restTemplate(){
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(RibbonDemo02Application.class, args);
}
}
有一个和服务提供者一样的Student实体类。
controller测试类:
package com.wc.study.ribbon.controller;
import com.wc.study.ribbon.bean.Student;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
/**
* Created by wangchen on 2019/1/22.
*/
@RestController
public class HelloController {
@Autowired
private RestTemplate restTemplate;
@GetMapping("/consumer-hello")
public String index(){
return restTemplate.getForObject("http://HELLO-SERVICE/hello?name={1}",String.class,"test");
}
@GetMapping("/consumer-save")
public String save(){
Student student = new Student("乔巴",12);
ResponseEntity<Student> entity = restTemplate.postForEntity("http://HELLO-SERVICE/save",student,Student.class);
return entity.getBody().toString();
}
}
全部启动,注意观察控制台输出语句。
在浏览器中输入 http://localhost:8080/
可以看到运行正常。
在浏览器中调用消费者端的controller。
http://localhost:8092/consumer-hello
http://localhost:8092/consumer-save
Ribbon是如何实现负载均衡的:
通过配置的@LoadBalanced注解修饰的RestTemplate来实现。
通过源码观察发现,通过LoadBalancerInteceptor注入了一个LoadBalancerClient的实现,当一个被@LoadBalanced修饰的restTemplate对象向外发起HTTP请求时,会被此拦截器拦截,由于在发送请求是使用的时服务名,可以根据服务名获取到实例并发起真正的请求。
重试机制:
Spring Cloud Eureka实现的服务治理机制实现了 CAP 中的AP即可用性和可靠性,与Zookeeper这类强调CP(一致性,可靠性)的服务治理框架最大的区别就是,Eureka为了实现一定的可用性牺牲了一定的一致性,在一定的情况下他宁愿接受故障也不要丢掉“健康”实例。例如当服务中心的网络断开时,由于所有的服务都无法维持心跳,Eureka会由于超过85%的实例丢失心跳而触发保护机制,注册中心将会保留此时所有的节点,实现服务的调用,即使其中存在有节点,但这样仍然可以保障大多数的服务正常消费。
由于SpringCloud的这一机制, 我们需要一些重试的机制,来加强一定程度上的容错。
spring.cloud.loadbalancer.retry.enable=true | 用来开启重试机制 默认是关闭的。 |
hello-service.ribbon.ConnectTimeOut=250 | 请求连接的超时时间 |
hello-service.ribbon.ReadTimeOut=1000 | 请求处理的超时时间 |
hello-service.ribbon.OkToRetryOnAllOperations=true | 对所有的操作请求都进行重试。 |
hello-service.ribbon.MaxAutoRetriesNextServer=2 | 切换实例的重试次数 |
hello-service.ribbon.MaxAutoRetries=1 | 对当前实例的重试次数 |