优雅停机

简介: 简单说就是、在对应用进程发送停止指令之后、能保证正在执行的业务操作不受影响。应用接收到停止指令之后的步骤应该是、停止接收访问请求、等待已经接收的请求处理完成、并能成功返回、这时才真正停止应用。

网络异常,图片无法展示
|

什么叫优雅停机

简单说就是、在对应用进程发送停止指令之后、能保证正在执行的业务操作不受影响。

应用接收到停止指令之后的步骤应该是、停止接收访问请求、等待已经接收的请求处理完成、并能成功返回、这时才真正停止应用。


就Java 语言生态来说、底层技术是支持的、所以我们才能实现在 Java 语言上各个 Web 容器的优雅停机。

关于 kill 命令


在 Linux 中 kill 指令负责杀死进程、其后可以紧跟一个数字,代表信号编号 signal。

执行 kill -l 可以打印出所有的信号编号。

kill -l
HUP INT QUIT ILL TRAP ABRT EMT FPE KILL BUS SEGV SYS PIPE ALRM TERM URG STOP TSTP CONT CHLD TTIN TTOU IO XCPU XFSZ VTALRM PROF WINCH INFO USR1 USR2
复制代码


我们比较熟悉的就是 kill -9 pid 、这个命令课可以理解为操作系统从内核级别强行杀死某个进程。

kill -15 pid 则可以理解为发送一个通知、告知应用主动关闭。而我们有时候通过ctrl+c 来杀掉进程其实这就相当于kill -2 pid ,用于通知前台进程终止进程。

Demo

@SpringBootApplication
public class JunitSpringBootApplication {
  public static void main(String[] args) {
     SpringApplication.run(JunitSpringBootApplication.class, args);
     Runtime.getRuntime().addShutdownHook(new Thread(){
       @Override
       public void run() {
         System.out.println("执行 shutdown hook");       }
     });
  }
}
复制代码
@RestController
public class HiController implements DisposableBean {
    @Override
    public void destroy() throws Exception {
        System.out.println("destroy bean.....");
    }
}
复制代码


以 Jar 包的形式将应用运行起来。

然后分别使用 kill -15 pidctrl+c

执行 shutdown hook
destroy bean.....
复制代码


kill -9 pid 命令则什么都没有输出。

源码

Runtime.getRuntime().addShutdownHook(new Thread(){
    @Override
    public void run() {
       System.out.println("执行 shutdown hook");          }
 });
复制代码

Runtime.class

public void addShutdownHook(Thread hook) {
  SecurityManager sm = System.getSecurityManager();
  if (sm != null) {
    sm.checkPermission(new RuntimePermission("shutdownHooks"));
  }
  ApplicationShutdownHooks.add(hook);
}
复制代码

ApplicationShutdownHooks.class

private static IdentityHashMap<Thread, Thread> hooks;
static {
  try {
    Shutdown.add(1 /* shutdown hook invocation order */,
                 false /* not registered if shutdown in progress */,
                 new Runnable() {
                   public void run() {
                     // 执行注册的 hooks
                     runHooks();
                   }
                 }
                );
    hooks = new IdentityHashMap<>();
  } catch (IllegalStateException e) {
    // application shutdown hooks cannot be added if
    // shutdown is in progress.
    hooks = null;
  }
}
// ================================
static synchronized void add(Thread hook) {
  if(hooks == null)
    throw new IllegalStateException("Shutdown in progress");
  if (hook.isAlive())
    throw new IllegalArgumentException("Hook already running");
  if (hooks.containsKey(hook))
    throw new IllegalArgumentException("Hook previously registered");
  hooks.put(hook, hook);
}
// 从 Map 中获取对应的线程、启动执行、并等待其返回
static void runHooks() {
  Collection<Thread> threads;
  synchronized(ApplicationShutdownHooks.class) {
    threads = hooks.keySet();
    hooks = null;
  }
  for (Thread hook : threads) {
    hook.start();
  }
  for (Thread hook : threads) {
    while (true) {
      try {
        hook.join();
        break;
      } catch (InterruptedException ignored) {
      }
    }
  }
}
复制代码


再进入 Shutdown.add

private static final int MAX_SYSTEM_HOOKS = 10;
private static final Runnable[] hooks = new Runnable[MAX_SYSTEM_HOOKS];
static void add(int slot, boolean registerShutdownInProgress, Runnable hook) {
  synchronized (lock) {
    if (hooks[slot] != null)
      throw new InternalError("Shutdown hook at slot " + slot + " already registered");
    if (!registerShutdownInProgress) {
      if (state > RUNNING)
        throw new IllegalStateException("Shutdown in progress");
    } else {
      if (state > HOOKS || (state == HOOKS && slot <= currentRunningHook))
        throw new IllegalStateException("Shutdown in progress");
    }
    hooks[slot] = hook;
  }
}
复制代码


