重学SpringCloud系列二之服务注册与发现---上

x33g5p2x  于2022-02-07 转载在 Spring  
字(39.7k)|赞(0)|评价(0)|浏览(264)

系列专辑:

  • 重学SpringCloud系列二之服务注册与发现
  • 重学SpringCloud系列二之服务注册与发现 (当前专辑)

构建eureka服务注册中心

Eureka服务注册中心

生产环境中的Eureka服务注册中心的架构应该如下图所示

可以简单的理解:

  • 服务注册中心就是一个婚姻介绍所
  • A服务找老婆,B服务找老公
  • 每个服务需要向婚姻介绍所登记(服务注册),也都需要从婚姻介绍所获取候选人信息(服务发现)

为了保障微服务的高可用,提供更高的并发能力,通常需要部署多份实现集群部署。为了快速的让大家入手Spring Cloud 微服务架构,此文只做简化版的服务注册中心的构建。后续我们会讲集群部署。

搭建Eureka服务注册中心

Spring Cloud版本管理

eureka服务注册中心,是我们第一次使用Spring Cloud相关的组件。所以我们需要在的父项目中添加统一的版本管理。

<?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>

  <!--  聚合工程,父工程打包方式为pom-->
  <groupId>dhy.xpy</groupId>
  <artifactId>StudyCloudWithMe</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>pom</packaging>

  <modules>
    <module>dhy-service-sms</module>
      <module>dhy-service-common</module>
    <module>dhy-dao-service</module>
      <module>dhy-service-rabc</module>
  </modules>

  <!--基础属性设置-->
  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <java.version>1.8</java.version>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
  </properties>

  <!-- 统一版本管理 -->
  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.5.RELEASE</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
      <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-dependencies</artifactId>
        <version>Hoxton.SR3</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
      <dependency>
        <groupId>dhy.xpy</groupId>
        <artifactId>dhy-service-common</artifactId>
        <version>1.0</version>
      </dependency>
      <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.1.4</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
      <dependency>
        <groupId>dhy.xpy</groupId>
        <artifactId>dhy-dao-service</artifactId>
        <version>1.0-SNAPSHOT</version>
      </dependency>
    </dependencies>
  </dependencyManagement>

  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-devtools</artifactId>
      <scope>runtime</scope>
      <optional>true</optional>
    </dependency>
    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <optional>true</optional>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
        <configuration>
          <excludes>
            <exclude>
              <groupId>org.projectlombok</groupId>
              <artifactId>lombok</artifactId>
            </exclude>
          </excludes>
        </configuration>
      </plugin>
    </plugins>
  </build>

</project>

新建Spring Boot项目

需要注意的是:eureka服务注册中心本身也是一个微服务,它是使用Spring Boot为基础服务框架搭建的。所以我们需要在父项目中新建一个Spring Boot子项目:dhy-server-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">
    <parent>
        <artifactId>StudyCloudWithMe</artifactId>
        <groupId>dhy.xpy</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>dhy-server-eureka</artifactId>

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

</project>
  • 注意这里spring Boot 2.0引入的包是spring-cloud-starter-netflix-eureka-server,正是该包帮助我们实现eureka服务注册中心。
  • 在Spring Boot 1.0版本引入的包是spring-cloud-starter-eureka,在Spring Boot2.0版本中不再使用。
  • 因为在父项目中进行了版本号的管理,所以子项目中不必写版本号。

修改application配置文件

server:
  port: 8761
  servlet:
    context-path: /eureka

spring:
  application:
    name: dhy-eureka-server

eureka:
  instance:
    hostname: localhost  #该服务部署的主机名称,参考windows的hosts文件或linux的/etc/hosts
  client:
    #是否从其他实例获取服务注册信息,因为这是一个单节点的EurekaServer,不需要同步其他的EurekaServer节点的数据,所以设置为false;
    fetch-registry: false
    #表示是否向eureka注册服务,即在自己的eureka中注册自己,默认为true,此处应该设置为false;
    register-with-eureka: false
    service-url:
      #设置与Eureka server交互的地址查询服务和注册服务都需要依赖这个地址。
      defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/

配置启动类注解

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

访问测试

访问http://localhost:8761/eureka/,出现如下页面表示我们服务注册中心搭建成功。

向服务注册中心注册服务

微服务注册客户端构建

在每个微服务中(dhy-service-rbac和dhy-service-sms)做如下的一些操作,将微服务注册到服务注册中心。通过maven引入eureka注册客户端。

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

配置application.yml

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/eureka/

注意:这里是两个eureka,我没写错。这里的url取决于eureka服务注册中心的配置。后文会说明。

在主启动类上添加@EnableEurekaClient注解

@EnableEurekaClient
public class DhyServiceSmsApplication {

当通过http://localhost:8761/eureka/访问服务注册中心用户界面的时候,出现如下红框中的微服务表示服务注册成功。

常见bug

  • 碰上这个报错,第一时间要取检查defaultZone配置的Url是否正确
  • 其次,要保证启动顺序,服务注册中心先启动,微服务后启动
  • 服务注册中心是否默认配置了Spring Security?(我们上一节没有配置相关安全认证)
  • 注意eureka客户端:http://localhost:8761/eureka/eureka/,有两个eureka。第一个eureka是server.servlet.context-path:
    /eureka。可以通过配置改变的;第二个eureka是服务注册的服务端点。
    这里我们没有配置server.servlet.context-path,因此不需要加两个eureka

第一个微服务调用

我们模拟一个短信发送的简单业务逻辑,业务不是重点,重点在于远程服务调用的演示

基础原理说明:

为了在dhy-service-rbac中更方便的调用远程服务,我们使用OpenFeign。它能使我们像调用本地函数一样调用远程服务api。

服务调用者基础配置(dhy-service-rbac)

通过maven坐标引入OpenFeign

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

在启动类上面加上注解

@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients
public class Main {

服务调用者业务实现(dhy-service-rbac)

在dhy-service-rbac本地服务中,伪装一个dhy-service-sms远程服务api接口。

@Component
@FeignClient("DHY-SERVICE-SMS")
public interface SmsService {
    @PostMapping("/sms/send")
    AjaxResponse send(@RequestParam("phoneNo") String phoneNo,@RequestParam("content") String content);
}

注意

  • "DHY-SERVICE-SMS"决定了我们要在当前服务中调用哪一个远程服务,"DHY-SERVICE-SMS"是远程服务的名称(spring.application.name)转大写,而且必须是大写。
  • 接口抽象方法定义决定了我们要远程调用"DHY-SERVICE-SMS"中的哪一个api,该抽象方法的定义就是一个"伪装"。将远程服务方法伪装成本地方法。

像调用本地服务一样调用伪装之后的远程服务

@RestController
public class SmsController {
    @Resource
    private SmsService smsService;
    
    @PostMapping("/sms/send")
    public AjaxResponse sendSms(@RequestParam("phoneNo") String phoneNo, @RequestParam("content") String content)
   {
       return smsService.send(phoneNo,content);
   }
}

在dhy-service-sms微服务模块中被真正调用的方法:

测试:

远程服务调用

HttpClient远程服务调用

实现服务进程之间的调用方法有很多,在SpringCloud之前常用如下:

