12 | 给应用添加优雅关停功能

提示:

在成功实现一个简单的 Web 服务器之后,接下来需要进一步完善其核心功能,使其更贴合实际需求并满足企业级应用场景的要求。例如,需要实现功能丰富的中间件、处理跨域访问、以及支持优雅关停等功能。本节课将深入介绍这些关键功能的实现,帮助我们打造一个更加健壮、高效、且易于维护的 Web 服务器。

添加优雅关停功能

Web 服务器通常都需要实现优雅关停功能。优雅关停服务器可以带来很多好处,例如提高 API 接口的成功率,减少系统脏数据的出现概率等。本节会详细介绍如何实现优雅关停功能。

优雅关停的必要性

在应用程序的生命周期中,新功能发布、缺陷修复、配置变更等操作都需要重启服务。在服务进程停止时,可能需要执行一些必要的处理工作,例如:

  • 正在执行的 HTTP 请求需要等待其完成并返回结果,否则可能会导致请求报错或产生脏数据;
  • 异步处理任务需要将缓存中的数据处理完成,否则可能会导致数据丢失或不一致;
  • 关闭数据库连接,否则数据库连接池可能会保留无效连接,浪费宝贵的连接资源。

为了解决上述问题,建议的做法是给应用添加优雅关停功能,以提高系统的健壮性。

优雅关停的实现思路

实现优雅关停的最佳实践是在服务进程停止前,等待所有任务处理完成后再退出进程。在 Go 应用开发中,可以通过向应用程序发送标准的系统信号来终止服务进程。以下是最常见的三种终止方式:

  • CTRL+C:发送 SIGINT 信号;
  • kill <pid>:向指定进程发送 SIGTERM 信号;
  • kill -9 <pid>:向指定进程发送 SIGKILL 信号,强制终止信号。需要注意的是,SIGKILL 信号既不能被应用程序捕获,也不能被阻塞或忽略。

提示:在日常关闭服务时,应尽量避免使用 kill -9 命令,因为此命令会导致应用进程无法执行优雅关停逻辑。

fastgo 实现优雅关停功能

如果能够捕获 SIGINTSIGTERM 信号,并在捕获后执行一些关停逻辑,就可以实现优雅关停功能。实际上,当前 Go 应用的优雅关停功能大多基于这一思路实现。

Go 语言提供了 os/signal 包,用于监听并处理接收到的信号。基于上述思路,我们可以通过 os/signal 实现优雅关停功能,代码如下:

go 复制代码
// 启动一些非阻塞任务,例如:HTTP / gRPC 服务,异步任务处理等逻辑

// 创建一个 os.Signal 类型的 channel,用于接收系统信号
quit := make(chan os.Signal, 1)
// 当执行 kill 命令时(不带参数),默认会发送 syscall.SIGTERM 信号
// 使用 kill -2 命令会发送 syscall.SIGINT 信号(例如按 CTRL+C 触发)
// 使用 kill -9 命令会发送 syscall.SIGKILL 信号,但 SIGKILL 信号无法被捕获,因此无需监听和处理
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
// 阻塞程序,等待从 quit channel 中接收到信号
<-quit

// 执行一些清理工作

// 程序自然退出

上述代码执行优雅关停的流程如下:

  1. 启动 HTTP/gRPC 服务或其他异步任务,以非阻塞方式运行。如果服务本身是阻塞方式,可以通过 Go 协程启动;
  2. 创建一个 os.Signal 类型的 channel,用于捕获应用程序的关停信号;
  3. 调用 signal.Notify 函数,设置需要捕获的信号类型,例如 syscall.SIGINTsyscall.SIGTERM
  4. 使用 <-quit 阻塞主程序,等待信号到来;
  5. 当系统接收到 SIGINTSIGTERM 信号时,会向 quit 通道写入一条 os.Signal 类型的数据;
  6. quit 读取到信号后,解除阻塞状态,执行后续清理工作。清理完成后,进程正常退出。清理工作可根据业务逻辑执行不同的操作,例如通过 net/http 包的 Shutdown 方法优雅关闭 HTTP 服务。

在 Go 项目开发中,还可以通过一些 Go 包,例如:fvbock/endless 来实现优雅关停,但更推荐上面的方式,简单,并且不需要引入新包。很多优秀的开源项目,例如 Kubernetes 优雅关停实现思路跟上述思路保持一致。

fastgo 根据以上优雅关停功能实现思路,实现了优雅关停逻辑,代码如下(位于 internal/apiserver/server.go 文件中):

