热更新
去年写了一个Agent的程序,用于收集生产服务器的一些数据,以及对应的一些自动化操作等, 写完之后经常要修修改改加一些新功能, 产线服务器数量就很多, 导致了每次更新都是个大动作,目前的做法是通过puppet管理,新版本就往puppet上丢,等他自动重启即可,由此联想到了老东家游戏服务的热加载,所以看了一下golang的热加载实现。
基本流程
第一种方式, 文件主体更新
- golang服务进程运行时监听USR2信号
- 进程收到USR2信号后, 下载新版本的客户端到本地
- fork子进程(启动新版本服务)
- 将上下文, 句柄等信息交到新的子进程
- 新进程开始监听socket请求
- 等待旧服务连接停止
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{})进行断言成函数,最后执行该函数。