为什么需要 graceful shutdonw
当你的程序被关闭或需要重启时,可能有些代码正在执行,如你的 Web 服务正在处理用户请求,如果直接关闭,正在处理的用户求就会失败,所以理想的处理方法是,当程序需要关闭和重启时,
- 告诉上一层(如负载均衡服务),服务马上要关闭了,不要再把新的请求路由给我了
- 把现在还没有结束的请求处理完,然后关闭和重启。
对于2, 通常会设定一个超时时间,防止某个请求处理的时间过长而导致程序无法正常关闭和关闭时间过长
基本流程
监听信号
通常的做法是监听信号量,代码如下:
package main
import (
"fmt"
"os"
"os/signal"
"syscall"
)
func main() {
sigs := make(chan os.Signal, 1)
done := make(chan bool)
signal.Notify(sigs, syscall.SIGINT)
go func() {
sig := <-sigs
fmt.Println("shutting down, caused by ", sig)
close(done)
}()
<-done
fmt.Println("Graceful shutdown.")
}
这里只监听了 interrupt
信号,当按 Ctrl-C
时,发送的就是这个信号, 也可以用通过 kill
命令来发送,如程序的PID 是 2345, 则 kill -s INT 2345
常用的信号量
1 HUP (hang up)
2 INT (interrupt)
3 QUIT (quit)
6 ABRT (abort)
9 KILL (non-catchable, non-ignorable kill)
14 ALRM (alarm clock)
15 TERM (software termination signal)
设置 timeout
在 go 里,超时处理通常通过 context
来实现
package main
import (
"context"
"fmt"
"os"
"os/signal"
"syscall"
"time"
)
const stopTimeout = time.Second * 10
func stop(done context.CancelFunc) {
time.Sleep(stopTimeout / 2)
done()
}
func main() {
sigs := make(chan os.Signal, 1)
done := make(chan bool)
signal.Notify(sigs, syscall.SIGINT)
go func() {
sig := <-sigs
fmt.Println("Shutting down, caused by ", sig)
ctx, cancel := context.WithTimeout(context.Background(), stopTimeout)
defer cancel()
go stop(cancel)
select {
case <-ctx.Done():
close(done)
}
}()
<-done
fmt.Println("Service shutdown.")
}
http 的 graceful shutdown
go 的 http
包提供了 Shutdown
方法用来实现在 Graceful Shutdown。
package main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"time"
)
const stopTimeout = time.Second * 10
func hello(w http.ResponseWriter, r *http.Request) {
time.Sleep(stopTimeout / 2)
w.WriteHeader(http.StatusOK)
}
func main() {
var srv = &http.Server{Addr: ":8080"}
sigs := make(chan os.Signal, 1)
done := make(chan bool)
signal.Notify(sigs, os.Interrupt)
http.HandleFunc("/hello", hello)
go func() {
<-sigs
ctx, cancel := context.WithTimeout(context.Background(), stopTimeout)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Println("Server shutdown with error: ", err)
}
close(done)
}()
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
log.Fatal("Http server start failed", err)
}
<-done
}
这里我们可以通过修改 hello
方法里的休眠时间来模拟比较耗时的请求。
测试方法:
启动服务后,在 Terminal 里通过 curl
来发请求,这个请求不会马上返回,因为我们在服务端设置了休眠。这个时候按 Ctrl-C
来关闭 Server.
- 当
hello
里的休眠时间小于stopTimeout
时,客户端会正常收到返回。 - 当
hello
里的休眠时间大于stopTimeout
时,客户端会无法收到正常的返回结果,会出现curl: (52) Empty reply from server
类似的错。
http.Server#Shutdown
的逻辑是:先关闭所有的 listeners
, 然后等待所有的连接(connection
)都处理完,再关闭服务。如果传入的 context
超时,则会直接返回。
文档如下:
Shutdown gracefully shuts down the server without interrupting any active connections. Shutdown works by first closing all open listeners, then closing all idle connections, and then waiting indefinitely for connections to return to idle and then shut down. If the provided context expires before the shutdown is complete, Shutdown returns the context's error, otherwise it returns any error returned from closing the Server's underlying Listener(s).
docker
当我们要把程序部署到 docker 里时,要注意 docker stop
是把主进程发送的 SIGTERM
信号,所以我们的程序也要同时监听这个信号。
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
另一点需要注意的时, docker stop
的默认等待时间是 10s
,所以我们的程序的 stopTimeout
应该要小于这个 10s
,或者通过 -t
指定 docker stop
的超时间。