转载本文请联系程序新视野公众号。前言,在上一篇中,我们讲到了微服务突然挂掉时的解放解决方案:调整健康检查周期,重试故障请求。朋友看完文章建议说说如何让微服务在服务正常关闭的情况下优雅下线。为什么说线下优雅呢?我们知道,在分布式应用中,为了满足CAP原则中的A(availability),Nacos、Eureka等注册中心的客户端都会对实例列表进行缓存。当应用程序正常关闭时,虽然可以主动调用注册中心注销,但是这些客户端缓存的实例列表需要过一段时间才会失效。上述情况可能会导致服务请求到已经关闭的实例。虽然可以通过重试机制解决这个问题,但是这种方案会造成重试,一定程度上会拖慢用户端的请求。这时候就需要优雅的离线操作了。让我们从通常关闭进程的几种方法开始。方法一:基于kill命令,SpringCloud本身支持关闭服务。当通过kill命令关闭进程时,会主动调用Shutdownhook注销当前实例。使用方法:killJava进程ID这种方式是利用了SpringCloud的Shutdownhook机制(本质上是SpringBoot提供的,针对具体的logout实现了SpringCloud服务发现功能),Nacos、Eureka等服务会被在服务关闭之前注销。但是这个注销只是告诉注册中心,客户端的缓存可能需要等待几秒(Nacos默认是5秒)才能感知到。这种Shutdownhook机制不仅适用于kill命令,也适用于程序正常退出,使用System.exit(),终端使用Ctrl+C等。但不适用于kill等场景-9如强制关机或服务器宕机。这种方案虽然相对于等待15秒直接挂断要好一些,但是并没有从本质上解决客户端缓存的问题,不推荐使用。方法二:基于/shutdown端点在SpringBoot中,提供了/shutdown端点。基于此,也可以实现优雅关机,不过本质上和第一种方法一样,都是基于Shutdownhook实现的。基于Shutdownhook处理完逻辑后,服务也会被关闭,但是也面临着客户端缓存的问题,所以不推荐。该方法首先需要在项目中引入相应的依赖:org.springframework.bootspring-boot-starter-actuator然后在项目配置启用/关闭端点:management:endpoint:shutdown:enabled:trueendpoints:web:exposure:include:shutdown然后在服务停止时请求相应的端点。下面是curl命令的一个例子:curl-Xhttp://instanceserviceaddress/actuator/shutdown方法三:基于/pause端点SpringBoot也提供了/pause端点(由SpringBootActuator提供)。通过/pause端点,您可以将/health处于UP状态的实例更改为Down状态。基本操作是在配置文件中开启暂停端点:management:endpoint:#开启暂停端点pause:enabled:true#部分版本暂停端点依赖重启端点restart:enabled:trueendpoints:web:exposure:include:pause,restart然后发送curl命令终止服务。注意这里需要一个POST请求。关于/pause端点的使用,不同的版本差别很大。笔者在使用SpringBoot2.4.2.RELEASE版本时,发现根本无法生效。在查看了SpringBoot和SpringCloud项目的Issues后,发现这个问题从2.3.1.RELEASE开始就有了。目前应该是最新版本把WebServer的管理改为SmartLifecycle的原因,但是SpringCloud好像已经放弃了这个支持(待考),最新版本调用了/pause端点没有任何回应。鉴于上述版本变化过多的原因,不建议使用/pause端点进行微服务的离线操作,但是使用/pause端点的整体思路还是值得学习的。基本思路是:当/pause端点被调用时,微服务的状态会由UP变为DOWN,服务本身仍然可以正常提供服务。当微服务被标记为DOWN时,会从注册中心移除,等待一段时间(比如5秒),当Nacos客户端缓存的实例列表有更新时,停止服务。这个思路的核心是:先关掉微服务的流量,再关闭或者重新发布。解决了正常发布时客户端缓存实例列表的问题。基于以上思路,其实你也可以自己实现相应的功能,比如提供一个Controller,先调用Controller中的方法将当前实例从Nacos注销,然后等待5秒,然后关闭通过脚本或其他方法提供服务。方法四:基于/service-registry端点方法三中提到的解决方案,如果SpringCloud能直接支持就更好了。不,SpringCloud提供/service-registry端点。但是从名称上,您可以知道一个专门为服务注册而实现的端点。在配置文件中打开/service-registry端点:management:endpoints:web:exposure:include:service-registrybase-path:/actuatorendpoint:serviceregistry:enabled:true访问http://localhost:8081/actuator端点查看enabled指定了以下端点:{"_links":{"self":{"href":"http://localhost:8081/actuator","templated":false},"serviceregistry":{"href":"http://localhost:8081/actuator/serviceregistry","templated":false}}}使用curl命令修改服务状态:curl-X"POST""http://localhost:8081/actuator/serviceregistry?status=DOWN"-H"Content-Type:application/vnd.spring-boot.actuator.v2+json;charset=UTF-8"在执行上面的命令前,查看Nacos对应实例的状态:可以看到实例详情中的按钮为“Offline”表示当前处于UP状态。执行上述curl命令后,实例详情中的按钮为“Online”,表示实例已经下线。以上命令相当于在Nacos管理后台手动操作实例上下线。当然,以上情况是基于SpringCloud和Nacos的模式实现的。本质上,SpringCloud定义了一个规范。例如,所有注册中心都需要实现ServiceRegistry接口。同时,在ServiceRegistry的抽象基础上,还定义了一个通用的Endpoint:@Endpoint(id="serviceregistry")publicvoidsetRegistration(Registrationregistration){this.registration=registration;}@WriteOperationpublicResponse(StatE{Assert.notNull(status,"statusmaynotbynull");if(this.registration==null){returnResponseEntity.status(HttpStatus.NOT_FOUND).body("noregistrationfound");}this.serviceRegistry.setStatus(this.registration,status);returnResponseEntity.ok().build();}@ReadOperationpublicResponseEntitygetStatus(){if(this.registration==null){returnResponseEntity.status(HttpStatus.NOT_FOUND).body("没有找到注册信息");}returnResponseEntity.ok().body(this.serviceRegistry.getStatus(this.registration));}}我们上面调用的Endpoint是通过上面的代码实现的,所以不仅是Nacos,注册中心也是基于SpringCloud集成,本质上就是所有支持这种方式的服务都是离线的。总结很多项目都在逐步进行微服务化改造,但是一旦由于微服务系统化,就会面临更加复杂的情况。本文主要针对Nacos在Spring云系统优雅下线分析微服务实战中的一个常见问题及解决方案,你在使用微服务,你注意到了吗?