  • HttpClient提供高效的、最新的、功能丰富的支持 HTTP 协议的客户端编程工具包。
  • RestTemplate设计是为了Spring更好的请求并解析Restful风格的接口返回值而设计的,对HttpClient进行了封装以提高其易用性。

在spring cloud体系中,先后出现了ribbon、Feign、OpenFeign等远程服务调用框架,易用性不断提高、功能也不断完善。这些内容在后面的章节中,会依次的详细介绍。

所以,我们要学习OpenFeign,实际上就是要学习

  • HttpClient或HttpUrlConnection或OkHttp
  • RestTemplate
  • Ribbon
  • Feign

先来看看最佳实践

下图是我们使用OpenFeign来定义的声明式远程服务调用接口,也是我们的最佳实践。我们要在心里面记住这六行代码,然后在本章中跟着我学习:Spring Cloud体系的远程服务调用是如何一步一步的从HttpClient 进化到 OpenFeign。

使用HttpClient实现远程服务调用

Junit测试类,远程调用:“/sms/send"短信发送接口服务。在测试之前,我们需要先把aservice-sms微服务启动起来。

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-test</artifactId>
   <scope>test</scope>
   <exclusions>
      <exclusion>
         <groupId>org.junit.vintage</groupId>
         <artifactId>junit-vintage-engine</artifactId>
      </exclusion>
   </exclusions>
</dependency>

执行测试用例之前,要确保已经maven引入了spring-boot-starter-test(Junit)

import com.fasterxml.jackson.databind.ObjectMapper;
import com.msg.AjaxResponse;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
import org.junit.jupiter.api.Test;

import java.io.*;
import java.util.ArrayList;
import java.util.List;

public class HttpClientTest {

  @Test
  void httpPost() throws Exception {
    //发送远程的http请求的地址
    String url = "http://localhost:2333/sms/send";
    //创建HttpClient对象
    CloseableHttpClient client = HttpClients.createDefault();
    //创建HttpPost对象, 发送post请求
    HttpPost method = new HttpPost(url);
    //封装发送到服务提供者的参数
    NameValuePair phoneNo = new BasicNameValuePair("phoneNo", "13214409773");
    NameValuePair content = new BasicNameValuePair("content", "HttpClient测试远程服务调用");
    List<NameValuePair> params = new ArrayList<>();
    params.add(phoneNo);
    params.add(content);
    //封装请求体数据
    method.setEntity(new UrlEncodedFormEntity(params, "UTF-8"));
    //发送具体的http请求
    HttpResponse response = client.execute(method);

    //获得服务提供者响应的具体数据
    HttpEntity entity = response.getEntity();

    //获得http的响应体
    InputStream is = entity.getContent();
    int len = 0;
    char[] buf = new char[1024];
    //使用字符流读---字节输入流转换为字符输入流
    Reader reader = new InputStreamReader(is);
    StringBuffer sb = new StringBuffer();
    while((len = reader.read(buf)) != -1){
      sb.append(String.valueOf(buf, 0, len));
    }

    //转成对象
    ObjectMapper mapper = new ObjectMapper();
    AjaxResponse ajaxResponse = mapper.readValue(sb.toString(), AjaxResponse.class);
    System.out.println(ajaxResponse);

  }

}

可以看到,使用HttpClient调用远程服务,有这样几个步骤:

  • 创建HttpClient对象
  • 请求参数、地址配置
  • 请求封装与发送
  • Http请求结果的获取HttpEntity
  • 获取Http请求结果中的响应体
  • 将响应体转成java对象

可以将HttpClient请求的代码,对比OpenFign最佳实践,代码实现的复杂度明显高的多。

RestTemplate远程服务调用

RestTemplate是Spring提供的一个访问Http服务的客户端类。从名称上来看,该类更多是针对RESTFUL风格API设计的。RestTemplate的底层实现仍然是HttpClient或HttpUrlConnection或OkHttp(三者可选),只是对它进行了封装,从而降低编码复杂度。

RestTemplate常用方法

RestTemplate提供的常用方法是以Http协议中的6个动词开头的:

HTTP Method常用方法描述
GETgetForObject发起GET请求响应对象
GETgetForEntity发起GET请求响应结果、包含响应对象、请求头、状态码等HTTP协议详细内容
POSTpostForObject发起POST请求响应对象
POSTpostForEntity发起POST请求响应结果、包含响应对象、请求头、状态码等HTTP协议详细内容
DELETEdelete发起HTTP的DELETE方法请求
PUTput发起HTTP的PUT方法请求

这些方法的名称清楚地表明它们调用的是哪个HTTP方法,而名称中包含的第二部分表示返回的内容。

远程服务调用

要使用RestTemplate ,必须是Spring环境,首先将它初始化为一个Bean。只做一次即可。

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

下面的Junit代码实现了使用RestTemplate发送Post请求,到“/sms/send”短信发送服务。从代码的实现复杂度上已经比使用HttpClient要简单了许多。

import com.dhy.Main;
import com.msg.AjaxResponse;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;

import javax.annotation.Resource;

//指定启动类
@ContextConfiguration(classes = Main.class)
@SpringBootTest
public class RestTemplateTest {

  @Resource
  private RestTemplate restTemplate;

  @Test
  void httpPostForObject() throws Exception {
    //发送远程http请求的url
    String url = "http://localhost:2333/sms/send";
    //发送到远程服务的参数
    MultiValueMap<String, Object> params = new LinkedMultiValueMap<>();
    params.add("phoneNo", "13214409773");
    params.add("content", "HttpClient测试远程服务调用");

    //通过RestTemplate对象发送post请求
    AjaxResponse ajaxResponse = restTemplate.postForObject(url, params, AjaxResponse.class);

    System.out.println(ajaxResponse);
  }
}

如果我们想获取请求结果中的更多Http协议信息,可以使用postForEntity方法。如下:

@Test
  void httpPostForEntity() throws Exception {
    //发送远程http请求的url
    String url = "http://localhost:2333/sms/send";
    //发送到远程服务的参数
    MultiValueMap<String, Object> params = new LinkedMultiValueMap<>();
    params.add("phoneNo", "13214409773");
    params.add("content", "HttpClient测试远程服务调用");

    //通过RestTemplate对象发送post请求
    ResponseEntity<AjaxResponse> entitys = restTemplate.postForEntity(url, params, AjaxResponse.class);

    System.out.println(entitys.getBody());

    //查看响应的状态码
    System.out.println(entitys.getStatusCodeValue());

    //查看响应头
    HttpHeaders headMap = entitys.getHeaders();
    for(Map.Entry<String, List<String>> m : headMap.entrySet()){
      System.out.println(m.getKey() + ": " + m.getValue());
    }
  }

RestTemplate底层实现的切换

RestTemplate底层实现最常用的有以下三种:

  • SimpleClientHttpRequestFactory(封装URLConnection,JDK自带,默认实现)
  • HttpComponentsClientHttpRequestFactory(封装第三方类库HttpClient)
  • OkHttp3ClientHttpRequestFactory(封装封装第三方类库OKHttp)

通常情况下,网上的资料认为OKHttp是目前执行效率最高的HTTP类库(笔者没实际测试过)。也就是下文代码中的最后一种。

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.client.OkHttp3ClientHttpRequestFactory;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;

@Configuration
public class ContextConfig {

