Spring Cloud之Ribbon客户端负载均衡

320 阅读7分钟

Spring Cloud之Ribbon客户端负载均衡

一、Ribbon

Ribbon概述

Ribbon是由Netflix开发的一款基于HTTP和TCP的负载均衡的开源软件。

负载均衡的作用

为了保证服务的高可用性,一般会将相同的服务部署多个实例,负载均衡的作用就是使获取服务的请求被均衡的分配到各个实例中。负载均衡一般分为服务端负载均衡和客户端负载均衡

负载均衡的过程

用户请求先到达负载均衡器(相当于一个服务),负载均衡器根据负载均衡算法将请求转发到微服务。

负载均衡算法有:轮训、随机、加权轮训、加权随机、地址哈希等方法,负载均衡器维护一份服务列表,根据负载均衡算法将请求转发到相应的微服务上,所以负载均衡可以为微服务集群分担请求,降低系统的压力。

客户端负载均衡

客户端负载均衡与服务端负载均衡的区别在于客户端要维护一份服务列表,Ribbon从Eureka Server获取服务列表,Ribbon根据负载均衡算法直接请求到具体的微服务,中间省去了负载均衡服务。

Ribbon实现负载均衡的过程

1、在消费微服务中使用Ribbon实现负载均衡,Ribbon先从EurekaServer中获取服务列表。

2、Ribbon根据负载均衡的算法去调用微服务。

二、RestTemplate

RestTemplate是一个用来发送REST请求的摸板,包含了GET、POST、PUT、DELETE等HTTP Method对应的方法

Get请求

GET请求对应的方法有getForEntitygetForObject

getForEntity

getForEntity方法返回ResponseEntity对象,该对象包含了返回报文头,报文体和状态码等信息。

有3个参数:第一个参数为Url,第二个参数为返回值的类型,第三个参数为请求的参数

getForEntity有三个重载方法:

	@Override
	public <T> ResponseEntity<T> getForEntity(String url, Class<T> responseType, Object... uriVariables)
			throws RestClientException {

		RequestCallback requestCallback = acceptHeaderRequestCallback(responseType);
		ResponseExtractor<ResponseEntity<T>> responseExtractor = responseEntityExtractor(responseType);
		return nonNull(execute(url, HttpMethod.GET, requestCallback, responseExtractor, uriVariables));
	}

	@Override
	public <T> ResponseEntity<T> getForEntity(String url, Class<T> responseType, Map<String, ?> uriVariables)
			throws RestClientException {

		RequestCallback requestCallback = acceptHeaderRequestCallback(responseType);
		ResponseExtractor<ResponseEntity<T>> responseExtractor = responseEntityExtractor(responseType);
		return nonNull(execute(url, HttpMethod.GET, requestCallback, responseExtractor, uriVariables));
	}

	@Override
	public <T> ResponseEntity<T> getForEntity(URI url, Class<T> responseType) throws RestClientException {
		RequestCallback requestCallback = acceptHeaderRequestCallback(responseType);
		ResponseExtractor<ResponseEntity<T>> responseExtractor = responseEntityExtractor(responseType);
		return nonNull(execute(url, HttpMethod.GET, requestCallback, responseExtractor));
	}

getForObject

getForObject方法和getForEntity方法类似,也有三个重载方法,参数类型和getForEntity方法一致

	@Override
	@Nullable
	public <T> T getForObject(String url, Class<T> responseType, Object... uriVariables) throws RestClientException {
		RequestCallback requestCallback = acceptHeaderRequestCallback(responseType);
		HttpMessageConverterExtractor<T> responseExtractor =
				new HttpMessageConverterExtractor<>(responseType, getMessageConverters(), logger);
		return execute(url, HttpMethod.GET, requestCallback, responseExtractor, uriVariables);
	}

	@Override
	@Nullable
	public <T> T getForObject(String url, Class<T> responseType, Map<String, ?> uriVariables) throws RestClientException {
		RequestCallback requestCallback = acceptHeaderRequestCallback(responseType);
		HttpMessageConverterExtractor<T> responseExtractor =
				new HttpMessageConverterExtractor<>(responseType, getMessageConverters(), logger);
		return execute(url, HttpMethod.GET, requestCallback, responseExtractor, uriVariables);
	}

	@Override
	@Nullable
	public <T> T getForObject(URI url, Class<T> responseType) throws RestClientException {
		RequestCallback requestCallback = acceptHeaderRequestCallback(responseType);
		HttpMessageConverterExtractor<T> responseExtractor =
				new HttpMessageConverterExtractor<>(responseType, getMessageConverters(), logger);
		return execute(url, HttpMethod.GET, requestCallback, responseExtractor);
	}

POST请求

