热更新

去年写了一个Agent的程序,用于收集生产服务器的一些数据,以及对应的一些自动化操作等, 写完之后经常要修修改改加一些新功能, 产线服务器数量就很多, 导致了每次更新都是个大动作,目前的做法是通过puppet管理,新版本就往puppet上丢,等他自动重启即可,由此联想到了老东家游戏服务的热加载,所以看了一下golang的热加载实现。

基本流程

第一种方式, 文件主体更新

  1. golang服务进程运行时监听USR2信号
  2. 进程收到USR2信号后, 下载新版本的客户端到本地
  3. fork子进程(启动新版本服务)
  4. 将上下文, 句柄等信息交到新的子进程
  5. 新进程开始监听socket请求
  6. 等待旧服务连接停止
ch := make(chan os.Signal, 10)
signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM, syscall.SIGUSR2)
for {
    sign := <-ch
    switch sign
    case syscall.SIGUSR2:
        if err := StartNewPro(); err != nil {
            ......
            break
        }
        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)
        ......
        process, _ := os.FindProcess(os.Getppid())
        process.Signal(syscall.SIGHUB)
        ......
    }
  • 注意: syscall 在windows 下无法使用,所以最开始我用了个最简单,也很low的办法处理windows, 即在windows 的服务收到了SIGUSR2的信号后,直接os/exec 调用个reload.sh的脚本, 其实也可以实现类似效果,但socket不会进行复制。

当然自己造轮子也不错,当然自己实现可能会有一些BUG之类的,常规的HTTP服务可以直接用endless和grace。

    func main() {
        app := gin.New()// 项目中时候的是gin框架
        router.Route(app)
        var server *http.Server
        server = &http.Server{
            Addr:    ":8080",
            Handler: app,
        }
        gracehttp.Serve(server)
    }
    func main() {
        endPoint := fmt.Sprintf(":%d", setting.HTTPPort)
        server := endless.NewServer(endPoint, routers.InitRouter())
        server.BeforeBegin = func(add string) {
            log.Printf("Actual pid is %d", syscall.Getpid())
        }
    }

第二种方式, 只更新配置文件

这种比较简单,接受USR1的信号进行配置文件,然后关闭新链接,等待所有connect 都为空了,再重新加载,代码比较简单,就不赘述了。

第三种方式, 基于plugin的方式进行更新
写过C++或者做过运维的一般都了解,C的代码发布经常是只更新 .so 文件, 这里的.so即是Linux的动态链接库,其作用是节省程序主体的大小,且可以灵活更新,golang的plugin即是和其原理类似,主要程序主体逻辑不修改,只改动插件的代码,即发布可以做到非常灵活。

plugina.go:

package main

import (
    "fmt"
)

func IamPluginA() {
    fmt.Println("Hello, I am PluginA!")
}

编译成插件

go build --buildmode=plugin -o plugina.so plugina.go

代码主体

package main

import (
    "fmt"
    "os"
    "plugin"
)

func main() {
    p, err := plugin.Open("./plugina.so")
    if err != nil {
        fmt.Println("error open plugin: ", err)
        os.Exit(-1)
    }
    s, err := p.Lookup("IamPluginA")
    if err != nil {
        fmt.Println("error lookup IamPluginA: ", err)
        os.Exit(-1)
    }
    if x, ok := s.(func()); ok {
        x()
    }
}

代码代码也比较简单,打开文件对象,找到函数,然后对函数对象(这会还是个interface{})进行断言成函数,最后执行该函数。

个人公众号, 分享一些日常开发,运维工作中的日常以及一些学习感悟,欢迎大家互相学习,交流

golang实现热更新的常规方式_Golang开发