    //默认实现,实际与urlConnection是一样的底层实现
    @Bean
    public RestTemplate restTemplate(){
        RestTemplate restTemplate = new RestTemplate();
        return restTemplate;
    }
    
   //默认实现
    @Bean("urlConnection")
    public RestTemplate urlConnectionRestTemplate(){
        RestTemplate restTemplate = new RestTemplate(new SimpleClientHttpRequestFactory());
        return restTemplate;
    }

    @Bean("httpClient")
    public RestTemplate httpClientRestTemplate(){
        RestTemplate restTemplate = new RestTemplate(new HttpComponentsClientHttpRequestFactory());
        return restTemplate;
    }

    @Bean("OKHttp3")
    public RestTemplate OKHttp3RestTemplate(){
        RestTemplate restTemplate = new RestTemplate(new OkHttp3ClientHttpRequestFactory());
        return restTemplate;
    }
}

在实际的应用中,只需要选择上面的代码中的其中一种RestTemplate Bean即可。

<dependency>
    <groupId>com.squareup.okhttp3</groupId>
    <artifactId>okhttp</artifactId>
    <version>4.7.2</version>
</dependency>

<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
    <version>4.5.12</version>
</dependency>

使用后两者远程连接工具,需要引入依赖

RestTemplate多实例负载均衡

1.当服务消费者需要调用某个远程微服务时,会先向服务注册中心询问该服务的注册信息(访问地址)

2.服务注册中心返回一个服务注册列表。即:服务提供者的访问地址

3.服务消费者按照自己的负载均衡算法,从注册列表中选出一个服务提供者实例,发起请求。
所谓负载均衡,用白话说就是一个好汉三个帮。一个A服务提供者压力大的时候,创建多个A服务提供者实例分担压力。
所谓负载均衡算法,用白话说就是具体到某一次的任务,在众多“好汉”中选择哪一个好汉来帮你,根据什么逻辑来选择。

本章内容主要是为大家介绍远程服务调用负载均衡,为了构建一个真实的多实例负载的远程服务调用,我们需要为服务提供者创建多个启动实例(服务于不同的端口)。

IDEA环境下微服务多端口多实例

第一个实例

在StudyCloudWithMe的应用中,目前dhy-service-sms是一个服务提供者,所以我们为它创建多个启动实例。如图:选择Edit Configurations

然后修改实例启动名称(加上端口),这样更容易辨认多个启动实例之间的区别。

勾选右上角的Allow parallel run(允许多实例运行),我的IDEA版本比较新,比较旧的版本该沟选项的名称叫做Single instance only(只允许单实例运行),所以此时应该把勾选去掉。

并且为启动实例加上虚拟机启动参数。指定实例的启动端口:

复制实例

有了第一个实例,我们通过复制创建第二个启动实例。同样修改实例启动名称和端口号,端口号要与之前的实例不一样。

这样我们第二个实例就创建好了,可以仿造这种方式创建三个或更多实例,只要你的硬件环境允许,并且是你需要的。

创建Compound

Compound的作用是多实例分组,同一组的实例可以分包一个包里面,同时启动。

将我们新建的两个dhy-service-sms实例包含到同一个Compound:dhy-service-sms里面

这样当我们启动Compound:dhy-service-sms的时候,2333和2444端口的微服务应用就一起启动了。

linux环境下微服务多端口多实例

上面的微服务多实例是在我们的开发环境中的实现,如果在linux生产环境下,我们可以通过如下命令启动一个微服务的多实例。当然,一般使用微服务的企业都会将微服务docker容器化,使用k8s做微服务的编排。所以以下的命令酌情使用。

java -jar  sms-service.jar -Dserver.port=2333
java -jar  sms-service.jar -Dserver.port=2444

基于RestTemplate的负载均衡

只需要在上一节的代码的基础上,加上@LoadBalanced注解即可实现远程服务调用的负载均衡。具体实现效果请看后文的测试。

加上@LoadBalanced注解之后,我们使用RestTemplate访问微服务的时候,就可以实现微服务多实例访问的负载均衡。注意:这里访问的地址是微服务的名称(大写),不再是某一个微服务实例的ip和端口。

先看一下@LoadBalanced注解的作用:

下文代码,在上一节中定义,此处做了1处修改:

远程服务调用负载均衡测试

为了使测试结果的显示更加直观,我们为“/sms/send”服务的控制台打印加上serverPort,从而能更清楚的看到响应结果。

启动如下实例:eureka服务注册中心、dhy-service compound多实例。然后我们会执行dhy-service-rbac中的测试用例:httpPostForObject

测试结果,当我们第一次执行上文的httpPostForObject单元测试的时候,2333实例的控制台结果如下:

当我们再一次执行上文的httpPostForObject单元测试的时候,2444实例的控制台结果如下。多次访问,如此循环,这是由负载均衡的策略决定的。

Ribbon调用流程源码解析

Spring Cloud Ribbon简介

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

Spring Cloud Ribbon 只是一个工具类框架,它不像服务注册中心、配置中心、API 网关那样需要独立部署,但是它几乎存在于每一个 Spring Cloud 构建的微服务和基础设施中。

实际上在上一节的代码学习中,我们已经使用了Ribbon,如上面的代码。可是我们并没有通过maven引入Ribbon相关的包啊?那是因为我们之前引入的spring-cloud-starter-netflix-eureka-client包中已经包含了ribbon。

如果你想单独引入Ribbon的话,可以通过如下maven坐标

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

Ribbon 模块

名 称说 明
ribbon-core一些比较核心且具有通用性的代码,客户端 API 的一些配置和其他 API 的定义。
ribbon-loadbalancer负载均衡模块,可独立使用,也可以和别的模块一起使用。 内置的负载均衡算法都实现在其中。
ribbon-eureka基于 Eureka 封装的模块,能够快速、方便地集成 Eureka。
ribbon-transport基于 Netty 实现多协议的支持,比如 HTTP、Tcp、Udp 等。
ribbon-httpclient基于 Apache HttpClient 封装的 REST 客户端,集成了负载均衡模块,可以直接在项目中使用来调用接口。
ribbon-exampleRibbon 使用代码示例,通过这些示例能够让你的学习事半功倍。

服务请求负载均衡调用流程源码解析

上文中在使用springcloud ribbon客户端负载均衡的时候,可以给RestTemplate bean 加一个@LoadBalanced注解,就能让这个RestTemplate在请求时拥有客户端负载均衡的能力。

LoadBalanced
/**
 * 该注解将RestTemplate bean标记为配置为使用LoadBalancerClient。
 */
@Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Qualifier
public @interface LoadBalanced {
}

加@LoadBalanced 注解的时候,也相当于加上了@Qualifier注解

通过源码可以发现这是一个LoadBalanced标记注解并且标记了@Qualifier(基于Spring Boot的自动配置机制),我们可以溯源到LoadBalancerAutoConfiguration自动装配类。

LoadBalancerAutoConfiguration
@Configuration(
    proxyBeanMethods = false
)
@ConditionalOnClass({RestTemplate.class})
@ConditionalOnBean({LoadBalancerClient.class})
@EnableConfigurationProperties({LoadBalancerRetryProperties.class})
public class LoadBalancerAutoConfiguration {

