![Search icon](https://hackernoon.imgix.net/search-new.png?w=19&h=19)
您是否曾沮丧地拔掉电脑的电源线?虽然这似乎是一种快速解决方案,但它可能会导致数据丢失和系统不稳定。在软件世界中,存在一个类似的概念:硬关机。这种突然终止可能会像物理终止一样导致问题。幸运的是,有一种更好的方法:正常关机。
通过集成优雅关机,我们为服务提供提前通知。这使它能够完成正在进行的请求,可能将状态信息保存到磁盘,并最终避免关机期间数据损坏。
本指南将深入探讨正常关闭的世界,特别关注它们在 Kubernetes 上运行的Go 应用程序中的实现。
在基于 Unix 的系统中实现正常关机的关键工具之一是信号的概念,简单来说,信号是一种将特定事物从另一个进程传达给一个进程的简单方法。通过了解信号的工作原理,我们可以利用它们在我们的应用程序中实施受控终止程序,确保关机过程平稳且数据安全。
有很多信号,你可以在这里找到它们,但我们只关注关闭信号:
这些信号可以由用户(Ctrl+C / Ctrl+D)、另一个程序/进程或系统本身(内核/操作系统)发送,例如,操作系统发送SIGSEGV又称分段错误。
为了在实际环境中探索优雅关闭的世界,让我们创建一个可以进行实验的简单服务。这个“实验鼠”服务将有一个端点,通过调用 Redis 的INCR命令来模拟一些实际工作(我们将添加一点延迟)。我们还将提供一个基本的 Kubernetes 配置来测试平台如何处理终止信号。
最终目标:确保我们的服务能够正常处理关闭,而不会丢失任何请求/数据。通过将并行发送的请求数与 Redis 中的最终计数器值进行比较,我们将能够验证我们的正常关闭实现是否成功。
我们不会详细介绍如何设置 Kubernetes 集群和 Redis,但您可以在我们的 Github 存储库中找到完整的设置。
验证过程如下:
让我们从基础的 Go HTTP 服务器开始。
硬关机/main.go
package main import ( "net/http" "os" "time" "github.com/go-redis/redis" ) func main() { redisdb := redis.NewClient(&redis.Options{ Addr: os.Getenv("REDIS_ADDR"), }) server := http.Server{ Addr: ":8080", } http.HandleFunc("/incr", func(w http.ResponseWriter, r *http.Request) { go processRequest(redisdb) w.WriteHeader(http.StatusOK) }) server.ListenAndServe() } func processRequest(redisdb *redis.Client) { // simulate some business logic here time.Sleep(time.Second * 5) redisdb.Incr("counter") }
当我们使用此代码运行验证程序时,我们会发现一些请求失败,并且计数器小于 1000 (每次运行的数量可能会有所不同)。
这显然意味着我们在滚动更新期间丢失了一些数据。😢
Go 提供了一个信号包,允许您处理 Unix 信号。需要注意的是,默认情况下,SIGINT 和 SIGTERM 信号会导致 Go 程序退出。为了让我们的 Go 应用程序不会突然退出,我们需要处理传入的信号。
有两种方法可以实现此目的。
使用渠道:
c := make(chan os.Signal, 1) signal.Notify(c, syscall.SIGTERM)
使用上下文(目前首选的方法):
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM) defer stop()
当列出的信号之一到达时,当返回的stop()函数被调用时,或者当父上下文的 Done 通道关闭时(以先发生者为准), NotifyContext将返回标记为完成(其 Done 通道已关闭)的父上下文的副本。
我们当前的 HTTP 服务器实现存在一些问题:
我们重写一下。
优雅关机/main.go
package main // imports var wg sync.WaitGroup func main() { ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM) defer stop() // redisdb, server http.HandleFunc("/incr", func(w http.ResponseWriter, r *http.Request) { wg.Add(1) go processRequest(redisdb) w.WriteHeader(http.StatusOK) }) // make it a goroutine go server.ListenAndServe() // listen for the interrupt signal <-ctx.Done() // stop the server if err := server.Shutdown(context.Background()); err != nil { log.Fatalf("could not shutdown: %v\n", err) } // wait for all goroutines to finish wg.Wait() // close redis connection redisdb.Close() os.Exit(0) } func processRequest(redisdb *redis.Client) { defer wg.Done() // simulate some business logic here time.Sleep(time.Second * 5) redisdb.Incr("counter") }
以下是更新摘要:
现在,如果我们重复验证过程,我们将看到所有 1000 个请求都得到正确处理。🎉
Echo、Gin、Fiber 等框架将为每个传入请求生成一个 goroutine,为其提供上下文,然后根据您决定的路由调用您的函数/处理程序。在我们的例子中,它将是为“/incr”路径提供给 HandleFunc 的匿名函数。
当你拦截SIGTERM信号并要求你的框架正常关闭时,会发生两件重要的事情(简单来说):
注意:一旦 Kubernetes 将您的 pod 标记为终止,它也会停止将来自负载均衡器的传入流量引导到您的 pod。
终止进程可能很复杂,尤其是涉及许多步骤(例如关闭连接)时。为确保进程顺利运行,您可以设置超时。此超时可充当安全网,如果进程花费的时间比预期的要长,则可以正常退出。
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() go func() { if err := server.Shutdown(shutdownCtx); err != nil { log.Fatalf("could not shutdown: %v\n", err) } }() select { case <-shutdownCtx.Done(): if shutdownCtx.Err() == context.DeadlineExceeded { log.Fatalln("timeout exceeded, forcing shutdown") } os.Exit(0) }
由于我们使用 Kubernetes 来部署服务,因此让我们深入了解它如何终止 Pod。一旦 Kubernetes 决定终止 Pod,将发生以下事件:
如您所见,如果您有一个长时间运行的终止过程,则可能需要增加terminationGracePeriodSeconds设置,以便您的应用程序有足够的时间正常关闭。
优雅关闭可保护数据完整性、保持无缝的用户体验并优化资源管理。凭借其丰富的标准库和对并发性的重视,Go 使开发人员能够轻松集成优雅关闭实践 - 这是在 Kubernetes 等容器化或编排环境中部署应用程序的必需品。
您可以在我们的 Github 存储库中找到 Go 代码和 Kubernetes 清单。