Golang 热更新

首先强类型的 golang 自己没有从语言层面支持热更新,也就是说大家可以理解为 golang 自身不支持热更新。不过有第三方的库让 golang 支持热更新,比如:https://github.com/rcrowley/goagain 与 https://github.com/facebookgo/grace, 这两个都是 star 在 1k 以上的,可用性稳定性应该不错(自己还没有尝试使用过-.-)。当然还有人提出使用 C 的方式来支持热更新。具体是通过编译成 so 共享库文件(so 为共享库,是 shared object,用于 Linux 下动态链接文件,和 window 下动态链接库文件 dll 差不多。特点:ELF 格式文件,共享库(动态库),类似于 DLL。节约资源,加快速度,代码升级简化),例如:

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152
package main#include #cgo LDFLAGS: -ldlvoid (*foo)(int);void set_foo(void* fn) {    foo = fn;}void call_foo(int i) {    foo(i);}*/
import "C"

import "fmt"

func () {    n := 0
    var bar string
    for {        hd := C.dlopen(C.CString(fmt.Sprintf("./foo-%d.so", n)), C.RTLD_LAZY)        if hd == nil {            panic("dlopen")        }        foo := C.dlsym(hd, C.CString("foo"))        if foo == nil {            panic("dlsym")        }        fmt.Printf("%vn", foo)        C.set_foo(foo)        C.call_foo(42)        fmt.Scanf("%s", &bar)        n++    }}so源码:
package main
import "fmt"

import "C"

func () {}//export foo
func foo(i C.int) {    fmt.Printf("%d-2n", i)}

用 go build -buildmode=c-shared -o foo-1.so mod.go 编译。需要 golang 编译器版本>=1.5。这是借助 C 的机制来实现的,go 的 execution modes 文档提到会有 go 原生的 plugin 模式。不过这种的可行性有待考究。

还有一种做法是可以将服务端微服务化;这样每个服务的重启成本很低,reload 数据库到内存的时间成本就会更低。另外为服务化后,可以针对不同的服务是否有必要热更新,结合脚本或其它方法实现(如:游戏运维的活动服务需要频繁变更)。而像一些基本的如用户,游戏逻辑等接口设计灵活一点的情况下;是完全没必要热更新的;每次版本变更停服重启就 ok。

nginx 是支持热升级的,可以用老进程服务先前链接的链接,使用新进程服务新的链接,即在不停止服务的情况下完成系统的升级与运行参数修改。那么热升级和热编译是不同的概念,热编译是通过监控文件的变化重新编译,然后重启进程。那么也可以用 golang 模仿 nginx 的方式来实现热更新。

根据以上的思路,谢大总结出了一套他自己实现 beego 热更新的方法,思路如下:

热升级的原理基本上就是:主进程 fork 一个进程,然后子进程 exec 相应的程序。那么这个过程中发生了什么呢?我们知道进程 fork 之后会把主进程的所有句柄、数据和堆栈、但是里面所有的句柄存在一个叫做 CloseOnExec,也就是执行 exec 的时候,copy 的所有的句柄都被关闭了,除非特别申明,而我们期望的是子进程能够复用主进程的 net.Listener 的句柄。一个进程一旦调用 exec 类函数,它本身就” 死亡” 了,系统把代码段替换成新的程序的代码,废弃原有的数据段和堆栈段,并为新程序分配新的数据段与堆栈段,唯一留下的,就是进程号,也就是说,对系统而言,还是同一个进程,不过已经是另一个程序了。

那么我们要做的:
第一步就是让子进程继承主进程的这个句柄,我们可以通过 os.StartProcess 的参数来附加 Files,把需要继承的句柄写在里面。

第二步就是我们希望子进程能够从这个句柄启动监听,还好 Go 里面支持 net.FileListener,直接从句柄来监听,但是我们需要子进程知道这个 FD,所以在启动子进程的时候我们设置了一个环境变量设置这个 FD。

第三步就是我们期望老的链接继续服务完,而新的链接采用新的进程,这里面有两个细节,第一就是老的链接继续服务,那么我们怎么有老链接存在?所以我们必须每次接收一个链接记录一下,这样我们就知道还存在没有服务完的链接,第二就是怎么让老进程停止接收数据,让新进程接收数据呢?大家都监听在同一个端口,理论上是随机来接收的,所以这里我们只要关闭老的链接的接收就行,这样就会使得在 l.Accept 的时候报错。

演示地址:http://my.oschina.net/astaxie/blog/136364
到底 golang 是不是一定要热更新功能,最后用达达来观点来总结一下。

没有热更新的确没那么方便,但是也没那么可怕。

原因:

需要临时重启更新就运营公告,如果实际较长就适当发放补偿。
Go 加载数据到内存的速度也比之前快很多,重启压力也没想象的那么大。
强类型语法在编译器提前排除了很多之前要到线上运行时才能发现的问题,所以 BUG 率低了。
所以没有热更新也顺利跑下来了。

不过以上只能做为参考,不同项目需求不一样,还是得结合实际情况来判断。

热更新肯定是可以做的,方案挺多,数据驱动、内嵌脚本或者无状态进程都可行,只是花多大代价换多少回报的问题。

如果评估下来觉得热更新必做不可,那么用再大代价也得做,这是项目存亡问题。

如果不是必须的,那就需要评估性价比了。

做热更新、换编程语言或者换服务端架构所花的代价,换来的产品在运营、运维或开发各方面的效率提升,是否划算。