轻松实现Go应用的优雅关闭:提升服务质量与稳定性

我是 LEE,老李,一个在 IT 行业摸爬滚打 17 年的技术老兵。

痛点分析

随着年底的临近,许多项目都进入了收官阶段,这也带来了代码审核和版本定档的挑战。在审查同事们的项目时,我注意到在实现容器内程序持续运行的部分,各种奇特的代码层出不穷,其中许多并不遵循容器设计原则。具体例子这里就不一一列举了,大家可以想象一下。

这种情况让我感到疑惑,似乎大家在编写代码时过于大胆。难怪在进行 Pod 重启或扩缩容时会出现各种奇怪的问题。尤其是,连基本的信号处理,如 "SIGINT/SIGTERM/SIGQUIT",都没有妥善处理,这真的让人无话可说。这种情况强调了遵守基本编程和设计原则的重要性。

到底是什么原因导致了这样的现象呢?我想原因有以下几点:

  1. 开发人员对容器设计原则的认识不足
    • 许多开发人员对容器的设计原则缺乏了解和重视。
    • 容器相关问题通常被视为运维团队的责任,而非开发过程中需要考虑的因素。
  2. 代码编写的简化态度
    • 有观点认为某些容器功能过于简单,因此不需要编写复杂的代码。
    • 这种简化态度导致代码只要能够运行,不论质量如何,都被接受。
  3. 缺乏统一标准,影响代码复用
    • 由于缺少统一的标准,开发人员往往独立实现功能。
    • 这种各自为政的做法导致代码难以在不同项目间复用。

有些人可能认为对于容器代码的编写不需要过分关注,觉得"无所谓"或"不知所措"。然而,这种想法是错误的,因为容器的运行需要确定性,且其设计原则也不能被忽视。编写这样的代码需要时间和精力,这可能会让已经忙碌的开发者感到更加繁重。作者提到自己也遇到了这个问题,于是考虑到这是许多人的共同需求,便从自己的项目中提取并重构了代码,最后将其开源,以便他人可以直接使用并复用。

在撰写这篇文章的过程中,我也调研了 GitHub 上几个主要的关于优雅关闭的项目,发现这些项目大多是随意完成的,并没有考虑太多的使用场景或遵循容器设计原则。因此决定开源,解决这一普遍问题。

预期实现目标

  • 便捷的优雅关闭功能:轻松实现应用的平滑关闭。
  • 广泛信号支持:兼容多种信号(SIGINT/SIGTERM/SIGQUIT),增强应用的响应能力。
  • 灵活的关闭选项:提供多样的关闭方法,包括关闭函数和 ctx.Context,以适应不同的应用需求。
  • 同时关闭多个对象:能够高效地处理多对象的同步关闭。
  • 定制化的超时关闭:支持设置自定义的超时时间,以优化对象的关闭过程。

介绍

G.S github.com/shengyanli1...

是一个 Go 语言编写的优雅关闭库,可以方便的实现应用优化关闭,支持多种信号和关闭方式,支持多个对象同时关闭,支持对象超时关闭。 作为我们内部基座代码中很小的一部分,它已经在我们的生产环境中使用了 2 年多,经过了充分的测试,可以放心使用。

架构设计

整个结构也非常简单,只有两个模块,一个是 TerminateSignal,一个是 WaitingUnit。 主打就是:"轻量、易用、快速"

模块介绍

1. 关闭信号 (TerminateSignal)

TerminateSignal 是一个结构体,它的主要功能就是注册需要取消的回调函数,然后等待关闭信号的到来,执行注册的回调函数。 同时 TerminateSignal 提供 Timeout 的机制,可以设置超时时间,超时之后,会强制执行注册的回调函数。也支持传入外部 ctx.Context 来与外部逻辑同步,当 ctx.Context 被取消的时候,也会强制执行注册的回调函数。

TerminateSignal 可以通过 CancelCallbacksRegistry 方法来注册需要取消的回调函数,也就是说一个 TerminateSignal 可以控制多个对象的优雅关闭。

也可以通过 GetStopCtx 方法来获取关闭信号的 Context。

