net/http 库启动服务后,平滑重启:
需要做哪些事?
- 服务启动:使用TCP启动连接、结合端口,启动服务
- 拷贝进程:fork一个子进程,其中需要复制进程元数据,重造一个。
- 上下文监听:获取系统信号、关闭。 ```java 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) { // 从系统中获取编码后的监听端口元数据 listenerEnv := os.Getenv(“LISTENER”) if listenerEnv == “” { return nil, fmt.Errorf(“unable to find LISTENER environment variable”) }
// Unmarshal the listener metadata.
// 解析元数据
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)
}
// 通过额外的元数据文件,重建端口监听
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) { // 启动TCP监听服务连接 ln, err := net.Listen(“tcp”, addr) if err != nil { // 此处会返回监听失败信息 return nil, err }
return ln, nil
}
func createOrImportListener(addr string) (net.Listener, error) { // Try and import a listener for addr. If it’s found, use it. ln, err := importListener(addr) if err == nil { fmt.Printf(“Imported listener file descriptor for %v.\n”, addr) return ln, nil }
// 当端口启动失败,证明这个端口已经被占用
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) { // 获取端口和元数据放在子进程中。 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 }
// Pass stdin, stdout, and stderr along with the listener to the child.
// 获取系统输入输出流文件
files := []*os.File{
os.Stdin,
os.Stdout,
os.Stderr,
lnFile,
}
// Get current environment and add in the listener to it.
// 获取环境和添加端口
environment := append(os.Environ(), "LISTENER="+string(listenerEnv))
// 获取进程名称和文件
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 { // 到底用多少缓冲合适呢?需要根据自己的服务大小?我觉得不需要,于是用1 //signalCh := make(chan os.Signal, 1024) signalCh := make(chan os.Signal, 1) // 此处可以接收很多种信息,本例子主要是接收SIGHUP信号,从而fork一个进程 // SIGHUP终止收到该信号的进程,用于重启。 // SIGINT强制结束进程 // SIGQUIT结束进程和dump core signal.Notify(signalCh, syscall.SIGHUP, syscall.SIGUSR2, syscall.SIGINT, syscall.SIGQUIT) // 出于保护机制,选择for、select、case来进行 读取channel。因为case可以保护channel在panic情况下不报错 for { select { case s := <-signalCh: fmt.Printf(“%v 信号接收.\n”, s) switch s {
case syscall.SIGHUP:
// fork一个子分支进程,保障运行后,再去关闭服务。即使有服务进来,也不会受到影响,依然运行。
p, err := forkChild(addr, ln)
if err != nil {
fmt.Printf("fork子分支失败: %v.\n", err)
continue
}
fmt.Printf("Forked child子分支Pid: %v.\n", p.Pid)
// 创建一个上下文,当关机时,超过5秒算是超时。
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// 返回关机时遇到的任何报错
return server.Shutdown(ctx)
case syscall.SIGUSR2:
// fork一个子分支进程.
p, err := forkChild(addr, ln)
if err != nil {
fmt.Printf("fork子分支失败: %v.\n", err)
continue
}
// 打印这个PID,等待更多信号
fmt.Printf("Forked child %v.\n", p.Pid)
case syscall.SIGINT, syscall.SIGQUIT:
// 创建一个上下文,当关机时,超过5秒算是超时。
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// 关闭期间报错,则返回错误
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() { var addr string // 端口addr=8080 // 打印标杆在这里输出命令行。本机地址、端口号 flag.StringVar(&addr, “addr”, “:8080”, “Address to listen on.”) // 读者知道就好,不用扣这个细节 // 创建端口与服务. 启动一个goroutine 跑一个Http服务 ln是 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)
// 等待系统信号,fork一个进程或退出
err = waitForSignals(addr, ln, server)
if err != nil {
fmt.Printf("Exiting: %v\n", err)
return
}
fmt.Printf("Exiting.\n")
}
如果是gin框架,又应该如何做呢?
- 只需要把gin的路由带过来,其实也是共用net/http
- 步骤和上面差不多,都是fork一个进程
- 我们可以使用endless的库进行守护即可
```java
package main
import (
"github.com/fvbock/endless"
"github.com/gin-gonic/gin"
"log"
)
func main() {
gin.SetMode(gin.ReleaseMode)
r := setRouter()
if err := endless.ListenAndServe(":9090", r); err != nil {
log.Fatalf("listen: %s\n", err)
}
}
func setRouter() *gin.Engine {
r := gin.Default()
r.GET("/", func(context *gin.Context) {
context.String(200, "hello world AAA!")
})
return r
}
具体想了解endless 做了哪些操作,可以去看一下endless.go