Eureka原理和应用(结合Feign)

基本概念

Eureka

Eureka出现原因

如果后端之间想要调用服务,其中一种方式就是通过 HTTP 调用,而一般情况下,url 会以硬编码的方式出现在代码中的,但如果开发环境发生了变化,导致地址改变,或者对应的服务可能是以集群的方式出现,即 url 可能有多个,那么此时代码是无法满足需求的。因此出现了以下几个问题:

① 服务消费者该如何获取服务提供者的地址信息?
② 如果有多个服务提供者,消费者该如何选择?
③ 消费者如何得知服务提供者的健康状态?

因此,出现了注册中心这一中间件,其中就包括 Eureka

Eureka原理

在Eureka架构中,微服务角色有两类:

1.EurekaServer:服务端,注册中心。可以记录服务信息以及实现心跳监控

2.EurekaClient:客户端。其中又分为以下两类:

(1)Provider:服务提供者。每一个服务提供者会注册自己的信息到 EurekaServer,并且每隔 30s 向 EurekaServer 发送心跳

(2)Consumer:服务消费者。每一个服务消费者会根据服务名称从 EurekaServer 拉取服务列表,并基于服务列表做负载均衡,选中一个微服务后发起远程调用

因此对于以上三个问题的回答:

1.服务消费者该如何获取服务提供者的地址信息:

服务提供者启动时向 Eureka 注册自己的信息,Eureka 保存这些信息,消费者根据服务名称向 Eureka 拉取提供者信息

2.如果有多个服务提供者,消费者该如何选择:

服务消费者利用负载均衡算法,从服务列表中挑选一个

3.消费者如何感知服务提供者健康状态:

服务提供者会每隔 30s 向 EurekaServer 发送心跳请求,报告健康状态,Eureka 会更新记录服务列表信息,心跳不正常会被剔除。这样消费者就可以拉取到最新的信息

Feign

介绍

在传统的调用 http 方法中,我们即使使用了注册中心,也需要手动编写 URL、请求参数、请求头等代码,例如:

String url = "http://userservice/user/" + order.getUserId();

存在下面的问题:

① 代码可读性差,编程体验不统一
② 参数复杂,URL 难以维护

Feign 是一个声明式(类似于Spring的声明式事务,只需要在配置文件中告诉Spring对谁加事务,把规则定义好,就可以让Spring完成事务的开启与关闭等操作,而不用去手动处理)的 http 客户端,官方地址:https://github.com/OpenFeign/feign,其作用就是帮助我们优雅的实现 http 请求的发送,解决上面提到的问题

一般我们会使用 Feign 来配合注册中心的使用,以更方便地进行服务发现、负载均衡和熔断处理等

使用案例

1.引入依赖(注意,需要先指定 Spring Cloud 版本)
SpringCloud 的版本需要与 SpringBoot 版本对应,可以查看官网https://spring.io/projects/spring-cloud

image-20240720085123837

<dependencyManagement>
        <!--指定Spring Cloud版本-->
        <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>
<dependencies>   
    <!--Feign客户端依赖-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
</dependencies>

2.在启动类中添加注解 EnableFeignClients,开启 Feign 的功能

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

3.编写 Feign 的客户端

@FeignClient("userservice")
public interface UserClient {
    @GetMapping("/user/{id}")
    User findById(@PathVariable Long id);
}

这里需要写想要远程调用的接口(也就是服务提供者对应的接口)的参数、url 等,写法与 SpringMVC 中定义接口的写法是类似的,其中 userservice 是对应的服务名,而注册中心会将这个服务名转换成对应的 ip 和端口

这里示例的接口的含义就表示,如果调用了该接口,就会使用 HTTP 进行远程调用,信息如下:

① 服务名称:userservice
② 请求方式:GET
③ 请求路径:/user/{id}
④ 请求参数:Long id
⑤ 返回值类型:User

4.在消费者方法里调用 Feign 客户端里的接口

@Autowired
private UserClient userClient;

public Order queryOrderById(Long orderId) {
        // 1.查询订单
        Order order = orderMapper.findById(orderId);
        //2.用Feign远程调用
        User user = userClient.findById(order.getUserId());
        //3.封装user和order
        order.setUser(user);
        // 4.返回
        return order;
}

