歡迎追蹤 appleboy/graceful 套件
Go 語言撰寫的服務如何優雅的重新啟動,避免工作執行到一半就被關閉,是一個很中要的議題。故實作了簡易 Graceful Shutdown 套件,讓服務都可以支援此功能,如果不知道什麼是 Graceful Shutdown 的朋友們,可以參考這篇『 [Go 教學] 什麼是 graceful shutdown?』,本篇跟大家介紹一個好用的套件『appleboy/graceful』,使用後。不用再擔心背景的服務沒完成就被關閉,不只是背景的工作需要處理,在關閉服務前,開發者也要確保部分工作要在關閉服務前才執行,像是關閉 Database 及 Redis 連線。
關閉背景執行工作
第一個範例就是當服務啟動了很多背景服務,該如何被正常通知,並且關閉,這邊其實就是用了 Context 來通知所有背景工作,底下實際範例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
| package main
import (
"context"
"log"
"time"
"github.com/appleboy/graceful"
)
func main() {
m := graceful.NewManager()
// Add job 01
m.AddRunningJob(func(ctx context.Context) error {
for {
select {
case <-ctx.Done():
return nil
default:
log.Println("working job 01")
time.Sleep(1 * time.Second)
}
}
})
// Add job 02
m.AddRunningJob(func(ctx context.Context) error {
for {
select {
case <-ctx.Done():
return nil
default:
log.Println("working job 02")
time.Sleep(500 * time.Millisecond)
}
}
})
<-m.Done()
}
|
大家可以看到 AddRunningJob
就是讓開發者把工作丟到背景處理,而其中的 ctx
參數就是在關閉服務時,會立即通知到此函式,重複性執行的工作,就需要透過 ctx.Done()
來確保工作可以正常關閉。如果只是執行單次性工作,就不需要用到。
關閉服務前執行工作
第二個範例,除了背景工作之外,在關閉服務前,一定會有部分工作需要執行,像是關閉 Database 連線等類似的工作性質 (如下圖),這時候可以透過另一個函式來使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
| func main() {
m := graceful.NewManager()
// Add job 01
m.AddRunningJob(func(ctx context.Context) error {
for {
select {
case <-ctx.Done():
return nil
default:
log.Println("working job 01")
time.Sleep(1 * time.Second)
}
}
})
// Add job 02
m.AddRunningJob(func(ctx context.Context) error {
for {
select {
case <-ctx.Done():
return nil
default:
log.Println("working job 02")
time.Sleep(500 * time.Millisecond)
}
}
})
// Add shutdown 01
m.AddShutdownJob(func() error {
log.Println("shutdown job 01 and wait 1 second")
time.Sleep(1 * time.Second)
return nil
})
// Add shutdown 02
m.AddShutdownJob(func() error {
log.Println("shutdown job 02 and wait 2 second")
time.Sleep(2 * time.Second)
return nil
})
<-m.Done()
}
|
透過 AddShutdownJob
函式可以將關閉服務前需要的工作加入,當系統收到通知時,會執行上述工作,執行完畢才會關閉服務。
實作細節
看完上面兩個範例,大家應該都知道如何使用 graceful 套件了,那來聊聊如何實作此套件,其實也沒有很難,透過 os/signal
跟 context
兩個內建的套件就可以完成上面功能。首先用 os/signal
偵測系統訊號。
1
2
3
4
5
6
| signal.Notify(
c,
syscall.SIGINT,
syscall.SIGTERM,
syscall.SIGTSTP,
)
|
接著建立兩個 context,其中一個是 shutdown 另一個是 done context,前者用來通知所有正在執行的工作停止,後者是讓 main 主程式進行最後的確認,確保工作都執行完後,才通知 done context
1
2
3
4
5
6
7
8
9
10
11
| type Manager struct {
lock *sync.RWMutex
shutdownCtx context.Context
shutdownCtxCancel context.CancelFunc
doneCtx context.Context
doneCtxCancel context.CancelFunc
logger Logger
runningWaitGroup *routineGroup
errors []error
runAtShutdown []ShtdownJob
}
|
接著看看執行 graceful shutdown 函式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| // doGracefulShutdown graceful shutdown all task
func (g *Manager) doGracefulShutdown() {
g.shutdownCtxCancel()
// doing shutdown job
for _, f := range g.runAtShutdown {
func(run ShtdownJob) {
g.runningWaitGroup.Run(func() {
g.doShutdownJob(run)
})
}(f)
}
go func() {
g.waitForJobs()
g.lock.Lock()
g.doneCtxCancel()
g.lock.Unlock()
}()
}
|
首先執行 cancel 函式通知正在執行的工作進行關閉,接著執行關閉服務前註冊的工作內容 (doShutdownJob
),透過 waiting group 進行最後的卡控,全部工作執行完畢後,透過 doneCtxCancel
來通知主程式 (main.go
) 結束。
心得
會自行開發此套件最主要原因是大部分服務都需要此功能,進而把會共用的功能抽出來在寫成套件,方便開發者進行實作,。如果喜歡此套件的話,歡迎追蹤 appleboy/graceful。
See also