2. 守护器 (WaitingUnit)

WaitingUnit 只有一个功能,等待所有的关闭信号执行完注册的关闭函数,然后退出。 它是一个阻塞函数,只有当所有的关闭信号都执行完成之后,才会返回。 WaitingUnit 通过相应进程的终结信号(SIGINT/SIGTERM/SIGQUIT)来触发所有的关闭信号执行。 属于事件驱动型的设计。

一个 WaitingUnit 可以控制多个 TerminateSignal 实例,也就是说一个 WaitingUnit 可以控制多个对象的优雅关闭。

安装

bash 复制代码
go get github.com/shengyanli1982/gs

使用举例

可以通过一个简单的例子就能知道如何使用 G.S,而且只需要简单的适配,就可以完成使用,根本不要关注 G.S 的实现细节。 它是多么的简单和易用。

下面通过 testTerminateSignaltestTerminateSignal2testTerminateSignal3 三个对象来模拟三不同的服务模块,每一个服务模块都一个名称不同的关闭方法。这个应用关闭时,需要先优雅关闭这三个服务。

举例代码

go 复制代码
package main

import (
	"fmt"
	"os"
	"time"

	"github.com/shengyanli1982/gs"
)

// simulate a service
type testTerminateSignal struct{}

func (t *testTerminateSignal) Close() {
	fmt.Println("testTerminateSignal.Close()")
}

// simulate a service
type testTerminateSignal2 struct{}

func (t *testTerminateSignal2) Shutdown() {
	fmt.Println("testTerminateSignal2.Shutdown()")
}

// simulate a service
type testTerminateSignal3 struct{}

func (t *testTerminateSignal3) Terminate() {
	fmt.Println("testTerminateSignal3.Terminate()")
}

func main() {
	// Create TerminateSignal instance
	s := gs.NewDefaultTerminateSignal()

	// create resources which want to be closed when the service is terminated
	t1 := &testTerminateSignal{}
	t2 := &testTerminateSignal2{}
	t3 := &testTerminateSignal3{}

	// Register the close method of the resource which want to be closed when the service is terminated
	s.CancelCallbacksRegistry(t1.Close, t2.Shutdown, t3.Terminate)

	// Create a goroutine to send a signal to the process after 2 seconds
	go func() {
		time.Sleep(2 * time.Second)
		p, err := os.FindProcess(os.Getpid())
		if err != nil {
			fmt.Println(err.Error())
		}
		err = p.Signal(os.Interrupt)
		if err != nil {
			fmt.Println(err.Error())
		}
	}()

	// Use WaitingForGracefulShutdown method to wait for the TerminateSignal instance to shutdown gracefully
	gs.WaitingForGracefulShutdown(s)

	fmt.Println("shutdown gracefully")
}

输出结果

bash 复制代码
# go run main.go
testTerminateSignal3.Terminate()
testTerminateSignal.Close()
testTerminateSignal2.Shutdown()
shutdown gracefully

代码解析

其核心由两个代码文件构成:terminal.gogracefull.go。这个项目的特点在于代码量相对较少,这有助于理解和维护。为了帮助读者更好地理解代码,我在代码中加入了详尽的注释,能够帮助大家对代码的工作原理和实现细节有一个清晰的理解。