	@LoadBalanced
	@Autowired(required = false)
	private List<RestTemplate> restTemplates = Collections.emptyList();   

        ......
}

@LoadBalanced@Autowried结合使用,表示这里自动装配的Bean是所有加@LoadBalanced注解的RestTemplate。我们继续往下看,在LoadBalancerAutoConfiguration自动装配类中的后半段

@ConditionalOnMissingClass({"org.springframework.retry.support.RetryTemplate"})
static class LoadBalancerInterceptorConfig {
  LoadBalancerInterceptorConfig() {
  }
  
  //ribbon负载均衡拦截器
  @Bean
  public LoadBalancerInterceptor ribbonInterceptor(LoadBalancerClient loadBalancerClient, LoadBalancerRequestFactory requestFactory) {
    return new LoadBalancerInterceptor(loadBalancerClient, requestFactory);
  }

  //为RestTemplate配置HttpRequest拦截器
  @Bean
  @ConditionalOnMissingBean
  //向容器中放入一个RestTemplateCustomizer对象实例
  public RestTemplateCustomizer restTemplateCustomizer(final LoadBalancerInterceptor loadBalancerInterceptor) {
    return (restTemplate) -> {
      List<ClientHttpRequestInterceptor> list = new ArrayList(restTemplate.getInterceptors());
      list.add(loadBalancerInterceptor);
      //给当前传入的restTemplate中设置拦截器
      restTemplate.setInterceptors(list);
    };
  }
}

上面的代码含义已经很明显:RestTemplate发送请求的时候,被LoadBalancerInterceptor拦截器拦截。

这里restTemplate的拦截器机制是restTemplate的一个高级特性,感兴趣可以了解一下

所以我们想看一看LoadBalancerInterceptor都做了些什么事情?

LoadBalancerInterceptor
public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor {
    private LoadBalancerClient loadBalancer;
    private LoadBalancerRequestFactory requestFactory;

    public LoadBalancerInterceptor(LoadBalancerClient loadBalancer, LoadBalancerRequestFactory requestFactory) {
        this.loadBalancer = loadBalancer;
        this.requestFactory = requestFactory;
    }

    public LoadBalancerInterceptor(LoadBalancerClient loadBalancer) {
        this(loadBalancer, new LoadBalancerRequestFactory(loadBalancer));
    }

    public ClientHttpResponse intercept(final HttpRequest request, final byte[] body, final ClientHttpRequestExecution execution) throws IOException {
        //获取原生的URI
        URI originalUri = request.getURI();
        //获取URI主机部分的服务名
        String serviceName = originalUri.getHost();
        Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri);
        return 
        (ClientHttpResponse)this.loadBalancer.
       //执行请求,选择服务的过程中会采用响应的负载均衡策略
       execute(serviceName, 
       //请求工厂会创建出一个请求对象
       this.requestFactory.createRequest(request, body, execution));
    }
}

重点看intercept方法 当我们restTemplate执行请求操作时,就会被拦截器拦截进入intercept方法.在该方法里面,获取到了request.getURI(),它的值如:http://DHY-SERVICE-SMS/sms/send 。

并从URI中获取服务名称serviceName,如:DHY-SERVICE-SMS。然后执行execute方法,

我们可以猜想一下,这个方法里面做什么?

  • 根据服务名称ASERVICE-SMS去服务注册中心获取“服务信息列表”(带ip和端口的服务地址)
  • 从获取到的“服务信息列表”中根据“算法”,获取一个微服务实例Server对象
  • 向该微服务实例(ip + 端口)发起请求
RibbonLoadBalancerClient

那么我们继续往下面看execute方法,是不是和我们猜想的一样呢?
Server:下文代码中,Ribbon Server指的是“服务提供者”所代表的微服务。

//首先进入这个方法里面
public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException {
  return this.execute(serviceId, (LoadBalancerRequest)request, (Object)null);
}

//然后进入这个重载的方法
public <T> T execute(String serviceId, LoadBalancerRequest<T> request, Object hint) throws IOException {
  //1.根据serviceName获取负载均衡器(包含负载均衡策略)
  ILoadBalancer loadBalancer = this.getLoadBalancer(serviceId);
  //2.根据“负载均衡策略”获取一个微服务的Server
  Server server = this.getServer(loadBalancer, hint);
  if (server == null) {
    throw new IllegalStateException("No instances available for " + serviceId);
  } else {
    RibbonLoadBalancerClient.RibbonServer ribbonServer = new RibbonLoadBalancerClient.RibbonServer(serviceId, server, this.isSecure(server, serviceId), this.serverIntrospector(serviceId).getMetadata(server));
    return this.execute(serviceId, (ServiceInstance)ribbonServer, (LoadBalancerRequest)request);
  }
}

public <T> T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest<T> request) throws IOException {
  Server server = null;
  if (serviceInstance instanceof RibbonLoadBalancerClient.RibbonServer) {
    server = ((RibbonLoadBalancerClient.RibbonServer)serviceInstance).getServer();
  }

  if (server == null) {
    throw new IllegalStateException("No instances available for " + serviceId);
  } else {
    RibbonLoadBalancerContext context = this.clientFactory.getLoadBalancerContext(serviceId);
    RibbonStatsRecorder statsRecorder = new RibbonStatsRecorder(context, server);

    try {
      //3.前面的代码将Server转成了ServiceInstance微服务实例,并向其发送服务请求
      T returnVal = request.apply(serviceInstance);
      statsRecorder.recordStats(returnVal);
      return returnVal;
    } catch (IOException var8) {
      statsRecorder.recordStats(var8);
      throw var8;
    } catch (Exception var9) {
      statsRecorder.recordStats(var9);
      ReflectionUtils.rethrowRuntimeException(var9);
      return null;
    }
  }
}

注意看上文源码中的三处关键注释,基本上证实了我们之前的猜想。我们还遗留了一个问题,那就是负载均衡器是根据什么样的算法,或者是说根据什么样的策略去选择Server,如何在多实例的微服务中选择其中的一个实例作为请求对象?那我们就在下一节来看一下这个问题。

Ribbon负载均衡策略源码解析

名词解释
Server:在本文中,Ribbon Server指的是“服务提供者”所代表的微服务。

ILoadBalancer接口

在上一节我们为大家介绍了Ribbon的负载均衡实现的HTTP请求流程,其中有一个非常关键的接口就是ILoadBalancer 。本节就来重点的介绍一下该接口的实现逻辑:即如何实现负载均衡策略。

public interface ILoadBalancer {
    void addServers(List<Server> var1);

    Server chooseServer(Object var1);

    void markServerDown(Server var1);

    /** @deprecated */
    @Deprecated
    List<Server> getServerList(boolean var1);

    List<Server> getReachableServers();

