Writing an HTTP service in Go is easy, but getting a running service to exit safely is not so straightforward.
If you are new to the term graceful shutdown, it refers to an HTTP service that stops accepting new requests after receiving a user’s exit command, and then actively exits after processing and responding to the batch of requests it is currently processing. Unlike SIGKILL
(kill -9
or force stop
), a safe exit minimizes service jitter as the program rolls over for updates.
The user’s exit command is typically SIGTERM
(k8s implementation) or SIGINT
(often corresponding to bash’s Ctrl + C
).
Listening for signals
Use the standard library signal to complete the signal listening, small field
It is worth noting that when signal.Notify()
is not used, Go has a default set of signal handling rules, such as SIGHUP
, SIGINT
or SIGTERM
will cause the program to exit directly.
Stop HTTP service
Calling the Shutdown() method of a running Server instance allows the service to be safely exited.
|
|
GracefulServer
has this usage.
|
|
Some readers may ask at this point, why do we need to encapsulate a layer of Server.ListenAndServe()
it, plus shutdownFinished
where the meaning of this channel? Don’t worry, Server.Shutdown()
’s documentation has a sentence.
When Shutdown is called, Serve, ListenAndServe, and ListenAndServeTLS immediately return ErrServerClosed. Make sure the program doesn ’t exit and waits instead for Shutdown to return.
ListenAndServe()returns immediately after
Shutdown()is called, and if we put
ListenAndServe()in the main function
main(), the main function will exit soon after. In Go, no matter what the state of the other goroutines is at this point, [main function exit exits the entire program](https://golang.org/ref/spec#Program_execution), so we can't safely ensure that
Server.Shutdown()is finished. So,
shutdownFinished` is placed here to provide protection.
Experienced readers may ask at this point, why not write the Server.ListenAndServe()
call to the goroutine and the signal listening and Server.Shutdown()
to the main function main()
? Like this example written in this way, but also do not need to wrap another layer of Server, is not beautiful?
Here is a matter of opinion, ListenAndServe()
in the goroutine, the error handling probability is log.Fatal(err)
such an operation, if the service is not the initiative to exit (such as the start immediately encountered the port occupation error), the main function main()
in the defer is not executed. I’ve used some extra complexity here to round out the safe exit logic a bit.
If you’re interested, I’ve put a copy of the full implementation on Github, so you can compile it and request another simultaneous exit for yourself to experience.
Subtle API
In fact, the use of shutdownFinished
to ensure that Server.Shutdown()
is finished is something I only recently realized. For about two years, I had been ignoring the fact that Server.ListenAndServe()
would return immediately after Server.Shutdown()
started executing, and had been using the incorrect implementation Gracefully-Shutdown-Done-Right/blob/master/wrong-way/main.go). And due to some chance coincidence, this buggy implementation has a great probability to exit correctly and safely if there are no HTTP requests being processed at the time of exit. _(:зゝ∠)_
In my personal opinion, Server.Shutdown()
is an easy API to misuse.
Server.Shutdown()
was discussed in 2013 and finally introduced in Go 1.8 in 2016. And the unexpected feature in the documentation we mentioned above stating that Server.ListenAndServe()
would return immediately after Server.Shutdown()
started executing was specifically added to the documentation six months later +/37161/), so maybe I’m not the only victim of this subtle and easy to accidentally misuse API. ``Later
Designing and adding external APIs is a really hard thing to do. The Go Team has recently been discussing how to make the standard library support generics, and I wish them the best of luck.