[자바] 정상 종료에 대한 몇 가지 생각

여기에 사진 설명 삽입

1. 개요

재판 : http://cxytiandi.com/blog/detail/15386

재판 : Xu Jingfeng Kirito의 기술 공유

재시작 스크립트는 최근 프로젝트, 운영 및 유지 보수가 kill -9재시작 springboot 임베디드 바람둥이 방식 을 사용 하는 것으로 밝혀졌습니다 . 사실 거의 모든 사람들이 동의했습니다. kill -9상대적으로 폭력적인 방식이지만 정확히 무엇을 가져올 것인지 단서를 분석 할 수있는 사람은 거의 없습니다. 이 기사는 주로 내 생각 과정을 기록합니다.

kill -9와 kill -15의 차이점은 무엇입니까?

과거에 웹 애플리케이션을 게시하는 일반적인 단계는 코드를 war 패키지로 래핑 한 다음 애플리케이션 컨테이너 (예 : Tomcat, Weblogic)가 장착 된 Linux 시스템에 드롭하는 것이 었습니다. 지금은 매우 다른 방식으로 애플리케이션을 시작 / 종료하려고합니다. 간단하게 시작 / 종료 스크립트를 실행하면됩니다. Springboot는 내장 된 tomcat 서버와 함께 전체 애플리케이션을 패키징하는 또 다른 방법을 제공합니다. 이는 의심 할 여지없이 애플리케이션을 게시하는 데 큰 편의를 제공하며, 스프링 부트 애플리케이션을 닫는 방법에 대한 질문도 제기합니다. 명백한 방법은 애플리케이션 이름을 기반으로 프로세스 ID를 찾고 프로세스 ID를 종료하여 애플리케이션을 닫는 것입니다.

위의 시나리오 설명은 내 질문을 제기합니다. 스프링 부트 애플리케이션 프로세스를 정상적으로 종료하는 방법은 무엇입니까? 다음은 가장 널리 사용되는 Linux 운영 체제입니다. 예를 들어, Linux kill 명령에서 프로세스를 종료하고 그 뒤에 신호 번호 (Signal)를 나타내는 숫자가 올 수 있으며 kill -l명령을 실행 하고 모든 신호 번호를 나열 할 수 있습니다.

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

이 기사에서는 주로 9 번째 신호 코드 KILL과 15 번째 신호 번호 TERM을 소개합니다.

먼저 두 가지의 차이점을 이해하기 만하면됩니다.

  1. kill -9 pid 운영 체제가 커널 수준에서 프로세스를 강제로 종료한다는 것을 이해할 수 있습니다.
  2. kill -15 pid 응용 프로그램이 적극적으로 닫히도록 알리는 알림을 보내는 것으로 이해할 수 있습니다.

이 비교는 여전히 약간 추상적이므로 응용 프로그램의 성능을 살펴 보겠습니다. 응용 프로그램을 종료하는이 두 명령의 차이점은 무엇입니까?

코드 준비

저자는 springboot와 많은 접촉을 가지고 있으므로 간단한 springboot 애플리케이션을 예로 들어 토론을 시작하고 다음 코드를 추가합니다.

1 DisposableBean 인터페이스를 구현하는 클래스 추가

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

2 JVM이 닫힐 때 후크 추가

@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 ...");
            }
        }));
    }
}

테스트 단계

실행 java -jar test-shutdown-1.0.jar응용 프로그램 및 실행
테스트 kill -9 pid,kill -15 pid,ctrl + c로그 출력 콘텐츠 후
테스트 결과를

kill -15 pid & ctrl + c, 효과는 동일하며 출력은 다음과 같습니다.

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, 애플리케이션 로그가 출력되지 않습니다.

kill -9 pid애플리케이션이 경계를 늦추어 애플리케이션이 반응 할 기회가 없다는 것을 알 수 있습니다 . 다른 한편 kill -15 pid으로는 먼저 AnnotationConfigEmbeddedWebApplicationContext (一个 ApplicationContext 的实现类)통지를 받고 테스트 코드 Shutdown Hook를 구현 한 다음 DisposableBean#destory()메소드 의 최종 구현 이 더 우아 합니다 . 어느 쪽이 좋든 나쁘 든 판단이 내려집니다.

일반적으로 응용 프로그램이 닫힐 때 다음과 같은 "후속"논리를 처리합니다.

  1. 소켓 링크 닫기
  2. 임시 파일 정리
  3. 구독자에게 메시지를 보내 오프라인으로 전환하도록 알립니다.
  4. 파괴 될 예정임을 자식 프로세스에 알립니다.
  5. 다양한 자원 공개
  6. 그리고 더 많은

