首页 > 技术文章 > 服务消费者(RestTemplate+Ribbon+feign)

mzhblog 2020-08-02 15:07 原文

负载均衡

​ spring cloud 体系中,我们知道服务之间的调用是通过http协议进行调用的。注册中心就是维护这些调用的服务的各个服务列表。在Spring中提供了RestTemplate,用于访问Rest服务的客户端,Spring Cloud体系中也是使用RestTemplate进行服务之间的调用。

​ 负载均衡(Load Balance),通常指将请求分摊到各个操作单元上进行处理,精髓在于均衡,也就是平均。负载均衡在我们日常开发中也是经常听到的,下面介绍几种常见的负载均衡技术实现。

  1. Nginx

    ​ Nginx是我们平时使用的较多的负载均衡技术实现,性能也是很不错。一个http请求请求至Nginx服务器,Nginx服务器根据请求和一定的算法将请求转发至真正的服务器,以达到负载均衡的目的。至于Nginx里面具体的负载均衡算法,了解过Nginx的人应该会比较熟悉,没了解过的请自己去看吧。

  2. DNS解析

    ​ 在DNS服务器上配置多个域名对应ip的记录,一个域名可以对应多组的ip地址。DNS服务器在解析域名时根据相应的算法,把通过域名的请求分配到合适的真实服务器上去。这样也可以达到一定的负载均衡,不至于一个服务器的压力过大。

RestTemplate

我们先看下官方是怎么定义的:

Synchronous client to perform HTTP requests, exposing a simple, template method API over underlying HTTP client libraries such as the JDK HttpURLConnection, Apache HttpComponents, and others. The RestTemplate offers templates for common scenarios by HTTP method, in addition to the generalized exchange and execute methods that support of less frequent cases.

​ 我们可以看到,RestTemplate采用的是同步方式执行Http请求的类,底层使用的是JDK的HttpURLConnection或者ApacheHttpComponents的类库,或者其他的类库。RestTemplate还提供了模版使得开发人员能够更简单发送Http请求。

RestTemplate中定义了很多和Rest资源交互的API,下面就介绍2个我们平时常用的GET和POST请求,在RestTemplate中是怎么请求的。

GET请求:

RequestEntity requestEntity = RequestEntity.get(new URI(uri)).build();
ResponseEntity<User> responseEntity2 = this.restTemplate.exchange(requestEntity, User.class);

exchage是一个通用的请求方法,接受一个RequestEntity对象,可以设置路径,请求头,请求信息等。最后返回一个ResponeseEntity实体。

当然GET请求也可以是getForEntity()getForObject()2中类型:

//getForEntity()
ResponseEntity<User> responseEntity = this.restTemplate.getForEntity(uri, User.class);
User user = responseEntity.getBody();
//getForObject()
User user = this.restTemplate.getForObject(uri, User.class);

POST请求:

HttpEntity<MultiValueMap> request = new HttpEntity<>(map, header);    ResponseEntity<String> exchangeResult = restTemplate.exchange(url, HttpMethod.POST, request, String.class);

map中存放post的数据,header里存放请求头相关的信息,最后返回一个ResponeseEntity实体类。

同样的,POST请求也是有着对应的getForObject()getForEntity()类型:

//postForObject()
User user = this.restTemplate.postForObject(uri, user, User.class);
//postForEntity()
ResponseEntity<User> responseEntity = this.restTemplate.postForEntity(uri, user, User.class);

请他的请求,也是类似,这里就不在此列举了,感兴趣的可以查看RestTesmplate的api

Ribbon

Spring Cloud Ribbon是基于Netflix Ribbon实现的客户端负载均衡组件。区别于Nginx的服务端负载均衡的实现。在结合Spring Cloud Eureka组件使用时,ribbonServerList会被重写,改为通过Eureka的注册中心来获取服务列表,可以通过简单的几行配置来实现客户端的负载均衡。

接下来我们来使用Spring Cloud Ribbon来实现客户端的负载均衡

提前项目准备:

1.Eureka:首先我们先启动一个Eureka 服务端作为注册中心
2.然后开启2个服务,注册到Eureka服务中去,我这里开启了2个oauth服务注册到Eureka服务端中
3.开始构建具有负载均衡功能的服务消费方apiGateWay

