天天看点

Ribbon&OpenFeign实现服务治理——注册、发现、负载均衡

一、网络模型

1、OSI的七层模型

七层的模型体现着程序设计过程中的单一职责的特性,每个模块都负责这自己的东西。

  • 应用层——老板下达收购Google的指令,并写完收购意向书

为计算机用户提供应用接口,也为用户直接提供各 种网络服务。我们常见应用层的网络服务协议有:HTTP,HTTPS,FTP,POP3、SMTP等。
  • 表示层——将中文意向受翻译成英文

表示层提供各种用于应用层数据的编码和转换功能,确保一个系统的应用层发送的数据能 被另一个系统的应用层识别。如果必要,该层可提供一种标准表示形式,用于将计算机内 部的多种数据格式转换成通信中采用的标准表示形式。数据压缩和加密也是表示层可提供 的转换功能之一
  • 会话层——找到Google的内部通信地址并将与收购意向书封装成一份信

负责建立、管理和终止表示层实体之间的通信会话。该层的通信由不同设备 中的应用程序之间的服务请求和响应组成。
  • 传输层——收发室的人根据重要重要程度选择快递公司

建立了主机端到端的链接,传输层的作用是为上层协议提供端到端的可靠和透明 的数据传输服务,包括处理差错控制和流量控制等问题。该层向高层屏蔽了下层数据通信 的细节,使高层用户看到的只是在两个传输实体间的一条主机到主机的、可由用户控制和 设定的、可靠的数据通路。我们通常说的,RPC,TCP,UDP就是在这一层。端口号既是这 里的“端”
  • 网络层——利用快递公司的网络将信件发送到Google

本层通过IP寻址来建立两个节点之间的连接,为远端的传输层送来的分组,选择合适的 路由和交换节点,正确无误地按照地址传送给目的端的运输层。就是通常说的IP层。这一层 就是我们经常说的IP协议层。IP协议是Internet的基础
  • 数据链路层——底层具体的物流工具和快递打包的封装规则

将比特组合成字节,再将字节组合成帧,使用链路层地址 (以太网使用MAC地址)来访问 介质,并进行差错检测。数据链路层又分为2个子层:逻辑链路控制子层(LLC)和媒体访 问控制子层(MAC)。MAC子层处理CSMA/CD算法、数据出错校验、成帧等;LLC子层定 义了一些字段使上次协议能共享数据链路层。 在实际使用中,LLC子层并非必需的
  • 物理层——底层具体的物流工具和快递打包的封装规则

实际最终信号的传输是通过物理层实现的。通过物理介质传输比特流。规定了电平、速 度和电缆针脚。常用设备有(各种物理设备)集线器、中继器、调制解调器、网线、双绞 线、同轴电缆。这些都是物理层的传输介质。

2、HTTP与RPC的区别

  • 两者一个在网络七层一个在网络四层
  • SpringCloud属于是一个生态在这个生态中语言不确定所有他倾向于采用HTTP
  • 当在一个应用内部各个模块的语言已知这时候默认采用RPC的技术来提高访问速度

3、RestFul理解

此处理解参照的是阮一峰的博文:

理解RESTful架构 - 阮一峰的网络日志 (ruanyifeng.com)

RESTful API 设计指南 - 阮一峰的网络日志 (ruanyifeng.com)

RestFul我们可以理解为是一种系统设计的风格,所有符合这种风格的架构都可以被称之为RestFul架构。其中的Rest是几个英文单词的缩写——Representational State Transfer

Representational:具象派的;代表性的,在阮一峰的博客中将它解释为表现层状态装换,我们或许也可以将它理解为资源的表现层状态装换

Resources:资源,它可以是一段文本、一张图片、一首歌曲、一种服务,总之就是一个具体的实在。你可以用一个URI(统一资源定位符)指向它,每种资源对应一个特定的URI。要获取这个资源,访问它的URI就可以,因此URI就成了每一个资源的地址或独一无二的识别符。

State Transfer:访问一个网站,就代表了客户端和服务器的一个互动过程。在这个过程中,势必涉及到数据和状态的变化。

3、什么是RestTemplate

传统情况下在java代码里访问restful服务,一般使用Apache的HttpClient。不过此 种方法使用起来太过繁琐。spring提供了一种简单便捷的模板类来进行操作,这 就是RestTemplate。

RestTemplate 是从 Spring3.0 开始支持的一个 HTTP 请求工具,它提供了常见的 REST请求方案的模版,例如 GET 请求、POST 请求、PUT 请求、DELETE 请求以 及一些通用的请求执行方法 exchange 以及 execute。RestTemplate 继承自 InterceptingHttpAccessor 并且实现了 RestOperations 接口,其中 RestOperations 接口定义了基本的 RESTful 操作,这些操作在 RestTemplate 中 都得到了实现。

