[Java] Algunas reflexiones sobre el cierre ordenado

Inserte la descripción de la imagen aquí

1. Información general

Reimpreso: http://cxytiandi.com/blog/detail/15386

Reimpreso de: intercambio de tecnología de Xu Jingfeng Kirito

Se ha descubierto que kill -9el script de reinicio revisó el proyecto reciente, la operación y el mantenimiento utilizan la forma de reinicio de springboot embebido de tomcat, de hecho, casi todos estuvieron de acuerdo en que: kill -9la forma relativamente violenta, pero exactamente lo que traerá, pero pocas personas pueden analizar una pista. Este artículo registra principalmente mi propio proceso de pensamiento.

¿Cuál es la diferencia entre kill -9 y kill -15?

En el pasado, el paso habitual para publicar aplicaciones WEB era envolver el código en un paquete war y luego colocarlo en una máquina Linux equipada con un contenedor de aplicaciones (como Tomcat, Weblogic). En este momento, queremos iniciar / cerrar la aplicación de una manera muy diferente. Simple, simplemente ejecute el script de inicio / apagado en él. Springboot proporciona otra forma de empaquetar toda la aplicación junto con el servidor Tomcat integrado, lo que sin duda aporta una gran comodidad a la publicación de aplicaciones y también plantea una pregunta: ¿cómo cerrar la aplicación Springboot? Una forma obvia es encontrar la identificación del proceso según el nombre de la aplicación y eliminar la identificación del proceso para cerrar la aplicación.

La descripción del escenario anterior plantea mi pregunta: ¿cómo eliminar un proceso de aplicación Springboot con gracia? Estos son solo el sistema operativo Linux más popular, por ejemplo, es responsable del proceso de matar en el comando kill de Linux, después de lo cual puede ir seguido de un número que representa el número de señal (Señal), ejecutar kill -lcomandos, puede enumerar todo el número de señal.

xu@ntzyz-qcloud ~ % kill -l                                                                     
HUP INT QUIT ILL TRAP ABRT BUS FPE KILL USR1 SEGV USR2 PIPE ALRM TERM STKFLT CHLD CONT STOP TSTP TTIN TTOU URG XCPU XFSZ VTALRM PROF WINCH POLL PWR SYS

Este artículo presenta principalmente el 9º código de señal KILL y el 15º número de señal TERM.

Primero, simplemente comprenda la diferencia entre los dos:

  1. kill -9 pid Se puede entender que el sistema operativo mata a la fuerza un proceso desde el nivel del kernel
  2. kill -15 pid Puede entenderse como el envío de una notificación para informar a la aplicación de que cierre activamente.

Esta comparación es todavía un poco abstracta, así que echemos un vistazo al rendimiento de la aplicación. ¿Cuál es la diferencia entre estos dos comandos para matar la aplicación?

Preparación de código

Dado que el autor tiene mucho contacto con springboot, comenzaré la discusión con una aplicación simple de springboot como ejemplo y agregaré el siguiente código.

1 Agregue una clase que implemente la interfaz DisposableBean

@Component
public class TestDisposableBean implements DisposableBean{
    
    
    @Override
    public void destroy() throws Exception {
    
    
        System.out.println("测试 Bean 已销毁 ...");
    }
}

2 Agregue ganchos cuando la JVM esté cerrada

@SpringBootApplication
@RestController
public class TestShutdownApplication implements DisposableBean {
    
    
    public static void main(String[] args) {
    
    
        SpringApplication.run(TestShutdownApplication.class, args);
        Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                System.out.println("执行 ShutdownHook ...");
            }
        }));
    }
}

Pasos de prueba

Ejecute java -jar test-shutdown-1.0.jarla
prueba en funcionamiento de la aplicación kill -9 pid,kill -15 pid,ctrl + cdespués de
los resultados de la prueba de contenido de salida del registro

kill -15 pid & ctrl + c, El efecto es el mismo, la salida es la siguiente

2018-01-14 16:55:32.424  INFO 8762 --- [       Thread-3] ationConfigEmbeddedWebApplicationContext : Closing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@2cdf8d8a: startup date [Sun Jan 14 16:55:24 UTC 2018]; root of context hierarchy
2018-01-14 16:55:32.432  INFO 8762 --- [       Thread-3] o.s.j.e.a.AnnotationMBeanExporter        : Unregistering JMX-exposed beans on shutdown
执行 ShutdownHook ...
测试 Bean 已销毁 ...
java -jar test-shutdown-1.0.jar  7.46s user 0.30s system 80% cpu 9.674 total