​ 这里使用apiGateWay作为服务消费方的原因是我们应用一般都是需要权限管理,验证登录用户的,这里apiGateWay通过调用oauth应用服务来验证用户的合法性。

  1. 首先添加依赖

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-eureka</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-ribbon</artifactId>
    </dependency>
    
  2. 启用服务发现客户端

    这里apiGateWay作为服务消费方,就相当于http请求过程充当了客户端的角色,被调用的服务就是服务端。先在客户端的启动类中声明要使用的RestTemplate

    @SpringBootApplication
    @EnableDiscoveryClient
    @EnableZuulProxy
    @Slf4j
    public class ApiGatewayApplication {
        public static void main(String[] args){
            SpringApplication.run(ApiGatewayApplication.class);
            log.info("ApiGatewayApplication启动");
        }
    
        @Bean
        @LoadBalanced
        RestTemplate restTemplate(){
            return new RestTemplate();
        }
    }
    
  3. 编写测试类

    @RestController
    @Slf4j
    public class RibbonClient {
        @Autowired
        RestTemplate restTemplate;
    
        @GetMapping("/demo")
        public String ribbonClientDemo(String paramName){
            log.info("请求参数paramName:{}",paramName);
            return restTemplate.getForObject("http://oauth/demo?paramName="+paramName,String.class);
        }
    }
    
  4. 启动应用,访问http://localhost:38763/demo?paramName=demo

    请求的端口是:38765
    或者是
    请求的端口是:38764
    

    然后我们多次发起请求,看被调用的服务的日志,可以发现请求确实被均匀的分配到开启的2个服务的。

    oauth(端口:38764)

    请求的参数是:demo
    serverPort:38764
    

    oauth(端口:38765)

    请求的参数是:demo
    serverPort:38765
    

    可以看到,2个服务被轮询调用。Ribbon默认的均衡策略是以轮询的方式去选择服务器。

    1.RandomRule:随机选取负载均衡策略,随机Random对象,在所有服务实例中随机找一个服务的索引号,然后从上线的服务中获取对应的服务。
    2.RoundRobinRule:线性轮询负载均衡策略。
    3.WeightedResponseTimeRule:响应时间作为选取权重的负载均衡策略,根据平均响应时间计算所有服务的权重,响应时间越短的服务权重越大,被选中的概率越高。刚启动时,如果统计信息不足,则使用线性轮询策略,等信息足够时,再切换到WeightedResponseTimeRule。
    4.RetryRule:使用线性轮询策略获取服务,如果获取失败则在指定时间内重试,重新获取可用服务。
    5.ClientConfigEnabledRoundRobinRule:默认通过线性轮询策略选取服务。通过继承该类,并且对choose方法进行重写,可以实现更多的策略,继承后保底使用RoundRobinRule策略。
    6.BestAvailableRule:继承自ClientConfigEnabledRoundRobinRule。从所有没有断开的服务中,选取到目前为止请求数量最小的服务。
    7.PredicateBasedRule:抽象类,提供一个choose方法的模板,通过调用AbstractServerPredicate实现类的过滤方法来过滤出目标的服务,再通过轮询方法选出一个服务。
    8.AvailabilityFilteringRule:按可用性进行过滤服务的负载均衡策略,会先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,还有并发的连接数超过阈值的服务,然后对剩余的服务列表进行线性轮询。
    9.ZoneAvoidanceRule:本身没有重写choose方法,用的还是抽象父类PredicateBasedRule的choose。
    

    如果没有符合我们业务需求的,那么我们也可以根据业务需求自己实现一个IRule。可以通过继承AbstractLoadBalancerRule来实现我们自己的负载均衡算法:

    @Slf4j
    public class MyRule extends AbstractLoadBalancerRule {
    
        @Override
        public void initWithNiwsConfig(IClientConfig iClientConfig){
    
        }
    
        /**
         * 自定义均衡策略
         * 这里简单实现返回列表的第一个服务
         */
        @Override
        public Server choose(Object o) {
            log.info("key:" + o);
          	//获取服务列表
            List<Server> allServers = getLoadBalancer().getAllServers();
            log.info(allServers.toString());
            return allServers.get(0);
        }
    }
    

    我们通过实现choose方法来实现我们自己的负载均衡算法。getLoadBalancer().getAllServers()可以获取到所有的服务信息,然后我们可以根据自己的策略来确定选择哪一个服务器。

    自定义策略之后呢,我们需要在服务调用方添加一个注解配置@FeignClient+配置类来修改spring cloud的默认配置。

    首先启动类上添加注解:

    @SpringBootApplication
    @EnableDiscoveryClient
    @EnableZuulProxy
    @Slf4j
    @FeignClient(value = "oauth",configuration = RuleConfig.class)
    public class ApiGatewayApplication {
        public static void main(String[] args){
            SpringApplication.run(ApiGatewayApplication.class);
            log.info("ApiGatewayApplication启动");
        }
    
        @Bean
        @LoadBalanced
        RestTemplate restTemplate(){
            return new RestTemplate();
        }
    }
    

    然后创建RuleConfig类来把策略修改为我们刚刚实现的策略:

    @Configuration
    public class RuleConfig {
        @Bean
        public IRule ribbonRule() {
            return new MyRule();
        }
    }
    

    重新启动应用,然后我们在发起请求,就会发现这次请求就只会转发给同一个服务,也就是server列表里的第一个服务,而不在是轮询的进行请求。

    然后可以看到MyRule类打印的日志:

    请求参数paramName:demo
    key:default
    [192.168.3.2:38764, 192.168.3.2:38765]
    

    服务列表中依旧是2个服务在运行,但请求通过我们自定义的策略,永远只会发送给第一个服务,而不会转发给第二个服务。

    当然我们也可以通过配置文件配置,这就自己去研究吧。。。

