接下来我们将讨论一个重要但经常被忽视的话题:如何安全地停止正在运行的应用程序。
目前,当我们停止API应用程序时(通常通过按Ctrl+C),它会立即终止,没有机会让正在处理的HTTP请求完成。这并不理想,原因有二:
- 这意味着客户端将无法收到对已经在处理的请求响应——他们将体验到的是HTTP连接的硬关闭。
- 我们的处理程序所执行的任何工作都可能处于未完成状态。
我们将通过向应用程序添加优雅关闭功能来缓解这些问题,以便在应用程序终止之前有机会完成正在处理的HTTP请求。
你将学习:
- 关闭信号:信号是什么,如何发送它们,以及如何在API应用程序中监听它们。
- 如何使用这些信号来使用Go的shutdown()方法触发HTTP服务器的安全关闭。
发送shutdown信号
当应用程序正在运行的时候,我们随时可以通过发送信号来终止程序。一种常用的方式,你应该在工作中已经使用过的,就是通过在键盘上按Ctrl+C键可以向应用程序发送中断信号-也称为SIGINT。
但这并非是唯一一种终止程序的方式。还有其他常用信号类型:
Singal(信号) | 描述 | 快捷键 | 可捕获 |
---|---|---|---|
SIGINT | 从键盘中断 | Ctrl+C | YES |
SIGQUIT | 从键盘退出 | Ctrl+\ | YES |
SIGKILL | 杀死程序(立即终止) | - | NO |
SIGTERM | 有序地终止进程 | - | YES |
要先解释下一些信号是可以捕捉到的,而另一些则不能。可捕获的信号可以被我们的应用程序拦截,或者被忽略,或者被用来触发某些操作(比如优雅的关闭)。其他信号,如SIGKILL,是无法捕捉和拦截的。
我们快速看一下这些信号的作用。如果你按照下面的步骤来做,继续用go run ./cmd/api命令来启动我们项目中的应用程序:
$ go run ./cmd/api
{"level":"INFO","time":"2021-12-19T01:46:23Z","message":"database connection pool established"}
{"level":"INFO","time":"2021-12-19T01:46:23Z","message":"starting server","properties":{"addr":":4000","env":"development"}}
这样做应该会在您的机器上启动一个名为api的进程。你可以使用pgrep命令来验证这个进程是否存在,如下所示:
$ pgrep -l api
4414 api
在我的例子中,我可以看到api进程正在运行,其进程ID为4414(您的进程ID很可能会不同)。一旦确认,尝试使用pkill命令向api进程发送SIGKILL信号,如下所示:
$ pkill -SIGKILL api
如果你回到应用程序启动窗口,你会发现程序已经被终止,在标准输出中打印的最后一行内容是:signal: killed。类似下面:
$ go run ./cmd/api
{"level":"INFO","time":"2021-12-19T01:46:23Z","message":"database connection pool established"}
{"level":"INFO","time":"2021-12-19T01:46:23Z","message":"starting server","properties":{"addr":":4000","env":"development"}}
signal: killed
你也可以发送其他信号尝试中断应用程序,例如pkill -SIGTERM api等。发送SIGQUIT,可以使用命令也可以通过快捷键来完成。SIGTERM信号会使应用程序退出时打印堆栈信息,如下所示:
$ go run ./cmd/api
{"level":"INFO","time":"2021-12-19T01:50:37Z","message":"database connection pool established"}
{"level":"INFO","time":"2021-12-19T01:50:37Z","message":"starting server","properties":{"addr":":4000","env":"development"}} SIGQUIT: quit
PC=0x46ebe1 m=0 sigcode=0
goroutine 0 [idle]:
runtime.futex(0x964870, 0x80, 0x0, 0x0, 0x0, 0x964720, 0x7ffd551034f8, 0x964420, 0x7ffd55103508, 0x40dcbf, ...)
/usr/local/go/src/runtime/sys_linux_amd64.s:579 +0x21 runtime.futexsleep(0x964870, 0x0, 0xffffffffffffffff)
/usr/local/go/src/runtime/os_linux.go:44 +0x46 runtime.notesleep(0x964870)
/usr/local/go/src/runtime/lock_futex.go:159 +0x9f runtime.mPark()
/usr/local/go/src/runtime/proc.go:1340 +0x39 runtime.stopm()
/usr/local/go/src/runtime/proc.go:2257 +0x92 runtime.findrunnable(0xc00002c000, 0x0)
/usr/local/go/src/runtime/proc.go:2916 +0x72e runtime.schedule()
/usr/local/go/src/runtime/proc.go:3125 +0x2d7 runtime.park_m(0xc000000180)
/usr/local/go/src/runtime/proc.go:3274 +0x9d runtime.mcall(0x0)
...
我们可以看到,这些信号在终止应用程序时是有效的——但问题是它们都导致应用程序立即退出。幸运的是,Go在os/signals包中提供了一些工具,我们可以使用这些工具拦截可捕获的信号并触发应用程序安全关闭。我们将在下一节中看看如何实现优雅关闭。