可以看到最大的 Runnable 的个数是10个、但是我们通过 ApplicationShutdownHooks 的 Map 存放多个关闭前处理线程。

Shutdown.add 运行 Runnable

private static void runHooks() {
  for (int i=0; i < MAX_SYSTEM_HOOKS; i++) {
    try {
      Runnable hook;
      synchronized (lock) {
        // acquire the lock to make sure the hook registered during
        // shutdown is visible here.
        currentRunningHook = i;
        hook = hooks[i];
      }
      if (hook != null) hook.run();
    } catch(Throwable t) {
      if (t instanceof ThreadDeath) {
        ThreadDeath td = (ThreadDeath)t;
        throw td;
      }
    }
  }
}
复制代码


当我们使用 kill -15 pid 或者 ctrl + c 的时候

/* Invoked by Runtime.exit, which does all the security checks.
     * Also invoked by handlers for system-provided termination events,
     * which should pass a nonzero status code.
     */
static void exit(int status) {
  boolean runMoreFinalizers = false;
  synchronized (lock) {
    if (status != 0) runFinalizersOnExit = false;
    switch (state) {
      case RUNNING:       /* Initiate shutdown */
        state = HOOKS;
        break;
      case HOOKS:         /* Stall and halt */
        break;
      case FINALIZERS:
        if (status != 0) {
          /* Halt immediately on nonzero status */
          halt(status);
        } else {
          /* Compatibility with old behavior:
                     * Run more finalizers and then halt
                     */
          runMoreFinalizers = runFinalizersOnExit;
        }
        break;
    }
  }
  if (runMoreFinalizers) {
    runAllFinalizers();
    halt(status);
  }
  synchronized (Shutdown.class) {
    /* Synchronize on the class object, causing any other thread
             * that attempts to initiate shutdown to stall indefinitely
             */
    // 这个方法会调起 runHooks 方法
    sequence();
    halt(status);
  }
}
复制代码


这个方法将会被执行。目前 Runnable 数组中指存在两个值、一个是 ApplicationShutdownHooks.class 放置进去的、一个是 DeleteOnExitHook 放置进去的(它的主要功能是删除某些文件)。

网络异常,图片无法展示
|

Spring Boot的 hooks 注册


直接进入到 SpringApplication 中

网络异常,图片无法展示
|


进入到 refresh 方法中

网络异常,图片无法展示
|

org.springframework.context.support.AbstractApplicationContext#registerShutdownHook

网络异常,图片无法展示
|

protected void doClose() {
  // Check whether an actual close attempt is necessary...
  if (this.active.get() && this.closed.compareAndSet(false, true)) {
    LiveBeansView.unregisterApplicationContext(this);
      publishEvent(new ContextClosedEvent(this));
    // Stop all Lifecycle beans, to avoid delays during individual destruction.
    if (this.lifecycleProcessor != null) {
      try {
        this.lifecycleProcessor.onClose();
      }
      catch (Throwable ex) {
      }
    }
    // 销毁单例 bean、destroy 方法就是这里被触发
    destroyBeans();
    // 关闭上下文以及 beanfactory
    closeBeanFactory();
    // 空实现、让子类去扩展
    onClose();
    // Switch to inactive.
    this.active.set(false);
  }
}
复制代码


所以我们可以有以下几种方式在 JVM 关闭前被调用

  • 监听 ContextClosedEvent 事件
  • Bean 销毁的注解或者 Spring 的销毁的接口中
  • onClose方法的重写

如何停止接收请求


只谈论 Tomcat 作为 servlet 容器

实现以下该接口、获取 Tomcat 的 Connector

@FunctionalInterface
public interface TomcatConnectorCustomizer {
   /**
    * Customize the connector.
    * @param connector the connector to customize
    */
   void customize(Connector connector);
}
复制代码


然后监听 Spring 的关闭事件

@Component
public class GracefulShutdownTomcat implements TomcatConnectorCustomizer, ApplicationListener<ContextClosedEvent> {
    private final Logger log = LoggerFactory.getLogger(GracefulShutdownTomcat.class);
    private volatile Connector connector;
    private final int waitTime = 30;
    @Override
    public void customize(Connector connector) {
        this.connector = connector;
    }
    @Override
    public void onApplicationEvent(ContextClosedEvent contextClosedEvent) {
        this.connector.pause();
        Executor executor = this.connector.getProtocolHandler().getExecutor();
        if (executor instanceof ThreadPoolExecutor) {
            try {
                ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) executor;
                threadPoolExecutor.shutdown();
                if (!threadPoolExecutor.awaitTermination(waitTime, TimeUnit.SECONDS)) {
                    log.warn("Tomcat thread pool did not shut down gracefully within " + waitTime + " seconds. Proceeding with forceful shutdown");
                }
            } catch (InterruptedException ex) {
                Thread.currentThread().interrupt();
            }
        }
    }
}
复制代码


