简介

在服务器开发的时候,往往都会有一些关于服务器关闭、服务器重启之类的问题出现。这里简单介绍了 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对应的信号之后,执行了退出操作,重启服务的时候也可以执行退出操作。
在这里插入图片描述