<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12

      Java應用的優雅停機

      一. 優雅停機的概念

      優雅停機一直是一個非常嚴謹的話題,但由于其僅僅存在于重啟、下線這樣的部署階段,導致很多人忽視了它的重要性,但沒有它,你永遠不能得到一個完整的應用生命周期,永遠會對系統的健壯性持懷疑態度。

      同時,優雅停機又是一個龐大的話題

      • 操作系統層面,提供了 kill -9 (SIGKILL)和 kill -15(SIGTERM) 兩種停機策略
      • 語言層面,Java 應用有 JVM shutdown hook 這樣的概念
      • 框架層面,Spring Boot 提供了 actuator 的下線 endpoint,提供了 ContextClosedEvent 事件
      • 容器層面,Docker :當執行 docker stop 命令時,容器內的進程會收到 SIGTERM 信號,那么 Docker Daemon 會在 10s 后,發出 SIGKILL 信號;K8S 在管理容器生命周期階段中提供了 prestop 鉤子方法。
      • 應用架構層面,不同架構存在不同的部署方案。單體式應用中,一般依靠 nginx 這樣的負載均衡組件進行手動切流,逐步部署集群;微服務架構中,各個節點之間有復雜的調用關系,上述這種方案就顯得不可靠了,需要有自動化的機制。

      為避免該話題過度發散,本文的重點將會集中在框架和應用架構層面,探討以 Dubbo 為代表的微服務架構在優雅停機上的最佳實踐。Dubbo 的優雅下線主要依賴于注冊中心組件,由其通知消費者摘除下線的節點,如下圖所示:

      上述的操作旨在讓服務消費者避開已經下線的機器,但這樣就算實現了優雅停機了嗎?似乎還漏掉了一步,在應用停機時,可能還存在執行到了一半的任務,試想這樣一個場景:一個 Dubbo 請求剛到達提供者,服務端正在處理請求,收到停機指令后,提供者直接停機,留給消費者的只會是一個沒有處理完畢的超時請求。

      結合上述的案例,我們總結出 Dubbo 優雅停機需要滿足兩點基本訴求:

      1. 服務消費者不應該請求到已經下線的服務提供者
      2. 在途請求需要處理完畢,不能被停機指令中斷

      優雅停機的意義:應用的重啟、停機等操作,不影響業務的連續性

      二. 操作系統層面的停機策略

      操作系統層面,提供了 kill -9 (SIGKILL)和 kill -15(SIGTERM) 兩種停機策略。

      在 Linux 中,kill 命令用于向進程發送信號,以通知該進程執行某些特定的操作。其中,kill 命令最常用的兩個參數是 -9 和 -15,它們分別表示發送 SIGKILL 和 SIGTERM 信號。

      如果使用 kill pid 則默認等價于 kill -15 pid

      • SIGKILL 信號是一個不能被阻塞、處理或忽略的信號,它會立即終止目標進程。使用 kill -9 命令發送 SIGKILL 信號可以強制終止進程,即使進程正在執行某些關鍵操作也會被立即終止,這可能會導致數據損壞或其他不良影響。因此,一般情況下不建議使用 kill -9 命令,除非必要情況下無法通過其他方式終止進程。

      • SIGTERM 信號是一個可以被阻塞、處理或忽略的信號,它也可以通知目標進程終止,但是它相對于 SIGKILL 信號來說更加溫和,目標進程可以在接收到 SIGTERM 信號時進行一些清理操作,例如保存數據、關閉文件、釋放資源等,然后再終止進程。使用 kill -15 命令發送 SIGTERM 信號通常被認為是一種優雅的方式來終止進程,因為這種方式允許進程在終止之前執行一些必要的清理操作,避免了數據損壞和其他不良影響。

      可以先簡單理解下這兩者的區別:kill -9 pid 可以理解為操作系統從內核級別強行殺死某個進程,kill -15 pid 則可以理解為發送一個通知,告知應用主動關閉。這么對比還是有點抽象,那我們就從應用的表現來看看,這兩個命令殺死應用到底有啥區別。

      因此,一般情況下建議首先嘗試使用 kill -15 命令發送 SIGTERM 信號來終止進程,只有在進程無法通過 SIGTERM 信號終止時才考慮使用 kill -9 命令發送 SIGKILL 信號。

      三. Docker 的優雅停機策略

      在 Docker 中,執行 docker stop 命令時,它會向容器中的主進程發送 SIGTERM 信號,而不是向容器中的每個進程都發送信號。容器中的主進程通常是在容器啟動時由 Docker 啟動的第一個進程。

      當接收到 SIGTERM 信號后,容器中的主進程可以選擇優雅地停止容器中的所有進程,以確保數據的一致性和完整性。主進程可以將 SIGTERM 信號傳遞給容器中的其他進程,以便它們可以進行必要的清理操作,并在完成后正常退出。

      如果容器中的進程不響應 SIGTERM 信號,Docker 會等待一定的時間(默認為 10 秒),然后向容器中的所有進程發送 SIGKILL 信號,以強制結束容器中的進程。如果我們需要修改 SIGTERM 信號等待的時間,可以在 docker run 命令中使用 --stop-timeout 參數來更改默認的停止超時時間,從而更改 docker stop 命令發送 SIGKILL 信號的等待時間。

      --stop-timeout 參數接受一個以秒為單位的整數值,表示容器在接收到 SIGTERM 信號后的最長等待時間。如果容器在等待時間內沒有正常停止,Docker 將會向容器發送 SIGKILL 信號,強制終止容器中的進程。

      需要注意的是,在 Docker 中,主進程將 SIGTERM 信號傳遞給容器中的其他進程是否自動進行取決于容器內部的實現。Docker 本身并沒有提供自動將 SIGTERM 信號傳遞給容器中的其他進程的功能。因此,在容器中實現優雅的停機策略需要確保容器內部的所有進程都能正確地處理 SIGTERM 信號,并在接收到信號后正確地執行必要的清理操作。容器中的主進程應該負責協調容器內部的所有進程,以確保容器在關閉時能夠正確地完成必要的清理操作,并避免數據丟失和其他問題。

      四. K8s Pod優雅停機策略

      在 Kubernetes 中,Pod 的優雅停機是指在容器關閉之前,讓容器有足夠的時間來完成正在進行的操作,例如完成正在處理的請求、保存狀態等等。這有助于防止數據丟失和請求中斷,同時也可以避免不必要的資源浪費。

      4.1 preStop鉤子

      Kubernetes 提供了一種優雅停機的機制,可以通過在 Pod 中設置 preStop 鉤子來實現。preStop 鉤子是在 Pod 中的容器被終止之前執行的一個命令或腳本,可以用來觸發一些清理操作,例如保存狀態、關閉連接等等。

      在 Pod 中設置 preStop 鉤子的方式如下:

      1. 在 Pod 的 YAML 文件中添加 preStop 鉤子定義:
      apiVersion: v1
      kind: Pod
      metadata:
        name: my-pod
      spec:
        containers:
        - name: my-container
          image: my-image
          # 添加 preStop 鉤子
          lifecycle:
            preStop:
              exec:
                command: ["/bin/sh", "-c", "my-cleanup-script"]
      

      在上述示例中,preStop 鉤子定義在 Pod 的 spec 部分中,command 字段指定了要執行的命令或腳本。

      1. 保存 YAML 文件并使用 kubectl apply 命令創建或更新 Pod:
      $ kubectl apply -f my-pod.yaml
      

      當 Kubernetes 接收到要停止 Pod 的請求時,就會執行 preStop 鉤子中定義的命令或腳本。一旦 preStop 鉤子執行完成,Kubernetes 就會終止 Pod 中的容器,以便進行下一步操作。這樣,您就可以在容器被終止之前完成一些清理操作,以確保不會出現數據丟失或請求中斷的情況。

      4.2 terminationGracePeriodSeconds

      如果在 Kubernetes 中的 Pod 中沒有指定 preStop 鉤子,那么當需要停止 Pod 時,Kubernetes 會使用以下默認的停機流程:

      1. Kubernetes 向 Pod 發送一個 TERM 信號,要求 Pod 中的所有容器進行優雅停機。
      2. 如果在一定時間內(默認是 30 秒)容器沒有響應 TERM 信號,Kubernetes 會向 Pod 中的所有容器發送一個 KILL 信號,強制終止容器。
      3. 一旦所有的容器都被終止,Kubernetes 就會將 Pod 標記為已終止,并觸發相應的事件。

      在 Kubernetes 中,可以通過在 Pod 的 spec 中設置 terminationGracePeriodSeconds 字段來更改默認的優雅停機等待時間,并且在一定時間內(由該字段指定)容器沒有響應 SIGTERM 信號后,Kubernetes 才會發送 SIGKILL 信號強制終止容器。

      例如,如果要將默認的優雅停機等待時間更改為 60 秒,可以將 Pod 的 spec 設置為:

      apiVersion: v1
      kind: Pod
      metadata:
        name: my-pod
      spec:
        containers:
        - name: my-container
          image: my-image
          # ...
          terminationGracePeriodSeconds: 60
      

      在上述示例中,terminationGracePeriodSeconds 字段設置為 60 秒,表示容器在接收到 SIGTERM 信號后的最長等待時間為 60 秒。如果容器在等待時間內沒有正常停止,Kubernetes 將會向容器發送 SIGKILL 信號,強制終止容器中的進程。

      需要注意的是,更改優雅停機等待時間可能會影響容器的正常停止。如果容器中的進程需要更長的時間來完成必要的清理操作,那么需要相應地調整優雅停機等待時間,以確保容器在關閉時能夠正確地完成必要的清理操作。同時,需要考慮到 Pod 中所有容器的清理操作需要在 terminationGracePeriodSeconds 所指定的時間內完成,否則會被強制終止。

      在 Kubernetes 中,即使在 Pod 中指定了 preStop 鉤子,Kubernetes 仍然會發送 SIGTERM 信號來通知容器停止運行。在 Pod 接收到 SIGTERM 信號后,Kubernetes 會等待 terminationGracePeriodSeconds 所指定的時間,以便容器中的進程有機會完成必要的清理操作。

      在等待時間內,Kubernetes 會運行 preStop 鉤子,以便容器可以在停止之前執行任何必要的清理操作,例如保存數據或與其他服務進行協調。如果 preStop 鉤子運行成功并且容器中的進程在等待時間內完成了必要的清理操作,那么容器會正常停止。否則,Kubernetes 將會向容器發送 SIGKILL 信號,強制終止容器中的進程。

      五. Java語言層面實現優雅停機

      JVM shutdown hook 是 Java 虛擬機提供的一個鉤子(hook),用于在 JVM 關閉之前執行一些必要的清理操作。在 Java 中,可以通過 Runtime 類的 addShutdownHook 方法注冊一個 shutdown hook,當 JVM 接收到中斷信號或者調用 System.exit 方法時,就會執行注冊的 shutdown hook。

      JVM shutdown hook 的具體實現方式如下:

      1. 創建一個繼承自 Thread 類的子類,用于實現 shutdown hook 的邏輯。
      2. 在子類中重寫 run 方法,編寫需要在 JVM 關閉前執行的清理操作。
      3. 在程序中使用 Runtime 類的 addShutdownHook 方法注冊 shutdown hook:
      Runtime.getRuntime().addShutdownHook(new MyShutdownHook());
      

      在上述示例中,MyShutdownHook 是繼承自 Thread 的子類,用于實現 shutdown hook 的邏輯。

      shutdown hook注意事項:

      • JVM shutdown hook 的執行順序是不確定的。當 JVM 接收到中斷信號或者調用 System.exit 方法時,就會同時啟動所有已注冊的 shutdown hook,但是它們的執行順序是不確定的。因此,在編寫 shutdown hook 時,需要考慮到多個 shutdown hook 之間的交互和依賴關系。
      • 程序退出時,JVM 會并發執行所有的應用 Shutdown Hook,并且只有所有 Shutdown Hook 都執行完,程序才正常退出。
      • 執行 Shutdown Hook 時,應該認為應用內的各種服務、資源都已經處于不可靠狀態。因此,編寫 Shutdown Hook 時要特別小心,不要有死鎖。Shutdown Hook 應該是線程安全的,且不依賴于應用資源,比如,假設你的 Shutdown Hook 依賴另一個服務,這個服務又注冊了自己的 Shutdown Hook,已經先行一步清理完自己的資源,這時候你的 Shutdown Hook 就會有問題

      六. SpringBoot 框架層面的優雅停機

      上面解釋過了,使用 kill -15 pid 的方式可以比較優雅的關閉 SpringBoot 應用,我們可能有以下的疑惑:SpringBoot/Spring 是如何響應這一關閉行為的呢?是先關閉了 tomcat,緊接著退出 JVM,還是相反的次序?它們又是如何互相關聯的?

      嘗試從日志開始著手分析,AnnotationConfigEmbeddedWebApplicationContext 打印出了 Closing 的行為,直接去源碼中一探究竟,最終在其父類 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 時,JVM 接收到關閉指令,觸發了這個 Shutdown Hook,進而由 Close() 方法去處理一些善后手段。具體的善后手段有哪些,則完全依賴于 ApplicationContextdoClose() 邏輯,包括了注釋中提及的銷毀緩存單例對象,發布 close 事件,關閉應用上下文等等,特別的,當 ApplicationContext 的實現類是 AnnotationConfigEmbeddedWebApplicationContext 時,還會處理一些 tomcat/jetty 一類內置應用服務器關閉的邏輯。

      窺見了 SpringBoot 內部的這些細節,更加應該了解到優雅關閉應用的必要性。JAVA 和 C 都提供了對 Signal 的封裝,我們也可以手動捕獲操作系統的這些 Signal,在此不做過多介紹,有興趣的朋友可以自己嘗試捕獲下。

      6.1 還有其他優雅關閉應用的方式嗎

      spring-boot-starter-actuator 模塊提供了一個 restful 接口,用于優雅停機。

      添加依賴:

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

      添加配置

      #啟用 shutdown
      endpoints.shutdown.enabled=true
      #禁用密碼驗證
      endpoints.shutdown.sensitive=false
      

      生產中請注意該端口需要設置權限,如配合 spring-security 使用。

      執行 curl -X POST host:port/shutdown 指令,關閉成功便可以獲得如下的返回:

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

      雖然 SpringBoot 提供了這樣的方式,但按我目前的了解,沒見到有人用這種方式停機,kill -15 pid 的方式達到的效果與此相同,將其列于此處只是為了方案的完整性。

      6.2 如何銷毀作為成員變量的線程池

      盡管 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 關閉,會釋放之,但顯然沒有做到本文一直在強調的兩個字,沒錯 —- 優雅。

      方法一的弊端在于線程池中提交的任務以及阻塞隊列中未執行的任務變得極其不可控,接收到停機指令后是立刻退出?還是等待任務執行完成?抑或是等待一定時間任務還沒執行完成則關閉?

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

      緊接著問題又來了,是 shutdown 還是 shutdownNow 呢?這兩個方法還是經常被誤用的,簡單對比這兩個方法。

      ThreadPoolExecutor 在 shutdown 之后會變成 SHUTDOWN 狀態,無法接受新的任務,隨后等待正在執行的任務執行完成。意味著,shutdown 只是發出一個命令,至于有沒有關閉還是得看線程自己。

      ThreadPoolExecutor 對于 shutdownNow 的處理則不太一樣,方法執行之后變成 STOP 狀態,并對執行中的線程調用 Thread.interrupt() 方法(但如果線程未處理中斷,則不會有任何事發生),所以并不代表“立刻關閉”。

      查看 shutdown 和 shutdownNow 的 java doc,會發現如下的提示:

      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 是不夠的。

      最終方案:參考 spring 中線程池的回收策略,我們得到了最終的解決方案。

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

      七. Dubbo 的優雅停機策略

      為了讓讀者對 Dubbo 的優雅停機有一個最基礎的理解,我們首先研究下 Dubbo 2.5.x 的版本,這個版本實現優雅停機的方案相對簡單,容易理解。

      7.1 入口類:AbstractConfig

      public abstract class AbstractConfig implements Serializable {
      	static {
              Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
                  public void run() {
                      ProtocolConfig.destroyAll();
                  }
              }, "DubboShutdownHook"));
          }
      }
      

      AbstractConfig 的靜態塊中,Dubbo 注冊了一個 shutdown hook,用于執行 Dubbo 預設的一些停機邏輯,繼續跟進 ProtocolConfig.destroyAll()

      7.2 ProtocolConfig

      public static void destroyAll() {
          if (!destroyed.compareAndSet(false, true)) {
              return;
          }
          AbstractRegistryFactory.destroyAll(); // ①注冊中心注銷
      
          // Wait for registry notification
          try {
              Thread.sleep(ConfigUtils.getServerShutdownTimeout()); // ② sleep 等待
          } catch (InterruptedException e) {
              logger.warn("Interrupted unexpectedly when waiting for registry notification during shutdown process!");
          }
      
          ExtensionLoader<Protocol> loader = ExtensionLoader.getExtensionLoader(Protocol.class);
          for (String protocolName : loader.getLoadedExtensions()) {
              try {
                  Protocol protocol = loader.getLoadedExtension(protocolName);
                  if (protocol != null) {
                      protocol.destroy(); // ③協議/流程注銷
                  }
              } catch (Throwable t) {
                  logger.warn(t.getMessage(), t);
              }
          }
      }
      

      Dubbo 中的 Protocol 這個詞不太能望文生義,它一般被翻譯為”協議”,但我更習慣將它理解為“流程”,從 Protocol 接口的三個方法反而更加容易理解。

      public interface Protocol {
          <T> Exporter<T> export(Invoker<T> invoker) throws RpcException;
          <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException;
          void destroy();
      }
      

      它定義了暴露、訂閱、注銷這三個生命周期方法,所以不難理解為什么 Dubbo 會把 shutdown hook 觸發后的注銷方法定義在 ProtocolConfig 中了。

      回到 ProtocolConfig 的源碼中,我把 ProtocolConfig 中執行的優雅停機邏輯分成了三部分,其中第 1,2 部分和注冊中心(Registry)相關,第 3 部分和協議/流程(Protocol)相關,分成下面的 7.3 和 7.4 兩部分來介紹。

      7.3 注冊中心注銷邏輯

      public abstract class AbstractRegistryFactory implements RegistryFactory {
      	public static void destroyAll() {
              LOCK.lock();
              try {
                  for (Registry registry : getRegistries()) {
                      try {
                          registry.destroy();
                      } catch (Throwable e) {
                          LOGGER.error(e.getMessage(), e);
                      }
                  }
                  REGISTRIES.clear();
              } finally {
                  // Release the lock
                  LOCK.unlock();
              }
          }
      }
      

      這段代碼對應了 7.2 小節 ProtocolConfig 源碼的第 1 部分,代表了注冊中心的注銷邏輯,更深一層的源碼不需要 debug 進去了,大致的邏輯就是刪除掉注冊中心中本節點對應的服務提供者地址。

      // Wait for registry notification
      try {
          Thread.sleep(ConfigUtils.getServerShutdownTimeout());
      } catch (InterruptedException e) {
          logger.warn("Interrupted unexpectedly when waiting for registry notification during shutdown process!");
      }
      

      這段代碼對應了 7.2 小節 ProtocolConfig 源碼的第 2 部分,ConfigUtils.getServerShutdownTimeout() 默認值是 10s,為什么需要在 shutdown hook 中等待 10s 呢?在注釋中可以發現這段代碼的端倪,原來是為了給服務消費者一點時間,確保等到注冊中心的通知。10s 顯然是一個經驗值,這里也不妨和大家探討一下,如何穩妥地設置這個值呢?

      • 設置的過短。由于注冊中心通知消費者取消訂閱某個地址是異步通知過去的,可能消費者還沒收到通知,提供者這邊就停機了,這就違背了我們的訴求 1:服務消費者不應該請求到已經下線的服務提供者
      • 設置的過長。這會導致發布時間變長,帶來不必要的等待。

      兩個情況對比下,起碼可以得出一個實踐經驗:如果拿捏不準等待時間,盡量設置一個寬松的一點的等待時間。

      這個值主要取決三點因素:

      • 集群規模的大小。如果只有幾個服務,每個服務只有幾個實例,那么再弱雞的注冊中心也能很快的下發通知。
      • 注冊中心的選型。以 Naocs 和 Zookeeper 為例,同等規模服務實例下 Nacos 在推送地址方面的能力遠超 Zookeeper。
      • 網絡狀況。服務提供者和服務消費者與注冊中心的交互邏輯走的 TCP 通信,網絡狀況也會影響到推送時間。

      所以需要根據實際部署場景測量出最合適的值。

      7.4 協議/流程注銷邏輯

      ExtensionLoader<Protocol> loader = ExtensionLoader.getExtensionLoader(Protocol.class);
      for (String protocolName : loader.getLoadedExtensions()) {
          try {
              Protocol protocol = loader.getLoadedExtension(protocolName);
              if (protocol != null) {
                  protocol.destroy();
              }
          } catch (Throwable t) {
              logger.warn(t.getMessage(), t);
          }
      }
      

      這段代碼對應了 7.2 小節 ProtocolConfig 源碼的第 3 部分,在運行時,loader.getLoadedExtension(protocolName) 這段代碼會加載到兩個協議 :DubboProtocolInjvm 。后者 Injvm 實在沒啥好講的,主要來分析一下 DubboProtocol 的邏輯。

      DubboProtocol 實現了我們前面提到的 Protocol 接口,它的 destory 方法是我們重點要看的。

      public class DubboProtocol extends AbstractProtocol {
      
          public void destroy() {
              for (String key : new ArrayList<String>(serverMap.keySet())) {
                  ExchangeServer server = serverMap.remove(key);
                  if (server != null) {
                  	server.close(ConfigUtils.getServerShutdownTimeout());
                  }
              }
      
              for (String key : new ArrayList<String>(referenceClientMap.keySet())) {
                  ExchangeClient client = referenceClientMap.remove(key);
                  if (client != null) {
                  	client.close(ConfigUtils.getServerShutdownTimeout());
                  }
              }
      
              for (String key : new ArrayList<String>(ghostClientMap.keySet())) {
                  ExchangeClient client = ghostClientMap.remove(key);
                  if (client != null) {
                      client.close(ConfigUtils.getServerShutdownTimeout());
                  }
              }
              stubServiceMethodsMap.clear();
              super.destroy();
          }
      }
      

      主要分成了兩部分注銷邏輯:server 和 client,注意這里是先注銷了服務提供者后,再注銷了服務消費者,這樣做是有意為之。在 RPC 調用中,經常是一個遠程調用觸發一個遠程調用,所以在關閉一個節點時,應該先切斷上游的流量,所以這里是先注銷了服務提供者,這樣從一定程度上,降低了后面服務消費者被調用到的可能性(當然,服務消費者也有可能被單獨調用到)。由于 server 和 client 的流程類似,所以我只選取了 server 部分來分析具體的注銷邏輯。

      public void close(final int timeout) {
             startClose();
             if (timeout > 0) {
                 final long max = (long) timeout;
                 final long start = System.currentTimeMillis();
      
                 if (getUrl().getParameter(Constants.CHANNEL_SEND_READONLYEVENT_KEY, true)) {
                     // 如果注冊中心有延遲,會立即受到readonly事件,下次不會再調用這臺機器,當前已經調用的會處理完
                     sendChannelReadOnlyEvent();
                 }
                 while (HeaderExchangeServer.this.isRunning() // ①
                         && System.currentTimeMillis() - start < max) {
                     try {
                         Thread.sleep(10);
                     } catch (InterruptedException e) {
                         logger.warn(e.getMessage(), e);
                     }
                 }
             }
             doClose(); // ②
             server.close(timeout); // ③
         }
         
         private boolean isRunning() {
             Collection<Channel> channels = getChannels();
             for (Channel channel : channels) {
                 if (DefaultFuture.hasFuture(channel)) {
                     return true;
                 }
             }
             return false;
         }
      
      private void doClose() {
             if (!closed.compareAndSet(false, true)) {
                 return;
             }
             stopHeartbeatTimer();
             try {
                 scheduled.shutdown();
             } catch (Throwable t) {
                 logger.warn(t.getMessage(), t);
             }
         }
      

      化繁為簡,這里只挑選上面代碼中打標的兩個地方進行分析

      1. 判斷服務端是否還在處理請求,在超時時間內一直等待到所有任務處理完畢
      2. 關閉心跳檢測
      3. 關閉 NettyServer

      特別需要關注第一點,正符合我們在一開始提出的優雅停機的訴求 2:“在途請求需要處理完畢,不能被停機指令中斷”

      7.5 Spring容器下Dubbo優雅停機

      上述的方案在不使用 Spring 時的確是無懈可擊的,但由于現在大多數開發者選擇使用 Spring 構建 Dubbo 應用,上述的方案會存在一些缺陷。

      由于 Spring 框架本身也依賴于 shutdown hook 執行優雅停機,并且與 Dubbo 的優雅停機會并發執行,而 Dubbo 的一些 Bean 受 Spring 托管,當 Spring 容器優先關閉時,會導致 Dubbo 的優雅停機流程無法獲取相關的 Bean,從而優雅停機失效。

      對這一部分感興趣的小伙伴請查看原文:一文聊透 Dubbo 優雅停機 - 徐靖峰|個人博客 (cnkirito.moe)

      八. 總結

      本文從操作系統、語言、框架層面分別闡述了一個Java應用優雅停機流程,從進程層面,優先推薦使用 SIGTERM 信號通知進程進入銷毀流程,JVM虛擬機進程會監聽該事件,并執行注冊的ShutdownHook。在框架層面不同的框架都會自行注冊自身框架的ShutdownHook,從而保證各種框架的正常銷毀退出。

      但是在容器或者K8S環境下,SIGTERM 信號發送后會有有限的寬容期,如果超過寬容期容器進程仍未正常退出,容器或者pod就會被SIGKILL強行銷毀,所以在容器或者K8S環境下應該合理調整寬容期時長,從而保證應用能夠正常停止。

      本文參考至:

      研究優雅停機時的一點思考 - 徐靖峰|個人博客 (cnkirito.moe)

      一文聊透 Dubbo 優雅停機 - 徐靖峰|個人博客 (cnkirito.moe)

      Java應用的優雅停機總結_Java_陳德偉_InfoQ寫作社區

      posted @ 2023-04-21 22:38  聽到微笑  閱讀(137)  評論(0)    收藏  舉報  來源
      主站蜘蛛池模板: 亚洲一区二区三午夜福利| 综1合AV在线播放| 亚洲最大av资源站无码av网址| 国模雨珍浓密毛大尺度150p| 国内揄拍国内精品人妻久久| 人妻少妇久久久久久97人妻| 亚洲三级香港三级久久| 国产在线精彩自拍视频| 亚洲人成电影在线天堂色| 国产免费一区二区不卡| 国产一区二区av天堂热| 99欧美日本一区二区留学生| 通化市| 无码高潮爽到爆的喷水视频app| 中文字幕日韩国产精品| 亚洲 日韩 国产 制服 在线 | 亚洲人亚洲人成电影网站色 | 免费一级黄色好看的国产| 亚洲一本二区偷拍精品| 濮阳市| 免费国产一级 片内射老| 亚洲综合一区二区三区在线| 日韩精品专区在线影观看| 91在线国内在线播放老师 | 成人免费亚洲av在线| 亚洲精品欧美综合二区| 国产成人精品一区二区三| 中文字幕无码久久精品| 辛集市| 国产精品自在自线免费观看| 国产va免费精品观看| 国产AV国片精品有毛| 久久免费偷拍视频有没有| 亚洲sm另类一区二区三区| 国产男女黄视频在线观看| 色吊丝一区二区中文字幕| 日韩V欧美V中文在线| 亚洲国产成人无码影片在线播放| 久久综合给合久久狠狠97色 | 国产在线精品欧美日韩电影| 国产精品福利一区二区三区|