发送POST请求主要有postForEntitypostForObjectpostForLocation(较少使用)三个方法。

postForEntity

	@Override
	public <T> ResponseEntity<T> postForEntity(String url, @Nullable Object request,
			Class<T> responseType, Object... uriVariables) throws RestClientException {

		RequestCallback requestCallback = httpEntityCallback(request, responseType);
		ResponseExtractor<ResponseEntity<T>> responseExtractor = responseEntityExtractor(responseType);
		return nonNull(execute(url, HttpMethod.POST, requestCallback, responseExtractor, uriVariables));
	}

	@Override
	public <T> ResponseEntity<T> postForEntity(String url, @Nullable Object request,
			Class<T> responseType, Map<String, ?> uriVariables) throws RestClientException {

		RequestCallback requestCallback = httpEntityCallback(request, responseType);
		ResponseExtractor<ResponseEntity<T>> responseExtractor = responseEntityExtractor(responseType);
		return nonNull(execute(url, HttpMethod.POST, requestCallback, responseExtractor, uriVariables));
	}

	@Override
	public <T> ResponseEntity<T> postForEntity(URI url, @Nullable Object request, Class<T> responseType)
			throws RestClientException {

		RequestCallback requestCallback = httpEntityCallback(request, responseType);
		ResponseExtractor<ResponseEntity<T>> responseExtractor = responseEntityExtractor(responseType);
		return nonNull(execute(url, HttpMethod.POST, requestCallback, responseExtractor));
	}

postForObject

	@Override
	@Nullable
	public <T> T postForObject(String url, @Nullable Object request, Class<T> responseType,
			Object... uriVariables) throws RestClientException {

		RequestCallback requestCallback = httpEntityCallback(request, responseType);
		HttpMessageConverterExtractor<T> responseExtractor =
				new HttpMessageConverterExtractor<>(responseType, getMessageConverters(), logger);
		return execute(url, HttpMethod.POST, requestCallback, responseExtractor, uriVariables);
	}

	@Override
	@Nullable
	public <T> T postForObject(String url, @Nullable Object request, Class<T> responseType,
			Map<String, ?> uriVariables) throws RestClientException {

		RequestCallback requestCallback = httpEntityCallback(request, responseType);
		HttpMessageConverterExtractor<T> responseExtractor =
				new HttpMessageConverterExtractor<>(responseType, getMessageConverters(), logger);
		return execute(url, HttpMethod.POST, requestCallback, responseExtractor, uriVariables);
	}

	@Override
	@Nullable
	public <T> T postForObject(URI url, @Nullable Object request, Class<T> responseType)
			throws RestClientException {

		RequestCallback requestCallback = httpEntityCallback(request, responseType);
		HttpMessageConverterExtractor<T> responseExtractor =
				new HttpMessageConverterExtractor<>(responseType, getMessageConverters());
		return execute(url, HttpMethod.POST, requestCallback, responseExtractor);
	}

postForLocation

	@Override
	@Nullable
	public URI postForLocation(String url, @Nullable Object request, Object... uriVariables)
			throws RestClientException {

		RequestCallback requestCallback = httpEntityCallback(request);
		HttpHeaders headers = execute(url, HttpMethod.POST, requestCallback, headersExtractor(), uriVariables);
		return (headers != null ? headers.getLocation() : null);
	}

	@Override
	@Nullable
	public URI postForLocation(String url, @Nullable Object request, Map<String, ?> uriVariables)
			throws RestClientException {

		RequestCallback requestCallback = httpEntityCallback(request);
		HttpHeaders headers = execute(url, HttpMethod.POST, requestCallback, headersExtractor(), uriVariables);
		return (headers != null ? headers.getLocation() : null);
	}

	@Override
	@Nullable
	public URI postForLocation(URI url, @Nullable Object request) throws RestClientException {
		RequestCallback requestCallback = httpEntityCallback(request);
		HttpHeaders headers = execute(url, HttpMethod.POST, requestCallback, headersExtractor());
		return (headers != null ? headers.getLocation() : null);
	}

PUT请求

发送PUT请求,使用的是put方法,put方法返回值是void类型

	@Override
	public void put(String url, @Nullable Object request, Object... uriVariables)
			throws RestClientException {

		RequestCallback requestCallback = httpEntityCallback(request);
		execute(url, HttpMethod.PUT, requestCallback, null, uriVariables);
	}

	@Override
	public void put(String url, @Nullable Object request, Map<String, ?> uriVariables)
			throws RestClientException {

		RequestCallback requestCallback = httpEntityCallback(request);
		execute(url, HttpMethod.PUT, requestCallback, null, uriVariables);
	}

	@Override
	public void put(URI url, @Nullable Object request) throws RestClientException {
		RequestCallback requestCallback = httpEntityCallback(request);
		execute(url, HttpMethod.PUT, requestCallback, null);
	}