自定义配置

Feign 运行自定义配置来覆盖默认配置,可以修改的配置如下:

类型作用说明
feign.Logger.Level修改日志级别包含四种不同的级别:NONE、BASIC、HEADERS、FULL
feign.codec.Decoder响应结果的解析器http 远程调用的结果做解析,例如解析 json 字符串为 java 对象
feign.codec.Encoder请求参数编码将请求参数编码,便于通过http请求发送
feign.Contract支持的注解格式默认是 SpringMVC 的注解
feign.Retryer失败重试机制请求失败的重试机制,默认是没有,不过会使用 Ribbon 的重试

Logger.Level:NONE 指的是没有任何日志(默认);BASIC 指的是会记录请求发送和结束的时间,以及耗时多久;HEADERS 指的是除了记录基本信息以外,还会带上请求头和响应头信息;FULL 则指的是除了记录请求头响应头的信息以外,还会记录请求体响应体等信息

Decoder:当我们发送一个请求时,响应回来的默认是个 json,而Decoder 的作用就是将这个 json 转变为对象

Encoder:将各式各样的请求参数转变为请求体

Contract:规定 Feign 支持哪种注解

Retryer:失败重试,例如请求端口 8081 时失败,就会去尝试 8082。Feign 默认不设置失败重试机制

一般我们需要配置的就是日志级别。调试错误时可以使用 FULL,但一般情况下,建议使用 BASIC 或 NONE,因为记录日志也会消耗一定的性能

配置方式:

1.配置文件方式

(1)全局生效

feign:
  client:
    config:
      default: #这里用 default 就是全局配置,如果是写服务名称,则是针对某个微服务的配置
        loggerLevel: FULL #日志级别

(2)局部生效

feign:
  client:
    config:
      userservice: #这里用 default 就是全局配置,如果是写服务名称,则是针对某个微服务的配置
        loggerLevel: FULL #日志级别

2.java 代码方式

需要先声明一个Bean:

public class DefaultFeignConfiguration {
    @Bean
    public Logger.Level logLevel() {
        return Logger.Level.BASIC;
    }
}

如果是全局配置,则把它放到 @EnableFeignClients 这个注解中

@EnableFeignClients(defaultConfiguration = DefaultFeignConfiguration.class)

如果是局部配置,则把它放到 @FeignClient 这个注解中

@FeignClient(value = "userservice", configuration = DefaultFeignConfiguration.class)

性能优化

Feign 底层的客户端实现:

URLConnection:默认实现,不支持连接池
Apache HttpClient:支持连接池
OKHttp:支持连接池

因此优化 Feign 的性能主要包括:

① 使用连接池代替默认的 URLConnection
日志级别,最好用 basic 或 none

连接池配置方式:

1.引入依赖

<!--HttpClient依赖-->
<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-httpclient</artifactId>
</dependency>

2.配置连接池

feign:
  httpclient:
    enabled: true #支持HttpClient的开关
    max-connections: 200 #最大连接数
    max-connections-per-route: 50 #单个路径的最大连接数

最佳实践

最佳实践指的是企业在使用一个东西的过程中,经过不断的踩坑后,总结出来的一个相对比较好的实用方式

关于 Feign 的最佳实践主要有两种方式:

1.继承

给消费者的 FeignClient 和提供者的 Controller 定义统一的父接口作为标准

由于消费者的 Feign 远程调用函数声明和提供者的 Controller 函数声明格式必须是一样的,因此可以定义一套统一的父接口

示例:

Feign最佳实践方式一

但这样会有一定的问题:

① 服务紧耦合(消费者的 Feign 远程调用和提供者的 Controller)
② 接口参数列表中的映射不会被继承(因为即使实现了接口,还是得重新书写一遍参数)

2.抽取

将FeignClient抽取为独立模块,并且把接口有关的实体类、默认的 Feign 配置都放到这个模块中,提供给所有消费者使用

