Go 實現熱重啟的詳細介紹

2021-10-09 08:12:11 字數 3865 閱讀 3592

最近在優化公司框架 trpc 時發現了乙個熱重啟相關的問題,優化之餘也總結沉澱下,對 go 如何實現熱重啟這方面的內容做乙個簡單的梳理。

1.什麼是熱重啟?

這是我理解的熱重啟的乙個大致描述。熱重啟現在還有沒有存在的必要?我的理解是看場景。

以後臺開發為例,假如運維平台有能力在服務公升級、重啟時自動踢掉流量,服務就緒後又自動加回流量,假如能夠合理預估服務 qps、請求處理時長,那麼只要配置乙個合理的停止前等待時間,是可以達到類似熱重啟的效果的。這樣的話,在後台服務裡面支援熱重啟就顯得沒什麼必要。但是,如果我們開發乙個微服務框架,不能對將來的部署平台、環境做這種假設,也有可能使用方只是部署在一兩台物理機上,也沒有其他的負載均衡設施,但不希望因為重啟受干擾,熱重啟就很有必要。當然還有一些更複雜、要求更苛刻的場景,也需要熱重啟的能力。

熱重啟是比較重要的一項保證服務質量的手段,還是值得了解下的,這也是本文介紹的初衷。

2.如何實現熱重啟?

如何實現熱重啟,這裡其實不能一概而論,要結合實際的場景來看(比如服務程式設計模型、對可用性要求的高低等)。大致的實現思路,可以先拋一下。

一般要實現熱重啟,大致要包括如下步驟:

首先,要讓老程序,這裡稱之為父程序了,先要 fork 出乙個子程序來代替它工作;

然後,子程序就緒之後,通知父程序,正常接受新連線請求、處理連線上收到的請求;

再然後,父程序處理完已建立連線上的請求後、連線空閒後,平滑退出。

聽上去是挺簡單的...

2.1.認識 fork

大家都知道fork()系統呼叫,父程序呼叫 fork 會建立乙個程序副本,**中還可以通過 fork 返回值是否為 0 來區分是子程序還是父程序。12

3456

78int main(char **ar**, int argc) else

}可能有些開發人員不知道 fork 的實現原理,或者不知道 fork 返回值為什麼在父子程序中不同,或者不知道如何做到父子程序中返回值不同……了解這些是要有點知識積累的。

2.2.返回值

簡單概括下,abi 定義了進行函式呼叫時的一些規範,如何傳遞引數,如何返回值等等,以 x86 為例,如果返回值是 rax 暫存器能夠容的一般都是通過 rax 暫存器返回的。

如果 rax 暫存器位寬無法容納下的返回值呢?也簡單,編譯器會安插些指令來完成這些神秘的操作,具體是什麼指令,就跟語言編譯器實現相關了。

c 語言,可能會將返回值的位址,傳遞到 rdi 或其他暫存器,被調函式內部呢,通過多條指令將返回值寫入 rdi 代指的記憶體區;

c 語言,也可能在被調函式內部,用多個暫存器 rax,rdx...一起暫存返回結果,函式返回時再將多個暫存器的值賦值到變數中;

也可能會像 golang 這樣,通過棧記憶體來返回;

2.3.fork 返回值

fork 系統呼叫的返回值,有點特殊,在父程序和子程序中,這個函式返回的值是不同的,如何做到的呢?

聯想下父程序呼叫 fork 的時候,作業系統核心需要幹些什麼呢?分配程序控制塊、分配 pid、分配記憶體空間……肯定有很多東西啦,這裡注意下程序的硬體上下文資訊,這些是非常重要的,在程序被排程演算法選中進行排程時,是需要還原硬體上下文資訊的。

linux fork 的時候,會對子程序的硬體上下文進行一定的修改,我就是讓你 fork 之後拿到的 pid 是 0,怎麼辦呢?前面 2.2 節提過了,對於那些小整數,rax 暫存器存下綽綽有餘,fork 返回時就是將作業系統分配的 pid 放到 rax 暫存器的。

那,對於子程序而言,我只要在 fork 的時候將它的硬體上下文 rax 暫存器清 0,然後等其他設定全 ok 後,再將其狀態從不可中斷等待狀態修改為可執行狀態,等其被排程器排程時,會先還原其硬體上下文資訊,包括 pc、rax 等等,這樣 fork 返回後,rax 中值為 0,最終賦值給 pid 的值就是 0。

因此,也就可以通過這種判斷 「pid 是否等於 0」 的方式來區分當前程序是父程序還是子程序了。

2.4.侷限性

很多人清楚 fork 可以建立乙個程序的副本並繼續往下執行,可以根據 fork 返回值來執行不同的分支邏輯。如果程序是多執行緒的,在乙個執行緒中呼叫 fork 會複製整個程序嗎?

fork 只能建立呼叫該函式的執行緒的副本,程序中其他執行的執行緒,fork 不予處理。這就意味著,對於多執行緒程式而言,寄希望於通過 fork 來建立乙個完整程序副本是不可行的。