二、Ribbon

1、微服务中的Ribbon

Spring Cloud Ribbon是一个基于HTTP和TCP的客户端负载均衡工具,他基于Netflix Ribbon实现,通过Spring Cloud的封装,可以让我们轻松的将面向服务的Rest模板请求自动转换成客户端负载均衡的调用。

Ribbon只具有负载均衡的能力,也就是说他可以在一些列的地址中根据一定的算法挑选出一个合适的地址,但这个地址的通信还是需要通过通信组件来进行,这样的组件如:RestTemplate、Feign等

Ribbon&OpenFeign实现服务治理——注册、发现、负载均衡

2、 RestTemplat基础使用与项目搭建

  • Producer

创建一个producer的服务,此服务中主要配置一个注册中心:

server:
  port: 7000

spring:
  application:
    name: ribbon-producer
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8847
           

提供者的启动类:

@EnableDiscoveryClient
@SpringBootApplication
public class RibbonProducerApplication {
    public static void main(String[] args) {
        SpringApplication.run(RibbonProducerApplication.class);
    }
}
           
  • Consumer

在consumer的服务的启动类中我们需要添加RestTemplat的Bean以供后面的使用:

@EnableDiscoveryClient
@SpringBootApplication
public class RibbonConsumerApplication {
    public static void main(String[] args) {
        SpringApplication.run(RibbonConsumerApplication.class);
    }

    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}
           

在consumer的服务中我们先在Controller的类中添加如下的属性:

@Slf4j
@RequestMapping("/consumer")
@RestController
public class ConsumerController {

    @Resource
    //import org.springframework.web.client.RestTemplate;
    private RestTemplate restTemplate;

    @Resource
    //import org.springframework.cloud.client.discovery.DiscoveryClient;
    private DiscoveryClient discoveryClient;

    @Resource
    //import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
    private LoadBalancerClient loadBalancerClient;
}
           

RestTemp的基础使用时简单的就就是new一个对象,然后调用这个对象的getForObject方法:

/**
     * 使用new的RestTemplate(http://localhost:7002/consumer/restTemplateTest)
     */
    @GetMapping("/restTemplateTest")
    public String restTemplateTest() {
        log.info("restTemplateTest invoke!");
        RestTemplate restTemplate = new RestTemplate();
        return restTemplate.getForObject("http://localhost:7000/provider/hello", String.class);
    }
           

这时候我们的服务就可以调通了,当然我们也可以不自己创建对象而是通过注解引入容器中的对象:

@GetMapping("/restTemplateTestUseBean")
public String restTemplateTestUseBean() {
    log.info("restTemplateTestUseBean invoke!");
    return restTemplate.getForObject("http://localhost:7000/provider/hello", String.class);
}
           

3、DiscoveryClient + 自实现负载均衡 + RestTemplat

/**
 * 使用DiscoveryClient获取服务列表进行随机服务调用(http://localhost:7002/consumer/discoveryClient)
 * 【注意】不能加@LoadBalanced,否则请求失败
 */
@GetMapping("/discoveryClient")
public String discoveryClient() {
    log.info("discoveryClient invoke!");
    // 获得服务id下的服务信息
    List<ServiceInstance> serviceInstanceList = discoveryClient.getInstances("ribbon-producer");
    serviceInstanceList.forEach(serviceInstance -> log.info("host={}, port={}, uri={}", serviceInstance.getHost()
            , serviceInstance.getPort(), serviceInstance.getUri()));
    int num = new Random().nextInt(serviceInstanceList.size());
    URI uri = serviceInstanceList.get(num).getUri();
    log.info("num={}, uri={}", num, uri);
    return restTemplate.getForObject( uri + "/provider/hello", String.class);
}
           

discoveryClient是SpringCloud中的一个实体,他会集成的服务发现管理工具——Nacos来发现服务,并获取对应的服务列表,我们拿到这个列表后根据自定义的策略取出对应的服务然后进行请求。

4、LoadBalancerClient + RestTempl

/**
 * 使用LoadBalancerClient自带的负载均衡功能执行服务调用(http://localhost:7002/consumer/loadBalancerClient)
 * 【注意】不能加@LoadBalanced,否则请求失败
 */
@GetMapping("/loadBalancerClient")
public String loadBalancerClient() {
    log.info("loadBalancerClient invoke!");
    // 获得负载均衡实例
    ServiceInstance serviceInstance = loadBalancerClient.choose("ribbon-producer");
    log.info("uri={}", serviceInstance.getUri());
    return restTemplate.getForObject( serviceInstance.getUri() + "/provider/hello", String.class);
}
           

