项目场景:
政务管理系统,采用spring-cloud微服务架构,模块间采用feign进行互相调用,在其他模块调用用户管理模块查询用户信息时,在应用启动后第一次调用与后续调用表现出巨大的耗时差异,第一次调用耗时很长。
问题描述
基于jdk-8环境,spring-boot2.x、spring-cloud微服务组件3.x版本框架下,在服务启动完成后,服务间的第一次调用耗时很长响应很慢,后续调用不存在该现象。该问题出现后,鉴于维护项目环境的稳定,未在项目的基础上进行该问题调试。于是从spring cloud openfeign拉取示例代码,在本地模拟该问题进行调试,由于原示例代码中没有eureka注册中心模块,本地调试在该示例基础上添加了注册中心模块,关键代码如下:
1. eureka注册中心
@EnableEurekaServer
@SpringBootApplication
public class EurekaApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaApplication.class, args);
}
}
2. feign-server
@SpringBootApplication
@EnableDiscoveryClient
@RestController
public class HelloServerApplication {
@Autowired
DiscoveryClient client;
@RequestMapping("/")
public String hello() {
List<ServiceInstance> instances = client.getInstances("HelloServer");
ServiceInstance selectedInstance = instances
.get(new Random().nextInt(instances.size()));
return "Hello World: " + selectedInstance.getServiceId() + ":" + selectedInstance
.getHost() + ":" + selectedInstance.getPort();
}
/**
* 手动唤醒FeignClient接口
*
* @return
*/
@RequestMapping("/warmup")
public String warmup() {
List<ServiceInstance> instances = client.getInstances("HelloServer");
ServiceInstance selectedInstance = instances
.get(new Random().nextInt(instances.size()));
return "Hello World: warmup: " + selectedInstance.getServiceId() + ":" + selectedInstance
.getHost() + ":" + selectedInstance.getPort();
}
public static void main(String[] args) {
SpringApplication.run(HelloServerApplication.class, args);
}
}
3. feign-client
@Slf4j
@SpringBootApplication
@EnableDiscoveryClient
@RestController
@EnableFeignClients
public class HelloClientApplication {
@Autowired
HelloClient client;
@RequestMapping("/")
public String hello() {
String hello = null;
StopWatch stopWatch = new StopWatch();
for (int i = 1; i < 10; i++) {
stopWatch.start("hello::" + i);
hello = client.hello();
stopWatch.stop();
}
for (StopWatch.TaskInfo taskInfo : stopWatch.getTaskInfo()) {
log.info("任务名称:{},执行耗时:{}", taskInfo.getTaskName(), taskInfo.getTimeMillis());
}
return hello;
}
public static void main(String[] args) {
SpringApplication.run(HelloClientApplication.class, args);
}
@FeignClient(value = "HelloServer")
interface HelloClient {
@RequestMapping(value = "/", method = GET)
String hello();
/**
* 手动唤醒FeignClient接口
*
* @return
*/
@RequestMapping(value = "/warmup", method = GET)
String warmup();
}
}
4. 问题日志
2023-08-11 11:19:48.091 INFO 22560 --- [nio-7211-exec-3] demo.HelloClientApplication : 任务名称:hello::1,执行耗时:143
2023-08-11 11:19:48.091 INFO 22560 --- [nio-7211-exec-3] demo.HelloClientApplication : 任务名称:hello::2,执行耗时:3
2023-08-11 11:19:48.091 INFO 22560 --- [nio-7211-exec-3] demo.HelloClientApplication : 任务名称:hello::3,执行耗时:2
2023-08-11 11:19:48.091 INFO 22560 --- [nio-7211-exec-3] demo.HelloClientApplication : 任务名称:hello::4,执行耗时:2
2023-08-11 11:19:48.091 INFO 22560 --- [nio-7211-exec-3] demo.HelloClientApplication : 任务名称:hello::5,执行耗时:1
2023-08-11 11:19:48.091 INFO 22560 --- [nio-7211-exec-3] demo.HelloClientApplication : 任务名称:hello::6,执行耗时:1
2023-08-11 11:19:48.091 INFO 22560 --- [nio-7211-exec-3] demo.HelloClientApplication : 任务名称:hello::7,执行耗时:1
2023-08-11 11:19:48.091 INFO 22560 --- [nio-7211-exec-3] demo.HelloClientApplication : 任务名称:hello::8,执行耗时:1
2023-08-11 11:19:48.091 INFO 22560 --- [nio-7211-exec-3] demo.HelloClientApplication : 任务名称:hello::9,执行耗时:1
原因分析:
通过查询spring-cloud-openfeign、spring-cloud-loadbalancer发现是spring-cloud对FeignClient执行的是“beans are lazy proxies that initialize”/“Spring Cloud LoadBalancer creates a separate Spring child context for each service id. By default, these contexts are initialised lazily, whenever the first request for a service id is being load-balanced”。除此之外,在spring-cloud-openfeign仓库中也有对于bean lazy initialize原因的部分说明。
解决方案:
知道了FeignClient客户端bean是lazy initialize,可以采用如下两种方式手动激活FeignClient客户端。如果纯按效率来的话,推荐第二种;如果考虑同spring-data结合使用不出现其他问题的话,推荐第一种相对性能提升的方式。
1.手动初始化负载均衡器客户端:第一次请求性能提升平均能达到45%左右
- 1.1. 参照地址为:issues#475中给出的一种方式
@Configuration
@RequiredArgsConstructor
public class LoadBalancerClientsInitializer {
private final LoadBalancerClientFactory loadBalancerClientFactory;
@EventListener(ApplicationReadyEvent.class)
public void onApplicationReadyEvent() {
loadBalancerClientFactory.getInstance("HelloServer",
HelloClientApplication.HelloClient.class);
}
}
- 1.2.执行日志
2023-08-11 12:21:15.963 INFO 10292 --- [nio-7211-exec-1] demo.HelloClientApplication : 任务名称:hello::1,执行耗时:86
2023-08-11 12:21:15.964 INFO 10292 --- [nio-7211-exec-1] demo.HelloClientApplication : 任务名称:hello::2,执行耗时:4
2023-08-11 12:21:15.964 INFO 10292 --- [nio-7211-exec-1] demo.HelloClientApplication : 任务名称:hello::3,执行耗时:2
2023-08-11 12:21:15.964 INFO 10292 --- [nio-7211-exec-1] demo.HelloClientApplication : 任务名称:hello::4,执行耗时:3
2023-08-11 12:21:15.964 INFO 10292 --- [nio-7211-exec-1] demo.HelloClientApplication : 任务名称:hello::5,执行耗时:3
2023-08-11 12:21:15.964 INFO 10292 --- [nio-7211-exec-1] demo.HelloClientApplication : 任务名称:hello::6,执行耗时:2
2023-08-11 12:21:15.964 INFO 10292 --- [nio-7211-exec-1] demo.HelloClientApplication : 任务名称:hello::7,执行耗时:2
2023-08-11 12:21:15.964 INFO 10292 --- [nio-7211-exec-1] demo.HelloClientApplication : 任务名称:hello::8,执行耗时:1
2023-08-11 12:21:15.964 INFO 10292 --- [nio-7211-exec-1] demo.HelloClientApplication : 任务名称:hello::9,执行耗时:2
2.FeignClient定义固定的唤醒接口,利用CommandLineRunner在应用启动完成后自动唤醒客户端。
- 2.1.FeignClient添加手动唤醒方法
@FeignClient(value = "HelloServer")
interface HelloClient {
@RequestMapping(value = "/", method = GET)
String hello();
/**
* 手动唤醒FeignClient接口
*
* @return
*/
@RequestMapping(value = "/warmup", method = GET)
String warmup();
}
- 2.2.FeignClient添加springboot启动成功后,自动调用客户端唤醒方法的初始化类
@Slf4j
@Component
public class FeignClientWarmup implements CommandLineRunner {
@Resource
private ApplicationContext applicationContext;
@Override
public void run(String... args) throws BeansException {
Map<String, Object> clients = applicationContext.getBeansWithAnnotation(FeignClient.class);
clients.forEach((k, v) -> {
Class<?> clazz = v.getClass();
try {
Method method = clazz.getMethod("warmup");
method.invoke(v);
log.warn("warmup feign client: " + clazz.getName());
} catch (Exception e) {
log.warn(String.format("warmup feign client fail, className: {}", clazz.getName()), e);
e.printStackTrace();
}
});
}
}
- 2.3.FeignServer添加唤醒方法实现
@SpringBootApplication
@EnableDiscoveryClient
@RestController
public class HelloServerApplication {
@Autowired
DiscoveryClient client;
@RequestMapping("/")
public String hello() {
List<ServiceInstance> instances = client.getInstances("HelloServer");
ServiceInstance selectedInstance = instances
.get(new Random().nextInt(instances.size()));
return "Hello World: " + selectedInstance.getServiceId() + ":" + selectedInstance
.getHost() + ":" + selectedInstance.getPort();
}
/**
* 手动唤醒FeignClient接口
*
* @return
*/
@RequestMapping("/warmup")
public String warmup() {
List<ServiceInstance> instances = client.getInstances("HelloServer");
ServiceInstance selectedInstance = instances
.get(new Random().nextInt(instances.size()));
return "Hello World: warmup: " + selectedInstance.getServiceId() + ":" + selectedInstance
.getHost() + ":" + selectedInstance.getPort();
}
public static void main(String[] args) {
SpringApplication.run(HelloServerApplication.class, args);
}
}
- 2.4.执行日志
# FeignClientWarmup唤醒执行日志
2023-08-11 14:34:16.044 WARN 14316 --- [ main] demo.FeignClientWarmup : warmup feign client: demo.$Proxy91
2023-08-11 14:34:16.609 INFO 14316 --- [)-192.168.22.48] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet'
2023-08-11 14:34:16.609 INFO 14316 --- [)-192.168.22.48] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
2023-08-11 14:34:16.611 INFO 14316 --- [)-192.168.22.48] o.s.web.servlet.DispatcherServlet : Completed initialization in 2 ms
2023-08-11 14:34:40.660 INFO 14316 --- [nio-7211-exec-3] demo.HelloClientApplication : 任务名称:hello::1,执行耗时:5
2023-08-11 14:34:40.660 INFO 14316 --- [nio-7211-exec-3] demo.HelloClientApplication : 任务名称:hello::2,执行耗时:4
2023-08-11 14:34:40.660 INFO 14316 --- [nio-7211-exec-3] demo.HelloClientApplication : 任务名称:hello::3,执行耗时:2
2023-08-11 14:34:40.660 INFO 14316 --- [nio-7211-exec-3] demo.HelloClientApplication : 任务名称:hello::4,执行耗时:2
2023-08-11 14:34:40.660 INFO 14316 --- [nio-7211-exec-3] demo.HelloClientApplication : 任务名称:hello::5,执行耗时:2
2023-08-11 14:34:40.660 INFO 14316 --- [nio-7211-exec-3] demo.HelloClientApplication : 任务名称:hello::6,执行耗时:2
2023-08-11 14:34:40.660 INFO 14316 --- [nio-7211-exec-3] demo.HelloClientApplication : 任务名称:hello::7,执行耗时:2
2023-08-11 14:34:40.660 INFO 14316 --- [nio-7211-exec-3] demo.HelloClientApplication : 任务名称:hello::8,执行耗时:4
2023-08-11 14:34:40.660 INFO 14316 --- [nio-7211-exec-3] demo.HelloClientApplication : 任务名称:hello::9,执行耗时:2