直接使用 Feign 的方式在于,对于不同的微服务来说,可能要调用另外的一个相同的微服务,因此都要在里面写上 Feign 客户端类。如果微服务越来越多,Feign 客户端就要重写很多遍,导致重复开发

Feign最佳实践方案2出现的问题

解决方式就是将其抽取成一个 feign-api,这个 api 中包含了 Feign 客户端类,同时也可以将返回值类、默认配置等全部封装在这个 api 中。将来这些微服务只需要引依赖,然后再进行远程调用即可。这样可以降低耦合度

Feign最佳实践方案2解决的方式

但这样的方式同样也有问题,比如说有些微服务可能只需要使用这个 api 中的某几个方法,但是引依赖是需要将所有的方法都引入进来的,就会显得有点多余

代码案例

EurekaServer

1.在 EurekaServer 的项目中引入 Server 依赖(注意,需要先指定 Spring Cloud 版本)
SpringCloud 的版本需要与 SpringBoot 版本对应,可以查看官网https://spring.io/projects/spring-cloud

image-20240720085123837

<dependencyManagement>
        <!--指定Spring Cloud版本-->
        <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>
<!--Eureka-Server-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>

2.编写启动类,添加 @EnableEurekaServer 注解

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

3.编写application.yml 配置文件

server:
  port: 10086 # 服务端口
spring:
  application:
    name: server # eureka服务的服务名称
eureka:
  client:
    service-url: # eureka的地址信息
      defaultZone: http://127.0.0.1:10086/eureka

Provider

1.在 Provider 的项目中引入 Client 依赖

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

2.编写 application.yml 配置文件

spring:
  application:
    name: provider # user服务的服务名称
eureka:
  client:
    service-url: # eureka的地址信息
      defaultZone: http://127.0.0.1:10086/eureka
server:
  port: 8081

3.编写 Controller 类中的服务方法

@RestController
@RequestMapping("/provider")
@Slf4j
public class WebController {

    @GetMapping("/user")
    public String getUser() {
        log.info("调用provider方法");
        return "张三";
    }

}

Consumer

1.在 Consumer 的项目中引入 Client、Feign、HttpClient 依赖

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

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

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

2.编写 application.yml 配置文件

spring:
  application:
    name: consumer # user服务的服务名称
eureka:
  client:
    service-url: # eureka的地址信息
      defaultZone: http://127.0.0.1:10086/eureka
feign:
  httpclient:
    enabled: true # 支持HttpClient的开关
    max-connections: 200 # 最大连接数
    max-connections-per-route: 50 # 单个路径的最大连接数

3.编写 Feign 客户端接口

@FeignClient("provider")
public interface UserClient {

    @GetMapping("/provider/user")
    String getUser();

}

@FeignClient 里的 provider 是对应的提供者的服务名,注册中心会将这个服务名转换成对应的 ip 和端口

4.编写 Controller 类中的消费方法

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

    @Autowired
    private UserClient userClient;

    @GetMapping("/user")
    public String getUser() {
        log.info("调用consumer方法");
        return userClient.getUser();
    }

}

测试

1.启动 Server 服务

使用浏览器进入http://localhost:10086/,可以看到 Eureka 的控制界面如下:

image-20240719113843765

2.启动 Consumer 和 Provider

这里我们只启动一个 Consumer 实例,但我们如果要测试负载均衡的效果,需要开启多个 Provider 实例。我们可以修改 IDEA 端口配置,模拟多实例

image-20240719114046977

如上图所示,将第二个 Provider 实例的端口修改成了 8082。我这里开启了三个实例,同理也把第三个实例的端口改成 8083

之后进入 Server 的控制界面:

image-20240719114248166

可以看到所有的服务都已经启动了

3.使用 Postman 测试

这里用 Postman 对 Consumer 发起 HTTP 请求

image-20240719114349763

可以看到结果已经正确返回了,再到 IDEA 的控制台查看

image-20240719114437426

可以看到是第三个 Provider 实例收到了请求

然后重复发送三次,可以得到如下结果:

image-20240719114524167

image-20240719114535714

image-20240719114541277

可以看到每个实例均收到了一次请求,因此可以看出,默认的负载均衡策略是轮询

  • 17
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值