5、@LoadBalanced + RestTemplat

/**
 * 使用@LoadBalanced修饰的RestTemplate来执行具有负载均衡功能的服务调用(http://localhost:7002/consumer/loadBalancerAnnotation)
 */
@GetMapping("/loadBalancerAnnotation")
public String loadBalancerAnnotation() {
    log.info("loadBalancerAnnotation invoke!");
    // 获得负载均衡实例
    return restTemplate.getForObject("http://ribbon-producer/provider/hello", String.class);
}
           

在这个方法中我们往容器中添加了一个@LoadBalanced的注解:

@Bean
@LoadBalanced
public RestTemplate restTemplate() {
    return new RestTemplate();
}
           

6、自定义负载均衡策略

如果要是想要指定负载均衡的策略可以在yaml的配置文件中添加如下配置:

ribbon-producer:
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
           

后面值是负载均衡策略的全路径名,其他的一个策略为:

  • RoundRobinRule 轮询策略,按顺序循环选择服务实例 
  • RandomRule 随机策略,随机选择服务实例 
  • AvaliabilityFilteringRule 可用过滤策略,会先过滤由于多次访问故障而处于断路 器跳闸状态的服务,还有并发的连接数量超过阈值的服务。然后对剩余的服务列 表按照轮询策略进行访问。 
  • WeightedResponseTimeRule 响应时间加权策略(对于不同配置或负载的服务, 请求偏向于打到负载小或性能高的服务上),根据平均响应的时间计算所有服务 的权重,响应时间越快服务权重越大,也就越大概率被选中,刚启动时如果统计 信息不足,则使用RoundRobinRule策略,等统计信息足够了,会切换到本策略。
  • RetryRule 重试策略(会使客户对于服务列表中不可用服务的调用无感,因为会 retry到别的服务),先按照RoundRobinRule的策略获取服务,如果获取失败,则 在制定时间内进行重试,获取可用服务。
  • BestAvailableRule 最低并发策略

7、负载均衡策略的原理

可以侧重看各个策略的实现原理

三、OpenFeign

1、什么是OpenFeign?

由于Netflix的Feign组件进入的维护阶段,所以Spring Cloud团队开始吸收开源的 Feign组件的简化,封装开发了OpenFeign组件,而使用上,OpenFeign与Feign是一 样的。

OpenFeign是一个声明式的伪Http客户端(即:封装了Http请求,底层还是使用的 RestTemplate发送的http请求),它使得编写Http客户端变得更简单。只需要创 建一个接口并加入相关注解。它具有可插拔的注解特性(可以使用SpringMVC注 解),可以使用Feign注解和JAX-RS注解。它将支持可插拔的编码器和解码器。默 认继承了Ribbon,默认实现了负载均衡。并且SpringCloud团队为Feign添加了 Spring MVC注解的支持。

2、项目框架的准备

  • Producer

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>
</dependencies>
           

在pom文件中主要添加的是Nacos、以及openfeign相关的依赖,配置文件中进行对Nacos相关的配置:

server:
  port: 8000
spring:
  application:
    name: openfeign-producer
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
           

在项目的主类中添加Nacos与Openfeign相关的注解:

@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
public class OpenFeignProducerApplication {
    public static void main(String[] args) {
        SpringApplication.run(OpenFeignProducerApplication.class);
    }
}
           
  • Consumer

在Consumer项目的依赖文件中的需要添加的依赖于Producer的是相同的,上面的我们也可以看到没有引用Ribbon的依赖这是因为OpenFeign中默认已经集成了Ribbon,有关Ribbon的负载均衡策略我们都是可以在yaml的配置文件中正常配置的

server:
  port: 8002

spring:
  application:
    name: openfeign-consumer
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
feign:
  client:
    config:
      openfeign-producer:
        connectTimeout: 5000
        readTimeout: 5000
           

主类中与Producer的项目一样也是添加Nacos与Openfeign相关的注解,这里就不再做展示

3、使用OpenFeign实现服务间的请求

使用OpenFeign实现服务之间的请求就像Mybatis进行CURD操作似的,我们需要一个接口来进行代理,在这个接口中指明此接口对应的是哪个服务:

@FeignClient(name = "openfeign-producer")
public interface ProviderService {
    @GetMapping("/provider/hello")
    String hello();
    /**
     * 传递基本类型参数
     */
    @GetMapping("/provider/baseParam")
    String baseParam(@RequestParam("name") String name, @RequestParam("age") Integer age);
    /**
     * 传递基本类型参数——路径
     */
    @GetMapping("/provider/baseParamPath/{name}/{age}")
    String baseParamPath(@PathVariable("name") String name, @PathVariable("age") Integer age);
    /**
     * 传递对象类型参数
     */
    @PostMapping("/provider/objectParam")
    String objectParam(@RequestBody User user);
    /**
     * 传递数组类型参数,通过queryString方式传递,即:/provider/arrayParam?params=1&params=2
     */
    @GetMapping("/provider/arrayParam")
    String arrayParam(@RequestParam("params") String[] params);
    /**
     * 传递集合类型参数
     */
    @GetMapping("/provider/listParam")
    String listParam(@RequestParam("params") List<String> params);
    /**
     * 传递集合类型参数
     * http://localhost:8000/provider/responseObj?userId=111
     */
    @GetMapping("/provider/responseObj")
    User responseObj(@RequestParam("userId") String userId);
    /**
     * 测试OpenFeign默认1秒请求超时
     * http://localhost:8000/producer/timeoutDemo
     */
    @GetMapping("/provider/timeoutDemo")
    String timeoutDemo();
}
           

这里面通过/provider/timeoutDemo来实现是个服务与代理之间的映射关系,@FeignClient(name = “openfeign-producer”)注解中相当于是建立了服务层面的关系。

@Slf4j
@RequestMapping("/consumer")
@RestController
public class ConsumerController {
    @Resource
    private ProviderService providerService;
    // http://localhost:8002/consumer/hello
    @GetMapping("/hello")
    public String hello() {
        log.info("Feign invoke!");
        return providerService.hello();
    }
    /**
     * 传递基本类型参数
     * http://localhost:8002/consumer/baseParam?name=muse&age=20
     */
    @GetMapping("/baseParam")
    public String baseParam(@RequestParam("name") String name, @RequestParam("age") Integer age) {
        log.info("consumer baseParam name={} age={}", name, age);
        return providerService.baseParam(name, age);
    }
    /**
     * 传递基本类型参数——路径
     * http://localhost:8002/consumer/baseParamPath/bob/30
     */
    @GetMapping("/baseParamPath/{name}/{age}")
    public String baseParamPath(@PathVariable("name") String name, @PathVariable("age") Integer age) {
        log.info("consumer baseParamPath name={} age={}", name, age);
        return providerService.baseParamPath(name, age);
    }
    /**
     * 传递对象类型参数
     * http://localhost:8002/consumer/objectParam
     */
    @GetMapping("/objectParam")
    public String objectParam() {
        User user = new User();
        user.setName("John");
        user.setAge(40);
        log.info("consumer objectParam name={} age={}", user.getName(), user.getAge());
        return providerService.objectParam(user);
    }
    /**
     * 传递数组类型参数
     * http://localhost:8002/consumer/arrayParam
     */
    @GetMapping("/arrayParam")
    public String arrayParam() {
        log.info("consumer arrayParam");
        String[] params = new String[]{"muse", "bob", "john"};
        return providerService.arrayParam(params);
    }
    /**
     * 传递集合类型参数
     * http://localhost:8002/consumer/listParam
     */
    @GetMapping("/listParam")
    public String listParam() {
        log.info("consumer listParam");
        List params = Lists.newArrayList("zhang3", "lee4", "wang5");
        return providerService.listParam(params);
    }
    /**
     * 传递集合类型参数
     * http://localhost:8002/consumer/responseObj?userId=111
     */
    @GetMapping("/responseObj")
    public User responseObj(@RequestParam("userId") String userId) {
        User user = providerService.responseObj(userId);
        log.info("consumer responseObj userId={} user={}", userId, user);
        return user;
    }
    /**
     * 测试OpenFeign默认1秒请求超时
     * http://localhost:8002/consumer/timeoutDemo
     */
    @GetMapping("/timeoutDemo")
    public String timeoutDemo() {
        log.info("consumer timeoutDemo!");
        providerService.timeoutDemo();
        return "consumer timeoutDemo!";
    }
}
           

在Controller层进行使用的时候我们直接通过代理的接口即可。

Dubbo是通过生产者与消费者都实现同一个接口同时通过配置文件来实现服务之间的一个注册、发现以及映射关系。而OpenFeign则是通过接口来实现,从使用者的层面来看,Dubbo好现实把对服务的调用隐藏的更深。

个人理解Dubbo项目的耦合度更高一些OpenFeign的耦合度低一些,因为他不直接对接口进行依赖,更像是直接通过网络请求进行的一种调用。但是总的来说,如果想要系统之间的调用可行就还是需要对彼此之间做一些约束,从个人的角度来说还是更喜欢Dubbo一些。

继续阅读