我是 LEE,老李,一个在 IT 行业摸爬滚打 17 年的技术老兵。
五一放假期间,本打算好好休息,却被一个频繁报警的问题困扰。我们是电商平台,一个部署在我们 Serverless
环境中的 KA
商户,由于商户在配置压力阈值时不当,导致商户核心负载在节日期间频繁扩缩容。
尽管我们设置了 5 分钟的缩容抑制,仍无法阻止请求像海浪一样不断涌向核心负载的 Pod
。这个 Pod
的扩缩容频率变化太快了,有时是 10 个,有时是 50 个,有时是 20 个,有时是 70 个。商户在五一期间经常出现订单提交失败的情况。
因此,我在五一假期最后一天被迫加班。节后第一天,我被大领导召见(兄弟们,我渡劫去了),说我在假期期间未能保障商户服务的稳定性,导致商户在节日期间出现订单提交失败。
最终,在 5 月 6 日下午的公司复盘会议上,我与业务负责人一起分析了日志和监控数据。我们发现商户的核心负载扩缩容频率变化太快,导致商户在休假期间经常出现订单提交失败的情况。最终,我们发现商户的底层业务代码存在问题。当 Pod
关闭时,没有优先关闭 Http
服务器,导致数据库连接和其他外部调用接口关闭时,仍有外部请求进入服务内部,导致商户在休假期间经常出现订单提交失败的情况。
即使你的代码和业务系统平时表现出色,但一个细节问题可能导致故障,让人认为你没有考虑到细节。尽管我们已经在关闭 Pod
时添加了优雅停机操作,但服务内部模块的关闭顺序可能会引发问题。如果不妥善处理,最终还是会出现故障。这也引出了今天的故事:优雅关闭。
举个优雅关闭的例子
下面是一个简单的 Go
语言程序,它监听系统信号,当接收到 SIGINT
或 SIGTERM
信号时,输出 Shutting down...
并退出程序。
package main
import (
"fmt"
"os"
"os/signal"
"syscall"
)
func main() {
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-c
fmt.Println("Shutting down...")
// 执行关闭流程
// 步骤1: ...
// 步骤2: ...
// 步骤3: ...
os.Exit(0)
}()
fmt.Println("Server is running...")
select {}
}
我想问问朋友们,有多少人曾经写过类似的代码?也许只是一个无限循环或者一个服务绑定在某个端口上,等待着被强制终止。这样的做法非常危险。当生产环境出现问题时,你会发现自己面临的挑战和被追责的风险。
事件背景
也许对于许多研发小伙伴来说,刚才的问题是日常工作中的一部分。但对我来说,这是一个巨大的挑战。作为 Serverless
平台的负责人,我负责确保平台的稳定性和性能。同时,我也是一个研发人员,负责维护多个底层框架和库。通过这次事件,我发现大多数研发小伙伴在关闭服务时都存在偷懒或完全忽视的情况,这是非常危险的。
经过认真总结,我发现如果研发小伙伴在关闭服务时考虑到业务服务模块的关闭顺序,这个问题就不会发生。说是这样说,实际情况是研发小伙伴并不会考虑到这个问题。即便有人有心要做这个事情,但由于业务模块的复杂性,很难保证每次都能正确地关闭服务内部的模块。
回顾过去,我有一个开源项目叫做 GS
,它是一个提供简单易用的优雅关闭 Go
语言库。它可以帮助开发者在关闭服务时优雅地关闭服务内部的模块。在向我们的研发小伙伴介绍这个库时,小伙伴反馈 GS
这个项目虽然有特色,可以帮助我们解决一定问题,但在这次事件中,因为 GS
内部结构单一,没有办法真正解决这个问题。
通过这次事件,我计划优化和调整 GS
项目,提供一定的关闭控制逻辑,帮助研发小伙伴更好地关闭服务内部的模块。同时,我也希望通过这个项目,帮助更多的研发小伙伴解决这个问题。
另外,如果有小伙伴对 Pod
关闭过程有更深入的了解,可以阅读我之前写的文章:详细解读 Kubernetes 中 Pod 优雅退出,帮你解决大问题...
痛点分析
在之前提到的优雅关闭例子中,作为一个研发人员,你可能会认为这样的代码已经足够了。
go func() {
<-c
fmt.Println("Shutting down...")
// 执行关闭流程
// 步骤1: ...
// 步骤2: ...
// 步骤3: ...
os.Exit(0)
}()
但是,当你的服务内部有多个模块时,你可能会发现这样的代码并不够用。其中一个关键的原因是:你每一次都要写一次编排逻辑,真正关闭的代码可能就那么几行,但编排逻辑可能会有很多行,最终引入了许多不确定性。用一个 BUG
来解决另一个 BUG
。 用更通俗的话说,我不可能每次都写一遍关闭逻辑,这样的代码太难维护了。而且在以后的迭代中,这么多不同模块的代码交叠在一起,难保不会出现问题。
即便我们日常开发中采用更好的开发习惯,在最后一步仍然面临如何编排关闭的任务,如下:
type Service1 struct {
// ...
}
func (s *Service1) Close() {
// 关闭流程
// 步骤1: ...
// 步骤2: ...
// 步骤3: ...
}
type Service2 struct {
// ...
}
func (s *Service2) Close() {
// 关闭流程
// 步骤1: ...
// 步骤2: ...
// 步骤3: ...
}
type Service3 struct {
// ...
}
func (s *Service3) Close() {
// 关闭流程
// 步骤1: ...
// 步骤2: ...
// 步骤3: ...
}
func main() {
service1 := &Service1{}
service2 := &Service2{}
service3 := &Service3{}
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-c
fmt.Println("Shutting down...")
// 关闭服务,顺序正确吗?
service1.Close()
service2.Close()
service3.Close()
os.Exit(0)
}()
fmt.Println("Server is running...")
select {}
}
从上面的代码中,可以详细看到在关闭部分,如果按照代码中的编写方式,关闭的顺序被明确指定了。我个人觉得痛点有如下几个方面:
- 代码重复:每次都要编写关闭逻辑,维护困难。
- 顺序编排难:关闭服务时难以正确安排模块关闭顺序。
- 种类单一:无法同时关闭多个服务。
- 无法控制:无法准确控制模块关闭顺序,可能导致错误。
- 无法扩展:关闭逻辑无法扩展,可能导致模块未完全关闭。
显然,这不是我们想要的,我们需要一个更好的方式来解决这个问题。
预期的关闭
说了这么多,不如来一个明确的预期,因为真正的预期才是我们想要的。
全部异步关闭
此时 service1
、service2
和 service3
都是异步关闭的,也就是说 asyncClose()
函数异步调用各个服务的 Close()
方法,最后有一个 asyncWait()
函数等待所有服务关闭完成。
func main() {
service1 := &Service1{}
service2 := &Service2{}
service3 := &Service3{}
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGINT, syscall.SIGTERM
)
go func() {
<-c
fmt.Println("Shutting down...")
asyncClose(service1.Close())
asyncClose(service2.Close())
asyncClose(service3.Close())
asyncWait()
os.Exit(0)
}()
fmt.Println("Server is running...")
select {}
}
全部严格同步
此时 service1
、service2
和 service3
都是严格同步关闭的,也就是说 syncClose()
函数同步调用各个服务的 Close()
方法,最后有一个 syncWait()
函数等待所有服务关闭完成。
func main() {
service1 := &Service1{}
service2 := &Service2{}
service3 := &Service3{}
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-c
fmt.Println("Shutting down...")
syncClose(service1.Close())
syncClose(service2.Close())
syncClose(service3.Close())
syncWait()
os.Exit(0)
}()
fmt.Println("Server is running...")
select {}
}
混合异步和同步
此时 service1
和 service2
是异步关闭的,service3
是严格同步关闭的,也就是说 asyncClose()
函数异步调用 service1
和 service2
的 Close()
方法,syncClose()
函数同步调用 service3
的 Close()
方法,最后有一个 asyncWait()
函数等待 service1
和 service2
关闭完成,有一个 syncWait()
函数等待 service3
关闭完成。 此时 service3
的关闭在 service1
和 service2
关闭完成后才会开始。
func main() {
service1 := &Service1{}
service2 := &Service2{}
service3 := &Service3{}
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-c
fmt.Println("Shutting down...")
asyncClose(service1.Close())
asyncClose(service2.Close())
asyncWait()
syncClose(service3.Close())
syncWait()
os.Exit(0)
}()
fmt.Println("Server is running...")
select {}
}
当然,以上的代码都只是示例,仅用于阐述问题。实际的代码可能会更复杂,涉及更多的编排模型。然而,这些示例代码确实可以帮助我们更好地理解服务内部模块的关闭顺序,从而更有效地关闭服务内部模块,起到抛砖引玉的作用。
预期效果
在原有的 GS
项目代码基础上,我们计划优化代码结构,完善关闭控制逻辑。我们将基于 TerminateSignal
信号的方式来组织服务内部模块的关闭顺序,并提供更多的关闭控制方式,以帮助研发小伙伴更有效地关闭服务内部模块。
改造之前的代码
// Close 是一个方法,它关闭 TerminateSignal 实例,并执行所有注册的回调函数。
// Close is a method that closes the TerminateSignal instance and executes all registered callback functions.
func (s *TerminateSignal) Close(wg *sync.WaitGroup) {
// 使用 sync.Once 确保 Close 方法只被执行一次。
// Use sync.Once to ensure that the Close method is only executed once.
s.once.Do(func() {
// 遍历并执行所有注册的回调函数。
// Iterate over and execute all registered callback functions.
for _, callback := range s.exec {
if callback != nil {
s.wg.Add(1)
// 在新的 goroutine 中执行回调函数,以实现并发。
// Execute the callback function in a new goroutine to achieve concurrency.
go s.worker(callback)
}
}
// 发送取消信号,使得所有使用该 Context 的 goroutine 都可以接收到取消信号。
// Send the cancel signal so that all goroutines using this Context can receive the cancel signal.
s.cancel()
// 等待所有的回调函数都执行完毕。
// Wait for all callback functions to finish executing.
s.wg.Wait()
// 如果 wg 不为 nil,那么调用 wg.Done() 方法,表示一个操作已经完成。
// If wg is not nil, call the wg.Done() method to indicate that an operation has been completed.
if wg != nil {
wg.Done()
}
})
}
改造之后的代码
// close 关闭 TerminateSignal 实例
// close the TerminateSignal instance
func (s *TerminateSignal) close(closeMode CloseType, wg *sync.WaitGroup) {
// 使用 sync.Once 确保 Close 只被执行一次
// Use sync.Once to ensure Close is only executed once
s.once.Do(func() {
// 将 closed 的值设置为 true,表示 TerminateSignal 已经关闭
// Set the value of closed to true, indicating that the TerminateSignal is closed
s.closed.Store(true)
// 遍历所有的回调函数
// Iterate over all callback functions
for _, fn := range s.handles {
// 如果回调函数不为空
// If the callback function is not null
if fn != nil {
// 增加等待组的计数,表示有一个新的任务需要等待完成
// Increase the count of the wait group, indicating that there is a new task to wait for completion
s.wg.Add(1)
// 根据关闭模式进行不同的处理
// Handle differently according to the close mode
switch closeMode {
// ASyncClose 表示异步关闭
// ASyncClose indicates asynchronous close
case ASyncClose:
// 在新的 goroutine 中执行 worker 函数,这样可以并发执行多个任务
// Execute the worker function in a new goroutine, so that multiple tasks can be executed concurrently
go s.worker(fn)
// SyncClose 表示同步关闭
// SyncClose indicates synchronous close
case SyncClose:
// 在当前 goroutine 中执行 worker 函数,这样可以保证任务按顺序执行
// Execute the worker function in the current goroutine, so that tasks can be executed in order
s.worker(fn)
}
}
}
// 取消 context
// Cancel the context
s.cancel()
// 等待所有的 worker 完成
// Wait for all workers to complete
s.wg.Wait()
// 如果外部的等待组不为空,调用 Done 方法
// If the external wait group is not null, call the Done method
if wg != nil {
wg.Done()
}
})
}
// Close 方法异步关闭 TerminateSignal 实例
// The Close method asynchronously closes the TerminateSignal instance
func (s *TerminateSignal) Close(wg *sync.WaitGroup) {
// 调用 close 方法,传入 ASyncClose 作为关闭模式和 wg 作为等待组
// Call the close method, passing in ASyncClose as the close mode and wg as the wait group
s.close(ASyncClose, wg)
}
// SyncClose 方法同步关闭 TerminateSignal 实例
// The SyncClose method synchronously closes the TerminateSignal instance
func (s *TerminateSignal) SyncClose(wg *sync.WaitGroup) {
// 调用 close 方法,传入 SyncClose 作为关闭模式和 wg 作为等待组
// Call the close method, passing in SyncClose as the close mode and wg as the wait group
s.close(SyncClose, wg)
}
通过对 Close
函数的重构,我们引入了 CloseType
类型,用于区分异步关闭和同步关闭。在各自的 case
中,我们可以根据不同的关闭模式执行相应的关闭逻辑。这样,我们就能更好地控制服务内部模块的关闭顺序,提供更多的关闭控制方式,帮助开发者更好地关闭服务内部模块。例如:ASyncClose
表示异步关闭,SyncClose
表示同步关闭。
思考过程
处理单个 TerminateSignal
信号时,操作相对简单。只需调用 Close
或 SyncClose
方法,等待所有回调函数执行完毕,然后就可以退出程序。
然而,我提供了 TerminateSignal
信号这样的数据结构,主要是为了能够处理多个 TerminateSignal
信号。在处理多个 TerminateSignal
信号时,我们需要考虑到这些信号的关闭顺序。这时,我们就需要根据不同的关闭模式,执行不同的关闭逻辑。
在思考过程中,我发现实际关闭任务存在一个排列组合的问题:
- 信号间:同步,信号内:同步
- 信号间:同步,信号内:异步
- 信号间:异步,信号内:同步
- 信号间:异步,信号内:异步
实际上,第 2 种和第 3 种情况的实际效果是一样的,所以
我们只需要考虑第 1,2 和 4 种情况。
在 GS
项目中,等待函数 WaitXXX
实际上是对若干 TerminateSignal
信号进行控制,因此在不同的 TerminateSignal
信号之间也存在不同的关闭逻辑:同步与异步。这就间接地引入了第三种 CloseType
类型:ForceSyncClose
,表示强制同步关闭。
总结下 GS
提供的关闭类型,我们需要考虑的关闭模式有:
ASyncClose
表示异步关闭,解决信号间:异步,信号内:异步的问题。SyncClose
表示同步关闭,解决信号间:异步,信号内:同步的问题。ForceSyncClose
表示强制同步关闭,解决信号间:同步,信号内:异步的问题。
至此,我们就可以更好地控制服务内部模块的关闭顺序,提供更多的关闭控制方式。当然,这只是一个开始,我们还可以继续优化和调整 GS
项目,提供更多的关闭控制逻辑,帮助开发者更好地关闭服务内部模块。
解决方案
在 GS
项目中,我们引入了 CloseType
类型,以区分异步关闭和同步关闭。在各自的 case
中,我们可以根据不同的关闭模式执行相应的关闭逻辑。
gracefull.go
// 定义了三种关闭类型:异步关闭、同步关闭和强制同步关闭
// Three types of closure are defined: asynchronous closure, synchronous closure, and forced synchronous closure
const (
// ASyncClose 表示异步关闭,即在新的 goroutine 中执行关闭操作
// ASyncClose represents asynchronous closure, i.e., the closure operation is performed in a new goroutine
ASyncClose CloseType = iota
// SyncClose 表示同步关闭,即在不同的 TerminateSignal 中同步执行关闭操作, eg: t1.Close() then t2.Close() then t3.Close()
// 在每个 TerminateSignal 中,是异步执行的
// SyncClose represents synchronous closure, i.e., the closure operation is performed synchronously in different TerminateSignal, eg: t1.Close() then t2.Close() then t3.Close()
// In each TerminateSignal, it is asynchronous
SyncClose
// ForceSyncClose 表示强制同步关闭,即在不同的 TerminateSignal 中同步执行关闭操作, eg: t1.Close() then t2.Close() then t3.Close()
// 在每个 TerminateSignal 中,是完全同步执行的
// ForceSyncClose represents forced synchronous closure, i.e., the closure operation is performed synchronously in different TerminateSignal, eg: t1.Close() then t2.Close() then t3.Close()
// In each TerminateSignal, it is completely synchronous
ForceSyncClose
)
// waiting 函数用于等待系统信号,并根据关闭模式和 TerminateSignal 进行不同的处理
// The waiting function waits for system signals and handles them differently according to the close mode and TerminateSignal
func waiting(mode CloseType, sigs ...*TerminateSignal) {
// 创建一个 os.Signal 类型的通道,用于接收系统信号
// Create a channel of type os.Signal to receive system signals
quit := make(chan os.Signal, 1)
// 注册我们关心的系统信号,当这些信号发生时,会发送到 quit 通道
// Register the system signals we care about, when these signals occur, they will be sent to the quit channel
signal.Notify(quit, syscall.SIGINT, syscall.SIGINT, syscall.SIGQUIT)
// 阻塞等待任何系统信号
// Block and wait for any system signal
<-quit
// 停止接收更多的系统信号
// Stop receiving more system signals
signal.Stop(quit)
// 关闭 quit 通道
// Close the quit channel
close(quit)
// 如果有提供 TerminateSignal,那么就等待它们全部关闭
// If TerminateSignal is provided, then wait for all of them to close
if len(sigs) > 0 {
// 根据关闭模式进行不同的处理
// Handle differently according to the close mode
switch mode {
// ASyncClose 表示异步关闭
// ASyncClose indicates asynchronous close
case ASyncClose:
// 创建一个 WaitGroup,用于等待所有的 TerminateSignal 关闭
// Create a WaitGroup to wait for all TerminateSignal to close
wg := sync.WaitGroup{}
// 添加等待的数量
// Add the number of waits
wg.Add(len(sigs))
// 对每一个 TerminateSignal,启动一个 goroutine 进行关闭操作
// For each TerminateSignal, start a goroutine to perform the close operation
for _, ts := range sigs {
go ts.Close(&wg)
}
// 等待所有的 TerminateSignal 都关闭
// Wait for all TerminateSignal to close
wg.Wait()
// SyncClose 表示同步关闭
// SyncClose indicates synchronous close
case SyncClose:
// 对每一个 TerminateSignal,同步进行关闭操作
// For each TerminateSignal, perform the close operation synchronously
for _, ts := range sigs {
ts.Close(nil)
}
// ForceSyncClose 表示强制同步关闭
// ForceSyncClose indicates forced synchronous close
case ForceSyncClose:
// 对每一个 TerminateSignal,强制同步进行关闭操作
// For each TerminateSignal, forcibly perform the close operation synchronously
for _, ts := range sigs {
ts.SyncClose(nil)
}
// 默认行为
// Default behavior
default:
// 默认情况下,不进行任何操作
// By default, do nothing
}
}
}
在 TerminateSignal
中,我们提供了 Close
和 SyncClose
方法,分别用于异步和同步关闭。Close
方法调用 close
函数,传入 ASyncClose
作为关闭模式和 wg
作为等待组。同样,SyncClose
方法调用 close
函数,但传入 SyncClose
作为关闭模式和 wg
作为等待组。
terminal.go
// close 关闭 TerminateSignal 实例
// close the TerminateSignal instance
func (s *TerminateSignal) close(closeMode CloseType, wg *sync.WaitGroup) {
// 使用 sync.Once 确保 Close 只被执行一次
// Use sync.Once to ensure Close is only executed once
s.once.Do(func() {
// 将 closed 的值设置为 true,表示 TerminateSignal 已经关闭
// Set the value of closed to true, indicating that the TerminateSignal is closed
s.closed.Store(true)
// 遍历所有的回调函数
// Iterate over all callback functions
for _, fn := range s.handles {
// 如果回调函数不为空
// If the callback function is not null
if fn != nil {
// 增加等待组的计数,表示有一个新的任务需要等待完成
// Increase the count of the wait group, indicating that there is a new task to wait for completion
s.wg.Add(1)
// 根据关闭模式进行不同的处理
// Handle differently according to the close mode
switch closeMode {
// ASyncClose 表示异步关闭
// ASyncClose indicates asynchronous close
case ASyncClose:
// 在新的 goroutine 中执行 worker 函数,这样可以并发执行多个任务
// Execute the worker function in a new goroutine, so that multiple tasks can be executed concurrently
go s.worker(fn)
// SyncClose 表示同步关闭
// SyncClose indicates synchronous close
case SyncClose:
// 在当前 goroutine 中执行 worker 函数,这样可以保证任务按顺序执行
// Execute the worker function in the current goroutine, so that tasks can be executed in order
s.worker(fn)
}
}
}
// 取消 context
// Cancel the context
s.cancel()
// 等待所有的 worker 完成
// Wait for all workers to complete
s.wg.Wait()
// 如果外部的等待组不为空,调用 Done 方法
// If the external wait group is not null, call the Done method
if wg != nil {
wg.Done()
}
})
}
// Close 方法异步关闭 TerminateSignal 实例
// The Close method asynchronously closes the TerminateSignal instance
func (s *TerminateSignal) Close(wg *sync.WaitGroup) {
// 调用 close 方法,传入 ASyncClose 作为关闭模式和 wg 作为等待组
// Call the close method, passing in ASyncClose as the close mode and wg as the wait group
s.close(ASyncClose, wg)
}
// SyncClose 方法同步关闭 TerminateSignal 实例
// The SyncClose
method synchronously closes the TerminateSignal instance
func (s *TerminateSignal) SyncClose(wg *sync.WaitGroup) {
// 调用 close 方法,传入 SyncClose 作为关闭模式和 wg 作为等待组
// Call the close method, passing in SyncClose as the close mode and wg as the wait group
s.close(SyncClose, wg)
}
通过这次的代码改造,我们已经让 GS
项目变得更加完善,提供了更多的关闭控制逻辑。当然,如果你对 TerminateSignal
信号的控制方式有不同的想法,你可以实现相关的 WaitXXX
函数,来实现你自己的关闭逻辑。
项目介绍
GS
是一个提供简单易用的优雅关闭 Go
语言库。它可以帮助开发者在关闭服务时优雅地关闭服务内部的模块。通过 GS
项目,我们可以更好地控制服务内部模块的关闭顺序。
快速上手
GS
是一个简单易用的优雅关闭库。使用它的步骤如下:
- 创建一个
TerminateSignal
实例。 - 注册需要在服务终止时关闭的资源。
- 使用适当的等待方法(
WaitForAsync
、WaitForSync
或WaitForForceSync
)等待TerminateSignal
实例优雅关闭。
接口介绍
创建实例
NewTerminateSignal
:创建一个新的TerminateSignal
实例。NewTerminateSignalWithContext
:创建一个带有上下文的新的TerminateSignal
实例。
终结信号
RegisterCancelHandles
:注册需要在服务终止时关闭的资源。GetStopContext
:获取TerminateSignal
实例的上下文。Close
:异步关闭TerminateSignal
实例。SyncClose
:同步关闭TerminateSignal
实例。
等待
WaitForAsync
:异步等待TerminateSignal
实例优雅关闭。WaitForSync
:同步等待TerminateSignal
实例优雅关闭。WaitForForceSync
:严格同步等待TerminateSignal
实例优雅关闭。
总结
在这次的经历中,我发现许多研发小伙伴在关闭服务时,往往存在忽视或者偷懒的情况,这是非常危险的。经过深入的思考和总结,我认为如果研发小伙伴在关闭服务时能够考虑到业务服务模块的关闭顺序,这个问题就可以得到有效的解决。然而,现实情况是,许多研发小伙伴并未考虑到这个问题。即使有些人有意识到这个问题,但由于业务模块的复杂性,很难保证每次都能正确地关闭服务内部的模块。
通过这次经历,我计划对 GS
项目进行优化和调整,以提供更多的关闭控制逻辑,帮助研发小伙伴更有效地关闭服务内部的模块。同时,我也希望通过这个项目,能够帮助更多的研发小伙伴解决这个问题。
如果你对这个项目感兴趣,欢迎访问我们的仓库,并提出你的建议和意见,我会尽快进行跟进。