Nacos 服务下线快 会导致dubbo直接调用,报错为No route to host
原因在于容器开始销毁的时间比dubbo服务下线的时间要早。因为容器销毁时,任何连接请求都会返回Connection refused。此时如果发生dubbo调用,就会有相应的报错。

报错日志:
image.png

https://github.com/alibaba/spring-cloud-alibaba/issues/1805

一、思路

1、让新的服务先启动起来,注册到注册中心,等待客户端发现新服务。
2、把待下线的服务从注册中心下线,等待客户端刷新服务列表。
3、把待下线的服务优雅停机。

二、实现方式

基于K8S滚动升级的机制,当新的POD准备就绪之后,旧的POD会被删除。在删除旧POD之前会调用容器preStop的钩子。

1、让新的服务先启动起来,注册到注册中心,等待客户端发现新服务。
K8S容器本身有一个就绪探针配置,当就绪探针返回正常,则开始删除旧POD。

  1. //就绪探针
  2. readinessProbe:
  3. httpGet:
  4. path: /actuator/health
  5. port: 8080
  6. scheme: HTTP
  7. initialDelaySeconds: 120
  8. timeoutSeconds: 300
  9. periodSeconds: 35
  10. successThreshold: 1
  11. failureThreshold: 1

我们可以使用SpringBoot的健康检查actuator作为就绪探针。核心的配置是initialDelaySeconds,当容器创建到第一次就绪检查的延迟时间。
initialDelaySeconds=服务启动完成耗时+客户端负责均衡刷新服务列表的时间(ribbon.ServerListRefreshInterval,默认是30s)

2、把将要下线的服务从注册中心下线,等待客户端刷新服务列表。
我们要实现一个preStop钩子,把服务从注册中心下线,而且要等待ribbon.ServerListRefreshInterval秒时间,等待客户端刷新服务,把当前服务剔除。
我们用的nacos作为注册中心,AbstractAutoServiceRegistration是服务注册的抽象基类,stop方法可以从服务中心下线服务,NacosAutoServiceRegistration是实现类,所以我们只需要调用NacosAutoServiceRegistration.stop()即可下线服务。

  1. public void stop() {
  2. if (this.getRunning().compareAndSet(true, false) && this.isEnabled()) {
  3. this.deregister();
  4. if (this.shouldRegisterManagement()) {
  5. this.deregisterManagement();
  6. }
  7. this.serviceRegistry.close();
  8. }
  9. }

自定义一个deregister的endpoint,让k8s的preStop去调用

  1. @Endpoint(id = "deregister")
  2. public class NacosServiceDeregisterEndpoint {
  3. private static final Logger log = LoggerFactory.getLogger(NacosServiceDeregisterEndpoint.class);
  4. private NacosAutoServiceRegistration serviceRegistration;
  5. public NacosServiceDeregisterEndpoint(NacosAutoServiceRegistration serviceRegistration) {
  6. this.serviceRegistration = serviceRegistration;
  7. }
  8. @ReadOperation
  9. public Map deregister() {
  10. log.info("开始服务下线");
  11. serviceRegistration.stop();
  12. log.info("完成服务下线");
  13. int wait = 10;
  14. log.info("等待{}s,等待客户端刷新服务列表", wait);
  15. int num = 0;
  16. while (true) {
  17. try {
  18. //等待客户端刷新服务发现列表
  19. TimeUnit.SECONDS.sleep(1);
  20. num++;
  21. log.info("还需等待{}秒", wait - num);
  22. if (num >= wait) {
  23. break;
  24. }
  25. } catch (InterruptedException e) {
  26. //ignore
  27. log.info("InterruptedException", e);
  28. }
  29. }
  30. log.info("等待结束");
  31. Map result = new HashMap<>();
  32. result.put("deregister", true);
  33. return result;
  34. }
  35. }

别忘记了要把endpoint注册到BeanFactory中

  1. @Configuration(proxyBeanMethods = false)
  2. public class NacosServiceDeregisterAutoConfig {
  3. @Bean
  4. public NacosServiceDeregisterEndpoint deregisterEndpoint(NacosAutoServiceRegistration registration) {
  5. return new NacosServiceDeregisterEndpoint(registration);
  6. }
  7. }

定义K8S的preStop钩子

  1. lifecycle:
  2. preStop:
  3. httpGet:
  4. path: /actuator/deregister
  5. port: 8080
  6. scheme: HTTP

3、把待下线的服务优雅停机
Spring Boot 已经支持优雅停机。
我们需要添加下面的配置,超时需要根据平时请求的耗时来定,可以稍微大一点也没关系。
server.shutdown=graceful spring.lifecycle.timeout-per-shutdown-phase=60s
最后还有一个重要的K8S配置
terminationGracePeriodSeconds要大于preStop+spring.lifecycle.timeout-per-shutdown-phase,可以设置大一点没什么关系。
//停止超时时间 terminationGracePeriodSeconds: 300


其他方案
https://blog.csdn.net/shine0181/article/details/122308860
https://my.oschina.net/u/4022819/blog/5528262