    List<Server> getAllServers();
}
  • chooseServer()方法是根据key去获取Server;该方法是ILoadBalancer接口中最重要的一个方法,决定了如何使用“负载均衡算法”选择合适的微服务实例Server,进行远程服务调用。
  • addServers()方法是添加一个Server集合;
  • markServerDown()方法用来标记某个服务下线;
  • getReachableServers()获取可用的Server集合;
  • getAllServers()获取所有的Server集合。

下图是ILoadBalancer接口的实现类图。

在上一节中,我们介绍了RibbonLoadBalancerClient,其中调用了ILoadBalancer 接口的chooseServer方法。

在BaseLoadBalancer(ILoadBalancer接口实现类)中可以找到如下的chooseServer()的方法实现。

从上图中可以看出,chooseServer()选择微服务实例Server的过程是在rule.choose方法中实现的。这里的rule是一个IRule接口的实现类对象。

IRule接口就是Ribbon负载均衡中,提供负载均衡策略抽象方法的接口。其默认的负载均衡策略就是:轮询。

轮询用白话讲就是,服务提供者轮着被调用,平均分、你一次、我一次。

IRule接口

Rule接口是负载均衡的策略接口,它有三个方法:

  • choose()是根据key 来获取微服务server
  • setLoadBalancer()和getLoadBalancer()是用来设置和获取ILoadBalancer的
public interface IRule {
  Server choose(Object var1);

  void setLoadBalancer(ILoadBalancer var1);

  ILoadBalancer getLoadBalancer();
}

下图是IRule接口的所有实现类,即:Ribbon默认提供的负载均衡策略。

1)RoundRobinRule(默认)

轮询选择,轮询Server服务数组中index,选择 index 对应位置的Server。即:取数组下标,每次加1,保证数组中的备选Server依次被调用。

2)RandomRule

从多个备选Server(服务提供者),随机选择一个 Server。

3)RetryRule

对选定的负载均衡策略机加上重试机制,也就是说当选定了某个策略进行请求负载时在一个配置时间段内若选择 Server 不成功,则一直尝试使用 subRule 的方式选择一个可用的 Server。

4)WeightedResponseTimeRule

对RoundRobinRule扩展,根据响应时间分配一个 Weight(权重),响应时间越长,Weight 越小,被选中的可能性越低。

5)ResponseTimeWeightedRule

作用同 WeightedResponseTimeRule,ResponseTime-Weighted Rule 后来改名为WeightedResponseTimeRule。

6)BestAvailablRule

先过滤掉因为多次访问故障,而被标记为Error的Server。然后选择一个并发量(ActiveRequestCount)最小的Server。俗话说就是:先去掉不能干活的,然后在能干活的里面找一个最闲的。

7)AvailabilityFilteringRule

过滤掉那些一直连接失败的且被标记为 circuit tripped 的后端 Server,并过滤掉那些高并发的后端 Server或者使用一个 AvailabilityPredicate 来包含过滤 Server 的逻辑。其实就是检查 Status 里记录的各个Server 的运行状态 。

8)ZoneAvoidanceRule

使用 ZoneAvoidancePredicate 和 AvailabilityPredicate 来判断是否选择某个Server,前一个判断判定一个 Zone 的运行性能是否可用,剔除不可用的 Zone(的所有Server),AvailabilityPredicate 用于过滤掉连接数过多的 Server。

后两种方式,需要结合断路、超时等参数配置。使用起来比较复杂,容易进坑。从笔者观察的角度使用者也比较少(几乎没见过),基础的Rule(轮询、权重、BestAvailabl)策略,使用简单,运行也比较高效。

如果真的没有合适的负载均衡策略,我们还可以根据自己的算法进行自定义。后面章节就会为大家介绍到。

负载均衡策略配置

Ribbon默认的负载均衡策略是:轮询,如果我们想调整一下负载均衡策略,可以通过如下的配置。在“服务消费者”的服务中,做ribbon负载均衡策略的调整。

网上有很多版本的配置方式(基于注解的、基于配置文件的),目前最简单的方式就是:

dhy-service-rbac:
    ribbon:
     NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule

针对dhy-service-rbac对外调用服务提供者的服务,使用RandomRule策略。

Ribbon重试机制与饥饿加载

重试机制

因为在微服务的实际生产环境中,有可能出现一种情况:某些服务的其中部分实例临时不可用。

所以,我们希望能够提供一种重试的机制。比如:dhy-service-sms服务可以启动多个实例,当实例1由于某些原因(网络等)瞬时无法正常响应服务时,为了保证业务正常执行,所以自动的向该服务的其他实例发起请求。这就是微服务的调用的“重试机制”。

从Camden SR2版本开始,Spring Cloud整合了Spring Retry来增强RestTemplate的重试能力,对于开发者来说只需通过简单的配置,原来那些通过RestTemplate实现的服务访问就会自动根据配置来实现重试策略。

<dependency>
	<artifactId>spring-retry</artifactId>
	<groupId>org.springframework.retry</groupId>
</dependency>

重试机制只有在引入了RetryTemplate才会生效,在LoadBalancerAutoConfiguration配置中分别对RetryTemplate和RetryInterceptor进行配置加载。

关于重试机制的若干配置属性,都定义在LoadBalancerRetryProperties属性配置类中。

需要注意的是:Ribbon有重试机制,Feign和OpenFeign也有重试机制(后面章节会介绍)。Feign和OpenFeign的底层就是Ribbon,所以当项目使用了Feign或OpenFeign的重试机制,就不要开启Ribbon的重试机制,反之亦然。否则重试配置重叠,实际重试次数是二者的笛卡尔积。

#该参数用来开启重试机制,默认也是true。
spring.cloud.loadbalancer.retry.enabled: true

与重试机制相关的参数配置。需要注意的是:请求超时时间设置过短、同时服务响应太慢,也会导致请求重试,在实际应用中要格外注意。

.ribbon.ConnectTimeout:请求连接的超时时间。  
.ribbon.ReadTimeout:请求处理的超时时间。
.ribbon.OkToRetryOnAllOperations:对所有操作请求都进行重试,默认只对GET请求重试。  
.ribbon.MaxAutoRetriesNextServer:切换实例的重试次数,默认为1。  
.ribbon.MaxAutoRetries:对当前实例的重试次数。默认为1。

饥饿加载

很多初学者,在实现微服务的时候经常会遇到一个问题:服务消费方调用服务提供方接口的时候,第一次请求经常会超时,而之后的调用就没有问题了。

造成第一次服务调用出现失败的原因主要是Ribbon进行客户端负载均衡的Client并不是在服务启动的时候就初始化好的,而是在调用的时候才会去创建相应的Client,所以第一次调用的耗时不仅仅包含发送HTTP请求的时间,还包含了创建RibbonClient的时间,这样一来如果创建时间速度较慢,同时设置的超时时间又比较短的话,很容易就会出现上面所描述的现象。

我们可以在服务调用方dhy-service-rbac,通过如下的配置来解决问题:

ribbon:
  eager-load:
      enabled: true   #开启饥饿加载
      clients: dhy-service-sms  #饥饿加载的服务

在启动的时候就会去加载Ribbon Client及被调用服务上下文,从而在实际发送请求的时候就可以直接使用,从而提高第一次服务请求的访问速度。启动时候打印日志如下:

如果不配置饥饿加载,这段日志在第一次发起远程服务调用的时候才会出现。

Ribbon自定义负载均衡策略

上一节我们讲了Ribbon默认提供的一些负载均衡策略,为了满足更多的场景需求,我们也可以通过自己去实现IRule接口,来自定义负载均衡策略。

自定义负载策略的方法

通过实现 IRule 接口可以自定义负载策略,主要的选择服务逻辑在 choose 方法中。我们这边只是演示怎么自定义负载策略,所以没写选择的逻辑,直接返回服务列表中第一个服务。具体代码如下所示。

import com.netflix.loadbalancer.ILoadBalancer;
import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.Server;

import java.util.List;

public class MyLoadBanlanceRule implements IRule {
  private ILoadBalancer lb;

  @Override
  public Server choose(Object key) {
    List<Server> servers = lb.getAllServers();
    for (Server server : servers) {
      System.out.println(server.getHostPort());
    }
    //这里没写算法,只是将服务列表中的第一个Server返回
    return servers.get(0);
  }

  @Override
  public void setLoadBalancer(ILoadBalancer lb) {
    this.lb = lb;
  }

  @Override
  public ILoadBalancer getLoadBalancer() {
    return lb;
  }
}

在Spring Cloud中,可通过配置的方式使用自定义的负载策略,dhy-service-rbac 是服务调用者的服务名称。

dhy-service-rbac:
  ribbon:
    NFLoadBalancerRuleClassName: com.dhy.MyLoadBanlanceRule

如果上面失效了,换下面的写法,在启动类上加上该注解,手动指明,不能将对应的对象放入容器

//指定什么样的Ribbon客户端使用我们指定的轮询算法
@RibbonClient(name = "CLOUD-PAYMENT-SERVICE", configuration = MySelfRule.class)

将自定义的MyLoadBanlanceRule 类手动初始化为一个Bean,作为全局配置

@Bean
public IRule lbRule() {
    return new MyLoadBanlanceRule(); //自定义负载均衡规则
}

注意:如果容器中存在了一个IRule实现子类实例对象,那么就会选用该实例对象,即所有服务都会用这个,起不到特殊化定制的要求

源码追踪

这里IRule最终的配置是在RibbonClientConfiguration中:

这个配置类很重要,里面指定了很多默认的属性还有默认拉取数据的配置

@Configuration(
    proxyBeanMethods = false
)
@EnableConfigurationProperties
@Import({HttpClientConfiguration.class, OkHttpRibbonConfiguration.class, RestClientRibbonConfiguration.class, HttpClientRibbonConfiguration.class})
public class RibbonClientConfiguration {
    public static final int DEFAULT_CONNECT_TIMEOUT = 1000;
    public static final int DEFAULT_READ_TIMEOUT = 1000;
    public static final boolean DEFAULT_GZIP_PAYLOAD = true;
    @RibbonClientName
    private String name = "client";
    @Autowired
    private PropertiesFactory propertiesFactory;

    public RibbonClientConfiguration() {
    }

    @Bean
    @ConditionalOnMissingBean
    public IClientConfig ribbonClientConfig() {
        DefaultClientConfigImpl config = new DefaultClientConfigImpl();
        config.loadProperties(this.name);
        config.set(CommonClientConfigKey.ConnectTimeout, 1000);
        config.set(CommonClientConfigKey.ReadTimeout, 1000);
        config.set(CommonClientConfigKey.GZipPayload, true);
        return config;
    }

    @Bean
    @ConditionalOnMissingBean
    public IRule ribbonRule(IClientConfig config) {
        if (this.propertiesFactory.isSet(IRule.class, this.name)) {
            return (IRule)this.propertiesFactory.get(IRule.class, config, this.name);
        } else {
            ZoneAvoidanceRule rule = new ZoneAvoidanceRule();
            rule.initWithNiwsConfig(config);
            return rule;
        }
    }

    @Bean
    @ConditionalOnMissingBean
    public IPing ribbonPing(IClientConfig config) {
        return (IPing)(this.propertiesFactory.isSet(IPing.class, this.name) ? (IPing)this.propertiesFactory.get(IPing.class, config, this.name) : new DummyPing());
    }

    @Bean
    @ConditionalOnMissingBean
    public ServerList<Server> ribbonServerList(IClientConfig config) {
        if (this.propertiesFactory.isSet(ServerList.class, this.name)) {
            return (ServerList)this.propertiesFactory.get(ServerList.class, config, this.name);
        } else {
            ConfigurationBasedServerList serverList = new ConfigurationBasedServerList();
            serverList.initWithNiwsConfig(config);
            return serverList;
        }
    }

    @Bean
    @ConditionalOnMissingBean
    public ServerListUpdater ribbonServerListUpdater(IClientConfig config) {
        return new PollingServerListUpdater(config);
    }

    @Bean
    @ConditionalOnMissingBean
    public ILoadBalancer ribbonLoadBalancer(IClientConfig config, ServerList<Server> serverList, ServerListFilter<Server> serverListFilter, IRule rule, IPing ping, ServerListUpdater serverListUpdater) {
        return (ILoadBalancer)(this.propertiesFactory.isSet(ILoadBalancer.class, this.name) ? (ILoadBalancer)this.propertiesFactory.get(ILoadBalancer.class, config, this.name) : new ZoneAwareLoadBalancer(config, rule, ping, serverList, serverListFilter, serverListUpdater));
    }

    @Bean
    @ConditionalOnMissingBean
    public ServerListFilter<Server> ribbonServerListFilter(IClientConfig config) {
        if (this.propertiesFactory.isSet(ServerListFilter.class, this.name)) {
            return (ServerListFilter)this.propertiesFactory.get(ServerListFilter.class, config, this.name);
        } else {
            ZonePreferenceServerListFilter filter = new ZonePreferenceServerListFilter();
            filter.initWithNiwsConfig(config);
            return filter;
        }
    }

    @Bean
    @ConditionalOnMissingBean
    public RibbonLoadBalancerContext ribbonLoadBalancerContext(ILoadBalancer loadBalancer, IClientConfig config, RetryHandler retryHandler) {
        return new RibbonLoadBalancerContext(loadBalancer, config, retryHandler);
    }

    @Bean
    @ConditionalOnMissingBean
    public RetryHandler retryHandler(IClientConfig config) {
        return new DefaultLoadBalancerRetryHandler(config);
    }

    @Bean
    @ConditionalOnMissingBean
    public ServerIntrospector serverIntrospector() {
        return new DefaultServerIntrospector();
    }

    @PostConstruct
    public void preprocess() {
        RibbonUtils.setRibbonProperty(this.name, CommonClientConfigKey.DeploymentContextBasedVipAddresses.key(), this.name);
    }

    static class OverrideRestClient extends RestClient {
        private IClientConfig config;
        private ServerIntrospector serverIntrospector;

        protected OverrideRestClient(IClientConfig config, ServerIntrospector serverIntrospector) {
            this.config = config;
            this.serverIntrospector = serverIntrospector;
            this.initWithNiwsConfig(this.config);
        }