그리고 kill -9 pid그것은 직접적인 아날로그 시스템 중단 시간이며, 시스템은 전력을 잃고 응용 프로그램이 너무 비우호적이므로 수확기를 사용하여 꽃 화분을 자르지 마십시오. 대신 대신 사용하는 kill -15 pid것입니다. 특정 관행 :kill -15 pid이 응용 프로그램을 닫을 수없는 경우 커널 수준 사용을 고려할 수 kill -9 pid있지만 조사 후 어떤 원인 kill -15 pid을 닫을 수 없는지 확인하십시오 .

springboot는 -15 TERM 신호를 어떻게 처리합니까?

위에서 설명한 것처럼, springboot 애플리케이션은 kill -15 pid를 사용하여 정상적으로 종료 될 수 있습니다. 다음과 같은 의심이있을 수 있습니다 : springboot / spring은이 종료 동작에 어떻게 반응합니까? 먼저 바람둥이를 닫은 다음 JVM을 종료 했습니까, 아니면 역순으로 했습니까? 그들은 서로 어떻게 관련되어 있습니까?

로그 시작부터 분석을 진행 AnnotationConfigEmbeddedWebApplicationContext하고, Closing of behavior를 인쇄하고, 소스 코드로 직접 이동하여 알아 내고, AbstractApplicationContext키 코드를 찾기 위해 부모에서 절정에 이릅니다 .

@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);
   }
}

조판과 이해의 용이성을 위해 소스 코드에서 예외 처리 코드의 일부를 제거하고 관련 주석을 추가했습니다. 컨테이너가 초기화되면 ApplicationContext가 이미 Shutdown Hook에 등록되어 있고,이 후크를 Close () 메서드라고합니다. 따라서 언제 실행 kill -15 pid하면 close 명령을받은 JVM이 Shutdown Hook을 트리거 한 다음 Close () 메서드에 의해 일부를 처리합니다. 여파. 파괴 단일 객체 캐시 의견을 포함하여 doClose의 ApplicationContext에 전적으로 의존하는 특정 재활 수단 () 논리는, close 이벤트, 애플리케이션 컨텍스트 및 해제, 언급 너무 가까이, 특히 ApplicationContext클래스가 달성 AnnotationConfigEmbeddedWebApplicationContext될 때 , 또한 일부 tomcat / jetty 내장 애플리케이션 서버 종료 로직을 ​​처리합니다.

springboot 내부에서 이러한 세부 사항을 살펴본 후에는 응용 프로그램을 정상적으로 닫아야하는 필요성을 이해해야합니다. JAVA와 C는 모두 Signal의 캡슐화를 제공합니다. 운영 체제의 이러한 Signal을 수동으로 캡처 할 수도 있습니다. 여기서는 많이 소개하지 않겠습니다. 관심이있는 경우 직접 캡처 해 볼 수 있습니다.

앱을 정상적으로 종료하는 다른 방법이 있습니까?
spring-boot-starter-actuator 모듈은 정상적인 종료를위한 편안한 인터페이스를 제공합니다.

종속성 추가

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

구성 추가

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

프로덕션에서이 포트는 스프링 보안과 함께 사용하는 것과 같은 권한을 설정해야합니다.

curl -X POST host:port/shutdown명령 실행 은 성공적인 반품 마감 후에 얻을 수 있습니다.

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

springboot는 이러한 방법을 제공하지만 현재의 이해에 따르면이 방법을 사용하여 기계를 중지하는 사람은 아무도 보지 못했습니다 .kill -15 pid 방법은 동일한 효과를 내며 프로그램의 완성도를 위해 여기에 나열되어 있습니다. .

스레드 풀을 멤버 변수로 파괴하는 방법은 무엇입니까?

JVM은 종료시 특정 자원을 회수하는 데 도움이되지만, 일부 서비스가 비동기 콜백 및 타이밍 작업을 사용하는 경우 부적절한 처리로 인해 비즈니스 문제가 발생할 수 있으며 그중 스레드 풀을 종료하는 방법이 일반적인 문제입니다.

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

애플리케이션이 종료 될 때 스레드 풀을 닫는 방법을 찾아야합니다 (JVM이 종료되고 컨테이너가 실행을 중지 함).

초기 계획 : 아무것도하지 마십시오. 정상적인 상황에서는 JVM이 닫히고 출시 될 것이기 때문에 큰 문제는 아니지만 분명히이 기사가 강조하고있는 두 단어, 예-우아함을 달성하지 못했습니다.