前面我們也提到了,fork 是實現熱重啟的重要一環,fork 這裡的這個侷限性,就制約著不同服務程式設計模型下的熱重啟實現方式。所以我們說具體問題具體分析,不同程式設計模型下實際上可以採用不同的實現方式。

3.單程序單執行緒模型

單程序單執行緒模型,可能很多人一聽覺得它已經被淘汰了,生產環境中不能用,真的麼?強如 redis,不就是單執行緒。強調下並非單執行緒模型沒用,ok,收回來,現在關注下單程序單執行緒模型如何實現熱重啟。

單程序單執行緒,實現熱重啟會比較簡單些:

fork 一下就可以建立出子程序,

子程序可以繼承父程序中的資源,如已經開啟的檔案描述符,包括父程序的 listenfd、connfd,

父程序,可以選擇關閉 listenfd,後續接受連線的任務就交給子程序來完成了,

父程序,甚至也可以關閉 connfd,讓子程序處理連線上的請求、回包等,也可以自身處理完已建立的連線上的請求;

父程序,在合適的時間點選擇退出,子程序開始變成頂梁柱。

核心思想就是這些,但是具體到實現,就有多種方法:

可以選擇 fork 的方式讓子程序拿到原來的 listenfd、connfd,

也可以選擇 unixdomain socket 的方式父程序將 listenfd、connfd 傳送給子程序。

有同學可能會想,我不傳遞這些 fd 行嗎?

比如我開啟了 reuseport,父程序直接處理完已建立連線 connfd 上的請求之後關閉,子程序裡 reuseport.listen 直接建立新的 listenfd。

也可以!但是有些問題必須要提前考慮到:

reuseport 雖然允許多個程序在同乙個埠上多次 listen,似乎滿足了要求,但是要知道只要 euid 相同,都可以在這個埠上 listen!是不安全的!

reuseport 實現和平台有關係,在 linux 平台上在同乙個 address+port 上 listen 多次,多個 listenfd 底層可以共享同乙個連線佇列,核心可以實現負載均衡,但是在 darwin 平台上卻不會!

當然這裡提到的這些問題,在多執行緒模型下肯定也存在。

4.單程序多執行緒模型

前面提到的問題,在多執行緒模型中也會出現:

fork 只能複製 calling thread,not whole process!

reuseport 多次在相同位址+埠 listen 得到的多個 fd,不同平台有不同的表現,可能無法做到接受連線時的 load banlance!

非 reuseport 情況下,多次 listen 會失敗!

不傳遞 fd,直接通過 reuseport 來重新 listen 得到 listenfd,不安全,不同服務程序例項可能會在同乙個埠上監聽,gg!

父程序平滑退出的邏輯,關閉 listenfd,等待 connfd 上請求處理結束,關閉 connfd,一切妥當後,父程序退出,子程序挑大樑!

5. 其他執行緒模型

其他執行緒都基本上避不開上述 3、4 的實現或者組合,對應問題相仿,不再贅述。

6. go 實現熱重啟:觸發時機

需要選擇乙個時機來觸發熱重啟,什麼時候觸發呢?作業系統提供了訊號機制,允許程序做出一些自定義的訊號處理。

殺死乙個程序,一般會通過kill -9傳送 sigkill 訊號給程序,這個訊號不允許捕獲,sigabort 也不允許捕獲,這樣可以允許程序所有者或者高許可權使用者控制程序生死,達到更好的管理效果。

kill 也可以用來傳送其他訊號給程序,如傳送 sigusr1、sigusr2、sigint 等等,程序中可以接收這些訊號,並針對性的做出處理。這裡可以選擇 sigusr1 或者 sigusr2 來通知程序熱重啟

exec go 重啟 如何用 Go 實現熱重啟

熱重啟 熱重啟 zero downtime 指新老程序無縫切換,在替換過程中可保持對 client 的服務。原理父程序監聽重啟訊號 在收到重啟訊號後,父程序呼叫 fork 同時傳遞 socket 描述符給子程序 子程序接收並監聽父程序傳遞的 socket 描述符 在子程序啟動成功之後,父程序停止接收...

熱部署 Springboot實現熱部署詳細講解

本文主要介紹springboot如何實現熱部署。熱部署就是當應用程式正在執行的時候公升級軟體或修改某一部分 配置檔案時,無需重新啟動應用,即可使公升級的軟體和修改後的 配置檔案生效。使用兩個classloader,乙個classloader載入那些不會改變的類 第三方jar包 另乙個classloa...

Go語言的詳細介紹 logo和版本

go語言的logo就是很簡潔的go兩個字母。go之所以叫go,是想表達這門語言的執行速度 開發速度 學習速度 develop 都像gopher一樣快。gopher是一種生活在加拿大的小動物,go的吉祥物就是這個小動物,它的中文名叫做囊地鼠,他們最大的特點就是挖洞速度特別快,當然可能不止是挖洞啦。go...