        public URI reconstructURIWithServer(Server server, URI original) {
            URI uri = RibbonUtils.updateToSecureConnectionIfNeeded(original, this.config, this.serverIntrospector, server);
            return super.reconstructURIWithServer(server, uri);
        }

        protected Client apacheHttpClientSpecificInitialization() {
            ApacheHttpClient4 apache = (ApacheHttpClient4)super.apacheHttpClientSpecificInitialization();
            apache.getClientHandler().getHttpClient().getParams().setParameter("http.protocol.cookie-policy", "ignoreCookies");
            return apache;
        }
    }
}

对于IRule的自动配置在在此:

@Bean
    @ConditionalOnMissingBean
    public ILoadBalancer ribbonLoadBalancer(IClientConfig config, ServerList<Server> serverList, ServerListFilter<Server> serverListFilter, IRule rule, IPing ping, ServerListUpdater serverListUpdater) {
        return (ILoadBalancer)(this.propertiesFactory.isSet(ILoadBalancer.class, this.name) ? (ILoadBalancer)this.propertiesFactory.get(ILoadBalancer.class, config, this.name) : new ZoneAwareLoadBalancer(config, rule, ping, serverList, serverListFilter, serverListUpdater));
    }
  • 如果我们在容器中放入了IRule实现子类,那么这里参数的IRule就会传入对应的值,然后走第二个new的分支

如果我们没有往容器中放入对应的实例对象,而是通过注解特殊化配置了某个服务用指定的轮询规则,那么源码流程是什么样的呢?

引入的类是重点

源码大家可以自行查看,这边只说功能

测试

执行了两次测试用例,两次都访问了同一个实例(dhy-service-sms2333),这跟我们的自定义的负载均衡策略是相匹配的。截图如下:

Feign与OpenFeign

其实OpenFeign的使用方式,已经为大家介绍过了,可以说是非常简单,就是@FeignClients注解加上Spring MVC注解的方式书写“伪装”接口函数,然后在业务需要的地方像使用本地方法一样调用接口函数。

实际上在OpenFeign出现之前,有一个阶段,开发者经常使用的是Netflix Feign。二者在使用方式、版本集成方面还是有一些差异性。本篇就为大家介绍一下。

Netlix Feign

  • Feign是Spring Cloud组件中的一个轻量级RESTful的HTTP服务客户端
  • Feign内置了Ribbon,用来做客户端负载均衡,去调用服务注册中心的服务。
  • Feign的使用方式是:使用Feign的注解定义接口,调用这个接口,就可以调用服务注册中心的服务
  • Feign本身不支持Spring MVC的注解,它有一套自己的注解。Feign的注解和用法请参考官方文档:https://github.com/OpenFeign/feign

下图中注释掉的部分是Feign的注解书写方式,没有注释掉的注解是Spring MVC的注解方式。

显然,Spring MVC的注解我们使用起来更加统一、方便、常用。

所以下文中的注释掉的这种注解的书写方式了解即可,已经没有必要学习使用了。

因为Feign在一些方面与Spring MVC常用习惯的兼容性不够好,Feign又是属于netflix的产品,该公司对于Spring Cloud社区的支持也逐渐减弱。所以Spring Cloud社区基于各种原因在Feign的基础上开发了OpenFeign。

Netflix Feign还是Open Feign?

1、maven坐标差异:

<dependency>
    <groupId>com.netflix.feign</groupId>
    <artifactId>feign-core</artifactId>
</dependency>

<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-core</artifactId>
</dependency>

2、官网地址差异: https://github.com/Netflix/feignhttps://github.com/OpenFeign/feign。不过现在访问https://github.com/Netflix/feign前者已经被重定向到了后者上。

3、发版历史:

  • Netflix Feign:1.0.0发布于2013.6,于2016.7月发布其最后一个版本8.18.0
  • Open Feign:首个版本便是9.0.0版,于2016.7月发布,然后一直持续发布到现在(未停止)

可以简单的理解:Netflix Feign仅仅只是改名成为了Open Feign而已,然后Open Feign项目在其基础上继续发展至今。9.0版本之前它叫Netflix Feign,自9.0版本起它改名叫Open Feign了。

Spring Cloud Feign还是Spring Cloud OpenFeign?

1、maven坐标差异:

<groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-feign</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

2、发版历史:

  • spring-cloud-starter-feign:2015.3发布1.0.0版本,2019.5.23发布器最后一个版本1.4.7.RELEASE
  • spring-cloud-starter-openfeign:2017.11发布其首个版本,版本号为:1.4.0.RELEASE。现在仍持续更新中,当下最新版为2.2.1.RELEASE

对于版本,可粗略的理解为:

  • spring-cloud-starter-openfeign是为Spring Cloud2.x准备的,只不过维持了一段时间的对1.x的兼容。
  • 而spring-cloud-starter-feign是专为Spring Cloud1.x服务。

Feign设计原理源码解析

在前面的章节主要为大家介绍了RestTemplate、Ribbon、Feign与OpenFeign。其中OpenFeign是实现远程接口调用目前的最佳实践方法。其接口定义如下:

@FeignClient("ASERVICE-SMS")
public interface SmsService {

    @PostMapping(value = "/sms/send")
    AjaxResponse send(@RequestParam("phoneNo") String phoneNo,
                      @RequestParam("content") String content);

}

那么,OpenFeign是如何将这样一个接口定义,转化为HTTP请求发送出去,又如何正确的解析远程服务的响应结果呢?那我们就需要为大家介绍一下OpenFeign的核心设计原理。

请求响应流程处理

  • 解析接口定义:并将解析的结果反射为方法。比如:通过注解定义post请求,反射结果就应该是RestTemplate的Post方法,而不应该是GET方法。
  • 根据Contract去解析接口定义:通过前面的讲解,目前一共有两种服务接口定义规范。一种是Default的feign的实现,一种是基于SpringMVC注解的SpringMvcContract。两种定义方式,导致的解析过程肯定是不一样的。
  • 报文编码/解码:比如:发送请求将参数转化为JSON,接受响应将JSON结果转化为返回值Bean。
  • 拦截器:为了方便网络传输,通常将http协议内容压缩。在发送的时候压缩,在响应的时候解压缩
  • 日志增强:Feign还为我们提供了日志的增强功能,方便我们查看请求内容及响应结果信息。

根据Contract解析接口定义

上面已经为大家介绍了书写Feign接口定义的两种方式,其中我们最常用的还是Spring MVC注解的方式。

  • org.springframework.cloud.openfeign.support.SpringMvcContract用来将Spring MVC的注解解析为MethodMetadata。
  • feign.Contract.Default用来将Feign的注解(上图注释的部分)解析为MethodMetadata。

MethodMetadata代表了该调用那个方法、采用哪个HTTP方法、请求头信息是什么等等信息。

HTTP内容格式编解码

需要注意的是在Encoder/ Decoder 实现实现类中,有一类叫做SpringDecoder和SpringEncoder。来看一下它的代码,从代码中我们已经可以明确的看到支持使用HttpMessageConverter进行请求响应内容的格式处理转换。

也就是说,我们在Spring MVC注解中常用的JSON、XML等数据格式,在接口定义中都可以被支持。

拦截器

