主頁 > 知識(shí)庫 > Go 實(shí)現(xiàn)熱重啟的詳細(xì)介紹

Go 實(shí)現(xiàn)熱重啟的詳細(xì)介紹

熱門標(biāo)簽:賺地圖標(biāo)注的錢犯法嗎 地圖標(biāo)注測(cè)試 廣東語音外呼系統(tǒng)供應(yīng)商 濮陽自動(dòng)外呼系統(tǒng)代理 烏魯木齊人工電銷機(jī)器人系統(tǒng) 澳門防封電銷卡 智能電銷機(jī)器人營(yíng)銷 長(zhǎng)沙ai機(jī)器人電銷 福州鐵通自動(dòng)外呼系統(tǒng)

最近在優(yōu)化公司框架 trpc 時(shí)發(fā)現(xiàn)了一個(gè)熱重啟相關(guān)的問題,優(yōu)化之余也總結(jié)沉淀下,對(duì) go 如何實(shí)現(xiàn)熱重啟這方面的內(nèi)容做一個(gè)簡(jiǎn)單的梳理。

1.什么是熱重啟?

熱重啟(Hot Restart),是一項(xiàng)保證服務(wù)可用性的手段。它允許服務(wù)重啟期間,不中斷已經(jīng)建立的連接,老服務(wù)進(jìn)程不再接受新連接請(qǐng)求,新連接請(qǐng)求將在新服務(wù)進(jìn)程中受理。對(duì)于原服務(wù)進(jìn)程中已經(jīng)建立的連接,也可以將其設(shè)為讀關(guān)閉,等待平滑處理完連接上的請(qǐng)求及連接空閑后再行退出。通過這種方式,可以保證已建立的連接不中斷,連接上的事務(wù)(請(qǐng)求、處理、響應(yīng))可以正常完成,新的服務(wù)進(jìn)程也可以正常接受連接、處理連接上的請(qǐng)求。當(dāng)然,熱重啟期間進(jìn)程平滑退出涉及到的不止是連接上的事務(wù),也有消息服務(wù)、自定義事務(wù)需要關(guān)注。

這是我理解的熱重啟的一個(gè)大致描述。熱重啟現(xiàn)在還有沒有存在的必要?我的理解是看場(chǎng)景。

以后臺(tái)開發(fā)為例,假如運(yùn)維平臺(tái)有能力在服務(wù)升級(jí)、重啟時(shí)自動(dòng)踢掉流量,服務(wù)就緒后又自動(dòng)加回流量,假如能夠合理預(yù)估服務(wù) QPS、請(qǐng)求處理時(shí)長(zhǎng),那么只要配置一個(gè)合理的停止前等待時(shí)間,是可以達(dá)到類似熱重啟的效果的。這樣的話,在后臺(tái)服務(wù)里面支持熱重啟就顯得沒什么必要。但是,如果我們開發(fā)一個(gè)微服務(wù)框架,不能對(duì)將來的部署平臺(tái)、環(huán)境做這種假設(shè),也有可能使用方只是部署在一兩臺(tái)物理機(jī)上,也沒有其他的負(fù)載均衡設(shè)施,但不希望因?yàn)橹貑⑹芨蓴_,熱重啟就很有必要。當(dāng)然還有一些更復(fù)雜、要求更苛刻的場(chǎng)景,也需要熱重啟的能力。

熱重啟是比較重要的一項(xiàng)保證服務(wù)質(zhì)量的手段,還是值得了解下的,這也是本文介紹的初衷。

2.如何實(shí)現(xiàn)熱重啟?

如何實(shí)現(xiàn)熱重啟,這里其實(shí)不能一概而論,要結(jié)合實(shí)際的場(chǎng)景來看(比如服務(wù)編程模型、對(duì)可用性要求的高低等)。大致的實(shí)現(xiàn)思路,可以先拋一下。

一般要實(shí)現(xiàn)熱重啟,大致要包括如下步驟:

  • 首先,要讓老進(jìn)程,這里稱之為父進(jìn)程了,先要 fork 出一個(gè)子進(jìn)程來代替它工作;
  • 然后,子進(jìn)程就緒之后,通知父進(jìn)程,正常接受新連接請(qǐng)求、處理連接上收到的請(qǐng)求;
  • 再然后,父進(jìn)程處理完已建立連接上的請(qǐng)求后、連接空閑后,平滑退出。

聽上去是挺簡(jiǎn)單的...

2.1.認(rèn)識(shí) fork

大家都知道fork()系統(tǒng)調(diào)用,父進(jìn)程調(diào)用 fork 會(huì)創(chuàng)建一個(gè)進(jìn)程副本,代碼中還可以通過 fork 返回值是否為 0 來區(qū)分是子進(jìn)程還是父進(jìn)程。

