我对优雅停服的要求是:

  1. 客户端已经进入的接口访问正常结束和返回且不再接收新的http请求
  2. 服务端正确有序的释放资源和更新节点服务状态

下面的内容都以以上两个条件为目标去讨论具体的方案

我们的环境和使用的中间件

  • MySql
  • MongoDB
  • RabbitMQ
  • Redis
  • eureka服务注册发现

连接这些中间件没有特殊意义只是为了我们测试停服时的释放资源过程。

接口代码放在了下面


测试结果

经测试发现在不切负载的情况下[kill -2 、 kill -15、actuator/shutdown ]会造成还没处理完毕的接口Empty reply from server ;kill -2 和 kill -15 是会终止还在过程中的接口, actuator/shutdown会继续接口过程运行,但是此时DB连接可能已经释放,所以会出现接口异常。

因此[kill -2 、 kill -15、actuator/shutdown ] 方案都必须搭配SLB或者nginx、网关之类来完成100%的优雅停服。但是不能切了负载之后立刻停服,在一些场景的接口可能运行时间比较长,已经请求到要停服的接口还没执行完毕就被停了那就会服务失败了。

停服方案

  1. 切换负载节点 然后 kill -9 {服务端没有及时更新服务注册发现的状态和释放资源 ,在一定情况下也会造成客户端访问异常}
  2. 切换负载节点 然后 kill -2 {服务端更新服务注册发现的状态和完全释放资源 ,必须在切了负载之后所在节点的所有接口访问都结束了的情况下才能保证web服务正常;不然客户端会出现:Empty reply from server;大概率不会造成客户端访问异常}
  3. 切换负载节点 然后 kill -15 {服务端更新服务注册发现的状态和释放资源 ,大概率不会造成客户端访问异常;大部分情况下与kill -2 一样的作用,也是要等切了负载之后等待所有接口处理完毕再执行kill}
  4. 切换负载节点 然后 actuator/shutdown {服务端更新服务注册发现的状态和释放资源不完全 ,会有进程残留的情况出现,不会中断接口还在处理的过程。也是要等切了负载之后等待所有接口处理完毕再执行}
  5. 使用shutdown: graceful 在开启了这个配置之后再对此PID kill -15 或者kill -2 这个节点就不会在接入新的http请求,但是已经进入的请求会在缓冲期内正常处理完毕然后返回,在此期间甚至不会完全释放中间件连接和资源,最后一个接口一旦处理完毕返回或者是缓冲器到了之后会立即关闭和释放服务器资源包括在 actuator/shutdown没有正确释放的AMQP资源,最后关掉进程。

在方案4actuator/shutdown,AMQP监听只最后还一直尝试重新启动监听,所以是没有清理完毕,其实这个情况可以在@PreDestory过程中去手段关闭释放这个对象。


结论

在不方便切节点负载的情况下使用actuator/shutdown和graceful/shutdown,后者更优。

如果可以切负载的话各个方式就是一样的效果。

在各种情况下graceful/shutdown都是一种对操作人员比较友好的方式,直接点说他就是actuator+kill 2/15 的增强版,推荐使用。

另外actuator之后资源没有清理完毕也可以使用kill -15之类的再去关闭进程。


另外我们需要提供两个对外的接口

码代码

/**
 * https://blog.csdn.net/wangxudongx
 * 
 * 测试接口
 */
@RestController
@Slf4j
public class TestController {

    private ChatsUserloginrecordMapper userloginrecordMapper;

    @Autowired
    public TestController(ChatsUserloginrecordMapper userloginrecordMapper) {
        this.userloginrecordMapper = userloginrecordMapper;
    }

