日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

exec go 重启_无停机优雅重启 Go 程序

發(fā)布時(shí)間:2024/9/27 编程问答 31 豆豆
生活随笔 收集整理的這篇文章主要介紹了 exec go 重启_无停机优雅重启 Go 程序 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

什么是優(yōu)雅重啟

在不停機(jī)的情況下,就地部署一個(gè)應(yīng)用程序的新版本或者修改其配置的能力已經(jīng)成為現(xiàn)代軟件系統(tǒng)的標(biāo)配。這篇文章討論優(yōu)雅重啟一個(gè)應(yīng)用的不同方法,并且提供一個(gè)功能獨(dú)立的案例來深挖實(shí)現(xiàn)細(xì)節(jié)。如果你不熟悉 Teleport 話,Teleport 是我們使用 Golang 針對(duì)彈性架構(gòu)設(shè)計(jì)的

SO_REUSEPORT vs 復(fù)制套接字的背景

為了推進(jìn) Teleport 高可用的工作,我們最近花了些時(shí)間研究如何優(yōu)雅重啟 Teleport 的 TLS 和 SSH 的端口監(jiān)聽器

Marek Majkowski 在他的博客文章你可以在套接字上設(shè)置 SO_REUSEPORT ,從而讓多個(gè)進(jìn)程能夠被綁定到同一個(gè)端口上。利用這個(gè)方法,你會(huì)有多個(gè)接受隊(duì)列向多個(gè)進(jìn)程提供數(shù)據(jù)。

復(fù)制套接字,并把它以文件的形式傳送給一個(gè)子進(jìn)程,然后在新的進(jìn)程中重新創(chuàng)建這個(gè)套接字。使用這種方法,你將有一個(gè)接受隊(duì)列向多個(gè)進(jìn)程提供數(shù)據(jù)。]

在我們初期的討論中,我們了解到幾個(gè)關(guān)于 SO_REUSEPORT 的問題。我們的一個(gè)工程師之前使用這個(gè)方法,并且注意到由于其多個(gè)接受隊(duì)列,有時(shí)候會(huì)丟棄掛起的 TCP 連接。除此之外,當(dāng)我們進(jìn)行這些討論的時(shí)候,Go 并沒有很好地支持在一個(gè) net.Listener 上設(shè)置 SO_REUSEPORT。然而,在過去的幾天中,在這個(gè)問題上有了進(jìn)展,看起來像

第二種方法也很吸引人,因?yàn)樗暮唵涡砸约按蠖鄶?shù)開發(fā)人員熟悉的傳統(tǒng)Unix 的 fork/exec 產(chǎn)生模型,即將所有打開文件傳遞給子進(jìn)程的約定。需要注意的一點(diǎn),os/exec 包實(shí)際上不贊同這種用法。主要是出于安全上的考量,它只傳遞 stdin , stdout 和 stderr 給子進(jìn)程。然而, os 包確實(shí)提供較低級(jí)的原語,可用于將文件傳遞給子程序,這就是我們想做的。

使用信號(hào)切換套接字進(jìn)程所有者

在我們看源碼之前,了解一些這個(gè)方法如何工作的細(xì)節(jié)是值得的。

啟動(dòng)一個(gè)全新的 Teleport 程序后,該進(jìn)程會(huì)在綁定的端口上創(chuàng)建一個(gè)監(jiān)聽套接字接受所有入站流量。對(duì)于 Teleport,入口流量就是 LTS 和 SSH 流量。我們添加了一個(gè)處理

應(yīng)該注意的是,當(dāng)一個(gè)套接字被復(fù)制時(shí),入棧流量會(huì)在兩個(gè)套接字之間以輪詢的方式進(jìn)行負(fù)載均衡。如下圖所示,這就意味著有一段時(shí)間,兩個(gè) Teleport 進(jìn)程都會(huì)接受新的連接。

