什么叫优雅停机

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

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

就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、因为

image-20210306090724533

不管是被调用了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));

https://www.jianshu.com/p/0c49eb23c627

https://www.cnkirito.moe/gracefully-shutdown/

https://w.cnblogs.com/qingquanzi/p/9018627.html

posted on 2021-03-06 15:25  -CoderLi  阅读(339)  评论(0编辑  收藏  举报