int main(char **argv, int argc) {
 pid_t pid = fork();
 if (pid == 0) {
 printf("i am child process");
 } else {
 printf("i am parent process, i have a child process named %d", pid);
 }
}

可能有些開發(fā)人員不知道 fork 的實(shí)現(xiàn)原理,或者不知道 fork 返回值為什么在父子進(jìn)程中不同,或者不知道如何做到父子進(jìn)程中返回值不同……了解這些是要有點(diǎn)知識(shí)積累的。

2.2.返回值

簡(jiǎn)單概括下,ABI 定義了進(jìn)行函數(shù)調(diào)用時(shí)的一些規(guī)范,如何傳遞參數(shù),如何返回值等等,以 x86 為例,如果返回值是 rax 寄存器能夠容的一般都是通過 rax 寄存器返回的。

如果 rax 寄存器位寬無法容納下的返回值呢?也簡(jiǎn)單,編譯器會(huì)安插些指令來完成這些神秘的操作,具體是什么指令,就跟語言編譯器實(shí)現(xiàn)相關(guān)了。

  • c 語言,可能會(huì)將返回值的地址,傳遞到 rdi 或其他寄存器,被調(diào)函數(shù)內(nèi)部呢,通過多條指令將返回值寫入 rdi 代指的內(nèi)存區(qū);
  • c 語言,也可能在被調(diào)函數(shù)內(nèi)部,用多個(gè)寄存器 rax,rdx...一起暫存返回結(jié)果,函數(shù)返回時(shí)再將多個(gè)寄存器的值賦值到變量中;
  • 也可能會(huì)像 golang 這樣,通過棧內(nèi)存來返回;

2.3.fork 返回值

fork 系統(tǒng)調(diào)用的返回值,有點(diǎn)特殊,在父進(jìn)程和子進(jìn)程中,這個(gè)函數(shù)返回的值是不同的,如何做到的呢?

聯(lián)想下父進(jìn)程調(diào)用 fork 的時(shí)候,操作系統(tǒng)內(nèi)核需要干些什么呢?分配進(jìn)程控制塊、分配 pid、分配內(nèi)存空間……肯定有很多東西啦,這里注意下進(jìn)程的硬件上下文信息,這些是非常重要的,在進(jìn)程被調(diào)度算法選中進(jìn)行調(diào)度時(shí),是需要還原硬件上下文信息的。

Linux fork 的時(shí)候,會(huì)對(duì)子進(jìn)程的硬件上下文進(jìn)行一定的修改,我就是讓你 fork 之后拿到的 pid 是 0,怎么辦呢?前面 2.2 節(jié)提過了,對(duì)于那些小整數(shù),rax 寄存器存下綽綽有余,fork 返回時(shí)就是將操作系統(tǒng)分配的 pid 放到 rax 寄存器的。

那,對(duì)于子進(jìn)程而言,我只要在 fork 的時(shí)候?qū)⑺挠布舷挛?rax 寄存器清 0,然后等其他設(shè)置全 ok 后,再將其狀態(tài)從不可中斷等待狀態(tài)修改為可運(yùn)行狀態(tài),等其被調(diào)度器調(diào)度時(shí),會(huì)先還原其硬件上下文信息,包括 PC、rax 等等,這樣 fork 返回后,rax 中值為 0,最終賦值給 pid 的值就是 0。

因此,也就可以通過這種判斷 “pid 是否等于 0” 的方式來區(qū)分當(dāng)前進(jìn)程是父進(jìn)程還是子進(jìn)程了。

2.4.局限性

很多人清楚 fork 可以創(chuàng)建一個(gè)進(jìn)程的副本并繼續(xù)往下執(zhí)行,可以根據(jù) fork 返回值來執(zhí)行不同的分支邏輯。如果進(jìn)程是多線程的,在一個(gè)線程中調(diào)用 fork 會(huì)復(fù)制整個(gè)進(jìn)程嗎?

fork 只能創(chuàng)建調(diào)用該函數(shù)的線程的副本,進(jìn)程中其他運(yùn)行的線程,fork 不予處理。這就意味著,對(duì)于多線程程序而言,寄希望于通過 fork 來創(chuàng)建一個(gè)完整進(jìn)程副本是不可行的。

前面我們也提到了,fork 是實(shí)現(xiàn)熱重啟的重要一環(huán),fork 這里的這個(gè)局限性,就制約著不同服務(wù)編程模型下的熱重啟實(shí)現(xiàn)方式。所以我們說具體問題具體分析,不同編程模型下實(shí)際上可以采用不同的實(shí)現(xiàn)方式。

3.單進(jìn)程單線程模型

單進(jìn)程單線程模型,可能很多人一聽覺得它已經(jīng)被淘汰了,生產(chǎn)環(huán)境中不能用,真的么?強(qiáng)如 redis,不就是單線程。強(qiáng)調(diào)下并非單線程模型沒用,ok,收回來,現(xiàn)在關(guān)注下單進(jìn)程單線程模型如何實(shí)現(xiàn)熱重啟。

