在Golang应用程序中优雅地处理服务器关闭问题

291 阅读2分钟

在这个例子中,我将向你展示如何在Go应用程序中优雅地处理HTTP服务器的关闭。这里的重点是在关闭服务器之前等待一定的时间,而不是直接关闭所有的 "实时 "请求/连接。阅读代码注释以了解行和函数的具体作用。有不同的方法来处理优雅的服务器关闭,所以这只是其中的一个例子,主要是基于GolangShutdown文档。我只是保留了必要的代码,以尽可能保持这个例子的相关性。

结构

.
├── cmd
│   └── client
│       └── main.go
├── go.mod
└── internal
    ├── app
    │   ├── client.go
    │   ├── handler.go
    │   └── server.go
    └── user
        └── get.go

逻辑

一个 "已知 "的关机信号发生了。例如:Ctrl-C,kill PID,docker stopdocker down 。如果当时没有 "活 "的请求/连接,则阻止新的请求进入,并关闭服务器,无需等待10 秒。然而,如果有一个或多个 "活 "的请求/连接,则阻止新的请求进入,并允许他们在关闭之前完成他们正在做的事情,10 秒。如果任何 "活着 "的请求/连接超过了10 秒,那么它就会被杀死。

文件

整个代码可以保存在一个文件中,也可以用不同的方式组织,所以由你来改进/重构它。

main.go

package main

import (
	"context"
	"log"
	"net/http"
	"os"
	"os/signal"
	"syscall"
	"time"

	"internal/app"
)

func main() {
	// Create the application instance.
	application := app.App{
		Server: app.Server(app.Handler()),
	}

	// idleChan channel is dedicated for shutting down all active connections.
	// Once actual shutdown occurred by closing this channel, the main goroutine
	// is shutdown.
	idleChan := make(chan struct{})

	go func() {
		// signChan channel is used to transmit signal notifications.
		signChan := make(chan os.Signal, 1)
		// Catch and relay certain signal(s) to signChan channel.
		signal.Notify(signChan, os.Interrupt, syscall.SIGTERM)
		// Blocking until a signal is sent over signChan channel.
		sig := <-signChan

		log.Println("shutdown:", sig)

		// Create a new context with a timeout duration. It helps allowing
		// timeout duration to all active connections in order for them to
		// finish their job. Any connections that wont complete within the
		// allowed timeout duration gets halted.
		ctx, cancel := context.WithTimeout(context.Background(), 10 * time.Second)
		defer cancel()

		if err := application.Stop(ctx); err == context.DeadlineExceeded {
			log.Println("shutdown: halted active connections")
		}

		// Actual shutdown trigger.
		close(idleChan)
	}()

	if err := application.Start(); err == http.ErrServerClosed {
		log.Println("shutdown: started")
	}

	// Blocking until the shutdown to complete then inform the main goroutine.
	<-idleChan

	log.Println("shutdown: completed")
}

client.go

package app

import (
	"context"
	"net/http"
)

type App struct {
	Server *http.Server
}

func (a App) Start() error {
	return a.Server.ListenAndServe()
}

func (a App) Stop(ctx context.Context) error {
	return a.Server.Shutdown(ctx)
}

handler.go

package app

import (
	"net/http"

	"internal/user"
)

func Handler() http.Handler {
	handler := http.NewServeMux()
	handler.HandleFunc("/client/api/v1/users", user.Get)

	return handler
}

server.go

package app

import (
	"net/http"
)

func Server(handler http.Handler) *http.Server {
	return &http.Server{
		Addr: ":8080",
		Handler: handler,
	}
}

get.go

package user

import (
	"fmt"
	"net/http"
	"time"
)

func Get(w http.ResponseWriter, _ *http.Request) {
	time.Sleep(15 * time.Second)

	_, _ = w.Write([]byte(fmt.Sprintf("Response: %d", 1)))
}

测试

当有实时连接时:

^C
2020/03/22 20:54:59 shutdown: interrupt
2020/03/22 20:54:59 shutdown: started
2020/03/22 20:55:04 shutdown: halted active connections
2020/03/22 20:55:04 shutdown: completed

当没有任何实时连接时:

^C
2020/03/22 20:54:34 shutdown: interrupt
2020/03/22 20:54:34 shutdown: started
2020/03/22 20:54:39 shutdown: completed