对于 connector.pause() 执行之后、应用还是会接受新的请求,然后 hung 住,直到线程池被 shutdown 、才会返回 connection peered。

其实比较好的做法可能是滚动部署吧、在流量低的时间段、将流量导入到其中一部分实例中、剩余部分不再有流量进入、然后关闭然后部署新的服务、确认没问题、将流量切换过来部署另一半。

如何关闭线程池


一个线程什么时候可以退出呢?当然只有线程自己才能知道。

所以我们这里要说的Thread的interrrupt方法,本质不是用来中断一个线程。是将线程设置一个中断状态。

1、如果此线程处于阻塞状态(比如调用了wait方法,io等待),则会立马退出阻塞,并抛出InterruptedException异常,线程就可以通过捕获InterruptedException来做一定的处理,然后让线程退出。

2、如果此线程正处于运行之中,则线程不受任何影响,继续运行,仅仅是线程的中断标记被设置为true。所以线程要在适当的位置通过调用isInterrupted方法来查看自己是否被中断,并做退出操作。

如果线程的interrupt方法先被调用,然后线程调用阻塞方法进入阻塞状态,InterruptedException异常依旧会抛出。

如果线程捕获InterruptedException异常后,继续调用阻塞方法,将不再触发InterruptedException异常。


线程池的关闭

线程池提供了两个关闭方法,shutdownNow和shuwdown方法。

shutdownNow方法的解释是:线程池拒接收新提交的任务,同时立马关闭线程池,线程池里的任务不再执行。

shutdown方法的解释是:线程池拒接收新提交的任务,同时等待线程池里的任务执行完毕后关闭线程池。

public List<Runnable> shutdownNow() {
    List<Runnable> tasks;
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        checkShutdownAccess();
        advanceRunState(STOP);
        interruptWorkers();
        tasks = drainQueue();
    } finally {
        mainLock.unlock();
    }
    tryTerminate();
    return tasks;
}
复制代码

advanceRunState(STOP); 将线程池的状态设置为 STOP

interruptWorkers(); 遍历线程池里的所有工作线程,然后调用线程的interrupt方法

private void interruptWorkers() {
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        for (Worker w : workers)
            w.interruptIfStarted();
    } finally {
        mainLock.unlock();
    }
}
复制代码

tasks = drainQueue(); 将还未执行的任务从队列中移除、返回给调用方

private List<Runnable> drainQueue() {
    BlockingQueue<Runnable> q = workQueue;
    ArrayList<Runnable> taskList = new ArrayList<Runnable>();
    q.drainTo(taskList);
    if (!q.isEmpty()) {
        for (Runnable r : q.toArray(new Runnable[0])) {
            if (q.remove(r))
                taskList.add(r);
        }
    }
    return taskList;
}
复制代码


shutdownNow 之后线程池的反应如何?

线程池的代码逻辑

try {
    while (task != null || (task = getTask()) != null) {
        w.lock();
        // If pool is stopping, ensure thread is interrupted;
        // if not, ensure thread is not interrupted.  This
        // requires a recheck in second case to deal with
        // shutdownNow race while clearing interrupt
        if ((runStateAtLeast(ctl.get(), STOP) ||
             (Thread.interrupted() &&
              runStateAtLeast(ctl.get(), STOP))) &&
            !wt.isInterrupted())
            wt.interrupt();
        try {
            beforeExecute(wt, task);
            Throwable thrown = null;
            try {
                task.run();
            } catch (RuntimeException x) {
                thrown = x; throw x;
            } catch (Error x) {
                thrown = x; throw x;
            } catch (Throwable x) {
                thrown = x; throw new Error(x);
            } finally {
                afterExecute(task, thrown);
            }
        } finally {
            task = null;
            w.completedTasks++;
            w.unlock();
        }
    }
    completedAbruptly = false;
} finally {
    processWorkerExit(w, completedAbruptly);
}
复制代码


正常线程池就是在这个 for 循环中执行、如果任务正处于运行状态、即 task.run() 处于运行状态、即使线程被标识为 interrupt、但是不受影响继续执行。但是如果刚刚好处于阻塞状态、则会抛出 InterruptedException。抛出异常则会导致这个循环结束。

还有就是当 getTask 方法返回为 null 的时候也会结束循环

网络异常,图片无法展示
|

因为 showdownNow的时候我们将所有的工作线程都进行了 interrupt、所以当它处于在任务队列中阻塞获取任务的时候、其会被打断。