feign

我们先来看下官网是怎么定义的:

Feign is a declarative web service client. It makes writing web service clients easier. To use Feign create an interface and annotate it. It has pluggable annotation support including Feign annotations and JAX-RS annotations. Feign also supports pluggable encoders and decoders. Spring Cloud adds support for Spring MVC annotations and for using the same HttpMessageConverters used by default in Spring Web. Spring Cloud integrates Ribbon and Eureka to provide a load balanced http client when using Feign.

Feign是一个声明示的Http客户端,它是的写Http客户端更加简单。它只需要通过注解相应的接口就可以实现,虽然通过上面的Ribbon里介绍的也可以创建Http客户端发起请求,但不是那么的优雅,Feign是NetFlix开发的声明式、模块式的HTTP客户端,可以帮助我们更好的、更快的开发调用HTTP API。

Feign应用

  1. 首先在工程中加入相应的依赖:

    <!-- feign -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
    
  2. 然后添加配置文件:

    server.port=38763
    spring.application.name=api-gateway
    spring.cloud.config.discovery.enabled=true
    spring.cloud.config.discovery.service-id=config-server
    spring.cloud.config.profile=@package.environment@
    eureka.client.service-url.defaultZone=http://localhost:38761/eureka/
    
  3. 创建启动类,添加注解@EnableFeignClients,开启Feign支持:

    @SpringBootApplication
    @EnableDiscoveryClient
    @EnableZuulProxy
    @Slf4j
    @EnableFeignClients
    public class ApiGatewayApplication {
        public static void main(String[] args){
            SpringApplication.run(ApiGatewayApplication.class);
            log.info("ApiGatewayApplication启动");
        }
    }
    
  4. 创建一个接口类IOauthClient,加入注解@FeignClient来指定这个接口要调用的服务的名称

    //使用@FeignClient注解来指定要调用的服务名称,即注册到eureka里的服务名称
    @FeignClient(name = "oauth")
    public interface IOauthClient {
    
        /**
         * value 指定要调用的接口
         * method 指定调用的方式是 GET、POST、PUT、DELETE等
         * @RequestParam 指定要传入的参数
         */
        @RequestMapping(value = "/demo",method = RequestMethod.GET)
        public String demo(@RequestParam("paramName") String paramName);
    }
    
  5. 然后在业务需要的地方调用该接口

    @RestController
    @Slf4j
    public class DemoController {
    
        @Autowired
        IOauthClient iOauthClient;
    
        @GetMapping("/demo")
        public String demo(String paramName){
            log.info("请求参数为:{}",paramName);
            return iOauthClient.demo(paramName);
        }
    }
    
  6. 启动应用,然后发起请求

    然后我们就可以看到页面上请求的返回值:

    请求的端口是:38764
    

    是不是和前面讲Ribbon一样的,不过这种实现方式是不是和前面的方式更加优雅呢?尤其是在调用的接口变多之后,这种实现方式是不是更加简单?

  7. 注意

    这里使用Feign的时候,需要注意的问题:

    1.GET请求多个参数的时候,需要使用@RequestParam

    2.POST请求使用@RequestBody注解参数

参考资料

欢迎关注我的公众号

本文由博客群发一文多发等运营工具平台 OpenWrite 发布

推荐阅读