방법 1의 단점은 스레드 풀에 제출 된 작업과 블로킹 대기열에서 실행되지 않은 작업이 극도로 제어 할 수 없게된다는 점입니다. 종료 명령을받은 후 즉시 종료됩니까? 아니면 작업이 완료 될 때까지 기다리시겠습니까? 아니면 작업이 종료되기 전에 일정 시간 동안 기다려야합니까?

계획 개선 :

초기 솔루션의 단점을 발견 한 후 즉시 다음과 같은 DisposableBean 인터페이스 사용을 생각했습니다.

@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는 종료 후 SHUTDOWN 상태가되어 새 작업을 수락 할 수 없으며 실행중인 작업의 실행이 완료 될 때까지 기다립니다. 즉, 종료는 명령 일 뿐이며 종료 여부는 스레드 자체에 따라 다릅니다.

ThreadPoolExecutor는 shutdownNow를 다르게 처리합니다. 메소드가 실행 된 후에는 STOP 상태가되고 실행중인 스레드에서 Thread.interrupt () 메소드를 호출합니다 (하지만 스레드가 인터럽트를 처리하지 않으면 아무 일도 발생하지 않음). "즉시 마감"을 의미하지는 않습니다.

종료 및 종료의 Java 문서를 확인하십시오. 이제 다음 팁을 찾을 수 있습니다.

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.

둘 다 추가 실행 awaitTermination방법이 필요하다는 것을 암시 하지만 실행 shutdown/shutdownNow만으로는 충분하지 않습니다.

최종 해결책 : 봄의 스레드 풀 재활용 전략을 참조하여 최종 해결책을 얻었습니다.



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();
            }
        }
    }
}

주석은 유지되고 일부 로그 코드는 제거되며 스레드 풀을 정상적으로 닫는 솔루션이 눈앞에 제시됩니다.

  1. waitForTasksToCompleteOnShutdown 플래그를 사용하여 모든 작업을 즉시 종료할지 아니면 작업이 완료되고 종료 될 때까지 기다릴 지 제어합니다.

  2. executor.awaitTermination(this.awaitTerminationSeconds, TimeUnit.SECONDS)); 작업이 무기한 실행되지 않도록 대기 시간을 제어합니다 (앞서 이미 강조했듯이 shutdownNow도 스레드 실행 중지를 보장하지 않음).

우리의 사고를 필요로하는보다 우아한 종료 전략

RPC 원칙을 분석하는 일련의 기사에서 언급했듯이 服务治理框架일반적으로 정상 종료 문제가 고려됩니다. 일반적인 관행은 事先隔断流量계속하는 것 关闭应用입니다.

일반적인 접근 방식은 从注册中心摘除알림을 수신하고 노드를 제거하여 정상적으로 종료 하도록 서비스 노드 와 가입자 를 보내는 것입니다 .

데이터베이스 작업과 관련하여 트랜잭션의 ACID 기능을 사용하여 정상적인 오프라인은 말할 것도없고 크래시가 중지 되더라도 비정상 데이터가 발생하지 않도록 할 수 있습니다.

또 다른 예는 메시지 큐가 ACK 메커니즘 + 메시지 지속성 또는 트랜잭션 메시지 보장에 의존 할 수 있다는 것입니다. 타이밍 작업이 더 많은 서비스의 경우 오프라인으로 처리 할 때 정상 종료 문제에 특히주의해야합니다. 이는 다른 서비스에 비해 장기 실행 서비스이기 때문입니다. 상황은 다운 타임 문제에 더 취약합니다. 멱등 성과 플래그를 사용하여 시간이 지정된 작업을 설계 할 수 있습니다.

트랜잭션, ACK 등의 기능 지원으로 다운 타임, 정전 kill -9 pid등의 상황 에서도 가능한 한 안정적인 서비스를 제공 할 수 있으며 , kill -15 pid정상적인 오프라인 상태에서의 셧다운 전략도 고려해야합니다. 마지막으로이 문제를 해결할 때 jvm 종료 후크에 대한 이해를 추가하십시오.

종료 후크는 후크가 종료 될 때까지 JVM이 실행 중인지 확인합니다. 이것은 또한 우리가 kill -15 pid 명령을받을 때 차단 작업을 수행하면 JVM을 종료하기 전에 작업 실행이 완료 될 때까지 기다릴 수 있음을 알려줍니다. 동시에 kill -15 pid를 실행할 때 일부 응용 프로그램을 종료 할 수없는 문제도 설명합니다. 네, 인터럽트가 차단됩니다.

기타 좋은 기사 : https://blog.csdn.net/u18256007842/article/details/85330504

추천

출처blog.csdn.net/qq_21383435/article/details/108523917