大家好,我是架構擺渡人。這是實踐經驗系列的第八篇文章,這個系列會給大家分享很多在實際工作中有用的經驗,如果有收獲,還請分享給更多的朋友。
服務部署,是一個避免不了的問題。按正常迭代的速度一般兩周會發一個版本,此時就需要部署新的代碼。發布方式,我相信主流的都是用滾動發布,因為這樣的成本是最低的,機器數量是固定的,一台台機器輪流發布。
但是我們總會在發布過程中碰到一些報錯信息,那是因為請求還沒結束,某些組件已經強制停止了,比如我們的數據源,比如異步任務還沒處理完。
那么如何解決這個問題呢?那就是服務優雅下線,估計大家都聽過這個詞,但我不知道有多少做到了隨時發布都不影響功能的正常使用。
優雅下線涉及點
外部請求必須處理完
服務時時刻刻都在處理請求,一旦收到要停止的命令,那么必須等待當前的請求執行完畢才能去關閉一些資源,否則就會出現各種異常。
除了等待,還需要讓外部的請求不要再過來,要告訴別人,我要下班了,不要來找我了,去找其他人吧。否則你永遠都下不了班,是一樣的道理。
異步任務必須處理完
這里的異步任務通常指我們放入線程池中進行處理的任務,如果強制進行程序的停止,那么線程池里的任務就會丟掉,所以除了同步被外部調用的邏輯要處理完,這種異步的邏輯也是要處理完的。
這里再提一點,就是如果異步任務丟失會對業務造成影響的這種場景,建議還是不要放到線程池里面進行處理,如果要放,那么必須有持久化,程序重啟后可以繼續執行。
消息必須消費完
消息也是異步任務的一種類型,我們的目標肯定也是需要讓消息消費完才行。但是消息跟線程池里的任務最大的差別就在於:消息是有持久化的,並且有重試功能。
就算消息沒消費完,程序強制停止,這條消息沒有ACK,然后就會重試到另一台機器的實例上繼續執行,前提是你的這個執行邏輯不能產生臟數據,一定要通過事務保證數據的一致性。
優雅下線解決方案
注銷服務實例
下線最重要的一件事情就是注銷自己的實例,這樣才不會有后續的請求過來。注銷實例主要是跟注冊中心交互,將自己的實例從注冊中心下線掉就行了。下線后服務消費者會重新從注冊中心拉取最新的實例列表,也就不會將請求路由過來。
如果要下線的這個服務不是一個內部服務,而是網關呢?網關是流量的入口,客戶端的請求過來的,客戶端是自然不知道網關有多少實例,所以在網關前面都有一個負載均衡器,比如常用的Nginx。
那就需要將這個下線的網關實例從Nginx中進行下線操作,這樣后續的流量才不會被轉發過來,跟內部服務是一樣的道理。
Nginx如果有獨立的模塊去對接注冊中心的話,那么還是把注冊中心的給下線掉,Nginx就能感知到下線動作。如果沒有對接,而是固定的配置信息,那么就需要改Nginx的配置,然后重新加載即可。
注銷MQ消費實例
通過下線注冊中心里面的實例,外部流量就不會請求過來。此時還需要將MQ的實例進行下線操作,告訴MQ的服務端,不要再給我推消息了或者是客戶端不再拉取消息。
實現思路
- 寫一個停止流量的接口,在接口中將本身實例從注冊中心,MQ進行下線操作。
- 寫一個檢測流量是否結束的接口,在接口中判斷當前是否還有正在工作的線程,有沒有正在處理的消息,有沒有正在執行的異步任務等等。
- 當完全沒有流量的時候,發布平台直接對當前進程進行kill操作,此時所有任務都已執行完並且沒有新流量進來,無損操作。
- 執行發布流程。
這里其實涉及到一個點,就是假如3分鍾了,還是有任務在處理,那么是否要強制中斷?
這里其實可以這么做,就是我們的服務本身的實例一旦下線,正常的話幾秒鍾后就應該沒有任務了,因為對外的接口基本上都是毫秒級響應。主要就怕異步任務,比如線程池里堆積了好多任務等待執行,所以大家需要去梳理下,如果有這種場景就調整,不要往線程池里堆積任務,這樣才能保證在下流量的時候能夠盡快執行完成。
總結
其實優雅下線的核心在於流量的切換,就是我要下線的這個服務必須把所有外部的流量都切走,然后再把沒處理完的事情處理完,完成后就可以直接重新發布了。
如果你們上了容器的話,容器管理平台應該是能夠提供優雅下線的方式,像K8s里面應該就有優雅停止Pod的方式,不過我對K8s不太熟,記得是有的,其實原理也很簡單,先啟動一個Pod,完成之后將流量切過去就行了,這種方式更簡單,充分利用了容器的優勢。