單進(jìn)程單線程,實(shí)現(xiàn)熱重啟會(huì)比較簡(jiǎn)單些:

  • fork 一下就可以創(chuàng)建出子進(jìn)程,
  • 子進(jìn)程可以繼承父進(jìn)程中的資源,如已經(jīng)打開的文件描述符,包括父進(jìn)程的 listenfd、connfd,
  • 父進(jìn)程,可以選擇關(guān)閉 listenfd,后續(xù)接受連接的任務(wù)就交給子進(jìn)程來完成了,
  • 父進(jìn)程,甚至也可以關(guān)閉 connfd,讓子進(jìn)程處理連接上的請(qǐng)求、回包等,也可以自身處理完已建立的連接上的請(qǐng)求;
  • 父進(jìn)程,在合適的時(shí)間點(diǎn)選擇退出,子進(jìn)程開始變成頂梁柱。

核心思想就是這些,但是具體到實(shí)現(xiàn),就有多種方法:

  • 可以選擇 fork 的方式讓子進(jìn)程拿到原來的 listenfd、connfd,
  • 也可以選擇 unixdomain socket 的方式父進(jìn)程將 listenfd、connfd 發(fā)送給子進(jìn)程。

有同學(xué)可能會(huì)想,我不傳遞這些 fd 行嗎?

  • 比如我開啟了 reuseport,父進(jìn)程直接處理完已建立連接 connfd 上的請(qǐng)求之后關(guān)閉,子進(jìn)程里 reuseport.Listen 直接創(chuàng)建新的 listenfd。

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

  • reuseport 雖然允許多個(gè)進(jìn)程在同一個(gè)端口上多次 listen,似乎滿足了要求,但是要知道只要 euid 相同,都可以在這個(gè)端口上 listen!是不安全的!
  • reuseport 實(shí)現(xiàn)和平臺(tái)有關(guān)系,在 Linux 平臺(tái)上在同一個(gè) address+port 上 listen 多次,多個(gè) listenfd 底層可以共享同一個(gè)連接隊(duì)列,內(nèi)核可以實(shí)現(xiàn)負(fù)載均衡,但是在 darwin 平臺(tái)上卻不會(huì)!

當(dāng)然這里提到的這些問題,在多線程模型下肯定也存在。

4.單進(jìn)程多線程模型

前面提到的問題,在多線程模型中也會(huì)出現(xiàn):

  • fork 只能復(fù)制 calling thread,not whole process!
  • reuseport 多次在相同地址+端口 listen 得到的多個(gè) fd,不同平臺(tái)有不同的表現(xiàn),可能無法做到接受連接時(shí)的 load banlance!
  • 非 reuseport 情況下,多次 listen 會(huì)失?。?/li>
  • 不傳遞 fd,直接通過 reuseport 來重新 listen 得到 listenfd,不安全,不同服務(wù)進(jìn)程實(shí)例可能會(huì)在同一個(gè)端口上監(jiān)聽,gg!
  • 父進(jìn)程平滑退出的邏輯,關(guān)閉 listenfd,等待 connfd 上請(qǐng)求處理結(jié)束,關(guān)閉 connfd,一切妥當(dāng)后,父進(jìn)程退出,子進(jìn)程挑大梁!

5. 其他線程模型

其他線程都基本上避不開上述 3、4 的實(shí)現(xiàn)或者組合,對(duì)應(yīng)問題相仿,不再贅述。

6. go 實(shí)現(xiàn)熱重啟:觸發(fā)時(shí)機(jī)

需要選擇一個(gè)時(shí)機(jī)來觸發(fā)熱重啟,什么時(shí)候觸發(fā)呢?操作系統(tǒng)提供了信號(hào)機(jī)制,允許進(jìn)程做出一些自定義的信號(hào)處理。

殺死一個(gè)進(jìn)程,一般會(huì)通過kill -9發(fā)送 SIGKILL 信號(hào)給進(jìn)程,這個(gè)信號(hào)不允許捕獲,SIGABORT 也不允許捕獲,這樣可以允許進(jìn)程所有者或者高權(quán)限用戶控制進(jìn)程生死,達(dá)到更好的管理效果。

kill 也可以用來發(fā)送其他信號(hào)給進(jìn)程,如發(fā)送 SIGUSR1、SIGUSR2、SIGINT 等等,進(jìn)程中可以接收這些信號(hào),并針對(duì)性的做出處理。這里可以選擇 SIGUSR1 或者 SIGUSR2 來通知進(jìn)程熱重啟。

go func() {
 ch := make(chan os.Signal, 1)
 signal.Notify(ch, os.SIGUSR2)
 - ch

 //接下來就可以做熱重啟相關(guān)的邏輯了
 ...
}()

