上篇大致提到的Golang的热更新,本篇将详细论述。

1、什么是热更新

网络上有这么一个例子来形容热更新,我觉得很形象很贴切:

一架行驶在高速上的大卡车,行驶过程中突然遭遇爆胎,热更新则是要求在不停车的情况下将车胎修补好,且补胎过程中卡车需要保持正常行驶。

软件的热更新就是指在保持系统正常运行的情况下对系统进行更新升级。常见的情况有:系统服务升级、修复现有逻辑、服务配置更新等。

2、热更新原理

先来看下Nginx热更新是如何做的?
Nginx支持运行中接收信号,方便开发者控制进程。

USR2WINCHQUIT

:在*nix系统中,信号(Signal)是一种进程间通信机制,它给应用程序提供一种异步的软件中断,使应用程序有机会接受其他程序或终端发送的命令(即信号)。

USR2

:Plugin包方式的Golang热更新本文暂不讨论。

3、热更新实现

Golang热更新可以细分为服务热『更新』(即热升级,类比Nginx的restart命令)与配置文件热更新(类比Nginx的reload命令)。接下来从实现细节处依次讨论。

3.1 服务热更新

大致流程如下:

USR2USR2
USR2
func (a *app) signalHandler(wg *sync.WaitGroup) {
    ch := make(chan os.Signal, 10)
    signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM, syscall.SIGUSR2)
    for {
        sig := <-ch
        switch sig {
        case syscall.SIGINT, syscall.SIGTERM:
            // 确保接收到INT/TERM信号时可以触发Golang标准的进程终止行为
            signal.Stop(ch)
            a.term(wg)
            return
        case syscall.SIGUSR2:
            err := a.preStartProcess()
            if err != nil {
                a.errors <- err
            }
            // 发起新进程
            if _, err := a.net.StartProcess(); err != nil {
                a.errors <- err
            }
        }
    }
}

复制当前进程socket连接,发起新进程

execSpec := &syscall.ProcAttr{
Env: os.Environ(),
Files: []uintptr{os.Stdin.Fd(), os.Stdout.Fd(), os.Stderr.Fd()},
}
fork, err := syscall.ForkExec(os.Args[0], os.Args, execSpec)
...

以上仅为代码示例,目前已经成熟的开源实现主要有:endless和facebook的grace,原理基本类似,fork一个子进程,子进程监听原有父进程socket端口,父进程优雅退出。

github.com/facebookgo/grace/gracehttp
func main() {
    app := gin.New()// 项目中时候的是gin框架
    router.Route(app)
    var server *http.Server
    server = &http.Server{
        Addr:    ":8080",
        Handler: app,
    }
    gracehttp.Serve(server)
}

利用go build命令编译,生成服务的可执行文件。
然后再用shell封装一下服务命令,生成restat.sh命令文件

#!/bin/sh

ps aux | grep wingo
count=`ps -ef | grep "wingo" | grep -v "grep" | wc -l`
echo ""

if [ 0 == $count ]; then
    echo "Wingo starting..."
    sudo ./wingo &
    echo "Wingo started"
else
    echo "Wingo Restarting..."
    sudo kill -USR2 $(ps -ef | grep "wingo" | grep -v grep | awk '{print $2}')
    echo "Wingo Restarted"
fi

sleep 1

ps aux | grep wingo

:其中wingo为服务的二进制名称。

于是,便可通过执行./restart.sh命令,达到对服务的热升级目的。

3.2 配置文件热更新

USR1
USR1USR1

主要代码实现:

// LoadAllConf 调用加载配置文件函数
// load为具体加载配置文件方法
func LoadAllConf(load func(bool)) {
    load(true)
    listenSIGUSR1(load)
}

// listenSIGUSR1 监听SIGUSR1信号
func listenSIGUSR1(f func(bool)) {
    s := make(chan os.Signal, 1)
    signal.Notify(s, syscall.SIGUSR1)
    go func() {
        for {
            <-s
            f(false)
            log.Println("Reloaded")
        }
    }()
}

利用go build命令编译,生成服务的可执行文件。
然后再用shell封装一下配置重载命令,生成reload.sh命令文件

#!/bin/sh

ps aux | grep wingo
echo ""

echo "Wingo Reloading..."
sudo kill -USR1 $(ps -ef | grep "wingo" | grep -v grep | awk '{print $2}')
echo "Wingo Reloaded"
echo ""

sleep 1

ps aux | grep wingo

于是,便可通过执行./reload.sh命令,达到对服务的配置文件热升级目的。

4、总结

本文主要描述了Golang服务热升级与配置文件热更新原理与主要代码实现,本质上也不是什么新内容,如果之前读过《Unix环境高级编程》,就会觉得很亲切。底层原理基本上是利用了信号这个软件中断机制,在运行中改变常驻进程的行为。

References