    /**
     * 测试接口1
     * 会执行60秒的一个接口 过程中会使用数据库连接
     * @param request
     * @param response
     * @return
     */
    @GetMapping("process1minutes")
    public String process2minutes(HttpServletRequest request, HttpServletResponse response) {

        for (Long i = 0L; i < 60; i++) {

            log.info("current is {} second",i);
            ChatsUserloginrecord userloginrecord = new ChatsUserloginrecord();
            userloginrecord.setUiid(5054736737103077385L);
            userloginrecord.setId(i);
            userloginrecordMapper.insertSelective(userloginrecord);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        return "处理完毕。";
    }

    /**
     * 测试接口2
     * 会执行60秒的一个接口 过程中会使用数据库连接
     * @param request
     * @param response
     * @return
     */
    @GetMapping("process1minutes2")
    public String process2minutes2(HttpServletRequest request, HttpServletResponse response) {

        for (Long i = 0L; i < 60; i++) {

            log.info("current is {} second",i);
            ChatsUserloginrecord userloginrecord = new ChatsUserloginrecord();
            userloginrecord.setUiid(5054736737103077382L);
            userloginrecord.setId(i);
            userloginrecordMapper.insertSelective(userloginrecord);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        return "处理完毕。";
    }
}

下面是各个命令下的关闭过程


kill -9

这个命令大家应该都熟悉,它不会让目标进程进行任何的清理。
同时这个信号不能被捕获或忽略,这是个重点,如果碰到一直关不掉的进程,在条件允许的情况下可以使用此命令来终结进程。

kill -2

  1. EurekaServiceRegistry 注销eureka服务注册并更新服务状态DOWN

  2. DiscoveryClient 改变eureka服务发现的状态DOWN

  3. ProtocolHandler [“https-jsse-nio-8080”] 暂停

  4. Stopping service [Tomcat] 关闭web服务

  5. Destroying Spring FrameworkServlet ‘dispatcherServlet’ 释放spring servlet

  6. Stopping ProtocolHandler [“https-jsse-nio-8080”] 停止

  7. Destroying ProtocolHandler [“https-jsse-nio-8080”] 释放

  8. o.s.s.concurrent.ThreadPoolTaskExecutor : Shutting down ExecutorService ‘threadPoolTaskExecutor’ 关闭线程池

  9. org.mongodb.driver.connection : Closed connection 关闭mongodb连接

  10. alibaba.druid.pool.DruidDataSource : {dataSource-1} closed 关闭数据库连接

  11. 释放servlet 执行@PreDestroy修饰的方法 : 会在服务器卸载Servlet的时候运行,并且只会被服务器调用一次,类似于Servlet的destroy()方法。被@PreDestroy修饰的方法会在destroy()方法之后运行,在Servlet被彻底卸载之前。

  12. Shutting down DiscoveryClient … 关闭服务发现

  13. Completed shut down of DiscoveryClient 关闭服务发现完成

未跑完的接口会停止执行

在后台任务的情况下进程被清理


kill -15

EurekaServiceRegistry 注销eureka服务注册并更新服务状态DOWN

DiscoveryClient 改变eureka服务发现的状态DOWN

ProtocolHandler [“https-jsse-nio-8080”] 暂停

Stopping service [Tomcat] 关闭web服务

Destroying Spring FrameworkServlet ‘dispatcherServlet’ 释放spring servlet

Stopping ProtocolHandler [“https-jsse-nio-8080”] 停止

Destroying ProtocolHandler [“https-jsse-nio-8080”] 释放

o.s.s.concurrent.ThreadPoolTaskExecutor : Shutting down ExecutorService ‘threadPoolTaskExecutor’ 关闭线程池

org.mongodb.driver.connection : Closed connection 关闭mongodb连接

alibaba.druid.pool.DruidDataSource : {dataSource-1} closed 关闭数据库连接

释放servlet 执行@PreDestroy修饰的方法 : 会在服务器卸载Servlet的时候运行,并且只会被服务器调用一次,类似于Servlet的destroy()方法。被@PreDestroy修饰的方法会在destroy()方法之后运行,在Servlet被彻底卸载之前。

Shutting down DiscoveryClient … 关闭服务发现

Completed shut down of DiscoveryClient 关闭服务发现完成

未跑完的接口会停止执行

在后台任务的情况下进程被清理

actuator/shutdown 关闭过程和使用

配置actuator/shutdown:

# 暴露监控端点
management:
  endpoints:
    web:
      exposure:
        include: '*'
      base-path: /actuator
  endpoint:
    shutdown:
      enabled: true
      whitelist: 127.0.0.1
      sensitive: true
      address: 127.0.0.1
    address: 127.0.0.1
curl https://127.0.0.1:9444/actuator/shutdown

或者

 curl -k https://127.0.0.1:9444/actuator/shutdown -X POST

其实actuator就适合做web服务状态监控。

EurekaServiceRegistry 注销eureka服务注册并更新服务状态DOWN

DiscoveryClient 改变eureka服务发现的状态DOWN

ProtocolHandler [“https-jsse-nio-8080”] 暂停

Stopping service [Tomcat] 关闭web服务

Destroying Spring FrameworkServlet ‘dispatcherServlet’ 释放spring servlet

Stopping ProtocolHandler [“https-jsse-nio-8080”] 停止

Destroying ProtocolHandler [“https-jsse-nio-8080”] 释放

o.s.s.concurrent.ThreadPoolTaskExecutor : Shutting down ExecutorService ‘threadPoolTaskExecutor’ 关闭线程池

org.mongodb.driver.connection : Closed connection 关闭mongodb连接

alibaba.druid.pool.DruidDataSource : {dataSource-1} closed 关闭数据库连接

释放servlet 执行@PreDestroy修饰的方法 : 会在服务器卸载Servlet的时候运行,并且只会被服务器调用一次,类似于Servlet的destroy()方法。被@PreDestroy修饰的方法会在destroy()方法之后运行,在Servlet被彻底卸载之前。

Shutting down DiscoveryClient … 关闭服务发现

Completed shut down of DiscoveryClient 关闭服务发现完成

SimpleMessageListenerContainer 消息队列监听尝试重启(如果AMQP没在此前关闭、释放就会尝试重启,建议在@PreDestroy过程里手动关闭和释放AMQP相关对象)

未跑完的接口会继续执行

在后台任务的情况下进程不会被清理


SpringBoot shutdown: graceful 优雅停服 停服过程及使用

在最新的 spring boot 2.3 版本,内置此功能

开启了此配置之后,进程在接收到kill -15 / kill -2命令会有一个缓冲期(可配置时长)中间会等着还在处理的接口执行完毕期间不会关闭数据库连接等接口执行期所需要的资源,但是会更新服务注册和发现的节点服务状态,然后此进程在缓冲器不再接收新的http请求,并会在最后一个接口或者缓冲器结束后清理资源关闭进程。

配置方式application.yml:

server:
  shutdown: graceful
spring:
  lifecycle:
    timeout-per-shutdown-phase: 30s  # 设定缓冲器为30秒;默认也是30秒

在配置了graceful执行了kill -15 命令之后再去访问web服务
在这里插入图片描述

但是上一次的请求还在处理过程中并在完成之后返回了处理结果。

在这里插入图片描述

执行了kill -15 PID命令

在这里插入图片描述

下面是缓冲器结束了之后的日志
在这里插入图片描述


tomcat/bin/shutdown.sh

shutdown脚本比较常见的问题:若在应用中启动了用户线程,在Linux系统Tomcat进程使用shutdown无法关闭进程,直接点说就是进程残留。

用shutdown.sh脚本控制Tomcat关闭的方式存在权限的风险,并且也会由于开发中的线程操作导致Tomcat无法关闭,所以这种方法在实际应用中使用情况较少。

而kill -15则能够安全的杀死Tomcat进程,并且由于JVM
shutdownhook的存在,我们可以对整个程序关闭时进行更强有力的控制,退出过程也更为优雅。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