DELETE请求

发送DELETE请求,使用的是delete方法,delete方法返回值是void类型,该方法也有三个重载方法

	@Override
	public void delete(String url, Object... uriVariables) throws RestClientException {
		execute(url, HttpMethod.DELETE, null, null, uriVariables);
	}

	@Override
	public void delete(String url, Map<String, ?> uriVariables) throws RestClientException {
		execute(url, HttpMethod.DELETE, null, null, uriVariables);
	}

	@Override
	public void delete(URI url) throws RestClientException {
		execute(url, HttpMethod.DELETE, null, null);
	}

三、创建Eureka-Server

引入依赖

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
        </dependency>
    </dependencies>
 
    <dependencyManagement>
        <dependencies>
            <!-- spring-cloud的版本是以伦敦地铁站的名称命名,同时根据字母表的顺序对应版本的时间顺序 -->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                 <version>Hoxton.SR8</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
    <dependencies>

配置

server:
  port: 8080

spring:
  application:
    name: Eureka-Server

  security:
    user:
      name: admin
      password: admin123
      
eureka:
  instance:
    hostname: localhost
  client:
    register-with-eureka: false
    fetch-registry: false
    serviceUrl:
      defaultZone: http://admin:admin123@localhost:8080/eureka/

配置启动类

@EnableEurekaServer
@SpringBootApplication
public class  EurekaServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(EurekaServerApplication.class, args);
    }
}

四、创建服务提供方

引入依赖

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>

配置

通过设置虚拟机参数: -DPORT=8082 启动相同服务不同端口的服务

server:
  port: ${PORT:8081}
  
spring:
  application:
    name: Server-Produce
    
eureka:
  instance:
    hostname: localhost
  client:
    register-with-eureka: true
    fetch-registry: true
    serviceUrl:
      defaultZone: http://admin:admin123@localhost:8080/eureka/

配置启动类

@EnableDiscoveryClient
@SpringBootApplication
public class ServerProduceApplication {

    public static void main(String[] args) {
        SpringApplication.run(ServerProduceApplication.class, args);
    }
}

暴露服务接口

@RestController
@RequestMapping("user")
public class UserController {

    private Logger log = LoggerFactory.getLogger(UserController.class);

    @GetMapping("get")
    public User get(Long id, String name) {
        return new User(id, name, "123456");
    }

    @GetMapping("/{id:\\d+}")
    public User get(@PathVariable Long id) {
        return new User(id, "name", "123456");
    }


    @GetMapping("/getUsers")
    public List<User> getUsers() {
        List<User> list = new ArrayList<>();
        list.add(new User(1L, "admin1", "123456"));
        list.add(new User(2L, "admin2", "123456"));
        log.info("getUsers: " + list);
        return list;
    }

    @PostMapping
    public void add(@RequestBody User user) {
        log.info("addUser Success: " + user);
    }

    @PutMapping
    public void update(@RequestBody User user) {
        log.info("updateUser Success: " + user);
    }

    @DeleteMapping("/{id:\\d+}")
    public void delete(@PathVariable Long id) {
        log.info("deleteUser Success: " + id);
    }
}

五、创建Ribbon消费者

Spring Cloud引入Ribbon配合restTemplate实现客户端负载均衡。

Java中远程调用技术有:webservice、socket、rmi、Apache HttpClient、OkHttp等。

引入依赖

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
        </dependency>
    </dependencies>

配置Ribbon参数

server:
  port: 8083
spring:
  application:
    name: Server-Consumer
