當前位置: 妍妍網 > 碼農

SpringBoot + Nacos + k8s 優雅停機

2024-03-07碼農

架構師(JiaGouX)

我們都是架構師!
架構未來,你來不來?

  • 1 概念

  • 2 用案例說話

  • 案例前:k8s 停機流程

  • k8s + springboot + nacos 案例

  • 案例最佳化

  • 3 再次最佳化

  • mq 和 定時任務

  • 流量控制

  • 4 小結


  • 1 概念

    優雅停機是什麽?網上說的優雅下線、無失真下線,都是一個意思。

    優雅停機,通常是指在裝置、系統或應用程式中止運作前,先執行一定的流程或動作,以確保數據的安全、預防錯誤並保證系統的整體穩定。

    一般來說,優雅停機可以參考以下步驟以實作:

    1. 備份數據 :立即將記憶體中的所有未保存的修改、緩存等數據保存到資料庫或磁盤中。

    2. 停止接收新的請求

    3. 處理未完成的請求

    4. 通知其他依賴元件

    5. 等待所有要素安全結束後,關閉系統

    在具體實施時,不同的裝置、不同的系統、不同的套用,所需要的優雅停機步驟也不盡相同,甚至需要根據不同的場景來選擇不同的方法。

    例如,在某些情況下,你可能需要讓使用者知道,系統即將關閉,並告訴他們應當保存所有的工作並結束系統;而在另一些情況下,你可能需要設計一種策略,能夠讓系統在無使用者介入的情況下,自動保存所有的狀態,並在下次啟動時恢復之。

    但是,無論在哪種情況下,優雅停機的目標都是保護數據,避免錯誤,並盡量減少到訪使用者或使用者的不便。

    上面的步驟,其實還缺了不少基礎的內容,比如,停止請求外,還要停止接收定時任務、停止接收mq訊息,等待他們的完成,這2項都是我們微服務中必不可缺的能力。

    因此,我希望透過本文,能夠更清晰,更詳細的講解,在我已知的真實業務場景下,如何做優雅停機。

    文中,很多內容不會講得太詳細,需要大家有一定的搜尋能力或者經驗!


    2 用案例說話

    隨著微服務的興起,運維方式由docker -> k8s 變化,優雅停機涉及到的點就越來越多!下面,我們用一個案例,說明優雅停機中的問題和問題解決方案。

    案例前:k8s 停機流程

    當程式設計師執行 kubectl delete pod 命令時,兩個過程開始:

    網路規則即將生效:
  • Kube-apiserver 收到 pod 刪除請求,並將 pod 的狀態更新為 Extinating at Etcd;

  • 終結點控制器從終結點物件中刪除 Pod 的 IP;

  • Kuber-proxy 根據 Endpoint 物件的更改更新 iptables 的規則,並且不再將流量路由到已刪除的 pod。

  • 刪除容器:
  • Kube-apiserver 收到 pod 刪除請求,並將 pod 的狀態更新為 Extinating at Etcd;

  • Kubelet 清理節點處的容器相關資源,如儲存、網路;

  • 添加 Prestop hook 勾點,等待流量不再發給pod;

  • Kubelet 將SIGTERM發送到容器;

  • 如果容器在預設的 30 秒內沒有結束,Kubelet 將發送 SIGKILL 並強制其結束。

  • 圖片

    k8s + springboot + nacos 案例

    圖片

    PreStopHook 做了2件事情:

    1. nacos反註冊

    2. 休眠35秒

    透過號誌關閉springboot程式;

    其中,k8s的 terminationGracePeriodSeconds (寬限期)設定為35s。

    問題

    springBoot程式關閉時間只有2s, 那麽該程式就無法處理完一些執行緒任務、異步訊息、定時任務等。為什麽呢?

    寬限期設定了35s,PreStop休眠了35s + 一個請求的時間,超過了寬限期,那麽 kubelet 就會給與 pod 增加一次性2s的寬限時間。Pod 的生命周期,2s不管程式是否正常結束,都會被Kill -9。

    為什麽反註冊之後需要休眠35s?

    這裏涉及到nacos服務發現原理,nacos服務變更響應時間:即時;ribbon 預設緩存重新整理時間30s;因此,一開始是設定30s的,發現還有feign請求失敗的情況,所以設定成了35s以解決這個問題!

    nacos服務變更響應時間真的是即時嗎?

    其實並不一定,nacos服務發現是透過http和udp實作的,udp是即時的,http最大等待時間是10s,但是,udp埠生產環境可能沒有開放!所以,案例中的nacos服務發現僅透過http定時輪詢實作。

    案例最佳化

    上面的案例可以最佳化的點

  • nacos 反註冊後休眠35s,是否可以減少;

  • terminationGracePeriodSeconds 設定多少合理?

  • 最佳化點1

    反註冊後休眠的35s時候受到nacos服務發現 + ribbon 緩存重新整理時間影響,正常應該是 服務發現時間 + 緩存重新整理時間 40s才能在極端情況下保證服務停機時,不會再有feign 請求進入。

    如果想要縮短這個時間

  • 啟用udp,這個需要和運維同學商量,否則10s等待少不了;

  • 監聽nacos服務變更通知,發現服務下線後,及時重新整理ribbon緩存;

  • /**
     * 訂閱 nacos 例項變更通知
     * 手動重新整理 ribbon 服務例項緩存
     * nacos client 1.4.6 【1.4.1有重大缺陷,要註意】
     */
    @Component
    @Slf4j
    public class NacosInstancesChangeEventListener extends Subscriber<InstancesChangeEvent> {
    @Resource
    private SpringClientFactory springClientFactory;
    @PostConstruct
    public void registerToNotifyCenter(){
    NotifyCenter.registerSubscriber(this);
    }
    @Override
    public void onEvent(InstancesChangeEvent event) {
    String service = event.getServiceName();
    // service: DEFAULT_GROUP@@demo ribbonService: demo
    String ribbonService = service.substring(service.indexOf("@@") + 2);
    log.info("#### 接收到微服務nacos例項變更事件:{} ribbonServiceName: {}", event.getServiceName(), ribbonService);
    ILoadBalancer loadBalancer = springClientFactory.getLoadBalancer(ribbonService);
    if(loadBalancer != null){
    ((ZoneAwareLoadBalancer<?>) loadBalancer).updateListOfServers();
    log.info("重新整理 ribbon 服務例項:{} 緩存成功", ribbonService);
    }
    }
    @Override
    public class<? extends com.alibaba.nacos.common.notify.Event> subscribeType() {
    return InstancesChangeEvent. class;
    }

    /**
    * nacos 1.4.4 ~ 1.4.6 需要加這個方法的實作, 2.1.2以後版本修復了該問題
    * 多註冊中心時,變更事件沒有隔離,因此需要實作該方法來判斷事件是否需要處理
    * @see <a href="https://github.com/alibaba/nacos/issues/8428">ISSUE #8428 - Nacos InstancesChange Event Scope</a>
    * **/
    @Override
    public boolean scopeMatches(InstancesChangeEvent event) {
    returntrue;
    }
    }



    最佳化點2

    terminationGracePeriodSeconds 的值應該略大於 PreStop耗時 + springBoot 停機時間,springBoot 停機時間是由程式業務決定的(mq訊息、定時任務、執行緒池任務、以及備份數據),網上的推薦做法是啟用springBoot的優雅停機功能,並實作自訂的關閉邏輯。

    springBoot優雅停機的預設緩沖時間是30s,因此, terminationGracePeriodSeconds 的時間個人建議10 + 30s即可。

    經過最佳化後

    圖片
    使用 actuator shutdown 方案

    有些網貼推薦使用 actuator shutdown 進行優雅停機,那麽看下其流程圖:

    圖片

    其實,真正的情況並非如上圖所示,因為呼叫shutdown後,springBoot就會進入優雅停機流程,但是這個流程沒有結束,然後就會被 kill -15 中斷,如果執行緒池沒有做好配置,執行緒池任務沒有結束,服務就會關閉。

    // 沒有設定下面參數,在kill -15時,執行緒池沒有執行結束,會被強制關閉
    threadPoolTaskExecutor.setWaitForTasksToCompleteOnShutdown(true);
    threadPoolTaskExecutor.setAwaitTerminationSeconds(30);


    3 再次最佳化

    mq 和 定時任務

    上面的方案中,提到nacos反註冊時,其他服務監聽反註冊事件,進行ribbon緩存重新整理,那麽,反註冊的服務(停機服務)自身,是否可以也監聽該事件呢?答案是可以的。

    停機的服務監聽nacos反註冊事件,判斷是自己反註冊了,表示準備關機,那麽就可以停止對mq訊息的監聽,停止定時任務,這樣就比在優雅停機時,進行mq 和 定時任務的停止更完美。

    圖片

    流量控制

    如果沒有使用k8s進行pod節點的流量控制,那麽大機率會使用 springCloud gateway作為服務閘道器,因此,gateway 服務也應該監聽nacos的反註冊事件,從而及時重新整理ribbon的緩存,關閉停機服務的流量。


    4 小結

    經過大量的資料參考、學習,最終得到的一份自己認為合格的優雅停機方案,裏面可能有較多的不專業表述,敬請諒解和指正,謝謝。

    在本文的最後,還要說下,優雅停機最大的挑戰並不是來源於這個優雅停機流程,機械化的流程前人都幫忙躺過了,剩下的是業務服務自身的邏輯:

  • 有沒有包含超過30s的業務邏輯,如執行超過30s的請求,定時任務、執行緒池任務或mq訊息;

  • 服務關閉時,如何保存未完成的任務、數據,實作自訂的關閉邏輯;

  • 介面邏輯是否做了冪等;

  • 如喜歡本文,請點選右上角,把文章分享到朋友圈
    如有想了解學習的技術點,請留言給若飛安排分享

    因公眾號更改推播規則,請點「在看」並加「星標」 第一時間獲取精彩技術分享

    ·END·

    相關閱讀:

    作者:hj545king

    來源:juejin.cn/post/7281486769290526757

    版權申明:內容來源網路,僅供學習研究,版權歸原創者所有。如有侵權煩請告知,我們會立即刪除並表示歉意。謝謝!

    架構師

    我們都是架構師!

    關註 架構師(JiaGouX),添加「星標」

    獲取每天技術幹貨,一起成為牛逼架構師

    技術群請 加若飛: 1321113940 進架構師群

    投稿、合作、版權等信箱: [email protected]