之前我们搭建过一个 ai服务器并设置了https访问 :
现在在他基础上 使用守护进程启动服务器+平滑重启
守护进程是在后台运行不受终端控制的进程(如输入、输出等),一般的网络服务都是以守护进程的方式运行。守护进程脱离终端的主要原因有两点:(1)用来启动守护进程的终端在启动守护进程之后,需要执行其他任务。(2)(如其他用户登录该终端后,以前的守护进程的错误信息不应出现)由终端上的一些键所产生的信号(如中断信号),不应对以前从该终端上启动的任何守护进程造成影响。要注意守护进程与后台运行程序(即加&启动的程序)的区别。
建立文件夹和文件 aichat/service/hotupdate.go ,hotupdate.go 内容如下:
package service
import "C"
import (
"context"
"errors"
"flag"
"log"
"net"
"net/http"
"os"
"os/exec"
"os/signal"
"syscall"
"time"
)
/************************** 热重启 ***************************/
var (
listener net.Listener = nil
graceful = flag.Bool("graceful", false, "listen on fd open 3 (internal use only)")
pemPath = "/usr/local/orange/conf/cert/online/go.daily886.com.pem"
keyPath = "/usr/local/orange/conf/cert/online/go.daily886.com.key"
)
//监听服务器
func Listenserver(server *http.Server){
var err error
//解析参数
flag.Parse()
//设置监听的对象(新建或已存在的socket描述符)
if *graceful {
//子进程监听父进程传递的 socket描述符
log.Println("listening on the existing file descriptor 3")
//子进程的 0 1 2 是预留给 标准输入 标准输出 错误输出
//因此传递的socket 描述符应该放在子进程的 3
f := os.NewFile(3,"")
listener,err = net.FileListener(f)
log.Printf( "graceful-reborn %v %v %#v \n", f.Fd(), f.Name(), listener)
}else{
//启动守护进程
daemonProcce(1,1);
//父进程监听新建的 socket 描述符
log.Println("listening on a new file descriptor")
listener,err = net.Listen("tcp",server.Addr)
log.Printf("Actual pid is %d\n", syscall.Getpid())
}
if err != nil{
log.Fatalf("listener error: %v\n",err)
}
go func(){
err = server.ServeTLS(listener,pemPath,keyPath)
log.Printf("server.Serve err: %v\n",err)
tcp,_ := listener.(*net.TCPListener)
fd,_ := tcp.File()
log.Printf( "first-boot %v %v %#v \n ", fd.Fd(),fd.Name(), listener)
}()
//监听信号
handleSignal(server)
log.Println("signal end")
}
//处理信号
func handleSignal(server *http.Server){
//把信号 赋值给 通道
ch := make(chan os.Signal, 1)
//监听信号
signal.Notify(ch, syscall.SIGINT,syscall.SIGTERM,syscall.SIGUSR2)
//阻塞主进程, 不停的监听系统信号
for{
//通道 赋值给 sig
sig := <-ch
log.Printf("signal receive: %v\n", sig)
ctx,_ := context.WithTimeout(context.Background(),20*time.Second)
switch sig{
case syscall.SIGINT,syscall.SIGTERM: //终止进程执行
log.Println("shutdown")
signal.Stop(ch) //停止通道
server.Shutdown(ctx) //关闭服务器窗口
log.Println("graceful shutdown")
return
case syscall.SIGUSR2: //进程热重启
log.Println("reload")
err := reload() //执行热重启
if err != nil{
log.Fatalf("listener error: %v\n",err)
}
//server.Shutdown(ctx)
log.Println("graceful reload")
return
}
}
}
//热重启
func reload() error{
tl, ok := listener.(*net.TCPListener)
if !ok {
return errors.New("listener is not tcp listener")
}
//获取socket描述符
currentFD, err := tl.File()
if err != nil {
return err
}
//设置传递给子进程的参数(包含 socket描述符)
args := []string{"-graceful"}
//args = append(args, "-continue")
cmd := exec.Command(os.Args[0],args...)
cmd.Stdout = os.Stdout //标准输出
cmd.Stderr = os.Stderr //错误输出
cmd.ExtraFiles = []*os.File{currentFD} //文件描述符
err = cmd.Start()
log.Printf("forked new pid %v: \n",cmd.Process.Pid)
if err != nil{
return err
}
return nil
}
/*
我们在父进程执行 cmd.ExtraFiles = []*os.File{f} 来传递 socket 描述符给子进程,子进程通过执行 f := os.NewFile(3, "") 来获取该描述符。值得注意的是,子进程的 0 、1 和 2 分别预留给标准输入、标准输出和错误输出,所以父进程传递的 socket 描述符在子进程的顺序是从 3 开始。
*/
//nochdir 是 程序初始路径 1是当前路径,0是系统根目录
//noclose 是 错误信息输出 1是输出当前, 0是不显示错误信息
func daemonProcce(nochdir, noclose int) (int,error){
// already a daemon
log.Printf("syscall.Getppid() %+v\n",syscall.Getppid())
//如果是守护进程 syscall.Getppid() = 1
if syscall.Getppid() == 1 {
/* Change the file mode mask */
syscall.Umask(0)
if nochdir == 0 {
os.Chdir("/")
}
return 0, nil
}
files := make([]*os.File, 3, 6)
if noclose == 0 {
nullDev, err := os.OpenFile("/dev/null", 0, 0)
if err != nil {
return 1, err
}
files[0], files[1], files[2] = nullDev, nullDev, nullDev
} else {
files[0], files[1], files[2] = os.Stdin, os.Stdout, os.Stderr
}
dir, _ := os.Getwd()
sysattrs := syscall.SysProcAttr{Setsid: true}
attrs := os.ProcAttr{Dir: dir, Env: os.Environ(), Files: files, Sys: &sysattrs}
proc, err := os.StartProcess(os.Args[0], os.Args, &attrs)
if err != nil {
return -1, err
}
proc.Release()
os.Exit(0)
return 0, nil
}
修改文件 aichat/main.go ,main.go 内容如下:
package main
import (
"log"
"net/http"
"github.com/gin-gonic/gin"
"github.com/spf13/viper"
"aichat/config"
"aichat/router"
"aichat/service"
)
func main() {
if err := config.Init();err != nil{
panic(err)
}
//设置gin模式
gin.SetMode(viper.GetString("common.server.runmode"))
//创建一个gin引擎
g := gin.New()
router.InitRouter(g)
log.Printf("开始监听服务器地址: %s\n", viper.GetString("common.server.url"))
// Listen and Server in https://127.0.0.1:8080
//err := g.Run(viper.GetString("common.server.addr")) //使用http
//err := g.RunTLS( //使用https
// viper.GetString("common.server.addr"),
// "/usr/local/orange/conf/cert/online/go.daily886.com.pem",
// "/usr/local/orange/conf/cert/online/go.daily886.com.key")
//if err != nil {
// log.Fatal("监听错误:", err)
//}
//使用热重启
// kill -USR2 pid 重启
// kill -INT pid 关闭
add := viper.GetString("common.server.addr")
srv := &http.Server{
Addr: add,
Handler: g,
}
log.Printf( "srv.Addr %v \n", srv.Addr)
service.Listenserver(srv)
}
打包并运行
[root@izj6c4jirdug8kh3uo6rdez aichat]# go build
[root@izj6c4jirdug8kh3uo6rdez aichat]# ./aichat
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
- using env: export GIN_MODE=release
- using code: gin.SetMode(gin.ReleaseMode)
[GIN-debug] GET / --> aichat/service.Index (6 handlers)
[GIN-debug] GET /chat --> aichat/service.AiChat (6 handlers)
[root@izj6c4jirdug8kh3uo6rdez aichat]# [GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
- using env: export GIN_MODE=release
- using code: gin.SetMode(gin.ReleaseMode)
[GIN-debug] GET / --> aichat/service.Index (6 handlers)
[GIN-debug] GET /chat --> aichat/service.AiChat (6 handlers)
2019/08/02 17:36:17 main.go:26: 开始监听服务器地址: :6663
2019/08/02 17:36:17 main.go:49: srv.Addr :6663
2019/08/02 17:36:17 hotupdate.go:137: syscall.Getppid() 4023
2019/08/02 17:36:17 main.go:26: 开始监听服务器地址: :6663
2019/08/02 17:36:17 main.go:49: srv.Addr :6663
2019/08/02 17:36:17 hotupdate.go:137: syscall.Getppid() 1
2019/08/02 17:36:17 hotupdate.go:50: listening on a new file descriptor
2019/08/02 17:36:17 hotupdate.go:52: Actual pid is 5359
平滑重启
[root@izj6c4jirdug8kh3uo6rdez ~]# kill -USR2 5359
2019/08/02 17:37:15 hotupdate.go:80: signal receive: user defined signal 2
2019/08/02 17:37:15 hotupdate.go:90: reload
2019/08/02 17:37:15 hotupdate.go:122: forked new pid 5412:
2019/08/02 17:37:15 hotupdate.go:96: graceful reload
2019/08/02 17:37:15 hotupdate.go:67: signal end
2019/08/02 17:37:15 main.go:26: 开始监听服务器地址: :6663
2019/08/02 17:37:15 main.go:49: srv.Addr :6663
2019/08/02 17:37:15 hotupdate.go:40: listening on the existing file descriptor 3
2019/08/02 17:37:15 hotupdate.go:45: graceful-reborn 3 &net.TCPListener{fd:(*net.netFD)(0xc0000e3580)}