7. 如何判斷熱重啟

那一個(gè) go 程序重新啟動(dòng)之后,所有運(yùn)行時(shí)狀態(tài)信息都是新的,那如何區(qū)分自己是否是子進(jìn)程呢,或者說我是否要執(zhí)行熱重啟邏輯呢?父進(jìn)程可以通過設(shè)置子進(jìn)程初始化時(shí)的環(huán)境變量,比如加個(gè) HOT_RESTART=1。

這就要求代碼中在合適的地方要先檢測(cè)環(huán)境變量 HOT_RESTART 是否為 1,如果成立,那就執(zhí)行熱重啟邏輯,否則就執(zhí)行全新的啟動(dòng)邏輯。

8. ForkExec

假如當(dāng)前進(jìn)程收到 SIGUSR2 信號(hào)之后,希望執(zhí)行熱重啟邏輯,那么好,需要先執(zhí)行 syscall.ForkExec(...)來創(chuàng)建一個(gè)子進(jìn)程,注意 go 不同于 cc++,它本身就是依賴多線程來調(diào)度協(xié)程的,天然就是多線程程序,只不過是他沒有使用 NPTL 線程庫來創(chuàng)建,而是通過 clone 系統(tǒng)調(diào)用來創(chuàng)建。

前面提過了,如果單純 fork 的話,只能復(fù)制調(diào)用 fork 函數(shù)的線程,對(duì)于進(jìn)程中的其他線程無能為力,所以對(duì)于 go 這種天然的多線程程序,必須從頭來一遍,再 exec 一下。所以 go 標(biāo)準(zhǔn)庫提供的函數(shù)是 syscall.ForkExec 而不是 syscall.Fork。

9. go 實(shí)現(xiàn)熱重啟: 傳遞 listenfd

go 里面?zhèn)鬟f fd 的方式,有這么幾種,父進(jìn)程 fork 子進(jìn)程的時(shí)候傳遞 fd,或者后面通過 unix domain socket 傳遞。需要注意的是,我們傳遞的實(shí)際上是 file description,而非 file descriptor。

附上一張類 unix 系統(tǒng)下 file descriptor、file description、inode 三者之間的關(guān)系圖:

fd 分配都是從小到大分配的,父進(jìn)程中的 fd 為 10,傳遞到子進(jìn)程中之后有可能就不是 10。那么傳遞到子進(jìn)程的 fd 是否是可以預(yù)測(cè)的呢?可以預(yù)測(cè),但是不建議。所以我提供了兩種實(shí)現(xiàn)方式。

9.1 ForkExec+ProcAttr{Files: []uintptr{}}

要傳遞一個(gè) listenfd 很簡(jiǎn)單,假如是類型 net.Listener,那就通過tcpln := ln.(*net.TCPListener); file, _ := tcpln.File(); fd := file.FD()來拿到 listener 底層 file description 對(duì)應(yīng)的 fd。

需要注意的是,這里的 fd 并非底層的 file description 對(duì)應(yīng)的初始 fd,而是被 dup2 復(fù)制出來的一個(gè) fd(調(diào)用 tcpln.File()的時(shí)候就已經(jīng)分配了),這樣底層 file description 引用計(jì)數(shù)就會(huì)+1。如果后面想通過 ln.Close()關(guān)閉監(jiān)聽套接字的話,sorry,關(guān)不掉。這里需要顯示的執(zhí)行 file.Close() 將新創(chuàng)建的 fd 關(guān)掉,使對(duì)應(yīng)的 file description 引用計(jì)數(shù)-1,保證 Close 的時(shí)候引用計(jì)數(shù)為 0,才可以正常關(guān)閉。

試想下,我們想實(shí)現(xiàn)熱重啟,是一定要等連接上接收的請(qǐng)求處理完才可以退出進(jìn)程的,但是這期間父進(jìn)程不能再接收新的連接請(qǐng)求,如果這里不能正常關(guān)閉 listener,那我們這個(gè)目標(biāo)就無法實(shí)現(xiàn)。所以這里對(duì) dup 出來的 fd 的處理要慎重些,不要遺忘。

OK,接下來說下 syscall.ProcAttr{Files: []uintptr{}},這里就是要傳遞的父進(jìn)程中的 fd,比如要傳遞 stdin、stdout、stderr 給子進(jìn)程,就需要將這幾個(gè)對(duì)應(yīng)的 fd 塞進(jìn)去 os.Stdin.FD(), os.Stdout.FD(), os.Stderr.FD(),如果要想傳遞剛才的 listenfd,就需要將上面的file.FD()返回的 fd 塞進(jìn)去。

