Go语言内置的os/signal包提供了系统信号监听能力,结合goroutine和channel机制,我们可以优雅地捕获系统信号,进而实现服务的平滑重启和配置热加载功能,避免服务重启时请求中断或者需要手动重启服务加载新配置的问题。

常见系统信号说明
在Linux系统中,常用的信号和对应含义如下:
- SIGINT:终端中断信号,通常是用户按下Ctrl+C触发,一般用于通知程序优雅退出
- SIGHUP:终端挂断信号,也可以自定义为触发配置热加载的信号
- SIGTERM:终止信号,是kill命令默认发送的信号,用于请求程序终止运行
监听系统信号的基础实现
首先我们需要实现基础的信号监听逻辑,通过创建一个channel接收信号,然后启动goroutine监听信号并做对应处理。
package main
import (
"fmt"
"os"
"os/signal"
"syscall"
)
func main() {
// 创建接收信号的channel
sigChan := make(chan os.Signal, 1)
// 监听SIGINT、SIGHUP、SIGTERM信号
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGHUP, syscall.SIGTERM)
fmt.Println("服务启动,等待信号...")
// 阻塞等待信号
sig := <-sigChan
fmt.Printf("接收到信号: %vn", sig)
}
实现配置热加载
配置热加载的逻辑是当程序接收到SIGHUP信号时,重新读取配置文件并加载到程序中,不需要重启服务。下面是完整的实现示例:
package main
import (
"encoding/json"
"fmt"
"os"
"os/signal"
"sync"
"syscall"
"time"
)
// 全局配置结构体
type Config struct {
Port int `json:"port"`
AppName string `json:"app_name"`
}
var (
config *Config
configLock sync.RWMutex
)
// 加载配置文件
func loadConfig() error {
// 这里读取当前目录下的config.json,实际可以根据需求调整路径
file, err := os.ReadFile("config.json")
if err != nil {
return err
}
tmpConfig := &Config{}
if err := json.Unmarshal(file, tmpConfig); err != nil {
return err
}
configLock.Lock()
config = tmpConfig
configLock.Unlock()
fmt.Println("配置加载成功")
return nil
}
// 获取当前配置
func getConfig() *Config {
configLock.RLock()
defer configLock.RUnlock()
return config
}
func main() {
// 初始化配置
if err := loadConfig(); err != nil {
fmt.Printf("初始配置加载失败: %vn", err)
return
}
// 创建信号监听channel
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGHUP, syscall.SIGTERM)
fmt.Printf("服务启动,当前配置: %+vn", getConfig())
fmt.Println("发送SIGHUP信号可触发配置热加载,发送SIGINT或SIGTERM可停止服务")
// 启动业务循环,模拟服务运行
go func() {
for {
cfg := getConfig()
fmt.Printf("[%s] 服务运行中,监听端口: %dn", time.Now().Format("2006-01-02 15:04:05"), cfg.Port)
time.Sleep(3 * time.Second)
}
}()
// 处理信号
for sig := range sigChan {
switch sig {
case syscall.SIGHUP:
// 接收到SIGHUP信号,重新加载配置
fmt.Println("接收到SIGHUP信号,开始热加载配置...")
if err := loadConfig(); err != nil {
fmt.Printf("配置热加载失败: %vn", err)
}
case syscall.SIGINT, syscall.SIGTERM:
// 接收到终止信号,优雅退出
fmt.Println("接收到终止信号,准备退出服务...")
// 这里可以添加资源清理逻辑
os.Exit(0)
}
}
}
对应的测试配置文件config.json内容如下:
{
"port": 8080,
"app_name": "test_service"
}
实现平滑重启
平滑重启的核心是当接收到重启信号时,先启动新的服务进程,等待新进程启动完成后再关闭旧进程,避免请求中断。Go语言可以通过fork新进程的方式实现,下面是简化版的平滑重启示例:
package main
import (
"fmt"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
var server *http.Server
func main() {
// 创建HTTP服务
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "当前服务进程ID: %d", os.Getpid())
})
server = &http.Server{
Addr: ":8080",
Handler: mux,
}
// 启动HTTP服务
go func() {
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
fmt.Printf("服务启动失败: %vn", err)
}
}()
fmt.Printf("服务启动成功,进程ID: %d,监听端口: 8080n", os.Getpid())
// 监听信号
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGHUP, syscall.SIGTERM)
for sig := range sigChan {
switch sig {
case syscall.SIGHUP:
// 接收到SIGHUP信号,执行平滑重启
fmt.Println("接收到平滑重启信号,开始重启...")
// 获取当前可执行文件路径
execPath, err := os.Executable()
if err != nil {
fmt.Printf("获取可执行文件路径失败: %vn", err)
continue
}
// fork新进程
proc, err := os.StartProcess(execPath, []string{execPath}, &os.ProcAttr{
Files: []*os.File{os.Stdin, os.Stdout, os.Stderr},
})
if err != nil {
fmt.Printf("启动新进程失败: %vn", err)
continue
}
fmt.Printf("新进程启动成功,进程ID: %d,等待新进程就绪后关闭旧进程n", proc.Pid)
// 等待1秒,模拟新进程启动就绪
time.Sleep(1 * time.Second)
// 关闭旧进程的HTTP服务
if err := server.Close(); err != nil {
fmt.Printf("关闭旧服务失败: %vn", err)
}
fmt.Println("旧服务已关闭,平滑重启完成")
return
case syscall.SIGINT, syscall.SIGTERM:
fmt.Println("接收到终止信号,关闭服务...")
server.Close()
return
}
}
}
注意事项
- 信号监听的channel建议设置缓冲,避免信号发送时阻塞
- 配置热加载时要注意并发安全,使用读写锁保护配置数据的读写
- 平滑重启时需要根据实际业务调整新进程的等待时间,确保新进程已经可以正常处理请求再关闭旧进程
- 如果是Windows系统,部分信号可能不支持,需要做系统适配