守护进程和平滑重启(八)

Gin-API 创建守护进程

实现函数

/*

Linux Mac 下运行

守护进程是生存期长的一种进程。它们独立于控制终端并且周期性的执行某种任务或等待处理某些发生的事件。

守护进程必须与其运行前的环境隔离开来。这些环境包括未关闭的文件描述符、控制终端、会话和进程组、工作目录以及文件创建掩码等。这些环境通常是守护进程从执行它的父进程(特别是shell)中继承下来的。

本程序只fork一次子进程,fork第二次主要目的是防止进程再次打开一个控制终端(不是必要的)。因为打开一个控制终端的前台条件是该进程必须是会话组长,再fork一次,子进程ID != sid(sid是进程父进程的sid),所以也无法打开新的控制终端

*/

package daemon

import (

"fmt"

"os"

"os/exec"

"syscall"

"time"

)

//var daemon = flag.Bool("d", false, "run app as a daemon process with -d=true")

func InitProcess() {

if syscall.Getppid() == 1 {

if err := os.Chdir("./"); err != nil {

panic(err)

}

syscall.Umask(0) // TODO TEST

return

}

fmt.Println("go daemon!!!")

fp, err := os.OpenFile("daemon.txt", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)

if err != nil {

panic(err)

}

defer func() {

_ = fp.Close()

}()

cmd := exec.Command(os.Args[0], os.Args[1:]...)

cmd.Stdout = fp

cmd.Stderr = fp

cmd.Stdin = nil

cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true} // TODO TEST

if err := cmd.Start(); err != nil {

panic(err)

}

_, _ = fp.WriteString(fmt.Sprintf(

"[PID] %d Start At %s\n", cmd.Process.Pid, time.Now().Format("2006-01-02 15:04:05")))

os.Exit(0)

}

初始化

func main() {

    daemon.InitProcess() 

    // ...

}

Gin-API 平滑重启

创建守护进程之后,我们的程序已经能够在后台正常跑通了,但这样还有个问题,那就是在重启服务时候怎么保证服务不中断?

例如Nginx这种7*24小时接收请求的服务,在程序升级、配置文件更新、或者插件加载的时候就需要重启,为保证重启过程不中断服务,我们会使用平滑重启

平滑重启原理

gin-api服务作为协程启动,做相应的处理并返回数据给客户端;主进程负责监听信号,根据信号进行关闭、重启操作

平滑重启步骤

1、主进程(原进程中的主进程)启动协程处理http请求,主进程开始监听终端信号

2、使用 kill -USR2 $pid 发起停止主进程的动作

3、主进程接收到信号量 12 (SIGUSR2) 后, 启动新的子进程,子进程接管父进程的标准输出、错误输出和socket描述符

4、子进程同样启动协程处理请求,子进程中的主进程继续监听终端信号

5、父进程中的主进程发起关闭协程的动作,该协程处理完所有请求后自动关闭(平滑关闭)

6、父进程中的主进程退出

使用 http.Server

由于gin库函数缺少上下文管理功能,所以我们需要使用http.Server来包裹gin服务,支持对服务的平滑关闭功能

实现方式

func (server *Server) Listen(graceful bool) error {

addr := fmt.Sprintf("%s:%d", server.Host, server.Port)

httpServer := &http.Server{

Addr:    addr,

Handler: server.Router,

}

// 判断是否为 reload

var err error

if graceful {

server.Logger.Info("listening on the existing file descriptor 3")

//子进程的 0 1 2 是预留给 标准输入 标准输出 错误输出

//因此传递的socket 描述符应该放在子进程的 3

f := os.NewFile(3, "")

// 获取 上个服务程序的 socket 的描述符

server.Listener, err = net.FileListener(f)

} else {

server.Logger.Info("listening on a new file descriptor")

server.Listener, err = net.Listen("tcp", httpServer.Addr)

server.Logger.Infof("Actual pid is %d\n", syscall.Getpid())

}

if err != nil {

server.Logger.Error(err)

return err

}

go func() {

// 开启服务

if err := httpServer.Serve(server.Listener); err != nil && err != http.ErrServerClosed {

err = errors.New(fmt.Sprintf("listen error:%v\n", err))

server.Logger.Fatal(err) // 报错退出

}

}()

return server.HandlerSignal(httpServer)

}

func (server *Server) HandlerSignal(httpServer *http.Server) error {

sign := make(chan os.Signal)

signal.Notify(sign, syscall.SIGINT, syscall.SIGTERM, syscall.SIGUSR2)

for {

// 接收信号量

sig := <-sign

server.Logger.Infof("Signal receive: %v\n", sig)

ctx, _ := context.WithTimeout(context.Background(), time.Second*10)

switch sig {

case syscall.SIGINT, syscall.SIGTERM:

// 关闭服务

server.Logger.Info("Shutdown Api Server")

signal.Stop(sign) // 停止通道

if err := httpServer.Shutdown(ctx); err != nil {

err = errors.New(fmt.Sprintf("Shutdown Api Server Error: %s", err))

return err

}

return nil

case syscall.SIGUSR2:

// 重启服务

server.Logger.Info("Reload Api Server")

// 先启动新服务

if err := server.Reload(); err != nil {

server.Logger.Errorf("Reload Api Server Error: %s", err)

continue

}

// 关闭旧服务

if err := httpServer.Shutdown(ctx); err != nil {

err = errors.New(fmt.Sprintf("Shutdown Api Server Error: %s", err))

return err

}

if err := destroyMgoPool(); err != nil {

return err

}

server.Logger.Info("Reload Api Server Successful")

return nil

}

}

}

func (server *Server) Reload() error {

tl, ok := server.Listener.(*net.TCPListener)

if !ok {

return errors.New("listener is not tcp listener")

}

f, err := tl.File()

if err != nil {

return err

}

// 命令行启动新程序

args := []string{"-graceful"}

cmd := exec.Command(os.Args[0], args...)

cmd.Stdout = os.Stdout        //  1

cmd.Stderr = os.Stderr        //  2

cmd.ExtraFiles = []*os.File{f} //  3

if err := cmd.Start(); err != nil {

return err

}

server.Logger.Infof("Forked New Pid %v: \n", cmd.Process.Pid)

return nil

}

深圳网站建设www.sz886.com

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,542评论 6 504
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,822评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,912评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,449评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,500评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,370评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,193评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,074评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,505评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,722评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,841评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,569评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,168评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,783评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,918评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,962评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,781评论 2 354