eureka:
  client:
    registerWithEureka: true #服务注册开关
    fetchRegistry: true #服务发现开关
    serviceUrl: #Eureka客户端与Eureka服务端进行交互的地址,多个中间用逗号分隔
      defaultZone: ${EUREKA_SERVER:http://www.eureka1.com:8888/eureka/,http://www.eureka2.com:9999/eureka/}
      defaultZone: http://admin:admin123@localhost:8080/eureka/
  instance:
    prefer-ip-address:  true  #将自己的ip地址注册到Eureka服务中
    ip-address: ${IP_ADDRESS:127.0.0.1}
    instance-id: ${spring.application.name}:${server.port} #指定实例id
ribbon:
  MaxAutoRetries: 2 #最大重试次数,当Eureka中可以找到服务,但是服务连不上时将会重试
  MaxAutoRetriesNextServer: 3 #切换实例的重试次数
  OkToRetryOnAllOperations: false  #对所有操作请求都进行重试,如果是get则可以,如果是post,put等操作没有实现幂等的情况下是很危险的,所以设置为false
  ConnectTimeout: 5000  #请求连接的超时时间
  ReadTimeout: 6000 #请求处理的超时时间

配置启动类

启动类中定义RestTemplate,使用@LoadBalanced注解开启负载均衡的功能
//@EnableDiscoveryClient向服务中心注册与发现
@EnableDiscoveryClient 
@SpringBootApplication
public class ManageCourseApplication {
    public static void main(String[] args) throws Exception {
        SpringApplication.run(ManageCourseApplication.class, args);
    }

    @Bean
    //@LoadBalanced表明这个restRemplate开启负载均衡的功能
    @LoadBalanced
    public RestTemplate restTemplate() {
        return new RestTemplate(new OkHttp3ClientHttpRequestFactory());
    }
}

消费服务接口

@RestController
public class TestController {

    @Autowired
    private RestTemplate restTemplate;

    /**
     * 方法参数在数组外
     * {1}为参数的占位符,匹配第三个参数数组的第一个元素
     *
     * @param id
     * @return
     */
    @GetMapping("user/get1")
    public User getUser1(Long id, String name) {
        ResponseEntity<User> forEntity = this.restTemplate.getForEntity("http://Server-Produce/user/get?id={id}&name={name}", User.class, id, name);
        HttpStatus statusCode = forEntity.getStatusCode();
        //通过返回报文状态码判断是否请求成功
        if (statusCode.is2xxSuccessful()) {
            User user = forEntity.getBody();
            return user;
        } else {
            return new User();
        }
    }

    /**
     * 方法参数在数组里
     *
     * @param id
     * @return
     */
    @GetMapping("user/get2")
    public User getUser2(Long id, String name) {
        Map<String, Object> params = new HashMap<>();
        params.put("id", id);
        params.put("name", name);
        User user = this.restTemplate.getForEntity("http://Server-Produce/user/get?id={id}&name={name}", User.class, params).getBody();
        return user;
    }

    /**
     * 第一个参数接收java.net.URI类型,通过org.springframework.web.util.UriComponentsBuilder来创建
     *
     * @param id
     * @return
     */
    @GetMapping("/get3/{id:\\d+}")
    public User getUser3(@PathVariable Long id) {
        Map<String, Object> params = new HashMap<>();
        params.put("id", id);
        URI uri = UriComponentsBuilder.fromUriString("http://Server-Produce/user/{id}").build().expand(params).encode().toUri();
        return this.restTemplate.getForEntity(uri, User.class).getBody();
    }

    @GetMapping("user/getALL")
    public List<User> getALL() {
        return this.restTemplate.getForObject("http://Server-Produce/user/getUsers", List.class);
    }

    @GetMapping("user/add")
    public String addUser() {
        User user = new User(1L, "admin", "admin123");
        HttpStatus status = this.restTemplate.postForEntity("http://Server-Produce/user", user, null).getStatusCode();
        if (status.is2xxSuccessful()) {
            return "addUser Success";
        } else {
            return "addUser Error";
        }
    }

    @GetMapping("user/update")
    public String updateUser() {
        User user = new User(1L, "admin", "123456");
        this.restTemplate.put("http://Server-Produce/user", user);
        return "updateUser Success";
    }

    @GetMapping("user/delete/{id:\\d+}")
    public String deleteUser(@PathVariable Long id) {
        this.restTemplate.delete("http://Server-Produce/user/{1}", id);
        return "deleteUser Success";
    }
}

Ribbon测试

启动两个Server-Produce服务,端口不一致,启动后观察Eureka Server的服务列表
在这里插入图片描述
添加@LoadBalanced注解后,restTemplate会走LoadBalancerInterceptor拦截器,此拦截器中会通过RibbonLoadBalancerClient查询服务地址

在这里插入图片描述
在这里插入图片描述
多次访问统一接口,发现Ribbon的负载均衡生效了
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

六、Ribbon配置

Ribbon的配置分为全局和指定服务名称。

指定全局的服务请求连接超时时间为200毫秒:

ribbon:
  ConnectTimeout: 200

指定服务的请求连接超时时间,在配置最前面加上服务名称就行了

Server-Produce:
  ribbon:
    ConnectTimeout: 200

设置服务的超时时间:

  ribbon:
    ReadTimeout: 1000

切换服务实例的重试次数:

  ribbon:
    MaxAutoRetriesNextServer: 1

对服务当前实例的重试次数:

  ribbon:
    MaxAutoRetries: 1

使用如上配置,当访问某服务实例遇到故障的时候,Ribbon会尝试再访问一次当前实例(次数由MaxAutoRetries配置),如果不行则换另一个相同服务实例进行访问(次数由 MaxAutoRetriesNextServer配置),最后还是不行则返回失败。