這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
零停機升級幾乎是現代網路服務的標配,其實現原理並不複雜……blablabla……( 從檔案描述符講起,省略一萬字)。現在有人確認 Go 也可實現零停機升級 TCP 服務或者更加簡短的叫法——熱更新。
原文在此:Zero Downtime upgrades of TCP servers in Go
—————-翻譯分隔線—————-
用 Go 實現零停機升級 TCP 服務
最近在 golang-nuts 郵件清單上有篇文章提到 Nginx 可以保持服務的時候進行升級,而無需停止它正在監聽的 socket。秘訣是取消監聽的 socket 上的 close-on-exec,然後 fork 並運行一個新的服務(用升級後的二進位檔案),並用參數告訴它使用繼承的檔案描述符,而不是調用 scoket() 和 listen(s)。
於是我就想試試在 Go 中是否能做到同樣的事情,以及對於標準庫需要做什麼樣的修改來達到這個效果。最終我實現了這個功能,而且只需要很小的修改,接下來會解釋一下是如何做到的。
相關的代碼在這裡。
在這個程式裡有許多有趣的東西,我會逐一介紹。然後,我使用了“介面注入”的模式。這在 Go 中是一個重要的模式,但我不認為這個模式被廣泛接受並編寫進入文檔。
當我開始思考這個問題的時候,我意識到其中的一個問題是需要在 http.(*Server).Serve 內部找到方法,當舊的伺服器正確關閉的時候,讓其停止調用 Accept()。問題是那裡沒有鉤子;唯一的跳出迴圈(“Accept,開啟一個 goroutine 來處理,然後重複這個過程”)的辦法是 Accept 返回一個錯誤。但是如果你認為 Accept 是一個系統調用,你可能會想:“我不能進入其中,並插入一個錯誤”。但是 Accept() 不是一個系統調用:它是 net.Listener 的一個介面。這意味著如果建立一個實現了 net.Listener 的自有對象,就可以將其傳遞給 http.(*Server).Serve 然後在 Accept() 中做想做的事情。
在第一次瞭解結構的內嵌類型時,我非常迷惑並失去了方向。而嘗試的時候,得到了各種混合的指標,並且發生了許多無法解釋的null 指標錯誤。這次,我又重新閱讀代碼,並有了一個大致的概念。當想要注入某個介面的方法時,類型嵌入是必須的。這使得可以繼承全部底層對象的實現,然後根據需要重新定義。參閱在 upgradable.go 中的 stoppableListener。net.Listener 介面需要三個方法,包括 Accept, Close 和 Addr。但是我只定義了其中的一個:Accept()。stoppableListener 為何能實現了 net.Listener 呢?因為另外兩個方法通過嵌入的方式實現了。只有 Accept() 有更加明確的定義。當編寫 Accept() 的時候,我需要明確指出如何同底層對象通訊,以便傳遞 Accept() 的調用。這裡的訣竅就是要理解嵌入類型是如何在結構體中使用類型名建立了一個新的欄位。因此可以通過 stoppableListenersl 的 sl.Listener 來調用其中的 net.Listener,同時可以通過 sl.Listener.Accept() 調用內部的 Accept()。
接下來,考慮如何處理 Serve() 的“停止”錯誤。用 os.Exit(0) 立刻退出是不正確的,因為可能還有 goroutine 正在服務 HTTP 用戶端。需要某種辦法瞭解到所有的用戶端都已經完成。再次利用注入,可以將 Accept() 返回的 net.Conn 進行封裝,然後來檢測當前啟動並執行串連的數量。這個注入 net.Conn 對象的技術還會有一些其他有趣的應用。例如,通過捕獲 Read() 或者 Write() 調用,可以對串連進行強制限速,而無需協議上的任何實現。甚至可以投機取巧地做一些加密之類的蠢事,同樣,無需在協議上進行。
當我確信可以完美的關閉服務之後,就需要瞭解如何在正確的檔案描述符上啟動一個新的服務。這是通過對 net 包的一個相當簡單的改動實現的。參閱補丁。由於我的懶惰,僅僅在 TCPListener 上實現了。理論上,對於使用其他 socket 的服務的零停機升級也是可能的,不過對於 net 包的修改僅僅適用 TCP 服務。Rog Peppe 向我指出了 net.FileListener 對象可以從 *os.File 建立(可以使用 os.NewFile 產生)。
最後一個問題是 net 總是會在其開啟的 socket 檔案描述符設定 close-on-exec 標識。因此需要在監聽的 socket 上關閉它,這樣檔案描述符在新的進程中也可使用。這需要在 syscall 庫上增加一些東西(關閉或不關閉)。
我手工測試了工作正常(在其他視窗用命令列 GET 調用)。同時使用 http_load 測試了負載。在 20 秒的基準負載測試中,得到了 3937 請求/秒的成績;然後再次測試,添加 “GET http://localhost:8000/upgrade”,同時在負載測試的時候執行了若干次二進位檔案的替換,這次得到了 3880 請求/秒的成績!這很酷!