子進(jìn)程中接收到這些 fd 之后,在類 unix 系統(tǒng)下一般會(huì)按照從 0、1、2、3 這樣遞增的順序來分配 fd,那么傳遞過去的 fd 是可以預(yù)測(cè)的,假如除了 stdin, stdout, stderr 再傳兩個(gè) listenfd,那么可以預(yù)測(cè)這兩個(gè)的 fd 應(yīng)該是 3,4。在類 unix 系統(tǒng)下一般都是這么處理的,子進(jìn)程中就可以根據(jù)傳遞 fd 的數(shù)量(比如通過環(huán)境變量傳遞給子進(jìn)程 FD_NUM=2),來從 3 開始計(jì)算,哦,這兩個(gè) fd 應(yīng)該是 3,4。

父子進(jìn)程可以通過一個(gè)約定的順序,來組織傳遞的 listenfd 的順序,以方便子進(jìn)程中按相同的約定進(jìn)行處理,當(dāng)然也可以通過 fd 重建 listener 之后來判斷對(duì)應(yīng)的監(jiān)聽 network+address,以區(qū)分該 listener 對(duì)應(yīng)的是哪一個(gè)邏輯 service。都是可以的!

需要注意的是,file.FD()返回的 fd 是非阻塞的,會(huì)影響到底層的 file description,在重建 listener 先將其設(shè)為 nonblock, syscall.SetNonBlock(fd),然后file, _ := os.NewFile(fd); tcplistener := net.FileListener(file),或者是udpconn := net.PacketConn(file),然后可以獲取 tcplistener、udpconn 的監(jiān)聽地址,來關(guān)聯(lián)其對(duì)應(yīng)的邏輯 service。

前面提到 file.FD()會(huì)將底層的 file description 設(shè)置為阻塞模式,這里再補(bǔ)充下,net.FileListener(f), net.PacketConn(f)內(nèi)部會(huì)調(diào)用 newFileFd()->dupSocket(),這幾個(gè)函數(shù)內(nèi)部會(huì)將 fd 對(duì)應(yīng)的 file description 重新設(shè)置為非阻塞。父子進(jìn)程中共享了 listener 對(duì)應(yīng)的 file description,所以不需要顯示設(shè)置為非阻塞。

有些微服務(wù)框架是支持對(duì)服務(wù)進(jìn)行邏輯 service 分組的,google pb 規(guī)范中也支持多 service 定義,這個(gè)在騰訊的 goneat、trpc 框架中也是有支持的。

當(dāng)然了,這里我不會(huì)寫一個(gè)完整的包含上述所有描述的 demo 給大家,這有點(diǎn)占篇幅,這里只貼一個(gè)精簡(jiǎn)版的實(shí)例,其他的讀者感興趣可以自己編碼測(cè)試。須知紙上得來終覺淺,還是要多實(shí)踐。

package main

import (
 "fmt"
 "io/ioutil"
 "log"
 "net"
 "os"
 "strconv"
 "sync"
 "syscall"
 "time"
)

const envRestart = "RESTART"
const envListenFD = "LISTENFD"

func main() {

 v := os.Getenv(envRestart)

 if v != "1" {

 ln, err := net.Listen("tcp", "localhost:8888")
 if err != nil {
 panic(err)
 }

 wg := sync.WaitGroup{}
 wg.Add(1)
 go func() {
 defer wg.Done()
 for {
 ln.Accept()
 }
 }()

 tcpln := ln.(*net.TCPListener)
 f, err := tcpln.File()
 if err != nil {
 panic(err)
 }

 os.Setenv(envRestart, "1")
 os.Setenv(envListenFD, fmt.Sprintf("%d", f.Fd()))

 _, err = syscall.ForkExec(os.Args[0], os.Args, syscall.ProcAttr{
 Env: os.Environ(),
 Files: []uintptr{os.Stdin.Fd(), os.Stdout.Fd(), os.Stderr.Fd(), f.Fd()},
 Sys: nil,
 })
 if err != nil {
 panic(err)
 }
 log.Print("parent pid:", os.Getpid(), ", pass fd:", f.Fd())
 f.Close()
 wg.Wait()

 } else {

 v := os.Getenv(envListenFD)
 fd, err := strconv.ParseInt(v, 10, 64)
 if err != nil {
 panic(err)
 }
 log.Print("child pid:", os.Getpid(), ", recv fd:", fd)

 // case1: 理解上面提及的file descriptor、file description的關(guān)系
 // 這里子進(jìn)程繼承了父進(jìn)程中傳遞過來的一些fd,但是fd數(shù)值與父進(jìn)程中可能是不同的
 // 取消注釋來測(cè)試...
 //ff := os.NewFile(uintptr(fd), "")
 //if ff != nil {
 // _, err := ff.Stat()
 // if err != nil {
 // log.Println(err)
 // }
 //}

 // case2: 假定父進(jìn)程中共享了fd 0\1\2\listenfd給子進(jìn)程,那再子進(jìn)程中可以預(yù)測(cè)到listenfd=3
 ff := os.NewFile(uintptr(3), "")
 fmt.Println("fd:", ff.Fd())
 if ff != nil {
 _, err := ff.Stat()
 if err != nil {
 panic(err)
 }

 // 這里pause, 運(yùn)行命令lsof -P -p $pid,檢查下有沒有l(wèi)istenfd傳過來,除了0,1,2,應(yīng)該有看到3
 // ctrl+d to continue
 ioutil.ReadAll(os.Stdin)

 fmt.Println("....")
 _, err = net.FileListener(ff)
 if err != nil {
 panic(err)
 }

 // 這里pause, 運(yùn)行命令lsof -P -p $pid, 會(huì)發(fā)現(xiàn)有兩個(gè)listenfd,
 // 因?yàn)榍懊嬲{(diào)用了ff.FD() dup2了一個(gè),如果這里不顯示關(guān)閉,listener將無法關(guān)閉
 ff.Close()

 time.Sleep(time.Minute)
 }

 time.Sleep(time.Minute)
 }
}