在我看来真正重要的就两部分代码:

  • terminal.go 文件中的 TerminateSignal 实现

    go 复制代码
    // 注册需要取消的回调函数
    // Register the callback function to be canceled
    func (s *TerminateSignal) CancelCallbacksRegistry(callbacks ...func()) {
    	s.exec = append(s.exec, callbacks...)
    }
    
    // 获取停止信号的 Context
    // Get the Context of the stop signal
    func (s *TerminateSignal) GetStopCtx() context.Context {
    	return s.ctx
    }
    
    // Close 关闭 TerminateSignal 实例
    // Close the TerminateSignal instance
    func (s *TerminateSignal) Close(wg *sync.WaitGroup) { // wg 用于通知 WaitingUint 关闭完成 (wg is used to notify WaitingUint that the shutdown is complete)
    	s.once.Do(func() {
    		// 批量执行关闭 (Batch execution of shutdown)
    		for _, cb := range s.exec {
    			if cb != nil {
    				s.wg.Add(1)
    				go s.worker(cb) // 执行注册的关闭函数 (Execute the registered shutdown function)
    			}
    		}
    		s.cancel()  // 发送关闭信号 (Send the shutdown signal)
    		s.wg.Wait() // 等待关闭完成 (Wait for the shutdown is complete)
    		if wg != nil {
    			wg.Done() // 通知关闭完成 (Notify the shutdown is complete)
    		}
    	})
    }
    
    // worker 执行回调函数
    func (s *TerminateSignal) worker(callback func()) {
    	defer s.wg.Done() // 通知关闭完成 (Notify the shutdown is complete)
    	<-s.ctx.Done()    // 等待关闭信号 (Wait for the shutdown signal)
    	callback()        // 执行回调函数 (Execute the callback function)
    }
  • gracefull.go 文件中的 WaitingUnit 结构体

    go 复制代码
    // 等待所有关闭信号
    // Wait for all shutdown signals
    func WaitingForGracefulShutdown(sigs ...*TerminateSignal) {
    	quit := make(chan os.Signal, 1)                                       // 创建一个接收信号的通道 (Create a channel to receive signals)
    	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) // 注册要接收的信号 (Register the signals to receive)
    	<-quit                                                                // 等待接收信号 (Wait for the signal to receive)
    	signal.Stop(quit)                                                     // 停止接收信号 (Stop receiving signals)
    	close(quit)                                                           // 关闭通道 (Close the channel)
    	if len(sigs) > 0 {                                                    // 执行关闭动作 (Execute the shutdown action)
    		wg := sync.WaitGroup{}
    		wg.Add(len(sigs))
    		// 批量执行 TerminateSignal 实例的关闭动作 (Batch execution of the shutdown action of each TerminateSignal instance)
    		for _, s := range sigs {
    			go s.Close(&wg) // 执行信号的关闭动作,wg 计数器减一 (Execute the shutdown action of each signal, wg counter minus one)
    		}
    		wg.Wait()
    	}
    }

WaitForGracefulShutdown 函数是整个项目的核心,它是一个阻塞函数,用于等待所有的关闭信号。 然后执行所有注册的关闭函数。 WaitForGracefulShutdown 函数就是通过 wg 来与关联的 TerminateSignal 实例进行通信的。

TerminateSignal 实例通过 wg 来通知 WaitForGracefulShutdown 关闭完成。

总结

通过设计和实现 G.S 这个项目,用最小的代价实现了优雅关闭的功能,实现一个逻辑代码多处复用,而不需要大量的修改代码。从另外一个角度讲,也提供了一种思路,希望能够帮助到大家。从这方面来说,我觉得这个项目还是比较有意思的。同时我也希望大家可以多多支持,多多使用,多多提建议,欢迎大家给我留言。

相关推荐
爱上语文17 分钟前
Springboot的三层架构
java·开发语言·spring boot·后端·spring
serve the people20 分钟前
springboot 单独新建一个文件实时写数据,当文件大于100M时按照日期时间做文件名进行归档
java·spring boot·后端
罗政6 小时前
[附源码]超简洁个人博客网站搭建+SpringBoot+Vue前后端分离
vue.js·spring boot·后端
拾光师7 小时前
spring获取当前request
java·后端·spring
Java小白笔记9 小时前
关于使用Mybatis-Plus 自动填充功能失效问题
spring boot·后端·mybatis
JOJO___11 小时前
Spring IoC 配置类 总结
java·后端·spring·java-ee
白总Server11 小时前
MySQL在大数据场景应用
大数据·开发语言·数据库·后端·mysql·golang·php
Lingbug12 小时前
.Net日志组件之NLog的使用和配置
后端·c#·.net·.netcore
计算机学姐13 小时前
基于SpringBoot+Vue的篮球馆会员信息管理系统
java·vue.js·spring boot·后端·mysql·spring·mybatis
好兄弟给我起把狙13 小时前
[Golang] Select
开发语言·后端·golang