kill -9 pid, No se generan registros de aplicaciones

Se puede encontrar que kill -9 pidla aplicación fue tomada por sorpresa, sin dejar ninguna posibilidad de que reaccione. Por otro lado kill -15 pid, son más elegantes, primero por la AnnotationConfigEmbeddedWebApplicationContext (一个 ApplicationContext 的实现类)recepción del aviso, seguido de la implementación del código de prueba Shutdown Hook, la implementación final del DisposableBean#destory()método. Cual es mejor o peor, se hace el juicio.

Generalmente, nos ocuparemos de la lógica de "consecuencias" cuando se cierre la aplicación, como

  1. Cerrar el enlace de socket
  2. Limpiar archivos temporales
  3. Envíe un mensaje al suscriptor para informarle que se desconecte
  4. Notifique al proceso hijo que está a punto de ser destruido
  5. Liberación de varios recursos
  6. y muchos más

Y kill -9 pides un tiempo de inactividad del sistema analógico directo, el sistema pierde potencia, por lo que la aplicación es demasiado poco amigable, no use la cosechadora para recortar macetas de flores. En su lugar, se usa kill -15 piden su lugar. Si encuentra una práctica en particular :kill -15 pidque no puede cerrar la aplicación, puede considerar usar el nivel de kernel kill -9 pid, pero asegúrese de que después de la investigación las causas kill -15 pidno se puedan cerrar.

¿Cómo maneja Springboot -15 TERM Signal

Como se explicó anteriormente, la aplicación springboot se puede cerrar sin problemas usando kill -15 pid. Podemos tener las siguientes dudas: ¿Cómo responde springboot / spring a este comportamiento de apagado? ¿Cerró Tomcat primero y luego salió de la JVM o en el orden inverso? ¿Cómo es que están relacionados?

Intente continuar con un análisis desde el inicio del registro, AnnotationConfigEmbeddedWebApplicationContextimprima el cierre de comportamiento, vaya directamente al código fuente para averiguarlo, culminando en su padre AbstractApplicationContextpara encontrar el código clave:

@Override
public void registerShutdownHook() {
    
    
  if (this.shutdownHook == null) {
    
    
    this.shutdownHook = new Thread() {
    
    
      @Override
      public void run() {
    
    
        synchronized (startupShutdownMonitor) {
    
    
          doClose();
        }
      }
    };
    Runtime.getRuntime().addShutdownHook(this.shutdownHook);
  }
}
@Override
public void close() {
    
    
   synchronized (this.startupShutdownMonitor) {
    
    
      doClose();
      if (this.shutdownHook != null) {
    
    
         Runtime.getRuntime().removeShutdownHook(this.shutdownHook);
      }
   }
}
protected void doClose() {
    
    
   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) {
    
    
         this.lifecycleProcessor.onClose();
      }
      // spring 的 BeanFactory 可能会缓存单例的 Bean 
      destroyBeans();
      // 关闭应用上下文&BeanFactory
      closeBeanFactory();
      // 执行子类的关闭逻辑
      onClose();
      this.active.set(false);
   }
}

Para facilitar la composición tipográfica y la facilidad de comprensión, eliminé parte del código de manejo de excepciones en el código fuente y agregué comentarios relevantes. Cuando se inicializa el contenedor, ApplicationContext ya está registrado como Shutdown Hook, este gancho se llama método Close (), por lo que cuando ejecutamos kill -15 pidwhen, la JVM que recibe la instrucción de cierre activa el Shutdown Hook, y luego mediante el método Close () para lidiar con algunos de los Secuelas. Medios de rehabilitación específicos que dependen completamente de la lógica doClose ApplicationContext (), incluida la destrucción de los comentarios de caché de objetos singleton mencionados, liberan el evento de cierre, el contexto de la aplicación y así cerrar, especialmente cuando ApplicationContextla clase se logra AnnotationConfigEmbeddedWebApplicationContextcuando el , También maneja cierta lógica de apagado del servidor de aplicaciones incorporado de tomcat / jetty.