這里用簡(jiǎn)單的代碼大致解釋了如何用 ProcAttr 來傳遞 listenfd。這里有個(gè)問題,假如后續(xù)父進(jìn)程中傳遞的 fd 修改了呢,比如不傳 stdin, stdout, stderr 的 fd 了,怎么辦?服務(wù)端是不是要開始預(yù)測(cè)應(yīng)該從 0 開始編號(hào)了?我們可以通過環(huán)境變量通知子進(jìn)程,比如傳遞的 fd 從哪個(gè)編號(hào)開始是 listenfd,一共有幾個(gè) listenfd,這樣也是可以實(shí)現(xiàn)的。

這種實(shí)現(xiàn)方式可以跨平臺(tái)。

感興趣的話,可以看下 facebook 提供的這個(gè)實(shí)現(xiàn)grace。

9.2 unix domain socket + cmsg

另一種,思路就是通過 unix domain socket + cmsg 來傳遞,父進(jìn)程啟動(dòng)的時(shí)候依然是通過 ForkExec 來創(chuàng)建子進(jìn)程,但是并不通過 ProcAttr 來傳遞 listenfd。

父進(jìn)程在創(chuàng)建子進(jìn)程之前,創(chuàng)建一個(gè) unix domain socket 并監(jiān)聽,等子進(jìn)程啟動(dòng)之后,建立到這個(gè) unix domain socket 的連接,父進(jìn)程此時(shí)開始將 listenfd 通過 cmsg 發(fā)送給子進(jìn)程,獲取 fd 的方式與 9.1 相同,該注意的 fd 關(guān)閉問題也是一樣的處理。

子進(jìn)程連接上 unix domain socket,開始接收 cmsg,內(nèi)核幫子進(jìn)程收消息的時(shí)候,發(fā)現(xiàn)里面有一個(gè)父進(jìn)程的 fd,內(nèi)核找到對(duì)應(yīng)的 file description,并為子進(jìn)程分配一個(gè) fd,將兩者建立起映射關(guān)系。然后回到子進(jìn)程中的時(shí)候,子進(jìn)程拿到的就是對(duì)應(yīng)該 file description 的 fd 了。通過 os.NewFile(fd)就可以拿到 file,然后再通過 net.FileListener 或者 net.PacketConn 就可以拿到 tcplistener 或者 udpconn。

剩下的獲取監(jiān)聽地址,關(guān)聯(lián)邏輯 service 的動(dòng)作,就與 9.1 小結(jié)描述的一致了。

這里我也提供一個(gè)可運(yùn)行的精簡(jiǎn)版的 demo,供大家了解、測(cè)試用。

package main

import (
 "fmt"
 "io/ioutil"
 "log"
 "net"
 "os"
 "strconv"
 "sync"
 "syscall"
 "time"

 passfd "github.com/ftrvxmtrx/fd"
)

const envRestart = "RESTART"
const envListenFD = "LISTENFD"
const unixsockname = "/tmp/xxxxxxxxxxxxxxxxx.sock"

