Nacos、Loadbalancer 和 OpenFeign 实现跨服务的调用
一、创建项目
创建父项目,引入springboot,springcloud,springcloudalibaba,再分别创建两个module, producer-serv和customer-serv,目的是实现customer服务中的changeValue接口访问producer服务中的addPrefix接口实现在名字前面加上前缀,注意sringcloud和springcloudalibab的版本对应关系。
附上项目源码地址
父项目版本选择如下
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>2020.0.1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2021.1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
<!-- 省略部分代码 -->
</dependencyManagement>
两个简单接口
@RestController
@Slf4j
@RequestMapping("customer")
public class CustomerController {
@Autowired
private CustomerService customerService;
@GetMapping("/changeValue")
public String changeValue(@RequestParam("name") String name){
return customerService.changeValue(name);
}
}
@Slf4j
@RestController
@RequestMapping("/producer")
@RefreshScope
public class ProducerController {
@Value("${defaultValue:Hello}")
private String setPrefix;
@Autowired
private ProducerService producerService;
@GetMapping("/addPrefix")
public String addPrefix(@RequestParam("name") String name) {
return producerService.addPrefix(name, setPrefix);
}
}
二、启动Nacos创建命名空间
-
Nacos启动,参考
Nacos集群搭建
篇,注意启动模式 -
Nacos创建命名空间
命名空间的作用相当于spring中profile,默认是在public这个命名空间下在下图右侧导航栏找到“命名空间”页面,进入该页面点击“新增命名空间”按钮,分别创建三个不同的环境:production、pre-production 和 dev,用来表示生产环境、预发环境和开发环境。在创建 namespace 的过程中,一定要保证命名空间的 ID 和项目中的 namespace 属性是一致的。
三、配置nacos地址并指定命名空间
在两个项目pom中引入spring-cloud-starter-alibaba-nacos-discovery
<!-- Nacos服务发现 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
同时在两个项目yml添加如下配置文件,注意端口号和应用名改一下
server:
port: 8081
spring:
application:
name: customer-serv
cloud:
nacos:
discovery:
# Nacos的服务注册地址,可以配置多个,逗号分隔
server-addr: 127.0.0.1:8848
# 服务注册到Nacos上的名称,一般不用配置
#service: customer-serv
# nacos客户端向服务端发送心跳的时间间隔,时间单位其实是ms
heart-beat-interval: 5000
# 服务端没有接受到客户端心跳请求就将其设为不健康的时间间隔,默认为15s
# 注:推荐值该值为15s即可,如果有的业务线希望服务下线或者出故障时希望尽快被发现,可以适当减少该值
heart-beat-timeout: 20000
# 元数据部分 - 可以自己随便定制
metadata:
mydata: testmydata
# 客户端在启动时是否读取本地配置项(一个文件)来获取服务列表
# 注:推荐该值为false,若改成true。则客户端会在本地的一个
# 文件中保存服务信息,当下次宕机启动时,会优先读取本地的配置对外提供服务。
naming-load-cache-at-start: false
# 命名空间ID,Nacos通过不同的命名空间来区分不同的环境,进行数据隔离,
namespace: dev
# 创建不同的集群
cluster-name: Cluster-A
# [注意]两个服务如果存在上下游调用关系,必须配置相同的group才能发起访问
group: myGroup
# 向注册中心注册服务,默认为true
# 如果只消费服务,不作为服务提供方,倒是可以设置成false,减少开销
register-enabled: true
启动测试,看是否两个服务已经注册到了nacos对应的命名空间中
四、远程服务调用
这里采用OpenFeign,在微服务架构的应用中用来实现远程调用,负载均衡和服务容错。OpenFeign基本介绍参考远程服务调用篇,在customer服务中引入Feign和loadbalancer对应的starter
<!-- Spring Cloud御用负载均衡组件Loadbalancer -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<!-- OpenFeign组件 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
创建OpenFeign接口
@FeignClient(value="producer-serv", path = "/producer")
public interface ProducerService {
@GetMapping("/addPrefix")
public String addPrefix(@RequestParam("name") String name);
}
在上面的代码中,我们在接口上声明了一个 FeignClient 注解,它专门用来标记被 OpenFeign 托管的接口。
在 FeignClient 注解中声明的 value 属性是目标服务的名称,在代码中我指定了 producer-serv,你需要确保这里的服务名称和 Nacos 服务器上显示的服务注册名称是一样的。
此外,FeignClient 注解中的 path 属性是一个可选项,如果你要调用的目标服务有一个统一的前置访问路径,比如 producer-serv 所有接口的访问路径都以 /producer 开头,那么你可以通过 path 属性来声明这个前置路径,这样一来,你就不用在每一个方法名上的注解中带上前置 Path 了。
在项目的启动阶段,OpenFeign 会查找所有被 FeignClient 注解修饰的接口,并代理该接口的所有方法调用。当我们调用接口方法的时候,OpenFeign 就会根据方法上定义的注解自动拼装 HTTP 请求路径和参数,并向目标服务发起真实调用。
因此,我们还需要在方法上定义 spring-web 注解(如 GetMapping、PostMapping),让 OpenFeign 拼装出正确的 Request URL 和请求参数。这时你要注意,OpenFeign 接口中定义的路径和参数必须与你要调用的目标服务中的保持一致。
远程服务调用
@Service
public class CustomerServiceImpl implements CustomerService {
@Autowired
private ProducerService producerService;
public String changeValue(String name) {
String res = "";
res = producerService.addPrefix(name);
return res;
}
}
被 FeignClient 注解修饰的对象,也会被添加到 Spring 上下文中。因此我们可以通过 Autowired 注入的方式来使用这些接口。
配置开启OpenFeign
我们打开 customer-serv 项目的启动类,你可以通过在类名之上添加一个EnableFeignClients 注解
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class CustomerServApplication {
public static void main(String[] args) {
SpringApplication.run(CustomerServApplication.class, args);
}
}
当然也可以给EnableFeignClients配置加载路径
// 通过指定包路径加载
@EnableFeignClients(basePackages = {"com.wz"})
// 通过指定Client类来加载
@EnableFeignClients(clients = {ProducerService.class})
// 扫描特定类所在的包路径下的FeignClient
@EnableFeignClients(basePackageClasses = {ProducerService.class})
五、启动测试
访问http://localhost:8081/customer/changeValue?name=Miles
,会默认在name参数值前面加上Hello
六、OpenFeign其他用法
6.1 日志信息打印
首先,你需要在配置文件中指定 FeignClient 接口的日志级别为 Debug。这样做是因为 OpenFeign 组件默认将日志信息以 debug 模式输出,而默认情况下 Spring Boot 的日志级别是 Info,因此我们必须将应用日志的打印级别改为 debug 后才能看到 OpenFeign 的日志。
打开customer-serv 模块的 application.yml 配置文件,在其中加上以下几行 logging 配置项。
logging:
level:
com.wz.customerserv.feign.ProducerService: debug
接下来,你还需要在应用的上下文中使用代码的方式声明 Feign 组件的日志级别。这里的日志级别并不是我们传统意义上的 Log Level,它是 OpenFeign 组件自定义的一种日志级别,用来控制 OpenFeign 组件向日志中写入什么内容。你可以打开 customer-serv 模块的 Configuration 配置类,在其中添加这样一段代码。
@Bean
Logger.Level feignLogger() {
return Logger.Level.FULL;
}
在上面这段代码中,我指定了 OpenFeign 的日志级别为 Full,在这个级别下所输出的日志文件将会包含最详细的服务调用信息。OpenFeign 总共有四种不同的日志级别,我来带你了解一下这四种级别下 OpenFeign 向日志中写入的内容。
-
NONE:不记录任何信息,这是 OpenFeign 默认的日志级别;
-
BASIC:只记录服务请求的 URL、HTTP Method、响应状态码(如 200、404 等)和服务调用的执行时间;
-
HEADERS:在 BASIC 的基础上,还记录了请求和响应中的 HTTP Headers;
-
FULL:在 HEADERS 级别的基础上,还记录了服务请求和服务响应中的 Body 和 metadata,FULL 级别记录了最完整的调用信息。
Feign 的日志级别指定为 Full,并启动项目发起一个远程调用,你就可以在日志中看到整个调用请求的信息,包括请求路径、Header 参数、Request Payload 和 Response Body。我拿了一个调用日志作为示例,你可以参考一下。
[ProducerService#addPrefix] ---> GET http://producer-serv/producer/addPrefix?name=Miles HTTP/1.1
[ProducerService#addPrefix] ---> END HTTP (0-byte body)
[ProducerService#addPrefix] <--- HTTP/1.1 200 (223ms)
[ProducerService#addPrefix] connection: keep-alive
[ProducerService#addPrefix] content-length: 11
[ProducerService#addPrefix] content-type: text/plain;charset=UTF-8
[ProducerService#addPrefix] date: Wed, 20 Apr 2022 15:10:20 GMT
[ProducerService#addPrefix] keep-alive: timeout=60
[ProducerService#addPrefix]
[ProducerService#addPrefix] Hello,Miles
[ProducerService#addPrefix] <--- END HTTP (11-byte body)
6.2 OpenFeign 超时判定
超时判定是一种保障可用性的手段。如果你要调用的目标服务的 RT(Response Time)值非常高,那么你的调用请求也会处于一个长时间挂起的状态,这是造成服务雪崩的一个重要因素。为了隔离下游接口调用超时所带来的的影响,我们可以在程序中设置一个超时判定的阈值,一旦下游接口的响应时间超过了这个阈值,那么程序会自动取消此次调用并返回一个异常。
customer 服务依赖 producer 服务,如果你想要对 producer的远程服务调用添加超时判定配置,那么我们可以在customer-serv模块下的 application.yml 文件中添加下面的配置项。
feign:
client:
config:
# 全局超时配置
default:
# 网络连接阶段1秒超时
connectTimeout: 1000
# 服务请求响应阶段5秒超时
readTimeout: 5000
# 针对某个特定服务的超时配置
producer-serv:
connectTimeout: 1000
readTimeout: 2000
default 节点配置了全局层面的超时判定规则,它的生效范围是所有 OpenFeign 发起的远程调用。
producer-serv 下面配置的超时规则只针对向 producer 服务发起的远程调用。如果你想要对某个特定服务配置单独的超时判定规则,那么可以用同样的方法,在 feign.client.config 下添加目标服务名称和超时判定规则。
在超时判定的规则中我定义了两个属性:connectTimeout 和 readTimeout。其中,connectTimeout 的超时判定作用于“建立网络连接”的阶段;而 readTimeout 的超时判定则作用于“服务请求响应”的阶段(在网络连接建立之后)。我们常说的 RT(即服务响应时间)受后者影响比较大。另外,这两个属性对应的超时时间单位都是毫秒。
配置好超时规则之后,我们可以验证一下。你可以在 producer服务中使用 Thread.sleep 方法强行让线程挂起几秒钟,制造一个超时场景。这时如果你通过 customer 服务调用了 producer服务,那么在日志中可以看到下面的报错信息,提示你服务请求超时。
[ProducerService#addPrefix] ---> GET http://producer-serv/producer/addPrefix?name=Miles HTTP/1.1
[ProducerService#addPrefix] ---> END HTTP (0-byte body)
[ProducerService#addPrefix] <--- ERROR SocketTimeoutException: Read timed out (2103ms)
[ProducerService#addPrefix] java.net.SocketTimeoutException: Read timed out
6.3 OpenFeign 降级
降级逻辑是在远程服务调用发生超时或者异常(比如 400、500 Error Code)的时候,自动执行的一段业务逻辑。你可以根据具体的业务需要编写降级逻辑,比如执行一段兜底逻辑将服务请求从失败状态中恢复,或者发送一个失败通知到相关团队提醒它们来线上排查问题。
我采用了一种完全不同的服务容错手段,那就是借助 OpenFeign 实现 Client 端的服务降级。尽管它的功能远不如 Sentinel 强大,但它相比于 Sentinel 而言更加轻量级且容易实现,足以满足一些简单的服务降级业务需求。
OpenFeign 对服务降级的支持是借助 Hystrix 组件实现的,由于 Hystrix 已经从 Spring Cloud 组件库中被移除,所以我们需要在 customer-serv子模块的 pom 文件中手动添加 hystrix 项目的依赖。
<!-- hystrix组件,专门用来演示OpenFeign降级 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
<version>2.2.10.RELEASE</version>
<exclusions>
<!-- 移除Ribbon负载均衡器,避免冲突 -->
<exclusion>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-netflix-ribbon</artifactId>
</exclusion>
</exclusions>
</dependency>
注意需要在yml配置文件中添加上一段,用于开启Hystrix服务降级
feign:
#整合Hystrix,开启服务降级
circuitbreaker:
enabled: true
添加好依赖项之后,我们就可以编写 OpenFeign 的降级类了。OpenFeign 支持两种不同的方式来指定降级逻辑,一种是定义 fallback 类,另一种是定义 fallback 工厂。
通过 fallback 类实现降级是最为简单的一种途径,如果你想要为 ProducerService这个 FeignClient 接口指定一段降级流程,那么我们可以定义一个降级类并实现 ProducerService接口。我写了一个 ProducerServiceFallback 类,你可以参考一下。
@Slf4j
@Component
public class ProducerServiceFallback implements ProducerService {
@Override
public String addPrefix(String name) {
log.info("由于请求异常,将执行服务降级");
return "请求异常";
}
}
在上面的代码中,我们可以看出 ProducerServiceFallback实现了 ProducerService中的所有方法。如果在实际的方法调用过程中,OpenFeign 接口的 addPrefix 远程调用发生了异常或者超时的情况,那么 OpenFeign 会主动执行对应的降级方法,也就是 ProducerServiceFallback类中的 addPrefix方法。
降级类定义好之后,你还需要在 ProducerService接口中将 ProducerServiceFallback类指定为降级类,这里你可以借助 FeignClient 接口的 fallback 属性来配置,你可以参考下面的代码。
@FeignClient(value="producer-serv", path = "/producer",
fallback = ProducerServiceFallback.class)
public interface ProducerService{
}
如果你想要在降级方法中获取到异常的具体原因,那么你就要借助 fallback 工厂的方式来指定降级逻辑了。按照 OpenFeign 的规范,自定义的 fallback 工厂需要实现 FallbackFactory 接口,我写了一个 ProducerServiceFallbackFactory类,你可以参考一下。
@Slf4j
@Component
public class ProducerServiceFallbackFactory implements FallbackFactory<ProducerService> {
@Override
public ProducerService create(Throwable cause) {
return new ProducerService() {
@Override
public String addPrefix(String name) {
log.info("由于请求异常,将执行服务降级");
log.info(cause.getMessage());
return "请求异常";
}
};
}
}
需要将这个工厂类添加到 ProducerService注解中,这个过程和指定 fallback 类的过程有一点不一样,你需要借助 FeignClient 注解的 fallbackFactory 属性来完成。你可以参考下面的代码。
@FeignClient(value="producer-serv", path = "/producer",
fallback = ProducerServiceFallback.class,
fallbackFactory = ProducerServiceFallbackFactory.class)
public interface ProducerService {
}