在服务器开发的时候,往往都会有一些关于服务器关闭、服务器重启之类的问题出现。这里简单介绍了 go服务器 收到signal信号之后的关闭、重启操作 —— 有不足的地方以后补充
参考:
优雅退出在Golang中的实现 (qq.com) - 朋友的公众号,可以点点关注,会不定时的分享go的一些相关知识
Go语言WEB框架(Gin)详解
os/signal
退出
kill -9
优雅的退出
func (p *router) Run(host, port string) {
//TODO 优化zap log日志,修改成自定义的zap 的logger
//err := p.engine.Run(host + ":" + port)
//if err != nil {
// //路由启动失败,终止程序
// panic(err)
//}
//下面所有代码(go1.8+)是为了优雅处理重启等动作
srv := &http.Server{
Addr: host + ":" + port,
Handler: p.engine,
ReadTimeout: constant.ReadTimeoutT * time.Second,
WriteTimeout: constant.WriteTimeoutT * time.Second,
}
go func() {
// 监听请求
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("listen: %s\n", err)
}
}()
// 优雅Shutdown(或重启)服务
// 有一点需要注意的是:这里设置的重启/关闭条件是 收到kill信号,即程序意外终止
// 假如,我在这里用一个go携程,让它在2秒之后,panic程序,并且不进行recover恢复程序,那么panic会给进程发送一个os.Exit(2)(源码中可以看),
// 这个是会被认为 程序主动退出 ,而非被kill 也就是进程不会收到 kill信号,也就无法重启/关闭了 (虽然panic让程序崩溃了)
// 当注释掉下方go程中的defer的时候,程序直接panic,而不重启,反之则可以重启
//go func() {
// defer Recover()
//
// time.Sleep(2 * time.Second)
// panic("dangerous")
//}()
quit := make(chan os.Signal)
signal.Notify(quit, os.Interrupt) // syscall.SIGKILL
<-quit
logger.Info("Shutdown Server...")
// 10秒后,关闭context-即服务器于10秒后重启,或关闭(gin中的Context,也就是说客户端发送的一次请求,如果在10秒内没处理完,那么这个ctx将会失效,即无法在服务器重启前,成功处理客户端的请求)
//TODO 设置一个context失效时间
ctx, cancel := context.WithTimeout(context.Background(), constant.ServerRestart*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
logger.Fatalf("Server Shutdown: ", err)
}
select {
case <-ctx.Done():
}
logger.Info("Server exiting...")
}
func Recover() {
if p := recover(); p != nil {
fmt.Println("p = ", p)
}
}
完整测试代码
logger.go
封装的一个zaplog的包,可以看我另一篇博客
go zap日志库的使用,以及封装
main.go
package main
import (
"context"
"vsavefile/internal/db"
"vsavefile/internal/logger"
"vsavefile/internal/route"
"vsavefile/pkg/config"
)
type Student struct {
Age int32
Name string
}
//
// ConfigInit
// @author: xulc
// @Description: db.InitDBCli 要打印日志,所以依赖Logger的初始化
//
func ConfigInit() {
// Logger init
logger.InitLogger()
// mongodb cli init
db.InitDBCli()
}
// DBInsert test insert data into database
func DBInsert() {
s1 := Student{27, "gousheng"}
insertResult, err := db.Collection.InsertOne(context.TODO(), s1)
if err != nil {
logger.Errorf("error inserting student: %v", err)
return
}
logger.Infof("inserted a singnle document: %v", insertResult)
}
func main() {
ConfigInit()
// test for db conn and insert data
DBInsert()
defer Disconnect()
host := config.File.MustValue("http_server", "host", "127.0.0.1")
port := config.File.MustValue("http_server", "port", "8888")
route.Router.InitRoute()
route.Router.AddRoute()
route.Router.Run(host, port)
//logger.InitLogger()
//defer logger.Logger.Sync()
}
//
// Disconnect
// @author: xulc
// @Description: 执行断开,关闭操作 断开连接,关闭缓冲区
// TODO 程序panic了,defer Disconnect()能否执行?
//
func Disconnect() {
// 断开数据库连接
err := db.MongoClient.Disconnect(context.TODO())
if err != nil {
logger.Errorf("error disconnected: %v", err)
}
logger.Info("db.MongoClient.Disconnect , MongoDB is closed!")
// 刷新日志缓冲区
// Sync调用底层核心的Sync方法,刷新所有缓冲的日志条目。应用程序应该注意在退出之前调用Sync。
logger.Sync()
}
route.go
package route
import (
"context"
"fmt"
"github.com/gin-gonic/gin"
"log"
"net/http"
"os"
"os/signal"
"time"
"vsavefile/internal/constant"
"vsavefile/internal/logger"
"vsavefile/internal/middleware"
"vsavefile/internal/route/controller"
)
var Router = &router{}
type router struct {
engine *gin.Engine
}
func (p *router) InitRoute() {
p.engine = gin.Default()
//上传文件时只限制程序可以使用多少内存,而不限制上传文件的大小,即使文件大小比这更大,也会写入临时文件
p.engine.MaxMultipartMemory = 8 << 20 // 8 MiB
//p.AddRoute()
}
func (p *router) AddRoute() {
//使用SetTrustedProxies 来信任此ip代理
//router.SetTrustedProxies([]string{"192.168.1.2"})
//如下是gin框架的要求, { 左括号必须重起一行,TODO 封装
vsavefileGroup := p.engine.Group("/cloudsave")
vsavefileGroup.POST("/upload", middleware.CheckToken, controller.Upload)
vsavefileGroup.GET("/download", middleware.CheckToken, controller.Download)
//{
// vsavefileGroup.POST("/upload", func(ctx *gin.Context) {
// //upload(ctx)
// })
// vsavefileGroup.GET("/download", func(ctx *gin.Context) {
// //download(ctx)
// })
//}
}
func (p *router) Run(host, port string) {
//TODO 优化zap log日志,修改成自定义的zap 的logger
//err := p.engine.Run(host + ":" + port)
//if err != nil {
// //路由启动失败,终止程序
// panic(err)
//}
//下面所有代码(go1.8+)是为了优雅处理重启等动作
srv := &http.Server{
Addr: host + ":" + port,
Handler: p.engine,
ReadTimeout: constant.ReadTimeoutT * time.Second,
WriteTimeout: constant.WriteTimeoutT * time.Second,
}
go func() {
// 监听请求
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("listen: %s\n", err)
}
}()
// 优雅Shutdown(或重启)服务
// 有一点需要注意的是:这里设置的重启/关闭条件是 收到kill信号,即程序意外终止
// 假如,我在这里用一个go携程,让它在2秒之后,panic程序,并且不进行recover恢复程序,那么panic会给进程发送一个os.Exit(2)(源码中可以看),
// 这个是会被认为 程序主动退出 ,而非被kill 也就是进程不会收到 kill信号,也就无法重启/关闭了 (虽然panic让程序崩溃了)
// 当注释掉下方go程中的defer的时候,程序直接panic,而不重启,反之则可以重启
//go func() {
// defer Recover()
//
// time.Sleep(2 * time.Second)
// panic("dangerous")
//}()
quit := make(chan os.Signal)
signal.Notify(quit, os.Interrupt) // syscall.SIGKILL
<-quit
logger.Info("Shutdown Server...")
// 10秒后,关闭context-即服务器于10秒后重启,或关闭(gin中的Context,也就是说客户端发送的一次请求,如果在10秒内没处理完,那么这个ctx将会失效,即无法在服务器重启前,成功处理客户端的请求)
//TODO 设置一个context失效时间
ctx, cancel := context.WithTimeout(context.Background(), constant.ServerRestart*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
logger.Fatalf("Server Shutdown: ", err)
}
select {
case <-ctx.Done():
}
logger.Info("Server exiting...")
}
func Recover() {
if p := recover(); p != nil {
fmt.Println("p = ", p)
}
}
controller.go
package controller
import (
"fmt"
"github.com/gin-gonic/gin"
"vsavefile/internal/logic"
)
var ctxChanMap map[*gin.Context]chan int
func init() {
ctxChanMap = make(map[*gin.Context]chan int)
}
func Upload(ctx *gin.Context) {
fmt.Println("Uploading...")
// 测试服务器shutdown 和 restart的功能 - 测试时,设置服务器context生命时间为10秒
time.Sleep(5 * time.Second)
// 用于测试,此次请求将在5秒之后,才将结果返回给客户端,在这5秒内如果服务器关闭,也能确保结果正确返回
// 优雅的退出,在route.go中的Run中设置了
}
func Download(ctx *gin.Context) {
ctx.Writer.Write([]byte("download..."))
}
如下,捕捉到了ctrl+c对应的信号之后,执行了退出操作,重启服务的时候也可以执行退出操作。