1. 前言
由于golang属于静态型语言,它编写的业务代码或者文件发生更改就需要重新编译运行项目,而在其它动态编译语言里这算是一大痛点。如果项目比较大,编译阶段很耗时间,比较影响开发跟测试效率。
所以今天笔者来介绍三个小妙招(热重载、平滑重启),这些技术通常一起使用,以确保开发、部署和关闭应用程序时,能够平滑、安全地进行操作,最大程度地减少对用户和客户端影响。
1.1 热重载(Hot ReLoading)
热重载是指在应用程序运行期间动态加载新的代码或资源。
1.2 平滑重启(Graceful Restart)
平滑重启是指在应用程序运行时,通过air 重新启动新的实例来替代旧实例。Golang的os/signal
包可用于优雅地处理信号并实现平滑重启。通过捕获信号( CTRL + C ),例如SIGTERM
和SIGINT
,确保所有正在进行的操作都能够完成,而不是强制性地立即终止。在接收到信号时触发关闭操作。此过程通常包括等待尚未完成的请求完成,释放资源,最终使应用程序平稳退出。从而实现零停机时间的更新。
2. Air介绍
Air 是为 Go 应用开发设计的另外一个热重载的命令行工具。只需在你的项目根目录下输入 air
,然后把它放在一边,专注于你的代码即可。
注意:该工具与生产环境的热部署无关。
3. Air安装
3.1 Golang Tools Install
进入项目的根目录,使用 Go 的版本为 1.16 或更高执行:
go install github.com/cosmtrek/air@latest
我们找到我们的go install
安装的air目录
笔者GOPATH目录地址是(若是不知道请自行百度):/home/liang/goprojects
然后把这个 air 二进制可执行文件做软连接映射到 bin 目录下(便于全局调度使用)
sudo ln -s /home/liang/goprojects/bin/air /usr/bin/air
打开一个新的 terminal
控制台,输入 air -v
检查 air
是否安装成功,如下图:
3.2 Shell Install(没试过)
# 安装在 ./bin/ 路径下
curl -sSfL https://raw.githubusercontent.com/cosmtrek/air/master/install.sh | sh -s
air -v
4. Air热重载
新建一个 go
项目,并在项目的根目录下创建一个 .air.conf
文件,复制air安装目录下 air_example.toml
文件里的内容到创建的 .air.conf
文件中
# [Air](https://github.com/cosmtrek/air) TOML 格式的配置文件
# 工作目录
# 使用 . 或绝对路径,请注意 `tmp_dir` 目录必须在 `root` 目录下
root = "."
tmp_dir = "tmp"
[build]
# 只需要写你平常编译使用的shell命令。你也可以使用 `make`
# Windows平台示例: cmd = "go build -o tmp\main.exe ."
cmd = "go build -o ./tmp/main ."
# 由`cmd`命令得到的二进制文件名
# Windows平台示例:bin = "tmp\main.exe"
bin = "tmp/main"
# 自定义执行程序的命令,可以添加额外的编译标识例如添加 GIN_MODE=release
# Windows平台示例:full_bin = "tmp\main.exe"
full_bin = "APP_ENV=dev APP_USER=air ./tmp/main"
# 监听以下文件扩展名的文件.
include_ext = ["go", "tpl", "tmpl", "html"]
# 忽略这些文件扩展名或目录
exclude_dir = ["assets", "tmp", "vendor", "frontend/node_modules"]
# 监听以下指定目录的文件
include_dir = []
# 排除以下文件
exclude_file = []
# 如果文件更改过于频繁,则没有必要在每次更改时都触发构建。可以设置触发构建的延迟时间
delay = 1000 # ms
# 在杀死进程之前发送中断信号(windows不支持此功能)
send_interrupt = true
# 发送中断信号后的延迟
kill_delay = 120000000000 # ms
# 发生构建错误时,停止运行旧的二进制文件。
stop_on_error = true
# air的日志文件名,该日志文件放置在你的`tmp_dir`中
log = "air_errors.log"
[log]
# 显示日志时间
time = true
[color]
# 自定义每个部分显示的颜色。如果找不到颜色,使用原始的应用程序日志。
main = "magenta"
watcher = "cyan"
build = "yellow"
runner = "green"
[misc]
# 退出时删除tmp目录
clean_on_exit = true
之后我们创建一个 main.go
文件,并编写一个简单的gin
项目
package main
import (
"github.com/gin-gonic/gin"
)
func main() {
// 初始化Gin框架
r := gin.Default()
// 定义路由
r.GET("/api/resource", func(c *gin.Context) {
c.JSON(200, gin.H{"message": "你好 欢迎使用 air 热重载"})
})
r.Run(":8080")
}
4.1 运行
运行的时候,不需要执行 go run mian.go 这样的命令了,而是在项目的根目录地址输入 air ,项目便可以热重载自动运行。
当main.go文件发生代码更改时,air 会根据配置文件自动重新编译及重新运行。
5. 关于平滑重启
当服务关闭时,它应该停止接收新的请求,同时完成正在进行的请求,返回其响应,然后关闭。这里使用air + 捕捉信号包的方式实现平滑重启。
5.1 代码实现
// 等待中断信号来优雅关掉服务器
quit := make(chan os.Signal, 1)
// 此处不会阻塞
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
<-quit
log.Println("ShutDown Server ...")
// 创建一个120秒超时的context
ctx, cancel := context.WithTimeout(context.Background(), time.Second*120)
defer cancel()
// 120秒内优雅关闭服务, (将未处理完的请求处理完再关闭服务), 超过120秒就强制退出
if err := server.Shutdown(ctx); err != nil {
log.Fatal("shut down:", err)
}
log.Println("Server exiting success")
上述代码中,当程序被中断时,signal.Notify
将向quit
通道发送一个信号。
syscall.SIGHUP
、syscall.SIGINT
和syscall.SIGTERM
是什么意思?
syscall.SIGINT
是用来在Ctrl+C
时优雅地关闭的,它也相当于os.Interrupt
。
syscall.SIGTERM
是常用的终止信号,也是docker
容器的默认信号,Kubernetes
也使用它。
syscall.SIGHUP
用于热重载配置。
5.2 其它信号常量介绍
信号 | 值 | 动作 | 说明 |
---|---|---|---|
SIGHUP | 1 | Term | 终端控制进程结束(终端连接断开) |
SIGINT | 2 | Term | 用户发送INTR字符(Ctrl+C)触发,它也相当于os.Interrupt |
SIGQUIT | 3 | Core | 用户发送QUIT字符(Ctrl+/)触发 |
SIGILL | 4 | Core | 非法指令(程序错误、试图执行数据段、栈溢出等) |
SIGABRT | 6 | Core | 调用abort函数触发 |
SIGFPE | 8 | Core | 算术运行错误(浮点运算错误、除数为零等) |
SIGKILL | 9 | Term | 无条件结束程序(不能被捕获、阻塞或忽略) |
SIGSEGV | 11 | Core | 无效内存引用(试图访问不属于自己的内存空间、对只读内存空间进行写操作) |
SIGPIPE | 13 | Term | 消息管道损坏(FIFO/Socket通信时,管道未打开而进行写操作) |
SIGALRM | 14 | Term | 时钟定时信号 |
SIGTERM | 15 | Term | 结束程序(可以被捕获、阻塞或忽略) |
SIGUSR1 | 30,10,16 | Term | 用户保留 |
SIGUSR2 | 31,12,17 | Term | 用户保留 |
SIGCHLD | 20,17,18 | Ign | 子进程结束(由父进程接收) |
SIGCONT | 19,18,25 | Cont | 继续执行已经停止的进程(不能被阻塞) |
SIGSTOP | 17,19,23 | Stop | 停止进程(不能被捕获、阻塞或忽略) |
SIGTSTP | 18,20,24 | Stop | 停止进程(可以被捕获、阻塞或忽略) |
SIGTTIN | 21,21,26 | Stop | 后台程序从终端中读取数据时触发 |
SIGTTOU | 22,22,27 | Stop | 后台程序向终端中写数据时触发 |