使用了注册中心的服务集群,各个业务节点通过RestTemplate、openFeign进行内部调用时,或者gateway做服务转发时,一般会使用从注册中心获取的服务节点列表进行负载路由,而当一个服务节点被暴力关闭时,从注册中心检测到节点处在不健康状态,到注册中心将该节点从维护列表中移除,再到其他业务节点将失效节点从自身维护的节点列表缓存数据中移除(定时拉取或广播通知方式),中间一般会经历数秒时长,这时如果有转向失效节点的新请求,就会不得不等满一个timeout周期,然后再反馈回调用端去做下一步是重试还是标记熔断的处理。总之,有异常发生,不优雅。
(假装有流程图)
现成的有不少优雅关闭服务的方案,比如使用actuator的shutdown端点、提供入口调用SpringApplication的exit()静态方法等,不过重要的是在做这些之前要能先主动的将节点从注册中心中下线,然后等待一个合适的时间段之后再关闭服务,这样能多给正在处理中的请求执行完成的机会。
下面是使用了nacos的服务下线示例代码:
/** 使用了nacos注册中心的服务关闭端点配置 */
@ConditionalOnClass(NacosAutoServiceRegistration.class)
@RestController
@RequestMapping("actuator")
@RequiredArgsConstructor
@Slf4j
public class NacosStopEndpoint {
private final NacosAutoServiceRegistration nacosAutoServiceRegistration;
private final ApplicationContext context;
/** 注销服务后关闭应用前等待的时间(毫秒) */
@Value("${stopService.waitTime:10000}")
private int waitTime;
/**
* 关闭服务 <br>
* 只接收localhost发起的请求
*
* @param request
* @return
*/
@PostMapping("stopService")
public ResponseEntity<Boolean> stopNacosService(
HttpServletRequest request) {
if (!request.getServerName().equalsIgnoreCase("localhost"))
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(false);
new Thread(
() -> {
log.info("Ready to stop service");
nacosAutoServiceRegistration.stop();
log.info("Nacos instance has been de-registered");
log.info("Waiting {} milliseconds...", waitTime);
try {
Thread.sleep(waitTime);
} catch (InterruptedException e) {
log.info("interrupted!", e);
}
log.info("Closing application...");
SpringApplication.exit(context);
((ConfigurableApplicationContext) context).close();
})
.start();
return ResponseEntity.ok(true);
}
}
这段代码提供了一个 /actuator/stopService
的POST端点,调用它即可优雅将服务下线,先把自身从nacos注册中心中移除,等待10秒后关闭spring应用。
接下来,还需要找一个合适的时机来调用这个接口。如果使用了k8s,可以在容器生命周期中的prestop阶段配置一个执行脚本:
lifecycle:
preStop:
exec:
command:
- /bin/sh
- '-c'
- 'curl -X POST ''http://localhost:8080/actuator/stopService'';\'
- sleep 30;
其中的sleep时间可以配置的比下线节点之后的等待时间稍长一些即可。