Gin多服务启动加优雅停机「停机日志不丢」

424 阅读3分钟

如何通过多进程提高Gin吞吐

以及优雅停机「独立协程接收停机/终止信号」

确保最终被关闭协程的服务日志可被记录

实现

StartGinServices控制同时启动服务进程数量 传入 全局WG实例

var (
	GinInstanceWG sync.WaitGroup
)

server.StartGinServices(&GinInstanceWG, 4)
GinInstanceWG.Wait()  // 等待所有服务启动的goroutine完成
import (
	"context"
	"fmt"
	"net/http"
	"os"
	"os/signal"
	"strconv"
	"sync"
	"sync/atomic"
	"syscall"

	"time"

	"golang.org/x/sync/errgroup"
)

type ServerInstance struct {
	Http *http.Server
}

var (
	DefaultAddr       = 48080
	ServerInstances   []ServerInstance
	acceptingRequests int32 = 1 // 原子标记位,初始允许接受请求
)

服务启停管理




func startGinServices(wg *sync.WaitGroup, starInstance ...int) {
	var (
		g errgroup.Group
	)

	if len(starInstance) != 0 && starInstance[0] != 0 {

		for i := DefaultAddr; i < DefaultAddr+starInstance[0]; i++ {
			httpServer := &http.Server{
				Addr:         ":" + strconv.Itoa(i),
				Handler:      SetupRouter(),
				ReadTimeout:  300 * time.Second,
				WriteTimeout: 300 * time.Second,
			}
			ServerInstances = append(ServerInstances, ServerInstance{
				Http: httpServer,
			})

			server := httpServer
			fmt.Printf("将启动:%v个实例,当前实例监听: %v\n", starInstance[0], server.Addr)
			g.Go(func() error {
				fmt.Printf("实例:%s 启动成功\n", server.Addr)

				if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
					fmt.Printf("实例:%s 启动失败:%v\n", server.Addr, err)
					AccessLogger.Sugar().Errorf("实例:%s 启动失败:%v\n", server.Addr, err)
					return err
				}

				return nil
			})

		}

		stop := make(chan os.Signal, 1)
		signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)

		go func() {
			<-stop
			atomic.StoreInt32(&acceptingRequests, 0) // Stop accepting new requests
			for _, server := range ServerInstances {
				wg.Add(1) // 在启动服务器协程前增加
				go ShutdownGinServices(server.Http, 5*time.Second, wg)
			}
                        # 独立异步协程用来接收停止信号,并确保所有停止进程结束后,日志仍然可被记录
			wg.Wait() // 等待所有服务器关闭和日志操作完成

		}()

		// 任意协程内ListenAndServe失败,会执行至这里得到错误;或所有成功执行,得到nil
		if err := g.Wait(); err != nil {

			fmt.Println("Error waiting for servers to start:", err)

		}

		fmt.Println("所有Gin进程,shutdown完毕")
		AccessLogger.Sugar().Info("所有Gin进程,shutdown完毕")
	}

	
}
  • 通过更新全局原子标记位 acceptingRequests 来阻止新的请求被接受。这是通过 atomic.StoreInt32(&acceptingRequests, 0) 实现的,确保所有新的入站请求都会被拒绝,从而减少正在处理的请求量,为服务关闭做准备。
  • 遍历所有服务器实例,并对每个实例调用 ShutdownGinServices 函数,传递一个超时时间和 sync.WaitGroup。这个函数尝试优雅地关闭服务器,允许当前正在处理的请求在超时时间内完成。
  • 使用 wg.Add(1) 在调用 ShutdownGinServices 前递增 WaitGroup 的计数,确保主协程在所有服务器关闭操作完成前不会退出。
  • 使用 wg.Wait() 阻塞协程,直到所有的 ShutdownGinServices 操作(每个操作在完成时调用 wg.Done())都执行完毕。这确保了在程序退出前,所有服务器都已经被正确关闭,所有关键的日志操作也已完成。
func StartGinServices(wg *sync.WaitGroup, starInstance ...int) {

	startGinServices(wg, starInstance...) // 启动所有服务

}
func ShutdownGinServices(server *http.Server, timeout time.Duration, wg *sync.WaitGroup) {
	defer wg.Done() // 确保在函数退出时调用 Done

	ctx, cancel := context.WithTimeout(context.Background(), timeout)
	defer cancel()

	if err := server.Shutdown(ctx); err != nil {
		fmt.Printf("Gin实例:%v,执行shutdown失败:%v\n", server.Addr, err)
		ErrorLogger.Sugar().Infof("Gin实例:%v,执行shutdown成功", server.Addr)
	} else {
		AccessLogger.Sugar().Infof("Gin实例:%v,执行shutdown成功", server.Addr)
		fmt.Printf("Gin实例:%v,执行shutdown成功\n", server.Addr)
	}
	
}

日志

最后一个被关闭的服务进程,其操作日志不会丢失

{"level":"info","timestamp":"2024-05-11 19:03:16.329","logger":"access","msg":"Gin实例::48083,执行shutdown成功"}
{"level":"info","timestamp":"2024-05-11 19:03:16.329","logger":"access","msg":"Gin实例::48082,执行shutdown成功"}
{"level":"info","timestamp":"2024-05-11 19:03:16.329","logger":"access","msg":"Gin实例::48081,执行shutdown成功"}
{"level":"info","timestamp":"2024-05-11 19:03:16.329","logger":"access","msg":"Gin实例::48080,执行shutdown成功"}
{"level":"info","timestamp":"2024-05-11 19:03:16.329","logger":"access","msg":"所有Gin进程,shutdown完毕"}