func main() {

 v := os.Getenv(envRestart)

 if v != "1" {

 ln, err := net.Listen("tcp", "localhost:8888")
 if err != nil {
 panic(err)
 }

 wg := sync.WaitGroup{}
 wg.Add(1)
 go func() {
 defer wg.Done()
 for {
 ln.Accept()
 }
 }()

 tcpln := ln.(*net.TCPListener)
 f, err := tcpln.File()
 if err != nil {
 panic(err)
 }

 os.Setenv(envRestart, "1")
 os.Setenv(envListenFD, fmt.Sprintf("%d", f.Fd()))

 _, err = syscall.ForkExec(os.Args[0], os.Args, syscall.ProcAttr{
 Env: os.Environ(),
 Files: []uintptr{os.Stdin.Fd(), os.Stdout.Fd(), os.Stderr.Fd(), /*f.Fd()*/}, // comment this when test unixsock
 Sys: nil,
 })
 if err != nil {
 panic(err)
 }
 log.Print("parent pid:", os.Getpid(), ", pass fd:", f.Fd())

 os.Remove(unixsockname)
 unix, err := net.Listen("unix", unixsockname)
 if err != nil {
 panic(err)
 }
 unixconn, err := unix.Accept()
 if err != nil {
 panic(err)
 }
 err = passfd.Put(unixconn.(*net.UnixConn), f)
 if err != nil {
 panic(err)
 }

 f.Close()
 wg.Wait()

 } else {

 v := os.Getenv(envListenFD)
 fd, err := strconv.ParseInt(v, 10, 64)
 if err != nil {
 panic(err)
 }
 log.Print("child pid:", os.Getpid(), ", recv fd:", fd)

 // case1: 有同學(xué)認(rèn)為以通過環(huán)境變量傳fd,通過環(huán)境變量肯定是不行的,fd根本不對(duì)應(yīng)子進(jìn)程中的fd
 //ff := os.NewFile(uintptr(fd), "")
 //if ff != nil {
 // _, err := ff.Stat()
 // if err != nil {
 // log.Println(err)
 // }
 //}

 // case2: 如果只有一個(gè)listenfd的情況下,那如果fork子進(jìn)程時(shí)保證只傳0\1\2\listenfd,那子進(jìn)程中l(wèi)istenfd一定是3
 //ff := os.NewFile(uintptr(3), "")
 //if ff != nil {
 // _, err := ff.Stat()
 // if err != nil {
 // panic(err)
 // }
 // // pause, ctrl+d to continue
 // ioutil.ReadAll(os.Stdin)
 // fmt.Println("....")
 // _, err = net.FileListener(ff) //會(huì)dup一個(gè)fd出來,有多個(gè)listener
 // if err != nil {
 // panic(err)
 // }
 // // lsof -P -p $pid, 會(huì)發(fā)現(xiàn)有兩個(gè)listenfd
 // time.Sleep(time.Minute)
 //}
 // 這里我們暫停下,方便運(yùn)行系統(tǒng)命令來查看進(jìn)程當(dāng)前的一些狀態(tài)
 // run: lsof -P -p $pid,檢查下listenfd情況

 ioutil.ReadAll(os.Stdin)
 fmt.Println(".....")

 unixconn, err := net.Dial("unix", unixsockname)
 if err != nil {
 panic(err)
 }

 files, err := passfd.Get(unixconn.(*net.UnixConn), 1, nil)
 if err != nil {
 panic(err)
 }

 // 這里再運(yùn)行命令:lsof -P -p $pid再檢查下listenfd情況

 f := files[0]
 f.Stat()

 time.Sleep(time.Minute)
 }
}

這種實(shí)現(xiàn)方式,僅限類 unix 系統(tǒng)。

如果有服務(wù)混布的情況存在,需要考慮下使用的 unix domain socket 的文件名,避免因?yàn)橹孛鸬膯栴},可以考慮通過”進(jìn)程名.pid“來作為 unix domain socket 的名字,并通過環(huán)境變量將其傳遞給子進(jìn)程。

10. go 實(shí)現(xiàn)熱重啟: 子進(jìn)程如何通過 listenfd 重建 listener

前面已經(jīng)提過了,當(dāng)拿到 fd 之后還不知道它對(duì)應(yīng)的是 tcp 的 listener,還是 udpconn,那怎么辦?都試下唄。

file, err := os.NewFile(fd)
// check error

tcpln, err := net.FileListener(file)
// check error

udpconn, err := net.PacketConn(file)
// check error

11. go 實(shí)現(xiàn)熱重啟:父進(jìn)程平滑退出

父進(jìn)程如何平滑退出呢,這個(gè)要看父進(jìn)程中都有哪些邏輯要平滑停止了。

11.1. 處理已建立連接上請(qǐng)求

可以從這兩個(gè)方面入手:

  • shutdown read,不再接受新的請(qǐng)求,對(duì)端繼續(xù)寫數(shù)據(jù)的時(shí)候會(huì)感知到失敗;
  • 繼續(xù)處理連接上已經(jīng)正常接收的請(qǐng)求,處理完成后,回包,close 連接;

也可以考慮,不進(jìn)行讀端關(guān)閉,而是等連接空閑一段時(shí)間后再 close,是否盡快關(guān)閉更符合要求就要結(jié)合場(chǎng)景、要求來看。

如果對(duì)可用性要求比較苛刻,可能也會(huì)需要考慮將 connfd、connfd 上已經(jīng)讀取寫入的 buffer 數(shù)據(jù)也一并傳遞給子進(jìn)程處理。

11.2. 消息服務(wù)

  • 確認(rèn)下自己服務(wù)的消息消費(fèi)、確認(rèn)機(jī)制是否合理
  • 不再收新消息
  • 處理完已收到的消息后,再退出