Después de echar un vistazo a estos detalles dentro de springboot, deberíamos entender la necesidad de cerrar las aplicaciones con elegancia. Tanto JAVA como C proporcionan la encapsulación de Signal. También podemos capturar manualmente estas señales del sistema operativo. No introduciré demasiado aquí. Si estás interesado, puedes intentar capturarlas tú mismo.

¿Hay otras formas de cerrar correctamente la aplicación?
El módulo de resorte-arranque-arrancador-actuador proporciona una interfaz tranquila para un apagado elegante.

Agregar dependencia

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

Agregar configuración

#启用shutdown
endpoints.shutdown.enabled=true
#禁用密码验证
endpoints.shutdown.sensitive=false

En producción, tenga en cuenta que este puerto debe tener permisos establecidos, como el uso con spring-security.

La ejecución curl -X POST host:port/shutdownde las instrucciones se puede obtener tras el cierre exitoso de la devolución:

{
    
    "message":"Shutting down, bye..."}

Aunque springboot proporciona dicho método, según mi conocimiento actual, no he visto a nadie usar este método para detener la máquina. El método pid kill -15 logra el mismo efecto, y se enumera aquí para la integridad del programa. .

¿Cómo destruir el grupo de subprocesos como variable miembro?

Aunque la JVM nos ayudará a recuperar ciertos recursos cuando se apague, si algunos servicios utilizan devoluciones de llamada asíncronas y tareas de sincronización, es probable que un manejo inadecuado cause problemas comerciales. Entre ellos, cómo cerrar el grupo de subprocesos es un problema típico.

@Service
public class SomeService {
    
    
    ExecutorService executorService = Executors.newFixedThreadPool(10);
    public void concurrentExecute() {
    
    
        executorService.execute(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                System.out.println("executed...");
            }
        });
    }
}

Necesitamos encontrar una manera de cerrar el grupo de subprocesos cuando la aplicación se apaga (la JVM se apaga y el contenedor deja de ejecutarse).

Plan inicial: no hacer nada. En circunstancias normales, esto no será un gran problema, porque la JVM está cerrada y será liberada, pero obviamente no logró las dos palabras que este artículo viene enfatizando, sí-elegante.

La desventaja del método uno es que las tareas enviadas en el grupo de subprocesos y las tareas no ejecutadas en la cola de bloqueo se vuelven extremadamente incontrolables. Después de recibir la instrucción de apagado, ¿salen inmediatamente? ¿O esperar a que se complete la tarea? ¿O es esperar un cierto período de tiempo antes de que se complete la tarea antes de cerrarla?

Mejora del esquema:

Después de descubrir las desventajas de la solución inicial, inmediatamente pensé en usar la interfaz DisposableBean, así:

@Service
public class SomeService implements DisposableBean{
    
    
    ExecutorService executorService = Executors.newFixedThreadPool(10);
    public void concurrentExecute() {
    
    
        executorService.execute(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                System.out.println("executed...");
            }
        });
    }
    @Override
    public void destroy() throws Exception {
    
    
        executorService.shutdownNow();
        //executorService.shutdown();
    }
}

ThreadPoolExecutor se APAGARÁ después del apagado, no podrá aceptar nuevas tareas y luego esperará a que se complete la ejecución de las tareas que se están ejecutando. Esto significa que el apagado es solo un comando, y si se apaga o no depende del hilo en sí.

ThreadPoolExecutor maneja el apagado Ahora de manera diferente. Después de que el método se ejecuta, se convierte en el estado STOP y llama al método Thread.interrupt () en el subproceso en ejecución (pero si el subproceso no maneja la interrupción, no sucederá nada), por lo que No significa "cerrar inmediatamente".

Verifique el documento java de shutdown and shutdown Ahora, encontrará los siguientes consejos:

shutdown() :Initiates an orderly shutdown in which previously submitted tasks are executed, but no new tasks will be accepted.Invocation has no additional effect if already shut down.This method does not wait for previously submitted tasks to complete execution.Use {
    
    @link #awaitTermination awaitTermination} to do that.
shutdownNow():Attempts to stop all actively executing tasks, halts the processing of waiting tasks, and returns a list of the tasks that were awaiting execution. These tasks are drained (removed) from the task queue upon return from this method.This method does not wait for actively executing tasks to terminate. Use {
    
    @link #awaitTermination awaitTermination} to do that.There are no guarantees beyond best-effort attempts to stop processing actively executing tasks. This implementation cancels tasks via {
    
    @link Thread#interrupt}, so any task that fails to respond to interrupts may never terminate.

Ambos sugieren que necesitamos un awaitTerminationmétodo de ejecución adicional , solo que la ejecución shutdown/shutdownNowno es suficiente.

La solución final: refiriéndonos a la estrategia de reciclaje del pool de hilos en primavera, obtuvimos la solución final.



public abstract class ExecutorConfigurationSupport extends CustomizableThreadFactory
      implements DisposableBean{
    
    
    @Override
    public void destroy() {
    
    
        shutdown();
    }
    /**
     * Perform a shutdown on the underlying ExecutorService.
     * @see java.util.concurrent.ExecutorService#shutdown()
     * @see java.util.concurrent.ExecutorService#shutdownNow()
     * @see #awaitTerminationIfNecessary()
     */
    public void shutdown() {
    
    
        if (this.waitForTasksToCompleteOnShutdown) {
    
    
            this.executor.shutdown();
        }
        else {
    
    
            this.executor.shutdownNow();
        }
        awaitTerminationIfNecessary();
    }
    /**
     * Wait for the executor to terminate, according to the value of the
     * {@link #setAwaitTerminationSeconds "awaitTerminationSeconds"} property.
     */
    private void awaitTerminationIfNecessary() {
    
    
        if (this.awaitTerminationSeconds > 0) {
    
    
            try {
    
    
                this.executor.awaitTermination(this.awaitTerminationSeconds, TimeUnit.SECONDS));
            }
            catch (InterruptedException ex) {
    
    
                Thread.currentThread().interrupt();
            }
        }
    }
}

Los comentarios se mantienen, se eliminan algunos códigos de registro y se presenta ante nuestros ojos una solución para cerrar con gracia el grupo de subprocesos.

  1. Use el indicador waitForTasksToCompleteOnShutdown para controlar si desea terminar todas las tareas inmediatamente o esperar a que se complete la tarea y luego salir.

  2. executor.awaitTermination(this.awaitTerminationSeconds, TimeUnit.SECONDS)); Controle el tiempo de espera para evitar que la tarea se ejecute indefinidamente (como ya se enfatizó anteriormente, incluso shutdownNow no garantiza que el hilo deje de ejecutarse).

Estrategias de cierre más elegantes que requieren nuestro pensamiento

Como se mencionó en nuestra serie de artículos que analizan los principios de RPC, 服务治理框架generalmente se considera el tema del cierre ordenado. La práctica habitual es 事先隔断流量continuar 关闭应用.

Un enfoque común es enviar el nodo de servicio 从注册中心摘除y los suscriptores para recibir notificaciones, eliminar el nodo y, por lo tanto, cerrarlo correctamente;

En lo que respecta a las operaciones de la base de datos, puede utilizar la función ACID de la transacción para asegurarse de que no se produzcan datos anormales incluso si se detiene el bloqueo, por no mencionar la conexión normal;

Otro ejemplo es que la cola de mensajes puede depender del mecanismo ACK + la persistencia del mensaje o la garantía del mensaje de transacción; para los servicios con más tareas de sincronización, debe prestar especial atención al problema de cierre ordenado cuando se maneja sin conexión, porque este es un servicio de larga duración, en comparación con otros servicios. La situación es más susceptible al problema del tiempo de inactividad. Puede utilizar idempotencia y banderas para diseñar tareas cronometradas ...

El soporte de funciones como transacciones y ACK kill -9 pidpuede hacer que el servicio sea lo más confiable posible incluso en situaciones como tiempo de inactividad, cortes de energía, etc., y también debemos pensar en kill -15 pidla estrategia de apagado en condiciones normales fuera de línea. Finalmente, agregue algo de comprensión del gancho de cierre de jvm al resolver este problema.

El gancho de cierre garantizará que la JVM se esté ejecutando hasta que finalice el gancho. Esto también nos aclara que si realizamos una operación de bloqueo cuando recibimos el comando kill -15 pid, podemos esperar a que se complete la ejecución de la tarea antes de apagar la JVM. Al mismo tiempo, también explica el problema de que algunas aplicaciones no pueden salir al ejecutar kill -15 pid. Sí, la interrupción está bloqueada.

Otros buenos artículos: https://blog.csdn.net/u18256007842/article/details/85330504

Supongo que te gusta

Origin blog.csdn.net/qq_21383435/article/details/108523917
Recomendado
Clasificación