背景
对于一个部署在生产环境的项目来说,我们希望当代码出现bug的时候,可以不用重启进程而达到动态修改代码的目的------
这就是代码热部署!
使用java做游戏服务器,最大的好处是,当代码出现bug,可以直接热更新代码来解决,而无须重启服务器。
如果使用JVM的Instrumentation功能,可以实现方法体内部的代码热更新,具体原理及操作可参考
如果使用类单列替换,甚至可以实现在类内部添加新的属性或者方法,具体原理及操作可参考
Go热更新
基本演示
插件代码 plugin.go
Go
package main
import "fmt"
func SayHello() {
fmt.Println("11111")
}
编译插件
在 Windows 命令行中,使用以下命令编译插件:
bash
go build -buildmode=plugin -o plugin.dll plugin.go
遗憾的是,截止到go 1.23.0,windows暂不支持plugin模式,直接报错:
go build -buildmode=plugin -o plugin.dll plugin.go
-buildmode=plugin not supported on windows/amd64
改成linux测试
go build -o plugin.so -buildmode=plugin plugin.go
主程序代码 main.go
Go
package main
import (
"fmt"
"plugin"
"time"
)
func loadPlugin() (func(), error) {
p, err := plugin.Open("plugin.so")
if err != nil {
return nil, err
}
sayHello, err := p.Lookup("SayHello")
if err != nil {
return nil, err
}
return sayHello.(func()), nil
}
func main() {
sayHello, err := loadPlugin()
if err != nil {
fmt.Println("Error loading plugin:", err)
return
}
sayHello()
// 模拟文件监控,这里简单使用定时检查
ticker := time.NewTicker(5 * time.Second)
for range ticker.C {
newSayHello, err := loadPlugin()
if err == nil {
sayHello = newSayHello
fmt.Println("Plugin reloaded.")
}
sayHello()
}
}
修改plugin.go代码
Go
package main
import "fmt"
func SayHello() {
fmt.Println("2222")
}
重新编译,发现重新加载了插件,但打印还是旧的。 百思不得其解,尝试添加输出文件的修改日期,或者输出函数指针地址,都找不到原因。最后,在网上偶然看到有文章说,plugin.Open()函数,对于同一个文件名称,只会加载一次。
由此想到一种思路,每次编译使用不同的名称,然后通过http的方式,通过main函数加载新的插件名称。代码如下:
Go
func updatePluginName(c *gin.Context) {
pluginName := c.Query("name")
if pluginName == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Plugin name is required"})
return
}
newSayHello, err := loadPlugin(pluginName)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to load plugin: %v", err)})
return
}
sayHello = newSayHello
c.JSON(http.StatusOK, gin.H{"message": "Plugin reloaded successfully"})
}
func main() {
r := gin.Default()
// 定义更新插件文件名的接口
r.GET("/update-plugin", updatePluginName)
// 启动 Gin 服务器
go func() {
if err := r.Run(":8090"); err != nil {
fmt.Printf("Failed to start server: %v\n", err)
}
}()
// 每隔一段时间调用一次 SayHello 函数
ticker := time.NewTicker(2 * time.Second)
for {
if sayHello != nil {
sayHello()
}
<-ticker.C
}
}
运行程序后,先执行
bash
go build -buildmode=plugin -o plugin.so plugin.go
curl "http://localhost:8090/update-plugin?name=plugin.so"
输出1111
修改plugin.go代码,再执行
bash
go build -buildmode=plugin -o plugin.so plugin2.go
curl "http://localhost:8090/update-plugin?name=plugin2.so"
输出2222
成功了!!
然而,Go 语言的 plugin
包在热更新方面存在诸多限制:
- 一次性加载 :
plugin.Open
对于同一个插件文件只能加载一次,若要更新插件,就必须更换文件名。 - 状态丢失:每次加载新的插件都会创建一个新的实例,旧插件的状态无法保留。
- 功能受限 :
plugin
包主要用于加载外部插件,无法像 Java Instrumentation 那样对已加载的类的方法体进行细粒度的修改。 - windows平台暂不支持
结论是:
Go的plugin机制在生产环境实现热更新,还有很长一段路要走。目前的功能完全是鸡肋!!