此外Feign还为我们定义了拦截器,帮助我们实现请求响应内容的gzip压缩与解压缩。注意这些拦截器不是Spring的拦截器,而是feign.RequestInterceptor。

日志增强

因为我们的微服务调用,都是在服务之间进行的。

不是我们传统意义上的,从浏览器请求服务,所以当服务请求出现异常的时候,我们需要查看服务的HTTP详细调用信息。

为此Feign为我们提供了日志增强接口。

提供了四种日志级别:

级别说明
NONE不输出任何日志
BASIC只输出Http 方法名称、请求URL、返回状态码和执行时间
HEADERS输出Http 方法名称、请求URL、返回状态码和执行时间 和 Header 信息
FULL记录Request 和Response的Header,Body和一些请求元数据

在理解了上面的这些Feign的请求相应流程及设计原理之后,我们做关于OpenFeign的各种配置才能更加的游刃有余。下一节就为大家介绍OpenFeign的相关配置。

Feign请求压缩与超时等配置

本文主要为大家介绍一下Feign相关的一些配置,在开始讲解之前,有一点是我们需要说明的:因为Feign的底层是基于Ribbon实现的,所以Ribbon配置在OpenFeign或者Feign的环境下,依然是生效的。

如何替换HTTP客户端实现

在之前的章节已经为大家讲过,RestTemplate的底层HTTP客户端实现有三种:

  • SimpleClientHttpRequestFactory(封装URLConnection,JDK自带,默认实现)
  • HttpComponentsClientHttpRequestFactory(封装第三方类库HttpClient)
  • OkHttp3ClientHttpRequestFactory(封装封装第三方类库OKHttp)

Feign 默认底层通过JDKjava.net.HttpURLConnection实现了feign.Client接口类,在每次发送请求的时候,都会创建新的HttpURLConnection 链接,这也就是为什么默认情况下Feign的性能很差的原因。

我们可以通过配置,在Feign中使用Apache HttpClient 或者OkHttp3等基于连接池的高性能Http客户端。

这个几乎是所有基于Spring CloudHTTP的微服务项目提升性能必做的步骤。

HTTPClient

那么如何在Feign中使用HttpClient的框架呢?

我们查看FeignAutoConfiguration.HttpClientFeignConfiguration的源码:

spring-cloud-netflix-core包中的org.springframework.cloud.netflix.feign.FeignAutoConfiguration.java

  • 从代码@ConditionalOnClass({ApacheHttpClient.class})注解可知,需要在pom文件上加上HttpClient的maven依赖
  • 需要在配置文件中配置feign.httpclient.enabled为true,从@ConditionalOnProperty注解可知,这个配置可以不写,因为在默认情况下就为true。

所以通常我们只需要在pom文件中引入feign-httpclient即可,该包里面包含apache httpclient。

<dependency> 
    <groupId>io.github.openfeign</groupId> 
    <artifactId>feign-httpclient</artifactId> 
</dependency>

使用OkHttp(推荐)

FeignAutoConfiguration.HttpClientFeignConfiguration的源码:

  • 从代码@ConditionalOnClass({OkHttpClient.class})注解可知,需要在pom文件上加上HttpClient的maven依赖
  • 从@ConditionalOnProperty注解可知,需要在配置文件中配置feign.okhttp.enabled为true。
<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-okhttp</artifactId>
</dependency>

请求压缩

Spring Cloud Feign支持对请求和响应进行GZIP压缩,以提高网络传输效率,配置方式如下:

# 配置请求GZIP压缩
feign.compression.request.enabled=true
# 配置响应GZIP压缩
feign.compression.response.enabled=true
# 配置压缩支持的MIME TYPE
feign.compression.request.mime-types=text/xml,application/xml,application/json
# 配置压缩数据大小的下限
feign.compression.request.min-request-size=2048

为什么要配置压缩数据大小的下限?

因为压缩操作本身也是要耗时的,对于数据量较小的HTTP请求或响应进行压缩,反而会造成性能下降。

日志配置

SpringCloudFeign为每一个FeignClient都提供了一个feign.Logger实例。可以根据logging.level.<FeignClient>参数配置格式来开启Feign客户端的DEBUG日志,其中<FeignClient>Feign客户端定义接口的完整路径。如:

logging:
  level: 
    com.aservice.rbac.feign.SmsService: debug

在创建feign client的时候,就创建了logger, 默认logger的名称是创建feign client的服务接口类的全路径,通俗的来讲就是加了@FeignClient接口类的全路径

然后再在配置类(比如主程序入口类)中加入Looger.Level的Bean

@Bean
public Logger.Level feignLoggerLevel(){
    return  Logger.Level.FULL;
}
级别说明
NONE不输出任何日志
BASIC只输出Http 方法名称、请求URL、返回状态码和执行时间
HEADERS输出Http 方法名称、请求URL、返回状态码和执行时间 和 Header 信息
FULL记录Request 和Response的Header,Body和一些请求元数据

这是spring cloud官方文档说明

请求日志的打印结果,更加详细,方便我们进行接口调试。

请求重试

需要注意的是:Ribbon有重试机制(前面已经介绍),Feign和OpenFeign也有重试机制。Feign和OpenFeign的底层就是Ribbon。所以当项目使用了Feign或OpenFeign的重试机制,就不要开启Ribbon的重试机制,反之亦然。否则重试配置重叠,实际重试次数是二者的笛卡尔积。

Feign 内置了一个重试器,当HTTP请求出现IO异常时,Feign会有一个最大尝试次数发送请求。重试器有如下几个控制参数:

重试参数说明默认值
period初始重试时间间隔,当请求失败后,重试器将会暂停 初始时间间隔(线程 sleep 的方式)后再开始,避免强刷请求,浪费性能100ms
maxPeriod当请求连续失败时,重试的时间间隔将按照:long interval = (long) (period * Math.pow(1.5, attempt - 1));计算,按照等比例方式延长,但是最大间隔时间为 maxPeriod, 设置此值能够避免 重试次数过多的情况下执行周期太长1000ms
maxAttempts最大重试次数5

具体的代码实现可参考:

https://github.com/OpenFeign/feign/blob/master/core/src/main/java/feign/Retryer.java

feign的重试机制默认是关闭的,源码如下

//FeignClientsConfiguration.java
	@Bean
	@ConditionalOnMissingBean
	public Retryer feignRetryer() {
		return Retryer.NEVER_RETRY;   #不要重试,就是关闭的意思
	}

当spring容器中不存在retryer这个实例的时候,会初始化这个bean, NEVER_RETRY(永远不重试)。如果想要开启的话,配置如下:

@Bean
    public Retryer feignRetryer() {
        return new Retryer.Default();
    }

在你的配置类中,添加如上代码就可以开启Feign的重试机制。但是,正如我们前文所讲的,Ribbon有重试机制(前面章节已经介绍),Feign和OpenFeign也有重试机制。二选一,通常使用Ribbon的重试机制即可,不要打开Feign的重试机制。

总结

本篇,主要简单介绍了Eureka服务注册中心和远程服务调用,为下一篇的服务注册与发现深入介绍奠定基础

Spring cloud官方文档

相关文章

微信公众号

最新文章

更多

目录