11.3. 自定義 AtExit 清理任務(wù)

有些任務(wù)會(huì)有些自定義任務(wù),希望進(jìn)程在退出之前,能夠執(zhí)行到,這種可以提供一個(gè)類似 AtExit 的注冊(cè)函數(shù),讓進(jìn)程退出之前能夠執(zhí)行業(yè)務(wù)自定義的清理邏輯。

不管是平滑重啟,還是其他正常退出,對(duì)該支持都是有一定需求的。

12. 其他

有些場(chǎng)景下也希望傳遞 connfd,包括 connfd 上對(duì)應(yīng)的讀寫的數(shù)據(jù)。

比如連接復(fù)用的場(chǎng)景,客戶端可能會(huì)通過同一個(gè)連接發(fā)送多個(gè)請(qǐng)求,假如在中間某個(gè)時(shí)刻服務(wù)端執(zhí)行熱重啟操作,服務(wù)端如果直接連接讀關(guān)閉會(huì)導(dǎo)致后續(xù)客戶端的數(shù)據(jù)發(fā)送失敗,客戶端關(guān)閉連接則可能導(dǎo)致之前已經(jīng)接收的請(qǐng)求也無法正常響應(yīng)。這種情況下,可以考慮服務(wù)端繼續(xù)處理連接上請(qǐng)求,等連接空閑再關(guān)閉。會(huì)不會(huì)一直不空閑呢?有可能。

其實(shí)服務(wù)端不能預(yù)測(cè)客戶端是否會(huì)采用連接復(fù)用模式,選擇一個(gè)更可靠的處理方式會(huì)更好些,如果場(chǎng)景要求比較苛刻,并不希望通過上層重試來解決的話。這種可以考慮將 connfd 以及 connfd 上讀寫的 buffer 數(shù)據(jù)一并傳遞給子進(jìn)程,交由子進(jìn)程來處理,這個(gè)時(shí)候需要關(guān)注的點(diǎn)更多,處理起來更復(fù)雜,感興趣的可以參考下 mosn 的實(shí)現(xiàn)。

13. 總結(jié)

熱重啟作為一種保證服務(wù)平滑重啟、升級(jí)的實(shí)現(xiàn)方式,在今天看來依然非常有價(jià)值。本文描述了實(shí)現(xiàn)熱重啟的一些大致思路,并且通過 demo 循序漸進(jìn)地描述了在 go 服務(wù)中如何予以實(shí)現(xiàn)。雖然沒有提供一個(gè)完整的熱重啟實(shí)例給大家,但是相信大家讀完之后應(yīng)該已經(jīng)可以親手實(shí)現(xiàn)了。

由于作者本人水平有限,難免會(huì)有描述疏漏之處,歡迎大家指正。

參考文章

Unix 高級(jí)編程:進(jìn)程間通信,Steven Richards

mosn 啟動(dòng)流程: https://mosn.io/blog/code/mosn-startup/

到此這篇關(guān)于Go 實(shí)現(xiàn)熱重啟的詳細(xì)介紹的文章就介紹到這了,更多相關(guān)go熱重啟內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

您可能感興趣的文章:
  • 詳解如何熱重啟golang服務(wù)器
  • 在Go中構(gòu)建并發(fā)TCP服務(wù)器
  • Go語言的http/2服務(wù)器功能及客戶端使用
  • 解析Go 標(biāo)準(zhǔn)庫 http.FileServer 實(shí)現(xiàn)靜態(tài)文件服務(wù)
  • MongoDB4.0在windows10下的安裝與服務(wù)配置教程詳解
  • goland服務(wù)熱重啟的配置文件

標(biāo)簽:調(diào)研邀請(qǐng) 慶陽 太原 貴陽 阿克蘇 西雙版納 德州 廣西

巨人網(wǎng)絡(luò)通訊聲明:本文標(biāo)題《Go 實(shí)現(xiàn)熱重啟的詳細(xì)介紹》,本文關(guān)鍵詞  實(shí)現(xiàn),熱,重啟,的,詳細(xì),介紹,;如發(fā)現(xiàn)本文內(nèi)容存在版權(quán)問題,煩請(qǐng)?zhí)峁┫嚓P(guān)信息告之我們,我們將及時(shí)溝通與處理。本站內(nèi)容系統(tǒng)采集于網(wǎng)絡(luò),涉及言論、版權(quán)與本站無關(guān)。
  • 相關(guān)文章
  • 下面列出與本文章《Go 實(shí)現(xiàn)熱重啟的詳細(xì)介紹》相關(guān)的同類信息!
  • 本頁收集關(guān)于Go 實(shí)現(xiàn)熱重啟的詳細(xì)介紹的相關(guān)信息資訊供網(wǎng)民參考!
  • 推薦文章