STOP = 536870912、SHUTDOWN=0。因为shutdownNow的时候将线程池的状态设置为 STOP、所以肯定会进入第一个红框的逻辑中返回 null。


shutdown

public void shutdown() {
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        checkShutdownAccess();
        advanceRunState(SHUTDOWN);
        interruptIdleWorkers();
        onShutdown(); // hook for ScheduledThreadPoolExecutor
    } finally {
        mainLock.unlock();
    }
    tryTerminate();
}
复制代码

将线程池的状态设置为 SHUTDOWN

将空闲的工作线程的标志位设置为 interrupt 。如何判断其是否空闲、通过Lock、因为

网络异常,图片无法展示
|

不管是被调用了interrupt的线程还是没被调用的线程,什么时候退出呢?,这就要看getTask方法的返回是否为null了。

在getTask里的if判断(上文中getTask代码截图中上边红色方框的代码)中,由于线程池被shutdown方法修改为SHUTDOWN状态,SHUTDOWN大于等于SHUTDOWN成立没问题,但是SHUTDOWN不在大于等于STOP状态,所以只有队列为空,getTask方法才会返回null,导致线程退出。


总结


  1. 当我们调用线程池的shutdownNow时,

如果线程正在getTask方法中执行,则会通过for循环进入到if语句,于是getTask返回null,从而线程退出。不管线程池里是否有未完成的任务。

如果线程因为执行提交到线程池里的任务而处于阻塞状态,则会导致报错(如果任务里没有捕获InterruptedException异常),否则线程会执行完当前任务,然后通过getTask方法返回为null来退出

  1. 当我们调用线程池的shuwdown方法时,

如果线程正在执行线程池里的任务,即便任务处于阻塞状态,线程也不会被中断,而是继续执行。

如果线程池阻塞等待从队列里读取任务,则会被唤醒,但是会继续判断队列是否为空,如果不为空会继续从队列里读取任务,为空则线程退出。

最后还有一个要记得、shutdownNow 和 shutdown 调用完、线程池并不是立马关闭的想要等待线程池关闭、还需要调用 awaitTermination 方法来阻塞等待。

this.executor.awaitTermination(this.awaitTerminationSeconds, TimeUnit.SECONDS));

www.jianshu.com/p/0c49eb23c…

www.cnkirito.moe/gracefully-…

w.cnblogs.com/qingquanzi/…

目录
相关文章
|
存储 人工智能 缓存
[大厂实践] 无停机迁移大规模关键流量 (上)
[大厂实践] 无停机迁移大规模关键流量 (上)
123 0
|
4月前
|
微服务
服务恢复正常
【8月更文挑战第19天】
52 2
|
运维 测试技术
6月27日阿里云故障说明
6月27日下午,我们在运维上的一个操作失误,导致一些客户访问阿里云官网控制台和使用部分产品功能出现问题。故障于北京时间2018年6月27日16:21左右开始,16:50分开始陆续恢复。对于这次故障,没有借口,我们不能也不该出现这样的失误!我们将认真复盘改进自动化运维技术和发布验证流程,敬畏每一行代码,敬畏每一份托付。
10781 2
|
13天前
|
监控 测试技术 网络虚拟化
如何提高系统的可用性时间
提高系统可用性时间的关键在于优化设计、强化监控与维护。通过冗余配置、故障转移、定期更新和实时监控等手段,可以有效减少系统停机时间,确保服务稳定运行。
|
7月前
|
NoSQL 关系型数据库 MySQL
主备切换大揭秘:保证系统永不停机的秘密
本文由小米分享,介绍了分布式系统中的主备切换机制,旨在确保高可用性和可靠性。内容涵盖热备和冷备的概念,以及MySQL和Redis的主从复制原理和配置方法。通过主从复制,当主服务器故障时,备服务器能接管工作,维持服务连续性。文章还讨论了主备切换的挑战,如数据一致性与切换延迟,并提出了相应的解决方案。最后,作者鼓励读者就该主题提出疑问和建议。
393 4
|
存储 Kubernetes API
优雅退出和零停机部署
如何在 Kubernetes 中使用优雅退出,使得业务具备零停机、无中断的部署发布。
241 1
|
存储 监控 安全
[大厂实践] 无停机迁移大规模关键流量 (下)
[大厂实践] 无停机迁移大规模关键流量 (下)
97 0
|
监控 容灾 安全
系统总出故障怎么办?
系统总出故障怎么办?
109 0
|
存储 监控 搜索推荐
|
监控 API
为什么系统越简单,宕机时间越少?
当新的需求出现时,我们总是倾向于通过其它方式或在现有系统上集成添加新功能。实际上,我们应考虑是否可以通过改变核心系统来满足新的需求。