go 复制代码
// Run 运行应用.
func (s *Server) Run() error {
    // 运行 HTTP 服务器
    // 打印一条日志,用来提示 HTTP 服务已经起来,方便排障
    slog.Info("Start to listening the incoming requests on http address", "addr", s.cfg.Addr)
    go func() {
        if err := s.srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
            slog.Error(err.Error())
            os.Exit(1)
        }
    }()

    // 创建一个 os.Signal 类型的 channel,用于接收系统信号
    quit := make(chan os.Signal, 1)
    // 当执行 kill 命令时(不带参数),默认会发送 syscall.SIGTERM 信号
    // 使用 kill -2 命令会发送 syscall.SIGINT 信号(例如按 CTRL+C 触发)
    // 使用 kill -9 命令会发送 syscall.SIGKILL 信号,但 SIGKILL 信号无法被捕获,因此无需监听和处理
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    // 阻塞程序,等待从 quit channel 中接收到信号
    <-quit

    slog.Info("Shutting down server ...")

    // 优雅关闭服务
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()

    // 先关闭依赖的服务,再关闭被依赖的服务
    // 10 秒内优雅关闭服务(将未处理完的请求处理完再关闭服务),超过 10 秒就超时退出
    if err := s.srv.Shutdown(ctx); err != nil {
        slog.Error("Insecure Server forced to shutdown", "err", err)
        return err
    }

    slog.Info("Server exited")

    return nil
}

在上述代码中,quit 收到 SIGINTSIGTERM 信号后,程序会解除阻塞状态,并调用 *http.Server类型实例的 Shutdown 方法优雅关停服务器。

通过 context.WithTimeout 创建上下文对象 ctx,其主要作用是为优雅关闭服务提供超时控制,确保服务在一定时间内完成清理工作。如果超过指定时间(这里是 10 秒),服务将被强制终止。ctx 被传递给 s.srv.Shutdown(ctx) 方法,用于通知服务相关的协程或其他子任务,当前服务正在关闭,并提供一个超时时间。服务中的任务可以通过监听 ctx.Done() 来检测是否需要终止,从而及时结束任务,避免资源泄漏。

*http.Server 类型的 Shutdown 方法的工作流程如下:首先关闭所有已开启的监听器,然后关闭所有空闲连接,最后等待所有活跃连接进入空闲状态后终止服务。如果传入的 ctx 在服务完成终止之前超时,则 Shutdown 方法会返回与 context 相关的错误。否则会返回由关闭服务监听器引发的其他错误。

Shutdown 方法被调用时,ServeListenAndServe 以及 ListenAndServeTLS 方法会立即返回 ErrServerClosed 错误。ErrServerClosed 错误被视为服务关闭时的正常行为。因此,如果 ListenAndServe 返回该错误,程序不会打印错误信息。

编译并测试优雅关停功能

执行以下命令,启动 fg-apiserver 服务:

bash 复制代码
$ ./build.sh
$ _output/fg-apiserver -c configs/fg-apiserver.yaml
time=2025-03-08T10:06:37.866+08:00 level=INFO msg="Start to listening the incoming requests on http address" addr=0.0.0.0:6666
^Ctime=2025-03-08T10:06:38.805+08:00 level=INFO msg="Shutting down server ..."
time=2025-03-08T10:06:38.806+08:00 level=INFO msg="Server exited"

在启动服务后,键入 CTRL+C,可以看到服务优雅退出日志。

相关推荐
Linguwen2 小时前
中山GEO分享会回顾文
人工智能·chatgpt
武子康2 小时前
多 Agent 不是多开几个终端:Pi 的 Sub-agent 取舍
人工智能·llm·agent
俊哥V3 小时前
每日 AI 研究简报 · 2026-08-22
人工智能·ai
龙虾PRO3 小时前
数据中台 + AI 落地:2026 五大趋势与零售出海完整实操案例
人工智能·零售
wish3663 小时前
K8S 免镜像部署:通过 API 上传文件动态创建服务(以 Ollama 为例)
人工智能·云原生·容器·kubernetes·local llm
AI导出鸭PC端3 小时前
文心怎样生成word文档?一键智能排版,AI导出鸭解决格式错乱痛点
人工智能·ai·word·豆包·deepseek·ai导出鸭
深小乐3 小时前
DeepSeek Harness 发布一周,登顶 Top10 的插件,暴露了哪些真实需求?
人工智能
小小克3 小时前
k8s集群部署的方法
云原生·容器·kubernetes
kobeyyl3 小时前
K8s集群部署核心原理与主流部署方法全解析
云原生·容器·kubernetes
奈斯先生Vector3 小时前
OpenScience 安装失败怎么办?Windows 环境、API 配置与本地服务排错指南
人工智能·windows·架构·开源·aigc·midjourney