父進(jìn)程的關(guān)閉是相同的事情,但是反過來做。一旦 Teleport 進(jìn)程接受到 SIGOUIT 信號(hào),他會(huì)開始關(guān)閉這個(gè)進(jìn)程,停止接受新的連接,等待所有的現(xiàn)有連接斷開或是超時(shí)發(fā)生。一旦入站流量被清空,這個(gè)瀕死進(jìn)程就會(huì)關(guān)閉它的監(jiān)聽套接字并且退出。這種情況下,新的進(jìn)程會(huì)接管內(nèi)核發(fā)送過來的所有請(qǐng)求。

優(yōu)雅重啟演練

我們基于上面的方法寫了一個(gè)簡單的程序,你可以自己嘗試使用一下。源代碼在文章的最后,你可以按照以下步驟嘗試這個(gè)例子。

首先,編譯和啟動(dòng)程序。

$ go build restart.go

$ ./restart &

[1] 95147

$ Created listener file descriptor for :8080.

$ curl http://localhost:8080/hello

Hello from 95147!

將 USR2 信號(hào)發(fā)送給初始進(jìn)程。現(xiàn)在,當(dāng)你訪問這個(gè) HTTP 入口的時(shí)候,他會(huì)返回兩個(gè)不同的進(jìn)程的 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!

殺死初始進(jìn)程后,你將只會(huì)從新的進(jìn)程中獲得返回。

$ 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!

最后殺死新進(jìn)程,訪問將會(huì)被拒絕。

$ 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ù)使用者的用戶體驗(yàn)。如果你想在 Teleport 中看到這一點(diǎn),我們邀請(qǐng)你瞧瞧我們的參考

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)被傳入到這個(gè)進(jìn)程中,從元數(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)入一個(gè) 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)入,這就意味著進(jìn)程必須自己創(chuàng)建一個(gè)。

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)境變量編碼在傳遞給這個(gè)子進(jì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 傳入子進(jìn)程。

// 譯注: 以上四個(gè)文件描述符分別為 0,1,2,3

files := []*os.File{

os.Stdin,

os.Stdout,

os.Stderr,

lnFile,

}

// 獲取當(dāng)前環(huán)境變量,并且傳入子進(jìn)程。

environment := append(os.Environ(), "LISTENER="+string(listenerEnv))

// 獲取當(dāng)前進(jìn)程名和工作目錄

execName, err := os.Executable()

if err != nil {

return nil, err

}

execDir := filepath.Dir(execName)

// 生成子進(jìn)程

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 一個(gè)子進(jìn)程。

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)建一個(gè)在 5 秒鐘過去的 Context, 使用這個(gè)超時(shí)定時(shí)器關(guān)閉。

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)

defer cancel()

// 返回關(guān)閉過程中發(fā)生的任何錯(cuò)誤。

return server.Shutdown(ctx)

case syscall.SIGUSR2:

// Fork 一個(gè)子進(jìn)程。

p, err := forkChild(addr, ln)

if err != nil {

fmt.Printf("Unable to fork child: %v.\n", err)

continue

}

// 輸出被 fork 的子進(jìn)程的 PID,并等待更多的信號(hào)。

fmt.Printf("Forked child %v.\n", p.Pid)

case syscall.SIGINT, syscall.SIGQUIT:

// 創(chuàng)建一個(gè)在 5 秒鐘過去的 Context, 使用這個(gè)超時(shí)定時(shí)器關(guān)閉。

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)

defer cancel()

// 返回關(guān)閉過程中發(fā)生的任何錯(cuò)誤。

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é)束的信號(hào)

err = waitForSignals(addr, ln, server)

if err != nil {

fmt.Printf("Exiting: %v\n", err)

return

}

fmt.Printf("Exiting.\n")

}

如果你讀到了這里

Teleport 是一個(gè)開源軟件,你可以免費(fèi)地在

本文由

歡迎關(guān)注站長公眾號(hào):polarisxu,有更多驚喜等著你!

總結(jié)

以上是生活随笔為你收集整理的exec go 重启_无停机优雅重启 Go 程序的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。