exec go 重启_无停机优雅重启 Go 程序
什么是優(yōu)雅重啟
在不停機的情況下,就地部署一個應(yīng)用程序的新版本或者修改其配置的能力已經(jīng)成為現(xiàn)代軟件系統(tǒng)的標(biāo)配。這篇文章討論優(yōu)雅重啟一個應(yīng)用的不同方法,并且提供一個功能獨立的案例來深挖實現(xiàn)細(xì)節(jié)。如果你不熟悉 Teleport 話,Teleport 是我們使用 Golang 針對彈性架構(gòu)設(shè)計的
SO_REUSEPORT vs 復(fù)制套接字的背景
為了推進 Teleport 高可用的工作,我們最近花了些時間研究如何優(yōu)雅重啟 Teleport 的 TLS 和 SSH 的端口監(jiān)聽器
Marek Majkowski 在他的博客文章你可以在套接字上設(shè)置 SO_REUSEPORT ,從而讓多個進程能夠被綁定到同一個端口上。利用這個方法,你會有多個接受隊列向多個進程提供數(shù)據(jù)。
復(fù)制套接字,并把它以文件的形式傳送給一個子進程,然后在新的進程中重新創(chuàng)建這個套接字。使用這種方法,你將有一個接受隊列向多個進程提供數(shù)據(jù)。]
在我們初期的討論中,我們了解到幾個關(guān)于 SO_REUSEPORT 的問題。我們的一個工程師之前使用這個方法,并且注意到由于其多個接受隊列,有時候會丟棄掛起的 TCP 連接。除此之外,當(dāng)我們進行這些討論的時候,Go 并沒有很好地支持在一個 net.Listener 上設(shè)置 SO_REUSEPORT。然而,在過去的幾天中,在這個問題上有了進展,看起來像
第二種方法也很吸引人,因為它的簡單性以及大多數(shù)開發(fā)人員熟悉的傳統(tǒng)Unix 的 fork/exec 產(chǎn)生模型,即將所有打開文件傳遞給子進程的約定。需要注意的一點,os/exec 包實際上不贊同這種用法。主要是出于安全上的考量,它只傳遞 stdin , stdout 和 stderr 給子進程。然而, os 包確實提供較低級的原語,可用于將文件傳遞給子程序,這就是我們想做的。
使用信號切換套接字進程所有者
在我們看源碼之前,了解一些這個方法如何工作的細(xì)節(jié)是值得的。
啟動一個全新的 Teleport 程序后,該進程會在綁定的端口上創(chuàng)建一個監(jiān)聽套接字接受所有入站流量。對于 Teleport,入口流量就是 LTS 和 SSH 流量。我們添加了一個處理
應(yīng)該注意的是,當(dāng)一個套接字被復(fù)制時,入棧流量會在兩個套接字之間以輪詢的方式進行負(fù)載均衡。如下圖所示,這就意味著有一段時間,兩個 Teleport 進程都會接受新的連接。
父進程的關(guān)閉是相同的事情,但是反過來做。一旦 Teleport 進程接受到 SIGOUIT 信號,他會開始關(guān)閉這個進程,停止接受新的連接,等待所有的現(xiàn)有連接斷開或是超時發(fā)生。一旦入站流量被清空,這個瀕死進程就會關(guān)閉它的監(jiān)聽套接字并且退出。這種情況下,新的進程會接管內(nèi)核發(fā)送過來的所有請求。
優(yōu)雅重啟演練
我們基于上面的方法寫了一個簡單的程序,你可以自己嘗試使用一下。源代碼在文章的最后,你可以按照以下步驟嘗試這個例子。
首先,編譯和啟動程序。
$ go build restart.go
$ ./restart &
[1] 95147
$ Created listener file descriptor for :8080.
$ curl http://localhost:8080/hello
Hello from 95147!
將 USR2 信號發(fā)送給初始進程。現(xiàn)在,當(dāng)你訪問這個 HTTP 入口的時候,他會返回兩個不同的進程的 PID。
$ kill -SIGUSR2 95147
user defined signal 2 signal received.
Forked child 95170.
$ Imported listener file descriptor for :8080.
$ curl http://localhost:8080/hello
Hello from 95170!
$ curl http://localhost:8080/hello
Hello from 95147!
殺死初始進程后,你將只會從新的進程中獲得返回。
$ kill -SIGTERM 95147
signal: killed
[1]+ Exit 1 go run restart.go
$ curl http://localhost:8080/hello
Hello from 95170!
$ curl http://localhost:8080/hello
Hello from 95170!
最后殺死新進程,訪問將會被拒絕。
$ kill -SIGTERM 95170
$ curl http://localhost:8080/hello
curl: (7) Failed to connect to localhost port 8080: Connection refused
總結(jié)和示例源代碼
像你看到,一旦你了解了他是如何工作的,增加優(yōu)雅重啟功能到 Go 寫的服務(wù)中是相當(dāng)簡單的事情,并且有效地提高服務(wù)使用者的用戶體驗。如果你想在 Teleport 中看到這一點,我們邀請你瞧瞧我們的參考
package main
import (
"context"
"encoding/json"
"flag"
"fmt"
"net"
"net/http"
"os"
"os/signal"
"path/filepath"
"syscall"
"time"
)
type listener struct {
Addr string `json:"addr"`
FD int `json:"fd"`
Filename string `json:"filename"`
}
func importListener(addr string) (net.Listener, error) {
// 從環(huán)境變量中抽離出被編碼的 listener 的元數(shù)據(jù)。
listenerEnv := os.Getenv("LISTENER")
if listenerEnv == "" {
return nil, fmt.Errorf("unable to find LISTENER environment variable")
}
// 解碼 listener 的元數(shù)據(jù)。
var l listener
err := json.Unmarshal([]byte(listenerEnv), &l)
if err != nil {
return nil, err
}
if l.Addr != addr {
return nil, fmt.Errorf("unable to find listener for %v", addr)
}
// 文件已經(jīng)被傳入到這個進程中,從元數(shù)據(jù)中抽離文件描述符和名字,為 listener 重建/發(fā)現(xiàn) *os.file
listenerFile := os.NewFile(uintptr(l.FD), l.Filename)
if listenerFile == nil {
return nil, fmt.Errorf("unable to create listener file: %v", err)
}
defer listenerFile.Close()
// Create a net.Listener from the *os.File.
ln, err := net.FileListener(listenerFile)
if err != nil {
return nil, err
}
return ln, nil
}
func createListener(addr string) (net.Listener, error) {
ln, err := net.Listen("tcp", addr)
if err != nil {
return nil, err
}
return ln, nil
}
func createOrImportListener(addr string) (net.Listener, error) {
// 嘗試為地址導(dǎo)入一個 listener, 如果導(dǎo)入成功,則使用。
ln, err := importListener(addr)
if err == nil {
fmt.Printf("Imported listener file descriptor for %v.\n", addr)
return ln, nil
}
// 沒有 listener 被導(dǎo)入,這就意味著進程必須自己創(chuàng)建一個。
ln, err = createListener(addr)
if err != nil {
return nil, err
}
fmt.Printf("Created listener file descriptor for %v.\n", addr)
return ln, nil
}
func getListenerFile(ln net.Listener) (*os.File, error) {
switch t := ln.(type) {
case *net.TCPListener:
return t.File()
case *net.UnixListener:
return t.File()
}
return nil, fmt.Errorf("unsupported listener: %T", ln)
}
func forkChild(addr string, ln net.Listener) (*os.Process, error) {
// 從 listener 中獲取文件描述符,在環(huán)境變量編碼在傳遞給這個子進程的元數(shù)據(jù)。
lnFile, err := getListenerFile(ln)
if err != nil {
return nil, err
}
defer lnFile.Close()
l := listener{
Addr: addr,
FD: 3,
Filename: lnFile.Name(),
}
listenerEnv, err := json.Marshal(l)
if err != nil {
return nil, err
}
// 將 stdin, stdout, stderr 和 listener 傳入子進程。
// 譯注: 以上四個文件描述符分別為 0,1,2,3
files := []*os.File{
os.Stdin,
os.Stdout,
os.Stderr,
lnFile,
}
// 獲取當(dāng)前環(huán)境變量,并且傳入子進程。
environment := append(os.Environ(), "LISTENER="+string(listenerEnv))
// 獲取當(dāng)前進程名和工作目錄
execName, err := os.Executable()
if err != nil {
return nil, err
}
execDir := filepath.Dir(execName)
// 生成子進程
p, err := os.StartProcess(execName, []string{execName}, &os.ProcAttr{
Dir: execDir,
Env: environment,
Files: files,
Sys: &syscall.SysProcAttr{},
})
if err != nil {
return nil, err
}
return p, nil
}
func waitForSignals(addr string, ln net.Listener, server *http.Server) error {
signalCh := make(chan os.Signal, 1024)
signal.Notify(signalCh, syscall.SIGHUP, syscall.SIGUSR2, syscall.SIGINT, syscall.SIGQUIT)
for {
select {
case s :=
fmt.Printf("%v signal received.\n", s)
switch s {
case syscall.SIGHUP:
// Fork 一個子進程。
p, err := forkChild(addr, ln)
if err != nil {
fmt.Printf("Unable to fork child: %v.\n", err)
continue
}
fmt.Printf("Forked child %v.\n", p.Pid)
// 創(chuàng)建一個在 5 秒鐘過去的 Context, 使用這個超時定時器關(guān)閉。
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// 返回關(guān)閉過程中發(fā)生的任何錯誤。
return server.Shutdown(ctx)
case syscall.SIGUSR2:
// Fork 一個子進程。
p, err := forkChild(addr, ln)
if err != nil {
fmt.Printf("Unable to fork child: %v.\n", err)
continue
}
// 輸出被 fork 的子進程的 PID,并等待更多的信號。
fmt.Printf("Forked child %v.\n", p.Pid)
case syscall.SIGINT, syscall.SIGQUIT:
// 創(chuàng)建一個在 5 秒鐘過去的 Context, 使用這個超時定時器關(guān)閉。
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// 返回關(guān)閉過程中發(fā)生的任何錯誤。
return server.Shutdown(ctx)
}
}
}
}
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello from %v!\n", os.Getpid())
}
func startServer(addr string, ln net.Listener) *http.Server {
http.HandleFunc("/hello", handler)
httpServer := &http.Server{
Addr: addr,
}
go httpServer.Serve(ln)
return httpServer
}
func main() {
// Parse command line flags for the address to listen on.
var addr string
flag.StringVar(&addr, "addr", ":8080", "Address to listen on.")
// Create (or import) a net.Listener and start a goroutine that runs
// a HTTP server on that net.Listener.
ln, err := createOrImportListener(addr)
if err != nil {
fmt.Printf("Unable to create or import a listener: %v.\n", err)
os.Exit(1)
}
server := startServer(addr, ln)
// 等待復(fù)制或結(jié)束的信號
err = waitForSignals(addr, ln, server)
if err != nil {
fmt.Printf("Exiting: %v\n", err)
return
}
fmt.Printf("Exiting.\n")
}
如果你讀到了這里
Teleport 是一個開源軟件,你可以免費地在
本文由
歡迎關(guān)注站長公眾號:polarisxu,有更多驚喜等著你!
總結(jié)
以上是生活随笔為你收集整理的exec go 重启_无停机优雅重启 Go 程序的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: java 多线程池_Java Threa
- 下一篇: 计算机考试报名无法弹出支付界面,教资报名